@12-apps/ui 1.23.0 → 2.1.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 (82) 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/data-views-grid-bodies.tsx +113 -62
  22. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +4 -0
  23. package/src/components/data-display/DataViews/data-views-overflow.ts +23 -51
  24. package/src/components/data-display/DataViews/index.ts +4 -0
  25. package/src/components/data-display/DataViews/list-card-rails.tsx +29 -0
  26. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +8 -5
  27. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +2 -1
  28. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +2 -1
  29. package/src/components/data-display/LoadingState/LoadingState.types.ts +2 -1
  30. package/src/components/data-display/Progress/Progress.types.ts +3 -2
  31. package/src/components/data-display/Sheet/Sheet.body.tsx +2 -1
  32. package/src/components/data-display/Sheet/Sheet.styles.ts +3 -2
  33. package/src/components/data-display/Sheet/Sheet.types.ts +7 -2
  34. package/src/components/data-display/Stepper/Stepper.types.ts +2 -1
  35. package/src/components/data-display/Table/Table.styles.ts +1 -1
  36. package/src/components/data-display/Table/Table.types.ts +3 -1
  37. package/src/components/data-display/Tooltip/Tooltip.types.ts +2 -1
  38. package/src/components/feedback/Dialog/Dialog.types.ts +2 -1
  39. package/src/components/feedback/Modal/Modal.types.ts +2 -1
  40. package/src/components/form/Button/Button.types.ts +3 -2
  41. package/src/components/form/Calendar/CalendarMonth.tsx +6 -1
  42. package/src/components/form/Command/Command.styles.ts +8 -7
  43. package/src/components/form/Command/Command.types.ts +3 -2
  44. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +2 -1
  45. package/src/components/form/Input/Input.types.ts +2 -1
  46. package/src/components/form/InputOTP/InputOTP.types.ts +3 -2
  47. package/src/components/form/Label/Label.styles.ts +8 -5
  48. package/src/components/form/Label/Label.tsx +1 -1
  49. package/src/components/form/Label/Label.types.ts +3 -2
  50. package/src/components/form/Menubar/Menubar.styles.ts +10 -8
  51. package/src/components/form/Menubar/Menubar.tsx +2 -1
  52. package/src/components/form/Menubar/Menubar.types.ts +7 -6
  53. package/src/components/form/RadioGroup/RadioGroup.types.ts +3 -2
  54. package/src/components/form/Select/Select.tsx +13 -1
  55. package/src/components/form/Select/Select.types.ts +5 -2
  56. package/src/components/form/Slider/Slider.types.ts +3 -2
  57. package/src/components/form/Switch/Switch.types.ts +3 -2
  58. package/src/components/form/Textarea/Textarea.types.ts +3 -2
  59. package/src/components/form/Toggle/Toggle.types.ts +3 -2
  60. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +3 -2
  61. package/src/components/layout/Separator/Separator.types.ts +2 -1
  62. package/src/components/layout/Spacer/Spacer.types.ts +2 -1
  63. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
  64. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +7 -2
  65. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +3 -2
  66. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +1 -0
  67. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +12 -2
  68. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +7 -2
  69. package/src/components/navigation/Pagination/Pagination.tsx +3 -1
  70. package/src/components/navigation/Pagination/Pagination.types.ts +5 -3
  71. package/src/components/navigation/Tabs/Tabs.types.ts +3 -2
  72. package/src/components/typography/Blockquote/Blockquote.types.ts +2 -1
  73. package/src/components/typography/Code/Code.types.ts +2 -1
  74. package/src/components/typography/Heading/Heading.types.ts +2 -1
  75. package/src/components/typography/Paragraph/Paragraph.types.ts +3 -2
  76. package/src/components/typography/Text/Text.types.ts +3 -2
  77. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +2 -1
  78. package/src/components/utility/Overflow/index.ts +6 -0
  79. package/src/components/utility/Overflow/overflow.ts +125 -0
  80. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +3 -2
  81. package/src/components/utility/index.ts +1 -0
  82. package/src/tokens/scales.ts +153 -0
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import React from "react";
4
+
3
5
  import { DataGrid, type GridColumn, type GridSort } from "../DataGrid";
4
6
  import { Box } from "../../../mui/Box";
5
7
 
@@ -10,9 +12,11 @@ import {
10
12
  DENSITY_CARD_COLUMNS,
11
13
  DENSITY_ROW_PADDING,
12
14
  useDataViewsLayout,
15
+ type DataViewsDensity,
13
16
  } from "./data-views-layout-context";
14
17
  import { DataViewsBoard, type BoardConfig } from "./DataViewsBoard";
15
18
  import { SelectAllStrip } from "./data-views-select-all-strip";
19
+ import { ListCardGroup, type ListGroupConfig } from "./list-card-rails";
16
20
  import type { DataViewCardSelection } from "./data-views-types";
17
21
  import type { DataViewsController } from "./use-data-views-state";
18
22
 
@@ -160,6 +164,10 @@ interface ListBodyProps<T extends Record<string, unknown>> {
160
164
  onToggleId: (id: string | number) => void;
161
165
  /** Gap between rows, from the density preference. */
162
166
  rowGap: number;
167
+ /** The list's shared column config. See {@link ListGroupConfig}. */
168
+ group?: ListGroupConfig<T>;
169
+ /** The density every row answers, handed to the group when there is one. */
170
+ density: DataViewsDensity;
163
171
  dataTestId?: string;
164
172
  emptyState?: React.ReactNode;
165
173
  }
@@ -186,28 +194,59 @@ function ListBody<T extends Record<string, unknown>>({
186
194
  selectedIds,
187
195
  onToggleId,
188
196
  rowGap,
197
+ group,
198
+ density,
189
199
  dataTestId,
190
200
  emptyState,
191
201
  }: ListBodyProps<T>): React.JSX.Element {
192
202
  if (rows.length === 0) {
193
203
  return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
194
204
  }
205
+ const testId = dataTestId ? `${dataTestId}-list` : "data-views-list";
206
+ const selectionFor = (id: string | number): DataViewCardSelection => ({
207
+ selected: selectedIds.has(id),
208
+ onToggleSelect: () => onToggleId(id),
209
+ scale: 1,
210
+ });
211
+
212
+ if (group) {
213
+ return (
214
+ <Box sx={{ mt: 1.5 }}>
215
+ <ListCardGroup
216
+ cells={group.cells}
217
+ metaColumns={group.metaColumns}
218
+ rails={group.rails}
219
+ reserveGutters={group.reserveGutters}
220
+ density={density}
221
+ gap={rowGap}
222
+ dataTestId={testId}
223
+ >
224
+ {rows.map((row) => {
225
+ const id = getRowId(row);
226
+ // A FRAGMENT, NOT A BOX. The row is subgrid over the group's tracks
227
+ // (`gridColumn: span railCount`), which only resolves while the card
228
+ // is a DIRECT child of the group's grid. One wrapper element and the
229
+ // span is measured against a grid that isn't there, so every rail
230
+ // collapses — the exact failure the group exists to prevent.
231
+ return (
232
+ <React.Fragment key={id}>
233
+ {renderListRow(row, selectionFor(id))}
234
+ </React.Fragment>
235
+ );
236
+ })}
237
+ </ListCardGroup>
238
+ </Box>
239
+ );
240
+ }
241
+
195
242
  return (
196
243
  <Box
197
244
  sx={{ mt: 1.5, display: "flex", flexDirection: "column", gap: rowGap }}
198
- data-testid={dataTestId ? `${dataTestId}-list` : "data-views-list"}
245
+ data-testid={testId}
199
246
  >
200
247
  {rows.map((row) => {
201
248
  const id = getRowId(row);
202
- return (
203
- <Box key={id}>
204
- {renderListRow(row, {
205
- selected: selectedIds.has(id),
206
- onToggleSelect: () => onToggleId(id),
207
- scale: 1,
208
- })}
209
- </Box>
210
- );
249
+ return <Box key={id}>{renderListRow(row, selectionFor(id))}</Box>;
211
250
  })}
212
251
  </Box>
213
252
  );
@@ -221,6 +260,8 @@ interface GridMainProps<T extends Record<string, unknown>> {
221
260
  renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
222
261
  /** Opt-in "Lista" layout — one full-width row per record. */
223
262
  renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
263
+ /** The Lista's shared columns. Omitted, each row resolves its own tracks. */
264
+ listGroup?: ListGroupConfig<T>;
224
265
  /** Opt-in "Quadro" (board) layout — needs `renderCard`, since it reuses the card. */
225
266
  board?: BoardConfig<T>;
226
267
  dataTestId?: string;
@@ -260,68 +301,78 @@ function Headerless<T extends Record<string, unknown>>({
260
301
  );
261
302
  }
262
303
 
263
- /** Picks the body from the layout context: board, list, cards, or the dense grid. */
264
- export function GridMain<T extends Record<string, unknown>>({
265
- c,
266
- getRowId,
267
- renderCard,
268
- renderListRow,
269
- board,
270
- dataTestId,
271
- emptyState,
272
- testIdPrefix,
273
- }: GridMainProps<T>): React.JSX.Element {
274
- const { layout, zoom, density } = useDataViewsLayout();
304
+ /**
305
+ * The body for one of the three HEADERLESS layouts, or `null` for the table.
306
+ *
307
+ * Split out from {@link GridMain} so the select-all strip is wrapped around the
308
+ * result exactly once instead of being repeated identically in every branch —
309
+ * which is also what keeps either function inside the line budget.
310
+ */
311
+ function headerlessBody<T extends Record<string, unknown>>(
312
+ { c, getRowId, renderCard, renderListRow, listGroup, board, dataTestId, emptyState }: GridMainProps<T>,
313
+ { layout, zoom, density }: ReturnType<typeof useDataViewsLayout>,
314
+ ): React.ReactNode | null {
275
315
  if (layout === "list" && renderListRow) {
276
316
  return (
277
- <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
278
- <ListBody
279
- rows={c.matched}
280
- renderListRow={renderListRow}
281
- getRowId={getRowId}
282
- selectedIds={c.selectedIds}
283
- onToggleId={c.toggleId}
284
- rowGap={DENSITY_ROW_PADDING[density]}
285
- dataTestId={dataTestId}
286
- emptyState={emptyState}
287
- />
288
- </Headerless>
317
+ <ListBody
318
+ rows={c.matched}
319
+ renderListRow={renderListRow}
320
+ getRowId={getRowId}
321
+ selectedIds={c.selectedIds}
322
+ onToggleId={c.toggleId}
323
+ rowGap={DENSITY_ROW_PADDING[density]}
324
+ group={listGroup}
325
+ density={density}
326
+ dataTestId={dataTestId}
327
+ emptyState={emptyState}
328
+ />
289
329
  );
290
330
  }
291
331
  if (layout === "board" && board && renderCard) {
292
332
  return (
293
- <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
294
- <DataViewsBoard
295
- rows={c.matched}
296
- board={board}
297
- getRowId={getRowId}
298
- renderCard={renderCard}
299
- selectedIds={c.selectedIds}
300
- onToggleId={c.toggleId}
301
- // DENSITY, not zoom: the board's own knob in the Exibição tab is how
302
- // wide its columns are, and it is the only sizing control the board
303
- // has since the zoom slider was removed.
304
- cardScale={DENSITY_BOARD_SCALE[density]}
305
- dataTestId={dataTestId}
306
- />
307
- </Headerless>
333
+ <DataViewsBoard
334
+ rows={c.matched}
335
+ board={board}
336
+ getRowId={getRowId}
337
+ renderCard={renderCard}
338
+ selectedIds={c.selectedIds}
339
+ onToggleId={c.toggleId}
340
+ // DENSITY, not zoom: the board's own knob in the Exibição tab is how
341
+ // wide its columns are, and it is the only sizing control the board
342
+ // has since the zoom slider was removed.
343
+ cardScale={DENSITY_BOARD_SCALE[density]}
344
+ dataTestId={dataTestId}
345
+ />
308
346
  );
309
347
  }
310
348
  if (layout === "cards" && renderCard) {
349
+ return (
350
+ <CardBody
351
+ rows={c.matched}
352
+ renderCard={renderCard}
353
+ getRowId={getRowId}
354
+ selectedIds={c.selectedIds}
355
+ onToggleId={c.toggleId}
356
+ minCardWidth={cardMinWidthForZoom(zoom)}
357
+ maxColumns={DENSITY_CARD_COLUMNS[density]}
358
+ cardScale={cardScaleForZoom(zoom)}
359
+ dataTestId={dataTestId}
360
+ emptyState={emptyState}
361
+ />
362
+ );
363
+ }
364
+ return null;
365
+ }
366
+
367
+ /** Picks the body from the layout context: board, list, cards, or the dense grid. */
368
+ export function GridMain<T extends Record<string, unknown>>(props: GridMainProps<T>): React.JSX.Element {
369
+ const { c, getRowId, dataTestId, emptyState, testIdPrefix } = props;
370
+ const layoutState = useDataViewsLayout();
371
+ const body = headerlessBody(props, layoutState);
372
+ if (body) {
311
373
  return (
312
374
  <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
313
- <CardBody
314
- rows={c.matched}
315
- renderCard={renderCard}
316
- getRowId={getRowId}
317
- selectedIds={c.selectedIds}
318
- onToggleId={c.toggleId}
319
- minCardWidth={cardMinWidthForZoom(zoom)}
320
- maxColumns={DENSITY_CARD_COLUMNS[density]}
321
- cardScale={cardScaleForZoom(zoom)}
322
- dataTestId={dataTestId}
323
- emptyState={emptyState}
324
- />
375
+ {body}
325
376
  </Headerless>
326
377
  );
327
378
  }
@@ -335,7 +386,7 @@ export function GridMain<T extends Record<string, unknown>>({
335
386
  sortBy={c.state.sortBy}
336
387
  onChangeSortBy={(next: GridSort[]) => c.patch({ sortBy: next })}
337
388
  sortMode={c.serverMode ? "server" : "client"}
338
- rowPadding={DENSITY_ROW_PADDING[density]}
389
+ rowPadding={DENSITY_ROW_PADDING[layoutState.density]}
339
390
  dataTestId={dataTestId}
340
391
  emptyState={emptyState}
341
392
  />
@@ -14,6 +14,7 @@ import {
14
14
  } from "./data-views-layout-context";
15
15
  import { FilterDialog, GridFilterPanel } from "./data-views-filter-panel";
16
16
  import { GridMain } from "./data-views-grid-bodies";
17
+ import type { ListGroupConfig } from "./list-card-rails";
17
18
  import { InlineFilterControls } from "./data-views-inline-bar";
18
19
  import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
19
20
  import { ShellToolbar } from "./data-views-shell-toolbar";
@@ -238,6 +239,8 @@ interface GridShellProps<T extends Record<string, unknown>> {
238
239
  board?: BoardConfig<T>;
239
240
  /** Opt-in "Lista" layout: one full-width, entity-rendered row per record. */
240
241
  renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
242
+ /** The Lista's shared columns, so its rows line up by construction. */
243
+ listGroup?: ListGroupConfig<T>;
241
244
  /** The page-level partition rendered as tabs under the toolbar. */
242
245
  scopes?: ScopeConfig[];
243
246
  /**
@@ -340,6 +343,7 @@ function ShellStack<T extends Record<string, unknown>>({
340
343
  getRowId={props.getRowId}
341
344
  renderCard={props.renderCard}
342
345
  renderListRow={props.renderListRow}
346
+ listGroup={props.listGroup}
343
347
  board={props.board}
344
348
  dataTestId={dataTestId}
345
349
  testIdPrefix={testIdPrefix}
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useRef } from "react";
4
4
 
5
+ import { splitToFit, useMeasuredWidth } from "../../utility/Overflow";
5
6
  import {
6
7
  estimateWidth,
7
8
  furnitureCost,
@@ -106,26 +107,6 @@ export function toOverflowFields<T extends Record<string, unknown>>(
106
107
  }
107
108
 
108
109
 
109
- /** Observe the bar's width. Returns 0 until the first measurement lands. */
110
- function useBarWidth(): { barRef: React.RefObject<HTMLDivElement | null>; width: number } {
111
- const barRef = useRef<HTMLDivElement | null>(null);
112
- const [width, setWidth] = useState(0);
113
- useEffect(() => {
114
- const element = barRef.current;
115
- // jsdom has no ResizeObserver, and neither does an SSR pass — with no
116
- // measurement the hook keeps every control inline, which is the honest
117
- // fallback: nothing is hidden behind a menu nobody can see.
118
- if (!element || typeof ResizeObserver === "undefined") return undefined;
119
- const observer = new ResizeObserver((entries) => {
120
- const entry = entries[0];
121
- if (entry) setWidth(entry.contentRect.width);
122
- });
123
- observer.observe(element);
124
- return () => observer.disconnect();
125
- }, []);
126
- return { barRef, width };
127
- }
128
-
129
110
  /** Is this control carrying a value the operator set? */
130
111
  function isActiveField<T extends Record<string, unknown>>(
131
112
  field: OverflowField<T>,
@@ -167,24 +148,11 @@ function splitFilters<T extends Record<string, unknown>>(
167
148
  /** Is the bar already down to icons? "Mais" shrinks with everything else. */
168
149
  compact: boolean,
169
150
  ): Split<T> {
170
- const none: OverflowField<T>[] = [];
171
- const widthOf = (field: OverflowField<T>): number => fieldWidth(field, pills, ranges);
172
- const cost = (fields: OverflowField<T>[]): number =>
173
- fields.reduce((sum, field) => sum + widthOf(field) + GAP, 0);
174
151
  const active = all.filter((field) => isActiveField(field, pills, ranges));
175
152
  const idle = all.filter((field) => !isActiveField(field, pills, ranges));
176
153
 
177
154
  // "Limpar" rides the end of the cluster whenever anything is applied.
178
155
  const clearCost = active.length > 0 ? RESERVED.clearAll + GAP : 0;
179
- let available = width - furniture - clearCost;
180
- if (cost(all) <= available) {
181
- return { inline: all, overflow: none, used: cost(all) + clearCost };
182
- }
183
-
184
- // There IS an overflow, so its button now costs room too — at the width it
185
- // will actually have, which on a phone is the icon-and-badge one.
186
- const overflowButton = compact ? RESERVED.overflowButtonCompact : RESERVED.overflowButton;
187
- available -= overflowButton;
188
156
  // APPLIED FIRST, BUT NOT EXEMPT.
189
157
  //
190
158
  // Applied controls take the visible slots ahead of idle ones — that part was
@@ -197,22 +165,21 @@ function splitFilters<T extends Record<string, unknown>>(
197
165
  // vanished silently: the trigger goes to the applied tone and says how many
198
166
  // are in there (see `MoreTrigger`), which is the same signal the pill itself
199
167
  // was carrying. A control you scroll off-screen carries no signal at all.
200
- const keep = new Set<string>();
201
- for (const field of [...active, ...idle]) {
202
- const next = widthOf(field) + GAP;
203
- if (next > available) continue;
204
- available -= next;
205
- keep.add(field.id);
206
- }
207
- // Rendered in the DECLARED order, not the applied-first one: which controls
208
- // are visible may change with the width, but the ones that stay must not
209
- // reshuffle under the operator's cursor.
210
- const inline = all.filter((field) => keep.has(field.id));
211
- return {
212
- inline,
213
- overflow: all.filter((field) => !keep.has(field.id)),
214
- used: cost(inline) + overflowButton + clearCost,
215
- };
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 };
216
183
  }
217
184
 
218
185
  /**
@@ -363,7 +330,12 @@ export function useFilterOverflow<T extends Record<string, unknown>>(
363
330
  /** Whether "Exportar" is on the bar — see `rightClusterCost`. */
364
331
  hasExport = true,
365
332
  ): OverflowSplit<T> {
366
- const { barRef, width } = useBarWidth();
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>();
367
339
  const signature = JSON.stringify({
368
340
  ids: all.map((field) => field.id),
369
341
  pills,
@@ -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