@usefragments/ui 1.3.2 → 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/README.md +3 -1
- package/dist/assets/ui.css +1261 -849
- package/dist/chart.cjs +60 -24
- package/dist/chart.js +38 -24
- package/dist/codeblock.cjs +14 -6
- package/dist/codeblock.js +14 -6
- 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/CodeBlock/index.d.ts.map +1 -1
- 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/DataTable.module.scss.cjs +35 -35
- package/dist/components/DataTable/DataTable.module.scss.js +35 -35
- 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/CodeBlock/index.tsx +38 -6
- 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/DataTable.module.scss +5 -0
- 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
|
@@ -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
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
"content"
|
|
14
14
|
],
|
|
15
15
|
"status": "stable",
|
|
16
|
+
"dependencies": [
|
|
17
|
+
{
|
|
18
|
+
"name": "@tiptap/react",
|
|
19
|
+
"version": ">=2.0.0",
|
|
20
|
+
"reason": "Optional — enables rich-text mode; without TipTap the editor falls back to a markdown textarea."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "@tiptap/starter-kit",
|
|
24
|
+
"version": ">=2.0.0",
|
|
25
|
+
"reason": "Optional — TipTap extension bundle used by rich-text mode."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "@tiptap/extension-link",
|
|
29
|
+
"version": ">=2.0.0",
|
|
30
|
+
"reason": "Optional — link formatting in rich-text mode."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
16
33
|
"sourcePath": "src/components/Editor/index.tsx",
|
|
17
34
|
"exportName": "Editor",
|
|
18
35
|
"propsSummary": [
|
|
@@ -251,7 +268,8 @@
|
|
|
251
268
|
"Separator",
|
|
252
269
|
"StatusIndicator",
|
|
253
270
|
"Content",
|
|
254
|
-
"StatusBar"
|
|
271
|
+
"StatusBar",
|
|
272
|
+
"preload"
|
|
255
273
|
]
|
|
256
274
|
},
|
|
257
275
|
"provenance": {
|
|
@@ -27,27 +27,63 @@ let _useEditor: ((config: Record<string, unknown>) => unknown) | null = null;
|
|
|
27
27
|
let _EditorContent: React.ComponentType<Record<string, unknown>> | null = null;
|
|
28
28
|
let _StarterKit: unknown = null;
|
|
29
29
|
let _LinkExtension: unknown = null;
|
|
30
|
-
let
|
|
30
|
+
let _tiptapLoadPromise: Promise<void> | null = null;
|
|
31
31
|
let _tiptapFailed = false;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
// Resolved with import() rather than require(): browser ESM bundles have no
|
|
34
|
+
// `require`, so the synchronous shape forced markdown mode even with TipTap installed.
|
|
35
|
+
function loadTipTapDeps(): Promise<void> {
|
|
36
|
+
if (!_tiptapLoadPromise) {
|
|
37
|
+
_tiptapLoadPromise = (async () => {
|
|
38
|
+
try {
|
|
39
|
+
const [tiptapReact, starterKit, linkExt] = await Promise.all([
|
|
40
|
+
import('@tiptap/react'),
|
|
41
|
+
import('@tiptap/starter-kit'),
|
|
42
|
+
import('@tiptap/extension-link'),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
_useEditor = tiptapReact.useEditor as unknown as (
|
|
46
|
+
config: Record<string, unknown>,
|
|
47
|
+
) => unknown;
|
|
48
|
+
_EditorContent = tiptapReact.EditorContent as unknown as React.ComponentType<
|
|
49
|
+
Record<string, unknown>
|
|
50
|
+
>;
|
|
51
|
+
_StarterKit =
|
|
52
|
+
(starterKit as { default?: unknown; StarterKit?: unknown }).default ??
|
|
53
|
+
(starterKit as { StarterKit?: unknown }).StarterKit ??
|
|
54
|
+
starterKit;
|
|
55
|
+
_LinkExtension =
|
|
56
|
+
(linkExt as { default?: unknown; Link?: unknown }).default ??
|
|
57
|
+
(linkExt as { Link?: unknown }).Link ??
|
|
58
|
+
linkExt;
|
|
59
|
+
} catch {
|
|
60
|
+
_tiptapFailed = true;
|
|
61
|
+
if (process.env.NODE_ENV === 'development') {
|
|
62
|
+
console.warn(
|
|
63
|
+
'[@usefragments/ui] Editor: TipTap is not installed; falling back to markdown mode. ' +
|
|
64
|
+
'Install it with: npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-link'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
50
69
|
}
|
|
70
|
+
return _tiptapLoadPromise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Kick off the lazy TipTap load on mount and re-render once it settles. */
|
|
74
|
+
function useTipTapDeps(): boolean {
|
|
75
|
+
const [, rerender] = React.useReducer((n: number) => n + 1, 0);
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if ((_useEditor && _EditorContent && _StarterKit) || _tiptapFailed) return;
|
|
78
|
+
let active = true;
|
|
79
|
+
void loadTipTapDeps().then(() => {
|
|
80
|
+
if (active) rerender();
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
active = false;
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
return !_tiptapFailed && !!_useEditor && !!_EditorContent && !!_StarterKit;
|
|
51
87
|
}
|
|
52
88
|
|
|
53
89
|
// ============================================
|
|
@@ -376,7 +412,16 @@ function countWords(text: string): number {
|
|
|
376
412
|
// Components
|
|
377
413
|
// ============================================
|
|
378
414
|
|
|
379
|
-
function EditorRoot({
|
|
415
|
+
function EditorRoot(props: EditorProps) {
|
|
416
|
+
const hasTipTap = useTipTapDeps();
|
|
417
|
+
// The rich implementation calls TipTap's useEditor hook, so an instance's
|
|
418
|
+
// mode must never change mid-lifetime. Keying by mode remounts the
|
|
419
|
+
// implementation exactly once, when the lazy TipTap load resolves.
|
|
420
|
+
return <EditorImpl key={hasTipTap ? 'rich' : 'markdown'} hasTipTap={hasTipTap} {...props} />;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function EditorImpl({
|
|
424
|
+
hasTipTap,
|
|
380
425
|
children,
|
|
381
426
|
value: controlledValue,
|
|
382
427
|
defaultValue = '',
|
|
@@ -394,7 +439,7 @@ function EditorRoot({
|
|
|
394
439
|
toolbarIcons,
|
|
395
440
|
className,
|
|
396
441
|
...htmlProps
|
|
397
|
-
}: EditorProps) {
|
|
442
|
+
}: EditorProps & { hasTipTap: boolean }) {
|
|
398
443
|
const contentRef = React.useRef<HTMLTextAreaElement>(null);
|
|
399
444
|
|
|
400
445
|
const [value, setValue] = useControllableState(
|
|
@@ -405,9 +450,6 @@ function EditorRoot({
|
|
|
405
450
|
|
|
406
451
|
const [saveStatus, setSaveStatus] = React.useState<EditorSaveStatus>('idle');
|
|
407
452
|
|
|
408
|
-
// Try loading TipTap
|
|
409
|
-
loadTipTapDeps();
|
|
410
|
-
const hasTipTap = !_tiptapFailed && _useEditor && _EditorContent && _StarterKit;
|
|
411
453
|
const mode: EditorMode = hasTipTap ? 'rich' : 'markdown';
|
|
412
454
|
|
|
413
455
|
// TipTap editor instance (only when available)
|
|
@@ -843,6 +885,8 @@ function EditorStatusBar({ showWordCount = true, showCharCount = true, className
|
|
|
843
885
|
// ============================================
|
|
844
886
|
|
|
845
887
|
export const Editor = Object.assign(EditorRoot, {
|
|
888
|
+
/** Start resolving TipTap before first render (optional). */
|
|
889
|
+
preload: loadTipTapDeps,
|
|
846
890
|
Toolbar: EditorToolbar,
|
|
847
891
|
ToolbarGroup: EditorToolbarGroup,
|
|
848
892
|
ToolbarButton: EditorToolbarButton,
|
|
@@ -9,19 +9,11 @@
|
|
|
9
9
|
@include surface-elevated;
|
|
10
10
|
@include popup-container;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
--_listbox-item-h: calc(
|
|
14
|
-
var(--fui-font-size-sm, #{$fui-font-size-sm}) *
|
|
15
|
-
var(--fui-line-height-normal, #{$fui-line-height-normal}) +
|
|
16
|
-
var(--fui-space-2, #{$fui-space-2}) * 2
|
|
17
|
-
);
|
|
12
|
+
@include popup-scroll-viewport;
|
|
18
13
|
|
|
19
14
|
min-width: 12rem;
|
|
20
|
-
// Show N items + half-peek scroll hint (default 4.5 items)
|
|
21
|
-
max-height:
|
|
22
|
-
var(--_listbox-item-h) * var(--fui-select-max-items, 4.5) +
|
|
23
|
-
var(--fui-space-1, #{$fui-space-1}) * 2
|
|
24
|
-
);
|
|
15
|
+
// Show N items + half-peek scroll hint (default 4.5 items).
|
|
16
|
+
max-height: var(--_popup-scroll-max-h);
|
|
25
17
|
overflow-y: auto;
|
|
26
18
|
border-color: var(--fui-form-group-border, $fui-border);
|
|
27
19
|
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
"chat"
|
|
13
13
|
],
|
|
14
14
|
"status": "stable",
|
|
15
|
+
"dependencies": [
|
|
16
|
+
{
|
|
17
|
+
"name": "react-markdown",
|
|
18
|
+
"version": ">=9.0.0",
|
|
19
|
+
"reason": "Optional — parses markdown; without it content renders as plain paragraphs."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "remark-gfm",
|
|
23
|
+
"version": ">=4.0.0",
|
|
24
|
+
"reason": "Optional — adds GitHub-flavored markdown (tables, strikethrough, task lists)."
|
|
25
|
+
}
|
|
26
|
+
],
|
|
15
27
|
"sourcePath": "src/components/Markdown/index.tsx",
|
|
16
28
|
"exportName": "Markdown",
|
|
17
29
|
"propsSummary": [
|
|
@@ -52,6 +64,7 @@
|
|
|
52
64
|
"Install react-markdown and remark-gfm as peer dependencies",
|
|
53
65
|
"Use the components prop to override default element rendering",
|
|
54
66
|
"Content is sanitized by react-markdown by default",
|
|
67
|
+
"The parser is loaded on demand, so the first render on a page is the plain-text fallback and parsed output arrives a tick later; call Markdown.preload() at startup to skip that frame",
|
|
55
68
|
"Falls back to plain text paragraphs if react-markdown is not installed",
|
|
56
69
|
"Standard div props (id, style, aria-*, data-*) are forwarded to the Markdown wrapper"
|
|
57
70
|
],
|
|
@@ -115,7 +128,8 @@
|
|
|
115
128
|
"ai": {
|
|
116
129
|
"compositionPattern": "compound",
|
|
117
130
|
"subComponents": [
|
|
118
|
-
"Root"
|
|
131
|
+
"Root",
|
|
132
|
+
"preload"
|
|
119
133
|
]
|
|
120
134
|
},
|
|
121
135
|
"provenance": {
|
|
@@ -8,10 +8,27 @@
|
|
|
8
8
|
.markdown {
|
|
9
9
|
@include text-base;
|
|
10
10
|
word-wrap: break-word;
|
|
11
|
-
|
|
11
|
+
// Prose colour is a hook, not a constant. Markdown is routinely rendered on a
|
|
12
|
+
// surface that carries its own foreground — a Message bubble on the accent
|
|
13
|
+
// fill, an Alert, a Callout — and a fixed grey there is unreadable rather than
|
|
14
|
+
// quiet. Those surfaces point the hook at `currentColor`; on a plain page it
|
|
15
|
+
// still resolves to the secondary ramp prose has always used.
|
|
16
|
+
color: var(--fui-markdown-text, var(--fui-text-secondary, #{$fui-text-secondary}));
|
|
12
17
|
font-size: var(--fui-font-size-md, $fui-font-size-md);
|
|
13
18
|
line-height: var(--fui-line-height-prose, $fui-line-height-prose);
|
|
14
19
|
|
|
20
|
+
// Outer spacing belongs to whatever holds the prose — a Message bubble's
|
|
21
|
+
// padding, a Card's, a page's rhythm. The block's own first and last margins
|
|
22
|
+
// would add to it, or collapse through it, and either way the container ends
|
|
23
|
+
// up looking mis-padded through no fault of its own.
|
|
24
|
+
> :first-child {
|
|
25
|
+
margin-top: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
> :last-child {
|
|
29
|
+
margin-bottom: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
// ----------------------------------------
|
|
16
33
|
// Headings
|
|
17
34
|
// ----------------------------------------
|
|
@@ -31,26 +31,45 @@ type ReactMarkdownType = React.ComponentType<{
|
|
|
31
31
|
|
|
32
32
|
let ReactMarkdown: ReactMarkdownType | null = null;
|
|
33
33
|
let remarkGfm: unknown = null;
|
|
34
|
-
let
|
|
34
|
+
let loadPromise: Promise<void> | null = null;
|
|
35
35
|
let loadFailed = false;
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Resolve react-markdown, once per page.
|
|
39
|
+
*
|
|
40
|
+
* `import()` rather than `require()`: this component is bundled into browser
|
|
41
|
+
* apps, where `require` is not defined, so a synchronous require throws a
|
|
42
|
+
* ReferenceError that this function's own catch would swallow — turning every
|
|
43
|
+
* render into the plain-text fallback with no way to tell that from a genuinely
|
|
44
|
+
* missing dependency. The same lazy-ESM shape as CodeBlock's shiki loader.
|
|
45
|
+
*/
|
|
46
|
+
function loadDeps(): Promise<void> {
|
|
47
|
+
if (!loadPromise) {
|
|
48
|
+
loadPromise = (async () => {
|
|
49
|
+
try {
|
|
50
|
+
const mod = await import('react-markdown');
|
|
51
|
+
ReactMarkdown = ((mod as { default?: ReactMarkdownType }).default ??
|
|
52
|
+
mod) as ReactMarkdownType;
|
|
53
|
+
} catch {
|
|
54
|
+
loadFailed = true;
|
|
55
|
+
if (process.env.NODE_ENV === 'development') {
|
|
56
|
+
console.warn(
|
|
57
|
+
'[@usefragments/ui] Markdown: react-markdown is not installed. ' +
|
|
58
|
+
'Install it with: npm install react-markdown remark-gfm'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
try {
|
|
65
|
+
const mod = await import('remark-gfm');
|
|
66
|
+
remarkGfm = (mod as { default?: unknown }).default ?? mod;
|
|
67
|
+
} catch {
|
|
68
|
+
// remark-gfm is optional; markdown still works without it
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
53
71
|
}
|
|
72
|
+
return loadPromise;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
// ============================================
|
|
@@ -74,17 +93,26 @@ function FallbackRenderer({ content }: { content: string }) {
|
|
|
74
93
|
|
|
75
94
|
const MarkdownRoot = React.forwardRef<HTMLDivElement, MarkdownProps>(
|
|
76
95
|
function Markdown({ content, components: componentOverrides, className, ...htmlProps }, ref) {
|
|
77
|
-
|
|
96
|
+
// The parser resolves asynchronously, so the first mount on a page renders
|
|
97
|
+
// the fallback and then swaps. Both module-level results are cached, so
|
|
98
|
+
// every later mount is synchronous; an app that knows prose is coming can
|
|
99
|
+
// skip even the first swap by calling Markdown.preload() up front.
|
|
100
|
+
const [, rerender] = React.useReducer((n: number) => n + 1, 0);
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (ReactMarkdown || loadFailed) return;
|
|
104
|
+
let active = true;
|
|
105
|
+
void loadDeps().then(() => {
|
|
106
|
+
if (active) rerender();
|
|
107
|
+
});
|
|
108
|
+
return () => {
|
|
109
|
+
active = false;
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
78
112
|
|
|
79
113
|
const classes = [styles.markdown, className].filter(Boolean).join(' ');
|
|
80
114
|
|
|
81
|
-
if (
|
|
82
|
-
if (loadFailed && process.env.NODE_ENV === 'development') {
|
|
83
|
-
console.warn(
|
|
84
|
-
'[@usefragments/ui] Markdown: react-markdown is not installed. ' +
|
|
85
|
-
'Install it with: npm install react-markdown remark-gfm'
|
|
86
|
-
);
|
|
87
|
-
}
|
|
115
|
+
if (!ReactMarkdown) {
|
|
88
116
|
return (
|
|
89
117
|
<div ref={ref} {...htmlProps} className={classes}>
|
|
90
118
|
<FallbackRenderer content={content} />
|
|
@@ -109,4 +137,10 @@ const MarkdownRoot = React.forwardRef<HTMLDivElement, MarkdownProps>(
|
|
|
109
137
|
|
|
110
138
|
export const Markdown = Object.assign(MarkdownRoot, {
|
|
111
139
|
Root: MarkdownRoot,
|
|
140
|
+
/**
|
|
141
|
+
* Start resolving the markdown parser before anything renders. Optional —
|
|
142
|
+
* for apps that know prose is imminent (a chat transcript, a docs route) and
|
|
143
|
+
* would rather not show the plain-text fallback for a frame.
|
|
144
|
+
*/
|
|
145
|
+
preload: loadDeps,
|
|
112
146
|
});
|
|
@@ -100,14 +100,24 @@ $fui-arrow-size: 10px !default;
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Check indicator (checkmark icon for checked items)
|
|
103
|
+
//
|
|
104
|
+
// Sized to the text it sits beside rather than to a 16px icon slot. Every item
|
|
105
|
+
// in a checkable menu reserves this column so labels stay aligned whether or not
|
|
106
|
+
// they are checked — which means its width is pure indent on every unchecked
|
|
107
|
+
// row, and at 1rem against 12px text it read as a margin rather than a marker.
|
|
103
108
|
.checkIndicator {
|
|
104
109
|
display: flex;
|
|
105
110
|
align-items: center;
|
|
106
111
|
justify-content: center;
|
|
107
|
-
width:
|
|
108
|
-
height:
|
|
112
|
+
width: 0.75rem;
|
|
113
|
+
height: 0.75rem;
|
|
109
114
|
flex-shrink: 0;
|
|
110
115
|
color: var(--fui-color-accent, $fui-color-accent);
|
|
116
|
+
|
|
117
|
+
svg {
|
|
118
|
+
width: 100%;
|
|
119
|
+
height: 100%;
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
// Checkbox item
|
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
"guidelines": [
|
|
81
81
|
"Always provide a role prop to determine styling",
|
|
82
82
|
"Use status prop to show message state (sending, streaming, error)",
|
|
83
|
+
"Pass avatar={null}, or set showAvatars={false} on the parent ConversationList, to hide avatars; avatarless assistant content is flush while the user's own bubble keeps compact space-1/space-2 padding",
|
|
84
|
+
"User message bubbles are capped at 80% of the available inline measure so they remain visually distinct from assistant responses",
|
|
85
|
+
"User and assistant message chrome uses neutral surface tokens with the primary text foreground; reserve brand accent for actions or semantic state",
|
|
83
86
|
"Consider showing timestamps for longer conversations",
|
|
84
87
|
"Provide hover actions for assistant messages (copy, regenerate)"
|
|
85
88
|
],
|
|
@@ -25,6 +25,25 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
.withoutAvatar {
|
|
29
|
+
padding-inline: 0;
|
|
30
|
+
|
|
31
|
+
// Avatarless assistant prose is part of the transcript rather than a bubble,
|
|
32
|
+
// so it should sit flush with the shared measure. The user's own turn remains
|
|
33
|
+
// a compact bubble to preserve authorship and alignment at a glance.
|
|
34
|
+
&.assistant {
|
|
35
|
+
.content {
|
|
36
|
+
padding: 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&.user {
|
|
41
|
+
.content {
|
|
42
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
// Role-based styles
|
|
29
48
|
.user {
|
|
30
49
|
flex-direction: row-reverse;
|
|
@@ -34,9 +53,13 @@
|
|
|
34
53
|
align-items: flex-end;
|
|
35
54
|
}
|
|
36
55
|
|
|
56
|
+
// Alignment, measure and the neutral surface distinguish the user's turn.
|
|
57
|
+
// Brand accent is reserved for actions and semantic state, not transcript
|
|
58
|
+
// chrome.
|
|
37
59
|
.content {
|
|
38
|
-
|
|
39
|
-
color: var(--fui-
|
|
60
|
+
max-inline-size: 80%;
|
|
61
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
62
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
40
63
|
}
|
|
41
64
|
}
|
|
42
65
|
|
|
@@ -98,8 +121,8 @@
|
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
.avatarUser {
|
|
101
|
-
background-color: var(--fui-
|
|
102
|
-
color: var(--fui-
|
|
124
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
125
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
.avatarAssistant {
|
|
@@ -146,6 +169,12 @@
|
|
|
146
169
|
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
147
170
|
word-wrap: break-word;
|
|
148
171
|
|
|
172
|
+
// A bubble sets its own foreground per role — primary text for the user, the
|
|
173
|
+
// danger ramp for an error. Anything rendered inside it has to take that
|
|
174
|
+
// colour rather than the page's, so a Markdown body dropped in here inherits
|
|
175
|
+
// instead of holding on to the prose grey.
|
|
176
|
+
--fui-markdown-text: currentColor;
|
|
177
|
+
|
|
149
178
|
// Prose styling for markdown content
|
|
150
179
|
p {
|
|
151
180
|
margin: 0;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import styles from './Message.module.scss';
|
|
5
|
+
import { useOptionalConversationList } from '../ConversationList';
|
|
5
6
|
import { Markdown } from '../Markdown';
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
@@ -239,6 +240,10 @@ function MessageRoot({
|
|
|
239
240
|
className,
|
|
240
241
|
...htmlProps
|
|
241
242
|
}: MessageProps) {
|
|
243
|
+
const conversationList = useOptionalConversationList();
|
|
244
|
+
const showAvatar = conversationList?.showAvatars !== false
|
|
245
|
+
&& avatar !== null
|
|
246
|
+
&& avatar !== false;
|
|
242
247
|
const contextValue: MessageContextValue = {
|
|
243
248
|
role,
|
|
244
249
|
status,
|
|
@@ -250,6 +255,7 @@ function MessageRoot({
|
|
|
250
255
|
styles[role],
|
|
251
256
|
status === 'error' && styles.error,
|
|
252
257
|
status === 'sending' && styles.sending,
|
|
258
|
+
!showAvatar && styles.withoutAvatar,
|
|
253
259
|
className,
|
|
254
260
|
].filter(Boolean).join(' ');
|
|
255
261
|
|
|
@@ -261,7 +267,7 @@ function MessageRoot({
|
|
|
261
267
|
data-role={role}
|
|
262
268
|
data-status={status}
|
|
263
269
|
>
|
|
264
|
-
{avatar !== undefined ? avatar : <MessageAvatar />}
|
|
270
|
+
{showAvatar && (avatar !== undefined ? avatar : <MessageAvatar />)}
|
|
265
271
|
<div className={styles.body}>
|
|
266
272
|
{children}
|
|
267
273
|
{actions && <MessageActions>{actions}</MessageActions>}
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"Use edgeCount to always show first/last pages",
|
|
80
80
|
"Use siblingCount to control how many pages surround the current page",
|
|
81
81
|
"Pair with Table component for data table pagination",
|
|
82
|
+
"Keep page labels on the body foreground; the current page uses a neutral active surface rather than brand accent",
|
|
82
83
|
"Pagination forwards standard nav props (id, aria-*, data-*, event handlers) to the root <nav>",
|
|
83
84
|
"Previous/Next/Item buttons compose your onClick handlers; call event.preventDefault() to stop the page change"
|
|
84
85
|
],
|
|
@@ -58,11 +58,12 @@
|
|
|
58
58
|
|
|
59
59
|
// Active page
|
|
60
60
|
.itemActive {
|
|
61
|
-
background-color: var(--fui-
|
|
62
|
-
color: var(--fui-
|
|
61
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
62
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
63
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
63
64
|
|
|
64
65
|
&:hover:not(:disabled) {
|
|
65
|
-
background-color: var(--fui-
|
|
66
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
|
|
106
107
|
@media (prefers-contrast: more) {
|
|
107
108
|
.itemActive {
|
|
108
|
-
outline: 2px solid var(--fui-
|
|
109
|
+
outline: 2px solid var(--fui-text-primary, $fui-text-primary);
|
|
109
110
|
outline-offset: -2px;
|
|
110
111
|
}
|
|
111
112
|
}
|