@12-apps/ui 2.1.0 → 3.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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/components/data-display/Alert/Alert.types.ts +1 -1
  3. package/src/components/data-display/Avatar/Avatar.types.ts +1 -1
  4. package/src/components/data-display/Avatar/Avatar.view.tsx +5 -1
  5. package/src/components/data-display/Badge/Badge.styles.ts +11 -2
  6. package/src/components/data-display/Badge/Badge.types.ts +1 -1
  7. package/src/components/data-display/Carousel/Carousel.types.ts +4 -4
  8. package/src/components/data-display/Chart/Chart.types.ts +1 -1
  9. package/src/components/data-display/Chart/ChartSpec.types.ts +1 -1
  10. package/src/components/data-display/Chip/Chip.tsx +1 -1
  11. package/src/components/data-display/Chip/Chip.types.ts +6 -3
  12. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +1 -1
  13. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +1 -1
  14. package/src/components/data-display/LoadingState/LoadingState.helpers.ts +4 -0
  15. package/src/components/data-display/LoadingState/LoadingState.types.ts +1 -1
  16. package/src/components/data-display/Progress/Progress.styles.ts +3 -0
  17. package/src/components/data-display/Progress/Progress.types.ts +2 -2
  18. package/src/components/data-display/Sheet/Sheet.types.ts +1 -1
  19. package/src/components/data-display/Stepper/Stepper.tsx +18 -6
  20. package/src/components/data-display/Stepper/Stepper.types.ts +2 -2
  21. package/src/components/data-display/Tooltip/Tooltip.types.ts +1 -1
  22. package/src/components/form/Button/Button.styles.ts +1 -0
  23. package/src/components/form/Command/Command.types.ts +1 -1
  24. package/src/components/form/CreatableSelect/CreatableSelect.tsx +5 -2
  25. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +1 -1
  26. package/src/components/form/Input/Input.styles.ts +4 -0
  27. package/src/components/form/Input/Input.types.ts +1 -1
  28. package/src/components/form/InputOTP/InputOTP.styles.ts +1 -0
  29. package/src/components/form/InputOTP/InputOTP.types.ts +1 -1
  30. package/src/components/form/RadioGroup/RadioGroup.styles.ts +1 -0
  31. package/src/components/form/RadioGroup/RadioGroup.types.ts +1 -1
  32. package/src/components/form/Select/Select.types.ts +1 -1
  33. package/src/components/form/Slider/Slider.styles.ts +1 -0
  34. package/src/components/form/Slider/Slider.types.ts +1 -1
  35. package/src/components/form/Switch/Switch.styles.ts +1 -0
  36. package/src/components/form/Switch/Switch.types.ts +1 -1
  37. package/src/components/form/Textarea/Textarea.styles.ts +1 -0
  38. package/src/components/form/Textarea/Textarea.types.ts +1 -1
  39. package/src/components/form/Toggle/Toggle.styles.ts +1 -0
  40. package/src/components/form/Toggle/Toggle.types.ts +1 -1
  41. package/src/components/form/ToggleGroup/ToggleGroup.tsx +1 -0
  42. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +1 -1
  43. package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +6 -3
  44. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +1 -1
  45. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +1 -1
  46. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +1 -1
  47. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +1 -1
  48. package/src/components/navigation/Pagination/Pagination.types.ts +1 -1
  49. package/src/components/navigation/Tabs/Tabs.types.ts +1 -1
  50. package/src/components/typography/Blockquote/Blockquote.tsx +1 -0
  51. package/src/components/typography/Blockquote/Blockquote.types.ts +1 -1
  52. package/src/components/typography/Code/Code.types.ts +1 -1
  53. package/src/components/typography/Heading/Heading.styles.ts +23 -7
  54. package/src/components/typography/Heading/Heading.types.ts +1 -1
  55. package/src/components/typography/Paragraph/Paragraph.tsx +1 -0
  56. package/src/components/typography/Paragraph/Paragraph.types.ts +1 -1
  57. package/src/components/typography/Text/Text.tsx +11 -5
  58. package/src/components/typography/Text/Text.types.ts +1 -1
  59. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +1 -0
  60. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +1 -1
  61. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +1 -1
  62. package/src/tokens/scales.ts +58 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@ export interface AlertProps extends Omit<MuiAlertProps, 'variant' | 'color' | 'r
11
11
  /**
12
12
  * The color of the alert (when not using variant-specific colors)
13
13
  */
14
- color?: Exclude<ColorValue, 'info'>;
14
+ color?: ColorValue;
15
15
 
16
16
  /**
17
17
  * Whether the alert should have a glow effect
@@ -57,7 +57,7 @@ export interface AvatarProps extends Omit<MuiAvatarProps, 'variant'> {
57
57
  /**
58
58
  * Color of the avatar background when no image is provided
59
59
  */
60
- color?: Exclude<ColorValue, 'info'>;
60
+ color?: ColorValue;
61
61
 
62
62
  /**
63
63
  * Whether the avatar is in a loading state
@@ -64,7 +64,11 @@ const getColorFromTheme = (theme: Theme, color: string): PaletteLike => {
64
64
  secondary: theme.palette.secondary,
65
65
  success: theme.palette.success,
66
66
  warning: theme.palette.warning,
67
- error: theme.palette.error,
67
+ info: theme.palette.info,
68
+ // `danger`, not `error`. This map was keyed on MUI's word while the prop
69
+ // took the house one, so `danger` matched nothing and fell through to
70
+ // primary — a red avatar rendering blue, silently.
71
+ danger: theme.palette.error,
68
72
  neutral: { main: theme.palette.grey[700], contrastText: '#fff' },
69
73
  };
70
74
 
@@ -24,8 +24,10 @@ const getColorFromTheme = (theme: Theme, color: string) => {
24
24
  secondary: theme.palette.secondary,
25
25
  success: theme.palette.success,
26
26
  warning: theme.palette.warning,
27
- error: theme.palette.error,
28
27
  info: theme.palette.info,
28
+ // `danger`, the house word the prop actually takes. Keyed on MUI's `error`
29
+ // it matched nothing and fell through to the default.
30
+ danger: theme.palette.error,
29
31
  neutral: {
30
32
  main: theme.palette.grey[600],
31
33
  light: theme.palette.grey[400],
@@ -74,7 +76,14 @@ export const getSizeStyles = (size: BadgeSize) => {
74
76
  fontSize: '0.875rem',
75
77
  padding: '0 8px',
76
78
  dotSize: 12,
77
- iconSize: '1rem' } };
79
+ iconSize: '1rem' },
80
+ xl: {
81
+ minWidth: 28,
82
+ height: 28,
83
+ fontSize: '1rem',
84
+ padding: '0 10px',
85
+ dotSize: 14,
86
+ iconSize: '1.125rem' } };
78
87
 
79
88
  return sizeMap[size] || sizeMap.md;
80
89
  };
@@ -14,7 +14,7 @@ export type BadgeVariant =
14
14
  | 'success'
15
15
  | 'warning';
16
16
 
17
- export type BadgeSize = Extract<SizeValue, 'xs' | 'sm' | 'md' | 'lg'>;
17
+ export type BadgeSize = SizeValue;
18
18
 
19
19
  export interface BadgeProps extends Omit<MuiBadgeProps, 'variant' | 'color' | 'content'> {
20
20
  /**
@@ -14,7 +14,7 @@ export interface CarouselProps {
14
14
  items: CarouselItem[];
15
15
  variant?: 'default' | 'glass' | 'gradient' | 'elevated' | 'minimal' | 'cards';
16
16
  size?: SizeValue;
17
- color?: Exclude<ColorValue, 'neutral'>;
17
+ color?: ColorValue;
18
18
  autoPlay?: boolean;
19
19
  autoPlayInterval?: number;
20
20
  loop?: boolean;
@@ -51,7 +51,7 @@ export interface CarouselIndicatorsProps {
51
51
  activeIndex: number;
52
52
  onSelect: (index: number) => void;
53
53
  position?: 'top' | 'bottom' | 'left' | 'right';
54
- color?: Exclude<ColorValue, 'neutral'>;
54
+ color?: ColorValue;
55
55
  variant?: 'dots' | 'lines' | 'numbers';
56
56
  className?: string;
57
57
  style?: CSSProperties;
@@ -64,8 +64,8 @@ export interface CarouselArrowsProps {
64
64
  disabled?: boolean;
65
65
  disablePrev?: boolean;
66
66
  disableNext?: boolean;
67
- color?: Exclude<ColorValue, 'neutral'>;
68
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
67
+ color?: ColorValue;
68
+ size?: SizeValue;
69
69
  className?: string;
70
70
  style?: CSSProperties;
71
71
  }
@@ -28,7 +28,7 @@ export interface ChartProps {
28
28
  type?: ChartType;
29
29
  variant?: 'default' | 'glass' | 'gradient' | 'elevated' | 'minimal' | 'neon';
30
30
  size?: SizeValue;
31
- color?: Exclude<ColorValue, 'neutral'>;
31
+ color?: ColorValue;
32
32
  height?: number;
33
33
  width?: number | string;
34
34
  glow?: boolean;
@@ -18,7 +18,7 @@ export type ChartSpecType = 'line' | 'bar' | 'area' | 'pie' | 'donut';
18
18
  * The house vocabulary minus `neutral`: a data series is always an accent, and
19
19
  * an unaccented series would be indistinguishable from the grid it sits on.
20
20
  */
21
- export type ChartSemanticColor = Exclude<ColorValue, 'neutral'>;
21
+ export type ChartSemanticColor = ColorValue;
22
22
 
23
23
  /**
24
24
  * Number formatting applied to tooltip values and the y axis.
@@ -15,7 +15,7 @@ import type { ChipProps } from './Chip.types';
15
15
 
16
16
  /** MUI draws two chip sizes; ours are the house abbreviations for them. */
17
17
  const muiSizeFor = (size: NonNullable<ChipProps['size']>): 'small' | 'medium' =>
18
- size === 'sm' ? 'small' : 'medium';
18
+ size === 'xs' || size === 'sm' ? 'small' : 'medium';
19
19
 
20
20
  type MuiChipColor =
21
21
  | 'primary'
@@ -13,10 +13,13 @@ export type ChipVariant = 'filled' | 'outlined';
13
13
  * `ChipSize` nowhere and reached MUI as an unknown value, and the chip silently
14
14
  * rendered at the default `medium`. Twelve of them are doing that today.
15
15
  *
16
- * Only two entries, because a MUI chip only draws two. Inventing `lg` here would
17
- * put a size in the type that nothing can render.
16
+ * All five stops, and `Chip.tsx` draws them: MUI gives a chip two heights, so
17
+ * `xs`/`sm` map to small and `md` and up to medium. Offering fewer than five
18
+ * here is what made callers write `size="sm"` against a type that did not have
19
+ * it — the scale is the vocabulary, and a component that speaks a dialect of it
20
+ * is the problem, not the solution.
18
21
  */
19
- export type ChipSize = Extract<SizeValue, 'sm' | 'md'>;
22
+ export type ChipSize = SizeValue;
20
23
 
21
24
  /**
22
25
  * The colours a chip can draw — THE HOUSE VOCABULARY, identical to `ButtonProps`.
@@ -26,7 +26,7 @@ export interface DateTimeDisplayProps {
26
26
  * Size variant
27
27
  * @default 'medium'
28
28
  */
29
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
29
+ size?: SizeValue;
30
30
  /**
31
31
  * Whether to show a tooltip with full date/time
32
32
  * @default true
@@ -20,7 +20,7 @@ export interface InteractiveTooltipProps extends Omit<MuiTooltipProps, 'variant'
20
20
  /**
21
21
  * Size of the tooltip
22
22
  */
23
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
23
+ size?: SizeValue;
24
24
 
25
25
  /**
26
26
  * Maximum width of the tooltip
@@ -4,15 +4,19 @@ export const SIZE_MAP: Record<
4
4
  LoadingStateSize,
5
5
  { spinner: number; text: 'body2' | 'body1' | 'h6' }
6
6
  > = {
7
+ xs: { spinner: 16, text: 'body2' },
7
8
  sm: { spinner: 24, text: 'body2' },
8
9
  md: { spinner: 40, text: 'body1' },
9
10
  lg: { spinner: 56, text: 'h6' },
11
+ xl: { spinner: 64, text: 'h6' },
10
12
  };
11
13
 
12
14
  export const SKELETON_ROW_HEIGHT: Record<LoadingStateSize, number> = {
15
+ xs: 14,
13
16
  sm: 20,
14
17
  md: 28,
15
18
  lg: 36,
19
+ xl: 44,
16
20
  };
17
21
 
18
22
  type LoadingStateDefaultedKeys = 'variant' | 'size' | 'skeletonRows';
@@ -1,6 +1,6 @@
1
1
  import type { SizeValue } from '../../../tokens/scales';
2
2
  export type LoadingStateVariant = 'spinner' | 'skeleton';
3
- export type LoadingStateSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
3
+ export type LoadingStateSize = SizeValue;
4
4
 
5
5
  export interface LoadingStateProps {
6
6
  /**
@@ -34,6 +34,7 @@ export const getColorFromTheme = (theme: Theme, color: string): PaletteColor =>
34
34
  success: theme.palette.success,
35
35
  warning: theme.palette.warning,
36
36
  error: theme.palette.error,
37
+ info: theme.palette.info,
37
38
  danger: theme.palette.error, // Add danger alias
38
39
  };
39
40
 
@@ -42,9 +43,11 @@ export const getColorFromTheme = (theme: Theme, color: string): PaletteColor =>
42
43
  };
43
44
 
44
45
  const SIZE_MAP: Record<ProgressSize, { height: number; circularSize: number; fontSize: string }> = {
46
+ xs: { height: 2, circularSize: 24, fontSize: '0.625rem' },
45
47
  sm: { height: 4, circularSize: 32, fontSize: '0.75rem' },
46
48
  md: { height: 6, circularSize: 40, fontSize: '0.875rem' },
47
49
  lg: { height: 8, circularSize: 48, fontSize: '1rem' },
50
+ xl: { height: 10, circularSize: 56, fontSize: '1.125rem' },
48
51
  };
49
52
 
50
53
  export const getSizeStyles = (size?: ProgressSize) => SIZE_MAP[size as ProgressSize] || SIZE_MAP.md;
@@ -2,7 +2,7 @@ import type { ColorValue, SizeValue } from '../../../tokens/scales';
2
2
  import type { LinearProgressProps } from '@mui/material';
3
3
 
4
4
  export type ProgressVariant = 'linear' | 'circular' | 'segmented' | 'gradient' | 'glass';
5
- export type ProgressSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
5
+ export type ProgressSize = SizeValue;
6
6
 
7
7
  export interface ProgressProps extends Omit<LinearProgressProps, 'variant' | 'color'> {
8
8
  /**
@@ -18,7 +18,7 @@ export interface ProgressProps extends Omit<LinearProgressProps, 'variant' | 'co
18
18
  /**
19
19
  * The color of the progress
20
20
  */
21
- color?: Exclude<ColorValue, 'info'>;
21
+ color?: ColorValue;
22
22
 
23
23
  /**
24
24
  * Whether the progress should have a glow effect
@@ -14,7 +14,7 @@ export interface SheetProps {
14
14
  * "the whole viewport", so it extends rather than replaces.
15
15
  */
16
16
  size?: SizeValue | 'full';
17
- color?: Exclude<ColorValue, 'neutral'>;
17
+ color?: ColorValue;
18
18
  glow?: boolean;
19
19
  pulse?: boolean;
20
20
  glass?: boolean;
@@ -8,6 +8,18 @@ import {
8
8
  import React from 'react';
9
9
 
10
10
  import type { Step, StepItemProps, StepperProps } from './Stepper.types';
11
+ import type { SizeValue } from '../../../tokens/scales';
12
+
13
+ /**
14
+ * The five house stops, drawn rather than collapsed.
15
+ *
16
+ * These were two-way ternaries on `sm`, which is what a two-value size prop
17
+ * allows; with the full scale they have to be real values or the three new
18
+ * stops would render identically to `md` and the prop would be a lie.
19
+ */
20
+ const STEP_MIN_WIDTH: Record<SizeValue, number> = { xs: 44, sm: 56, md: 80, lg: 96, xl: 112 };
21
+ const STEP_ICON_SIZE: Record<SizeValue, number> = { xs: 14, sm: 18, md: 24, lg: 30, xl: 36 };
22
+
11
23
 
12
24
  const StepperRoot = styled(Box, {
13
25
  shouldForwardProp: (prop) => prop !== 'orientation',
@@ -37,12 +49,12 @@ const StepItem = styled(Box, {
37
49
  }));
38
50
 
39
51
  /** Circle diameter per size. */
40
- const CIRCLE_SIZE = { sm: 32, md: 48 } as const;
52
+ const CIRCLE_SIZE: Record<SizeValue, number> = { xs: 24, sm: 32, md: 48, lg: 60, xl: 72 };
41
53
 
42
54
  const StepButton = styled(Button, {
43
55
  shouldForwardProp: (prop) =>
44
56
  !['isActive', 'isCompleted', 'clickable', 'stepSize'].includes(prop as string),
45
- })<{ isActive: boolean; isCompleted: boolean; clickable: boolean; stepSize: 'sm' | 'md' }>(
57
+ })<{ isActive: boolean; isCompleted: boolean; clickable: boolean; stepSize: SizeValue }>(
46
58
  ({ theme, isActive, isCompleted, clickable, stepSize }) => ({
47
59
  minWidth: CIRCLE_SIZE[stepSize],
48
60
  width: CIRCLE_SIZE[stepSize],
@@ -80,7 +92,7 @@ const StepButton = styled(Button, {
80
92
  const StepConnector = styled(Box, {
81
93
  shouldForwardProp: (prop) =>
82
94
  !['orientation', 'isCompleted', 'size'].includes(prop as string),
83
- })<{ orientation: 'horizontal' | 'vertical'; isCompleted: boolean; size: 'sm' | 'md' }>(
95
+ })<{ orientation: 'horizontal' | 'vertical'; isCompleted: boolean; size: SizeValue }>(
84
96
  ({ theme, orientation, isCompleted, size }) => ({
85
97
  backgroundColor: isCompleted ? theme.palette.primary.main : theme.palette.grey[300],
86
98
  transition: theme.transitions.create('background-color', {
@@ -115,7 +127,7 @@ const StepLabel = styled(Box, {
115
127
  marginTop: orientation === 'horizontal' ? 8 : 0,
116
128
  }));
117
129
 
118
- type StepSize = 'sm' | 'md';
130
+ type StepSize = SizeValue;
119
131
  type LabelVariant = 'caption' | 'body2';
120
132
 
121
133
  /** Space-joined state classes for a step (active / completed / optional). */
@@ -132,7 +144,7 @@ function stepClasses(isActive: boolean, isCompleted: boolean, optional?: boolean
132
144
  /** The circle content-column width per orientation/size. */
133
145
  function contentSx(orientation: 'horizontal' | 'vertical', size: StepSize) {
134
146
  return orientation === 'horizontal'
135
- ? { minWidth: size === 'sm' ? 56 : 80, maxWidth: 150, flex: '0 1 auto' }
147
+ ? { minWidth: STEP_MIN_WIDTH[size], maxWidth: 150, flex: '0 1 auto' }
136
148
  : { flexShrink: 0 };
137
149
  }
138
150
 
@@ -162,7 +174,7 @@ const StepCircle: React.FC<
162
174
  data-testid={`stepper-step-icon-${index}`}
163
175
  >
164
176
  {isCompleted ? (
165
- <CheckCircle sx={{ fontSize: size === 'sm' ? 18 : 24 }} />
177
+ <CheckCircle sx={{ fontSize: STEP_ICON_SIZE[size] }} />
166
178
  ) : (
167
179
  <Typography variant={labelVariant} fontWeight="bold">
168
180
  {index + 1}
@@ -41,7 +41,7 @@ export interface StepperProps {
41
41
  * takes far less vertical space — useful above the fold on mobile checkouts.
42
42
  * @default 'md'
43
43
  */
44
- size?: Extract<SizeValue, 'sm' | 'md'>;
44
+ size?: SizeValue;
45
45
 
46
46
  /**
47
47
  * Navigation variant controlling step access
@@ -89,7 +89,7 @@ export interface StepItemProps {
89
89
  isCompleted: boolean;
90
90
  isLast: boolean;
91
91
  orientation: 'horizontal' | 'vertical';
92
- size: 'sm' | 'md';
92
+ size: SizeValue;
93
93
  variant: 'linear' | 'non-linear';
94
94
  clickable: boolean;
95
95
  onStepClick?: (id: string) => void;
@@ -20,7 +20,7 @@ export interface TooltipProps extends Omit<MuiTooltipProps, 'variant'> {
20
20
  /**
21
21
  * Size of the tooltip
22
22
  */
23
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
23
+ size?: SizeValue;
24
24
 
25
25
  /**
26
26
  * Maximum width of the tooltip
@@ -46,6 +46,7 @@ export const getColorFromTheme = (theme: Theme, color: string): ColorPalette =>
46
46
  secondary: theme.palette.secondary as ColorPalette,
47
47
  success: theme.palette.success as ColorPalette,
48
48
  warning: theme.palette.warning as ColorPalette,
49
+ info: theme.palette.info as ColorPalette,
49
50
  danger: theme.palette.error as ColorPalette,
50
51
  };
51
52
 
@@ -23,7 +23,7 @@ export interface CommandProps {
23
23
  onSelect?: (item: CommandItem) => void;
24
24
  variant?: 'default' | 'glass' | 'gradient' | 'minimal' | 'elevated';
25
25
  size?: SizeValue;
26
- color?: Exclude<ColorValue, 'neutral'>;
26
+ color?: ColorValue;
27
27
  glow?: boolean;
28
28
  pulse?: boolean;
29
29
  loading?: boolean;
@@ -9,6 +9,7 @@ import { useId, useMemo } from 'react';
9
9
 
10
10
  import { FormControl, FormLabel, FormMessage } from '../Form';
11
11
  import type { CreatableSelectOption, CreatableSelectProps } from './CreatableSelect.types';
12
+ import type { SizeValue } from '../../../tokens/scales';
12
13
 
13
14
  /** Internal option shape: a real option, or the synthetic "create new" row. */
14
15
  interface InternalOption extends CreatableSelectOption {
@@ -25,7 +26,9 @@ const filterOptions = createFilterOptions<InternalOption>();
25
26
  const INDENT_STEP = 16;
26
27
 
27
28
  /** `sm`/`md` → MUI's own scale, the same mapping {@link Input} uses. */
28
- const MUI_SIZE = { sm: 'small', md: 'medium' } as const;
29
+ // All five house stops onto MUI's two. `xs`/`sm` draw small; `md` and up draw
30
+ // medium, because an autocomplete field has no third or fourth height to give.
31
+ const MUI_SIZE = { xs: 'small', sm: 'small', md: 'medium', lg: 'medium', xl: 'medium' } as const;
29
32
 
30
33
  /** Match the outlined border treatment of `Input` so siblings look identical. */
31
34
  const fieldSx = {
@@ -92,7 +95,7 @@ function renderField(
92
95
  placeholder?: string;
93
96
  error?: string;
94
97
  loading: boolean;
95
- size: 'sm' | 'md';
98
+ size: SizeValue;
96
99
  dataTestId: string;
97
100
  },
98
101
  ): React.JSX.Element {
@@ -45,7 +45,7 @@ export interface CreatableSelectProps {
45
45
  * Control height, matching {@link Input}'s scale so the two line up when they
46
46
  * share a grid row (default: `md`).
47
47
  */
48
- size?: Extract<SizeValue, 'sm' | 'md'>;
48
+ size?: SizeValue;
49
49
  /** Base test id; the input is `<dataTestId>-input`. */
50
50
  dataTestId?: string;
51
51
  }
@@ -34,10 +34,14 @@ export const muiVariantFor = (variant: InputVariant): 'outlined' | 'filled' | 's
34
34
  }
35
35
  };
36
36
 
37
+ // MUI gives a text field two heights, so the five house stops are drawn as two
38
+ // plus padding: `xs` tightens below small, `lg`/`xl` open up above medium.
37
39
  export const SIZE_MAP = {
40
+ xs: { size: 'small' as const, sx: { '& .MuiInputBase-input': { padding: '6px 10px' } } },
38
41
  sm: { size: 'small' as const },
39
42
  md: { size: 'medium' as const },
40
43
  lg: { size: 'medium' as const, sx: { '& .MuiInputBase-input': { padding: '16px 14px' } } },
44
+ xl: { size: 'medium' as const, sx: { '& .MuiInputBase-input': { padding: '20px 16px' } } },
41
45
  };
42
46
 
43
47
  export const glowStyles = (theme: Theme): CSSObject => ({
@@ -4,7 +4,7 @@ import type { InputHTMLAttributes, ReactNode } from 'react';
4
4
  import type React from 'react';
5
5
 
6
6
  export type InputVariant = 'outlined' | 'filled' | 'glass' | 'underline' | 'gradient';
7
- export type InputSize = Extract<SizeValue, 'sm' | 'md' | 'lg'>;
7
+ export type InputSize = SizeValue;
8
8
 
9
9
  export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'color'> {
10
10
  variant?: InputVariant;
@@ -23,6 +23,7 @@ const namedPalette = (theme: Theme, color: string): ColorPalette => {
23
23
  secondary: theme.palette.secondary,
24
24
  success: theme.palette.success,
25
25
  warning: theme.palette.warning,
26
+ info: theme.palette.info,
26
27
  danger: theme.palette.error,
27
28
  };
28
29
 
@@ -9,7 +9,7 @@ export interface InputOTPProps {
9
9
  /**
10
10
  * The color theme of the OTP input
11
11
  */
12
- color?: Exclude<ColorValue, 'info'>;
12
+ color?: ColorValue;
13
13
 
14
14
  /**
15
15
  * The size of the OTP input
@@ -29,6 +29,7 @@ const namedPalette = (theme: Theme, color: string): ColorPalette => {
29
29
  secondary: theme.palette.secondary,
30
30
  success: theme.palette.success,
31
31
  warning: theme.palette.warning,
32
+ info: theme.palette.info,
32
33
  danger: theme.palette.error,
33
34
  };
34
35
 
@@ -19,7 +19,7 @@ export interface RadioGroupProps extends Omit<MuiRadioGroupProps, 'color'> {
19
19
  /**
20
20
  * The color theme of the radio group
21
21
  */
22
- color?: Exclude<ColorValue, 'info'>;
22
+ color?: ColorValue;
23
23
 
24
24
  /**
25
25
  * The size of the radio group
@@ -38,7 +38,7 @@ export interface SelectProps extends Omit<MuiSelectProps, 'variant' | 'size'> {
38
38
  * Size of the select component
39
39
  * @default 'medium'
40
40
  */
41
- size?: Extract<SizeValue, 'sm' | 'md'>;
41
+ size?: SizeValue;
42
42
  /**
43
43
  * Placeholder text when no option is selected
44
44
  */
@@ -25,6 +25,7 @@ const namedPalette = (theme: Theme, color: string): ColorPalette => {
25
25
  secondary: theme.palette.secondary,
26
26
  success: theme.palette.success,
27
27
  warning: theme.palette.warning,
28
+ info: theme.palette.info,
28
29
  danger: theme.palette.error,
29
30
  };
30
31
 
@@ -10,7 +10,7 @@ export interface SliderProps extends Omit<MuiSliderProps, 'color' | 'size'> {
10
10
  /**
11
11
  * The color theme of the slider
12
12
  */
13
- color?: Exclude<ColorValue, 'info'>;
13
+ color?: ColorValue;
14
14
 
15
15
  /**
16
16
  * The size of the slider
@@ -35,6 +35,7 @@ const namedPalette = (theme: Theme, color: string): ColorPalette => {
35
35
  secondary: theme.palette.secondary,
36
36
  success: theme.palette.success,
37
37
  warning: theme.palette.warning,
38
+ info: theme.palette.info,
38
39
  danger: theme.palette.error,
39
40
  };
40
41
 
@@ -11,7 +11,7 @@ export interface SwitchProps extends Omit<MuiSwitchProps, 'color' | 'size'> {
11
11
  /**
12
12
  * The color theme of the switch
13
13
  */
14
- color?: Exclude<ColorValue, 'info'>;
14
+ color?: ColorValue;
15
15
 
16
16
  /**
17
17
  * The size of the switch
@@ -80,6 +80,7 @@ export const getColorFromTheme = (theme: Theme, color: string): ResolvedPalette
80
80
  secondary: theme.palette.secondary,
81
81
  success: theme.palette.success,
82
82
  warning: theme.palette.warning,
83
+ info: theme.palette.info,
83
84
  danger: theme.palette.error,
84
85
  };
85
86
 
@@ -11,7 +11,7 @@ export interface TextareaProps extends Omit<TextareaAutosizeProps, 'variant' | '
11
11
  /**
12
12
  * The color theme of the textarea
13
13
  */
14
- color?: Exclude<ColorValue, 'info'>;
14
+ color?: ColorValue;
15
15
 
16
16
  /**
17
17
  * The size of the textarea
@@ -42,6 +42,7 @@ export const getColorFromTheme = (theme: Theme, color: string): TogglePalette =>
42
42
  secondary: theme.palette.secondary,
43
43
  success: theme.palette.success,
44
44
  warning: theme.palette.warning,
45
+ info: theme.palette.info,
45
46
  danger: theme.palette.error,
46
47
  };
47
48
 
@@ -11,7 +11,7 @@ export interface ToggleProps extends Omit<ToggleButtonProps, 'color' | 'size'> {
11
11
  /**
12
12
  * The color theme of the toggle
13
13
  */
14
- color?: Exclude<ColorValue, 'info'>;
14
+ color?: ColorValue;
15
15
 
16
16
  /**
17
17
  * The size of the toggle
@@ -20,6 +20,7 @@ const getColorFromTheme = (theme: Theme, color: string) => {
20
20
  secondary: theme.palette.secondary,
21
21
  success: theme.palette.success,
22
22
  warning: theme.palette.warning,
23
+ info: theme.palette.info,
23
24
  danger: theme.palette.error,
24
25
  };
25
26
 
@@ -18,7 +18,7 @@ export interface ToggleGroupProps extends Omit<ToggleButtonGroupProps, 'color' |
18
18
  /**
19
19
  * The color theme of the toggle group
20
20
  */
21
- color?: Exclude<ColorValue, 'info'>;
21
+ color?: ColorValue;
22
22
 
23
23
  /**
24
24
  * The size of the toggle group
@@ -207,6 +207,9 @@ export const breadcrumbLinkStyles = ({
207
207
  padding: theme.spacing(0.375, 0.5) } });
208
208
 
209
209
  export const sizeIconMap = {
210
- sm: 'small' as const,
211
- md: 'small' as const,
212
- lg: 'medium' as const };
210
+ xs: 'small',
211
+ sm: 'small',
212
+ md: 'small',
213
+ lg: 'medium',
214
+ xl: 'medium',
215
+ } as const;
@@ -106,7 +106,7 @@ export interface BreadcrumbsProps extends Omit<MuiBreadcrumbsProps, 'separator'>
106
106
  * Size of the breadcrumbs
107
107
  * @default 'md'
108
108
  */
109
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
109
+ size?: SizeValue;
110
110
 
111
111
  /**
112
112
  * Color scheme for separators and accents
@@ -83,7 +83,7 @@ export interface ContextMenuProps extends Omit<MenuProps, 'open' | 'anchorEl' |
83
83
  /**
84
84
  * Size of the menu items
85
85
  */
86
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
86
+ size?: SizeValue;
87
87
 
88
88
  /**
89
89
  * Custom className for the trigger element
@@ -102,7 +102,7 @@ export interface DropdownMenuProps extends Omit<MenuProps, 'open' | 'variant'> {
102
102
  /**
103
103
  * Size of the menu items
104
104
  */
105
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
105
+ size?: SizeValue;
106
106
 
107
107
  /**
108
108
  * Maximum height of the menu
@@ -86,7 +86,7 @@ export interface NavigationMenuProps {
86
86
  /**
87
87
  * Size of the menu items
88
88
  */
89
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
89
+ size?: SizeValue;
90
90
 
91
91
  /**
92
92
  * Minimal style with no borders, shadows, or container backgrounds
@@ -11,7 +11,7 @@ export interface PaginationProps extends Omit<MuiPaginationProps, 'variant' | 's
11
11
  /**
12
12
  * Size of the pagination buttons
13
13
  */
14
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
14
+ size?: SizeValue;
15
15
 
16
16
  /**
17
17
  * Current page number
@@ -58,7 +58,7 @@ export interface TabsProps extends Omit<MuiTabsProps, 'variant' | 'children' | '
58
58
  /**
59
59
  * Size of the tabs
60
60
  */
61
- size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
61
+ size?: SizeValue;
62
62
 
63
63
  /**
64
64
  * Array of tab items
@@ -20,6 +20,7 @@ const getColorFromTheme = (theme: Theme, color: string) => {
20
20
  secondary: theme.palette.secondary,
21
21
  success: theme.palette.success,
22
22
  warning: theme.palette.warning,
23
+ info: theme.palette.info,
23
24
  danger: theme.palette.error,
24
25
  };
25
26
 
@@ -7,6 +7,6 @@ export interface BlockquoteProps extends React.HTMLAttributes<HTMLElement> {
7
7
  variant?: BlockquoteVariant;
8
8
  author?: string;
9
9
  source?: string;
10
- color?: Exclude<ColorValue, 'info'>;
10
+ color?: ColorValue;
11
11
  children: React.ReactNode;
12
12
  }
@@ -8,6 +8,6 @@ export interface CodeProps extends React.HTMLAttributes<HTMLElement> {
8
8
  language?: string;
9
9
  copyable?: boolean;
10
10
  lineNumbers?: boolean;
11
- size?: Extract<SizeValue, 'xs' | 'sm' | 'md' | 'lg'>;
11
+ size?: SizeValue;
12
12
  children: React.ReactNode;
13
13
  }
@@ -1,24 +1,32 @@
1
1
  import type { CSSObject, Theme } from '@mui/material';
2
2
 
3
3
  import type { HeadingProps } from './Heading.types';
4
+ import type { ColorValue } from '../../../tokens/scales';
4
5
 
5
6
  type Level = NonNullable<HeadingProps['level']>;
6
7
  type Weight = NonNullable<HeadingProps['weight']>;
7
8
 
8
- const getColorFromTheme = (theme: Theme, color: string): string => {
9
+ // Exhaustive over ColorValue, and deliberately without a fallback. The map
10
+ // happened to hold all seven already, but it was `Record<string, string>` with
11
+ // `|| text.primary` behind it — so nothing kept it that way, and the eighth
12
+ // colour would have rendered as ordinary body text with no complaint. That is
13
+ // the failure this whole vocabulary exists to make impossible; `Text` was
14
+ // tightened the same way and this one was missed.
15
+ const getColorFromTheme = (theme: Theme, color: ColorValue): string => {
9
16
  if (color === 'neutral') {
10
17
  return theme.palette.text.primary;
11
18
  }
12
19
 
13
- const colorMap: Record<string, string> = {
20
+ const colorMap: Record<Exclude<ColorValue, 'neutral'>, string> = {
14
21
  primary: theme.palette.primary.main,
15
22
  secondary: theme.palette.secondary.main,
16
23
  success: theme.palette.success.main,
17
24
  warning: theme.palette.warning.main,
25
+ info: theme.palette.info.main,
18
26
  danger: theme.palette.error.main,
19
27
  };
20
28
 
21
- return colorMap[color] || theme.palette.text.primary;
29
+ return colorMap[color];
22
30
  };
23
31
 
24
32
  const WEIGHTS: Record<Weight, number> = {
@@ -50,22 +58,30 @@ const PRIMARY_STOPS = (theme: Theme): [string, string] => [
50
58
  theme.palette.secondary.main,
51
59
  ];
52
60
 
53
- const GRADIENT_STOPS: Record<string, (theme: Theme) => [string, string]> = {
61
+ // Keyed over the whole vocabulary, with no `?? PRIMARY_STOPS` behind it. Two
62
+ // stops were missing — `info` and `neutral` — so a gradient heading in either
63
+ // colour painted the PRIMARY gradient and looked deliberate while ignoring the
64
+ // prop entirely. The fallback is what hid it, so the fallback is gone.
65
+ const GRADIENT_STOPS: Record<ColorValue, (theme: Theme) => [string, string]> = {
54
66
  primary: PRIMARY_STOPS,
55
67
  secondary: (theme) => [theme.palette.secondary.main, theme.palette.primary.main],
56
68
  success: (theme) => [theme.palette.success.light, theme.palette.success.dark],
57
69
  warning: (theme) => [theme.palette.warning.light, theme.palette.warning.dark],
70
+ info: (theme) => [theme.palette.info.light, theme.palette.info.dark],
58
71
  danger: (theme) => [theme.palette.error.light, theme.palette.error.dark],
72
+ // The grey ramp has no light/dark pair, and every stop on it is
73
+ // `string | undefined` under `noUncheckedIndexedAccess`.
74
+ neutral: (theme) => [theme.palette.grey[500] ?? '#9e9e9e', theme.palette.grey[900] ?? '#212121'],
59
75
  };
60
76
 
61
- const gradientFor = (theme: Theme, color: string) => {
62
- const [from, to] = (GRADIENT_STOPS[color] ?? PRIMARY_STOPS)(theme);
77
+ const gradientFor = (theme: Theme, color: ColorValue) => {
78
+ const [from, to] = GRADIENT_STOPS[color](theme);
63
79
  return `linear-gradient(135deg, ${from} 0%, ${to} 100%)`;
64
80
  };
65
81
 
66
82
  export interface HeadingFlags {
67
83
  customLevel?: string;
68
- customColor?: string;
84
+ customColor?: ColorValue;
69
85
  customWeight?: string;
70
86
  gradient?: boolean;
71
87
  }
@@ -5,7 +5,7 @@ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'display';
5
5
 
6
6
  export interface HeadingProps extends React.HTMLAttributes<globalThis.HTMLHeadingElement> {
7
7
  level?: HeadingLevel;
8
- color?: Exclude<ColorValue, 'info'>;
8
+ color?: ColorValue;
9
9
  weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
10
10
  gradient?: boolean;
11
11
  children: React.ReactNode;
@@ -15,6 +15,7 @@ const getColorFromTheme = (theme: Theme, color: string) => {
15
15
  secondary: theme.palette.secondary.main,
16
16
  success: theme.palette.success.main,
17
17
  warning: theme.palette.warning.main,
18
+ info: theme.palette.info.main,
18
19
  danger: theme.palette.error.main,
19
20
  };
20
21
 
@@ -5,7 +5,7 @@ export type ParagraphVariant = 'default' | 'lead' | 'muted' | 'small';
5
5
 
6
6
  export interface ParagraphProps extends React.HTMLAttributes<globalThis.HTMLParagraphElement> {
7
7
  variant?: ParagraphVariant;
8
- color?: Exclude<ColorValue, 'info'>;
8
+ color?: ColorValue;
9
9
  size?: SizeValue;
10
10
  children: React.ReactNode;
11
11
  }
@@ -6,8 +6,9 @@ import { styled } from '@mui/material/styles';
6
6
  import React from 'react';
7
7
 
8
8
  import type { TextProps } from './Text.types';
9
+ import type { ColorValue } from '../../../tokens/scales';
9
10
 
10
- const getColorFromTheme = (theme: Theme, color: string) => {
11
+ const getColorFromTheme = (theme: Theme, color: ColorValue): string => {
11
12
  if (color === 'neutral') {
12
13
  return theme.palette.text.primary;
13
14
  }
@@ -16,14 +17,19 @@ const getColorFromTheme = (theme: Theme, color: string) => {
16
17
  return theme.palette.text.secondary;
17
18
  }
18
19
 
19
- const colorMap: Record<string, string> = {
20
+ // Exhaustive over ColorValue on purpose. It was a partial `Record<string, …>`
21
+ // with a silent `|| text.primary` fallback, so `info` — accepted by the type
22
+ // — rendered as ordinary body text. Typing the key means a colour added to
23
+ // the vocabulary cannot reach here without a home.
24
+ const colorMap: Record<Exclude<ColorValue, 'neutral' | 'secondary'>, string> = {
20
25
  primary: theme.palette.primary.main,
21
26
  success: theme.palette.success.main,
22
27
  warning: theme.palette.warning.main,
28
+ info: theme.palette.info.main,
23
29
  danger: theme.palette.error.main,
24
30
  };
25
31
 
26
- return colorMap[color] || theme.palette.text.primary;
32
+ return colorMap[color];
27
33
  };
28
34
 
29
35
  const SIZE_MAP: Record<string, { fontSize: string; lineHeight: number }> = {
@@ -45,7 +51,7 @@ const WEIGHT_MAP: Record<string, number> = {
45
51
  interface TextStyleArgs {
46
52
  theme: Theme;
47
53
  customVariant?: string;
48
- customColor?: string;
54
+ customColor?: ColorValue;
49
55
  customSize?: string;
50
56
  customWeight?: string;
51
57
  italic?: boolean;
@@ -119,7 +125,7 @@ const StyledText = styled(Typography, {
119
125
  ].includes(prop as string),
120
126
  })<{
121
127
  customVariant?: string;
122
- customColor?: string;
128
+ customColor?: ColorValue;
123
129
  customSize?: string;
124
130
  customWeight?: string;
125
131
  italic?: boolean;
@@ -5,7 +5,7 @@ export type TextVariant = 'body' | 'heading' | 'caption' | 'code';
5
5
 
6
6
  export interface TextProps extends React.HTMLAttributes<HTMLSpanElement> {
7
7
  variant?: TextVariant;
8
- color?: Exclude<ColorValue, 'info'>;
8
+ color?: ColorValue;
9
9
  size?: SizeValue;
10
10
  weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
11
11
  as?: keyof React.JSX.IntrinsicElements;
@@ -24,6 +24,7 @@ import {
24
24
  import type { AnimationSize, AnimationVariant } from './AnimatedIcon.types';
25
25
 
26
26
  export const sizeConfigs: Record<AnimationSize, { size: number; fontSize: number }> = {
27
+ xs: { size: 12, fontSize: 10 },
27
28
  sm: { size: 24, fontSize: 20 },
28
29
  md: { size: 32, fontSize: 28 },
29
30
  lg: { size: 48, fontSize: 44 },
@@ -20,7 +20,7 @@ export type AnimationVariant =
20
20
  | 'neonFlicker'
21
21
  | 'breathe';
22
22
 
23
- export type AnimationSize = Extract<SizeValue, 'sm' | 'md' | 'lg' | 'xl'>;
23
+ export type AnimationSize = SizeValue;
24
24
 
25
25
  export type ShadowStyle = 'soft' | 'hard' | 'elevated' | 'none';
26
26
 
@@ -19,7 +19,7 @@ export type WorkflowStepOrientation = 'horizontal' | 'vertical';
19
19
  /**
20
20
  * Color theme for the workflow step component
21
21
  */
22
- export type WorkflowStepColor = Exclude<ColorValue, 'info'>;
22
+ export type WorkflowStepColor = ColorValue;
23
23
 
24
24
  /**
25
25
  * Size variant for step indicators
@@ -18,13 +18,13 @@ import type { Theme } from '@mui/material/styles';
18
18
  *
19
19
  * So the vocabulary is declared once, here, and components derive from it.
20
20
  *
21
- * DERIVE, DON'T COPY, AND SUBSET HONESTLY. A component that cannot draw the
22
- * whole scale narrows with `Extract` rather than restating a union — an
23
- * independent copy is what drifts. And it narrows rather than accepting the lot:
24
- * a MUI chip draws two sizes, so putting `lg` in its type would promise
25
- * something nothing can render.
26
- *
27
- * size?: Extract<SizeValue, 'sm' | 'md'>;
21
+ * DERIVE, DON'T COPY — AND TAKE THE WHOLE VOCABULARY. Every size prop is
22
+ * `SizeValue` and every colour prop is `ColorValue`, with no `Extract` and no
23
+ * `Exclude`. Handing a component a slice was tried and undone: it is the same
24
+ * fragmentation wearing the canonical type's name, and it leaves a caller having
25
+ * to remember which components accept `xs` and which accept `info`. Where the
26
+ * underlying widget draws fewer steps than five, that is a rendering detail and
27
+ * belongs at the MUI boundary below — not in the prop's type.
28
28
  *
29
29
  * A component whose size or colour is not this concept at all keeps its own type
30
30
  * — `Scrollbar`'s `thin|medium|thick` is a track width, `Sheet`'s `full` is a
@@ -37,9 +37,17 @@ import type { Theme } from '@mui/material/styles';
37
37
  *
38
38
  * Abbreviated, because that is what 26 of the 30 components with a real size
39
39
  * already used and what every caller has been taught to write.
40
+ *
41
+ * Declared as the array and the type together so the two cannot disagree. The
42
+ * runtime list is what Storybook `argTypes.options` and the demo grids iterate;
43
+ * spelled by hand in each story it drifts, and it had — twelve colour lists
44
+ * omitted `info`, so the control that documents the vocabulary offered a
45
+ * component less than the component accepted.
40
46
  */
41
47
  export type SizeValue = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
42
48
 
49
+ export const SIZE_VALUES = ['xs', 'sm', 'md', 'lg', 'xl'] as const satisfies readonly SizeValue[];
50
+
43
51
  /**
44
52
  * The house colour vocabulary.
45
53
  *
@@ -53,6 +61,8 @@ export type SizeValue = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
53
61
  * component calls its unaccented state). The translation must be a real mapping
54
62
  * and never a cast: a cast is precisely what let `danger` through untouched and
55
63
  * had MUI fall back to grey.
64
+ *
65
+ * Array and type declared together, for the reason given on {@link SIZE_VALUES}.
56
66
  */
57
67
  export type ColorValue =
58
68
  | 'primary'
@@ -63,6 +73,47 @@ export type ColorValue =
63
73
  | 'danger'
64
74
  | 'neutral';
65
75
 
76
+ export const COLOR_VALUES = [
77
+ 'primary',
78
+ 'secondary',
79
+ 'success',
80
+ 'warning',
81
+ 'info',
82
+ 'danger',
83
+ 'neutral',
84
+ ] as const satisfies readonly ColorValue[];
85
+
86
+ /* ── Array and union, pinned to each other ────────────────────────────────── */
87
+
88
+ /**
89
+ * The arrays above are written out rather than derived, because deriving the
90
+ * union from the array (`(typeof COLOR_VALUES)[number]`) costs the alias its
91
+ * name: TypeScript resolves the indexed access eagerly, and every error in the
92
+ * package stops saying `not assignable to type 'ColorValue'` and starts dumping
93
+ * seven string literals. In a package whose entire purpose is teaching this
94
+ * vocabulary, the name in the error message is worth keeping.
95
+ *
96
+ * Writing both means they could disagree, so both directions are checked here.
97
+ * `satisfies` above catches an entry that is not a member. These catch the other
98
+ * half — a member missing from the array — which `satisfies` cannot see and
99
+ * which is the direction that actually bit: twelve story control lists offered
100
+ * six colours for a vocabulary of seven, so `info` was undocumented everywhere.
101
+ *
102
+ * A gap makes the assignment below fail, and the error names the missing member.
103
+ */
104
+ type Missing<Union extends string, Listed extends string> = Exclude<Union, Listed>;
105
+
106
+ const _allSizesListed: Missing<SizeValue, (typeof SIZE_VALUES)[number]> extends never
107
+ ? true
108
+ : never = true;
109
+
110
+ const _allColorsListed: Missing<ColorValue, (typeof COLOR_VALUES)[number]> extends never
111
+ ? true
112
+ : never = true;
113
+
114
+ void _allSizesListed;
115
+ void _allColorsListed;
116
+
66
117
  /* ── The MUI boundary ─────────────────────────────────────────────────────── */
67
118
 
68
119
  /** What MUI calls a size. Three entries against our five. */