@dashforge/tw 0.1.0-beta → 0.2.1-beta
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/A11Y.md +130 -0
- package/CHANGELOG.md +231 -0
- package/dist/index.d.ts +1 -82
- package/dist/index.esm.js +1608 -11
- package/dist/src/components/AspectRatio/AspectRatio.d.ts +36 -0
- package/dist/src/components/AspectRatio/AspectRatio.d.ts.map +1 -0
- package/dist/src/components/AspectRatio/aspectRatio.types.d.ts +50 -0
- package/dist/src/components/AspectRatio/aspectRatio.types.d.ts.map +1 -0
- package/dist/src/components/Box/Box.d.ts +36 -0
- package/dist/src/components/Box/Box.d.ts.map +1 -0
- package/dist/src/components/Box/box.types.d.ts +54 -0
- package/dist/src/components/Box/box.types.d.ts.map +1 -0
- package/dist/src/components/Box/box.variants.d.ts +402 -0
- package/dist/src/components/Box/box.variants.d.ts.map +1 -0
- package/dist/src/components/Button/Button.d.ts.map +1 -1
- package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/src/components/Container/Container.d.ts +35 -0
- package/dist/src/components/Container/Container.d.ts.map +1 -0
- package/dist/src/components/Container/container.types.d.ts +41 -0
- package/dist/src/components/Container/container.types.d.ts.map +1 -0
- package/dist/src/components/Container/container.variants.d.ts +93 -0
- package/dist/src/components/Container/container.variants.d.ts.map +1 -0
- package/dist/src/components/Divider/Divider.d.ts +50 -0
- package/dist/src/components/Divider/Divider.d.ts.map +1 -0
- package/dist/src/components/Divider/divider.types.d.ts +47 -0
- package/dist/src/components/Divider/divider.types.d.ts.map +1 -0
- package/dist/src/components/Divider/divider.variants.d.ts +129 -0
- package/dist/src/components/Divider/divider.variants.d.ts.map +1 -0
- package/dist/src/components/Grid/Grid.d.ts +32 -0
- package/dist/src/components/Grid/Grid.d.ts.map +1 -0
- package/dist/src/components/Grid/grid.types.d.ts +103 -0
- package/dist/src/components/Grid/grid.types.d.ts.map +1 -0
- package/dist/src/components/Grid/grid.variants.d.ts +453 -0
- package/dist/src/components/Grid/grid.variants.d.ts.map +1 -0
- package/dist/src/components/NumberField/NumberField.d.ts.map +1 -1
- package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/src/components/Stack/Stack.d.ts +24 -0
- package/dist/src/components/Stack/Stack.d.ts.map +1 -0
- package/dist/src/components/Stack/stack.types.d.ts +61 -0
- package/dist/src/components/Stack/stack.types.d.ts.map +1 -0
- package/dist/src/components/Stack/stack.variants.d.ts +163 -0
- package/dist/src/components/Stack/stack.variants.d.ts.map +1 -0
- package/dist/src/components/Typography/Typography.d.ts +42 -0
- package/dist/src/components/Typography/Typography.d.ts.map +1 -0
- package/dist/src/components/Typography/typography.types.d.ts +61 -0
- package/dist/src/components/Typography/typography.types.d.ts.map +1 -0
- package/dist/src/components/Typography/typography.variants.d.ts +167 -0
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -0
- package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts +42 -0
- package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts +42 -0
- package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts.map +1 -0
- package/dist/src/index.d.ts +23 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/AspectRatio/AspectRatio.test.tsx +173 -0
- package/src/components/AspectRatio/AspectRatio.tsx +73 -0
- package/src/components/AspectRatio/aspectRatio.types.ts +54 -0
- package/src/components/Box/Box.test.tsx +349 -0
- package/src/components/Box/Box.tsx +83 -0
- package/src/components/Box/box.types.ts +61 -0
- package/src/components/Box/box.variants.ts +211 -0
- package/src/components/Button/Button.tsx +11 -0
- package/src/components/Checkbox/Checkbox.tsx +26 -2
- package/src/components/Container/Container.test.tsx +209 -0
- package/src/components/Container/Container.tsx +74 -0
- package/src/components/Container/container.types.ts +45 -0
- package/src/components/Container/container.variants.ts +81 -0
- package/src/components/Divider/Divider.test.tsx +241 -0
- package/src/components/Divider/Divider.tsx +140 -0
- package/src/components/Divider/divider.types.ts +52 -0
- package/src/components/Divider/divider.variants.ts +115 -0
- package/src/components/Grid/Grid.test.tsx +321 -0
- package/src/components/Grid/Grid.tsx +148 -0
- package/src/components/Grid/grid.types.ts +112 -0
- package/src/components/Grid/grid.variants.ts +133 -0
- package/src/components/NumberField/NumberField.tsx +30 -2
- package/src/components/RadioGroup/RadioGroup.tsx +23 -1
- package/src/components/Stack/Stack.test.tsx +309 -0
- package/src/components/Stack/Stack.tsx +115 -0
- package/src/components/Stack/stack.types.ts +68 -0
- package/src/components/Stack/stack.variants.ts +80 -0
- package/src/components/Typography/Typography.test.tsx +230 -0
- package/src/components/Typography/Typography.tsx +112 -0
- package/src/components/Typography/typography.types.ts +65 -0
- package/src/components/Typography/typography.variants.ts +113 -0
- package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +86 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +59 -0
- package/src/components/VisuallyHidden/visuallyHidden.types.ts +45 -0
- package/src/index.ts +50 -1
- package/LICENSE +0 -21
- package/dist/components/AppShell/AppShell.d.ts +0 -32
- package/dist/components/AppShell/AppShell.d.ts.map +0 -1
- package/dist/components/AppShell/appShell.types.d.ts +0 -62
- package/dist/components/AppShell/appShell.types.d.ts.map +0 -1
- package/dist/components/AppShell/appShell.variants.d.ts +0 -65
- package/dist/components/AppShell/appShell.variants.d.ts.map +0 -1
- package/dist/components/Autocomplete/Autocomplete.d.ts +0 -32
- package/dist/components/Autocomplete/Autocomplete.d.ts.map +0 -1
- package/dist/components/Autocomplete/autocomplete.types.d.ts +0 -214
- package/dist/components/Autocomplete/autocomplete.types.d.ts.map +0 -1
- package/dist/components/Autocomplete/autocomplete.variants.d.ts +0 -214
- package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +0 -23
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +0 -98
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +0 -85
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +0 -1
- package/dist/components/Button/Button.d.ts +0 -44
- package/dist/components/Button/Button.d.ts.map +0 -1
- package/dist/components/Button/button.types.d.ts +0 -66
- package/dist/components/Button/button.types.d.ts.map +0 -1
- package/dist/components/Button/button.variants.d.ts +0 -104
- package/dist/components/Button/button.variants.d.ts.map +0 -1
- package/dist/components/Checkbox/Checkbox.d.ts +0 -31
- package/dist/components/Checkbox/Checkbox.d.ts.map +0 -1
- package/dist/components/Checkbox/checkbox.types.d.ts +0 -86
- package/dist/components/Checkbox/checkbox.types.d.ts.map +0 -1
- package/dist/components/Checkbox/checkbox.variants.d.ts +0 -109
- package/dist/components/Checkbox/checkbox.variants.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +0 -38
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +0 -80
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +0 -90
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +0 -1
- package/dist/components/DateTimePicker/DateTimePicker.d.ts +0 -49
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +0 -1
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +0 -95
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +0 -1
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +0 -165
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +0 -1
- package/dist/components/LeftNav/LeftNav.d.ts +0 -34
- package/dist/components/LeftNav/LeftNav.d.ts.map +0 -1
- package/dist/components/LeftNav/leftNav.types.d.ts +0 -136
- package/dist/components/LeftNav/leftNav.types.d.ts.map +0 -1
- package/dist/components/LeftNav/leftNav.variants.d.ts +0 -143
- package/dist/components/LeftNav/leftNav.variants.d.ts.map +0 -1
- package/dist/components/NumberField/NumberField.d.ts +0 -18
- package/dist/components/NumberField/NumberField.d.ts.map +0 -1
- package/dist/components/NumberField/numberField.types.d.ts +0 -79
- package/dist/components/NumberField/numberField.types.d.ts.map +0 -1
- package/dist/components/NumberField/numberField.variants.d.ts +0 -169
- package/dist/components/NumberField/numberField.variants.d.ts.map +0 -1
- package/dist/components/OTPField/OTPField.d.ts +0 -25
- package/dist/components/OTPField/OTPField.d.ts.map +0 -1
- package/dist/components/OTPField/otpField.types.d.ts +0 -74
- package/dist/components/OTPField/otpField.types.d.ts.map +0 -1
- package/dist/components/OTPField/otpField.variants.d.ts +0 -105
- package/dist/components/OTPField/otpField.variants.d.ts.map +0 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +0 -35
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +0 -1
- package/dist/components/RadioGroup/radioGroup.types.d.ts +0 -103
- package/dist/components/RadioGroup/radioGroup.types.d.ts.map +0 -1
- package/dist/components/RadioGroup/radioGroup.variants.d.ts +0 -166
- package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +0 -1
- package/dist/components/Snackbar/Snackbar.d.ts +0 -37
- package/dist/components/Snackbar/Snackbar.d.ts.map +0 -1
- package/dist/components/Snackbar/snackbar.types.d.ts +0 -86
- package/dist/components/Snackbar/snackbar.types.d.ts.map +0 -1
- package/dist/components/Snackbar/snackbar.variants.d.ts +0 -153
- package/dist/components/Snackbar/snackbar.variants.d.ts.map +0 -1
- package/dist/components/Switch/Switch.d.ts +0 -15
- package/dist/components/Switch/Switch.d.ts.map +0 -1
- package/dist/components/Switch/switch.types.d.ts +0 -47
- package/dist/components/Switch/switch.types.d.ts.map +0 -1
- package/dist/components/Switch/switch.variants.d.ts +0 -105
- package/dist/components/Switch/switch.variants.d.ts.map +0 -1
- package/dist/components/TextField/TextField.d.ts +0 -25
- package/dist/components/TextField/TextField.d.ts.map +0 -1
- package/dist/components/TextField/textField.types.d.ts +0 -72
- package/dist/components/TextField/textField.types.d.ts.map +0 -1
- package/dist/components/TextField/textField.variants.d.ts +0 -160
- package/dist/components/TextField/textField.variants.d.ts.map +0 -1
- package/dist/components/Textarea/Textarea.d.ts +0 -16
- package/dist/components/Textarea/Textarea.d.ts.map +0 -1
- package/dist/components/Textarea/textarea.types.d.ts +0 -63
- package/dist/components/Textarea/textarea.types.d.ts.map +0 -1
- package/dist/components/Textarea/textarea.variants.d.ts +0 -197
- package/dist/components/Textarea/textarea.variants.d.ts.map +0 -1
- package/dist/components/TopBar/TopBar.d.ts +0 -33
- package/dist/components/TopBar/TopBar.d.ts.map +0 -1
- package/dist/components/TopBar/topBar.types.d.ts +0 -49
- package/dist/components/TopBar/topBar.types.d.ts.map +0 -1
- package/dist/components/TopBar/topBar.variants.d.ts +0 -79
- package/dist/components/TopBar/topBar.variants.d.ts.map +0 -1
- package/dist/components/_shared/resolveValidationState.d.ts +0 -42
- package/dist/components/_shared/resolveValidationState.d.ts.map +0 -1
- package/dist/hooks/useAccessState.d.ts +0 -37
- package/dist/hooks/useAccessState.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/dist/utils/cn.d.ts +0 -26
- package/dist/utils/cn.d.ts.map +0 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
Fragment,
|
|
4
|
+
cloneElement,
|
|
5
|
+
forwardRef,
|
|
6
|
+
isValidElement,
|
|
7
|
+
type ElementType,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
12
|
+
import { cn } from '../../utils/cn.js';
|
|
13
|
+
import { stackVariants } from './stack.variants.js';
|
|
14
|
+
import type { StackProps } from './stack.types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Walk the children, inserting `divider` BETWEEN every consecutive pair.
|
|
18
|
+
*
|
|
19
|
+
* Implementation notes:
|
|
20
|
+
* • `React.Children.toArray` assigns auto-keys but does NOT recursively
|
|
21
|
+
* flatten Fragments — it treats them as opaque single children. So
|
|
22
|
+
* `<Stack><><a/><b/></><c/></Stack>` is 2 boundaries (fragment + c),
|
|
23
|
+
* yielding ONE divider. Hoist items out of the fragment when you
|
|
24
|
+
* need a divider between them. Documented + asserted in the tests.
|
|
25
|
+
* • Dividers are wrapped in `<Fragment>` with a deterministic key
|
|
26
|
+
* derived from the boundary index — stable across re-renders so
|
|
27
|
+
* React reconciles correctly when children re-order.
|
|
28
|
+
* • If `divider` is a valid element, we `cloneElement` once per
|
|
29
|
+
* boundary (cheaper than re-rendering the JSX expression N-1 times).
|
|
30
|
+
* For string/number dividers we wrap in a span automatically.
|
|
31
|
+
*/
|
|
32
|
+
function interleaveDividers(children: ReactNode, divider: ReactNode): ReactNode[] {
|
|
33
|
+
const items = Children.toArray(children);
|
|
34
|
+
if (items.length <= 1) return items;
|
|
35
|
+
|
|
36
|
+
const result: ReactNode[] = [];
|
|
37
|
+
items.forEach((child, i) => {
|
|
38
|
+
result.push(child);
|
|
39
|
+
if (i < items.length - 1) {
|
|
40
|
+
const key = `df-stack-divider-${i}`;
|
|
41
|
+
if (isValidElement(divider)) {
|
|
42
|
+
result.push(cloneElement(divider as ReactElement<{ key?: string }>, { key }));
|
|
43
|
+
} else {
|
|
44
|
+
result.push(<Fragment key={key}>{divider}</Fragment>);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `<Stack>` — flex container 1D, the layout primitive.
|
|
53
|
+
*
|
|
54
|
+
* This is the ONLY component in @dashforge/tw that does flex. Box
|
|
55
|
+
* doesn't, Grid does CSS Grid (not flex). The strict naming → engine
|
|
56
|
+
* mapping is the whole point: when you read `<Stack>` in a JSX tree,
|
|
57
|
+
* you instantly know it's flex. No `<Box display="flex" ...>` traps.
|
|
58
|
+
*
|
|
59
|
+
* Direction defaults to `'col'` (vertical stack) — the most common
|
|
60
|
+
* case for forms, sidebars, settings panels. Pass `direction="row"`
|
|
61
|
+
* for horizontal layouts (toolbars, button rows, breadcrumbs).
|
|
62
|
+
*
|
|
63
|
+
* The `divider` prop is the runtime-only piece: TV can't encode the
|
|
64
|
+
* "render this between each child" logic as a class, so we walk the
|
|
65
|
+
* children at render time. The walk is O(n); for n ≤ ~10 (typical
|
|
66
|
+
* Stack content) the cost is negligible. For very long Stacks (1000+
|
|
67
|
+
* items), prefer to render dividers as part of each child instead.
|
|
68
|
+
*
|
|
69
|
+
* When `asChild` is true, the divider prop is silently ignored — Slot
|
|
70
|
+
* requires a single child, and the N-1 insertion has nowhere to act.
|
|
71
|
+
*/
|
|
72
|
+
export const Stack = forwardRef<HTMLElement, StackProps>(
|
|
73
|
+
function Stack(props, ref) {
|
|
74
|
+
const {
|
|
75
|
+
direction,
|
|
76
|
+
align,
|
|
77
|
+
justify,
|
|
78
|
+
gap,
|
|
79
|
+
wrap,
|
|
80
|
+
fullWidth,
|
|
81
|
+
fullHeight,
|
|
82
|
+
divider,
|
|
83
|
+
as,
|
|
84
|
+
asChild = false,
|
|
85
|
+
sx,
|
|
86
|
+
children,
|
|
87
|
+
...rest
|
|
88
|
+
} = props;
|
|
89
|
+
|
|
90
|
+
const classes = cn(
|
|
91
|
+
stackVariants({ direction, align, justify, gap, wrap, fullWidth, fullHeight }),
|
|
92
|
+
sx,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (asChild) {
|
|
96
|
+
// Slot expects a single child; divider has no place here.
|
|
97
|
+
return (
|
|
98
|
+
<Slot ref={ref} className={classes} {...rest}>
|
|
99
|
+
{children as ReactElement}
|
|
100
|
+
</Slot>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const Tag = (as ?? 'div') as ElementType;
|
|
105
|
+
const content = divider != null ? interleaveDividers(children, divider) : children;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Tag ref={ref as never} className={classes} {...rest}>
|
|
109
|
+
{content}
|
|
110
|
+
</Tag>
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
Stack.displayName = 'Stack';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { StackVariants } from './stack.variants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for `<Stack>` — flex container 1D, the layout primitive.
|
|
6
|
+
*
|
|
7
|
+
* Axes:
|
|
8
|
+
* • direction — flex-direction (default 'col')
|
|
9
|
+
* • align — items-* (cross-axis)
|
|
10
|
+
* • justify — justify-* (main-axis)
|
|
11
|
+
* • gap — token-scale spacing step
|
|
12
|
+
* • wrap — flex-wrap
|
|
13
|
+
* • fullWidth/fullHeight — w-full / h-full
|
|
14
|
+
* • divider — node rendered N-1 times between children
|
|
15
|
+
*
|
|
16
|
+
* Polymorphism:
|
|
17
|
+
* • as — override the HTML tag (default 'div')
|
|
18
|
+
* • asChild — render via Radix Slot onto the single child
|
|
19
|
+
*
|
|
20
|
+
* What Stack does NOT do (route to other primitives):
|
|
21
|
+
* • Surface chrome (border, bg, shadow) → wrap in <Box>
|
|
22
|
+
* • 2D layout (rows AND columns) → use <Grid>
|
|
23
|
+
* • Text styling → <Typography>
|
|
24
|
+
*/
|
|
25
|
+
export interface StackProps
|
|
26
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'>,
|
|
27
|
+
Pick<StackVariants,
|
|
28
|
+
'direction' | 'align' | 'justify' | 'gap' | 'wrap'
|
|
29
|
+
| 'fullWidth' | 'fullHeight'> {
|
|
30
|
+
/**
|
|
31
|
+
* Node rendered N-1 times BETWEEN children — mirror of MUI Stack's
|
|
32
|
+
* divider prop. Useful for visual separators (a `<hr>`, a thin
|
|
33
|
+
* `<Box variant="outlined" sx="h-px border-0 border-t">`, an
|
|
34
|
+
* `<svg>` glyph) that need to follow the flex direction.
|
|
35
|
+
*
|
|
36
|
+
* The node is cloned for each insertion via `cloneElement`; pass a
|
|
37
|
+
* stable element (not a function) for best React reconciliation.
|
|
38
|
+
* `React.Children.toArray` flattens fragments before the walk, so
|
|
39
|
+
* `<Stack divider={...}><><...></></Stack>` works as expected.
|
|
40
|
+
*/
|
|
41
|
+
divider?: ReactNode;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Override the rendered HTML tag. Defaults to `'div'`. Use when the
|
|
45
|
+
* Stack also has semantic meaning — `<Stack as="nav">` for a nav
|
|
46
|
+
* bar, `<Stack as="ul">` for a list (children become `<li>` via
|
|
47
|
+
* native HTML, not by us).
|
|
48
|
+
*
|
|
49
|
+
* Ignored when `asChild` is true.
|
|
50
|
+
*/
|
|
51
|
+
as?: ElementType;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render via Radix `Slot` — the Stack styles paint onto the single
|
|
55
|
+
* React child instead of wrapping it. Mutually exclusive with `as`
|
|
56
|
+
* (when both are passed, `asChild` wins). The `divider` prop is
|
|
57
|
+
* ignored when `asChild` is true (the Slot pattern wraps a single
|
|
58
|
+
* node, so the N-1 insertion logic has no place to act).
|
|
59
|
+
*/
|
|
60
|
+
asChild?: boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Utility classes appended to the variant chain. Resolved via
|
|
64
|
+
* `tailwind-merge` so the consumer's classes always win over the
|
|
65
|
+
* variant defaults.
|
|
66
|
+
*/
|
|
67
|
+
sx?: string;
|
|
68
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `stackVariants` — flex container 1D, the layout primitive of @dashforge/tw.
|
|
5
|
+
*
|
|
6
|
+
* Architectural role (planned with the user — F9 deep dive):
|
|
7
|
+
*
|
|
8
|
+
* Stack is the ONLY way to do flex in this library. Box is the
|
|
9
|
+
* surface (border / bg / shadow); Stack is the arrangement (direction
|
|
10
|
+
* / align / justify / gap). Two primitives, two responsibilities,
|
|
11
|
+
* zero overlap.
|
|
12
|
+
*
|
|
13
|
+
* This rules out the MUI failure mode where every `<Box display="flex"
|
|
14
|
+
* gap={2}>` quietly becomes the de-facto flex container, drowning
|
|
15
|
+
* the surface vs layout distinction. Here, if you see `<Stack>` in
|
|
16
|
+
* the JSX, you KNOW it's flex; if you see `<Box>`, you KNOW it's
|
|
17
|
+
* not. The component name carries the intent.
|
|
18
|
+
*
|
|
19
|
+
* Axes:
|
|
20
|
+
* • direction — row / col (+ reverse variants)
|
|
21
|
+
* • align / justify — cross-axis / main-axis alignment
|
|
22
|
+
* • gap — token-scale step (mirror Box spacing scale)
|
|
23
|
+
* • wrap — flex-wrap
|
|
24
|
+
* • divider — runtime-only (handled in Stack.tsx, not here)
|
|
25
|
+
*
|
|
26
|
+
* Sizing (`fullWidth`, `fullHeight`) is duplicated from Box because
|
|
27
|
+
* Stack often plays the role of a full-width strip / full-height
|
|
28
|
+
* column — re-typing `sx="w-full"` every time would be friction.
|
|
29
|
+
*/
|
|
30
|
+
export const stackVariants = tv({
|
|
31
|
+
base: 'flex',
|
|
32
|
+
|
|
33
|
+
variants: {
|
|
34
|
+
direction: {
|
|
35
|
+
row: 'flex-row',
|
|
36
|
+
col: 'flex-col',
|
|
37
|
+
'row-reverse': 'flex-row-reverse',
|
|
38
|
+
'col-reverse': 'flex-col-reverse',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
align: {
|
|
42
|
+
start: 'items-start',
|
|
43
|
+
center: 'items-center',
|
|
44
|
+
end: 'items-end',
|
|
45
|
+
stretch: 'items-stretch',
|
|
46
|
+
baseline: 'items-baseline',
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
justify: {
|
|
50
|
+
start: 'justify-start',
|
|
51
|
+
center: 'justify-center',
|
|
52
|
+
end: 'justify-end',
|
|
53
|
+
between: 'justify-between',
|
|
54
|
+
around: 'justify-around',
|
|
55
|
+
evenly: 'justify-evenly',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* `gap` — explicit literal mapping for the 11 token-scale steps.
|
|
60
|
+
* Same set as Box's spacing axes (p, m, etc.) so muscle memory
|
|
61
|
+
* carries over: `<Stack gap={4}>` aligns visually with `<Box p={4}>`.
|
|
62
|
+
*/
|
|
63
|
+
gap: {
|
|
64
|
+
0: 'gap-0', '0.5': 'gap-0.5', 1: 'gap-1', 2: 'gap-2', 3: 'gap-3',
|
|
65
|
+
4: 'gap-4', 6: 'gap-6', 8: 'gap-8', 12: 'gap-12', 16: 'gap-16',
|
|
66
|
+
24: 'gap-24',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
wrap: { true: 'flex-wrap' },
|
|
70
|
+
|
|
71
|
+
fullWidth: { true: 'w-full' },
|
|
72
|
+
fullHeight: { true: 'h-full' },
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
defaultVariants: {
|
|
76
|
+
direction: 'col',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type StackVariants = VariantProps<typeof stackVariants>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import { Typography } from './Typography.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Suite mirrors the Button.test.tsx shape (rendering · variants · color ·
|
|
8
|
+
* override · polymorphism) so the test surface is uniform across the
|
|
9
|
+
* package.
|
|
10
|
+
*
|
|
11
|
+
* Strategy: render → query → assert on `tagName` (for HTML-element
|
|
12
|
+
* resolution) and `className` (for variant resolution). We deliberately
|
|
13
|
+
* DON'T snapshot: snapshots churn on every variant-class edit and offer
|
|
14
|
+
* no signal on the actual contract — the contract is "this variant
|
|
15
|
+
* emits THESE specific Tailwind utility classes", which we assert
|
|
16
|
+
* positively below.
|
|
17
|
+
*/
|
|
18
|
+
describe('<Typography>', () => {
|
|
19
|
+
// ─── Rendering / default tag mapping ────────────────────────────────
|
|
20
|
+
describe('default tag mapping', () => {
|
|
21
|
+
it('renders <p> for body1 by default', () => {
|
|
22
|
+
const { container } = render(<Typography>hello</Typography>);
|
|
23
|
+
expect(container.firstElementChild?.tagName).toBe('P');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders <h1> for variant="h1"', () => {
|
|
27
|
+
const { container } = render(<Typography variant="h1">title</Typography>);
|
|
28
|
+
expect(container.firstElementChild?.tagName).toBe('H1');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders <h3> for variant="h3"', () => {
|
|
32
|
+
const { container } = render(<Typography variant="h3">title</Typography>);
|
|
33
|
+
expect(container.firstElementChild?.tagName).toBe('H3');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders <span> for variant="caption"', () => {
|
|
37
|
+
const { container } = render(<Typography variant="caption">note</Typography>);
|
|
38
|
+
expect(container.firstElementChild?.tagName).toBe('SPAN');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders <span> for variant="overline"', () => {
|
|
42
|
+
const { container } = render(<Typography variant="overline">label</Typography>);
|
|
43
|
+
expect(container.firstElementChild?.tagName).toBe('SPAN');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Variant classes ────────────────────────────────────────────────
|
|
48
|
+
describe('variant → utility chain', () => {
|
|
49
|
+
it('h1 has text-5xl + font-bold', () => {
|
|
50
|
+
const { container } = render(<Typography variant="h1">x</Typography>);
|
|
51
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
52
|
+
expect(cls).toContain('text-5xl');
|
|
53
|
+
expect(cls).toContain('font-bold');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('body2 has text-sm', () => {
|
|
57
|
+
const { container } = render(<Typography variant="body2">x</Typography>);
|
|
58
|
+
expect(container.firstElementChild?.className).toContain('text-sm');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('overline has uppercase + tracking', () => {
|
|
62
|
+
const { container } = render(<Typography variant="overline">x</Typography>);
|
|
63
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
64
|
+
expect(cls).toContain('uppercase');
|
|
65
|
+
expect(cls).toMatch(/tracking-\[/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── Color intent ───────────────────────────────────────────────────
|
|
70
|
+
describe('color', () => {
|
|
71
|
+
it('color="primary" emits text-primary-* with dark variant', () => {
|
|
72
|
+
const { container } = render(<Typography color="primary">x</Typography>);
|
|
73
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
74
|
+
expect(cls).toContain('text-primary-700');
|
|
75
|
+
expect(cls).toContain('dark:text-primary-400');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('color="danger" emits text-danger-*', () => {
|
|
79
|
+
const { container } = render(<Typography color="danger">x</Typography>);
|
|
80
|
+
expect(container.firstElementChild?.className).toContain('text-danger-700');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('color="inherit" emits text-inherit', () => {
|
|
84
|
+
const { container } = render(<Typography color="inherit">x</Typography>);
|
|
85
|
+
expect(container.firstElementChild?.className).toContain('text-inherit');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── Override semantics ─────────────────────────────────────────────
|
|
90
|
+
describe('override', () => {
|
|
91
|
+
it('sx wins over variant defaults via tailwind-merge', () => {
|
|
92
|
+
const { container } = render(
|
|
93
|
+
<Typography variant="h1" color="primary" sx="text-pink-500">x</Typography>,
|
|
94
|
+
);
|
|
95
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
96
|
+
// tailwind-merge collapses the conflicting `text-*` colour to the last one
|
|
97
|
+
expect(cls).toContain('text-pink-500');
|
|
98
|
+
expect(cls).not.toContain('text-primary-700');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('weight prop overrides variant default weight', () => {
|
|
102
|
+
const { container } = render(<Typography variant="h1" weight="normal">x</Typography>);
|
|
103
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
104
|
+
// h1's default `font-bold` is dropped by tailwind-merge, replaced by font-normal
|
|
105
|
+
expect(cls).toContain('font-normal');
|
|
106
|
+
expect(cls).not.toContain('font-bold');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('truncate adds the truncate utility', () => {
|
|
110
|
+
const { container } = render(<Typography truncate>x</Typography>);
|
|
111
|
+
expect(container.firstElementChild?.className).toContain('truncate');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('gutterBottom adds mb-3', () => {
|
|
115
|
+
const { container } = render(<Typography gutterBottom>x</Typography>);
|
|
116
|
+
expect(container.firstElementChild?.className).toContain('mb-3');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── Polymorphism — `as` and `asChild` ───────────────────────────────
|
|
121
|
+
describe('polymorphism', () => {
|
|
122
|
+
it('as="section" overrides the variant-default tag', () => {
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<Typography variant="h1" as="section">x</Typography>,
|
|
125
|
+
);
|
|
126
|
+
expect(container.firstElementChild?.tagName).toBe('SECTION');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('asChild renders the single child element with merged className', () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<Typography variant="h2" color="primary" asChild>
|
|
132
|
+
<a href="#">link</a>
|
|
133
|
+
</Typography>,
|
|
134
|
+
);
|
|
135
|
+
const el = container.firstElementChild;
|
|
136
|
+
expect(el?.tagName).toBe('A');
|
|
137
|
+
expect(el?.className).toContain('text-4xl');
|
|
138
|
+
expect(el?.className).toContain('text-primary-700');
|
|
139
|
+
expect(el?.getAttribute('href')).toBe('#');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('asChild wins over `as` when both are passed', () => {
|
|
143
|
+
const { container } = render(
|
|
144
|
+
<Typography variant="h1" as="section" asChild>
|
|
145
|
+
<article>x</article>
|
|
146
|
+
</Typography>,
|
|
147
|
+
);
|
|
148
|
+
// Slot renders the child element, ignoring `as`
|
|
149
|
+
expect(container.firstElementChild?.tagName).toBe('ARTICLE');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Pass-through ────────────────────────────────────────────────────
|
|
154
|
+
describe('pass-through props', () => {
|
|
155
|
+
it('forwards data-* attributes', () => {
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<Typography data-testid="typo">x</Typography>,
|
|
158
|
+
);
|
|
159
|
+
expect(container.firstElementChild?.getAttribute('data-testid')).toBe('typo');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('forwards aria-label', () => {
|
|
163
|
+
const { container } = render(
|
|
164
|
+
<Typography aria-label="heading">x</Typography>,
|
|
165
|
+
);
|
|
166
|
+
expect(container.firstElementChild?.getAttribute('aria-label')).toBe('heading');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── F11-bis edge cases ─────────────────────────────────────────────
|
|
171
|
+
describe('multi-axis combinations', () => {
|
|
172
|
+
it('variant=h2 + color=primary + weight=normal + align=center + truncate + gutterBottom', () => {
|
|
173
|
+
const { container } = render(
|
|
174
|
+
<Typography
|
|
175
|
+
variant="h2"
|
|
176
|
+
color="primary"
|
|
177
|
+
weight="normal"
|
|
178
|
+
align="center"
|
|
179
|
+
truncate
|
|
180
|
+
gutterBottom
|
|
181
|
+
>
|
|
182
|
+
x
|
|
183
|
+
</Typography>,
|
|
184
|
+
);
|
|
185
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
186
|
+
expect(cls).toContain('text-4xl'); // h2 size
|
|
187
|
+
expect(cls).toContain('text-primary-700'); // color intent
|
|
188
|
+
expect(cls).toContain('font-normal'); // weight override (vs h2 default font-bold)
|
|
189
|
+
expect(cls).not.toContain('font-bold'); // overridden
|
|
190
|
+
expect(cls).toContain('text-center'); // align
|
|
191
|
+
expect(cls).toContain('truncate'); // truncate
|
|
192
|
+
expect(cls).toContain('mb-3'); // gutterBottom
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('noWrap + truncate: truncate wins (later in chain, tailwind-merge collapses whitespace-nowrap)', () => {
|
|
196
|
+
const { container } = render(<Typography noWrap truncate>x</Typography>);
|
|
197
|
+
// truncate utility already includes whitespace-nowrap — tailwind-merge
|
|
198
|
+
// collapses the redundant nowrap from noWrap
|
|
199
|
+
expect(container.firstElementChild?.className).toContain('truncate');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('every variant maps to its expected default tag', () => {
|
|
203
|
+
const cases: Array<[Parameters<typeof Typography>[0]['variant'], string]> = [
|
|
204
|
+
['h1', 'H1'], ['h2', 'H2'], ['h3', 'H3'], ['h4', 'H4'], ['h5', 'H5'], ['h6', 'H6'],
|
|
205
|
+
['subtitle1', 'P'], ['subtitle2', 'P'],
|
|
206
|
+
['body1', 'P'], ['body2', 'P'],
|
|
207
|
+
['caption', 'SPAN'], ['overline', 'SPAN'],
|
|
208
|
+
];
|
|
209
|
+
cases.forEach(([variant, tag]) => {
|
|
210
|
+
const { container } = render(<Typography variant={variant}>x</Typography>);
|
|
211
|
+
expect(container.firstElementChild?.tagName).toBe(tag);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('color edge cases', () => {
|
|
217
|
+
const INTENTS = ['primary', 'secondary', 'success', 'warning', 'danger', 'info', 'muted'] as const;
|
|
218
|
+
it.each(INTENTS)('color="%s" emits the right text-* + dark pair', (color) => {
|
|
219
|
+
const { container } = render(<Typography color={color}>x</Typography>);
|
|
220
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
221
|
+
if (color === 'muted') {
|
|
222
|
+
expect(cls).toContain('text-neutral-600');
|
|
223
|
+
expect(cls).toContain('dark:text-neutral-400');
|
|
224
|
+
} else {
|
|
225
|
+
expect(cls).toContain(`text-${color}-700`);
|
|
226
|
+
expect(cls).toContain(`dark:text-${color}-400`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { forwardRef, type ElementType, type ReactElement } from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
import { typographyVariants } from './typography.variants.js';
|
|
5
|
+
import type { TypographyProps } from './typography.types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default HTML tag per variant. Headings get their semantic level (h1→h1
|
|
9
|
+
* etc.); subtitle/body get `<p>` (block, paragraph semantics);
|
|
10
|
+
* caption/overline get `<span>` (inline, no implicit block break).
|
|
11
|
+
*
|
|
12
|
+
* Override per-instance via the `as` prop — useful when the semantic
|
|
13
|
+
* heading level should differ from the visual scale (e.g. a hero
|
|
14
|
+
* rendered as `<h2>` but visually styled `h1`).
|
|
15
|
+
*/
|
|
16
|
+
const VARIANT_TO_TAG: Record<NonNullable<TypographyProps['variant']>, ElementType> = {
|
|
17
|
+
h1: 'h1',
|
|
18
|
+
h2: 'h2',
|
|
19
|
+
h3: 'h3',
|
|
20
|
+
h4: 'h4',
|
|
21
|
+
h5: 'h5',
|
|
22
|
+
h6: 'h6',
|
|
23
|
+
subtitle1: 'p',
|
|
24
|
+
subtitle2: 'p',
|
|
25
|
+
body1: 'p',
|
|
26
|
+
body2: 'p',
|
|
27
|
+
caption: 'span',
|
|
28
|
+
overline: 'span',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `<Typography>` — semantic typed text, the foundation of every readable
|
|
33
|
+
* surface in @dashforge/tw.
|
|
34
|
+
*
|
|
35
|
+
* Why this exists:
|
|
36
|
+
* Tailwind ships a typographic scale (`text-xl`, `font-bold`,
|
|
37
|
+
* `leading-relaxed`) but leaves the SEMANTIC HTML tag and the
|
|
38
|
+
* intent-coloured palette to the consumer. That's fine for one-off
|
|
39
|
+
* marketing surfaces, but at app scale it means every `<h2>` and every
|
|
40
|
+
* body paragraph re-derives its own utility chain — and they drift.
|
|
41
|
+
*
|
|
42
|
+
* Typography moves that decision into a typed prop set: the visual
|
|
43
|
+
* scale, the intent colour, the alignment, the truncation all live in
|
|
44
|
+
* `tailwind-variants` and resolve to the same utility chain everywhere
|
|
45
|
+
* in the app. The default HTML tag is inferred from `variant` so the
|
|
46
|
+
* semantic layer follows the visual layer; `as` and `asChild` are the
|
|
47
|
+
* two escape hatches when you need something else.
|
|
48
|
+
*
|
|
49
|
+
* Layering:
|
|
50
|
+
* • Sits BENEATH every component that renders text — `<Button>`'s
|
|
51
|
+
* label, `<TextField>`'s helper text, MDX prose in our own docs.
|
|
52
|
+
* • Composes ON TOP of `@dashforge/tw-tokens` colour scales (so
|
|
53
|
+
* `color="primary"` paints `text-primary-700` in light, `-400` in dark,
|
|
54
|
+
* reactive to `setMode()`).
|
|
55
|
+
* • Polymorphic via Radix Slot — pairs cleanly with router `<Link>`,
|
|
56
|
+
* `<button>`, or `<a>` without injecting an extra wrapper element.
|
|
57
|
+
*
|
|
58
|
+
* Polymorphism rules — `as` vs `asChild`:
|
|
59
|
+
* • `as` swaps the rendered tag (we still render the element ourselves).
|
|
60
|
+
* • `asChild` removes our element entirely — the single React child
|
|
61
|
+
* becomes the rendered tag, with our className/ref merged onto it.
|
|
62
|
+
* This is the Radix Slot pattern; use when you need a router Link
|
|
63
|
+
* to style as a heading.
|
|
64
|
+
* When BOTH are passed, `asChild` wins. We considered making this a
|
|
65
|
+
* compile-time error via discriminated unions, but the API surface
|
|
66
|
+
* already has 8 axes and adding one more dimension to the props type
|
|
67
|
+
* would inflate IntelliSense suggestions for marginal benefit. The
|
|
68
|
+
* runtime preference is documented; the test asserts it.
|
|
69
|
+
*/
|
|
70
|
+
export const Typography = forwardRef<HTMLElement, TypographyProps>(
|
|
71
|
+
function Typography(props, ref) {
|
|
72
|
+
const {
|
|
73
|
+
variant = 'body1',
|
|
74
|
+
color,
|
|
75
|
+
weight,
|
|
76
|
+
align,
|
|
77
|
+
truncate,
|
|
78
|
+
noWrap,
|
|
79
|
+
gutterBottom,
|
|
80
|
+
as,
|
|
81
|
+
asChild = false,
|
|
82
|
+
sx,
|
|
83
|
+
children,
|
|
84
|
+
...rest
|
|
85
|
+
} = props;
|
|
86
|
+
|
|
87
|
+
const classes = cn(
|
|
88
|
+
typographyVariants({ variant, color, weight, align, truncate, noWrap, gutterBottom }),
|
|
89
|
+
sx,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// asChild wins over `as` when both are passed (see component header).
|
|
93
|
+
if (asChild) {
|
|
94
|
+
return (
|
|
95
|
+
<Slot ref={ref} className={classes} {...rest}>
|
|
96
|
+
{children as ReactElement}
|
|
97
|
+
</Slot>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Resolve the rendered tag: explicit `as` > variant default > fallback.
|
|
102
|
+
const Tag = (as ?? VARIANT_TO_TAG[variant] ?? 'span') as ElementType;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Tag ref={ref as never} className={classes} {...rest}>
|
|
106
|
+
{children}
|
|
107
|
+
</Tag>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
Typography.displayName = 'Typography';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes } from 'react';
|
|
2
|
+
import type { TypographyVariants } from './typography.variants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for `<Typography>`.
|
|
6
|
+
*
|
|
7
|
+
* Composition with native `HTMLAttributes`:
|
|
8
|
+
* • `className` is omitted in favour of `sx` (string of utility classes)
|
|
9
|
+
* — same convention as Button/TextField/Checkbox/Switch in this
|
|
10
|
+
* package. `sx` is merged via `tailwind-merge`, so the consumer's
|
|
11
|
+
* classes always win over variant defaults.
|
|
12
|
+
* • `color` is omitted because the native HTML attribute (the
|
|
13
|
+
* deprecated `color="red"` from HTML4) collides with our intent
|
|
14
|
+
* prop — TypeScript would otherwise widen to a confusing union.
|
|
15
|
+
*
|
|
16
|
+
* Variant axes picked from `TypographyVariants`:
|
|
17
|
+
* • `variant` — the type scale (h1–h6, subtitle1/2, body1/2, caption, overline)
|
|
18
|
+
* • `color` — the intent (primary, secondary, success, warning, danger, info, neutral, muted, inherit)
|
|
19
|
+
* • `weight` — overrides the variant's default font-weight
|
|
20
|
+
* • `align` — text-align
|
|
21
|
+
* • `truncate` — one-line ellipsis
|
|
22
|
+
* • `noWrap` — one-line without ellipsis
|
|
23
|
+
* • `gutterBottom` — adds mb-3 (mirror of MUI's same prop)
|
|
24
|
+
*
|
|
25
|
+
* Polymorphism:
|
|
26
|
+
* • `as` — override the HTML tag while keeping the variant's visual
|
|
27
|
+
* style (e.g. `<Typography variant="h1" as="h2">` renders an
|
|
28
|
+
* `<h2>` styled like h1 — useful when the semantic heading
|
|
29
|
+
* level matters more than the type scale).
|
|
30
|
+
* • `asChild` — render as the single child element via Radix Slot
|
|
31
|
+
* (so the styles paint onto a `<Link>`, `<a>`, etc.).
|
|
32
|
+
* Mutually exclusive with `as` — see component header for
|
|
33
|
+
* the rationale.
|
|
34
|
+
*/
|
|
35
|
+
export interface TypographyProps
|
|
36
|
+
extends Omit<HTMLAttributes<HTMLElement>, 'className' | 'color'>,
|
|
37
|
+
Pick<TypographyVariants, 'variant' | 'color' | 'weight' | 'align' | 'truncate' | 'noWrap' | 'gutterBottom'> {
|
|
38
|
+
/**
|
|
39
|
+
* Override the HTML tag. Defaults to a sensible mapping per variant
|
|
40
|
+
* (h1→h1, …, body1→p, caption→span, overline→span). Use when the
|
|
41
|
+
* semantic heading level should differ from the visual scale —
|
|
42
|
+
* e.g. a "hero" rendered as an `<h2>` but styled like `h1`.
|
|
43
|
+
*
|
|
44
|
+
* Ignored when `asChild` is true.
|
|
45
|
+
*/
|
|
46
|
+
as?: ElementType;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render via Radix `Slot` — the Typography styles paint onto the
|
|
50
|
+
* single React child instead of wrapping it in our own tag. Useful
|
|
51
|
+
* for `<Typography asChild><Link>...</Link></Typography>` to get a
|
|
52
|
+
* styled router link with no extra DOM.
|
|
53
|
+
*
|
|
54
|
+
* Mutually exclusive with `as` (when both are passed, `asChild` wins
|
|
55
|
+
* and `as` is ignored — see Typography.tsx for the reason).
|
|
56
|
+
*/
|
|
57
|
+
asChild?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Utility classes appended to the variant chain. Resolved via
|
|
61
|
+
* `tailwind-merge` so the consumer's classes always win over the
|
|
62
|
+
* variant defaults — e.g. `sx="text-pink-500"` overrides `color="primary"`.
|
|
63
|
+
*/
|
|
64
|
+
sx?: string;
|
|
65
|
+
}
|