timeline ui

This commit is contained in:
Siddharth
2025-11-16 00:10:49 -07:00
parent 41572298d6
commit 6287fa90c8
5 changed files with 56 additions and 14 deletions
@@ -0,0 +1,4 @@
.squircle {
border-radius: 12px;
-corner-smoothing: antialiased;
}
+8 -11
View File
@@ -1,6 +1,7 @@
import { useItem } from "dnd-timeline";
import type { Span } from "dnd-timeline";
import { cn } from "@/lib/utils";
import glassStyles from "./ItemGlass.module.css";
interface ItemProps {
id: string;
@@ -11,7 +12,7 @@ interface ItemProps {
onSelect?: () => void;
}
export default function Item({ id, span, rowId, children, isSelected = false, onSelect }: ItemProps) {
export default function Item({ id, span, rowId, isSelected = false, onSelect }: ItemProps) {
const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem({
id,
span,
@@ -28,22 +29,18 @@ export default function Item({ id, span, rowId, children, isSelected = false, on
>
<div style={itemContentStyle}>
<div
className={cn(
"border rounded-lg shadow-sm w-full overflow-hidden flex items-center justify-center px-3 transition-all duration-150 cursor-grab active:cursor-grabbing group relative",
isSelected
? "border-2 border-red-500 bg-indigo-600 shadow-xl"
: "border bg-indigo-500"
)}
className={cn(
"w-full overflow-hidden flex items-center justify-center transition-all duration-150 cursor-grab active:cursor-grabbing group relative",
glassStyles.glassPurple
)}
style={{ height: 60 }}
onClick={(event) => {
event.stopPropagation();
onSelect?.();
}}
>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-150" />
<span className="text-sm font-semibold text-white truncate relative z-10 drop-shadow-sm">
{children}
</span>
<div className={cn(glassStyles.zoomEndCap, glassStyles.left)} />
<div className={cn(glassStyles.zoomEndCap, glassStyles.right)} />
</div>
</div>
</div>
@@ -0,0 +1,32 @@
.glassPurple {
position: relative;
border-radius: 12px;
-corner-smoothing: antialiased;
background: radial-gradient(circle at 60% 55%, rgba(104, 61, 196, 0.92) 60%, rgba(60, 20, 120, 0.85) 100%);
border: none;
box-shadow: 0 2px 8px 0 rgba(88,36,204,0.10) inset, 0 1px 3px 0 rgba(0,0,0,0.10);
backdrop-filter: blur(2px) saturate(120%);
-webkit-backdrop-filter: blur(2px) saturate(120%);
}
.zoomEndCap {
position: absolute;
top: 0;
background: #3c3c3c;
width: 18px;
height: 100%;
pointer-events: none;
z-index: 2;
transition: background 0.2s, box-shadow 0.2s;
}
.zoomEndCap.left {
left: -9px;
cursor: ew-resize;
}
.zoomEndCap.right {
right: -9px;
transform: scaleX(-1);
cursor: ew-resize;
}
@@ -72,7 +72,8 @@ function calculateTimelineScale(durationSeconds: number): TimelineScaleConfig {
const intervalMs = Math.round(selectedCandidate.intervalSeconds * 1000);
const gridMs = Math.round(selectedCandidate.gridSeconds * 1000);
const minItemDurationMs = Math.max(100, Math.min(intervalMs, gridMs * 2));
// Set minItemDurationMs to 1ms for maximum granularity
const minItemDurationMs = 1;
const defaultItemDurationMs = Math.min(
Math.max(minItemDurationMs, intervalMs * 2),
totalMs > 0 ? totalMs : intervalMs * 2,
@@ -337,7 +338,7 @@ export default function TimelineEditor({
zoomRegions,
onZoomAdded,
onZoomSpanChange,
onZoomDelete,
// Removed unused onZoomDelete prop
selectedZoomId,
onSelectZoom,
}: TimelineEditorProps) {
@@ -374,8 +375,14 @@ export default function TimelineEditor({
}, [zoomRegions, totalMs, safeMinDurationMs, onZoomSpanChange]);
const hasOverlap = useCallback((newSpan: Span, excludeId?: string): boolean => {
// Snap if gap is 2ms or less
return zoomRegions.some((region) => {
if (region.id === excludeId) return false;
// If the new span is within 2ms of another region, treat as overlap (snap)
const gapBefore = newSpan.start - region.endMs;
const gapAfter = region.startMs - newSpan.end;
if (gapBefore > 0 && gapBefore <= 2) return true;
if (gapAfter > 0 && gapAfter <= 2) return true;
return !(newSpan.end <= region.startMs || newSpan.start >= region.endMs);
});
}, [zoomRegions]);
@@ -149,6 +149,8 @@ export default function TimelineWrapper({
[clampRange, onRangeChange, totalMs],
);
// To maximize granularity, disable grid snapping by not passing rangeGridSizeDefinition
// and allow pixel-level movement for items.
return (
<TimelineContext
range={range}
@@ -156,7 +158,7 @@ export default function TimelineWrapper({
onResizeEnd={onResizeEnd}
onDragEnd={onDragEnd}
autoScroll={{ enabled: false }}
rangeGridSizeDefinition={gridSizeMs > 0 ? gridSizeMs : undefined}
// Remove rangeGridSizeDefinition to avoid snap effect
>
{children}
</TimelineContext>