@djangocfg/widget-data 0.1.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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/package.json +122 -0
  4. package/src/DataGrid/README.md +48 -0
  5. package/src/DataGrid/context/DataGridContext.tsx +442 -0
  6. package/src/DataGrid/context/index.ts +4 -0
  7. package/src/DataGrid/index.ts +34 -0
  8. package/src/DataGrid/lazy.tsx +23 -0
  9. package/src/DataGrid/parts/DataGrid.tsx +125 -0
  10. package/src/DataGrid/parts/DataGridCell.tsx +30 -0
  11. package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
  12. package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
  13. package/src/DataGrid/parts/DataGridRow.tsx +84 -0
  14. package/src/DataGrid/parts/index.ts +7 -0
  15. package/src/DataGrid/types.ts +95 -0
  16. package/src/DataTable/README.md +42 -0
  17. package/src/DataTable/context/DataTableContext.tsx +434 -0
  18. package/src/DataTable/context/index.ts +4 -0
  19. package/src/DataTable/index.ts +33 -0
  20. package/src/DataTable/lazy.tsx +23 -0
  21. package/src/DataTable/parts/DataTable.tsx +142 -0
  22. package/src/DataTable/parts/DataTableHeader.tsx +103 -0
  23. package/src/DataTable/parts/DataTablePagination.tsx +115 -0
  24. package/src/DataTable/parts/DataTableRow.tsx +96 -0
  25. package/src/DataTable/parts/index.ts +6 -0
  26. package/src/DataTable/types.ts +88 -0
  27. package/src/FileIcon/FileIcon.tsx +87 -0
  28. package/src/FileIcon/get-file-icon.ts +73 -0
  29. package/src/FileIcon/icons/icon-data.ts +399 -0
  30. package/src/FileIcon/index.ts +19 -0
  31. package/src/FileIcon/loader.ts +29 -0
  32. package/src/FileIcon/specialFolders.ts +110 -0
  33. package/src/FileIcon/treeAdapter.tsx +49 -0
  34. package/src/JsonTree/JsonViewer.tsx +720 -0
  35. package/src/JsonTree/README.md +142 -0
  36. package/src/JsonTree/index.tsx +11 -0
  37. package/src/JsonTree/lazy.tsx +22 -0
  38. package/src/JsonTree/types.ts +91 -0
  39. package/src/Kanban/context/KanbanContext.tsx +165 -0
  40. package/src/Kanban/context/index.ts +4 -0
  41. package/src/Kanban/index.ts +31 -0
  42. package/src/Kanban/lazy.tsx +23 -0
  43. package/src/Kanban/parts/Kanban.tsx +77 -0
  44. package/src/Kanban/parts/KanbanCard.tsx +76 -0
  45. package/src/Kanban/parts/KanbanColumn.tsx +79 -0
  46. package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
  47. package/src/Kanban/parts/index.ts +6 -0
  48. package/src/Kanban/types.ts +45 -0
  49. package/src/Listbox/context/ListboxProvider.tsx +616 -0
  50. package/src/Listbox/context/index.ts +8 -0
  51. package/src/Listbox/index.ts +28 -0
  52. package/src/Listbox/lazy.tsx +31 -0
  53. package/src/Listbox/parts/ListboxGroup.tsx +71 -0
  54. package/src/Listbox/parts/ListboxItem.tsx +151 -0
  55. package/src/Listbox/parts/index.ts +12 -0
  56. package/src/Listbox/types.ts +98 -0
  57. package/src/Masonry/README.md +79 -0
  58. package/src/Masonry/context/MasonryProvider.tsx +1205 -0
  59. package/src/Masonry/context/index.ts +8 -0
  60. package/src/Masonry/index.ts +18 -0
  61. package/src/Masonry/lazy.tsx +20 -0
  62. package/src/Masonry/parts/index.ts +2 -0
  63. package/src/Masonry/types.ts +44 -0
  64. package/src/Timeline/context/TimelineProvider.tsx +474 -0
  65. package/src/Timeline/context/index.ts +16 -0
  66. package/src/Timeline/index.ts +29 -0
  67. package/src/Timeline/lazy.tsx +27 -0
  68. package/src/Timeline/parts/index.ts +2 -0
  69. package/src/Timeline/types.ts +25 -0
  70. package/src/Tree/FinderTree.tsx +42 -0
  71. package/src/Tree/README.md +542 -0
  72. package/src/Tree/TreeDndProvider.tsx +137 -0
  73. package/src/Tree/TreeRoot.tsx +290 -0
  74. package/src/Tree/components/TreeChevron.tsx +48 -0
  75. package/src/Tree/components/TreeContent.tsx +70 -0
  76. package/src/Tree/components/TreeDropIndicator.tsx +65 -0
  77. package/src/Tree/components/TreeEmpty.tsx +22 -0
  78. package/src/Tree/components/TreeEmptyArea.tsx +160 -0
  79. package/src/Tree/components/TreeError.tsx +24 -0
  80. package/src/Tree/components/TreeIcon.tsx +39 -0
  81. package/src/Tree/components/TreeIndentGuides.tsx +33 -0
  82. package/src/Tree/components/TreeLabel.tsx +39 -0
  83. package/src/Tree/components/TreeRenameInput.tsx +114 -0
  84. package/src/Tree/components/TreeRow.tsx +300 -0
  85. package/src/Tree/components/TreeSearchInput.tsx +50 -0
  86. package/src/Tree/components/TreeSkeleton.tsx +22 -0
  87. package/src/Tree/components/index.ts +28 -0
  88. package/src/Tree/context/TreeContext.tsx +409 -0
  89. package/src/Tree/context/TreeContextValue.ts +139 -0
  90. package/src/Tree/context/async-children/collect-ids.ts +27 -0
  91. package/src/Tree/context/async-children/index.ts +8 -0
  92. package/src/Tree/context/async-children/use-async-children.ts +157 -0
  93. package/src/Tree/context/clipboard/index.ts +4 -0
  94. package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
  95. package/src/Tree/context/dnd/index.ts +8 -0
  96. package/src/Tree/context/dnd/use-dnd.ts +194 -0
  97. package/src/Tree/context/expansion/index.ts +4 -0
  98. package/src/Tree/context/expansion/use-expansion.ts +55 -0
  99. package/src/Tree/context/hooks.ts +177 -0
  100. package/src/Tree/context/index.ts +16 -0
  101. package/src/Tree/context/menu/builtin-actions.ts +358 -0
  102. package/src/Tree/context/menu/index.ts +11 -0
  103. package/src/Tree/context/menu/render.tsx +104 -0
  104. package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
  105. package/src/Tree/context/persist/index.ts +4 -0
  106. package/src/Tree/context/persist/use-persist-sync.ts +74 -0
  107. package/src/Tree/context/rename/index.ts +4 -0
  108. package/src/Tree/context/rename/use-rename.ts +113 -0
  109. package/src/Tree/context/selection/index.ts +4 -0
  110. package/src/Tree/context/selection/use-selection.ts +146 -0
  111. package/src/Tree/context/state/index.ts +6 -0
  112. package/src/Tree/context/state/initial.ts +41 -0
  113. package/src/Tree/context/state/reducer.ts +76 -0
  114. package/src/Tree/context/state/types.ts +46 -0
  115. package/src/Tree/data/appearance.ts +237 -0
  116. package/src/Tree/data/childCache.ts +43 -0
  117. package/src/Tree/data/clipboard.ts +33 -0
  118. package/src/Tree/data/createDemoTree.ts +42 -0
  119. package/src/Tree/data/dnd.ts +123 -0
  120. package/src/Tree/data/finderShortcuts.ts +67 -0
  121. package/src/Tree/data/flatten.ts +66 -0
  122. package/src/Tree/data/index.ts +45 -0
  123. package/src/Tree/data/persist.ts +62 -0
  124. package/src/Tree/data/renameUtils.ts +51 -0
  125. package/src/Tree/data/selection.ts +161 -0
  126. package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  127. package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
  128. package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  129. package/src/Tree/hooks/index.ts +25 -0
  130. package/src/Tree/hooks/keyboard/activation.ts +27 -0
  131. package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  132. package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  133. package/src/Tree/hooks/keyboard/index.ts +10 -0
  134. package/src/Tree/hooks/keyboard/types.ts +39 -0
  135. package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  136. package/src/Tree/hooks/type-ahead/index.ts +5 -0
  137. package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  138. package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
  139. package/src/Tree/index.tsx +135 -0
  140. package/src/Tree/lazy.tsx +14 -0
  141. package/src/Tree/types/activation.ts +30 -0
  142. package/src/Tree/types/adapter.ts +70 -0
  143. package/src/Tree/types/index.ts +27 -0
  144. package/src/Tree/types/labels.ts +97 -0
  145. package/src/Tree/types/loader.ts +9 -0
  146. package/src/Tree/types/node.ts +38 -0
  147. package/src/Tree/types/root-props.ts +158 -0
  148. package/src/Tree/types/selection.ts +3 -0
  149. package/src/Tree/types/slots.ts +64 -0
  150. package/src/index.ts +21 -0
  151. package/src/stories/_fixtures/categories.ts +51 -0
  152. package/src/stories/_fixtures/finder-store.ts +196 -0
  153. package/src/stories/_fixtures/fs.ts +83 -0
  154. package/src/stories/_fixtures/index.ts +5 -0
  155. package/src/stories/_fixtures/org.ts +46 -0
  156. package/src/stories/_fixtures/status.ts +21 -0
  157. package/src/styles/index.css +10 -0
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ export {
4
+ Masonry,
5
+ MasonryItem,
6
+ MasonryViewport,
7
+ useMasonryContext,
8
+ } from "./MasonryProvider";
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ export {
4
+ Masonry,
5
+ MasonryItem,
6
+ useMasonryContext,
7
+ } from "./context/MasonryProvider";
8
+ export type {
9
+ MasonryProps,
10
+ MasonryItemProps,
11
+ Positioner,
12
+ PositionerItem,
13
+ } from "./types";
14
+
15
+ // The dynamic-import wrapper. Exported here because the subpath resolves to
16
+ // this barrel: in the retired ui-tools `lazy.tsx` was reachable from no declared export,
17
+ // so `LazyMasonry` was dead code for every tool but `JsonTree`.
18
+ export { LazyMasonry } from './lazy';
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
4
+ import type { MasonryProps } from './types';
5
+
6
+ export const LazyMasonry = createLazyComponent<MasonryProps>(
7
+ () => import('./context/MasonryProvider').then((m) => ({ default: m.Masonry })),
8
+ {
9
+ displayName: 'LazyMasonry',
10
+ fallback: <LoadingFallback minHeight={400} text="Loading masonry…" />,
11
+ },
12
+ );
13
+
14
+ // Re-export light surface synchronously
15
+ export * from './types';
16
+ export {
17
+ Masonry,
18
+ MasonryItem,
19
+ useMasonryContext,
20
+ } from './context/MasonryProvider';
@@ -0,0 +1,2 @@
1
+ // Parts are exported directly from context/MasonryProvider.tsx
2
+ // This file exists for structural consistency with the design guide.
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ export interface MasonryProps extends React.ComponentProps<"div"> {
4
+ columnWidth?: number;
5
+ columnCount?: number;
6
+ maxColumnCount?: number;
7
+ gap?: number | { column: number; row: number };
8
+ itemHeight?: number;
9
+ defaultWidth?: number;
10
+ defaultHeight?: number;
11
+ overscan?: number;
12
+ scrollFps?: number;
13
+ fallback?: React.ReactNode;
14
+ linear?: boolean;
15
+ asChild?: boolean;
16
+ }
17
+
18
+ export interface MasonryItemProps extends React.ComponentProps<"div"> {
19
+ asChild?: boolean;
20
+ }
21
+
22
+ export interface Positioner {
23
+ columnCount: number;
24
+ columnWidth: number;
25
+ set: (index: number, height: number) => void;
26
+ get: (index: number) => PositionerItem | undefined;
27
+ update: (updates: number[]) => void;
28
+ range: (
29
+ low: number,
30
+ high: number,
31
+ onItemRender: (index: number, left: number, top: number) => void,
32
+ ) => void;
33
+ size: () => number;
34
+ estimateHeight: (itemCount: number, defaultItemHeight: number) => number;
35
+ shortestColumn: () => number;
36
+ all: () => PositionerItem[];
37
+ }
38
+
39
+ export interface PositionerItem {
40
+ top: number;
41
+ left: number;
42
+ height: number;
43
+ columnIndex: number;
44
+ }
@@ -0,0 +1,474 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { useDirection } from "@radix-ui/react-direction";
6
+ import { useComposedRefs } from "@djangocfg/ui-core/lib";
7
+ import { cn } from "@djangocfg/ui-core/lib";
8
+ import { cva } from "class-variance-authority";
9
+ import type {
10
+ TimelineProps,
11
+ TimelineDirection,
12
+ TimelineOrientation,
13
+ TimelineVariant,
14
+ TimelineStatus,
15
+ DivProps,
16
+ TimelineConnectorProps,
17
+ TimelineTimeProps,
18
+ } from "../types";
19
+
20
+ const ROOT_NAME = "Timeline";
21
+ const ITEM_NAME = "TimelineItem";
22
+ const DOT_NAME = "TimelineDot";
23
+ const CONNECTOR_NAME = "TimelineConnector";
24
+ const CONTENT_NAME = "TimelineContent";
25
+
26
+ type ItemElement = React.ComponentRef<"div">;
27
+
28
+ function getItemStatus(itemIndex: number, activeIndex?: number): TimelineStatus {
29
+ if (activeIndex === undefined) return "pending";
30
+ if (itemIndex < activeIndex) return "completed";
31
+ if (itemIndex === activeIndex) return "active";
32
+ return "pending";
33
+ }
34
+
35
+ function getSortedEntries(
36
+ entries: [string, React.RefObject<ItemElement | null>][],
37
+ ) {
38
+ return entries.sort((a, b) => {
39
+ const elementA = a[1].current;
40
+ const elementB = b[1].current;
41
+ if (!elementA || !elementB) return 0;
42
+ const position = elementA.compareDocumentPosition(elementB);
43
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
44
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
45
+ return 0;
46
+ });
47
+ }
48
+
49
+ function useLazyRef<T>(init: () => T): React.RefObject<T> {
50
+ const ref = React.useRef<T | null>(null);
51
+ if (ref.current === null) {
52
+ ref.current = init();
53
+ }
54
+ return ref as React.RefObject<T>;
55
+ }
56
+
57
+ interface StoreState {
58
+ items: Map<string, React.RefObject<ItemElement | null>>;
59
+ }
60
+
61
+ interface Store {
62
+ subscribe: (callback: () => void) => () => void;
63
+ getState: () => StoreState;
64
+ notify: () => void;
65
+ onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => void;
66
+ onItemUnregister: (id: string) => void;
67
+ getNextItemStatus: (id: string, activeIndex?: number) => TimelineStatus | undefined;
68
+ getItemIndex: (id: string) => number;
69
+ }
70
+
71
+ const StoreContext = React.createContext<Store | null>(null);
72
+
73
+ function useStoreContext(consumerName: string) {
74
+ const context = React.useContext(StoreContext);
75
+ if (!context) {
76
+ throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
77
+ }
78
+ return context;
79
+ }
80
+
81
+ function useStore<T>(selector: (store: Store) => T): T {
82
+ const store = useStoreContext("useStore");
83
+ const getSnapshot = React.useCallback(() => selector(store), [store, selector]);
84
+ return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
85
+ }
86
+
87
+ interface TimelineContextValue {
88
+ dir: TimelineDirection;
89
+ orientation: TimelineOrientation;
90
+ variant: TimelineVariant;
91
+ activeIndex?: number;
92
+ }
93
+
94
+ const TimelineContext = React.createContext<TimelineContextValue | null>(null);
95
+
96
+ function useTimelineContext(consumerName: string) {
97
+ const context = React.useContext(TimelineContext);
98
+ if (!context) {
99
+ throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
100
+ }
101
+ return context;
102
+ }
103
+
104
+ const timelineVariants = cva(
105
+ "relative flex [--timeline-connector-thickness:0.125rem] [--timeline-dot-size:0.875rem]",
106
+ {
107
+ variants: {
108
+ orientation: {
109
+ vertical: "flex-col",
110
+ horizontal: "flex-row items-start",
111
+ },
112
+ variant: {
113
+ default: "",
114
+ alternate: "",
115
+ },
116
+ },
117
+ compoundVariants: [
118
+ { orientation: "vertical", variant: "default", class: "gap-6" },
119
+ { orientation: "horizontal", variant: "default", class: "gap-8" },
120
+ { orientation: "vertical", variant: "alternate", class: "relative w-full gap-3" },
121
+ { orientation: "horizontal", variant: "alternate", class: "items-center gap-4" },
122
+ ],
123
+ defaultVariants: { orientation: "vertical", variant: "default" },
124
+ },
125
+ );
126
+
127
+ function Timeline(props: TimelineProps) {
128
+ const {
129
+ orientation = "vertical",
130
+ variant = "default",
131
+ dir: dirProp,
132
+ activeIndex,
133
+ asChild,
134
+ className,
135
+ ...rootProps
136
+ } = props;
137
+
138
+ const dir = useDirection(dirProp) as TimelineDirection;
139
+
140
+ const listenersRef = useLazyRef(() => new Set<() => void>());
141
+ const stateRef = useLazyRef<StoreState>(() => ({ items: new Map() }));
142
+
143
+ const store = React.useMemo<Store>(() => {
144
+ return {
145
+ subscribe: (cb) => {
146
+ listenersRef.current.add(cb);
147
+ return () => listenersRef.current.delete(cb);
148
+ },
149
+ getState: () => stateRef.current,
150
+ notify: () => {
151
+ for (const cb of listenersRef.current) {
152
+ cb();
153
+ }
154
+ },
155
+ onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => {
156
+ stateRef.current.items.set(id, ref);
157
+ store.notify();
158
+ },
159
+ onItemUnregister: (id: string) => {
160
+ stateRef.current.items.delete(id);
161
+ store.notify();
162
+ },
163
+ getNextItemStatus: (id: string, activeIndex?: number) => {
164
+ const entries = Array.from(stateRef.current.items.entries());
165
+ const sortedEntries = getSortedEntries(entries);
166
+ const currentIndex = sortedEntries.findIndex(([key]) => key === id);
167
+ if (currentIndex === -1 || currentIndex === sortedEntries.length - 1) {
168
+ return undefined;
169
+ }
170
+ const nextItemIndex = currentIndex + 1;
171
+ return getItemStatus(nextItemIndex, activeIndex);
172
+ },
173
+ getItemIndex: (id: string) => {
174
+ const entries = Array.from(stateRef.current.items.entries());
175
+ const sortedEntries = getSortedEntries(entries);
176
+ return sortedEntries.findIndex(([key]) => key === id);
177
+ },
178
+ };
179
+ }, [listenersRef, stateRef]);
180
+
181
+ const contextValue = React.useMemo<TimelineContextValue>(
182
+ () => ({ dir, orientation, variant, activeIndex }),
183
+ [dir, orientation, variant, activeIndex],
184
+ );
185
+
186
+ const RootPrimitive = asChild ? Slot : "div";
187
+
188
+ return (
189
+ <StoreContext.Provider value={store}>
190
+ <TimelineContext.Provider value={contextValue}>
191
+ <RootPrimitive
192
+ role="list"
193
+ aria-orientation={orientation}
194
+ data-slot="timeline"
195
+ data-orientation={orientation}
196
+ data-variant={variant}
197
+ dir={dir}
198
+ className={cn(timelineVariants({ orientation, variant, className }))}
199
+ {...rootProps}
200
+ />
201
+ </TimelineContext.Provider>
202
+ </StoreContext.Provider>
203
+ );
204
+ }
205
+
206
+ interface TimelineItemContextValue {
207
+ id: string;
208
+ status: TimelineStatus;
209
+ isAlternateRight: boolean;
210
+ }
211
+
212
+ const TimelineItemContext = React.createContext<TimelineItemContextValue | null>(null);
213
+
214
+ function useTimelineItemContext(consumerName: string) {
215
+ const context = React.useContext(TimelineItemContext);
216
+ if (!context) {
217
+ throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
218
+ }
219
+ return context;
220
+ }
221
+
222
+ const timelineItemVariants = cva("relative flex", {
223
+ variants: {
224
+ orientation: { vertical: "", horizontal: "" },
225
+ variant: { default: "", alternate: "" },
226
+ isAlternateRight: { true: "", false: "" },
227
+ },
228
+ compoundVariants: [
229
+ { orientation: "vertical", variant: "default", class: "gap-3 pb-8 last:pb-0" },
230
+ { orientation: "horizontal", variant: "default", class: "flex-col gap-3" },
231
+ { orientation: "vertical", variant: "alternate", isAlternateRight: false, class: "w-1/2 gap-3 pr-6 pb-12 last:pb-0" },
232
+ { orientation: "vertical", variant: "alternate", isAlternateRight: true, class: "ml-auto w-1/2 flex-row-reverse gap-3 pb-12 pl-6 last:pb-0" },
233
+ { orientation: "horizontal", variant: "alternate", class: "grid min-w-0 grid-rows-[1fr_auto_1fr] gap-3" },
234
+ ],
235
+ defaultVariants: { orientation: "vertical", variant: "default", isAlternateRight: false },
236
+ });
237
+
238
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
239
+
240
+ function TimelineItem(props: DivProps) {
241
+ const { asChild, className, id, ref, ...itemProps } = props;
242
+
243
+ const { dir, orientation, variant, activeIndex } = useTimelineContext(ITEM_NAME);
244
+ const store = useStoreContext(ITEM_NAME);
245
+
246
+ const instanceId = React.useId();
247
+ const itemId = id ?? instanceId;
248
+ const itemRef = React.useRef<ItemElement | null>(null);
249
+ const composedRef = useComposedRefs(ref, itemRef);
250
+
251
+ const itemIndex = useStore((state) => state.getItemIndex(itemId));
252
+
253
+ const status = React.useMemo<TimelineStatus>(() => {
254
+ return getItemStatus(itemIndex, activeIndex);
255
+ }, [activeIndex, itemIndex]);
256
+
257
+ useIsomorphicLayoutEffect(() => {
258
+ store.onItemRegister(itemId, itemRef);
259
+ return () => {
260
+ store.onItemUnregister(itemId);
261
+ };
262
+ }, [id, store]);
263
+
264
+ const isAlternateRight = variant === "alternate" && itemIndex % 2 === 1;
265
+
266
+ const itemContextValue = React.useMemo<TimelineItemContextValue>(
267
+ () => ({ id: itemId, status, isAlternateRight }),
268
+ [itemId, status, isAlternateRight],
269
+ );
270
+
271
+ const ItemPrimitive = asChild ? Slot : "div";
272
+
273
+ return (
274
+ <TimelineItemContext.Provider value={itemContextValue}>
275
+ <ItemPrimitive
276
+ role="listitem"
277
+ aria-current={status === "active" ? "step" : undefined}
278
+ data-slot="timeline-item"
279
+ data-status={status}
280
+ data-orientation={orientation}
281
+ data-alternate-right={isAlternateRight ? "" : undefined}
282
+ id={itemId}
283
+ dir={dir}
284
+ className={cn(timelineItemVariants({ orientation, variant, isAlternateRight, className }))}
285
+ {...itemProps}
286
+ ref={composedRef}
287
+ />
288
+ </TimelineItemContext.Provider>
289
+ );
290
+ }
291
+
292
+ const timelineContentVariants = cva("flex-1", {
293
+ variants: {
294
+ orientation: { vertical: "", horizontal: "" },
295
+ variant: { default: "", alternate: "" },
296
+ isAlternateRight: { true: "", false: "" },
297
+ },
298
+ compoundVariants: [
299
+ { variant: "alternate", orientation: "vertical", isAlternateRight: false, class: "text-right" },
300
+ { variant: "alternate", orientation: "horizontal", isAlternateRight: false, class: "row-start-3 pt-2" },
301
+ { variant: "alternate", orientation: "horizontal", isAlternateRight: true, class: "row-start-1 pb-2" },
302
+ ],
303
+ defaultVariants: { orientation: "vertical", variant: "default", isAlternateRight: false },
304
+ });
305
+
306
+ function TimelineContent(props: DivProps) {
307
+ const { asChild, className, ...contentProps } = props;
308
+ const { variant, orientation } = useTimelineContext(CONTENT_NAME);
309
+ const { status, isAlternateRight } = useTimelineItemContext(CONTENT_NAME);
310
+
311
+ const ContentPrimitive = asChild ? Slot : "div";
312
+
313
+ return (
314
+ <ContentPrimitive
315
+ data-slot="timeline-content"
316
+ data-status={status}
317
+ className={cn(timelineContentVariants({ orientation, variant, isAlternateRight, className }))}
318
+ {...contentProps}
319
+ />
320
+ );
321
+ }
322
+
323
+ const timelineDotVariants = cva(
324
+ "relative z-10 flex size-[var(--timeline-dot-size)] shrink-0 items-center justify-center rounded-full border-2 bg-background",
325
+ {
326
+ variants: {
327
+ status: {
328
+ completed: "border-primary",
329
+ active: "border-primary",
330
+ pending: "border-border",
331
+ },
332
+ orientation: { vertical: "", horizontal: "" },
333
+ variant: { default: "", alternate: "" },
334
+ isAlternateRight: { true: "", false: "" },
335
+ },
336
+ compoundVariants: [
337
+ { variant: "alternate", orientation: "vertical", isAlternateRight: false, class: "absolute -right-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background" },
338
+ { variant: "alternate", orientation: "vertical", isAlternateRight: true, class: "absolute -left-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background" },
339
+ { variant: "alternate", orientation: "horizontal", class: "row-start-2 bg-background" },
340
+ { variant: "alternate", status: "completed", class: "bg-background" },
341
+ { variant: "alternate", status: "active", class: "bg-background" },
342
+ ],
343
+ defaultVariants: { status: "pending", orientation: "vertical", variant: "default", isAlternateRight: false },
344
+ },
345
+ );
346
+
347
+ function TimelineDot(props: DivProps) {
348
+ const { asChild, className, ...dotProps } = props;
349
+ const { orientation, variant } = useTimelineContext(DOT_NAME);
350
+ const { status, isAlternateRight } = useTimelineItemContext(DOT_NAME);
351
+
352
+ const DotPrimitive = asChild ? Slot : "div";
353
+
354
+ return (
355
+ <DotPrimitive
356
+ data-slot="timeline-dot"
357
+ data-status={status}
358
+ data-orientation={orientation}
359
+ className={cn(timelineDotVariants({ status, orientation, variant, isAlternateRight, className }))}
360
+ {...dotProps}
361
+ />
362
+ );
363
+ }
364
+
365
+ const timelineConnectorVariants = cva("absolute z-0", {
366
+ variants: {
367
+ isCompleted: { true: "bg-primary", false: "bg-border" },
368
+ orientation: { vertical: "", horizontal: "" },
369
+ variant: { default: "", alternate: "" },
370
+ isAlternateRight: { true: "", false: "" },
371
+ },
372
+ compoundVariants: [
373
+ { orientation: "vertical", variant: "default", class: "start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-3 h-[calc(100%+0.5rem)] w-[var(--timeline-connector-thickness)]" },
374
+ { orientation: "horizontal", variant: "default", class: "start-3 top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]" },
375
+ { orientation: "vertical", variant: "alternate", isAlternateRight: false, class: "top-2 -right-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]" },
376
+ { orientation: "vertical", variant: "alternate", isAlternateRight: true, class: "top-2 -left-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]" },
377
+ { orientation: "horizontal", variant: "alternate", class: "top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] left-3 row-start-2 h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]" },
378
+ ],
379
+ defaultVariants: { isCompleted: false, orientation: "vertical", variant: "default", isAlternateRight: false },
380
+ });
381
+
382
+ function TimelineConnector(props: TimelineConnectorProps) {
383
+ const { asChild, forceMount, className, ...connectorProps } = props;
384
+ const { orientation, variant, activeIndex } = useTimelineContext(CONNECTOR_NAME);
385
+ const { id, status, isAlternateRight } = useTimelineItemContext(CONNECTOR_NAME);
386
+
387
+ const nextItemStatus = useStore((state) => state.getNextItemStatus(id, activeIndex));
388
+ const isLastItem = nextItemStatus === undefined;
389
+
390
+ if (!forceMount && isLastItem) return null;
391
+
392
+ const isConnectorCompleted = nextItemStatus === "completed" || nextItemStatus === "active";
393
+
394
+ const ConnectorPrimitive = asChild ? Slot : "div";
395
+
396
+ return (
397
+ <ConnectorPrimitive
398
+ aria-hidden="true"
399
+ data-slot="timeline-connector"
400
+ data-completed={isConnectorCompleted ? "" : undefined}
401
+ data-status={status}
402
+ data-orientation={orientation}
403
+ className={cn(timelineConnectorVariants({ isCompleted: isConnectorCompleted, orientation, variant, isAlternateRight, className }))}
404
+ {...connectorProps}
405
+ />
406
+ );
407
+ }
408
+
409
+ function TimelineHeader(props: DivProps) {
410
+ const { asChild, className, ...headerProps } = props;
411
+ const HeaderPrimitive = asChild ? Slot : "div";
412
+
413
+ return (
414
+ <HeaderPrimitive
415
+ data-slot="timeline-header"
416
+ className={cn("flex flex-col gap-1", className)}
417
+ {...headerProps}
418
+ />
419
+ );
420
+ }
421
+
422
+ function TimelineTitle(props: DivProps) {
423
+ const { asChild, className, ...titleProps } = props;
424
+ const TitlePrimitive = asChild ? Slot : "div";
425
+
426
+ return (
427
+ <TitlePrimitive
428
+ data-slot="timeline-title"
429
+ className={cn("font-semibold leading-none", className)}
430
+ {...titleProps}
431
+ />
432
+ );
433
+ }
434
+
435
+ function TimelineDescription(props: DivProps) {
436
+ const { asChild, className, ...descriptionProps } = props;
437
+ const DescriptionPrimitive = asChild ? Slot : "div";
438
+
439
+ return (
440
+ <DescriptionPrimitive
441
+ data-slot="timeline-description"
442
+ className={cn("text-muted-foreground text-sm", className)}
443
+ {...descriptionProps}
444
+ />
445
+ );
446
+ }
447
+
448
+ function TimelineTime(props: TimelineTimeProps) {
449
+ const { asChild, className, ...timeProps } = props;
450
+ const TimePrimitive = asChild ? Slot : "time";
451
+
452
+ return (
453
+ <TimePrimitive
454
+ data-slot="timeline-time"
455
+ className={cn("text-muted-foreground text-xs", className)}
456
+ {...timeProps}
457
+ />
458
+ );
459
+ }
460
+
461
+ export {
462
+ Timeline,
463
+ TimelineItem,
464
+ TimelineContent,
465
+ TimelineDot,
466
+ TimelineConnector,
467
+ TimelineHeader,
468
+ TimelineTitle,
469
+ TimelineDescription,
470
+ TimelineTime,
471
+ useTimelineContext,
472
+ useTimelineItemContext,
473
+ useStore,
474
+ };
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ export {
4
+ Timeline,
5
+ TimelineItem,
6
+ TimelineContent,
7
+ TimelineDot,
8
+ TimelineConnector,
9
+ TimelineHeader,
10
+ TimelineTitle,
11
+ TimelineDescription,
12
+ TimelineTime,
13
+ useTimelineContext,
14
+ useTimelineItemContext,
15
+ useStore,
16
+ } from "./TimelineProvider";
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ export {
4
+ Timeline,
5
+ TimelineItem,
6
+ TimelineContent,
7
+ TimelineDot,
8
+ TimelineConnector,
9
+ TimelineHeader,
10
+ TimelineTitle,
11
+ TimelineDescription,
12
+ TimelineTime,
13
+ useTimelineContext,
14
+ useTimelineItemContext,
15
+ } from "./context/TimelineProvider";
16
+ export type {
17
+ TimelineProps,
18
+ TimelineDirection,
19
+ TimelineOrientation,
20
+ TimelineVariant,
21
+ TimelineStatus,
22
+ TimelineConnectorProps,
23
+ TimelineTimeProps,
24
+ } from "./types";
25
+
26
+ // The dynamic-import wrapper. Exported here because the subpath resolves to
27
+ // this barrel: in the retired ui-tools `lazy.tsx` was reachable from no declared export,
28
+ // so `LazyTimeline` was dead code for every tool but `JsonTree`.
29
+ export { LazyTimeline } from './lazy';
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
4
+ import type { TimelineProps } from './types';
5
+
6
+ export const LazyTimeline = createLazyComponent<TimelineProps>(
7
+ () => import('./context/TimelineProvider').then((m) => ({ default: m.Timeline })),
8
+ {
9
+ displayName: 'LazyTimeline',
10
+ fallback: <LoadingFallback minHeight={200} text="Loading timeline…" />,
11
+ },
12
+ );
13
+
14
+ // Re-export light surface synchronously
15
+ export * from './types';
16
+ export {
17
+ TimelineItem,
18
+ TimelineContent,
19
+ TimelineDot,
20
+ TimelineConnector,
21
+ TimelineHeader,
22
+ TimelineTitle,
23
+ TimelineDescription,
24
+ TimelineTime,
25
+ useTimelineContext,
26
+ useTimelineItemContext,
27
+ } from './context/TimelineProvider';
@@ -0,0 +1,2 @@
1
+ // Parts are exported directly from context/TimelineProvider.tsx
2
+ // This file exists for structural consistency with the design guide.
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ export type TimelineDirection = "ltr" | "rtl";
4
+ export type TimelineOrientation = "vertical" | "horizontal";
5
+ export type TimelineVariant = "default" | "alternate";
6
+ export type TimelineStatus = "completed" | "active" | "pending";
7
+
8
+ export interface DivProps extends React.ComponentProps<"div"> {
9
+ asChild?: boolean;
10
+ }
11
+
12
+ export interface TimelineProps extends DivProps {
13
+ dir?: TimelineDirection;
14
+ orientation?: TimelineOrientation;
15
+ variant?: TimelineVariant;
16
+ activeIndex?: number;
17
+ }
18
+
19
+ export interface TimelineConnectorProps extends DivProps {
20
+ forceMount?: boolean;
21
+ }
22
+
23
+ export interface TimelineTimeProps extends React.ComponentProps<"time"> {
24
+ asChild?: boolean;
25
+ }