@12-apps/ui 2.1.0 → 3.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 (75) hide show
  1. package/components-catalog.md +8 -0
  2. package/package.json +2 -1
  3. package/src/components/data-display/Alert/Alert.types.ts +1 -1
  4. package/src/components/data-display/Avatar/Avatar.types.ts +1 -1
  5. package/src/components/data-display/Avatar/Avatar.view.tsx +5 -1
  6. package/src/components/data-display/Badge/Badge.styles.ts +11 -2
  7. package/src/components/data-display/Badge/Badge.types.ts +1 -1
  8. package/src/components/data-display/Carousel/Carousel.types.ts +4 -4
  9. package/src/components/data-display/Chart/Chart.types.ts +1 -1
  10. package/src/components/data-display/Chart/ChartSpec.types.ts +1 -1
  11. package/src/components/data-display/Chip/Chip.tsx +1 -1
  12. package/src/components/data-display/Chip/Chip.types.ts +6 -3
  13. package/src/components/data-display/DataViews/data-views-day-input.tsx +43 -22
  14. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +1 -1
  15. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +1 -1
  16. package/src/components/data-display/LoadingState/LoadingState.helpers.ts +4 -0
  17. package/src/components/data-display/LoadingState/LoadingState.types.ts +1 -1
  18. package/src/components/data-display/Progress/Progress.styles.ts +3 -0
  19. package/src/components/data-display/Progress/Progress.types.ts +2 -2
  20. package/src/components/data-display/Sheet/Sheet.types.ts +1 -1
  21. package/src/components/data-display/Stepper/Stepper.tsx +18 -6
  22. package/src/components/data-display/Stepper/Stepper.types.ts +2 -2
  23. package/src/components/data-display/Tooltip/Tooltip.types.ts +1 -1
  24. package/src/components/form/Button/Button.styles.ts +1 -0
  25. package/src/components/form/Command/Command.types.ts +1 -1
  26. package/src/components/form/CreatableSelect/CreatableSelect.tsx +5 -2
  27. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +1 -1
  28. package/src/components/form/DateRangePicker/DateRangePicker.dates.ts +149 -0
  29. package/src/components/form/DateRangePicker/DateRangePicker.hooks.ts +87 -0
  30. package/src/components/form/DateRangePicker/DateRangePicker.md +228 -0
  31. package/src/components/form/DateRangePicker/DateRangePicker.quick.ts +97 -0
  32. package/src/components/form/DateRangePicker/DateRangePicker.range.ts +105 -0
  33. package/src/components/form/DateRangePicker/DateRangePicker.tsx +146 -0
  34. package/src/components/form/DateRangePicker/DateRangePicker.types.ts +193 -0
  35. package/src/components/form/DateRangePicker/DateRangePickerFields.tsx +90 -0
  36. package/src/components/form/DateRangePicker/DateRangePickerQuickList.tsx +123 -0
  37. package/src/components/form/DateRangePicker/index.ts +25 -0
  38. package/src/components/form/Input/Input.styles.ts +4 -0
  39. package/src/components/form/Input/Input.types.ts +1 -1
  40. package/src/components/form/InputOTP/InputOTP.styles.ts +1 -0
  41. package/src/components/form/InputOTP/InputOTP.types.ts +1 -1
  42. package/src/components/form/RadioGroup/RadioGroup.styles.ts +1 -0
  43. package/src/components/form/RadioGroup/RadioGroup.types.ts +1 -1
  44. package/src/components/form/Select/Select.types.ts +1 -1
  45. package/src/components/form/Slider/Slider.styles.ts +1 -0
  46. package/src/components/form/Slider/Slider.types.ts +1 -1
  47. package/src/components/form/Switch/Switch.styles.ts +1 -0
  48. package/src/components/form/Switch/Switch.types.ts +1 -1
  49. package/src/components/form/Textarea/Textarea.styles.ts +1 -0
  50. package/src/components/form/Textarea/Textarea.types.ts +1 -1
  51. package/src/components/form/Toggle/Toggle.styles.ts +1 -0
  52. package/src/components/form/Toggle/Toggle.types.ts +1 -1
  53. package/src/components/form/ToggleGroup/ToggleGroup.tsx +1 -0
  54. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +1 -1
  55. package/src/components/form/index.ts +1 -0
  56. package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +6 -3
  57. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +1 -1
  58. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +1 -1
  59. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +1 -1
  60. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +1 -1
  61. package/src/components/navigation/Pagination/Pagination.types.ts +1 -1
  62. package/src/components/navigation/Tabs/Tabs.types.ts +1 -1
  63. package/src/components/typography/Blockquote/Blockquote.tsx +1 -0
  64. package/src/components/typography/Blockquote/Blockquote.types.ts +1 -1
  65. package/src/components/typography/Code/Code.types.ts +1 -1
  66. package/src/components/typography/Heading/Heading.styles.ts +23 -7
  67. package/src/components/typography/Heading/Heading.types.ts +1 -1
  68. package/src/components/typography/Paragraph/Paragraph.tsx +1 -0
  69. package/src/components/typography/Paragraph/Paragraph.types.ts +1 -1
  70. package/src/components/typography/Text/Text.tsx +11 -5
  71. package/src/components/typography/Text/Text.types.ts +1 -1
  72. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +1 -0
  73. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +1 -1
  74. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +1 -1
  75. package/src/tokens/scales.ts +58 -7
@@ -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
@@ -7,6 +7,7 @@ export * from './CepField';
7
7
  export * from './Checkbox';
8
8
  export * from './CodeEditor';
9
9
  export * from './Command';
10
+ export * from './DateRangePicker';
10
11
  export * from './Form';
11
12
  export * from './Input';
12
13
  export * from './InputOTP';
@@ -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. */