@12-apps/ui 1.22.0 → 2.0.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 (88) hide show
  1. package/package.json +3 -1
  2. package/src/components/data-display/Alert/Alert.types.ts +2 -1
  3. package/src/components/data-display/Avatar/Avatar.types.ts +10 -2
  4. package/src/components/data-display/Badge/Badge.types.ts +3 -2
  5. package/src/components/data-display/Carousel/Carousel.parts.tsx +12 -10
  6. package/src/components/data-display/Carousel/Carousel.styles.ts +4 -3
  7. package/src/components/data-display/Carousel/Carousel.tsx +2 -1
  8. package/src/components/data-display/Carousel/Carousel.types.ts +7 -6
  9. package/src/components/data-display/Chart/Chart.tsx +2 -1
  10. package/src/components/data-display/Chart/Chart.types.ts +3 -2
  11. package/src/components/data-display/Chart/ChartSpec.types.ts +6 -7
  12. package/src/components/data-display/Chart/SpecChart.tsx +2 -1
  13. package/src/components/data-display/Chart/chart-internals.ts +3 -2
  14. package/src/components/data-display/Chart/chart-spec.ts +11 -1
  15. package/src/components/data-display/Chip/Chip.tsx +23 -3
  16. package/src/components/data-display/Chip/Chip.types.ts +37 -3
  17. package/src/components/data-display/Chip/chip.md +7 -3
  18. package/src/components/data-display/Chip/index.ts +1 -1
  19. package/src/components/data-display/DataViews/DataViewsGrid.tsx +10 -0
  20. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +3 -0
  21. package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +37 -3
  22. package/src/components/data-display/DataViews/data-views-day-input.tsx +22 -3
  23. package/src/components/data-display/DataViews/data-views-display-panel.tsx +4 -2
  24. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +113 -62
  25. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +17 -1
  26. package/src/components/data-display/DataViews/data-views-more-fields.tsx +16 -23
  27. package/src/components/data-display/DataViews/data-views-more-filters.tsx +7 -2
  28. package/src/components/data-display/DataViews/data-views-number-input.tsx +14 -4
  29. package/src/components/data-display/DataViews/data-views-overflow-costs.ts +87 -16
  30. package/src/components/data-display/DataViews/data-views-overflow.ts +104 -71
  31. package/src/components/data-display/DataViews/index.ts +4 -0
  32. package/src/components/data-display/DataViews/list-card-rails.tsx +29 -0
  33. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +8 -5
  34. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +2 -1
  35. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +2 -1
  36. package/src/components/data-display/LoadingState/LoadingState.types.ts +2 -1
  37. package/src/components/data-display/Progress/Progress.types.ts +3 -2
  38. package/src/components/data-display/Sheet/Sheet.body.tsx +2 -1
  39. package/src/components/data-display/Sheet/Sheet.styles.ts +3 -2
  40. package/src/components/data-display/Sheet/Sheet.types.ts +7 -2
  41. package/src/components/data-display/Stepper/Stepper.types.ts +2 -1
  42. package/src/components/data-display/Table/Table.styles.ts +1 -1
  43. package/src/components/data-display/Table/Table.types.ts +3 -1
  44. package/src/components/data-display/Tooltip/Tooltip.types.ts +2 -1
  45. package/src/components/feedback/Dialog/Dialog.types.ts +2 -1
  46. package/src/components/feedback/Modal/Modal.types.ts +2 -1
  47. package/src/components/form/Button/Button.types.ts +3 -2
  48. package/src/components/form/Command/Command.styles.ts +8 -7
  49. package/src/components/form/Command/Command.types.ts +3 -2
  50. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +2 -1
  51. package/src/components/form/Input/Input.types.ts +2 -1
  52. package/src/components/form/InputOTP/InputOTP.types.ts +3 -2
  53. package/src/components/form/Label/Label.styles.ts +8 -5
  54. package/src/components/form/Label/Label.tsx +1 -1
  55. package/src/components/form/Label/Label.types.ts +3 -2
  56. package/src/components/form/Menubar/Menubar.styles.ts +10 -8
  57. package/src/components/form/Menubar/Menubar.tsx +2 -1
  58. package/src/components/form/Menubar/Menubar.types.ts +7 -6
  59. package/src/components/form/RadioGroup/RadioGroup.types.ts +3 -2
  60. package/src/components/form/Select/Select.tsx +13 -1
  61. package/src/components/form/Select/Select.types.ts +5 -2
  62. package/src/components/form/Slider/Slider.types.ts +3 -2
  63. package/src/components/form/Switch/Switch.types.ts +3 -2
  64. package/src/components/form/Textarea/Textarea.types.ts +3 -2
  65. package/src/components/form/Toggle/Toggle.types.ts +3 -2
  66. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +3 -2
  67. package/src/components/layout/Separator/Separator.types.ts +2 -1
  68. package/src/components/layout/Spacer/Spacer.types.ts +2 -1
  69. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
  70. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +7 -2
  71. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +3 -2
  72. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +1 -0
  73. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +12 -2
  74. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +7 -2
  75. package/src/components/navigation/Pagination/Pagination.tsx +3 -1
  76. package/src/components/navigation/Pagination/Pagination.types.ts +5 -3
  77. package/src/components/navigation/Tabs/Tabs.types.ts +3 -2
  78. package/src/components/typography/Blockquote/Blockquote.types.ts +2 -1
  79. package/src/components/typography/Code/Code.types.ts +2 -1
  80. package/src/components/typography/Heading/Heading.types.ts +2 -1
  81. package/src/components/typography/Paragraph/Paragraph.types.ts +3 -2
  82. package/src/components/typography/Text/Text.types.ts +3 -2
  83. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +2 -1
  84. package/src/components/utility/Overflow/index.ts +6 -0
  85. package/src/components/utility/Overflow/overflow.ts +125 -0
  86. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +3 -2
  87. package/src/components/utility/index.ts +1 -0
  88. package/src/tokens/scales.ts +153 -0
@@ -1,8 +1,17 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useRef } from "react";
4
4
 
5
- import { estimateWidth, GAP, isRangeSet, pillText, RESERVED } from "./data-views-overflow-costs";
5
+ import { splitToFit, useMeasuredWidth } from "../../utility/Overflow";
6
+ import {
7
+ estimateWidth,
8
+ furnitureCost,
9
+ GAP,
10
+ isRangeSet,
11
+ pillText,
12
+ RESERVED,
13
+ rightClusterCost,
14
+ } from "./data-views-overflow-costs";
6
15
  import type { FilterFieldConfig, RangeFieldConfig, RangeValue } from "./data-views-types";
7
16
 
8
17
  /**
@@ -98,26 +107,6 @@ export function toOverflowFields<T extends Record<string, unknown>>(
98
107
  }
99
108
 
100
109
 
101
- /** Observe the bar's width. Returns 0 until the first measurement lands. */
102
- function useBarWidth(): { barRef: React.RefObject<HTMLDivElement | null>; width: number } {
103
- const barRef = useRef<HTMLDivElement | null>(null);
104
- const [width, setWidth] = useState(0);
105
- useEffect(() => {
106
- const element = barRef.current;
107
- // jsdom has no ResizeObserver, and neither does an SSR pass — with no
108
- // measurement the hook keeps every control inline, which is the honest
109
- // fallback: nothing is hidden behind a menu nobody can see.
110
- if (!element || typeof ResizeObserver === "undefined") return undefined;
111
- const observer = new ResizeObserver((entries) => {
112
- const entry = entries[0];
113
- if (entry) setWidth(entry.contentRect.width);
114
- });
115
- observer.observe(element);
116
- return () => observer.disconnect();
117
- }, []);
118
- return { barRef, width };
119
- }
120
-
121
110
  /** Is this control carrying a value the operator set? */
122
111
  function isActiveField<T extends Record<string, unknown>>(
123
112
  field: OverflowField<T>,
@@ -154,24 +143,16 @@ function splitFilters<T extends Record<string, unknown>>(
154
143
  pills: Record<string, string[]>,
155
144
  ranges: Record<string, RangeValue>,
156
145
  width: number,
146
+ /** Search + counter + right cluster + chrome, at the rung being considered. */
147
+ furniture: number,
148
+ /** Is the bar already down to icons? "Mais" shrinks with everything else. */
149
+ compact: boolean,
157
150
  ): Split<T> {
158
- const none: OverflowField<T>[] = [];
159
- const widthOf = (field: OverflowField<T>): number => fieldWidth(field, pills, ranges);
160
- const cost = (fields: OverflowField<T>[]): number =>
161
- fields.reduce((sum, field) => sum + widthOf(field) + GAP, 0);
162
151
  const active = all.filter((field) => isActiveField(field, pills, ranges));
163
152
  const idle = all.filter((field) => !isActiveField(field, pills, ranges));
164
153
 
165
154
  // "Limpar" rides the end of the cluster whenever anything is applied.
166
155
  const clearCost = active.length > 0 ? RESERVED.clearAll + GAP : 0;
167
- let available =
168
- width - RESERVED.search - RESERVED.counter - RESERVED.right - RESERVED.chrome - clearCost;
169
- if (cost(all) <= available) {
170
- return { inline: all, overflow: none, used: cost(all) + clearCost };
171
- }
172
-
173
- // There IS an overflow, so its button now costs room too.
174
- available -= RESERVED.overflowButton;
175
156
  // APPLIED FIRST, BUT NOT EXEMPT.
176
157
  //
177
158
  // Applied controls take the visible slots ahead of idle ones — that part was
@@ -184,22 +165,21 @@ function splitFilters<T extends Record<string, unknown>>(
184
165
  // vanished silently: the trigger goes to the applied tone and says how many
185
166
  // are in there (see `MoreTrigger`), which is the same signal the pill itself
186
167
  // was carrying. A control you scroll off-screen carries no signal at all.
187
- const keep = new Set<string>();
188
- for (const field of [...active, ...idle]) {
189
- const next = widthOf(field) + GAP;
190
- if (next > available) continue;
191
- available -= next;
192
- keep.add(field.id);
193
- }
194
- // Rendered in the DECLARED order, not the applied-first one: which controls
195
- // are visible may change with the width, but the ones that stay must not
196
- // reshuffle under the operator's cursor.
197
- const inline = all.filter((field) => keep.has(field.id));
198
- return {
199
- inline,
200
- overflow: all.filter((field) => !keep.has(field.id)),
201
- used: cost(inline) + RESERVED.overflowButton + clearCost,
202
- };
168
+ //
169
+ // The keep-what-fits loop itself is `splitToFit` (`utility/Overflow`) — the
170
+ // one part of this that is not filter-shaped, and the part a second cluster
171
+ // in the design system would otherwise have had to reimplement.
172
+ const split = splitToFit(all, {
173
+ widthOf: (field) => fieldWidth(field, pills, ranges),
174
+ keyOf: (field) => field.id,
175
+ gap: GAP,
176
+ available: width - furniture - clearCost,
177
+ // The overflow button at the width it will actually have, which on a phone
178
+ // is the icon-and-badge one.
179
+ overflowCost: compact ? RESERVED.overflowButtonCompact : RESERVED.overflowButton,
180
+ priority: [...active, ...idle],
181
+ });
182
+ return { ...split, used: split.used + clearCost };
203
183
  }
204
184
 
205
185
  /**
@@ -224,6 +204,8 @@ function computeSplit<T extends Record<string, unknown>>(
224
204
  pills: Record<string, string[]>,
225
205
  ranges: Record<string, RangeValue>,
226
206
  width: number,
207
+ /** Does the host render "Exportar" beside "Exibir"? Half the cluster if not. */
208
+ hasExport: boolean,
227
209
  ): Omit<OverflowSplit<T>, "barRef"> {
228
210
  // Unmeasured (SSR, or jsdom without a ResizeObserver) ⇒ degrade nothing.
229
211
  if (width === 0) {
@@ -237,12 +219,47 @@ function computeSplit<T extends Record<string, unknown>>(
237
219
  searchTakeover: false,
238
220
  };
239
221
  }
240
- const { inline, overflow, used } = splitFilters(all, pills, ranges, width);
222
+ // Pass 1 prices the furniture at its widest — no rung has been taken yet.
223
+ let split = splitFilters(all, pills, ranges, width, furnitureCost(UNCOLLAPSED, hasExport), false);
224
+ let flags = ladderFlags(split, width, hasExport);
225
+ // RE-SPEND WHAT THE LATER RUNGS FREED.
226
+ //
227
+ // Rungs 2/4/5 turn 200 + 96 + 216 of furniture into 44 + 0 + 140, and until
228
+ // this loop existed nobody handed those ~330px back to the filters: the
229
+ // split was decided once, against the uncollapsed row, so a phone shed every
230
+ // control and then sat with a band of empty bar beside "Mais 5".
231
+ //
232
+ // Settles rather than oscillates, and cannot overfill: a cheaper furniture
233
+ // can only give the filters MORE room, more filter width can only push the
234
+ // ladder FURTHER down, and a further rung can only make the furniture
235
+ // cheaper again — so the budget decreases monotonically and the loop exits
236
+ // the moment it stops moving. The bound is a backstop, not the exit.
237
+ let budget = furnitureCost(UNCOLLAPSED, hasExport);
238
+ for (let pass = 0; pass < 3; pass += 1) {
239
+ const freed = furnitureCost(flags, hasExport);
240
+ if (freed >= budget) break;
241
+ budget = freed;
242
+ split = splitFilters(all, pills, ranges, width, budget, flags.compactControls);
243
+ flags = ladderFlags(split, width, hasExport);
244
+ }
245
+ return { inline: split.inline, overflow: split.overflow, ...flags };
246
+ }
247
+
248
+ /** The widest the furniture ever is — every rung still untaken. */
249
+ const UNCOLLAPSED = { searchCollapsed: false, counterHidden: false, compactControls: false };
250
+
251
+ /** Rungs 2 and 4-6, priced against what the filters actually took. */
252
+ function ladderFlags<T extends Record<string, unknown>>(
253
+ split: Split<T>,
254
+ width: number,
255
+ hasExport: boolean,
256
+ ): Omit<OverflowSplit<T>, "barRef" | "inline" | "overflow"> {
257
+ const { overflow, used } = split;
241
258
  const base = width - used - RESERVED.chrome;
242
259
 
243
260
  // Would the search still make its minimum with the labels on?
244
- const compactControls = base - RESERVED.counter - RESERVED.right < RESERVED.search;
245
- const rightCost = compactControls ? RESERVED.rightCompact : RESERVED.right;
261
+ const compactControls = base - RESERVED.counter - rightClusterCost(hasExport, false) < RESERVED.search;
262
+ const rightCost = rightClusterCost(hasExport, compactControls);
246
263
  // …and with them off?
247
264
  const searchCollapsed = base - RESERVED.counter - rightCost < RESERVED.search;
248
265
  // Everything has collapsed and the row STILL overflows. The controls cannot
@@ -258,26 +275,29 @@ function computeSplit<T extends Record<string, unknown>>(
258
275
  // on there BEING an overflow, because with no panel the footer does not
259
276
  // exist and dropping this would strand the operator.
260
277
  const clearAllHidden = counterHidden && overflow.length > 0;
261
- // What the keyword box would get if the operator expanded the magnifier and
262
- // everything else stayed put. `searchCollapsed` is a different question — it
263
- // asks whether the box fits at its PREFERRED 200px, and is deliberately
264
- // pessimistic — so reusing it to decide the takeover evicted the filters on
265
- // screens where the box had plenty of room to simply shrink.
266
- const searchBoxRoom = base - (counterHidden ? 0 : RESERVED.counter) - rightCost;
278
+ // What the keyword box would get if the operator expanded the magnifier.
279
+ // `searchCollapsed` is a different question — it asks whether the box fits at
280
+ // its PREFERRED 200px, and is deliberately pessimistic — so reusing it to
281
+ // decide the takeover evicted the filters on screens where the box had plenty
282
+ // of room to simply shrink.
283
+ //
284
+ // Asked with the inline filters SHED, because shedding them is exactly what
285
+ // expanding the box does: the row re-splits and they go behind "Mais".
286
+ // Measuring against the resting row instead made a filter argue for its own
287
+ // eviction — now that a large phone keeps one, its width came out of `base`,
288
+ // pushed the box under `usableSearch`, and tipped a row that had ample space
289
+ // to share into a full takeover.
290
+ const overflowButton = compactControls
291
+ ? RESERVED.overflowButtonCompact
292
+ : RESERVED.overflowButton;
293
+ const shedRoom = width - RESERVED.chrome - (overflow.length > 0 ? overflowButton : 0);
294
+ const searchBoxRoom = shedRoom - (counterHidden ? 0 : RESERVED.counter) - rightCost;
267
295
  // Below this a box is too narrow to read what you typed into it, and shrinking
268
296
  // further buys nothing; that is the only point at which taking the whole
269
297
  // cluster is worth losing the filters.
270
298
  const searchTakeover = searchCollapsed && searchBoxRoom < RESERVED.usableSearch;
271
299
 
272
- return {
273
- inline,
274
- overflow,
275
- compactControls,
276
- counterHidden,
277
- searchCollapsed,
278
- clearAllHidden,
279
- searchTakeover,
280
- };
300
+ return { compactControls, counterHidden, searchCollapsed, clearAllHidden, searchTakeover };
281
301
  }
282
302
 
283
303
  /**
@@ -307,12 +327,25 @@ export function useFilterOverflow<T extends Record<string, unknown>>(
307
327
  * that is mid-edit.
308
328
  */
309
329
  frozen = false,
330
+ /** Whether "Exportar" is on the bar — see `rightClusterCost`. */
331
+ hasExport = true,
310
332
  ): OverflowSplit<T> {
311
- const { barRef, width } = useBarWidth();
312
- const signature = JSON.stringify({ ids: all.map((field) => field.id), pills, ranges, width });
333
+ // The measurement is `useMeasuredWidth` (`utility/Overflow`): the same
334
+ // ResizeObserver every collapsing cluster in the design system reads, so
335
+ // there is one loop to get right rather than one per bar. `0` until the
336
+ // first measurement lands (and forever under SSR or jsdom), which
337
+ // `computeSplit` reads as "degrade nothing".
338
+ const { ref: barRef, width } = useMeasuredWidth<HTMLDivElement>();
339
+ const signature = JSON.stringify({
340
+ ids: all.map((field) => field.id),
341
+ pills,
342
+ ranges,
343
+ width,
344
+ hasExport,
345
+ });
313
346
  const cache = useRef<{ signature: string; split: ReturnType<typeof computeSplit<T>> } | null>(null);
314
347
  if (cache.current === null || (!frozen && cache.current.signature !== signature)) {
315
- cache.current = { signature, split: computeSplit(all, pills, ranges, width) };
348
+ cache.current = { signature, split: computeSplit(all, pills, ranges, width, hasExport) };
316
349
  }
317
350
  return { ...cache.current.split, barRef };
318
351
  }
@@ -22,6 +22,10 @@ export {
22
22
  type ListCardCellConfig,
23
23
  DEFAULT_CELL_WIDTH,
24
24
  } from "./list-card-cells";
25
+ // The list's columns declared at the TABLE, not per card — which is what makes
26
+ // a Lista line up by construction. Without it a consumer can still hand a cell
27
+ // config to each row, and they agree until one row carries a wider value.
28
+ export type { ListGroupConfig } from "./list-card-rails";
25
29
  // Published by a drag container so a row can draw the marker; a consumer wiring
26
30
  // its own DnD needs the type to build the value.
27
31
  export { DropIndicator } from "./data-views-drag";
@@ -223,6 +223,35 @@ export function cellRailCount(cells: number): number {
223
223
  return CELL_FIXED_RAILS + cells + 1;
224
224
  }
225
225
 
226
+ /**
227
+ * A {@link ListCardGroup}'s configuration, as a value a caller can pass around.
228
+ *
229
+ * The same fields the group takes as props, named once so a table can accept
230
+ * "the list's columns" as a single prop and hand them straight over. It is what
231
+ * makes a list line up BY CONSTRUCTION rather than by convention: without it
232
+ * every row resolves its own tracks from its own copy of the config, which
233
+ * agrees with its neighbours right until one row carries an unusually wide value.
234
+ */
235
+ export interface ListGroupConfig<T extends Record<string, unknown>> {
236
+ /**
237
+ * The cells every row renders. Supplying these replaces the named-slot layout
238
+ * (title/subtitle/meta/value/status) for the rows inside.
239
+ */
240
+ cells?: readonly ListCardCellConfig<T>[];
241
+ /**
242
+ * How many labelled pairs the rows carry, in the named-slot layout. The group
243
+ * renders its children opaquely and cannot count them.
244
+ */
245
+ metaColumns?: number;
246
+ /** Override individual rails; anything omitted keeps its default. */
247
+ rails?: Partial<ListRails>;
248
+ /**
249
+ * Hold the drag and select gutters open even when nothing uses them. On by
250
+ * default, so a selectable list and a read-only one still line up.
251
+ */
252
+ reserveGutters?: boolean;
253
+ }
254
+
226
255
  /**
227
256
  * A list of {@link BaseListCard}s that share one set of columns.
228
257
  *
@@ -3,17 +3,20 @@ import { forwardRef } from 'react';
3
3
 
4
4
  import { useDateTimeParts } from './DateTimeDisplay.hooks';
5
5
  import type { DateTimeDisplayProps } from './DateTimeDisplay.types';
6
+ import type { SizeValue } from '../../../tokens/scales';
6
7
 
7
8
  /**
8
9
  * Get font sizes based on size prop
9
10
  */
10
- function getFontSizes(size: 'small' | 'medium' | 'large') {
11
+ function getFontSizes(size: SizeValue) {
11
12
  switch (size) {
12
- case 'small':
13
+ case 'xs':
14
+ case 'sm':
13
15
  return { date: '0.75rem', time: '0.65rem' };
14
- case 'large':
16
+ case 'lg':
17
+ case 'xl':
15
18
  return { date: '1rem', time: '0.875rem' };
16
- case 'medium':
19
+ case 'md':
17
20
  default:
18
21
  return { date: '0.875rem', time: '0.75rem' };
19
22
  }
@@ -81,7 +84,7 @@ export const DateTimeDisplay = forwardRef<HTMLSpanElement, DateTimeDisplayProps>
81
84
  dateFormat = 'short',
82
85
  timeFormat = '12h',
83
86
  showTimezone = false,
84
- size = 'medium',
87
+ size = 'md',
85
88
  showTooltip = true,
86
89
  tooltipContent,
87
90
  sx,
@@ -1,3 +1,4 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { SxProps, Theme } from '@mui/material';
2
3
 
3
4
  export interface DateTimeDisplayProps {
@@ -25,7 +26,7 @@ export interface DateTimeDisplayProps {
25
26
  * Size variant
26
27
  * @default 'medium'
27
28
  */
28
- size?: 'small' | 'medium' | 'large';
29
+ size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
29
30
  /**
30
31
  * Whether to show a tooltip with full date/time
31
32
  * @default true
@@ -1,3 +1,4 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { TooltipProps as MuiTooltipProps } from '@mui/material';
2
3
 
3
4
  export interface InteractiveTooltipProps extends Omit<MuiTooltipProps, 'variant' | 'title'> {
@@ -19,7 +20,7 @@ export interface InteractiveTooltipProps extends Omit<MuiTooltipProps, 'variant'
19
20
  /**
20
21
  * Size of the tooltip
21
22
  */
22
- size?: 'sm' | 'md' | 'lg';
23
+ size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
23
24
 
24
25
  /**
25
26
  * Maximum width of the tooltip
@@ -1,5 +1,6 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  export type LoadingStateVariant = 'spinner' | 'skeleton';
2
- export type LoadingStateSize = 'sm' | 'md' | 'lg';
3
+ export type LoadingStateSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
3
4
 
4
5
  export interface LoadingStateProps {
5
6
  /**
@@ -1,7 +1,8 @@
1
+ import type { ColorValue, SizeValue } from '../../../tokens/scales';
1
2
  import type { LinearProgressProps } from '@mui/material';
2
3
 
3
4
  export type ProgressVariant = 'linear' | 'circular' | 'segmented' | 'gradient' | 'glass';
4
- export type ProgressSize = 'sm' | 'md' | 'lg';
5
+ export type ProgressSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
5
6
 
6
7
  export interface ProgressProps extends Omit<LinearProgressProps, 'variant' | 'color'> {
7
8
  /**
@@ -17,7 +18,7 @@ export interface ProgressProps extends Omit<LinearProgressProps, 'variant' | 'co
17
18
  /**
18
19
  * The color of the progress
19
20
  */
20
- color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'neutral';
21
+ color?: Exclude<ColorValue, 'info'>;
21
22
 
22
23
  /**
23
24
  * Whether the progress should have a glow effect
@@ -3,6 +3,7 @@ import React from 'react';
3
3
 
4
4
  import { SheetContent, SheetFooter, SheetHeader } from './Sheet.parts';
5
5
  import type { SheetProps } from './Sheet.types';
6
+ import { muiColor } from '../../../tokens/scales';
6
7
 
7
8
  export interface SheetBodyProps extends Pick<
8
9
  SheetProps,
@@ -50,7 +51,7 @@ const SheetLoading: React.FC<Pick<SheetBodyProps, 'color'>> = ({ color }) => (
50
51
  minHeight: 200,
51
52
  }}
52
53
  >
53
- <CircularProgress color={color} />
54
+ <CircularProgress color={color === undefined ? undefined : muiColor(color, 'inherit')} />
54
55
  </Box>
55
56
  );
56
57
 
@@ -4,6 +4,7 @@ import type React from 'react';
4
4
 
5
5
  import { glowAnimation, pulseAnimation, shimmerAnimation } from './Sheet.animations';
6
6
  import type { SheetProps } from './Sheet.types';
7
+ import { accentFor } from '../../../tokens/scales';
7
8
 
8
9
  type Position = NonNullable<SheetProps['position']>;
9
10
  type Variant = NonNullable<SheetProps['variant']>;
@@ -140,7 +141,7 @@ const SURFACES: Record<Variant, (input: SurfaceInput) => Record<string, unknown>
140
141
  135deg,
141
142
  ${theme.palette.background.paper} 0%,
142
143
  ${alpha(accent, 0.08)} 50%,
143
- ${alpha(theme.palette[color]?.dark || accent, 0.12)} 100%
144
+ ${alpha(accentFor(theme, color)?.dark || accent, 0.12)} 100%
144
145
  )`
145
146
  : theme.palette.background.paper,
146
147
  position: 'relative' as const,
@@ -211,7 +212,7 @@ const baseSurface = (input: VariantStyleInput, accent: string) => {
211
212
 
212
213
  const variantStyles = (input: VariantStyleInput) => {
213
214
  const { theme, variant, color, position, glow, pulse, rounded } = input;
214
- const accent = theme.palette[color]?.main || theme.palette.primary.main;
215
+ const accent = accentFor(theme, color)?.main || theme.palette.primary.main;
215
216
 
216
217
  return {
217
218
  ...baseSurface(input, accent),
@@ -1,3 +1,4 @@
1
+ import type { ColorValue, SizeValue } from '../../../tokens/scales';
1
2
  import type { CSSProperties,ReactNode } from 'react';
2
3
 
3
4
  export interface SheetProps {
@@ -8,8 +9,12 @@ export interface SheetProps {
8
9
  description?: string;
9
10
  position?: 'top' | 'bottom' | 'left' | 'right';
10
11
  variant?: 'default' | 'glass' | 'gradient' | 'elevated' | 'minimal' | 'draggable';
11
- size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
12
- color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info';
12
+ /**
13
+ * The house scale, plus `full`. `full` is not a size on the scale — it is
14
+ * "the whole viewport", so it extends rather than replaces.
15
+ */
16
+ size?: SizeValue | 'full';
17
+ color?: Exclude<ColorValue, 'neutral'>;
13
18
  glow?: boolean;
14
19
  pulse?: boolean;
15
20
  glass?: boolean;
@@ -1,3 +1,4 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { ReactNode } from 'react';
2
3
 
3
4
  export interface Step {
@@ -40,7 +41,7 @@ export interface StepperProps {
40
41
  * takes far less vertical space — useful above the fold on mobile checkouts.
41
42
  * @default 'md'
42
43
  */
43
- size?: 'sm' | 'md';
44
+ size?: Extract<SizeValue, 'sm' | 'md'>;
44
45
 
45
46
  /**
46
47
  * Navigation variant controlling step access
@@ -46,7 +46,7 @@ const getStripeColorFromTheme = (theme: { palette: { primary: { main: string };
46
46
  info: theme.palette.info.main,
47
47
  success: theme.palette.success.main,
48
48
  warning: theme.palette.warning.main,
49
- error: theme.palette.error.main,
49
+ danger: theme.palette.error.main,
50
50
  neutral: theme.palette.action.hover };
51
51
  return colorMap[stripeColor];
52
52
  };
@@ -1,3 +1,4 @@
1
+ import type { ColorValue } from '../../../tokens/scales';
1
2
  import type { TableProps as MuiTableProps } from '@mui/material';
2
3
  import type React from 'react';
3
4
 
@@ -57,7 +58,8 @@ export interface SelectionConfig {
57
58
  rowKeyExtractor?: (rowData: Record<string, unknown>, index: number) => string | number;
58
59
  }
59
60
 
60
- export type TableStripeColor = 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error' | 'neutral';
61
+ /** The full house vocabulary — a stripe can take any accent, or none. */
62
+ export type TableStripeColor = ColorValue;
61
63
 
62
64
  export interface TableProps extends Omit<MuiTableProps, 'variant'> {
63
65
  /**
@@ -1,3 +1,4 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { TooltipProps as MuiTooltipProps } from '@mui/material';
2
3
 
3
4
  export interface TooltipProps extends Omit<MuiTooltipProps, 'variant'> {
@@ -19,7 +20,7 @@ export interface TooltipProps extends Omit<MuiTooltipProps, 'variant'> {
19
20
  /**
20
21
  * Size of the tooltip
21
22
  */
22
- size?: 'sm' | 'md' | 'lg';
23
+ size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
23
24
 
24
25
  /**
25
26
  * Maximum width of the tooltip
@@ -1,8 +1,9 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { DialogProps as MuiDialogProps } from '@mui/material';
2
3
  import type { ReactNode } from 'react';
3
4
 
4
5
  export type DialogVariant = 'default' | 'glass' | 'fullscreen' | 'drawer';
5
- export type DialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
6
+ export type DialogSize = SizeValue;
6
7
 
7
8
  export interface DialogProps extends Omit<MuiDialogProps, 'variant' | 'title'> {
8
9
  children: ReactNode;
@@ -1,8 +1,9 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { ModalProps as MuiModalProps } from '@mui/material';
2
3
  import type { ReactNode } from 'react';
3
4
 
4
5
  export type ModalVariant = 'center' | 'top' | 'bottom' | 'glass';
5
- export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
6
+ export type ModalSize = SizeValue;
6
7
 
7
8
  export interface ModalProps extends Omit<MuiModalProps, 'children'> {
8
9
  children: ReactNode;
@@ -1,3 +1,4 @@
1
+ import type { ColorValue, SizeValue } from '../../../tokens/scales';
1
2
  import type { ButtonProps as MuiButtonProps } from '@mui/material';
2
3
  import type * as React from 'react';
3
4
 
@@ -10,12 +11,12 @@ export interface ButtonProps extends Omit<MuiButtonProps, 'variant' | 'color' |
10
11
  /**
11
12
  * The color of the button
12
13
  */
13
- color?: 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'danger' | 'neutral';
14
+ color?: ColorValue;
14
15
 
15
16
  /**
16
17
  * The size of the button
17
18
  */
18
- size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
19
+ size?: SizeValue;
19
20
 
20
21
  /**
21
22
  * Whether the button is in loading state
@@ -2,6 +2,7 @@ import { alpha } from '@mui/material';
2
2
  import type { CSSObject, Theme } from '@mui/material';
3
3
 
4
4
  import type { CommandProps } from './Command.types';
5
+ import { accentFor } from '../../../tokens/scales';
5
6
 
6
7
  type CommandVariant = NonNullable<CommandProps['variant']>;
7
8
  type CommandColor = NonNullable<CommandProps['color']>;
@@ -24,14 +25,14 @@ const emphasisStyles = (
24
25
  pulse: boolean,
25
26
  ): CSSObject => ({
26
27
  ...(glow && {
27
- boxShadow: `0 0 20px ${alpha(theme.palette[color].main, 0.4)}`,
28
+ boxShadow: `0 0 20px ${alpha(accentFor(theme, color).main, 0.4)}`,
28
29
  }),
29
30
  ...(pulse && {
30
31
  animation: 'pulse 2s infinite',
31
32
  '@keyframes pulse': {
32
- '0%': { boxShadow: `0 0 0 0 ${alpha(theme.palette[color].main, 0.4)}` },
33
- '70%': { boxShadow: `0 0 0 10px ${alpha(theme.palette[color].main, 0)}` },
34
- '100%': { boxShadow: `0 0 0 0 ${alpha(theme.palette[color].main, 0)}` },
33
+ '0%': { boxShadow: `0 0 0 0 ${alpha(accentFor(theme, color).main, 0.4)}` },
34
+ '70%': { boxShadow: `0 0 0 10px ${alpha(accentFor(theme, color).main, 0)}` },
35
+ '100%': { boxShadow: `0 0 0 0 ${alpha(accentFor(theme, color).main, 0)}` },
35
36
  },
36
37
  }),
37
38
  });
@@ -46,12 +47,12 @@ const surfaceStyles = (
46
47
  return {
47
48
  backgroundColor: alpha(theme.palette.background.paper, 0.1),
48
49
  backdropFilter: 'blur(20px)',
49
- border: `1px solid ${alpha(theme.palette[color].main, 0.2)}`,
50
+ border: `1px solid ${alpha(accentFor(theme, color).main, 0.2)}`,
50
51
  };
51
52
  case 'gradient':
52
53
  return {
53
- background: `linear-gradient(135deg, ${theme.palette[color].main}, ${theme.palette[color].dark})`,
54
- color: theme.palette[color].contrastText,
54
+ background: `linear-gradient(135deg, ${accentFor(theme, color).main}, ${accentFor(theme, color).dark})`,
55
+ color: accentFor(theme, color).contrastText,
55
56
  };
56
57
  case 'elevated':
57
58
  return { boxShadow: theme.shadows[8] };
@@ -1,3 +1,4 @@
1
+ import type { ColorValue, SizeValue } from '../../../tokens/scales';
1
2
  import type { CSSProperties,ReactNode } from 'react';
2
3
 
3
4
  export interface CommandItem {
@@ -21,8 +22,8 @@ export interface CommandProps {
21
22
  onValueChange?: (value: string) => void;
22
23
  onSelect?: (item: CommandItem) => void;
23
24
  variant?: 'default' | 'glass' | 'gradient' | 'minimal' | 'elevated';
24
- size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
25
- color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info';
25
+ size?: SizeValue;
26
+ color?: Exclude<ColorValue, 'neutral'>;
26
27
  glow?: boolean;
27
28
  pulse?: boolean;
28
29
  loading?: boolean;
@@ -1,3 +1,4 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  /** One selectable option: a stable `value` and its human `label`. */
2
3
  export interface CreatableSelectOption {
3
4
  value: string;
@@ -44,7 +45,7 @@ export interface CreatableSelectProps {
44
45
  * Control height, matching {@link Input}'s scale so the two line up when they
45
46
  * share a grid row (default: `md`).
46
47
  */
47
- size?: 'sm' | 'md';
48
+ size?: Extract<SizeValue, 'sm' | 'md'>;
48
49
  /** Base test id; the input is `<dataTestId>-input`. */
49
50
  dataTestId?: string;
50
51
  }
@@ -1,9 +1,10 @@
1
+ import type { SizeValue } from '../../../tokens/scales';
1
2
  import type { InputLabelProps as MuiInputLabelProps } from '@mui/material';
2
3
  import type { InputHTMLAttributes, ReactNode } from 'react';
3
4
  import type React from 'react';
4
5
 
5
6
  export type InputVariant = 'outlined' | 'filled' | 'glass' | 'underline' | 'gradient';
6
- export type InputSize = 'sm' | 'md' | 'lg';
7
+ export type InputSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
7
8
 
8
9
  export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'color'> {
9
10
  variant?: InputVariant;