@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.
- package/dist/assets/ui.css +1176 -766
- package/dist/chart.cjs +60 -24
- package/dist/chart.js +38 -24
- package/dist/colorpicker.cjs +56 -9
- package/dist/colorpicker.js +34 -9
- package/dist/components/Badge/Badge.module.scss.cjs +23 -16
- package/dist/components/Badge/Badge.module.scss.js +23 -16
- package/dist/components/Badge/index.cjs +8 -1
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js +8 -1
- package/dist/components/Button/Button.module.scss.cjs +16 -13
- package/dist/components/Button/Button.module.scss.js +16 -13
- package/dist/components/Button/index.d.ts +2 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Chart/index.d.ts +3 -0
- package/dist/components/Chart/index.d.ts.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
- package/dist/components/ColorPicker/index.d.ts +4 -0
- package/dist/components/ColorPicker/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
- package/dist/components/Combobox/Combobox.module.scss.js +23 -23
- package/dist/components/Command/Command.module.scss.cjs +11 -11
- package/dist/components/Command/Command.module.scss.js +11 -11
- package/dist/components/ConversationList/ConversationList.module.scss.cjs +13 -10
- package/dist/components/ConversationList/ConversationList.module.scss.js +14 -11
- package/dist/components/ConversationList/index.cjs +26 -22
- package/dist/components/ConversationList/index.d.ts +5 -1
- package/dist/components/ConversationList/index.d.ts.map +1 -1
- package/dist/components/ConversationList/index.js +27 -23
- package/dist/components/DataTable/index.d.ts +4 -1
- package/dist/components/DataTable/index.d.ts.map +1 -1
- package/dist/components/DatePicker/index.d.ts +3 -0
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/Editor/index.d.ts +4 -1
- package/dist/components/Editor/index.d.ts.map +1 -1
- package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
- package/dist/components/Listbox/Listbox.module.scss.js +8 -8
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Markdown/index.d.ts +16 -0
- package/dist/components/Markdown/index.d.ts.map +1 -1
- package/dist/components/Menu/Menu.module.scss.cjs +14 -14
- package/dist/components/Menu/Menu.module.scss.js +14 -14
- package/dist/components/Message/Message.module.scss.cjs +23 -20
- package/dist/components/Message/Message.module.scss.js +24 -21
- package/dist/components/Message/index.cjs +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
- package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
- package/dist/components/Pagination/Pagination.module.scss.js +7 -7
- package/dist/components/Prompt/Prompt.module.scss.cjs +52 -22
- package/dist/components/Prompt/Prompt.module.scss.js +52 -22
- package/dist/components/Prompt/index.cjs +197 -2
- package/dist/components/Prompt/index.d.ts +98 -3
- package/dist/components/Prompt/index.d.ts.map +1 -1
- package/dist/components/Prompt/index.js +198 -3
- package/dist/components/Select/Select.module.scss.cjs +29 -17
- package/dist/components/Select/Select.module.scss.js +30 -18
- package/dist/components/Select/index.cjs +45 -8
- package/dist/components/Select/index.d.ts +17 -2
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +45 -8
- package/dist/components/Sidebar/Sidebar.module.scss.cjs +45 -42
- package/dist/components/Sidebar/Sidebar.module.scss.js +45 -42
- package/dist/components/Sidebar/index.cjs +2 -0
- package/dist/components/Sidebar/index.d.ts +6 -1
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +2 -0
- package/dist/components/Text/Text.module.scss.cjs +63 -51
- package/dist/components/Text/Text.module.scss.js +63 -51
- package/dist/components/Text/index.d.ts +4 -2
- package/dist/components/Text/index.d.ts.map +1 -1
- package/dist/data-table.cjs +210 -28
- package/dist/data-table.js +189 -29
- package/dist/datepicker.cjs +63 -14
- package/dist/datepicker.js +41 -14
- package/dist/editor.cjs +67 -16
- package/dist/editor.js +45 -16
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/markdown.cjs +63 -19
- package/dist/markdown.js +41 -19
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Badge/Badge.contract.json +28 -5
- package/src/components/Badge/Badge.module.scss +21 -0
- package/src/components/Badge/index.tsx +10 -1
- package/src/components/Button/Button.contract.json +9 -3
- package/src/components/Button/Button.module.scss +34 -0
- package/src/components/Button/index.tsx +11 -1
- package/src/components/Chart/Chart.contract.json +7 -0
- package/src/components/Chart/index.tsx +44 -27
- package/src/components/CodeBlock/CodeBlock.contract.json +7 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +7 -2
- package/src/components/ColorPicker/ColorPicker.contract.json +9 -1
- package/src/components/ColorPicker/index.tsx +42 -12
- package/src/components/Combobox/Combobox.module.scss +4 -13
- package/src/components/ConversationList/ConversationList.contract.json +16 -1
- package/src/components/ConversationList/ConversationList.module.scss +12 -0
- package/src/components/ConversationList/index.tsx +40 -26
- package/src/components/DataTable/DataTable.contract.json +8 -1
- package/src/components/DataTable/index.tsx +282 -51
- package/src/components/DatePicker/DatePicker.contract.json +8 -1
- package/src/components/DatePicker/index.tsx +51 -22
- package/src/components/Editor/Editor.contract.json +19 -1
- package/src/components/Editor/index.tsx +67 -23
- package/src/components/Listbox/Listbox.module.scss +3 -11
- package/src/components/Markdown/Markdown.contract.json +15 -1
- package/src/components/Markdown/Markdown.module.scss +18 -1
- package/src/components/Markdown/index.tsx +58 -24
- package/src/components/Menu/Menu.module.scss +12 -2
- package/src/components/Message/Message.contract.json +3 -0
- package/src/components/Message/Message.module.scss +33 -4
- package/src/components/Message/index.tsx +7 -1
- package/src/components/Pagination/Pagination.contract.json +1 -0
- package/src/components/Pagination/Pagination.module.scss +5 -4
- package/src/components/Prompt/Prompt.contract.json +40 -1
- package/src/components/Prompt/Prompt.module.scss +351 -4
- package/src/components/Prompt/index.tsx +324 -0
- package/src/components/Select/Select.contract.json +11 -1
- package/src/components/Select/Select.module.scss +80 -13
- package/src/components/Select/index.tsx +73 -7
- package/src/components/Sidebar/Sidebar.contract.json +1 -0
- package/src/components/Sidebar/Sidebar.module.scss +10 -0
- package/src/components/Sidebar/index.tsx +7 -0
- package/src/components/Text/Text.contract.json +7 -4
- package/src/components/Text/Text.module.scss +18 -0
- package/src/components/Text/index.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/tokens/_mixins.scss +39 -4
- 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 =
|
|
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
|
|
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
|
-
{
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
|
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
|
|
10
|
+
// Dependency (@tanstack/react-table) — lazy import()
|
|
11
11
|
// ============================================
|
|
12
|
-
// `@tanstack/react-table` is an optional peer dependency
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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
|
|
28
|
+
let _reactTableLoadPromise: Promise<void> | null = null;
|
|
30
29
|
let _reactTableFailed = false;
|
|
31
30
|
|
|
32
|
-
function loadReactTable() {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
226
|
-
// optional `react-day-picker` peer
|
|
227
|
-
//
|
|
228
|
-
//
|
|
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
|
|
237
|
+
let _rdpLoadPromise: Promise<void> | null = null;
|
|
238
238
|
let _rdpFailed = false;
|
|
239
239
|
|
|
240
|
-
function loadDayPickerDeps(): void {
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
571
|
-
if (
|
|
572
|
-
// react-day-picker is an optional peer: render nothing
|
|
573
|
-
// when a consumer mounts <DatePicker> without
|
|
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
|