@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.
Files changed (145) hide show
  1. package/README.md +3 -1
  2. package/dist/assets/ui.css +1261 -849
  3. package/dist/chart.cjs +60 -24
  4. package/dist/chart.js +38 -24
  5. package/dist/codeblock.cjs +14 -6
  6. package/dist/codeblock.js +14 -6
  7. package/dist/colorpicker.cjs +56 -9
  8. package/dist/colorpicker.js +34 -9
  9. package/dist/components/Badge/Badge.module.scss.cjs +23 -16
  10. package/dist/components/Badge/Badge.module.scss.js +23 -16
  11. package/dist/components/Badge/index.cjs +8 -1
  12. package/dist/components/Badge/index.d.ts +3 -0
  13. package/dist/components/Badge/index.d.ts.map +1 -1
  14. package/dist/components/Badge/index.js +8 -1
  15. package/dist/components/Button/Button.module.scss.cjs +16 -13
  16. package/dist/components/Button/Button.module.scss.js +16 -13
  17. package/dist/components/Button/index.d.ts +2 -1
  18. package/dist/components/Button/index.d.ts.map +1 -1
  19. package/dist/components/Chart/index.d.ts +3 -0
  20. package/dist/components/Chart/index.d.ts.map +1 -1
  21. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
  22. package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
  23. package/dist/components/CodeBlock/index.d.ts.map +1 -1
  24. package/dist/components/ColorPicker/index.d.ts +4 -0
  25. package/dist/components/ColorPicker/index.d.ts.map +1 -1
  26. package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
  27. package/dist/components/Combobox/Combobox.module.scss.js +23 -23
  28. package/dist/components/Command/Command.module.scss.cjs +11 -11
  29. package/dist/components/Command/Command.module.scss.js +11 -11
  30. package/dist/components/ConversationList/ConversationList.module.scss.cjs +13 -10
  31. package/dist/components/ConversationList/ConversationList.module.scss.js +14 -11
  32. package/dist/components/ConversationList/index.cjs +26 -22
  33. package/dist/components/ConversationList/index.d.ts +5 -1
  34. package/dist/components/ConversationList/index.d.ts.map +1 -1
  35. package/dist/components/ConversationList/index.js +27 -23
  36. package/dist/components/DataTable/DataTable.module.scss.cjs +35 -35
  37. package/dist/components/DataTable/DataTable.module.scss.js +35 -35
  38. package/dist/components/DataTable/index.d.ts +4 -1
  39. package/dist/components/DataTable/index.d.ts.map +1 -1
  40. package/dist/components/DatePicker/index.d.ts +3 -0
  41. package/dist/components/DatePicker/index.d.ts.map +1 -1
  42. package/dist/components/Editor/index.d.ts +4 -1
  43. package/dist/components/Editor/index.d.ts.map +1 -1
  44. package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
  45. package/dist/components/Listbox/Listbox.module.scss.js +8 -8
  46. package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
  47. package/dist/components/Markdown/Markdown.module.scss.js +1 -1
  48. package/dist/components/Markdown/index.d.ts +16 -0
  49. package/dist/components/Markdown/index.d.ts.map +1 -1
  50. package/dist/components/Menu/Menu.module.scss.cjs +14 -14
  51. package/dist/components/Menu/Menu.module.scss.js +14 -14
  52. package/dist/components/Message/Message.module.scss.cjs +23 -20
  53. package/dist/components/Message/Message.module.scss.js +24 -21
  54. package/dist/components/Message/index.cjs +5 -1
  55. package/dist/components/Message/index.d.ts.map +1 -1
  56. package/dist/components/Message/index.js +5 -1
  57. package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
  58. package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
  59. package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
  60. package/dist/components/Pagination/Pagination.module.scss.js +7 -7
  61. package/dist/components/Prompt/Prompt.module.scss.cjs +52 -22
  62. package/dist/components/Prompt/Prompt.module.scss.js +52 -22
  63. package/dist/components/Prompt/index.cjs +197 -2
  64. package/dist/components/Prompt/index.d.ts +98 -3
  65. package/dist/components/Prompt/index.d.ts.map +1 -1
  66. package/dist/components/Prompt/index.js +198 -3
  67. package/dist/components/Select/Select.module.scss.cjs +29 -17
  68. package/dist/components/Select/Select.module.scss.js +30 -18
  69. package/dist/components/Select/index.cjs +45 -8
  70. package/dist/components/Select/index.d.ts +17 -2
  71. package/dist/components/Select/index.d.ts.map +1 -1
  72. package/dist/components/Select/index.js +45 -8
  73. package/dist/components/Sidebar/Sidebar.module.scss.cjs +45 -42
  74. package/dist/components/Sidebar/Sidebar.module.scss.js +45 -42
  75. package/dist/components/Sidebar/index.cjs +2 -0
  76. package/dist/components/Sidebar/index.d.ts +6 -1
  77. package/dist/components/Sidebar/index.d.ts.map +1 -1
  78. package/dist/components/Sidebar/index.js +2 -0
  79. package/dist/components/Text/Text.module.scss.cjs +63 -51
  80. package/dist/components/Text/Text.module.scss.js +63 -51
  81. package/dist/components/Text/index.d.ts +4 -2
  82. package/dist/components/Text/index.d.ts.map +1 -1
  83. package/dist/data-table.cjs +210 -28
  84. package/dist/data-table.js +189 -29
  85. package/dist/datepicker.cjs +63 -14
  86. package/dist/datepicker.js +41 -14
  87. package/dist/editor.cjs +67 -16
  88. package/dist/editor.js +45 -16
  89. package/dist/index.cjs +1 -0
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +2 -1
  93. package/dist/markdown.cjs +63 -19
  94. package/dist/markdown.js +41 -19
  95. package/fragments.json +1 -1
  96. package/package.json +2 -2
  97. package/src/components/Badge/Badge.contract.json +28 -5
  98. package/src/components/Badge/Badge.module.scss +21 -0
  99. package/src/components/Badge/index.tsx +10 -1
  100. package/src/components/Button/Button.contract.json +9 -3
  101. package/src/components/Button/Button.module.scss +34 -0
  102. package/src/components/Button/index.tsx +11 -1
  103. package/src/components/Chart/Chart.contract.json +7 -0
  104. package/src/components/Chart/index.tsx +44 -27
  105. package/src/components/CodeBlock/CodeBlock.contract.json +7 -0
  106. package/src/components/CodeBlock/CodeBlock.module.scss +7 -2
  107. package/src/components/CodeBlock/index.tsx +38 -6
  108. package/src/components/ColorPicker/ColorPicker.contract.json +9 -1
  109. package/src/components/ColorPicker/index.tsx +42 -12
  110. package/src/components/Combobox/Combobox.module.scss +4 -13
  111. package/src/components/ConversationList/ConversationList.contract.json +16 -1
  112. package/src/components/ConversationList/ConversationList.module.scss +12 -0
  113. package/src/components/ConversationList/index.tsx +40 -26
  114. package/src/components/DataTable/DataTable.contract.json +8 -1
  115. package/src/components/DataTable/DataTable.module.scss +5 -0
  116. package/src/components/DataTable/index.tsx +282 -51
  117. package/src/components/DatePicker/DatePicker.contract.json +8 -1
  118. package/src/components/DatePicker/index.tsx +51 -22
  119. package/src/components/Editor/Editor.contract.json +19 -1
  120. package/src/components/Editor/index.tsx +67 -23
  121. package/src/components/Listbox/Listbox.module.scss +3 -11
  122. package/src/components/Markdown/Markdown.contract.json +15 -1
  123. package/src/components/Markdown/Markdown.module.scss +18 -1
  124. package/src/components/Markdown/index.tsx +58 -24
  125. package/src/components/Menu/Menu.module.scss +12 -2
  126. package/src/components/Message/Message.contract.json +3 -0
  127. package/src/components/Message/Message.module.scss +33 -4
  128. package/src/components/Message/index.tsx +7 -1
  129. package/src/components/Pagination/Pagination.contract.json +1 -0
  130. package/src/components/Pagination/Pagination.module.scss +5 -4
  131. package/src/components/Prompt/Prompt.contract.json +40 -1
  132. package/src/components/Prompt/Prompt.module.scss +351 -4
  133. package/src/components/Prompt/index.tsx +324 -0
  134. package/src/components/Select/Select.contract.json +11 -1
  135. package/src/components/Select/Select.module.scss +80 -13
  136. package/src/components/Select/index.tsx +73 -7
  137. package/src/components/Sidebar/Sidebar.contract.json +1 -0
  138. package/src/components/Sidebar/Sidebar.module.scss +10 -0
  139. package/src/components/Sidebar/index.tsx +7 -0
  140. package/src/components/Text/Text.contract.json +7 -4
  141. package/src/components/Text/Text.module.scss +18 -0
  142. package/src/components/Text/index.tsx +4 -2
  143. package/src/index.ts +1 -0
  144. package/src/tokens/_mixins.scss +39 -4
  145. package/src/tokens/_variables.scss +30 -0
@@ -12,9 +12,15 @@ import styles from "./Select.module.scss";
12
12
 
13
13
  export type SelectValue = string;
14
14
 
15
+ export type SelectVariant = "field" | "ghost";
16
+
15
17
  export interface SelectOption {
16
18
  value: SelectValue;
17
19
  label: string;
20
+ /** A dimmed qualifier after the label, in the trigger as well as the list —
21
+ * a tier beside a model, a path beside a filename. Use it when the label
22
+ * alone is ambiguous but the qualifier is not what you are choosing by. */
23
+ hint?: string;
18
24
  disabled?: boolean;
19
25
  }
20
26
 
@@ -58,6 +64,11 @@ export interface SelectProps extends FormFieldProps {
58
64
  /** Size variant.
59
65
  * @default "md" */
60
66
  size?: "sm" | "md" | "lg";
67
+ /** Visual treatment. `ghost` drops the field shell for a compact, borderless
68
+ * control — for toolbars and dense rows, where a bordered field reads as a
69
+ * form. Pair with `size="sm"`.
70
+ * @default "field" */
71
+ variant?: SelectVariant;
61
72
  /** Wrapper class name */
62
73
  className?: string;
63
74
  }
@@ -65,10 +76,15 @@ export interface SelectProps extends FormFieldProps {
65
76
  export interface SelectTriggerProps extends React.HTMLAttributes<HTMLButtonElement> {
66
77
  children?: React.ReactNode;
67
78
  placeholder?: string;
79
+ /** Leading adornment shown before the value — usually says what the choice is
80
+ * about, so the visible text can be the choice itself. Ignored when you pass
81
+ * your own `children`. */
82
+ icon?: React.ReactNode;
68
83
  }
69
84
 
70
85
  export interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
71
- children: React.ReactNode;
86
+ /** Omit to render the items the root's `options` prop describes. */
87
+ children?: React.ReactNode;
72
88
  sideOffset?: number;
73
89
  align?: "start" | "center" | "end";
74
90
  /** Maximum number of visible options before scrolling. Shows half of the next item as a scroll hint. @default 4 */
@@ -142,6 +158,12 @@ interface SelectContextValue {
142
158
  registerItem: (value: SelectValue, content: React.ReactNode) => void;
143
159
  unregisterItem: (value: SelectValue) => void;
144
160
  size: "sm" | "md" | "lg";
161
+ variant: SelectVariant;
162
+ /** Items built from the `options` prop, so a custom composition can put the
163
+ * trigger where it wants without having to re-render the list itself — and
164
+ * without losing whatever the root knows about an option that a bare label
165
+ * does not carry. */
166
+ optionItems: React.ReactNode;
145
167
  }
146
168
 
147
169
  const SelectContext = React.createContext<SelectContextValue>({
@@ -149,8 +171,22 @@ const SelectContext = React.createContext<SelectContextValue>({
149
171
  registerItem: () => {},
150
172
  unregisterItem: () => {},
151
173
  size: "md",
174
+ variant: "field",
175
+ optionItems: null,
152
176
  });
153
177
 
178
+ // Label and qualifier as one node, so the trigger and the list render the same
179
+ // thing and the registry has a single entry to hand back.
180
+ function renderOptionLabel(option: SelectOption): React.ReactNode {
181
+ if (!option.hint) return option.label;
182
+ return (
183
+ <>
184
+ {option.label}
185
+ <span className={styles.hint}>{option.hint}</span>
186
+ </>
187
+ );
188
+ }
189
+
154
190
  // Walk the declared Select children at render time to build a value→label map.
155
191
  // The trigger reads this so a preselected value renders its label immediately,
156
192
  // without waiting for the lazily-portaled popup to mount and register items.
@@ -194,6 +230,7 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
194
230
  helperText,
195
231
  error,
196
232
  size: sizeProp,
233
+ variant = "field",
197
234
  className,
198
235
  }: SelectProps,
199
236
  ref
@@ -235,7 +272,7 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
235
272
  () =>
236
273
  options?.map((option) => (
237
274
  <SelectItem key={option.value} value={option.value} disabled={option.disabled}>
238
- {option.label}
275
+ {renderOptionLabel(option)}
239
276
  </SelectItem>
240
277
  )) ?? null,
241
278
  [options]
@@ -267,7 +304,7 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
267
304
  // registry, which still wins for items that register dynamically.
268
305
  const declaredItems = React.useMemo(() => {
269
306
  const map = new Map<SelectValue, React.ReactNode>();
270
- options?.forEach((option) => map.set(option.value, option.label));
307
+ options?.forEach((option) => map.set(option.value, renderOptionLabel(option)));
271
308
  collectDeclaredItems(children, map);
272
309
  return map;
273
310
  }, [children, options]);
@@ -287,8 +324,19 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
287
324
  registerItem,
288
325
  unregisterItem,
289
326
  size,
327
+ variant,
328
+ optionItems,
290
329
  }),
291
- [placeholder, selectedValue, resolvedItems, registerItem, unregisterItem, size]
330
+ [
331
+ placeholder,
332
+ selectedValue,
333
+ resolvedItems,
334
+ registerItem,
335
+ unregisterItem,
336
+ size,
337
+ variant,
338
+ optionItems,
339
+ ]
292
340
  );
293
341
 
294
342
  const { helperId, errorId, hasError, errorMessage } = useFormFieldIds("select", {
@@ -297,7 +345,13 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
297
345
  error,
298
346
  });
299
347
 
300
- const wrapperClasses = [styles.wrapper, className].filter(Boolean).join(" ");
348
+ const wrapperClasses = [
349
+ styles.wrapper,
350
+ variant === "ghost" && styles.wrapperGhost,
351
+ className,
352
+ ]
353
+ .filter(Boolean)
354
+ .join(" ");
301
355
  const helperClasses = [styles.helper, hasError && styles.helperError].filter(Boolean).join(" ");
302
356
 
303
357
  return (
@@ -337,7 +391,13 @@ const SelectRoot = React.forwardRef<HTMLDivElement, SelectProps>(function Select
337
391
  );
338
392
  });
339
393
 
340
- function SelectTrigger({ children, placeholder, className, ...htmlProps }: SelectTriggerProps) {
394
+ function SelectTrigger({
395
+ children,
396
+ placeholder,
397
+ icon,
398
+ className,
399
+ ...htmlProps
400
+ }: SelectTriggerProps) {
341
401
  const context = React.useContext(SelectContext);
342
402
  const placeholderText = placeholder ?? context.placeholder;
343
403
 
@@ -345,6 +405,10 @@ function SelectTrigger({ children, placeholder, className, ...htmlProps }: Selec
345
405
  styles.trigger,
346
406
  context.size === "sm" && styles.triggerSm,
347
407
  context.size === "lg" && styles.triggerLg,
408
+ // Ghost unsets the field shell and the size mixin's box. Both are single
409
+ // classes like this one, so it wins on being declared after them in
410
+ // Select.module.scss — the one place that ordering is guaranteed.
411
+ context.variant === "ghost" && styles.triggerGhost,
348
412
  className,
349
413
  ]
350
414
  .filter(Boolean)
@@ -362,6 +426,7 @@ function SelectTrigger({ children, placeholder, className, ...htmlProps }: Selec
362
426
  <BaseSelect.Trigger {...htmlProps} className={classes}>
363
427
  {children ?? (
364
428
  <>
429
+ {icon && <span className={styles.triggerIcon}>{icon}</span>}
365
430
  <span className={styles.value}>{displayContent}</span>
366
431
  <BaseSelect.Icon className={styles.icon}>
367
432
  <ChevronDownIcon />
@@ -380,6 +445,7 @@ function SelectContent({
380
445
  maxVisibleItems,
381
446
  ...htmlProps
382
447
  }: SelectContentProps) {
448
+ const { optionItems } = React.useContext(SelectContext);
383
449
  const popupClasses = [styles.popup, className].filter(Boolean).join(" ");
384
450
 
385
451
  const popupStyle =
@@ -394,7 +460,7 @@ function SelectContent({
394
460
  <BaseSelect.Portal>
395
461
  <BaseSelect.Positioner sideOffset={sideOffset} align={align} className={styles.positioner}>
396
462
  <BaseSelect.Popup {...htmlProps} className={popupClasses} style={popupStyle}>
397
- {children}
463
+ {children ?? optionItems}
398
464
  </BaseSelect.Popup>
399
465
  </BaseSelect.Positioner>
400
466
  </BaseSelect.Portal>
@@ -129,6 +129,7 @@
129
129
  "Use SidebarProvider to enable external triggers and keyboard shortcuts",
130
130
  "Use asChild with routing libraries (Next.js Link, React Router NavLink)",
131
131
  "Sidebar.Item and Sidebar.SubItem onClick handlers receive the click event object",
132
+ "Use `multiline` on Sidebar.Item when the row is a list entry rather than a nav link — a title over a status or preview line. Without it the label is one truncating line",
132
133
  "Use Sidebar.MenuSkeleton while loading navigation data"
133
134
  ],
134
135
  "accessibility": [
@@ -388,6 +388,16 @@ $fui-sidebar-rail-indicator-height-hover: 4rem !default; // 56px
388
388
  padding-top: 0.071rem;
389
389
  }
390
390
 
391
+ // A row whose label is a block rather than a link: a title over a status line,
392
+ // a name over an address, a subject over a preview. The single-line ellipsis is
393
+ // right for navigation and wrong here, so it comes off — but the label keeps
394
+ // bounding itself, so anything inside it that truncates still truncates against
395
+ // the rail's width instead of widening it.
396
+ .itemMultiline .itemLabel {
397
+ white-space: normal;
398
+ min-width: 0;
399
+ }
400
+
391
401
  .itemBadge {
392
402
  display: flex;
393
403
  align-items: center;
@@ -144,6 +144,11 @@ export interface SidebarItemProps extends Omit<React.HTMLAttributes<HTMLElement>
144
144
  href?: string;
145
145
  /** Click handler (renders as button) */
146
146
  onClick?: React.MouseEventHandler<HTMLElement>;
147
+ /**
148
+ * Let the label be a block instead of one truncating line — a title over a
149
+ * status line, a subject over a preview. For list rows rather than nav links.
150
+ */
151
+ multiline?: boolean;
147
152
  /** Whether this item has a submenu */
148
153
  hasSubmenu?: boolean;
149
154
  /** Whether submenu is expanded (controlled) */
@@ -774,6 +779,7 @@ function SidebarItem({
774
779
  badge,
775
780
  href,
776
781
  onClick,
782
+ multiline = false,
777
783
  hasSubmenu = false,
778
784
  expanded: controlledExpanded,
779
785
  defaultExpanded = false,
@@ -796,6 +802,7 @@ function SidebarItem({
796
802
  styles.item,
797
803
  active && styles.itemActive,
798
804
  disabled && styles.itemDisabled,
805
+ multiline && styles.itemMultiline,
799
806
  hasSubmenu && styles.itemHasSubmenu,
800
807
  expanded && styles.itemExpanded,
801
808
  className,
@@ -19,7 +19,7 @@
19
19
  "variant: section-label",
20
20
  "size: 2xs|xs|sm|base|md|lg|xl|2xl|3xl|4xl",
21
21
  "weight: normal|medium|semibold|bold (default: \"normal\")",
22
- "color: primary|secondary|tertiary|muted (default: \"primary\")",
22
+ "color: primary|secondary|tertiary|muted|success|warning|danger (default: \"primary\")",
23
23
  "font: sans|mono (default: sans)",
24
24
  "truncate: boolean",
25
25
  "lineClamp: number",
@@ -102,14 +102,17 @@
102
102
  },
103
103
  "color": {
104
104
  "type": "enum",
105
- "description": "Text color (muted is an alias for tertiary)",
105
+ "description": "Text color (muted is an alias for tertiary; success/warning/danger are for a run of text that carries a state on its own \u2014 prefer Badge or Alert when the state deserves a container)",
106
106
  "default": "\"primary\"",
107
107
  "required": false,
108
108
  "values": [
109
109
  "primary",
110
110
  "secondary",
111
111
  "tertiary",
112
- "muted"
112
+ "muted",
113
+ "success",
114
+ "warning",
115
+ "danger"
113
116
  ]
114
117
  },
115
118
  "font": {
@@ -216,7 +219,7 @@
216
219
  "as: string - HTML element",
217
220
  "size: 2xs|xs|sm|base|md|lg|xl|2xl|3xl|4xl - font size (px: 10/12/14/14/15/20/24/30/36/48)",
218
221
  "weight: normal|medium|semibold|bold - font weight",
219
- "color: primary|secondary|tertiary|muted - text color (muted = tertiary)",
222
+ "color: primary|secondary|tertiary|muted|success|warning|danger - text color (muted = tertiary; semantic colours for inline state)",
220
223
  "font: sans|mono - font family",
221
224
  "truncate: boolean - enable truncation",
222
225
  "lineClamp: number - max lines"
@@ -88,6 +88,24 @@
88
88
  color: var(--fui-text-tertiary, $fui-text-tertiary);
89
89
  }
90
90
 
91
+ // Semantic colours, for a run of text that carries a state on its own — a
92
+ // count going over budget, an inline "expired", a figure in the red. Where
93
+ // there is a component for the job (Badge, Alert, Callout) use that instead:
94
+ // this is for the cases where the state is one word inside a line, and
95
+ // wrapping it in a box would be more chrome than the fact is worth. Colour is
96
+ // never the only carrier — the words still have to say it.
97
+ .color-success {
98
+ color: var(--fui-color-success, $fui-color-success);
99
+ }
100
+
101
+ .color-warning {
102
+ color: var(--fui-color-warning, $fui-color-warning);
103
+ }
104
+
105
+ .color-danger {
106
+ color: var(--fui-color-danger, $fui-color-danger);
107
+ }
108
+
91
109
  // Font family
92
110
  .mono {
93
111
  font-family: var(--fui-font-mono, $fui-font-mono);
@@ -19,9 +19,11 @@ export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'colo
19
19
  /** Font weight.
20
20
  * @default "normal" */
21
21
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
22
- /** Text color. `"muted"` is an alias for `"tertiary"`.
22
+ /** Text color. `"muted"` is an alias for `"tertiary"`. `"success" | "warning" | "danger"`
23
+ * are for a run of text that carries a state on its own; prefer Badge or Alert when the
24
+ * state deserves a container.
23
25
  * @default "primary" */
24
- color?: 'primary' | 'secondary' | 'tertiary' | 'muted';
26
+ color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'success' | 'warning' | 'danger';
25
27
  /** Font family.
26
28
  * @default "sans" */
27
29
  font?: 'sans' | 'mono';
package/src/index.ts CHANGED
@@ -567,6 +567,7 @@ export {
567
567
 
568
568
  // Chart
569
569
  export {
570
+ Chart,
570
571
  ChartContainer,
571
572
  ChartTooltip,
572
573
  ChartTooltipContent,
@@ -114,11 +114,41 @@
114
114
  @mixin popup-container {
115
115
  display: flex;
116
116
  flex-direction: column;
117
- gap: 1px;
118
- padding: var(--fui-space-1, #{$fui-space-1});
117
+ gap: var(--fui-popup-row-gap, #{$fui-popup-row-gap});
118
+ padding: var(--fui-popup-padding, #{$fui-popup-padding});
119
119
  box-shadow: var(--fui-shadow-md, #{$fui-shadow-md});
120
120
  }
121
121
 
122
+ // Scroll viewport for the popups that peek — Select, Listbox, Combobox. Sets
123
+ // `--_popup-scroll-max-h`; the consumer applies it, because Base UI puts an
124
+ // inline `max-height: 100%` on some of these popups that only `!important` beats.
125
+ //
126
+ // Row *pitch*, not row height: `popup-container` is a flex column, so N rows
127
+ // occupy N pitches less the trailing gap. (A grouped popup nests its rows one
128
+ // level down, where that gap does not reach, so the peek there runs a few
129
+ // pixels shy of half a row — visible only as a slightly smaller sliver.)
130
+ // Every term here reads the token that actually draws it — which is the point.
131
+ // Written out longhand in three separate stylesheets, this calc billed
132
+ // $fui-space-2 of vertical padding for a row `popup-item` pads with
133
+ // $fui-space-1, deriving a 46px row against a real 32px one, so all three
134
+ // popups outgrew their own content and never scrolled at all.
135
+ @mixin popup-scroll-viewport {
136
+ --_popup-row-h: calc(
137
+ var(--fui-popup-item-font-size, #{$fui-popup-item-font-size}) *
138
+ var(--fui-line-height-normal, #{$fui-line-height-normal}) +
139
+ var(--fui-popup-item-padding-block, #{$fui-popup-item-padding-block}) * 2 +
140
+ var(--fui-popup-row-gap, #{$fui-popup-row-gap})
141
+ );
142
+
143
+ // The + 2px is the 1px border `surface-elevated` draws top and bottom: these
144
+ // popups are border-box, so max-height pays for the frame before the rows.
145
+ --_popup-scroll-max-h: calc(
146
+ var(--_popup-row-h) *
147
+ var(--fui-select-max-items, 4.5) - var(--fui-popup-row-gap, #{$fui-popup-row-gap}) +
148
+ var(--fui-popup-padding, #{$fui-popup-padding}) * 2 + 2px
149
+ );
150
+ }
151
+
122
152
  // ============================================
123
153
  // Popup / menu item — canonical interaction model
124
154
  // ============================================
@@ -136,16 +166,21 @@
136
166
  // `&[data-highlighted] { @include popup-item-highlight; }` covers both. Custom popups
137
167
  // should map `&:hover, &.<active>, &[data-highlighted]` to the same mixin.
138
168
 
139
- // Dropdown/popup item base. Includes button-reset and text-base.
169
+ // Dropdown/popup item base. Includes button-reset and text-base, then steps the
170
+ // size down to the popup scale — see $fui-popup-item-font-size. Select, Listbox
171
+ // and Combobox size their scroll viewport from the same token, so read it there
172
+ // too rather than re-deriving an item height from the body size.
140
173
  @mixin popup-item {
141
174
  @include button-reset;
142
175
  @include text-base;
143
176
 
177
+ font-size: var(--fui-popup-item-font-size, #{$fui-popup-item-font-size});
144
178
  display: flex;
145
179
  align-items: center;
146
180
  gap: var(--fui-space-1, #{$fui-space-1});
147
181
  width: 100%;
148
- padding: var(--fui-space-1, #{$fui-space-1}) var(--fui-space-2, #{$fui-space-2});
182
+ padding: var(--fui-popup-item-padding-block, #{$fui-popup-item-padding-block})
183
+ var(--fui-popup-item-padding-inline, #{$fui-popup-item-padding-inline});
149
184
  border-radius: var(--fui-radius-sm, #{$fui-radius-sm});
150
185
  cursor: pointer;
151
186
  outline: none;
@@ -73,6 +73,14 @@ $fui-font-size-2xl: 2.143rem !default; // 30px (large display)
73
73
  $fui-font-size-3xl: 2.571rem !default; // 36px (h1, page title)
74
74
  $fui-font-size-4xl: 3.429rem !default; // 48px (hero display)
75
75
 
76
+ // Dropdown/popup list items — menus, selects, comboboxes, listboxes, command
77
+ // palettes. One step below chrome text: a popup is a dense list scanned against
78
+ // the trigger that opened it, and matching the trigger's own size made the list
79
+ // read heavier than the control. Named rather than inlined because Select,
80
+ // Listbox and Combobox derive their scroll height from this exact number; a
81
+ // literal in the mixin would let the height calcs drift out of step with it.
82
+ $fui-popup-item-font-size: $fui-font-size-xs !default; // 12px
83
+
76
84
  $fui-font-weight-normal: 400 !default;
77
85
  $fui-font-weight-medium: 500 !default;
78
86
  $fui-font-weight-semibold: 600 !default;
@@ -110,6 +118,20 @@ $fui-space-8: computed.$computed-space-8 !default;
110
118
  $fui-space-10: computed.$computed-space-10 !default;
111
119
  $fui-space-12: computed.$computed-space-12 !default;
112
120
 
121
+ // --------------------------------------------
122
+ // Popup geometry
123
+ // --------------------------------------------
124
+ // The insets a popup list and its rows carry. Named rather than inlined because
125
+ // Select, Listbox and Combobox size their scroll viewport from these exact
126
+ // numbers to show N rows plus a half-peek hint. Left as literals in the mixin,
127
+ // the two sides drifted: the height calcs charged $fui-space-2 of vertical
128
+ // padding for a row `popup-item` pads with $fui-space-1, so every one of those
129
+ // popups peeked further than its own "4.5 items" comment promised.
130
+ $fui-popup-padding: $fui-space-1 !default; // container inset
131
+ $fui-popup-row-gap: 1px !default; // flex gap between rows
132
+ $fui-popup-item-padding-block: $fui-space-1 !default;
133
+ $fui-popup-item-padding-inline: $fui-space-2 !default;
134
+
113
135
  // --------------------------------------------
114
136
  // Border Radius (derived from radius-style seed)
115
137
  // --------------------------------------------
@@ -397,6 +419,7 @@ $fui-code-selection-bg: color-mix(
397
419
  --fui-font-size-2xl: #{$fui-font-size-2xl};
398
420
  --fui-font-size-3xl: #{$fui-font-size-3xl};
399
421
  --fui-font-size-4xl: #{$fui-font-size-4xl};
422
+ --fui-popup-item-font-size: #{$fui-popup-item-font-size};
400
423
  --fui-font-weight-normal: #{$fui-font-weight-normal};
401
424
  --fui-font-weight-medium: #{$fui-font-weight-medium};
402
425
  --fui-font-weight-semibold: #{$fui-font-weight-semibold};
@@ -427,6 +450,13 @@ $fui-code-selection-bg: color-mix(
427
450
  --fui-space-10: #{$fui-space-10};
428
451
  --fui-space-12: #{$fui-space-12};
429
452
 
453
+ // Popup geometry — the numbers Select/Listbox/Combobox derive their scroll
454
+ // viewport from. Override these and the peek height follows.
455
+ --fui-popup-padding: #{$fui-popup-padding};
456
+ --fui-popup-row-gap: #{$fui-popup-row-gap};
457
+ --fui-popup-item-padding-block: #{$fui-popup-item-padding-block};
458
+ --fui-popup-item-padding-inline: #{$fui-popup-item-padding-inline};
459
+
430
460
  // Border radius
431
461
  --fui-radius-sm: #{$fui-radius-sm};
432
462
  --fui-radius-md: #{$fui-radius-md};