@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.
- package/package.json +3 -1
- package/src/components/data-display/Alert/Alert.types.ts +2 -1
- package/src/components/data-display/Avatar/Avatar.types.ts +10 -2
- package/src/components/data-display/Badge/Badge.types.ts +3 -2
- package/src/components/data-display/Carousel/Carousel.parts.tsx +12 -10
- package/src/components/data-display/Carousel/Carousel.styles.ts +4 -3
- package/src/components/data-display/Carousel/Carousel.tsx +2 -1
- package/src/components/data-display/Carousel/Carousel.types.ts +7 -6
- package/src/components/data-display/Chart/Chart.tsx +2 -1
- package/src/components/data-display/Chart/Chart.types.ts +3 -2
- package/src/components/data-display/Chart/ChartSpec.types.ts +6 -7
- package/src/components/data-display/Chart/SpecChart.tsx +2 -1
- package/src/components/data-display/Chart/chart-internals.ts +3 -2
- package/src/components/data-display/Chart/chart-spec.ts +11 -1
- package/src/components/data-display/Chip/Chip.tsx +23 -3
- package/src/components/data-display/Chip/Chip.types.ts +37 -3
- package/src/components/data-display/Chip/chip.md +7 -3
- package/src/components/data-display/Chip/index.ts +1 -1
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +10 -0
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +3 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +113 -62
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +4 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +23 -51
- package/src/components/data-display/DataViews/index.ts +4 -0
- package/src/components/data-display/DataViews/list-card-rails.tsx +29 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +8 -5
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +2 -1
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +2 -1
- package/src/components/data-display/LoadingState/LoadingState.types.ts +2 -1
- package/src/components/data-display/Progress/Progress.types.ts +3 -2
- package/src/components/data-display/Sheet/Sheet.body.tsx +2 -1
- package/src/components/data-display/Sheet/Sheet.styles.ts +3 -2
- package/src/components/data-display/Sheet/Sheet.types.ts +7 -2
- package/src/components/data-display/Stepper/Stepper.types.ts +2 -1
- package/src/components/data-display/Table/Table.styles.ts +1 -1
- package/src/components/data-display/Table/Table.types.ts +3 -1
- package/src/components/data-display/Tooltip/Tooltip.types.ts +2 -1
- package/src/components/feedback/Dialog/Dialog.types.ts +2 -1
- package/src/components/feedback/Modal/Modal.types.ts +2 -1
- package/src/components/form/Button/Button.types.ts +3 -2
- package/src/components/form/Calendar/CalendarMonth.tsx +6 -1
- package/src/components/form/Command/Command.styles.ts +8 -7
- package/src/components/form/Command/Command.types.ts +3 -2
- package/src/components/form/CreatableSelect/CreatableSelect.types.ts +2 -1
- package/src/components/form/Input/Input.types.ts +2 -1
- package/src/components/form/InputOTP/InputOTP.types.ts +3 -2
- package/src/components/form/Label/Label.styles.ts +8 -5
- package/src/components/form/Label/Label.tsx +1 -1
- package/src/components/form/Label/Label.types.ts +3 -2
- package/src/components/form/Menubar/Menubar.styles.ts +10 -8
- package/src/components/form/Menubar/Menubar.tsx +2 -1
- package/src/components/form/Menubar/Menubar.types.ts +7 -6
- package/src/components/form/RadioGroup/RadioGroup.types.ts +3 -2
- package/src/components/form/Select/Select.tsx +13 -1
- package/src/components/form/Select/Select.types.ts +5 -2
- package/src/components/form/Slider/Slider.types.ts +3 -2
- package/src/components/form/Switch/Switch.types.ts +3 -2
- package/src/components/form/Textarea/Textarea.types.ts +3 -2
- package/src/components/form/Toggle/Toggle.types.ts +3 -2
- package/src/components/form/ToggleGroup/ToggleGroup.types.ts +3 -2
- package/src/components/layout/Separator/Separator.types.ts +2 -1
- package/src/components/layout/Spacer/Spacer.types.ts +2 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +7 -2
- package/src/components/navigation/ContextMenu/ContextMenu.types.ts +3 -2
- package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +1 -0
- package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +12 -2
- package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +7 -2
- package/src/components/navigation/Pagination/Pagination.tsx +3 -1
- package/src/components/navigation/Pagination/Pagination.types.ts +5 -3
- package/src/components/navigation/Tabs/Tabs.types.ts +3 -2
- package/src/components/typography/Blockquote/Blockquote.types.ts +2 -1
- package/src/components/typography/Code/Code.types.ts +2 -1
- package/src/components/typography/Heading/Heading.types.ts +2 -1
- package/src/components/typography/Paragraph/Paragraph.types.ts +3 -2
- package/src/components/typography/Text/Text.types.ts +3 -2
- package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +2 -1
- package/src/components/utility/Overflow/index.ts +6 -0
- package/src/components/utility/Overflow/overflow.ts +125 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +3 -2
- package/src/components/utility/index.ts +1 -0
- package/src/tokens/scales.ts +153 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
export interface NavigationMenuItem {
|
|
@@ -76,12 +77,16 @@ export interface NavigationMenuProps {
|
|
|
76
77
|
/**
|
|
77
78
|
* Color scheme of the menu
|
|
78
79
|
*/
|
|
79
|
-
|
|
80
|
+
/**
|
|
81
|
+
* `neutral` is what this called `default`: the unaccented state, renamed to
|
|
82
|
+
* the house word rather than kept as a third spelling of the same idea.
|
|
83
|
+
*/
|
|
84
|
+
color?: Extract<ColorValue, 'primary' | 'secondary' | 'neutral'>;
|
|
80
85
|
|
|
81
86
|
/**
|
|
82
87
|
* Size of the menu items
|
|
83
88
|
*/
|
|
84
|
-
size?: 'sm' | 'md' | 'lg'
|
|
89
|
+
size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
92
|
* Minimal style with no borders, shadows, or container backgrounds
|
|
@@ -139,7 +139,9 @@ const PaginationControl: React.FC<
|
|
|
139
139
|
showFirstButton={p.showFirstButton && !isDots}
|
|
140
140
|
showLastButton={p.showLastButton && !isDots}
|
|
141
141
|
disabled={p.disabled}
|
|
142
|
-
|
|
142
|
+
// MUI spells the unaccented pagination colour `standard`; ours is
|
|
143
|
+
// `neutral`, translated here rather than carried as a second name.
|
|
144
|
+
color={p.color === undefined ? undefined : p.color === 'neutral' ? 'standard' : p.color}
|
|
143
145
|
renderItem={renderItem}
|
|
144
146
|
{...rest}
|
|
145
147
|
/>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type { PaginationProps as MuiPaginationProps } from '@mui/material';
|
|
2
3
|
import type { ReactNode } from 'react';
|
|
3
4
|
|
|
4
|
-
export interface PaginationProps extends Omit<MuiPaginationProps, 'variant' | 'size'> {
|
|
5
|
+
export interface PaginationProps extends Omit<MuiPaginationProps, 'variant' | 'size' | 'color'> {
|
|
5
6
|
/**
|
|
6
7
|
* The variant of the pagination
|
|
7
8
|
*/
|
|
@@ -10,7 +11,7 @@ export interface PaginationProps extends Omit<MuiPaginationProps, 'variant' | 's
|
|
|
10
11
|
/**
|
|
11
12
|
* Size of the pagination buttons
|
|
12
13
|
*/
|
|
13
|
-
size?: 'sm' | 'md' | 'lg'
|
|
14
|
+
size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Current page number
|
|
@@ -85,7 +86,8 @@ export interface PaginationProps extends Omit<MuiPaginationProps, 'variant' | 's
|
|
|
85
86
|
/**
|
|
86
87
|
* Custom color for the active page
|
|
87
88
|
*/
|
|
88
|
-
|
|
89
|
+
/** `neutral` is what MUI spells `standard`; it is translated at the boundary. */
|
|
90
|
+
color?: Extract<ColorValue, 'primary' | 'secondary' | 'neutral'>;
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
93
|
* Whether to show page info text
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type { TabsProps as MuiTabsProps } from '@mui/material';
|
|
2
3
|
import type { ReactNode } from 'react';
|
|
3
4
|
|
|
@@ -57,7 +58,7 @@ export interface TabsProps extends Omit<MuiTabsProps, 'variant' | 'children' | '
|
|
|
57
58
|
/**
|
|
58
59
|
* Size of the tabs
|
|
59
60
|
*/
|
|
60
|
-
size?: 'sm' | 'md' | 'lg'
|
|
61
|
+
size?: Extract<SizeValue, 'sm' | 'md' | 'lg'>;
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* Array of tab items
|
|
@@ -77,7 +78,7 @@ export interface TabsProps extends Omit<MuiTabsProps, 'variant' | 'children' | '
|
|
|
77
78
|
/**
|
|
78
79
|
* Color theme for the tabs
|
|
79
80
|
*/
|
|
80
|
-
color?: 'primary' | 'secondary'
|
|
81
|
+
color?: Extract<ColorValue, 'primary' | 'secondary'>;
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* Whether tabs should fill the full width
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ColorValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type BlockquoteVariant = 'default' | 'bordered' | 'citation';
|
|
@@ -6,6 +7,6 @@ export interface BlockquoteProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
6
7
|
variant?: BlockquoteVariant;
|
|
7
8
|
author?: string;
|
|
8
9
|
source?: string;
|
|
9
|
-
color?:
|
|
10
|
+
color?: Exclude<ColorValue, 'info'>;
|
|
10
11
|
children: React.ReactNode;
|
|
11
12
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type CodeVariant = 'inline' | 'block' | 'highlight';
|
|
@@ -7,6 +8,6 @@ export interface CodeProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
7
8
|
language?: string;
|
|
8
9
|
copyable?: boolean;
|
|
9
10
|
lineNumbers?: boolean;
|
|
10
|
-
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
11
|
+
size?: Extract<SizeValue, 'xs' | 'sm' | 'md' | 'lg'>;
|
|
11
12
|
children: React.ReactNode;
|
|
12
13
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { ColorValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'display';
|
|
4
5
|
|
|
5
6
|
export interface HeadingProps extends React.HTMLAttributes<globalThis.HTMLHeadingElement> {
|
|
6
7
|
level?: HeadingLevel;
|
|
7
|
-
color?:
|
|
8
|
+
color?: Exclude<ColorValue, 'info'>;
|
|
8
9
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
|
9
10
|
gradient?: boolean;
|
|
10
11
|
children: React.ReactNode;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type ParagraphVariant = 'default' | 'lead' | 'muted' | 'small';
|
|
4
5
|
|
|
5
6
|
export interface ParagraphProps extends React.HTMLAttributes<globalThis.HTMLParagraphElement> {
|
|
6
7
|
variant?: ParagraphVariant;
|
|
7
|
-
color?:
|
|
8
|
-
size?:
|
|
8
|
+
color?: Exclude<ColorValue, 'info'>;
|
|
9
|
+
size?: SizeValue;
|
|
9
10
|
children: React.ReactNode;
|
|
10
11
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type TextVariant = 'body' | 'heading' | 'caption' | 'code';
|
|
4
5
|
|
|
5
6
|
export interface TextProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
6
7
|
variant?: TextVariant;
|
|
7
|
-
color?:
|
|
8
|
-
size?:
|
|
8
|
+
color?: Exclude<ColorValue, 'info'>;
|
|
9
|
+
size?: SizeValue;
|
|
9
10
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
|
10
11
|
as?: keyof React.JSX.IntrinsicElements;
|
|
11
12
|
italic?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
export type AnimationVariant =
|
|
@@ -19,7 +20,7 @@ export type AnimationVariant =
|
|
|
19
20
|
| 'neonFlicker'
|
|
20
21
|
| 'breathe';
|
|
21
22
|
|
|
22
|
-
export type AnimationSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
23
|
+
export type AnimationSize = Extract<SizeValue, 'sm' | 'md' | 'lg' | 'xl'>;
|
|
23
24
|
|
|
24
25
|
export type ShadowStyle = 'soft' | 'hard' | 'elevated' | 'none';
|
|
25
26
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MEASURE A ROW, THEN DECIDE WHAT FITS — the two pieces every "…and the rest
|
|
7
|
+
* goes in the menu" cluster needs, and the only two that are not specific to
|
|
8
|
+
* what is being collapsed.
|
|
9
|
+
*
|
|
10
|
+
* It was extracted from `DataViews`' filter toolbar (`data-views-overflow`),
|
|
11
|
+
* which still owns everything filter-shaped about that bar: what a pill costs,
|
|
12
|
+
* that an APPLIED filter ranks ahead of an idle one, and the ladder that
|
|
13
|
+
* strips the search and the counter. None of that generalises. What does is
|
|
14
|
+
* the pair below — observe a container's width, and greedily keep what fits —
|
|
15
|
+
* and a second copy of a resize-measurement loop is exactly the kind of thing
|
|
16
|
+
* that drifts silently from the first.
|
|
17
|
+
*
|
|
18
|
+
* Both are presentation only: nothing here removes an action, changes a
|
|
19
|
+
* handler, or decides that something should not be reachable. What does not
|
|
20
|
+
* fit is meant to be rendered SOMEWHERE ELSE by the caller — a menu, a panel —
|
|
21
|
+
* never dropped.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Observe an element's content width. `0` until the first measurement lands.
|
|
26
|
+
*
|
|
27
|
+
* Zero is a meaningful answer, not a missing one: an SSR pass and a jsdom test
|
|
28
|
+
* both have no `ResizeObserver`, and the callers here read `0` as "degrade
|
|
29
|
+
* nothing" — keep every control inline. That is the honest fallback, because
|
|
30
|
+
* the alternative hides controls behind a trigger that the same environment
|
|
31
|
+
* cannot show either.
|
|
32
|
+
*/
|
|
33
|
+
export function useMeasuredWidth<T extends HTMLElement = HTMLDivElement>(): {
|
|
34
|
+
ref: RefObject<T | null>;
|
|
35
|
+
width: number;
|
|
36
|
+
} {
|
|
37
|
+
const ref = useRef<T | null>(null);
|
|
38
|
+
const [width, setWidth] = useState(0);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const element = ref.current;
|
|
41
|
+
if (!element || typeof ResizeObserver === "undefined") return undefined;
|
|
42
|
+
const observer = new ResizeObserver((entries) => {
|
|
43
|
+
const entry = entries[0];
|
|
44
|
+
if (entry) setWidth(entry.contentRect.width);
|
|
45
|
+
});
|
|
46
|
+
observer.observe(element);
|
|
47
|
+
return () => observer.disconnect();
|
|
48
|
+
}, []);
|
|
49
|
+
return { ref, width };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What one split costs the row, so a caller can price whatever comes next. */
|
|
53
|
+
export interface FitResult<Item> {
|
|
54
|
+
/** Rendered on the row, in the order they were DECLARED. */
|
|
55
|
+
inline: Item[];
|
|
56
|
+
/** Everything that had no room, also in declared order. */
|
|
57
|
+
overflow: Item[];
|
|
58
|
+
/** Width the surviving items occupy, including the overflow trigger. */
|
|
59
|
+
used: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FitRequest<Item> {
|
|
63
|
+
/** Rendered width of one item, however the caller prices it. */
|
|
64
|
+
widthOf: (item: Item) => number;
|
|
65
|
+
/** Stable identity — the split is decided as a set, then re-ordered. */
|
|
66
|
+
keyOf: (item: Item) => string;
|
|
67
|
+
/** The gap between two items, charged once per item kept. */
|
|
68
|
+
gap: number;
|
|
69
|
+
/** Room the items have to share, with fixed furniture already subtracted. */
|
|
70
|
+
available: number;
|
|
71
|
+
/**
|
|
72
|
+
* What the overflow trigger costs — charged ONLY when something overflows,
|
|
73
|
+
* because a row where everything fits renders no trigger at all.
|
|
74
|
+
*/
|
|
75
|
+
overflowCost: number;
|
|
76
|
+
/**
|
|
77
|
+
* The order in which items claim the visible slots. Defaults to the declared
|
|
78
|
+
* order; pass a re-ordering to rank some items ahead of others.
|
|
79
|
+
*
|
|
80
|
+
* Ranking is not exempting. A high-ranked item still overflows once even the
|
|
81
|
+
* first slots run out, which is what stops a row of "important" items from
|
|
82
|
+
* claiming more width than it has and painting past its own edge.
|
|
83
|
+
*/
|
|
84
|
+
priority?: readonly Item[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Keep what fits, shed the rest — ONE AT A TIME, never all at once.
|
|
89
|
+
*
|
|
90
|
+
* The all-or-nothing version throws away room that was there: with four items
|
|
91
|
+
* and space for three, hiding all four leaves the row emptier than it needed
|
|
92
|
+
* to be.
|
|
93
|
+
*
|
|
94
|
+
* Items claim slots in `priority` order but are RENDERED in declared order:
|
|
95
|
+
* which ones are visible may change with the width, and that is the point —
|
|
96
|
+
* but the ones that stay must not reshuffle under the pointer.
|
|
97
|
+
*/
|
|
98
|
+
export function splitToFit<Item>(
|
|
99
|
+
items: readonly Item[],
|
|
100
|
+
request: FitRequest<Item>,
|
|
101
|
+
): FitResult<Item> {
|
|
102
|
+
const { widthOf, keyOf, gap, available, overflowCost, priority } = request;
|
|
103
|
+
const cost = (list: readonly Item[]): number =>
|
|
104
|
+
list.reduce((sum, item) => sum + widthOf(item) + gap, 0);
|
|
105
|
+
|
|
106
|
+
if (cost(items) <= available) {
|
|
107
|
+
return { inline: [...items], overflow: [], used: cost(items) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// There IS an overflow, so its trigger now costs room too.
|
|
111
|
+
let room = available - overflowCost;
|
|
112
|
+
const keep = new Set<string>();
|
|
113
|
+
for (const item of priority ?? items) {
|
|
114
|
+
const next = widthOf(item) + gap;
|
|
115
|
+
if (next > room) continue;
|
|
116
|
+
room -= next;
|
|
117
|
+
keep.add(keyOf(item));
|
|
118
|
+
}
|
|
119
|
+
const inline = items.filter((item) => keep.has(keyOf(item)));
|
|
120
|
+
return {
|
|
121
|
+
inline,
|
|
122
|
+
overflow: items.filter((item) => !keep.has(keyOf(item))),
|
|
123
|
+
used: cost(inline) + overflowCost,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
1
2
|
import type { ComponentProps } from 'react';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -18,12 +19,12 @@ export type WorkflowStepOrientation = 'horizontal' | 'vertical';
|
|
|
18
19
|
/**
|
|
19
20
|
* Color theme for the workflow step component
|
|
20
21
|
*/
|
|
21
|
-
export type WorkflowStepColor =
|
|
22
|
+
export type WorkflowStepColor = Exclude<ColorValue, 'info'>;
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Size variant for step indicators
|
|
25
26
|
*/
|
|
26
|
-
export type WorkflowStepSize =
|
|
27
|
+
export type WorkflowStepSize = SizeValue;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Individual step item configuration
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Theme } from '@mui/material/styles';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE TWO VOCABULARIES EVERY COMPONENT SPEAKS.
|
|
5
|
+
*
|
|
6
|
+
* Before this file the package had both halves of every argument at once: 26
|
|
7
|
+
* components sized themselves `xs|sm|md|lg|xl` and four used MUI's
|
|
8
|
+
* `small|medium|large`; 14 coloured themselves `danger|neutral` and 15 used
|
|
9
|
+
* MUI's `error`. Nothing enforced either, because most of these props were
|
|
10
|
+
* spelled out as inline unions and a couple were plain `string`.
|
|
11
|
+
*
|
|
12
|
+
* That is not a tidiness problem. A caller learns the vocabulary from whichever
|
|
13
|
+
* component they used first, and the wrong half is not rejected — it reaches MUI
|
|
14
|
+
* as an unknown value and silently falls back. `Chip` shipped with `color?:
|
|
15
|
+
* string` and MUI's `small|medium`, so `color="danger"` rendered grey and
|
|
16
|
+
* `size="sm"` rendered medium; twelve live call sites were doing exactly that,
|
|
17
|
+
* and every test passed because they all asserted on text.
|
|
18
|
+
*
|
|
19
|
+
* So the vocabulary is declared once, here, and components derive from it.
|
|
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'>;
|
|
28
|
+
*
|
|
29
|
+
* A component whose size or colour is not this concept at all keeps its own type
|
|
30
|
+
* — `Scrollbar`'s `thin|medium|thick` is a track width, `Sheet`'s `full` is a
|
|
31
|
+
* viewport fraction, `LazyImage`'s size is pixels. Forcing those into the scale
|
|
32
|
+
* would be consistency for its own sake.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The house size scale.
|
|
37
|
+
*
|
|
38
|
+
* Abbreviated, because that is what 26 of the 30 components with a real size
|
|
39
|
+
* already used and what every caller has been taught to write.
|
|
40
|
+
*/
|
|
41
|
+
export type SizeValue = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The house colour vocabulary.
|
|
45
|
+
*
|
|
46
|
+
* `danger` and `neutral` are OURS; the rest are MUI's own names already. That
|
|
47
|
+
* sentence started life in `Button.tsx`, which is the component most callers
|
|
48
|
+
* meet first and therefore the one that taught the vocabulary — so it is the one
|
|
49
|
+
* the rest of the package follows.
|
|
50
|
+
*
|
|
51
|
+
* Components that hand these to MUI translate at the boundary (`danger` →
|
|
52
|
+
* `error`, `neutral` → `default` or `inherit` depending on what the underlying
|
|
53
|
+
* component calls its unaccented state). The translation must be a real mapping
|
|
54
|
+
* and never a cast: a cast is precisely what let `danger` through untouched and
|
|
55
|
+
* had MUI fall back to grey.
|
|
56
|
+
*/
|
|
57
|
+
export type ColorValue =
|
|
58
|
+
| 'primary'
|
|
59
|
+
| 'secondary'
|
|
60
|
+
| 'success'
|
|
61
|
+
| 'warning'
|
|
62
|
+
| 'info'
|
|
63
|
+
| 'danger'
|
|
64
|
+
| 'neutral';
|
|
65
|
+
|
|
66
|
+
/* ── The MUI boundary ─────────────────────────────────────────────────────── */
|
|
67
|
+
|
|
68
|
+
/** What MUI calls a size. Three entries against our five. */
|
|
69
|
+
export type MuiSize = 'small' | 'medium' | 'large';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The house scale collapsed onto MUI's three.
|
|
73
|
+
*
|
|
74
|
+
* Five onto three loses information, and it loses it at the ends on purpose:
|
|
75
|
+
* `xs`/`sm` both draw small and `lg`/`xl` both draw large, because MUI has no
|
|
76
|
+
* fourth and fifth step to give them. A component that genuinely needs five
|
|
77
|
+
* distinct sizes styles them itself rather than routing through here — this is
|
|
78
|
+
* for the ones that just forward the prop.
|
|
79
|
+
*/
|
|
80
|
+
export const muiSize = (size: SizeValue): MuiSize => {
|
|
81
|
+
if (size === 'xs' || size === 'sm') return 'small';
|
|
82
|
+
return size === 'md' ? 'medium' : 'large';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** What MUI calls a semantic colour on most components. */
|
|
86
|
+
export type MuiColor = 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'error';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The house vocabulary translated for MUI, with the caller naming its neutral.
|
|
90
|
+
*
|
|
91
|
+
* `neutral` is the awkward one: MUI spells the unaccented state `default` on a
|
|
92
|
+
* chip, `inherit` on a button and an icon button, and `grey` in the palette. So
|
|
93
|
+
* the caller passes the word its own MUI component uses rather than this
|
|
94
|
+
* pretending there is one answer.
|
|
95
|
+
*
|
|
96
|
+
* A MAPPING, NEVER A CAST. The cast is what let `danger` reach MUI untouched and
|
|
97
|
+
* silently fall back to grey.
|
|
98
|
+
*/
|
|
99
|
+
export function muiColor<N extends string>(color: ColorValue, neutralAs: N): MuiColor | N {
|
|
100
|
+
if (color === 'danger') return 'error';
|
|
101
|
+
return color === 'neutral' ? neutralAs : color;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The house vocabulary as a `theme.palette` key.
|
|
106
|
+
*
|
|
107
|
+
* Separate from {@link muiColor} because the palette has no `default` and no
|
|
108
|
+
* `inherit` — an unaccented colour there is `grey`.
|
|
109
|
+
*/
|
|
110
|
+
export type PaletteKey = MuiColor | 'grey';
|
|
111
|
+
|
|
112
|
+
export const paletteKey = (color: ColorValue): PaletteKey =>
|
|
113
|
+
color === 'danger' ? 'error' : color === 'neutral' ? 'grey' : color;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A `ColorValue` resolved to a four-stop accent from the theme.
|
|
117
|
+
*
|
|
118
|
+
* `paletteKey` is not enough on its own for anything that reads `.main`: MUI's
|
|
119
|
+
* palette has no neutral `PaletteColor`, and `palette.grey` is a 50–900 ramp
|
|
120
|
+
* with no `main`, `light`, `dark` or `contrastText` on it at all. So `neutral`
|
|
121
|
+
* is resolved from the ramp here, once, instead of every component discovering
|
|
122
|
+
* the same hole and branching around it.
|
|
123
|
+
*
|
|
124
|
+
* Typed against the theme structurally so this file does not drag in MUI's
|
|
125
|
+
* `Theme` and become a dependency of everything that imports a token.
|
|
126
|
+
*/
|
|
127
|
+
export interface Accent {
|
|
128
|
+
main: string;
|
|
129
|
+
light: string;
|
|
130
|
+
dark: string;
|
|
131
|
+
contrastText: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function accentFor(theme: Theme, color: ColorValue): Accent {
|
|
135
|
+
if (color === 'neutral') {
|
|
136
|
+
const grey = theme.palette.grey;
|
|
137
|
+
return {
|
|
138
|
+
// Explicit fallbacks: the ramp is indexed, and under
|
|
139
|
+
// `noUncheckedIndexedAccess` every stop is `string | undefined`.
|
|
140
|
+
main: grey[600] ?? '#757575',
|
|
141
|
+
light: grey[400] ?? '#bdbdbd',
|
|
142
|
+
dark: grey[800] ?? '#424242',
|
|
143
|
+
// Grey 600 carries white text in both themes.
|
|
144
|
+
contrastText: '#fff',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Past the `neutral` return, `paletteKey` can only produce a `MuiColor` — but
|
|
148
|
+
// its signature still admits `'grey'`, and TS cannot narrow a return type by
|
|
149
|
+
// the argument. Naming the key here keeps the accent branch honestly typed
|
|
150
|
+
// instead of casting the hole shut.
|
|
151
|
+
const key: MuiColor = color === 'danger' ? 'error' : color;
|
|
152
|
+
return theme.palette[key];
|
|
153
|
+
}
|