@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,338 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
Announcements,
|
|
5
|
+
CollisionDetection,
|
|
6
|
+
DragCancelEvent,
|
|
7
|
+
DragEndEvent,
|
|
8
|
+
DragOverEvent,
|
|
9
|
+
DragStartEvent,
|
|
10
|
+
} from "@dnd-kit/core";
|
|
11
|
+
import {
|
|
12
|
+
closestCenter,
|
|
13
|
+
DndContext,
|
|
14
|
+
DragOverlay,
|
|
15
|
+
useDroppable,
|
|
16
|
+
} from "@dnd-kit/core";
|
|
17
|
+
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
|
18
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
19
|
+
import {
|
|
20
|
+
createContext,
|
|
21
|
+
type HTMLAttributes,
|
|
22
|
+
type ReactNode,
|
|
23
|
+
useContext,
|
|
24
|
+
useState,
|
|
25
|
+
} from "react";
|
|
26
|
+
import { createPortal } from "react-dom";
|
|
27
|
+
import { useCn, useUiConfig } from "../ui/ui-config";
|
|
28
|
+
import { SURFACE_RADIUS_CLASS } from "../utils/surface-radius";
|
|
29
|
+
import { useDragSensors } from "../utils/useDragSensors";
|
|
30
|
+
import { resolveOverColumnId } from "./kanbanDropTarget";
|
|
31
|
+
|
|
32
|
+
export interface KanbanConfig {
|
|
33
|
+
/** Replaces the built-in classes of a part. */
|
|
34
|
+
classes?: {
|
|
35
|
+
/**
|
|
36
|
+
* `KanbanCard`: grab cursor, the transitions it runs itself and press
|
|
37
|
+
* feedback. The radius and the dragging state are added on top.
|
|
38
|
+
*/
|
|
39
|
+
card?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The transitions `KanbanCard` runs itself (press scale and drag fade),
|
|
43
|
+
* appended to the reorder transition dnd-kit writes inline. A CSS
|
|
44
|
+
* `transition` list.
|
|
45
|
+
*/
|
|
46
|
+
cardTransition?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultClasses = {
|
|
50
|
+
card: "cursor-grab transition-[opacity,transform,scale] duration-150 active:scale-[0.98]",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CARD_TRANSITION =
|
|
54
|
+
"scale 150ms cubic-bezier(0.22, 1, 0.36, 1), opacity 150ms cubic-bezier(0.22, 1, 0.36, 1)";
|
|
55
|
+
|
|
56
|
+
export type KanbanItem = {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
column: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type KanbanColumnDef = {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type KanbanContextValue = {
|
|
68
|
+
columns: KanbanColumnDef[];
|
|
69
|
+
data: KanbanItem[];
|
|
70
|
+
activeCardId: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Column the dragged card would land in. Distinct from a column's own
|
|
73
|
+
* `isOver`: pointing at a card resolves `over` to that card, so the column
|
|
74
|
+
* under the pointer never reports `isOver` once it holds any cards.
|
|
75
|
+
*/
|
|
76
|
+
overColumnId: string | null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const KanbanContext = createContext<KanbanContextValue>({
|
|
80
|
+
columns: [],
|
|
81
|
+
data: [],
|
|
82
|
+
activeCardId: null,
|
|
83
|
+
overColumnId: null,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const useKanbanContext = () => useContext(KanbanContext);
|
|
87
|
+
|
|
88
|
+
// --- KanbanBoard: droppable column zone ---
|
|
89
|
+
|
|
90
|
+
export type KanbanBoardProps = {
|
|
91
|
+
id: string;
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
className?: string;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const KanbanBoard = ({
|
|
98
|
+
id,
|
|
99
|
+
children,
|
|
100
|
+
className,
|
|
101
|
+
disabled = false,
|
|
102
|
+
}: KanbanBoardProps) => {
|
|
103
|
+
const { isOver, setNodeRef } = useDroppable({ id, disabled });
|
|
104
|
+
const { overColumnId } = useKanbanContext();
|
|
105
|
+
const merge = useCn();
|
|
106
|
+
const isDropTarget = !disabled && (isOver || overColumnId === id);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={merge(
|
|
111
|
+
"flex size-full min-h-40 flex-col overflow-hidden bg-surface-secondary/40 text-xs transition-[background-color,border-color]",
|
|
112
|
+
SURFACE_RADIUS_CLASS,
|
|
113
|
+
className,
|
|
114
|
+
// After `className` on purpose: callers set a column background, and
|
|
115
|
+
// tailwind-merge keeps whichever `bg-*` / `border-*` comes last — so
|
|
116
|
+
// the drop highlight has to be the last word or it never shows.
|
|
117
|
+
isDropTarget && "border border-primary/40 bg-primary/10",
|
|
118
|
+
)}
|
|
119
|
+
ref={setNodeRef}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// --- KanbanCard: sortable item wrapper ---
|
|
127
|
+
|
|
128
|
+
export type KanbanCardProps = {
|
|
129
|
+
id: string;
|
|
130
|
+
children: ReactNode;
|
|
131
|
+
className?: string;
|
|
132
|
+
onClick?: () => void;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const KanbanCard = ({
|
|
136
|
+
id,
|
|
137
|
+
children,
|
|
138
|
+
className,
|
|
139
|
+
onClick,
|
|
140
|
+
}: KanbanCardProps) => {
|
|
141
|
+
const {
|
|
142
|
+
attributes,
|
|
143
|
+
listeners,
|
|
144
|
+
setNodeRef,
|
|
145
|
+
transition,
|
|
146
|
+
transform,
|
|
147
|
+
isDragging,
|
|
148
|
+
} = useSortable({ id });
|
|
149
|
+
const config = useUiConfig("Kanban");
|
|
150
|
+
const classes = { ...defaultClasses, ...config?.classes };
|
|
151
|
+
const merge = useCn();
|
|
152
|
+
|
|
153
|
+
const style = {
|
|
154
|
+
/**
|
|
155
|
+
* `useSortable` hands back a `transition` string, and putting it in inline
|
|
156
|
+
* style outranks every `transition-*` class on the element — the shorthand
|
|
157
|
+
* resets `transition-property`, so while a sort is animating the class below
|
|
158
|
+
* is not merely overridden on duration, it stops applying at all and the
|
|
159
|
+
* press scale snaps. dnd-kit's own entry only ever names `transform`.
|
|
160
|
+
*
|
|
161
|
+
* Appending the two properties the card animates itself keeps the reflow
|
|
162
|
+
* tween dnd-kit wants and the press and drag-fade the card wants. The value
|
|
163
|
+
* is `undefined` when nothing is sorting, which is why it is filtered rather
|
|
164
|
+
* than interpolated.
|
|
165
|
+
*/
|
|
166
|
+
transition: [transition, config?.cardTransition ?? DEFAULT_CARD_TRANSITION]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(", "),
|
|
169
|
+
transform: CSS.Transform.toString(transform),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
style={style}
|
|
175
|
+
{...listeners}
|
|
176
|
+
{...attributes}
|
|
177
|
+
ref={setNodeRef}
|
|
178
|
+
onClick={onClick}
|
|
179
|
+
className={merge(
|
|
180
|
+
// Press feedback on pointer-down, not on release — the card has to
|
|
181
|
+
// acknowledge the grab before the drag threshold is crossed, or the
|
|
182
|
+
// first few pixels of every drag read as a dead control.
|
|
183
|
+
//
|
|
184
|
+
// `scale` is named explicitly alongside `transform`: an arbitrary
|
|
185
|
+
// `transition-[…]` emits exactly the properties listed, and Tailwind
|
|
186
|
+
// compiles `scale-[0.98]` to the individual `scale` property, which
|
|
187
|
+
// `transform` does not match — so this press was a no-op. `transform`
|
|
188
|
+
// stays for the inline one `useSortable` writes below.
|
|
189
|
+
classes.card,
|
|
190
|
+
SURFACE_RADIUS_CLASS,
|
|
191
|
+
isDragging && "pointer-events-none cursor-grabbing opacity-30",
|
|
192
|
+
className,
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// --- KanbanCards: SortableContext + auto-filter by column from context ---
|
|
201
|
+
|
|
202
|
+
export type KanbanCardsProps = Omit<
|
|
203
|
+
HTMLAttributes<HTMLDivElement>,
|
|
204
|
+
"children" | "id"
|
|
205
|
+
> & {
|
|
206
|
+
children: (item: KanbanItem) => ReactNode;
|
|
207
|
+
id: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const KanbanCards = ({
|
|
211
|
+
children,
|
|
212
|
+
className,
|
|
213
|
+
id,
|
|
214
|
+
...props
|
|
215
|
+
}: KanbanCardsProps) => {
|
|
216
|
+
const { data } = useKanbanContext();
|
|
217
|
+
const filteredData = data.filter((item) => item.column === id);
|
|
218
|
+
const itemIds = filteredData.map((item) => item.id);
|
|
219
|
+
const merge = useCn();
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<SortableContext items={itemIds}>
|
|
223
|
+
<div className={merge("flex grow flex-col gap-1.5", className)} {...props}>
|
|
224
|
+
{filteredData.map(children)}
|
|
225
|
+
</div>
|
|
226
|
+
</SortableContext>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// --- KanbanHeader: simple styled header ---
|
|
231
|
+
|
|
232
|
+
export type KanbanHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
233
|
+
|
|
234
|
+
export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => {
|
|
235
|
+
const merge = useCn();
|
|
236
|
+
return <div className={merge("m-0 p-2 font-semibold text-sm", className)} {...props} />;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// --- KanbanProvider: DnD context + Kanban context + a11y ---
|
|
240
|
+
|
|
241
|
+
export type KanbanProviderProps = {
|
|
242
|
+
children: ReactNode;
|
|
243
|
+
columns: KanbanColumnDef[];
|
|
244
|
+
data: KanbanItem[];
|
|
245
|
+
overlay?: ReactNode;
|
|
246
|
+
className?: string;
|
|
247
|
+
onDragStart?: (event: DragStartEvent) => void;
|
|
248
|
+
onDragEnd?: (event: DragEndEvent) => void;
|
|
249
|
+
onDragOver?: (event: DragOverEvent) => void;
|
|
250
|
+
onDragCancel?: (event: DragCancelEvent) => void;
|
|
251
|
+
collisionDetection?: CollisionDetection;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const KanbanProvider = ({
|
|
255
|
+
children,
|
|
256
|
+
columns,
|
|
257
|
+
data,
|
|
258
|
+
overlay,
|
|
259
|
+
className,
|
|
260
|
+
onDragStart,
|
|
261
|
+
onDragEnd,
|
|
262
|
+
onDragOver,
|
|
263
|
+
onDragCancel,
|
|
264
|
+
collisionDetection = closestCenter,
|
|
265
|
+
}: KanbanProviderProps) => {
|
|
266
|
+
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
|
267
|
+
const [overColumnId, setOverColumnId] = useState<string | null>(null);
|
|
268
|
+
|
|
269
|
+
const sensors = useDragSensors({ sortable: true });
|
|
270
|
+
|
|
271
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
272
|
+
const card = data.find((item) => item.id === event.active.id);
|
|
273
|
+
if (card) {
|
|
274
|
+
setActiveCardId(String(event.active.id));
|
|
275
|
+
}
|
|
276
|
+
onDragStart?.(event);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleDragOver = (event: DragOverEvent) => {
|
|
280
|
+
const overId = event.over ? String(event.over.id) : null;
|
|
281
|
+
setOverColumnId(resolveOverColumnId(overId, columns, data));
|
|
282
|
+
onDragOver?.(event);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
286
|
+
setActiveCardId(null);
|
|
287
|
+
setOverColumnId(null);
|
|
288
|
+
onDragEnd?.(event);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleDragCancel = (event: DragCancelEvent) => {
|
|
292
|
+
setActiveCardId(null);
|
|
293
|
+
setOverColumnId(null);
|
|
294
|
+
onDragCancel?.(event);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const announcements: Announcements = {
|
|
298
|
+
onDragStart({ active }) {
|
|
299
|
+
const item = data.find((d) => d.id === active.id);
|
|
300
|
+
const col = columns.find((c) => c.id === item?.column);
|
|
301
|
+
return `Picked up card "${item?.name}" from "${col?.name}" column`;
|
|
302
|
+
},
|
|
303
|
+
onDragOver({ active, over }) {
|
|
304
|
+
const item = data.find((d) => d.id === active.id);
|
|
305
|
+
const col = columns.find((c) => c.id === over?.id);
|
|
306
|
+
return `Dragged card "${item?.name}" over "${col?.name}" column`;
|
|
307
|
+
},
|
|
308
|
+
onDragEnd({ active, over }) {
|
|
309
|
+
const item = data.find((d) => d.id === active.id);
|
|
310
|
+
const col = columns.find((c) => c.id === over?.id);
|
|
311
|
+
return `Dropped card "${item?.name}" into "${col?.name}" column`;
|
|
312
|
+
},
|
|
313
|
+
onDragCancel({ active }) {
|
|
314
|
+
const item = data.find((d) => d.id === active.id);
|
|
315
|
+
return `Cancelled dragging card "${item?.name}"`;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<KanbanContext.Provider
|
|
321
|
+
value={{ columns, data, activeCardId, overColumnId }}
|
|
322
|
+
>
|
|
323
|
+
<DndContext
|
|
324
|
+
accessibility={{ announcements }}
|
|
325
|
+
collisionDetection={collisionDetection}
|
|
326
|
+
onDragEnd={handleDragEnd}
|
|
327
|
+
onDragOver={handleDragOver}
|
|
328
|
+
onDragStart={handleDragStart}
|
|
329
|
+
onDragCancel={handleDragCancel}
|
|
330
|
+
sensors={sensors}
|
|
331
|
+
>
|
|
332
|
+
{className ? <div className={className}>{children}</div> : children}
|
|
333
|
+
{typeof window !== "undefined" &&
|
|
334
|
+
createPortal(<DragOverlay>{overlay}</DragOverlay>, document.body)}
|
|
335
|
+
</DndContext>
|
|
336
|
+
</KanbanContext.Provider>
|
|
337
|
+
);
|
|
338
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KanbanColumnDef, KanbanItem } from "./kanban";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The column a dragged card would land in, given whatever dnd-kit reports as
|
|
5
|
+
* `over`.
|
|
6
|
+
*
|
|
7
|
+
* `over` is the column only while the pointer is on that column's empty space —
|
|
8
|
+
* once it is on a card, `over` is the card. A column that relies on its own
|
|
9
|
+
* `isOver` therefore stops highlighting the moment it holds anything, which is
|
|
10
|
+
* every column that matters. Resolving the card back to its column is what makes
|
|
11
|
+
* the drop target visible during a real drag.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveOverColumnId(
|
|
14
|
+
overId: string | null,
|
|
15
|
+
columns: KanbanColumnDef[],
|
|
16
|
+
data: KanbanItem[],
|
|
17
|
+
): string | null {
|
|
18
|
+
if (overId === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (columns.some((column) => column.id === overId)) {
|
|
22
|
+
return overId;
|
|
23
|
+
}
|
|
24
|
+
return data.find((item) => item.id === overId)?.column ?? null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DndContext,
|
|
5
|
+
type DragEndEvent,
|
|
6
|
+
rectIntersection,
|
|
7
|
+
useDraggable,
|
|
8
|
+
useDroppable,
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { useCn } from "../ui/ui-config";
|
|
13
|
+
import { useDragSensors } from "../utils/useDragSensors";
|
|
14
|
+
|
|
15
|
+
export type { DragEndEvent as SortableListDragEndEvent } from "@dnd-kit/core";
|
|
16
|
+
|
|
17
|
+
export type SortableListItemsProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const SortableListItems = ({ children, className }: SortableListItemsProps) => {
|
|
23
|
+
const merge = useCn();
|
|
24
|
+
return (
|
|
25
|
+
<div className={merge("flex flex-1 flex-col gap-0.5", className)}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SortableListHeaderProps =
|
|
32
|
+
| {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
name: string;
|
|
37
|
+
color: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const SortableListHeader = (props: SortableListHeaderProps) => {
|
|
42
|
+
const merge = useCn();
|
|
43
|
+
return "children" in props ? (
|
|
44
|
+
props.children
|
|
45
|
+
) : (
|
|
46
|
+
<div
|
|
47
|
+
className={merge(
|
|
48
|
+
"flex shrink-0 items-center gap-2 bg-foreground/5 p-3",
|
|
49
|
+
props.className,
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
className="h-2 w-2 rounded-full"
|
|
54
|
+
style={{ backgroundColor: props.color }}
|
|
55
|
+
/>
|
|
56
|
+
<p className="m-0 font-semibold text-sm">{props.name}</p>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type SortableListGroupProps = {
|
|
62
|
+
id: string;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
className?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const SortableListGroup = ({ id, children, className }: SortableListGroupProps) => {
|
|
68
|
+
const { setNodeRef, isOver } = useDroppable({ id });
|
|
69
|
+
const merge = useCn();
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={merge(
|
|
74
|
+
"transition-colors",
|
|
75
|
+
isOver && "bg-surface-secondary/40",
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
ref={setNodeRef}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type SortableListItemProps = {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly index: number;
|
|
89
|
+
readonly parent: string;
|
|
90
|
+
readonly children?: ReactNode;
|
|
91
|
+
readonly className?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Not built on `ListRow`: that row always wraps its children in a padded body
|
|
95
|
+
// `<div>` and adds its own surface, radius and hover, so rendering through it
|
|
96
|
+
// would change this item's DOM for every caller. This item is only the draggable box.
|
|
97
|
+
export const SortableListItem = ({
|
|
98
|
+
id,
|
|
99
|
+
name,
|
|
100
|
+
index,
|
|
101
|
+
parent,
|
|
102
|
+
children,
|
|
103
|
+
className,
|
|
104
|
+
}: SortableListItemProps) => {
|
|
105
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
106
|
+
useDraggable({
|
|
107
|
+
id,
|
|
108
|
+
data: { index, parent },
|
|
109
|
+
});
|
|
110
|
+
const merge = useCn();
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={merge(
|
|
115
|
+
"cursor-grab",
|
|
116
|
+
isDragging && "cursor-grabbing opacity-50",
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
style={{
|
|
120
|
+
transform: transform
|
|
121
|
+
? `translateX(${transform.x}px) translateY(${transform.y}px)`
|
|
122
|
+
: "none",
|
|
123
|
+
}}
|
|
124
|
+
{...listeners}
|
|
125
|
+
{...attributes}
|
|
126
|
+
ref={setNodeRef}
|
|
127
|
+
>
|
|
128
|
+
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type SortableListProviderProps = {
|
|
134
|
+
children: ReactNode;
|
|
135
|
+
onDragEnd: (event: DragEndEvent) => void;
|
|
136
|
+
className?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const SortableListProvider = ({
|
|
140
|
+
children,
|
|
141
|
+
onDragEnd,
|
|
142
|
+
className,
|
|
143
|
+
}: SortableListProviderProps) => {
|
|
144
|
+
// A lone `PointerSensor` armed on distance meant the list could not be
|
|
145
|
+
// reordered by touch at all: the whole row is the drag handle, the drag is
|
|
146
|
+
// restricted to the vertical axis, and that is the same axis the finger
|
|
147
|
+
// scrolls on — so the browser claimed the gesture and cancelled the pointer
|
|
148
|
+
// every time.
|
|
149
|
+
const sensors = useDragSensors();
|
|
150
|
+
const merge = useCn();
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<DndContext
|
|
154
|
+
sensors={sensors}
|
|
155
|
+
collisionDetection={rectIntersection}
|
|
156
|
+
modifiers={[restrictToVerticalAxis]}
|
|
157
|
+
onDragEnd={onDragEnd}
|
|
158
|
+
>
|
|
159
|
+
<div className={merge("flex size-full flex-col", className)}>{children}</div>
|
|
160
|
+
</DndContext>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
4
|
+
import { IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
|
|
5
|
+
import { motion } from "motion/react";
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { springTransition } from "../motion/presets";
|
|
8
|
+
import { useCn, useUiConfig } from "../ui/ui-config";
|
|
9
|
+
|
|
10
|
+
const themes = [
|
|
11
|
+
{
|
|
12
|
+
key: "system",
|
|
13
|
+
icon: IconDeviceDesktop,
|
|
14
|
+
label: "System theme",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "light",
|
|
18
|
+
icon: IconSun,
|
|
19
|
+
label: "Light theme",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "dark",
|
|
23
|
+
icon: IconMoon,
|
|
24
|
+
label: "Dark theme",
|
|
25
|
+
},
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
type ThemeKey = "light" | "dark" | "system";
|
|
29
|
+
|
|
30
|
+
export interface ThemeSwitcherConfig {
|
|
31
|
+
/** Replaces the built-in classes of a part. */
|
|
32
|
+
classes?: {
|
|
33
|
+
/** Each theme button: size, shape and press feedback. */
|
|
34
|
+
button?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultClasses = {
|
|
39
|
+
button:
|
|
40
|
+
"relative h-6 w-6 rounded-full transition-[scale] duration-150 ease-out active:scale-[0.94] hit-target",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ThemeSwitcherProps = {
|
|
44
|
+
value?: ThemeKey;
|
|
45
|
+
onChange?: (theme: ThemeKey) => void;
|
|
46
|
+
defaultValue?: ThemeKey;
|
|
47
|
+
className?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const ThemeSwitcher = ({
|
|
51
|
+
value,
|
|
52
|
+
onChange,
|
|
53
|
+
defaultValue = "system",
|
|
54
|
+
className,
|
|
55
|
+
}: ThemeSwitcherProps) => {
|
|
56
|
+
const classes = { ...defaultClasses, ...useUiConfig("ThemeSwitcher")?.classes };
|
|
57
|
+
const merge = useCn();
|
|
58
|
+
const [theme, setTheme] = useControllableState({
|
|
59
|
+
defaultProp: defaultValue,
|
|
60
|
+
prop: value,
|
|
61
|
+
onChange,
|
|
62
|
+
});
|
|
63
|
+
const handleThemeClick = useCallback(
|
|
64
|
+
(themeKey: ThemeKey) => {
|
|
65
|
+
setTheme(themeKey);
|
|
66
|
+
},
|
|
67
|
+
[setTheme],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// No mount gate: the selected theme comes from props rather than anything
|
|
71
|
+
// browser-only, so a client render has no hydration mismatch to skip the
|
|
72
|
+
// first paint for.
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={merge(
|
|
76
|
+
"relative isolate flex h-8 rounded-full bg-background p-1 ring-1 ring-border",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{themes.map(({ key, icon: Icon, label }) => {
|
|
81
|
+
const isActive = theme === key;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
aria-label={label}
|
|
86
|
+
className={classes.button}
|
|
87
|
+
key={key}
|
|
88
|
+
onClick={() => handleThemeClick(key)}
|
|
89
|
+
type="button"
|
|
90
|
+
>
|
|
91
|
+
{isActive && (
|
|
92
|
+
<motion.div
|
|
93
|
+
className="absolute inset-0 rounded-full bg-secondary"
|
|
94
|
+
layoutId="activeTheme"
|
|
95
|
+
transition={springTransition}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
<Icon
|
|
99
|
+
className={merge(
|
|
100
|
+
"relative z-10 m-auto h-4 w-4",
|
|
101
|
+
isActive ? "text-foreground" : "text-muted-foreground",
|
|
102
|
+
)}
|
|
103
|
+
/>
|
|
104
|
+
</button>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|