@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,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 _tiptapLoaded = false;
30
+ let _tiptapLoadPromise: Promise<void> | null = null;
31
31
  let _tiptapFailed = false;
32
32
 
33
- function loadTipTapDeps() {
34
- if (_tiptapLoaded) return;
35
- _tiptapLoaded = true;
36
- try {
37
- // eslint-disable-next-line @typescript-eslint/no-require-imports
38
- const tiptapReact = require('@tiptap/react');
39
- // eslint-disable-next-line @typescript-eslint/no-require-imports
40
- const starterKit = require('@tiptap/starter-kit');
41
- // eslint-disable-next-line @typescript-eslint/no-require-imports
42
- const linkExt = require('@tiptap/extension-link');
43
-
44
- _useEditor = tiptapReact.useEditor;
45
- _EditorContent = tiptapReact.EditorContent;
46
- _StarterKit = starterKit.default ?? starterKit.StarterKit ?? starterKit;
47
- _LinkExtension = linkExt.default ?? linkExt.Link ?? linkExt;
48
- } catch {
49
- _tiptapFailed = true;
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
- // Item height derived from text-base font + vertical padding (matches Select/Combobox)
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: calc(
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
- color: var(--fui-text-secondary, $fui-text-secondary);
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 loadAttempted = false;
34
+ let loadPromise: Promise<void> | null = null;
35
35
  let loadFailed = false;
36
36
 
37
- function loadDeps() {
38
- if (loadAttempted) return;
39
- loadAttempted = true;
40
-
41
- try {
42
- // eslint-disable-next-line @typescript-eslint/no-require-imports
43
- ReactMarkdown = require('react-markdown').default || require('react-markdown');
44
- } catch {
45
- loadFailed = true;
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
- try {
49
- // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- remarkGfm = require('remark-gfm').default || require('remark-gfm');
51
- } catch {
52
- // remark-gfm is optional; markdown still works without it
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
- loadDeps();
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 (loadFailed || !ReactMarkdown) {
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: 1rem;
108
- height: 1rem;
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
- background-color: var(--fui-color-accent, $fui-color-accent);
39
- color: var(--fui-color-on-accent, $fui-color-on-accent);
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-color-accent, $fui-color-accent);
102
- color: var(--fui-color-on-accent, $fui-color-on-accent);
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-color-accent, $fui-color-accent);
62
- color: var(--fui-color-on-accent, $fui-color-on-accent);
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-color-accent-hover, $fui-color-accent-hover);
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-color-accent, $fui-color-accent);
109
+ outline: 2px solid var(--fui-text-primary, $fui-text-primary);
109
110
  outline-offset: -2px;
110
111
  }
111
112
  }
@@ -27,7 +27,10 @@
27
27
  "maxRows: number (default: 8)",
28
28
  "autoResize: boolean (default: true)",
29
29
  "submitOnEnter: boolean (default: true)",
30
- "variant: default|fixed|sticky (default: default)"
30
+ "variant: default|fixed|sticky (default: default)",
31
+ "appearance: panel|seamless (default: panel)",
32
+ "onFiles: function",
33
+ "accept: string"
31
34
  ],
32
35
  "props": {
33
36
  "children": {
@@ -108,6 +111,26 @@
108
111
  "fixed",
109
112
  "sticky"
110
113
  ]
114
+ },
115
+ "onFiles": {
116
+ "type": "function",
117
+ "description": "Called with files added by the attach button, a paste, or a drop anywhere on the card. Providing it enables all three.",
118
+ "required": false
119
+ },
120
+ "accept": {
121
+ "type": "string",
122
+ "description": "accept filter for the attach button's picker, e.g. \"image/*\"",
123
+ "required": false
124
+ },
125
+ "appearance": {
126
+ "type": "enum",
127
+ "description": "How the card is divided up. \"panel\" keeps the toolbar as a filled footer under a rule; \"seamless\" makes the whole card one writing surface with the controls floating on it.",
128
+ "default": "panel",
129
+ "required": false,
130
+ "values": [
131
+ "panel",
132
+ "seamless"
133
+ ]
111
134
  }
112
135
  },
113
136
  "usage": {
@@ -126,6 +149,14 @@
126
149
  "Use loading state during API calls",
127
150
  "Prompt.Textarea forwards native textarea props (autoComplete, inputMode, aria-*, data-*, handlers)",
128
151
  "Prompt.Textarea composes your onChange/onKeyDown handlers with built-in submit and auto-resize behavior",
152
+ "Use appearance=\"seamless\" for agent composers where the controls should float on the writing surface rather than sit in a footer",
153
+ "Seamless controls share the dense 32px token scale with body-sized labels and standard glyphs so mixed Prompt.Select, Prompt.Attach and Prompt.Submit rows remain optically aligned",
154
+ "Seamless secondary controls keep transparent hover and open states so they read as inline tools; only Prompt.Submit carries a persistent filled shape",
155
+ "Seamless Prompt.Submit mirrors the trailing ink gutter established by Prompt.Attach at the leading edge",
156
+ "Prompt.Submit uses a neutral inverse surface by default so the primary action stays visible without consuming semantic brand color",
157
+ "Use Prompt.ModeButton for a setting you toggle and Prompt.Select for one you pick from a list",
158
+ "Wire onFiles once: the attach button, pasting a screenshot and dropping on the card all go through it",
159
+ "Render Prompt.Attachments above Prompt.Textarea — attachments are part of the message, not a setting on it",
129
160
  "Consider showing usage/token limits for AI contexts"
130
161
  ],
131
162
  "accessibility": [
@@ -150,6 +181,11 @@
150
181
  "description": "Shows token usage indicator",
151
182
  "code": "<Prompt onSubmit={(value) => console.log(value)}>\n <Prompt.Textarea />\n <Prompt.Toolbar>\n <Prompt.Actions>\n <Prompt.ActionButton aria-label=\"Add attachment\">\n +\n </Prompt.ActionButton>\n <Prompt.ModeButton active>Auto</Prompt.ModeButton>\n </Prompt.Actions>\n <Prompt.Info>\n <Prompt.Usage>52% used</Prompt.Usage>\n <Prompt.Submit />\n </Prompt.Info>\n </Prompt.Toolbar>\n</Prompt>"
152
183
  },
184
+ {
185
+ "name": "Agent Composer",
186
+ "description": "Seamless surface with the scope of the run chosen on the toolbar",
187
+ "code": "<Prompt\n appearance=\"seamless\"\n submitOnEnter={false}\n minRows={3}\n onSubmit={(value) => console.log(value)}\n>\n <Prompt.Textarea placeholder=\"What should be true when this is done?\" />\n <Prompt.Toolbar>\n <Prompt.Actions>\n <Prompt.Select\n aria-label=\"Model\"\n defaultValue=\"claude-opus-4-8\"\n options={[{ value: 'claude-opus-4-8', label: 'Opus 4.8' }, { value: 'claude-sonnet-5', label: 'Sonnet 5' }]}\n />\n <Prompt.ModeButton>Plan</Prompt.ModeButton>\n </Prompt.Actions>\n <Prompt.Info>\n <Prompt.Submit />\n </Prompt.Info>\n </Prompt.Toolbar>\n</Prompt>"
188
+ },
153
189
  {
154
190
  "name": "Loading State",
155
191
  "description": "During API submission",
@@ -200,6 +236,9 @@
200
236
  "Info",
201
237
  "ActionButton",
202
238
  "ModeButton",
239
+ "Select",
240
+ "Attach",
241
+ "Attachments",
203
242
  "Usage",
204
243
  "Submit"
205
244
  ]