timeline ui
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.squircle {
|
||||
border-radius: 12px;
|
||||
-corner-smoothing: antialiased;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user