@usefragments/ui 1.3.3 → 1.4.0

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 (137) hide show
  1. package/dist/assets/ui.css +1176 -766
  2. package/dist/chart.cjs +60 -24
  3. package/dist/chart.js +38 -24
  4. package/dist/colorpicker.cjs +56 -9
  5. package/dist/colorpicker.js +34 -9
  6. package/dist/components/Badge/Badge.module.scss.cjs +23 -16
  7. package/dist/components/Badge/Badge.module.scss.js +23 -16
  8. package/dist/components/Badge/index.cjs +8 -1
  9. package/dist/components/Badge/index.d.ts +3 -0
  10. package/dist/components/Badge/index.d.ts.map +1 -1
  11. package/dist/components/Badge/index.js +8 -1
  12. package/dist/components/Button/Button.module.scss.cjs +16 -13
  13. package/dist/components/Button/Button.module.scss.js +16 -13
  14. package/dist/components/Button/index.d.ts +2 -1
  15. package/dist/components/Button/index.d.ts.map +1 -1
  16. package/dist/components/Chart/index.d.ts +3 -0
  17. package/dist/components/Chart/index.d.ts.map +1 -1
  18. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
  19. package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
  20. package/dist/components/ColorPicker/index.d.ts +4 -0
  21. package/dist/components/ColorPicker/index.d.ts.map +1 -1
  22. package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
  23. package/dist/components/Combobox/Combobox.module.scss.js +23 -23
  24. package/dist/components/Command/Command.module.scss.cjs +11 -11
  25. package/dist/components/Command/Command.module.scss.js +11 -11
  26. package/dist/components/ConversationList/ConversationList.module.scss.cjs +13 -10
  27. package/dist/components/ConversationList/ConversationList.module.scss.js +14 -11
  28. package/dist/components/ConversationList/index.cjs +26 -22
  29. package/dist/components/ConversationList/index.d.ts +5 -1
  30. package/dist/components/ConversationList/index.d.ts.map +1 -1
  31. package/dist/components/ConversationList/index.js +27 -23
  32. package/dist/components/DataTable/index.d.ts +4 -1
  33. package/dist/components/DataTable/index.d.ts.map +1 -1
  34. package/dist/components/DatePicker/index.d.ts +3 -0
  35. package/dist/components/DatePicker/index.d.ts.map +1 -1
  36. package/dist/components/Editor/index.d.ts +4 -1
  37. package/dist/components/Editor/index.d.ts.map +1 -1
  38. package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
  39. package/dist/components/Listbox/Listbox.module.scss.js +8 -8
  40. package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
  41. package/dist/components/Markdown/Markdown.module.scss.js +1 -1
  42. package/dist/components/Markdown/index.d.ts +16 -0
  43. package/dist/components/Markdown/index.d.ts.map +1 -1
  44. package/dist/components/Menu/Menu.module.scss.cjs +14 -14
  45. package/dist/components/Menu/Menu.module.scss.js +14 -14
  46. package/dist/components/Message/Message.module.scss.cjs +23 -20
  47. package/dist/components/Message/Message.module.scss.js +24 -21
  48. package/dist/components/Message/index.cjs +5 -1
  49. package/dist/components/Message/index.d.ts.map +1 -1
  50. package/dist/components/Message/index.js +5 -1
  51. package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
  52. package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
  53. package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
  54. package/dist/components/Pagination/Pagination.module.scss.js +7 -7
  55. package/dist/components/Prompt/Prompt.module.scss.cjs +52 -22
  56. package/dist/components/Prompt/Prompt.module.scss.js +52 -22
  57. package/dist/components/Prompt/index.cjs +197 -2
  58. package/dist/components/Prompt/index.d.ts +98 -3
  59. package/dist/components/Prompt/index.d.ts.map +1 -1
  60. package/dist/components/Prompt/index.js +198 -3
  61. package/dist/components/Select/Select.module.scss.cjs +29 -17
  62. package/dist/components/Select/Select.module.scss.js +30 -18
  63. package/dist/components/Select/index.cjs +45 -8
  64. package/dist/components/Select/index.d.ts +17 -2
  65. package/dist/components/Select/index.d.ts.map +1 -1
  66. package/dist/components/Select/index.js +45 -8
  67. package/dist/components/Sidebar/Sidebar.module.scss.cjs +45 -42
  68. package/dist/components/Sidebar/Sidebar.module.scss.js +45 -42
  69. package/dist/components/Sidebar/index.cjs +2 -0
  70. package/dist/components/Sidebar/index.d.ts +6 -1
  71. package/dist/components/Sidebar/index.d.ts.map +1 -1
  72. package/dist/components/Sidebar/index.js +2 -0
  73. package/dist/components/Text/Text.module.scss.cjs +63 -51
  74. package/dist/components/Text/Text.module.scss.js +63 -51
  75. package/dist/components/Text/index.d.ts +4 -2
  76. package/dist/components/Text/index.d.ts.map +1 -1
  77. package/dist/data-table.cjs +210 -28
  78. package/dist/data-table.js +189 -29
  79. package/dist/datepicker.cjs +63 -14
  80. package/dist/datepicker.js +41 -14
  81. package/dist/editor.cjs +67 -16
  82. package/dist/editor.js +45 -16
  83. package/dist/index.cjs +1 -0
  84. package/dist/index.d.ts +1 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +2 -1
  87. package/dist/markdown.cjs +63 -19
  88. package/dist/markdown.js +41 -19
  89. package/fragments.json +1 -1
  90. package/package.json +2 -2
  91. package/src/components/Badge/Badge.contract.json +28 -5
  92. package/src/components/Badge/Badge.module.scss +21 -0
  93. package/src/components/Badge/index.tsx +10 -1
  94. package/src/components/Button/Button.contract.json +9 -3
  95. package/src/components/Button/Button.module.scss +34 -0
  96. package/src/components/Button/index.tsx +11 -1
  97. package/src/components/Chart/Chart.contract.json +7 -0
  98. package/src/components/Chart/index.tsx +44 -27
  99. package/src/components/CodeBlock/CodeBlock.contract.json +7 -0
  100. package/src/components/CodeBlock/CodeBlock.module.scss +7 -2
  101. package/src/components/ColorPicker/ColorPicker.contract.json +9 -1
  102. package/src/components/ColorPicker/index.tsx +42 -12
  103. package/src/components/Combobox/Combobox.module.scss +4 -13
  104. package/src/components/ConversationList/ConversationList.contract.json +16 -1
  105. package/src/components/ConversationList/ConversationList.module.scss +12 -0
  106. package/src/components/ConversationList/index.tsx +40 -26
  107. package/src/components/DataTable/DataTable.contract.json +8 -1
  108. package/src/components/DataTable/index.tsx +282 -51
  109. package/src/components/DatePicker/DatePicker.contract.json +8 -1
  110. package/src/components/DatePicker/index.tsx +51 -22
  111. package/src/components/Editor/Editor.contract.json +19 -1
  112. package/src/components/Editor/index.tsx +67 -23
  113. package/src/components/Listbox/Listbox.module.scss +3 -11
  114. package/src/components/Markdown/Markdown.contract.json +15 -1
  115. package/src/components/Markdown/Markdown.module.scss +18 -1
  116. package/src/components/Markdown/index.tsx +58 -24
  117. package/src/components/Menu/Menu.module.scss +12 -2
  118. package/src/components/Message/Message.contract.json +3 -0
  119. package/src/components/Message/Message.module.scss +33 -4
  120. package/src/components/Message/index.tsx +7 -1
  121. package/src/components/Pagination/Pagination.contract.json +1 -0
  122. package/src/components/Pagination/Pagination.module.scss +5 -4
  123. package/src/components/Prompt/Prompt.contract.json +40 -1
  124. package/src/components/Prompt/Prompt.module.scss +351 -4
  125. package/src/components/Prompt/index.tsx +324 -0
  126. package/src/components/Select/Select.contract.json +11 -1
  127. package/src/components/Select/Select.module.scss +80 -13
  128. package/src/components/Select/index.tsx +73 -7
  129. package/src/components/Sidebar/Sidebar.contract.json +1 -0
  130. package/src/components/Sidebar/Sidebar.module.scss +10 -0
  131. package/src/components/Sidebar/index.tsx +7 -0
  132. package/src/components/Text/Text.contract.json +7 -4
  133. package/src/components/Text/Text.module.scss +18 -0
  134. package/src/components/Text/index.tsx +4 -2
  135. package/src/index.ts +1 -0
  136. package/src/tokens/_mixins.scss +39 -4
  137. package/src/tokens/_variables.scss +30 -0
@@ -13,6 +13,8 @@ export type AutoScrollBehavior = boolean | 'smart';
13
13
  export interface ConversationListProps extends React.HTMLAttributes<HTMLDivElement> {
14
14
  /** Message components */
15
15
  children: React.ReactNode;
16
+ /** Show avatars for messages and typing indicators in this conversation */
17
+ showAvatars?: boolean;
16
18
  /** Auto-scroll behavior: true (always), false (never), or 'smart' (only when near bottom) */
17
19
  autoScroll?: AutoScrollBehavior;
18
20
  /** Callback when user scrolls to top (for loading history) */
@@ -47,12 +49,17 @@ export interface TypingIndicatorProps extends React.HTMLAttributes<HTMLDivElemen
47
49
 
48
50
  interface ConversationListContextValue {
49
51
  scrollToBottom: () => void;
52
+ showAvatars: boolean;
50
53
  }
51
54
 
52
55
  const ConversationListContext = React.createContext<ConversationListContextValue | null>(null);
53
56
 
57
+ export function useOptionalConversationList() {
58
+ return React.useContext(ConversationListContext);
59
+ }
60
+
54
61
  export function useConversationList() {
55
- const context = React.useContext(ConversationListContext);
62
+ const context = useOptionalConversationList();
56
63
  if (!context) {
57
64
  throw new Error('useConversationList must be used within a ConversationList');
58
65
  }
@@ -113,11 +120,16 @@ function TypingIndicator({
113
120
  className,
114
121
  ...htmlProps
115
122
  }: TypingIndicatorProps) {
116
- const classes = [styles.typingIndicator, className].filter(Boolean).join(' ');
123
+ const showAvatar = (useOptionalConversationList()?.showAvatars ?? true) && Boolean(avatar);
124
+ const classes = [
125
+ styles.typingIndicator,
126
+ !showAvatar && styles.withoutAvatar,
127
+ className,
128
+ ].filter(Boolean).join(' ');
117
129
 
118
130
  return (
119
- <div {...htmlProps} className={classes} aria-label={`${name} is typing`}>
120
- {avatar && <div className={styles.typingAvatar}>{avatar}</div>}
131
+ <div {...htmlProps} className={classes} role="status" aria-label={`${name} is typing`}>
132
+ {showAvatar && <div className={styles.typingAvatar}>{avatar}</div>}
121
133
  <div className={styles.typingContent}>
122
134
  <span className={styles.typingDot} />
123
135
  <span className={styles.typingDot} />
@@ -133,6 +145,7 @@ function TypingIndicator({
133
145
 
134
146
  function ConversationListRoot({
135
147
  children,
148
+ showAvatars = true,
136
149
  autoScroll = 'smart',
137
150
  onScrollTop,
138
151
  loadingHistory = false,
@@ -145,7 +158,6 @@ function ConversationListRoot({
145
158
  const containerRef = React.useRef<HTMLDivElement>(null);
146
159
  const contentRef = React.useRef<HTMLDivElement>(null);
147
160
  const isNearBottomRef = React.useRef(true);
148
- const prevChildrenCountRef = React.useRef(0);
149
161
  const userOnScroll = htmlProps.onScroll;
150
162
 
151
163
  // Check if user is near the bottom
@@ -185,31 +197,33 @@ function ConversationListRoot({
185
197
  }
186
198
  }, [checkIsNearBottom, onScrollTop, scrollTopThreshold, userOnScroll]);
187
199
 
188
- // Auto-scroll on new messages
189
- React.useEffect(() => {
190
- const childrenCount = React.Children.count(children);
191
- const hasNewMessages = childrenCount > prevChildrenCountRef.current;
192
- prevChildrenCountRef.current = childrenCount;
193
-
194
- if (!hasNewMessages) return;
195
-
196
- if (autoScroll === true) {
197
- scrollToBottom();
198
- } else if (autoScroll === 'smart' && isNearBottomRef.current) {
199
- scrollToBottom();
200
- }
201
- }, [children, autoScroll, scrollToBottom]);
202
-
203
- // Initial scroll to bottom
200
+ // Keep the reader at the newest content as the conversation grows.
201
+ //
202
+ // Child count is the wrong signal for this. A list whose messages are wrapped
203
+ // in a single element — a measured column, a virtualiser, a fragment the
204
+ // caller maps into — never changes count, so the list would pin once on mount
205
+ // and never again. And even when the count does change, the height keeps
206
+ // moving afterwards: markdown reflows, images decode, syntax highlighting
207
+ // lands, a streaming reply grows a character at a time. Rendered height is
208
+ // what actually moves the bottom of the list, so that is what this watches.
204
209
  React.useEffect(() => {
205
- if (autoScroll) {
206
- scrollToBottom('instant');
207
- }
210
+ const content = contentRef.current;
211
+ if (!content || !autoScroll) return;
212
+
213
+ // Instant, not smooth: while a reply streams this fires on every frame of
214
+ // growth, and overlapping smooth scrolls fight each other into a stutter.
215
+ // The context's scrollToBottom() stays smooth for deliberate jumps.
216
+ const observer = new ResizeObserver(() => {
217
+ if (autoScroll === true || isNearBottomRef.current) scrollToBottom('instant');
218
+ });
219
+ observer.observe(content);
220
+ return () => observer.disconnect();
208
221
  }, [autoScroll, scrollToBottom]);
209
222
 
210
- const contextValue: ConversationListContextValue = {
223
+ const contextValue = React.useMemo<ConversationListContextValue>(() => ({
211
224
  scrollToBottom,
212
- };
225
+ showAvatars,
226
+ }), [scrollToBottom, showAvatars]);
213
227
 
214
228
  const hasChildren = React.Children.count(children) > 0;
215
229
 
@@ -5,6 +5,13 @@
5
5
  "category": "display",
6
6
  "tags": ["table", "data", "grid", "list", "sorting", "tanstack"],
7
7
  "status": "stable",
8
+ "dependencies": [
9
+ {
10
+ "name": "@tanstack/react-table",
11
+ "version": ">=8.0.0",
12
+ "reason": "Optional — powers sorting, selection, and expansion; without it DataTable renders a static, non-interactive table."
13
+ }
14
+ ],
8
15
  "sourcePath": "src/components/DataTable/index.tsx",
9
16
  "exportName": "DataTable",
10
17
  "propsSummary": [
@@ -281,7 +288,7 @@
281
288
  },
282
289
  "ai": {
283
290
  "compositionPattern": "simple",
284
- "subComponents": ["Root", "Columns"],
291
+ "subComponents": ["Root", "Columns", "preload"],
285
292
  "commonPatterns": [
286
293
  "<DataTable columns={createColumns([{key:\"name\",header:\"Name\"},{key:\"status\",header:\"Status\"}])} data={[{name:\"Item 1\",status:\"Active\"}]} />"
287
294
  ]
@@ -7,15 +7,14 @@ import { ExpandIcon, SortAscIcon, SortDescIcon, SortIcon } from "./DataTable.ico
7
7
  import { DataTableSkeletonRows, useArrowKeyRowNav } from "./DataTable.support";
8
8
 
9
9
  // ============================================
10
- // Dependency (@tanstack/react-table) — lazy-required
10
+ // Dependency (@tanstack/react-table) — lazy import()
11
11
  // ============================================
12
- // `@tanstack/react-table` is an optional peer dependency. It is pulled in via a
13
- // synchronous `require()` (mirroring Chart/Editor) rather than a static ESM
14
- // import so a bundler's dep-optimizer (e.g. Vite dev) does not create a static
15
- // import edge for consumers who only use, say, `{ Button }`. `require` resolves
16
- // synchronously, so `useReactTable` (a hook) is available before the first hook
17
- // call and hook order stays stable across renders. When the peer is absent the
18
- // component throws a friendly install message on first render.
12
+ // `@tanstack/react-table` is an optional peer dependency, resolved with a
13
+ // dynamic `import()` rather than `require()`: browser ESM bundles have no
14
+ // `require`, so the synchronous shape failed even with the peer installed
15
+ // (same lazy-ESM shape as CodeBlock's shiki loader). While the module resolves
16
+ // the table renders skeleton rows; if it is genuinely missing, a static
17
+ // fallback table renders instead of throwing.
19
18
 
20
19
  type ReactTableModule = {
21
20
  flexRender: (...args: any[]) => React.ReactNode;
@@ -26,18 +25,43 @@ type ReactTableModule = {
26
25
  };
27
26
 
28
27
  let _reactTable: ReactTableModule | null = null;
29
- let _reactTableLoaded = false;
28
+ let _reactTableLoadPromise: Promise<void> | null = null;
30
29
  let _reactTableFailed = false;
31
30
 
32
- function loadReactTable() {
33
- if (_reactTableLoaded) return;
34
- _reactTableLoaded = true;
35
- try {
36
- // eslint-disable-next-line @typescript-eslint/no-require-imports
37
- _reactTable = require("@tanstack/react-table") as ReactTableModule;
38
- } catch {
39
- _reactTableFailed = true;
31
+ function loadReactTable(): Promise<void> {
32
+ if (!_reactTableLoadPromise) {
33
+ _reactTableLoadPromise = (async () => {
34
+ try {
35
+ _reactTable = (await import("@tanstack/react-table")) as unknown as ReactTableModule;
36
+ } catch {
37
+ _reactTableFailed = true;
38
+ if (process.env.NODE_ENV === "development") {
39
+ console.warn(
40
+ "[@usefragments/ui] DataTable: @tanstack/react-table is not installed. " +
41
+ "Rendering a static table without sorting, selection, or expansion. " +
42
+ "Install it with: npm install @tanstack/react-table"
43
+ );
44
+ }
45
+ }
46
+ })();
40
47
  }
48
+ return _reactTableLoadPromise;
49
+ }
50
+
51
+ /** Kick off the lazy load on mount and re-render once it settles. */
52
+ function useReactTableDeps(): "ready" | "pending" | "failed" {
53
+ const [, rerender] = React.useReducer((n: number) => n + 1, 0);
54
+ React.useEffect(() => {
55
+ if (_reactTable || _reactTableFailed) return;
56
+ let active = true;
57
+ void loadReactTable().then(() => {
58
+ if (active) rerender();
59
+ });
60
+ return () => {
61
+ active = false;
62
+ };
63
+ }, []);
64
+ return _reactTable ? "ready" : _reactTableFailed ? "failed" : "pending";
41
65
  }
42
66
 
43
67
  /** Horizontal alignment for a column's header + cells. */
@@ -156,6 +180,48 @@ function getColumnSizeStyle(column: {
156
180
  };
157
181
  }
158
182
 
183
+ function computeTableClasses(opts: {
184
+ size: "sm" | "md";
185
+ density?: DataTableDensity;
186
+ striped: boolean;
187
+ bordered: boolean;
188
+ hasExplicitColumnSizing: boolean;
189
+ className?: string;
190
+ wrapperClassName?: string;
191
+ wrapperPropsClassName?: string;
192
+ }): { rootClasses: string; wrapperClasses: string } {
193
+ const densityClass =
194
+ opts.density === "condensed"
195
+ ? styles.densityCondensed
196
+ : opts.density === "regular"
197
+ ? styles.densityRegular
198
+ : opts.density === "relaxed"
199
+ ? styles.densityRelaxed
200
+ : undefined;
201
+
202
+ const rootClasses = [
203
+ styles.table,
204
+ opts.hasExplicitColumnSizing && styles.fixedLayout,
205
+ styles[opts.size],
206
+ densityClass,
207
+ opts.striped && styles.striped,
208
+ opts.className,
209
+ ]
210
+ .filter(Boolean)
211
+ .join(" ");
212
+
213
+ const wrapperClasses = [
214
+ styles.wrapper,
215
+ opts.bordered && styles.bordered,
216
+ opts.wrapperClassName,
217
+ opts.wrapperPropsClassName,
218
+ ]
219
+ .filter(Boolean)
220
+ .join(" ");
221
+
222
+ return { rootClasses, wrapperClasses };
223
+ }
224
+
159
225
  function isInteractiveTarget(target: EventTarget | null, currentTarget: HTMLTableRowElement) {
160
226
  if (!(target instanceof Element)) return false;
161
227
 
@@ -170,7 +236,7 @@ function isInteractiveTarget(target: EventTarget | null, currentTarget: HTMLTabl
170
236
  );
171
237
  }
172
238
 
173
- function DataTableRoot<T>({
239
+ function DataTableLive<T>({
174
240
  columns: userColumns,
175
241
  data,
176
242
  getRowId,
@@ -204,15 +270,10 @@ function DataTableRoot<T>({
204
270
  "aria-describedby": ariaDescribedBy,
205
271
  ...htmlProps
206
272
  }: DataTableProps<T>) {
207
- loadReactTable();
208
- if (_reactTableFailed || !_reactTable) {
209
- throw new Error(
210
- "[@usefragments/ui] DataTable: @tanstack/react-table is not installed. " +
211
- "Install it with: npm install @tanstack/react-table"
212
- );
213
- }
273
+ // Only rendered once useReactTableDeps() reports "ready", so the module is
274
+ // guaranteed here and hook order stays stable for this component's lifetime.
214
275
  const { flexRender, getCoreRowModel, getExpandedRowModel, getSortedRowModel, useReactTable } =
215
- _reactTable;
276
+ _reactTable as ReactTableModule;
216
277
 
217
278
  const tableRef = React.useRef<HTMLTableElement>(null);
218
279
  useArrowKeyRowNav(tableRef, !!onRowClick);
@@ -298,34 +359,17 @@ function DataTableRoot<T>({
298
359
  });
299
360
 
300
361
  const isEmpty = !loading && data.length === 0;
301
- const densityClass =
302
- density === "condensed"
303
- ? styles.densityCondensed
304
- : density === "regular"
305
- ? styles.densityRegular
306
- : density === "relaxed"
307
- ? styles.densityRelaxed
308
- : undefined;
309
-
310
- const rootClasses = [
311
- styles.table,
312
- hasExplicitColumnSizing && styles.fixedLayout,
313
- styles[size],
314
- densityClass,
315
- striped && styles.striped,
316
- className,
317
- ]
318
- .filter(Boolean)
319
- .join(" ");
320
362
  const { className: wrapperPropsClassName, ...restWrapperProps } = wrapperProps ?? {};
321
- const wrapperClasses = [
322
- styles.wrapper,
323
- bordered && styles.bordered,
363
+ const { rootClasses, wrapperClasses } = computeTableClasses({
364
+ size,
365
+ density,
366
+ striped,
367
+ bordered,
368
+ hasExplicitColumnSizing,
369
+ className,
324
370
  wrapperClassName,
325
371
  wrapperPropsClassName,
326
- ]
327
- .filter(Boolean)
328
- .join(" ");
372
+ });
329
373
 
330
374
  if (isEmpty) {
331
375
  return (
@@ -542,6 +586,191 @@ function DataTableRoot<T>({
542
586
  );
543
587
  }
544
588
 
589
+ // ============================================
590
+ // Static fallback (peer resolving or missing)
591
+ // ============================================
592
+
593
+ function staticColumnSizeStyle<T>(col: DataTableColumn<T>): React.CSSProperties | undefined {
594
+ const { size, minSize, maxSize } = col;
595
+ if (size === undefined && minSize === undefined && maxSize === undefined) return undefined;
596
+ const resolved = size ?? minSize ?? maxSize;
597
+ return { width: resolved, minWidth: minSize ?? resolved, maxWidth: maxSize ?? resolved };
598
+ }
599
+
600
+ function staticCellContent<T>(col: DataTableColumn<T>, row: T, index: number): React.ReactNode {
601
+ const raw = col.accessorFn
602
+ ? col.accessorFn(row)
603
+ : col.accessorKey
604
+ ? (row as Record<string, unknown>)[col.accessorKey]
605
+ : undefined;
606
+ if (typeof col.cell === "function") {
607
+ try {
608
+ return col.cell({
609
+ row: { original: row, id: String(index), index, depth: 0 },
610
+ getValue: () => raw,
611
+ column: { id: col.id ?? col.accessorKey ?? "" },
612
+ });
613
+ } catch {
614
+ // Cell renderers that depend on TanStack row APIs fall back to the raw value.
615
+ }
616
+ }
617
+ return typeof raw === "string" || typeof raw === "number" ? raw : null;
618
+ }
619
+
620
+ /**
621
+ * Rendered while @tanstack/react-table resolves (skeleton rows) and when it is
622
+ * not installed (a plain, non-interactive table of the data). Sorting,
623
+ * selection, expansion, and row interactivity require the peer.
624
+ */
625
+ function DataTableStatic<T>({
626
+ pending,
627
+ columns,
628
+ data,
629
+ getRowId,
630
+ emptyMessage = "No data available",
631
+ emptyState,
632
+ loading = false,
633
+ skeletonRows = 6,
634
+ density,
635
+ hideHeader = false,
636
+ size = "md",
637
+ className,
638
+ caption,
639
+ captionHidden = false,
640
+ striped = false,
641
+ bordered = false,
642
+ wrapperClassName,
643
+ wrapperProps,
644
+ "aria-label": ariaLabel,
645
+ "aria-describedby": ariaDescribedBy,
646
+ // Interactive props are inert without the peer — accepted but unused.
647
+ sortable: _sortable,
648
+ sorting: _sorting,
649
+ onSortingChange: _onSortingChange,
650
+ selectable: _selectable,
651
+ showCheckbox: _showCheckbox,
652
+ rowSelection: _rowSelection,
653
+ onRowSelectionChange: _onRowSelectionChange,
654
+ onRowClick: _onRowClick,
655
+ getRowProps: _getRowProps,
656
+ getSubRows: _getSubRows,
657
+ expanded: _expanded,
658
+ onExpandedChange: _onExpandedChange,
659
+ ...htmlProps
660
+ }: DataTableProps<T> & { pending: boolean }) {
661
+ const hasExplicitColumnSizing = columns.some(
662
+ (column) =>
663
+ column.size !== undefined || column.minSize !== undefined || column.maxSize !== undefined
664
+ );
665
+ const { className: wrapperPropsClassName, ...restWrapperProps } = wrapperProps ?? {};
666
+ const { rootClasses, wrapperClasses } = computeTableClasses({
667
+ size,
668
+ density,
669
+ striped,
670
+ bordered,
671
+ hasExplicitColumnSizing,
672
+ className,
673
+ wrapperClassName,
674
+ wrapperPropsClassName,
675
+ });
676
+
677
+ const showSkeleton = pending || loading;
678
+ const isEmpty = !showSkeleton && data.length === 0;
679
+ const columnCount = Math.max(columns.length, 1);
680
+
681
+ return (
682
+ <div {...restWrapperProps} className={wrapperClasses}>
683
+ <table
684
+ {...htmlProps}
685
+ className={rootClasses}
686
+ aria-label={ariaLabel}
687
+ aria-describedby={ariaDescribedBy}
688
+ aria-busy={showSkeleton || undefined}
689
+ >
690
+ {caption && (
691
+ <caption className={captionHidden ? styles.captionHidden : styles.caption}>
692
+ {caption}
693
+ </caption>
694
+ )}
695
+ {!hideHeader && !isEmpty && (
696
+ <thead className={styles.thead}>
697
+ <tr className={styles.headerRow}>
698
+ {columns.map((col, colIndex) => (
699
+ <th
700
+ key={col.id ?? col.accessorKey ?? colIndex}
701
+ className={styles.th}
702
+ style={staticColumnSizeStyle(col)}
703
+ scope="col"
704
+ data-align={col.align}
705
+ >
706
+ <div className={styles.headerContent}>
707
+ {typeof col.header === "string" ? col.header : (col.id ?? col.accessorKey ?? "")}
708
+ </div>
709
+ </th>
710
+ ))}
711
+ </tr>
712
+ </thead>
713
+ )}
714
+ <tbody className={styles.tbody}>
715
+ {showSkeleton ? (
716
+ <DataTableSkeletonRows
717
+ rowCount={skeletonRows}
718
+ columnCount={columnCount}
719
+ rowClassName={styles.row}
720
+ cellClassName={styles.td}
721
+ barClassName={styles.skeletonBar}
722
+ />
723
+ ) : isEmpty ? (
724
+ <tr className={styles.row}>
725
+ <td className={styles.td} colSpan={columnCount}>
726
+ <div className={styles.emptyState}>
727
+ {emptyState ?? <span className={styles.emptyMessage}>{emptyMessage}</span>}
728
+ </div>
729
+ </td>
730
+ </tr>
731
+ ) : (
732
+ data.map((row, rowIndex) => {
733
+ const rowKey = getRowId?.(row) ?? rowIndex;
734
+ return (
735
+ <tr key={rowKey} className={styles.row}>
736
+ {columns.map((col, colIndex) => {
737
+ const content = staticCellContent(col, row, rowIndex);
738
+ return (
739
+ <td
740
+ key={col.id ?? col.accessorKey ?? colIndex}
741
+ className={[styles.td, col.truncate && styles.truncate]
742
+ .filter(Boolean)
743
+ .join(" ")}
744
+ data-align={col.align}
745
+ title={typeof content === "string" ? content : undefined}
746
+ style={staticColumnSizeStyle(col)}
747
+ >
748
+ {content}
749
+ </td>
750
+ );
751
+ })}
752
+ </tr>
753
+ );
754
+ })
755
+ )}
756
+ </tbody>
757
+ </table>
758
+ </div>
759
+ );
760
+ }
761
+
762
+ // ============================================
763
+ // Root — picks live vs static by dependency state
764
+ // ============================================
765
+
766
+ function DataTableRoot<T>(props: DataTableProps<T>) {
767
+ const deps = useReactTableDeps();
768
+ // Distinct component types per state: the live table calls useReactTable, so
769
+ // it must only ever mount once the module is resolved.
770
+ if (deps === "ready") return <DataTableLive {...props} />;
771
+ return <DataTableStatic {...props} pending={deps === "pending"} />;
772
+ }
773
+
545
774
  // Helper to create simple columns without TanStack's createColumnHelper
546
775
  export function createColumns<T>(
547
776
  columns: Array<{
@@ -565,4 +794,6 @@ export function createColumns<T>(
565
794
  export const DataTable = Object.assign(DataTableRoot, {
566
795
  Root: DataTableRoot,
567
796
  Columns: createColumns,
797
+ /** Start resolving @tanstack/react-table before first render (optional). */
798
+ preload: loadReactTable,
568
799
  });
@@ -5,6 +5,13 @@
5
5
  "category": "forms",
6
6
  "tags": ["date", "picker", "calendar", "range", "form", "input"],
7
7
  "status": "stable",
8
+ "dependencies": [
9
+ {
10
+ "name": "react-day-picker",
11
+ "version": ">=9.0.0",
12
+ "reason": "Optional — renders the calendar popup; the trigger and form field work without it."
13
+ }
14
+ ],
8
15
  "sourcePath": "src/components/DatePicker/index.tsx",
9
16
  "exportName": "DatePicker",
10
17
  "propsSummary": [
@@ -267,7 +274,7 @@
267
274
  },
268
275
  "ai": {
269
276
  "compositionPattern": "compound",
270
- "subComponents": ["Trigger", "Content", "Calendar", "Preset"],
277
+ "subComponents": ["Trigger", "Content", "Calendar", "Preset", "preload"],
271
278
  "requiredChildren": ["Trigger", "Content"],
272
279
  "commonPatterns": [
273
280
  "<DatePicker size=\"sm\" selected={date} onSelect={setDate}><DatePicker.Trigger placeholder=\"Pick a date\" /><DatePicker.Content><DatePicker.Calendar /></DatePicker.Content></DatePicker>",
@@ -222,10 +222,10 @@ function formatDateForHiddenInput(date?: Date): string {
222
222
  // Lazy-loaded dependency (react-day-picker)
223
223
  // ============================================
224
224
  //
225
- // Loaded on demand via require() so the barrel never statically references the
226
- // optional `react-day-picker` peer. Importing anything else from
227
- // @usefragments/ui must not drag the calendar (or its transitive `date-fns`
228
- // dependency) into a consumer's build graph. Mirrors the Chart/recharts pattern.
225
+ // Loaded on demand via import() so the barrel never statically references the
226
+ // optional `react-day-picker` peer (or its transitive `date-fns` dependency),
227
+ // and unlike require(), which browser ESM bundles do not define — the
228
+ // calendar actually resolves when the peer is installed.
229
229
 
230
230
  type DayPickerComponent = React.ComponentType<Record<string, unknown>>;
231
231
  type RdpEnum = Record<string, string>;
@@ -234,22 +234,46 @@ let _DayPicker: DayPickerComponent | null = null;
234
234
  let _UI: RdpEnum | null = null;
235
235
  let _SelectionState: RdpEnum | null = null;
236
236
  let _DayFlag: RdpEnum | null = null;
237
- let _rdpLoaded = false;
237
+ let _rdpLoadPromise: Promise<void> | null = null;
238
238
  let _rdpFailed = false;
239
239
 
240
- function loadDayPickerDeps(): void {
241
- if (_rdpLoaded) return;
242
- _rdpLoaded = true;
243
- try {
244
- // eslint-disable-next-line @typescript-eslint/no-require-imports
245
- const rdp = require("react-day-picker");
246
- _DayPicker = rdp.DayPicker as DayPickerComponent;
247
- _UI = rdp.UI as RdpEnum;
248
- _SelectionState = rdp.SelectionState as RdpEnum;
249
- _DayFlag = rdp.DayFlag as RdpEnum;
250
- } catch {
251
- _rdpFailed = true;
240
+ function loadDayPickerDeps(): Promise<void> {
241
+ if (!_rdpLoadPromise) {
242
+ _rdpLoadPromise = (async () => {
243
+ try {
244
+ const rdp = await import("react-day-picker");
245
+ _DayPicker = rdp.DayPicker as unknown as DayPickerComponent;
246
+ _UI = rdp.UI as unknown as RdpEnum;
247
+ _SelectionState = rdp.SelectionState as unknown as RdpEnum;
248
+ _DayFlag = rdp.DayFlag as unknown as RdpEnum;
249
+ } catch {
250
+ _rdpFailed = true;
251
+ if (process.env.NODE_ENV === "development") {
252
+ console.warn(
253
+ "[@usefragments/ui] DatePicker: react-day-picker is not installed. " +
254
+ "Install it with: npm install react-day-picker"
255
+ );
256
+ }
257
+ }
258
+ })();
252
259
  }
260
+ return _rdpLoadPromise;
261
+ }
262
+
263
+ /** Kick off the lazy react-day-picker load on mount and re-render once it settles. */
264
+ function useDayPickerDeps(): boolean {
265
+ const [, rerender] = React.useReducer((n: number) => n + 1, 0);
266
+ React.useEffect(() => {
267
+ if (_DayPicker || _rdpFailed) return;
268
+ let active = true;
269
+ void loadDayPickerDeps().then(() => {
270
+ if (active) rerender();
271
+ });
272
+ return () => {
273
+ active = false;
274
+ };
275
+ }, []);
276
+ return _DayPicker !== null;
253
277
  }
254
278
 
255
279
  // ============================================
@@ -321,7 +345,10 @@ const DatePickerRoot = React.forwardRef<HTMLDivElement, DatePickerProps>(functio
321
345
  ref
322
346
  ) {
323
347
  const size = useResolvedControlSize(sizeProp);
324
- // Load deps eagerly so date formatters are available in the trigger
348
+ // Warm the calendar dependency while the popover is still closed.
349
+ React.useEffect(() => {
350
+ void loadDayPickerDeps();
351
+ }, []);
325
352
  const [internalSelected, setInternalSelected] = React.useState<Date | null>(selectedProp ?? null);
326
353
  const [internalRange, setInternalRange] = React.useState<DateRange | null>(
327
354
  selectedRangeProp ?? null
@@ -567,10 +594,10 @@ function DatePickerCalendar({
567
594
  []
568
595
  );
569
596
 
570
- loadDayPickerDeps();
571
- if (_rdpFailed || !_DayPicker || !_UI) {
572
- // react-day-picker is an optional peer: render nothing rather than crash
573
- // when a consumer mounts <DatePicker> without installing the calendar dep.
597
+ const rdpReady = useDayPickerDeps();
598
+ if (!rdpReady || !_DayPicker || !_UI) {
599
+ // react-day-picker is an optional peer: render nothing while it resolves,
600
+ // and nothing at all when a consumer mounts <DatePicker> without it.
574
601
  return null;
575
602
  }
576
603
  const DayPicker = _DayPicker;
@@ -665,6 +692,8 @@ export const DatePicker = Object.assign(DatePickerRoot, {
665
692
  Content: DatePickerContent,
666
693
  Calendar: DatePickerCalendar,
667
694
  Preset: DatePickerPreset,
695
+ /** Start resolving react-day-picker before first render (optional). */
696
+ preload: loadDayPickerDeps,
668
697
  });
669
698
 
670
699
  // Re-export individual components