@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/ui",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "description": "Customizable UI components built on Base UI headless primitives",
6
6
  "author": "Conan McNicholl",
@@ -241,7 +241,7 @@
241
241
  "vite": "^6.0.0",
242
242
  "vitest": "^2.1.8",
243
243
  "vitest-axe": "^0.1.0",
244
- "@usefragments/core": "1.7.0"
244
+ "@usefragments/core": "1.7.1"
245
245
  },
246
246
  "files": [
247
247
  "src",
@@ -9,10 +9,13 @@
9
9
  "exportName": "Badge",
10
10
  "propsSummary": [
11
11
  "children: node (required)",
12
- "variant: default|success|warning|error|info|outline (default: default)",
12
+ "variant: default|success|warning|error|info|outline|label|dim (default: default)",
13
13
  "size: sm|md|lg (default: md)",
14
14
  "dot: boolean (default: false)",
15
+ "dotPulse: boolean (default: false)",
16
+ "dotColor: string",
15
17
  "icon: node",
18
+ "active: boolean (default: false)",
16
19
  "onRemove: function",
17
20
  "announce: boolean (default: false)",
18
21
  "role: union"
@@ -25,10 +28,10 @@
25
28
  },
26
29
  "variant": {
27
30
  "type": "enum",
28
- "description": "Visual style indicating severity or category",
31
+ "description": "Visual style indicating severity or category. `label` is a hairline-bordered metadata pill (pair with dotColor); `dim` is transparent filter-pill chrome (pair with active).",
29
32
  "default": "default",
30
33
  "required": false,
31
- "values": ["default", "success", "warning", "error", "info", "outline"]
34
+ "values": ["default", "success", "warning", "error", "info", "outline", "label", "dim"]
32
35
  },
33
36
  "size": {
34
37
  "type": "enum",
@@ -43,11 +46,28 @@
43
46
  "default": "false",
44
47
  "required": false
45
48
  },
49
+ "dotPulse": {
50
+ "type": "boolean",
51
+ "description": "Breathe the status dot, for a state that is still happening rather than one that has settled. Opacity only, and honours prefers-reduced-motion",
52
+ "default": "false",
53
+ "required": false
54
+ },
55
+ "dotColor": {
56
+ "type": "string",
57
+ "description": "Color for the leading status dot. Applies to variant=\"label\" (and the generic dot prop). Accepts any CSS color including CSS custom properties.",
58
+ "required": false
59
+ },
46
60
  "icon": {
47
61
  "type": "node",
48
62
  "description": "Optional icon element before the text",
49
63
  "required": false
50
64
  },
65
+ "active": {
66
+ "type": "boolean",
67
+ "description": "Marks variant=\"dim\" as the currently selected filter. Has no effect on other variants.",
68
+ "default": "false",
69
+ "required": false
70
+ },
51
71
  "onRemove": {
52
72
  "type": "function",
53
73
  "description": "Makes the badge removable. Called when X is clicked.",
@@ -149,9 +169,12 @@
149
169
  "contract": {
150
170
  "propsSummary": [
151
171
  "children: ReactNode - badge label (required)",
152
- "variant: default|success|warning|error|info|outline - visual style",
153
- "size: sm|md - badge size",
172
+ "variant: default|success|warning|error|info|outline|label|dim - visual style",
173
+ "size: sm|md|lg - badge size",
154
174
  "dot: boolean - show status dot indicator",
175
+ "dotPulse: boolean - breathe the dot for an in-progress state (respects prefers-reduced-motion)",
176
+ "dotColor: string - color for the leading status dot",
177
+ "active: boolean - marks variant=\"dim\" as the selected filter",
155
178
  "onRemove: () => void - makes badge removable",
156
179
  "announce: boolean - opt into role=\"status\" + auto status aria-label"
157
180
  ],
@@ -116,6 +116,27 @@ $fui-badge-dot-md: 6px !default;
116
116
  }
117
117
  }
118
118
 
119
+ // "Still happening", as distinct from "is in this state". A live badge and a
120
+ // settled one are otherwise the same object, and the difference between them is
121
+ // the one thing a status board is scanned for — so it is carried by the only
122
+ // motion the component has. Opacity only: nothing reflows, and a reader who has
123
+ // asked for less motion loses the breathing and keeps the badge.
124
+ .dotPulse {
125
+ animation: fui-badge-dot-pulse 1.6s ease-in-out infinite;
126
+ }
127
+
128
+ @keyframes fui-badge-dot-pulse {
129
+ 50% {
130
+ opacity: 0.35;
131
+ }
132
+ }
133
+
134
+ @media (prefers-reduced-motion: reduce) {
135
+ .dotPulse {
136
+ animation: none;
137
+ }
138
+ }
139
+
119
140
  .icon {
120
141
  display: flex;
121
142
  align-items: center;
@@ -26,6 +26,9 @@ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
26
26
  size?: "sm" | "md" | "lg";
27
27
  /** Show a status dot before the label */
28
28
  dot?: boolean;
29
+ /** Breathe the status dot, for a state that is still happening rather than
30
+ * one that has settled. Respects `prefers-reduced-motion`. */
31
+ dotPulse?: boolean;
29
32
  /** Color for the leading status dot. Applies to `variant="label"` (and the
30
33
  * generic `dot` prop). Accepts any CSS color including CSS custom properties. */
31
34
  dotColor?: string;
@@ -47,6 +50,7 @@ const BadgeRoot = React.forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
47
50
  variant = "default",
48
51
  size = "md",
49
52
  dot = false,
53
+ dotPulse = false,
50
54
  dotColor,
51
55
  icon,
52
56
  active = false,
@@ -90,7 +94,12 @@ const BadgeRoot = React.forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
90
94
  role={role ?? (announce ? "status" : undefined)}
91
95
  aria-label={effectiveAriaLabel}
92
96
  >
93
- {dot && <span className={styles.dot} aria-hidden="true" />}
97
+ {dot && (
98
+ <span
99
+ className={[styles.dot, dotPulse && styles.dotPulse].filter(Boolean).join(" ")}
100
+ aria-hidden="true"
101
+ />
102
+ )}
94
103
  {icon && (
95
104
  <span className={styles.icon} aria-hidden="true">
96
105
  {icon}
@@ -15,7 +15,7 @@
15
15
  "propsSummary": [
16
16
  "as: button|a",
17
17
  "children: node (required)",
18
- "variant: primary|secondary|ghost|link|danger|outlined|outline|icon (default: \"primary\")",
18
+ "variant: primary|secondary|ghost|link|quiet|danger|outlined|outline|icon (default: \"primary\")",
19
19
  "size: xs|sm|md|lg (default: \"md\")",
20
20
  "icon: boolean",
21
21
  "fullWidth: boolean",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "variant": {
41
41
  "type": "enum",
42
- "description": "Visual style variant (\"icon\" is a convenience alias for outlined + icon-only layout, \"link\" is accent-coloured + transparent for tertiary CTAs)",
42
+ "description": "Visual style variant (\"icon\" is a convenience alias for outlined + icon-only layout, \"link\" is accent-coloured + transparent for tertiary CTAs, \"quiet\" is a neutral text button with no box and no height floor for meta lines and footnotes)",
43
43
  "default": "\"primary\"",
44
44
  "required": false,
45
45
  "values": [
@@ -47,6 +47,7 @@
47
47
  "secondary",
48
48
  "ghost",
49
49
  "link",
50
+ "quiet",
50
51
  "danger",
51
52
  "outlined",
52
53
  "outline",
@@ -134,6 +135,11 @@
134
135
  "description": "Accent-coloured transparent button for tertiary CTAs",
135
136
  "code": "<Button variant=\"link\">View all →</Button>"
136
137
  },
138
+ {
139
+ "name": "Quiet",
140
+ "description": "Neutral text button with no box and no height floor, for meta lines and footnotes",
141
+ "code": "<Button variant=\"quiet\" size=\"xs\">main</Button>"
142
+ },
137
143
  {
138
144
  "name": "Danger",
139
145
  "description": "Destructive action requiring attention",
@@ -184,7 +190,7 @@
184
190
  ],
185
191
  "contract": {
186
192
  "propsSummary": [
187
- "variant: primary|secondary|ghost|link|danger|outlined|outline|icon (default: primary, icon = outlined + icon-only, link = accent transparent)",
193
+ "variant: primary|secondary|ghost|link|quiet|danger|outlined|outline|icon (default: primary, icon = outlined + icon-only, link = accent transparent, quiet = neutral text button, no box)",
188
194
  "size: xs|sm|md|lg (default: md, xs for inline row-action controls)",
189
195
  "disabled: boolean - disables interaction",
190
196
  "type: button|submit|reset (default: button)",
@@ -180,6 +180,40 @@
180
180
  --_button-shadow-active: none;
181
181
  }
182
182
 
183
+ // Quiet: a button that reads as text. The neutral sibling of `link` — nothing
184
+ // but the foreground changes on hover, because in a meta line, a breadcrumb or
185
+ // a footnote a box would be the loudest thing on the row for the least
186
+ // important one. It is also the one variant that drops the interactive height
187
+ // floor: there is no box to hold open, so it sits on the line it is in and
188
+ // takes its hit target from the text and the row's own padding. That makes it
189
+ // the wrong choice for a standalone action — reach for `ghost` there.
190
+ .quiet {
191
+ --_button-bg: transparent;
192
+ --_button-bg-hover: transparent;
193
+ --_button-bg-active: transparent;
194
+ --_button-border: transparent;
195
+ --_button-border-hover: transparent;
196
+ --_button-color: var(--fui-text-tertiary, #{$fui-text-tertiary});
197
+ --_button-shadow: none;
198
+ --_button-shadow-hover: none;
199
+ --_button-shadow-active: none;
200
+
201
+ min-height: 0;
202
+ padding: 0;
203
+ font-weight: var(--fui-font-weight-normal, $fui-font-weight-normal);
204
+
205
+ // Only the custom property is restated: `color` reads it on .button, so the
206
+ // foreground follows without a second `color` declaration to keep in step.
207
+ &:hover:not(:disabled):not([data-disabled]) {
208
+ --_button-color: var(--fui-text-secondary, #{$fui-text-secondary});
209
+ }
210
+
211
+ &:active:not(:disabled):not([data-disabled]),
212
+ &[data-pressed] {
213
+ --_button-color: var(--fui-text-primary, #{$fui-text-primary});
214
+ }
215
+ }
216
+
183
217
  .danger {
184
218
  // Flat danger fill — no gradient. Hover/active darken the solid colour.
185
219
  --_button-bg: var(--fui-color-danger, #{$fui-color-danger});
@@ -29,9 +29,19 @@ type ButtonBaseProps = {
29
29
  * @see https://usefragments.com/components/button#variants
30
30
  * @remarks
31
31
  * - `"link"` — accent-coloured, transparent; for tertiary CTAs like "View all →"
32
+ * - `"quiet"` — neutral text button with no box and no height floor; for meta lines and footnotes
32
33
  * - `"icon"` — convenience alias for outlined + icon-only layout
33
34
  * - `"outline"` — alias for `"outlined"` */
34
- variant?: "primary" | "secondary" | "ghost" | "link" | "danger" | "outlined" | "outline" | "icon";
35
+ variant?:
36
+ | "primary"
37
+ | "secondary"
38
+ | "ghost"
39
+ | "link"
40
+ | "quiet"
41
+ | "danger"
42
+ | "outlined"
43
+ | "outline"
44
+ | "icon";
35
45
  /** Button size. `"xs"` is for inline row-action controls (accept/dismiss, table-row icons) where `"sm"` is too tall.
36
46
  * @default "md"
37
47
  * @see https://usefragments.com/components/button#sizes */
@@ -10,6 +10,13 @@
10
10
  "recharts"
11
11
  ],
12
12
  "status": "stable",
13
+ "dependencies": [
14
+ {
15
+ "name": "recharts",
16
+ "version": ">=2.0.0 || >=3.0.0",
17
+ "reason": "Optional — powers Chart.Tooltip and Chart.Legend; both render nothing until it is installed."
18
+ }
19
+ ],
13
20
  "sourcePath": "src/components/Chart/index.tsx",
14
21
  "exportName": "ChartContainer",
15
22
  "propsSummary": [
@@ -60,20 +60,46 @@ type RechartsLegendProps = Record<string, unknown>;
60
60
 
61
61
  let _RechartsTooltip: React.ComponentType<Record<string, unknown>> | null = null;
62
62
  let _RechartsLegend: React.ComponentType<Record<string, unknown>> | null = null;
63
- let _chartLoaded = false;
63
+ let _chartLoadPromise: Promise<void> | null = null;
64
64
  let _chartFailed = false;
65
65
 
66
- function loadChartDeps() {
67
- if (_chartLoaded) return;
68
- _chartLoaded = true;
69
- try {
70
- // eslint-disable-next-line @typescript-eslint/no-require-imports
71
- const rc = require('recharts');
72
- _RechartsTooltip = rc.Tooltip as React.ComponentType<Record<string, unknown>>;
73
- _RechartsLegend = rc.Legend as React.ComponentType<Record<string, unknown>>;
74
- } catch {
75
- _chartFailed = true;
66
+ // Resolved with import() rather than require(): browser ESM bundles have no
67
+ // `require`, so the synchronous shape disabled recharts even when installed.
68
+ function loadChartDeps(): Promise<void> {
69
+ if (!_chartLoadPromise) {
70
+ _chartLoadPromise = (async () => {
71
+ try {
72
+ const rc = await import('recharts');
73
+ _RechartsTooltip = rc.Tooltip as unknown as React.ComponentType<Record<string, unknown>>;
74
+ _RechartsLegend = rc.Legend as unknown as React.ComponentType<Record<string, unknown>>;
75
+ } catch {
76
+ _chartFailed = true;
77
+ if (process.env.NODE_ENV === 'development') {
78
+ console.warn(
79
+ '[@usefragments/ui] Chart: recharts is not installed. ' +
80
+ 'Install it with: npm install recharts'
81
+ );
82
+ }
83
+ }
84
+ })();
76
85
  }
86
+ return _chartLoadPromise;
87
+ }
88
+
89
+ /** Kick off the lazy recharts load on mount and re-render once it settles. */
90
+ function useChartDeps(): boolean {
91
+ const [, rerender] = React.useReducer((n: number) => n + 1, 0);
92
+ React.useEffect(() => {
93
+ if (_RechartsTooltip || _chartFailed) return;
94
+ let active = true;
95
+ void loadChartDeps().then(() => {
96
+ if (active) rerender();
97
+ });
98
+ return () => {
99
+ active = false;
100
+ };
101
+ }, []);
102
+ return _RechartsTooltip !== null;
77
103
  }
78
104
 
79
105
 
@@ -252,7 +278,7 @@ export function ChartTooltip({
252
278
  content,
253
279
  ...props
254
280
  }: ChartTooltipProps) {
255
- loadChartDeps();
281
+ const chartReady = useChartDeps();
256
282
 
257
283
  const defaultContent = React.useCallback(
258
284
 
@@ -269,13 +295,8 @@ export function ChartTooltip({
269
295
  [indicator, hideLabel, hideIndicator, labelFormatter, valueFormatter],
270
296
  );
271
297
 
272
- if (_chartFailed || !_RechartsTooltip) {
273
- if (_chartFailed && process.env.NODE_ENV === 'development') {
274
- console.warn(
275
- '[@usefragments/ui] Chart: recharts is not installed. ' +
276
- 'Install it with: npm install recharts'
277
- );
278
- }
298
+ // Nothing until recharts resolves; nothing at all if it is not installed.
299
+ if (!chartReady || !_RechartsTooltip) {
279
300
  return null;
280
301
  }
281
302
 
@@ -330,19 +351,13 @@ type ChartLegendProps = RechartsLegendProps & {
330
351
  };
331
352
 
332
353
  export function ChartLegend({ content, ...props }: ChartLegendProps) {
333
- loadChartDeps();
354
+ const chartReady = useChartDeps();
334
355
 
335
356
  const defaultContent = (legendProps: Record<string, unknown>) => (
336
357
  <ChartLegendContent {...(legendProps as ChartLegendContentProps)} />
337
358
  );
338
359
 
339
- if (_chartFailed || !_RechartsLegend) {
340
- if (_chartFailed && process.env.NODE_ENV === 'development') {
341
- console.warn(
342
- '[@usefragments/ui] Chart: recharts is not installed. ' +
343
- 'Install it with: npm install recharts'
344
- );
345
- }
360
+ if (!chartReady || !_RechartsLegend) {
346
361
  return null;
347
362
  }
348
363
 
@@ -362,4 +377,6 @@ export const Chart = Object.assign(ChartContainer, {
362
377
  TooltipContent: ChartTooltipContent,
363
378
  Legend: ChartLegend,
364
379
  LegendContent: ChartLegendContent,
380
+ /** Start resolving recharts before first render (optional). */
381
+ preload: loadChartDeps,
365
382
  });
@@ -12,6 +12,13 @@
12
12
  "diff"
13
13
  ],
14
14
  "status": "stable",
15
+ "dependencies": [
16
+ {
17
+ "name": "shiki",
18
+ "version": ">=1.0.0",
19
+ "reason": "Optional — provides syntax highlighting; without it code renders unhighlighted."
20
+ }
21
+ ],
15
22
  "sourcePath": "src/components/CodeBlock/index.tsx",
16
23
  "exportName": "CodeBlock",
17
24
  "propsSummary": [
@@ -318,9 +318,14 @@ $fui-code-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2) !default;
318
318
  word-break: break-word;
319
319
  }
320
320
 
321
+ // `display: block` here would double-space every wrapped block: shiki emits a
322
+ // newline between each `.line`, and under `pre-wrap` that newline becomes a
323
+ // line box of its own the moment the lines stop being inline. The base rule's
324
+ // `inline-block` keeps it in the inline flow, and a full-width inline-block
325
+ // still wraps and still grows taller, which is all this variant needs.
321
326
  :global(.line) {
322
- display: block;
323
- width: auto;
327
+ display: inline-block;
328
+ width: 100%;
324
329
  }
325
330
  }
326
331
 
@@ -12,6 +12,13 @@
12
12
  "theme"
13
13
  ],
14
14
  "status": "stable",
15
+ "dependencies": [
16
+ {
17
+ "name": "react-colorful",
18
+ "version": ">=5.0.0",
19
+ "reason": "Optional — renders the visual picker surface; the swatch and hex input work without it."
20
+ }
21
+ ],
15
22
  "sourcePath": "src/components/ColorPicker/index.tsx",
16
23
  "exportName": "ColorPicker",
17
24
  "propsSummary": [
@@ -200,7 +207,8 @@
200
207
  "ai": {
201
208
  "compositionPattern": "compound",
202
209
  "subComponents": [
203
- "Root"
210
+ "Root",
211
+ "preload"
204
212
  ]
205
213
  },
206
214
  "provenance": {
@@ -7,20 +7,48 @@ import { Popover as BasePopover } from "@base-ui/react/popover";
7
7
  // Lazy-loaded dependency (react-colorful)
8
8
  // ============================================
9
9
 
10
- let _HexColorPicker: any = null;
11
- let _colorfulLoaded = false;
10
+ let _HexColorPicker: React.ComponentType<{
11
+ color: string;
12
+ onChange: (color: string) => void;
13
+ }> | null = null;
14
+ let _colorfulLoadPromise: Promise<void> | null = null;
12
15
  let _colorfulFailed = false;
13
16
 
14
- function loadColorfulDeps() {
15
- if (_colorfulLoaded) return;
16
- _colorfulLoaded = true;
17
- try {
18
- // eslint-disable-next-line @typescript-eslint/no-require-imports
19
- const rc = require("react-colorful");
20
- _HexColorPicker = rc.HexColorPicker;
21
- } catch {
22
- _colorfulFailed = true;
17
+ // Resolved with import() rather than require(): browser ESM bundles have no
18
+ // `require`, so the synchronous shape hid the picker even when installed.
19
+ function loadColorfulDeps(): Promise<void> {
20
+ if (!_colorfulLoadPromise) {
21
+ _colorfulLoadPromise = (async () => {
22
+ try {
23
+ const rc = await import("react-colorful");
24
+ _HexColorPicker = rc.HexColorPicker;
25
+ } catch {
26
+ _colorfulFailed = true;
27
+ if (process.env.NODE_ENV === "development") {
28
+ console.warn(
29
+ "[@usefragments/ui] ColorPicker: react-colorful is not installed. " +
30
+ "Install it with: npm install react-colorful"
31
+ );
32
+ }
33
+ }
34
+ })();
23
35
  }
36
+ return _colorfulLoadPromise;
37
+ }
38
+
39
+ /** Kick off the lazy react-colorful load on mount and re-render once it settles. */
40
+ function useColorfulDeps(): void {
41
+ const [, rerender] = React.useReducer((n: number) => n + 1, 0);
42
+ React.useEffect(() => {
43
+ if (_HexColorPicker || _colorfulFailed) return;
44
+ let active = true;
45
+ void loadColorfulDeps().then(() => {
46
+ if (active) rerender();
47
+ });
48
+ return () => {
49
+ active = false;
50
+ };
51
+ }, []);
24
52
  }
25
53
  import { Field } from "@base-ui/react/field";
26
54
  import { useResolvedControlSize } from "../ComponentDefaults";
@@ -75,7 +103,7 @@ const ColorPickerRoot = React.forwardRef<HTMLDivElement, ColorPickerProps>(funct
75
103
  ) {
76
104
  const size = useResolvedControlSize(sizeProp);
77
105
  const resolvedHelperText = helperText ?? description;
78
- loadColorfulDeps();
106
+ useColorfulDeps();
79
107
  const [internalValue, setInternalValue] = React.useState(defaultValue);
80
108
  const [inputValue, setInputValue] = React.useState(value ?? defaultValue);
81
109
  const displayValue = value !== undefined ? value : internalValue;
@@ -179,4 +207,6 @@ const ColorPickerRoot = React.forwardRef<HTMLDivElement, ColorPickerProps>(funct
179
207
 
180
208
  export const ColorPicker = Object.assign(ColorPickerRoot, {
181
209
  Root: ColorPickerRoot,
210
+ /** Start resolving react-colorful before first render (optional). */
211
+ preload: loadColorfulDeps,
182
212
  });
@@ -194,22 +194,13 @@
194
194
  @include surface-elevated;
195
195
  @include popup-container;
196
196
 
197
- border-color: var(--fui-form-group-border, $fui-border);
198
-
199
- // Item height derived from text-base font + vertical padding
200
- --_combobox-item-h: calc(
201
- var(--fui-font-size-sm, #{$fui-font-size-sm}) *
202
- var(--fui-line-height-normal, #{$fui-line-height-normal}) +
203
- var(--fui-space-2, #{$fui-space-2}) * 2
204
- );
197
+ @include popup-scroll-viewport;
205
198
 
199
+ border-color: var(--fui-form-group-border, $fui-border);
206
200
  min-width: var(--anchor-width);
207
- // Show N items + half-peek scroll hint (default 4.5 items)
201
+ // Show N items + half-peek scroll hint (default 4.5 items).
208
202
  // !important needed to override Base UI's inline max-height: 100%
209
- max-height: calc(
210
- var(--_combobox-item-h) * var(--fui-select-max-items, 4.5) +
211
- var(--fui-space-1, #{$fui-space-1}) * 2
212
- ) !important;
203
+ max-height: var(--_popup-scroll-max-h) !important;
213
204
  overflow-y: auto !important;
214
205
 
215
206
  // Animation
@@ -16,6 +16,7 @@
16
16
  "exportName": "ConversationList",
17
17
  "propsSummary": [
18
18
  "children: node (required)",
19
+ "showAvatars: boolean (default: true)",
19
20
  "autoScroll: union (default: smart)",
20
21
  "onScrollTop: function",
21
22
  "loadingHistory: boolean (default: false)",
@@ -29,6 +30,12 @@
29
30
  "description": "Message components",
30
31
  "required": true
31
32
  },
33
+ "showAvatars": {
34
+ "type": "boolean",
35
+ "description": "Show avatars for messages and typing indicators in this conversation",
36
+ "default": "true",
37
+ "required": false
38
+ },
32
39
  "autoScroll": {
33
40
  "type": "union",
34
41
  "description": "Auto-scroll behavior: true (always), false (never), or \"smart\" (only when near bottom)",
@@ -78,6 +85,8 @@
78
85
  ],
79
86
  "guidelines": [
80
87
  "Use autoScroll=\"smart\" for best UX (only auto-scrolls when near bottom)",
88
+ "Set showAvatars={false} for avatarless transcripts; messages remove their outer inline inset automatically",
89
+ "Conversation chrome uses neutral surfaces and the body foreground; reserve brand accent for actions or semantic state",
81
90
  "Implement onScrollTop for loading older messages",
82
91
  "ConversationList composes your onScroll handler with internal auto-scroll/history logic",
83
92
  "Provide an emptyState for new conversations",
@@ -85,7 +94,7 @@
85
94
  ],
86
95
  "accessibility": [
87
96
  "Uses proper ARIA roles for separators",
88
- "Typing indicator has aria-label",
97
+ "Typing indicator uses a status role with an aria-label",
89
98
  "Smooth scroll respects reduced motion preferences",
90
99
  "Keyboard navigation works within scrollable container"
91
100
  ]
@@ -101,6 +110,11 @@
101
110
  "description": "Messages grouped by date",
102
111
  "code": "<ConversationList>\n <ConversationList.DateSeparator date={new Date(Date.now() - 86400000)} />\n <Message role=\"user\">\n <Message.Content>A message from yesterday</Message.Content>\n </Message>\n <ConversationList.DateSeparator date={new Date()} />\n <Message role=\"assistant\">\n <Message.Content>And a message from today!</Message.Content>\n </Message>\n</ConversationList>"
103
112
  },
113
+ {
114
+ "name": "Without Avatars",
115
+ "description": "Compact transcript without message or typing-indicator avatars",
116
+ "code": "<ConversationList showAvatars={false}>\n <Message role=\"user\">\n <Message.Content>Hello!</Message.Content>\n </Message>\n <Message role=\"assistant\">\n <Message.Content>How can I help?</Message.Content>\n </Message>\n</ConversationList>"
117
+ },
104
118
  {
105
119
  "name": "With Typing Indicator",
106
120
  "description": "Shows assistant is typing",
@@ -132,6 +146,7 @@
132
146
  "contract": {
133
147
  "propsSummary": [
134
148
  "children: ReactNode - Message components",
149
+ "showAvatars: boolean - show message and typing-indicator avatars (default: true)",
135
150
  "autoScroll: boolean | \"smart\" - scroll behavior (default: \"smart\")",
136
151
  "onScrollTop: (event?) => void - callback for loading history (receives scroll event)",
137
152
  "loadingHistory: boolean - show history loading spinner",
@@ -9,6 +9,7 @@
9
9
  display: flex;
10
10
  flex-direction: column;
11
11
  flex: 1;
12
+ color: var(--fui-text-primary, $fui-text-primary);
12
13
  overflow-y: auto;
13
14
  overflow-x: hidden;
14
15
  scroll-behavior: smooth;
@@ -25,6 +26,13 @@
25
26
  gap: var(--fui-space-2, $fui-space-2);
26
27
  padding: var(--fui-space-3, $fui-space-3);
27
28
  min-height: 100%;
29
+ // Own the full height of the conversation. As a shrinkable flex item it was
30
+ // squashed to the height of the scroll port while its messages overflowed
31
+ // past it — the list still scrolled, because the overflow lands on the
32
+ // scroller, but this element reported a constant height no matter how much
33
+ // was in it. Anything measuring the conversation, auto-scroll included, was
34
+ // measuring the window rather than the content.
35
+ flex-shrink: 0;
28
36
  }
29
37
 
30
38
  // ============================================
@@ -95,6 +103,10 @@
95
103
  padding: var(--fui-space-2, $fui-space-2);
96
104
  }
97
105
 
106
+ .withoutAvatar {
107
+ padding-inline: 0;
108
+ }
109
+
98
110
  .typingAvatar {
99
111
  flex-shrink: 0;
100
112
  display: flex;