@vvedantb/ui 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/speech-input.tsx +5 -3
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- package/tokens/src/css/markdown.css +318 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dayjs from "../utils/dayjs";
|
|
4
|
+
import { DndContext, useDraggable } from "@dnd-kit/core";
|
|
5
|
+
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
|
6
|
+
import type { FC, ReactNode } from "react";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { useCn } from "../ui/ui-config";
|
|
9
|
+
import {
|
|
10
|
+
CONTROL_RADIUS_CLASS,
|
|
11
|
+
SURFACE_RADIUS_CLASS,
|
|
12
|
+
} from "../utils/surface-radius";
|
|
13
|
+
import {
|
|
14
|
+
useGanttContext,
|
|
15
|
+
useGanttClasses,
|
|
16
|
+
getAddRange,
|
|
17
|
+
getOffset,
|
|
18
|
+
getWidth,
|
|
19
|
+
type GanttFeature,
|
|
20
|
+
} from "./gantt-provider";
|
|
21
|
+
import { useDragSensors } from "../utils/useDragSensors";
|
|
22
|
+
|
|
23
|
+
export type GanttFeatureDragHelperProps = {
|
|
24
|
+
featureId: GanttFeature["id"];
|
|
25
|
+
direction: "left" | "right";
|
|
26
|
+
date: Date | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const GanttFeatureDragHelper: FC<GanttFeatureDragHelperProps> = ({
|
|
30
|
+
direction,
|
|
31
|
+
featureId,
|
|
32
|
+
date,
|
|
33
|
+
}) => {
|
|
34
|
+
const gantt = useGanttContext();
|
|
35
|
+
const classes = useGanttClasses();
|
|
36
|
+
const merge = useCn();
|
|
37
|
+
const { attributes, listeners, setNodeRef } = useDraggable({
|
|
38
|
+
id: `feature-drag-helper-${featureId}`,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const isPressed = Boolean(attributes["aria-pressed"]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => gantt.setDragging(isPressed), [isPressed, gantt]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={merge(
|
|
48
|
+
"group -translate-y-1/2 cursor-col-resize absolute top-1/2 z-3 h-full w-6 rounded-md outline-hidden",
|
|
49
|
+
direction === "left" ? "-left-2.5" : "-right-2.5",
|
|
50
|
+
)}
|
|
51
|
+
data-gantt-no-pan
|
|
52
|
+
ref={setNodeRef}
|
|
53
|
+
{...attributes}
|
|
54
|
+
{...listeners}
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
className={merge(
|
|
58
|
+
// The handle slides out on hover via translate, not `left`/`right`:
|
|
59
|
+
// an inset animation relayouts every frame, a transform does not.
|
|
60
|
+
// Only opacity and the slide transition, so this stays on the
|
|
61
|
+
// compositor while the user is mid-drag.
|
|
62
|
+
//
|
|
63
|
+
// The named property is `translate`, not `transform`: the slide is
|
|
64
|
+
// `translate-x-*`, which Tailwind compiles to `translate: …`, and an
|
|
65
|
+
// arbitrary `transition-[…]` matches property names literally — so
|
|
66
|
+
// the handle used to appear at its final offset in one frame.
|
|
67
|
+
classes.dragHandle,
|
|
68
|
+
direction === "left" ? "left-0" : "right-0",
|
|
69
|
+
direction === "left" ? "translate-x-2.5" : "-translate-x-2.5",
|
|
70
|
+
"group-hover:translate-x-0",
|
|
71
|
+
isPressed && "translate-x-0",
|
|
72
|
+
"group-hover:opacity-100",
|
|
73
|
+
isPressed && "opacity-100",
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
{date && (
|
|
77
|
+
<div
|
|
78
|
+
className={merge(
|
|
79
|
+
"-translate-x-1/2 absolute top-10 hidden whitespace-nowrap bg-background/90 px-2 py-1 text-foreground text-xs backdrop-blur-lg group-hover:block",
|
|
80
|
+
SURFACE_RADIUS_CLASS,
|
|
81
|
+
isPressed && "block",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{dayjs(date).format("MMM DD, YYYY")}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type GanttFeatureItemCardProps = Pick<GanttFeature, "id"> & {
|
|
92
|
+
children?: ReactNode;
|
|
93
|
+
className?: string;
|
|
94
|
+
onClick?: () => void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const GanttFeatureItemCard: FC<GanttFeatureItemCardProps> = ({
|
|
98
|
+
id,
|
|
99
|
+
children,
|
|
100
|
+
className,
|
|
101
|
+
onClick,
|
|
102
|
+
}) => {
|
|
103
|
+
const gantt = useGanttContext();
|
|
104
|
+
const merge = useCn();
|
|
105
|
+
const { attributes, listeners, setNodeRef } = useDraggable({ id });
|
|
106
|
+
const isPressed = Boolean(attributes["aria-pressed"]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => gantt.setDragging(isPressed), [isPressed, gantt]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
data-gantt-no-pan
|
|
113
|
+
className={merge(
|
|
114
|
+
"relative h-full w-full overflow-hidden border border-border/70 text-xs",
|
|
115
|
+
CONTROL_RADIUS_CLASS,
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<div
|
|
120
|
+
className={merge(
|
|
121
|
+
"h-full w-full",
|
|
122
|
+
isPressed ? "cursor-grabbing" : "cursor-grab",
|
|
123
|
+
)}
|
|
124
|
+
{...attributes}
|
|
125
|
+
{...listeners}
|
|
126
|
+
onClick={onClick}
|
|
127
|
+
ref={setNodeRef}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type GanttFeatureItemProps = GanttFeature & {
|
|
136
|
+
onMove?: (id: string, startDate: Date, endDate: Date | null) => void;
|
|
137
|
+
onClick?: () => void;
|
|
138
|
+
children?: ReactNode;
|
|
139
|
+
className?: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const GanttFeatureItem: FC<GanttFeatureItemProps> = ({
|
|
143
|
+
onMove,
|
|
144
|
+
onClick,
|
|
145
|
+
children,
|
|
146
|
+
className,
|
|
147
|
+
...feature
|
|
148
|
+
}) => {
|
|
149
|
+
const gantt = useGanttContext();
|
|
150
|
+
const classes = useGanttClasses();
|
|
151
|
+
const merge = useCn();
|
|
152
|
+
const timelineStartDate = useMemo(() => {
|
|
153
|
+
const firstYear = gantt.timelineData[0]?.year ?? new Date().getFullYear();
|
|
154
|
+
return new Date(firstYear, 0, 1);
|
|
155
|
+
}, [gantt.timelineData]);
|
|
156
|
+
|
|
157
|
+
const [startAt, setStartAt] = useState<Date>(feature.startAt);
|
|
158
|
+
const [endAt, setEndAt] = useState<Date | null>(feature.endAt);
|
|
159
|
+
|
|
160
|
+
const width = useMemo(
|
|
161
|
+
() => getWidth(startAt, endAt, gantt),
|
|
162
|
+
[startAt, endAt, gantt],
|
|
163
|
+
);
|
|
164
|
+
const offset = useMemo(
|
|
165
|
+
() => getOffset(startAt, timelineStartDate, gantt),
|
|
166
|
+
[startAt, timelineStartDate, gantt],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const addRange = useMemo(() => getAddRange(gantt.range), [gantt.range]);
|
|
170
|
+
|
|
171
|
+
// Pixels per day for the active range, so drag deltas map to dates correctly
|
|
172
|
+
// in every zoom (daily columns are per-day; monthly/quarterly are per-month).
|
|
173
|
+
const pxPerDay = useMemo(() => {
|
|
174
|
+
const colW = (gantt.columnWidth * gantt.zoom) / 100;
|
|
175
|
+
return gantt.range === "daily" ? colW : colW / 30;
|
|
176
|
+
}, [gantt.columnWidth, gantt.zoom, gantt.range]);
|
|
177
|
+
|
|
178
|
+
const [, setPreviousMouseX] = useState(0);
|
|
179
|
+
const [previousStartAt, setPreviousStartAt] = useState(startAt);
|
|
180
|
+
const [previousEndAt, setPreviousEndAt] = useState(endAt);
|
|
181
|
+
|
|
182
|
+
// A `MouseSensor` alone meant a gantt bar could not be moved or resized by
|
|
183
|
+
// touch or pen at all — the three `DndContext`s below never armed off a
|
|
184
|
+
// finger.
|
|
185
|
+
const sensors = useDragSensors();
|
|
186
|
+
|
|
187
|
+
const handleItemDragStart = useCallback(() => {
|
|
188
|
+
const mouseX = gantt.scrollX;
|
|
189
|
+
setPreviousMouseX(mouseX);
|
|
190
|
+
setPreviousStartAt(startAt);
|
|
191
|
+
setPreviousEndAt(endAt);
|
|
192
|
+
}, [gantt.scrollX, gantt.ref, startAt, endAt]);
|
|
193
|
+
|
|
194
|
+
const handleItemDragMove = useCallback(
|
|
195
|
+
(event: { delta: { x: number } }) => {
|
|
196
|
+
const daysDelta = Math.round(event.delta.x / pxPerDay);
|
|
197
|
+
const newStartDate = dayjs(previousStartAt)
|
|
198
|
+
.add(daysDelta, "day")
|
|
199
|
+
.toDate();
|
|
200
|
+
const newEndDate = previousEndAt
|
|
201
|
+
? dayjs(previousEndAt).add(daysDelta, "day").toDate()
|
|
202
|
+
: null;
|
|
203
|
+
|
|
204
|
+
setStartAt(newStartDate);
|
|
205
|
+
setEndAt(newEndDate);
|
|
206
|
+
},
|
|
207
|
+
[pxPerDay, previousStartAt, previousEndAt],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const onDragEnd = useCallback(
|
|
211
|
+
() => onMove?.(feature.id, startAt, endAt),
|
|
212
|
+
[onMove, feature.id, startAt, endAt],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const handleLeftDragMove = useCallback(
|
|
216
|
+
(event: { delta: { x: number } }) => {
|
|
217
|
+
const daysDelta = Math.round(event.delta.x / pxPerDay);
|
|
218
|
+
const newStartAt = dayjs(feature.startAt).add(daysDelta, "day").toDate();
|
|
219
|
+
setStartAt(newStartAt);
|
|
220
|
+
},
|
|
221
|
+
[pxPerDay, feature.startAt],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const handleRightDragMove = useCallback(
|
|
225
|
+
(event: { delta: { x: number } }) => {
|
|
226
|
+
const daysDelta = Math.round(event.delta.x / pxPerDay);
|
|
227
|
+
const newEndAt = dayjs(feature.endAt).add(daysDelta, "day").toDate();
|
|
228
|
+
setEndAt(newEndAt);
|
|
229
|
+
},
|
|
230
|
+
[pxPerDay, feature.endAt],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// When the bar sits outside the visible canvas, show a clickable date chip
|
|
234
|
+
// pinned to the viewport edge that scrolls back to it (Linear's "← date").
|
|
235
|
+
const viewportWidth = gantt.ref?.current?.clientWidth ?? 0;
|
|
236
|
+
const visibleEnd =
|
|
237
|
+
gantt.scrollX + Math.max(0, viewportWidth - gantt.sidebarWidth);
|
|
238
|
+
const roundedOffset = Math.round(offset);
|
|
239
|
+
const roundedWidth = Math.round(width);
|
|
240
|
+
const offscreenLeft =
|
|
241
|
+
viewportWidth > 0 && roundedOffset + roundedWidth < gantt.scrollX;
|
|
242
|
+
const offscreenRight = viewportWidth > 0 && roundedOffset > visibleEnd;
|
|
243
|
+
const dateLabel = endAt
|
|
244
|
+
? `${dayjs(startAt).format("MMM D")} – ${dayjs(endAt).format("MMM D")}`
|
|
245
|
+
: dayjs(startAt).format("MMM D");
|
|
246
|
+
const indicatorClass = classes.offscreenIndicator;
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div
|
|
250
|
+
className={merge("relative flex w-max min-w-full py-0.5", className)}
|
|
251
|
+
style={{ height: "var(--gantt-row-height)" }}
|
|
252
|
+
>
|
|
253
|
+
<div
|
|
254
|
+
className="pointer-events-auto absolute top-0.5"
|
|
255
|
+
style={{
|
|
256
|
+
height: "calc(var(--gantt-row-height) - 4px)",
|
|
257
|
+
width: Math.round(width),
|
|
258
|
+
left: Math.round(offset),
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{onMove && (
|
|
262
|
+
<DndContext
|
|
263
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
264
|
+
onDragEnd={onDragEnd}
|
|
265
|
+
onDragMove={handleLeftDragMove}
|
|
266
|
+
sensors={sensors}
|
|
267
|
+
>
|
|
268
|
+
<GanttFeatureDragHelper
|
|
269
|
+
date={startAt}
|
|
270
|
+
direction="left"
|
|
271
|
+
featureId={feature.id}
|
|
272
|
+
/>
|
|
273
|
+
</DndContext>
|
|
274
|
+
)}
|
|
275
|
+
<DndContext
|
|
276
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
277
|
+
onDragEnd={onDragEnd}
|
|
278
|
+
onDragMove={handleItemDragMove}
|
|
279
|
+
onDragStart={handleItemDragStart}
|
|
280
|
+
sensors={sensors}
|
|
281
|
+
>
|
|
282
|
+
<GanttFeatureItemCard id={feature.id} onClick={onClick}>
|
|
283
|
+
{children ?? (
|
|
284
|
+
<div className="flex h-full w-full items-center bg-surface-secondary/40 px-2">
|
|
285
|
+
<p className="flex-1 truncate text-xs">{feature.name}</p>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</GanttFeatureItemCard>
|
|
289
|
+
</DndContext>
|
|
290
|
+
{onMove && (
|
|
291
|
+
<DndContext
|
|
292
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
293
|
+
onDragEnd={onDragEnd}
|
|
294
|
+
onDragMove={handleRightDragMove}
|
|
295
|
+
sensors={sensors}
|
|
296
|
+
>
|
|
297
|
+
<GanttFeatureDragHelper
|
|
298
|
+
date={endAt ?? addRange(startAt, 2)}
|
|
299
|
+
direction="right"
|
|
300
|
+
featureId={feature.id}
|
|
301
|
+
/>
|
|
302
|
+
</DndContext>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
{offscreenLeft && (
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
data-gantt-no-pan
|
|
309
|
+
onClick={() => gantt.scrollToFeature?.(feature)}
|
|
310
|
+
className={indicatorClass}
|
|
311
|
+
style={{ left: gantt.scrollX + 8 }}
|
|
312
|
+
>
|
|
313
|
+
<span aria-hidden>←</span>
|
|
314
|
+
{dateLabel}
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
{offscreenRight && (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
data-gantt-no-pan
|
|
321
|
+
onClick={() => gantt.scrollToFeature?.(feature)}
|
|
322
|
+
className={merge(indicatorClass, "-translate-x-full")}
|
|
323
|
+
style={{
|
|
324
|
+
left:
|
|
325
|
+
gantt.scrollX +
|
|
326
|
+
Math.max(0, viewportWidth - gantt.sidebarWidth) -
|
|
327
|
+
8,
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
{dateLabel}
|
|
331
|
+
<span aria-hidden>→</span>
|
|
332
|
+
</button>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export type GanttFeatureListGroupProps = {
|
|
339
|
+
children: ReactNode;
|
|
340
|
+
className?: string;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const GanttFeatureListGroup: FC<GanttFeatureListGroupProps> = ({
|
|
344
|
+
children,
|
|
345
|
+
className,
|
|
346
|
+
}) => (
|
|
347
|
+
<div className={className} style={{ paddingTop: "var(--gantt-row-height)" }}>
|
|
348
|
+
{children}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
export type GanttFeatureRowProps = {
|
|
353
|
+
features: GanttFeature[];
|
|
354
|
+
onMove?: (id: string, startAt: Date, endAt: Date | null) => void;
|
|
355
|
+
children?: (feature: GanttFeature) => ReactNode;
|
|
356
|
+
className?: string;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export const GanttFeatureRow: FC<GanttFeatureRowProps> = ({
|
|
360
|
+
features,
|
|
361
|
+
onMove,
|
|
362
|
+
children,
|
|
363
|
+
className,
|
|
364
|
+
}) => {
|
|
365
|
+
const sortedFeatures = [...features].sort(
|
|
366
|
+
(a, b) => a.startAt.getTime() - b.startAt.getTime(),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const featureWithPositions: (GanttFeature & { subRow: number })[] = [];
|
|
370
|
+
const subRowEndTimes: Date[] = [];
|
|
371
|
+
|
|
372
|
+
for (const feature of sortedFeatures) {
|
|
373
|
+
let subRow = 0;
|
|
374
|
+
while ((subRowEndTimes[subRow] ?? feature.startAt) > feature.startAt) {
|
|
375
|
+
subRow++;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (subRow === subRowEndTimes.length) {
|
|
379
|
+
subRowEndTimes.push(feature.endAt);
|
|
380
|
+
} else {
|
|
381
|
+
subRowEndTimes[subRow] = feature.endAt;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
featureWithPositions.push({ ...feature, subRow });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const merge = useCn();
|
|
388
|
+
const maxSubRows = Math.max(1, subRowEndTimes.length);
|
|
389
|
+
const subRowHeight = 36;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
className={merge("relative", className)}
|
|
394
|
+
style={{
|
|
395
|
+
height: `${maxSubRows * subRowHeight}px`,
|
|
396
|
+
minHeight: "var(--gantt-row-height)",
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
{featureWithPositions.map((feature) => (
|
|
400
|
+
<div
|
|
401
|
+
className="absolute w-full"
|
|
402
|
+
key={feature.id}
|
|
403
|
+
style={{
|
|
404
|
+
top: `${feature.subRow * subRowHeight}px`,
|
|
405
|
+
height: `${subRowHeight}px`,
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
<GanttFeatureItem {...feature} onMove={onMove}>
|
|
409
|
+
{children ? (
|
|
410
|
+
children(feature)
|
|
411
|
+
) : (
|
|
412
|
+
<p className="flex-1 truncate text-xs">{feature.name}</p>
|
|
413
|
+
)}
|
|
414
|
+
</GanttFeatureItem>
|
|
415
|
+
</div>
|
|
416
|
+
))}
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export type GanttFeatureListProps = {
|
|
422
|
+
className?: string;
|
|
423
|
+
children: ReactNode;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
export const GanttFeatureList: FC<GanttFeatureListProps> = ({
|
|
427
|
+
className,
|
|
428
|
+
children,
|
|
429
|
+
}) => {
|
|
430
|
+
const merge = useCn();
|
|
431
|
+
return (
|
|
432
|
+
<div
|
|
433
|
+
className={merge("absolute top-0 left-0 h-full w-max", className)}
|
|
434
|
+
style={{ marginTop: "var(--gantt-header-height)" }}
|
|
435
|
+
>
|
|
436
|
+
{children}
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dayjs from "../utils/dayjs";
|
|
4
|
+
import type { FC, ReactNode } from "react";
|
|
5
|
+
import { useId } from "react";
|
|
6
|
+
import { useCn } from "../ui/ui-config";
|
|
7
|
+
import { useGanttContext, type GanttRange } from "./gantt-provider";
|
|
8
|
+
import { GanttColumns } from "./gantt-timeline";
|
|
9
|
+
|
|
10
|
+
export type GanttContentHeaderProps = {
|
|
11
|
+
renderHeaderItem: (index: number) => ReactNode;
|
|
12
|
+
title: string;
|
|
13
|
+
columns: number;
|
|
14
|
+
/** Marks the column matching "now" so it can be visually emphasised. */
|
|
15
|
+
isColumnCurrent?: (index: number) => boolean;
|
|
16
|
+
/** When provided, only flagged columns get a left divider (e.g. week starts);
|
|
17
|
+
* otherwise every column gets a right divider. */
|
|
18
|
+
isColumnDivider?: (index: number) => boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const GanttContentHeader: FC<GanttContentHeaderProps> = ({
|
|
22
|
+
title,
|
|
23
|
+
columns,
|
|
24
|
+
renderHeaderItem,
|
|
25
|
+
isColumnCurrent,
|
|
26
|
+
isColumnDivider,
|
|
27
|
+
}) => {
|
|
28
|
+
const id = useId();
|
|
29
|
+
const merge = useCn();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="sticky top-0 z-20 grid w-full shrink-0 border-b border-border bg-background/95 backdrop-blur-xs"
|
|
34
|
+
style={{ height: "var(--gantt-header-height)" }}
|
|
35
|
+
>
|
|
36
|
+
<div>
|
|
37
|
+
<div
|
|
38
|
+
className="sticky inline-flex whitespace-nowrap px-3 py-2 font-medium text-foreground/70 text-xs"
|
|
39
|
+
style={{ left: "var(--gantt-sidebar-width)" }}
|
|
40
|
+
>
|
|
41
|
+
<p>{title}</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
className="grid w-full"
|
|
46
|
+
style={{
|
|
47
|
+
gridTemplateColumns: `repeat(${columns}, var(--gantt-column-width))`,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{Array.from({ length: columns }).map((_, index) => (
|
|
51
|
+
<div
|
|
52
|
+
className={merge(
|
|
53
|
+
"shrink-0 py-1 text-center text-xs text-muted-foreground",
|
|
54
|
+
isColumnDivider
|
|
55
|
+
? isColumnDivider(index)
|
|
56
|
+
? "border-l border-border/60"
|
|
57
|
+
: ""
|
|
58
|
+
: "border-r border-border/50",
|
|
59
|
+
isColumnCurrent?.(index) &&
|
|
60
|
+
"bg-primary/5 font-medium text-foreground",
|
|
61
|
+
)}
|
|
62
|
+
key={`${id}-${index}`}
|
|
63
|
+
>
|
|
64
|
+
{renderHeaderItem(index)}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const DailyHeader: FC = () => {
|
|
73
|
+
const gantt = useGanttContext();
|
|
74
|
+
const now = dayjs();
|
|
75
|
+
|
|
76
|
+
return gantt.timelineData.map((year) =>
|
|
77
|
+
year.quarters
|
|
78
|
+
.flatMap((quarter) => quarter.months)
|
|
79
|
+
.map((month, index) => {
|
|
80
|
+
const monthStart = new Date(year.year, index, 1);
|
|
81
|
+
// Linear's grid is weekly: label + divide only at week starts (Sundays).
|
|
82
|
+
const isWeekStart = (item: number) =>
|
|
83
|
+
dayjs(monthStart).add(item, "day").day() === 0;
|
|
84
|
+
return (
|
|
85
|
+
<div className="relative flex flex-col" key={`${year.year}-${index}`}>
|
|
86
|
+
<GanttContentHeader
|
|
87
|
+
columns={month.days}
|
|
88
|
+
isColumnDivider={isWeekStart}
|
|
89
|
+
isColumnCurrent={(item) =>
|
|
90
|
+
now.year() === year.year &&
|
|
91
|
+
now.month() === index &&
|
|
92
|
+
now.date() === item + 1
|
|
93
|
+
}
|
|
94
|
+
renderHeaderItem={(item: number) =>
|
|
95
|
+
isWeekStart(item) ? (
|
|
96
|
+
<span>{dayjs(monthStart).add(item, "day").format("D")}</span>
|
|
97
|
+
) : null
|
|
98
|
+
}
|
|
99
|
+
title={dayjs(monthStart).format("MMMM YYYY")}
|
|
100
|
+
/>
|
|
101
|
+
<GanttColumns columns={month.days} isColumnDivider={isWeekStart} />
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const MonthlyHeader: FC = () => {
|
|
109
|
+
const gantt = useGanttContext();
|
|
110
|
+
const now = dayjs();
|
|
111
|
+
|
|
112
|
+
return gantt.timelineData.map((year) => (
|
|
113
|
+
<div className="relative flex flex-col" key={year.year}>
|
|
114
|
+
<GanttContentHeader
|
|
115
|
+
columns={year.quarters.flatMap((quarter) => quarter.months).length}
|
|
116
|
+
isColumnCurrent={(item) =>
|
|
117
|
+
now.year() === year.year && now.month() === item
|
|
118
|
+
}
|
|
119
|
+
renderHeaderItem={(item: number) => (
|
|
120
|
+
<p>{dayjs(new Date(year.year, item, 1)).format("MMM")}</p>
|
|
121
|
+
)}
|
|
122
|
+
title={`${year.year}`}
|
|
123
|
+
/>
|
|
124
|
+
<GanttColumns
|
|
125
|
+
columns={year.quarters.flatMap((quarter) => quarter.months).length}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const QuarterlyHeader: FC = () => {
|
|
132
|
+
const gantt = useGanttContext();
|
|
133
|
+
const now = dayjs();
|
|
134
|
+
|
|
135
|
+
return gantt.timelineData.map((year) =>
|
|
136
|
+
year.quarters.map((quarter, quarterIndex) => (
|
|
137
|
+
<div
|
|
138
|
+
className="relative flex flex-col"
|
|
139
|
+
key={`${year.year}-${quarterIndex}`}
|
|
140
|
+
>
|
|
141
|
+
<GanttContentHeader
|
|
142
|
+
columns={quarter.months.length}
|
|
143
|
+
isColumnCurrent={(item) =>
|
|
144
|
+
now.year() === year.year && now.month() === quarterIndex * 3 + item
|
|
145
|
+
}
|
|
146
|
+
renderHeaderItem={(item: number) => (
|
|
147
|
+
<p>
|
|
148
|
+
{dayjs(new Date(year.year, quarterIndex * 3 + item, 1)).format(
|
|
149
|
+
"MMM",
|
|
150
|
+
)}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
title={`Q${quarterIndex + 1} ${year.year}`}
|
|
154
|
+
/>
|
|
155
|
+
<GanttColumns columns={quarter.months.length} />
|
|
156
|
+
</div>
|
|
157
|
+
)),
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const headers: Record<GanttRange, FC> = {
|
|
162
|
+
daily: DailyHeader,
|
|
163
|
+
monthly: MonthlyHeader,
|
|
164
|
+
quarterly: QuarterlyHeader,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type GanttHeaderProps = {
|
|
168
|
+
className?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const GanttHeader: FC<GanttHeaderProps> = ({ className }) => {
|
|
172
|
+
const gantt = useGanttContext();
|
|
173
|
+
const merge = useCn();
|
|
174
|
+
const Header = headers[gantt.range];
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className={merge("-space-x-px flex h-full w-max", className)}>
|
|
178
|
+
<Header />
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|