@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,83 @@
|
|
|
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 { boxVariants } from './box.variants.js';
|
|
5
|
+
import type { BoxProps } from './box.types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `<Box>` — the surface primitive of @dashforge/tw.
|
|
9
|
+
*
|
|
10
|
+
* What it IS:
|
|
11
|
+
* A polymorphic container with typed surface variants (plain /
|
|
12
|
+
* outlined / elevated / soft / solid) × 7 intent colours, plus
|
|
13
|
+
* spacing / sizing / rounded / elevation as enumerated token-scale
|
|
14
|
+
* props. Replaces MUI's Box + Paper + Card + Surface in one component.
|
|
15
|
+
*
|
|
16
|
+
* What it is NOT (deliberate, enforced by the prop set):
|
|
17
|
+
* • Not a flex container → use <Stack>
|
|
18
|
+
* • Not a grid container → use <Grid>
|
|
19
|
+
* • Not a paragraph → use <Typography>
|
|
20
|
+
* • Not a button → use <Button>
|
|
21
|
+
*
|
|
22
|
+
* The "Box is not flex" rule is the spine of the layout layer. Without
|
|
23
|
+
* it, every `<div>` in an app gravitates back to Box and the surface
|
|
24
|
+
* vs layout distinction collapses. Box's job is the SURFACE around
|
|
25
|
+
* content; Stack/Grid's job is the ARRANGEMENT of content. Two
|
|
26
|
+
* primitives, two responsibilities.
|
|
27
|
+
*
|
|
28
|
+
* When BOTH `as` and `asChild` are passed, `asChild` wins. Same rule
|
|
29
|
+
* as Typography — documented to avoid the dimension-bloat of a
|
|
30
|
+
* discriminated union over an 11-axis props type.
|
|
31
|
+
*
|
|
32
|
+
* Layering:
|
|
33
|
+
* • Sits BENEATH every visual chrome — every card, every panel,
|
|
34
|
+
* every section background.
|
|
35
|
+
* • Composes ON TOP of @dashforge/tw-tokens (color + spacing + radius
|
|
36
|
+
* + shadow scales) via the dashforgePreset CSS variables.
|
|
37
|
+
* • Pairs with Typography for text content, Stack/Grid for layout
|
|
38
|
+
* children inside it.
|
|
39
|
+
*/
|
|
40
|
+
export const Box = forwardRef<HTMLElement, BoxProps>(
|
|
41
|
+
function Box(props, ref) {
|
|
42
|
+
const {
|
|
43
|
+
variant,
|
|
44
|
+
color,
|
|
45
|
+
elevation,
|
|
46
|
+
rounded,
|
|
47
|
+
p, px, py, m, mx, my,
|
|
48
|
+
fullWidth,
|
|
49
|
+
fullHeight,
|
|
50
|
+
as,
|
|
51
|
+
asChild = false,
|
|
52
|
+
sx,
|
|
53
|
+
children,
|
|
54
|
+
...rest
|
|
55
|
+
} = props;
|
|
56
|
+
|
|
57
|
+
const classes = cn(
|
|
58
|
+
boxVariants({
|
|
59
|
+
variant, color, elevation, rounded,
|
|
60
|
+
p, px, py, m, mx, my,
|
|
61
|
+
fullWidth, fullHeight,
|
|
62
|
+
}),
|
|
63
|
+
sx,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (asChild) {
|
|
67
|
+
return (
|
|
68
|
+
<Slot ref={ref} className={classes} {...rest}>
|
|
69
|
+
{children as ReactElement}
|
|
70
|
+
</Slot>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const Tag = (as ?? 'div') as ElementType;
|
|
75
|
+
return (
|
|
76
|
+
<Tag ref={ref as never} className={classes} {...rest}>
|
|
77
|
+
{children}
|
|
78
|
+
</Tag>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
Box.displayName = 'Box';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes } from 'react';
|
|
2
|
+
import type { BoxVariants } from './box.variants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for `<Box>` — the surface primitive.
|
|
6
|
+
*
|
|
7
|
+
* What lives in props (typed, ergonomic):
|
|
8
|
+
* • Surface — variant, color, elevation, rounded
|
|
9
|
+
* • Spacing — p, px, py, m, mx, my (token-scale steps)
|
|
10
|
+
* • Sizing — fullWidth, fullHeight
|
|
11
|
+
* • Polymorphism — as, asChild
|
|
12
|
+
* • Override escape — sx (utility string, merged via tailwind-merge)
|
|
13
|
+
*
|
|
14
|
+
* What does NOT live here (deliberate, see Box.tsx header):
|
|
15
|
+
* • display / flex / grid / gap → use Stack or Grid
|
|
16
|
+
* • position / top / z-index → use sx
|
|
17
|
+
* • overflow / cursor → use sx
|
|
18
|
+
* • animation / transition → use sx
|
|
19
|
+
*
|
|
20
|
+
* Native attribute overrides:
|
|
21
|
+
* • `className` is omitted in favour of `sx` (string of utilities,
|
|
22
|
+
* resolved by tailwind-merge so consumer wins over variant defaults)
|
|
23
|
+
* — same convention as Button/TextField/Checkbox/Switch.
|
|
24
|
+
* • `color` is omitted: collides with the deprecated HTML4 `color`
|
|
25
|
+
* attribute. Our typed `color` (intent) is the right one.
|
|
26
|
+
*/
|
|
27
|
+
export interface BoxProps
|
|
28
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'color'>,
|
|
29
|
+
Pick<BoxVariants,
|
|
30
|
+
'variant' | 'color' | 'elevation' | 'rounded'
|
|
31
|
+
| 'p' | 'px' | 'py' | 'm' | 'mx' | 'my'
|
|
32
|
+
| 'fullWidth' | 'fullHeight'> {
|
|
33
|
+
/**
|
|
34
|
+
* Override the rendered HTML tag. Defaults to `'div'`. Useful when
|
|
35
|
+
* the surface should also carry semantic meaning — `<Box as="section">`
|
|
36
|
+
* for a page section, `<Box as="article">` for a card-shaped article.
|
|
37
|
+
*
|
|
38
|
+
* Ignored when `asChild` is true.
|
|
39
|
+
*/
|
|
40
|
+
as?: ElementType;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render via Radix `Slot` — the Box styles paint onto the single
|
|
44
|
+
* React child instead of wrapping it in our own element. Useful for
|
|
45
|
+
* `<Box asChild><Link>...</Link></Box>` to get a styled router link
|
|
46
|
+
* with no extra DOM wrapper.
|
|
47
|
+
*
|
|
48
|
+
* Mutually exclusive with `as` (when both are passed, `asChild` wins
|
|
49
|
+
* — see Box.tsx for the reasoning).
|
|
50
|
+
*/
|
|
51
|
+
asChild?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Utility classes appended to the variant chain. Resolved via
|
|
55
|
+
* `tailwind-merge` so the consumer's classes always win over the
|
|
56
|
+
* variant defaults. Use for one-off overrides AND for utility
|
|
57
|
+
* dimensions Box deliberately doesn't expose as props (overflow,
|
|
58
|
+
* position, animation, etc.).
|
|
59
|
+
*/
|
|
60
|
+
sx?: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `boxVariants` — the surface primitive for @dashforge/tw.
|
|
5
|
+
*
|
|
6
|
+
* Architectural choice (planned with the user — F9 deep dive):
|
|
7
|
+
*
|
|
8
|
+
* Box replaces FOUR overlapping concepts from MUI in one component:
|
|
9
|
+
* • Box (typed div)
|
|
10
|
+
* • Paper (surface with elevation)
|
|
11
|
+
* • Card (Paper specialisation)
|
|
12
|
+
* • Surface (Joy UI's outlined / soft / solid / plain variants)
|
|
13
|
+
*
|
|
14
|
+
* The reason for the consolidation: in MUI you have to compose two or
|
|
15
|
+
* three of these to express even basic intent ("an outlined card with
|
|
16
|
+
* warning tone"). Here, one `<Box variant="outlined" color="warning">`
|
|
17
|
+
* says exactly that.
|
|
18
|
+
*
|
|
19
|
+
* Variant taxonomy (5 axes, intentionally non-overlapping with Stack/Grid):
|
|
20
|
+
*
|
|
21
|
+
* • plain — bare div + padding + radius. The escape hatch.
|
|
22
|
+
* • outlined — 1px border + subtle bg tint. The "card lite".
|
|
23
|
+
* • elevated — bg surface + shadow scale (0-5). The "floating panel".
|
|
24
|
+
* • soft — semi-transparent intent bg + intent text. The "callout".
|
|
25
|
+
* • solid — solid intent bg + contrasting text. The "CTA banner".
|
|
26
|
+
*
|
|
27
|
+
* `color` applies to outlined/soft/solid (each gets the 7 intent
|
|
28
|
+
* variants). `elevated` is color-agnostic (always neutral surface +
|
|
29
|
+
* shadow scale). `plain` is everything-agnostic.
|
|
30
|
+
*
|
|
31
|
+
* Spacing axes (p/px/py/m/mx/my): mapped explicitly to the 11 token
|
|
32
|
+
* steps from @dashforge/tw-tokens (0, 0.5, 1, 2, 3, 4, 6, 8, 12, 16, 24).
|
|
33
|
+
* Tailwind JIT requires literal class strings — building them dynamically
|
|
34
|
+
* with template literals would purge them. The verbosity below is the
|
|
35
|
+
* cost of keeping the bundle CSS-pure and rebuild-free.
|
|
36
|
+
*
|
|
37
|
+
* What this does NOT do (deliberate, see component docs):
|
|
38
|
+
* • No display / flex / grid props → use Stack or Grid
|
|
39
|
+
* • No position / overflow / z-index → use `sx`
|
|
40
|
+
* • No animation / transition → use `sx`
|
|
41
|
+
*
|
|
42
|
+
* The "Box is not flex" rule is the spine of the layout layer. Without
|
|
43
|
+
* it, every `<div>` in an app gravitates back to Box and the surface
|
|
44
|
+
* vs layout distinction collapses — exactly the failure mode this
|
|
45
|
+
* primitive exists to prevent.
|
|
46
|
+
*/
|
|
47
|
+
export const boxVariants = tv({
|
|
48
|
+
base: 'block',
|
|
49
|
+
|
|
50
|
+
variants: {
|
|
51
|
+
/*
|
|
52
|
+
* Surface variant. Compound with `color` for outlined / soft / solid;
|
|
53
|
+
* standalone for plain / elevated.
|
|
54
|
+
*/
|
|
55
|
+
variant: {
|
|
56
|
+
plain: '',
|
|
57
|
+
outlined: 'border',
|
|
58
|
+
elevated: 'bg-white dark:bg-neutral-900',
|
|
59
|
+
soft: '',
|
|
60
|
+
solid: '',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Intent color. Only meaningful when variant is outlined / soft /
|
|
65
|
+
* solid (resolved via compoundVariants below). For plain / elevated
|
|
66
|
+
* this axis is ignored at the visual level — but kept in the type
|
|
67
|
+
* so the prop is always available without conditional typing.
|
|
68
|
+
*/
|
|
69
|
+
color: {
|
|
70
|
+
primary: '',
|
|
71
|
+
secondary: '',
|
|
72
|
+
success: '',
|
|
73
|
+
warning: '',
|
|
74
|
+
danger: '',
|
|
75
|
+
info: '',
|
|
76
|
+
neutral: '',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Shadow scale — relevant for `variant='elevated'`. We keep elevation
|
|
81
|
+
* as a separate axis (not folded into `variant`) so consumers can
|
|
82
|
+
* dial it up/down without changing the variant. Default `0` = no
|
|
83
|
+
* shadow (consistent with MUI's elevation=0).
|
|
84
|
+
*/
|
|
85
|
+
elevation: {
|
|
86
|
+
0: 'shadow-none',
|
|
87
|
+
1: 'shadow-sm',
|
|
88
|
+
2: 'shadow',
|
|
89
|
+
3: 'shadow-md',
|
|
90
|
+
4: 'shadow-lg',
|
|
91
|
+
5: 'shadow-xl',
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
rounded: {
|
|
95
|
+
none: 'rounded-none',
|
|
96
|
+
sm: 'rounded-sm',
|
|
97
|
+
md: 'rounded-md',
|
|
98
|
+
lg: 'rounded-lg',
|
|
99
|
+
xl: 'rounded-xl',
|
|
100
|
+
'2xl':'rounded-2xl',
|
|
101
|
+
full: 'rounded-full',
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Spacing — six axes (p/px/py/m/mx/my), 11 token steps each.
|
|
106
|
+
* Literals enumerated explicitly so Tailwind's JIT scanner finds
|
|
107
|
+
* every class. Token steps mirror @dashforge/tw-tokens spacing scale.
|
|
108
|
+
*/
|
|
109
|
+
p: {
|
|
110
|
+
0: 'p-0', '0.5': 'p-0.5', 1: 'p-1', 2: 'p-2', 3: 'p-3',
|
|
111
|
+
4: 'p-4', 6: 'p-6', 8: 'p-8', 12: 'p-12', 16: 'p-16',
|
|
112
|
+
24: 'p-24',
|
|
113
|
+
},
|
|
114
|
+
px: {
|
|
115
|
+
0: 'px-0', '0.5': 'px-0.5', 1: 'px-1', 2: 'px-2', 3: 'px-3',
|
|
116
|
+
4: 'px-4', 6: 'px-6', 8: 'px-8', 12: 'px-12', 16: 'px-16',
|
|
117
|
+
24: 'px-24',
|
|
118
|
+
},
|
|
119
|
+
py: {
|
|
120
|
+
0: 'py-0', '0.5': 'py-0.5', 1: 'py-1', 2: 'py-2', 3: 'py-3',
|
|
121
|
+
4: 'py-4', 6: 'py-6', 8: 'py-8', 12: 'py-12', 16: 'py-16',
|
|
122
|
+
24: 'py-24',
|
|
123
|
+
},
|
|
124
|
+
m: {
|
|
125
|
+
0: 'm-0', '0.5': 'm-0.5', 1: 'm-1', 2: 'm-2', 3: 'm-3',
|
|
126
|
+
4: 'm-4', 6: 'm-6', 8: 'm-8', 12: 'm-12', 16: 'm-16',
|
|
127
|
+
24: 'm-24',
|
|
128
|
+
},
|
|
129
|
+
mx: {
|
|
130
|
+
0: 'mx-0', '0.5': 'mx-0.5', 1: 'mx-1', 2: 'mx-2', 3: 'mx-3',
|
|
131
|
+
4: 'mx-4', 6: 'mx-6', 8: 'mx-8', 12: 'mx-12', 16: 'mx-16',
|
|
132
|
+
24: 'mx-24',
|
|
133
|
+
},
|
|
134
|
+
my: {
|
|
135
|
+
0: 'my-0', '0.5': 'my-0.5', 1: 'my-1', 2: 'my-2', 3: 'my-3',
|
|
136
|
+
4: 'my-4', 6: 'my-6', 8: 'my-8', 12: 'my-12', 16: 'my-16',
|
|
137
|
+
24: 'my-24',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
fullWidth: { true: 'w-full' },
|
|
141
|
+
fullHeight: { true: 'h-full' },
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* Compound variants — where surface × color get their actual visual.
|
|
146
|
+
* Twenty-one entries: 7 (outlined) + 7 (soft) + 7 (solid).
|
|
147
|
+
* `plain` and `elevated` don't appear here (no color contribution).
|
|
148
|
+
*
|
|
149
|
+
* Dark-mode pair is baked in: light-mode picks the 50-300 steps,
|
|
150
|
+
* dark-mode picks the 800-950 steps — both reactive to setMode()
|
|
151
|
+
* via the @dashforge/tw-theme CSS variables.
|
|
152
|
+
*/
|
|
153
|
+
compoundVariants: [
|
|
154
|
+
// ─── outlined × color ─────────────────────────────────────────────
|
|
155
|
+
{ variant: 'outlined', color: 'primary',
|
|
156
|
+
class: 'border-primary-300 bg-primary-50/40 dark:border-primary-800 dark:bg-primary-950/30' },
|
|
157
|
+
{ variant: 'outlined', color: 'secondary',
|
|
158
|
+
class: 'border-secondary-300 bg-secondary-50/40 dark:border-secondary-800 dark:bg-secondary-950/30' },
|
|
159
|
+
{ variant: 'outlined', color: 'success',
|
|
160
|
+
class: 'border-success-300 bg-success-50/40 dark:border-success-800 dark:bg-success-950/30' },
|
|
161
|
+
{ variant: 'outlined', color: 'warning',
|
|
162
|
+
class: 'border-warning-300 bg-warning-50/40 dark:border-warning-800 dark:bg-warning-950/30' },
|
|
163
|
+
{ variant: 'outlined', color: 'danger',
|
|
164
|
+
class: 'border-danger-300 bg-danger-50/40 dark:border-danger-800 dark:bg-danger-950/30' },
|
|
165
|
+
{ variant: 'outlined', color: 'info',
|
|
166
|
+
class: 'border-info-300 bg-info-50/40 dark:border-info-800 dark:bg-info-950/30' },
|
|
167
|
+
{ variant: 'outlined', color: 'neutral',
|
|
168
|
+
class: 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900' },
|
|
169
|
+
|
|
170
|
+
// ─── soft × color ─────────────────────────────────────────────────
|
|
171
|
+
{ variant: 'soft', color: 'primary',
|
|
172
|
+
class: 'bg-primary-100 text-primary-900 dark:bg-primary-950/50 dark:text-primary-100' },
|
|
173
|
+
{ variant: 'soft', color: 'secondary',
|
|
174
|
+
class: 'bg-secondary-100 text-secondary-900 dark:bg-secondary-950/50 dark:text-secondary-100' },
|
|
175
|
+
{ variant: 'soft', color: 'success',
|
|
176
|
+
class: 'bg-success-100 text-success-900 dark:bg-success-950/50 dark:text-success-100' },
|
|
177
|
+
{ variant: 'soft', color: 'warning',
|
|
178
|
+
class: 'bg-warning-100 text-warning-900 dark:bg-warning-950/50 dark:text-warning-100' },
|
|
179
|
+
{ variant: 'soft', color: 'danger',
|
|
180
|
+
class: 'bg-danger-100 text-danger-900 dark:bg-danger-950/50 dark:text-danger-100' },
|
|
181
|
+
{ variant: 'soft', color: 'info',
|
|
182
|
+
class: 'bg-info-100 text-info-900 dark:bg-info-950/50 dark:text-info-100' },
|
|
183
|
+
{ variant: 'soft', color: 'neutral',
|
|
184
|
+
class: 'bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' },
|
|
185
|
+
|
|
186
|
+
// ─── solid × color ────────────────────────────────────────────────
|
|
187
|
+
{ variant: 'solid', color: 'primary',
|
|
188
|
+
class: 'bg-primary-600 text-white dark:bg-primary-500' },
|
|
189
|
+
{ variant: 'solid', color: 'secondary',
|
|
190
|
+
class: 'bg-secondary-600 text-white dark:bg-secondary-500' },
|
|
191
|
+
{ variant: 'solid', color: 'success',
|
|
192
|
+
class: 'bg-success-600 text-white dark:bg-success-500' },
|
|
193
|
+
{ variant: 'solid', color: 'warning',
|
|
194
|
+
class: 'bg-warning-500 text-white dark:bg-warning-600' },
|
|
195
|
+
{ variant: 'solid', color: 'danger',
|
|
196
|
+
class: 'bg-danger-600 text-white dark:bg-danger-500' },
|
|
197
|
+
{ variant: 'solid', color: 'info',
|
|
198
|
+
class: 'bg-info-600 text-white dark:bg-info-500' },
|
|
199
|
+
{ variant: 'solid', color: 'neutral',
|
|
200
|
+
class: 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900' },
|
|
201
|
+
],
|
|
202
|
+
|
|
203
|
+
defaultVariants: {
|
|
204
|
+
variant: 'plain',
|
|
205
|
+
color: 'neutral',
|
|
206
|
+
elevation: 0,
|
|
207
|
+
rounded: 'none',
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
export type BoxVariants = VariantProps<typeof boxVariants>;
|
|
@@ -109,6 +109,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
109
109
|
sx
|
|
110
110
|
);
|
|
111
111
|
|
|
112
|
+
/*
|
|
113
|
+
* `aria-busy` announces the loading state to assistive tech.
|
|
114
|
+
* `disabled` alone hides the reason (perm-denied vs loading vs
|
|
115
|
+
* intrinsic), so SR users only hear "dimmed/inactive" without
|
|
116
|
+
* knowing why. Adding `aria-busy={true}` while loading distinguishes
|
|
117
|
+
* "wait for the action to finish" from "you can't do this".
|
|
118
|
+
*/
|
|
119
|
+
const ariaBusy = loading ? true : undefined;
|
|
120
|
+
|
|
112
121
|
// `asChild` renders through Radix Slot: the immediate child element
|
|
113
122
|
// gets the resolved className, no extra Button DOM is emitted.
|
|
114
123
|
if (asChild) {
|
|
@@ -118,6 +127,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
118
127
|
className={classes}
|
|
119
128
|
data-disabled={effectiveDisabled || undefined}
|
|
120
129
|
aria-disabled={effectiveDisabled || undefined}
|
|
130
|
+
aria-busy={ariaBusy}
|
|
121
131
|
>
|
|
122
132
|
{children}
|
|
123
133
|
</Slot>
|
|
@@ -129,6 +139,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
129
139
|
ref={ref}
|
|
130
140
|
type={rest.type ?? 'button'}
|
|
131
141
|
disabled={effectiveDisabled}
|
|
142
|
+
aria-busy={ariaBusy}
|
|
132
143
|
className={classes}
|
|
133
144
|
{...rest}
|
|
134
145
|
>
|
|
@@ -193,11 +193,35 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
193
193
|
ref={registration?.ref as React.Ref<HTMLButtonElement> | undefined}
|
|
194
194
|
className={cn(v.control(), slotProps?.control?.className)}
|
|
195
195
|
>
|
|
196
|
+
{/*
|
|
197
|
+
* Radix.Indicator natively mounts only when `data-state` is
|
|
198
|
+
* `checked` or `indeterminate` — i.e. it tracks Radix's
|
|
199
|
+
* internal state directly, no React state dependency.
|
|
200
|
+
*
|
|
201
|
+
* The previous implementation used `forceMount` + a React
|
|
202
|
+
* conditional `{resolvedChecked === true ? <CheckIcon /> : null}`,
|
|
203
|
+
* which broke standalone uncontrolled mode: Radix would flip
|
|
204
|
+
* its internal `data-state` on click (turning the control blue
|
|
205
|
+
* via `data-[state=checked]:bg-primary-500`) but the React
|
|
206
|
+
* snapshot for `resolvedChecked` stayed stale, so the
|
|
207
|
+
* `<CheckIcon />` never mounted. Result: blue box with no
|
|
208
|
+
* tick after user interaction.
|
|
209
|
+
*
|
|
210
|
+
* Dropping forceMount + the conditional defers the mount
|
|
211
|
+
* decision to Radix (the single source of truth in all three
|
|
212
|
+
* modes — controlled, uncontrolled, bridge). Indicator mounts
|
|
213
|
+
* exactly when the checkbox is checked OR indeterminate.
|
|
214
|
+
*
|
|
215
|
+
* Indeterminate caveat: this renders the check glyph for
|
|
216
|
+
* BOTH checked AND indeterminate states. Previously
|
|
217
|
+
* indeterminate rendered nothing inside the blue square
|
|
218
|
+
* (same level of broken). A future improvement would render
|
|
219
|
+
* a dash for indeterminate — out of scope here.
|
|
220
|
+
*/}
|
|
196
221
|
<RadixCheckbox.Indicator
|
|
197
222
|
className={cn(v.indicator(), slotProps?.indicator?.className)}
|
|
198
|
-
forceMount
|
|
199
223
|
>
|
|
200
|
-
|
|
224
|
+
<CheckIcon className="h-full w-full" />
|
|
201
225
|
</RadixCheckbox.Indicator>
|
|
202
226
|
</RadixCheckbox.Root>
|
|
203
227
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import { Container } from './Container.js';
|
|
5
|
+
|
|
6
|
+
describe('<Container>', () => {
|
|
7
|
+
// ─── Default rendering ──────────────────────────────────────────────
|
|
8
|
+
describe('default rendering', () => {
|
|
9
|
+
it('renders a <div> by default', () => {
|
|
10
|
+
const { container } = render(<Container>x</Container>);
|
|
11
|
+
expect(container.firstElementChild?.tagName).toBe('DIV');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('has mx-auto + w-full baseline', () => {
|
|
15
|
+
const { container } = render(<Container>x</Container>);
|
|
16
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
17
|
+
expect(cls).toContain('mx-auto');
|
|
18
|
+
expect(cls).toContain('w-full');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('default size is xl (max-w-screen-xl)', () => {
|
|
22
|
+
const { container } = render(<Container>x</Container>);
|
|
23
|
+
expect(container.firstElementChild?.className).toContain('max-w-screen-xl');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('default px is true — responsive padding ramp', () => {
|
|
27
|
+
const { container } = render(<Container>x</Container>);
|
|
28
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
29
|
+
expect(cls).toContain('px-4');
|
|
30
|
+
expect(cls).toContain('sm:px-6');
|
|
31
|
+
expect(cls).toContain('lg:px-8');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('default centerContent is false — no flex column', () => {
|
|
35
|
+
const { container } = render(<Container>x</Container>);
|
|
36
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
37
|
+
// baseline doesn't include flex
|
|
38
|
+
expect(cls.split(' ')).not.toContain('flex');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── Size axis ──────────────────────────────────────────────────────
|
|
43
|
+
describe('size', () => {
|
|
44
|
+
it('size="sm"', () => {
|
|
45
|
+
const { container } = render(<Container size="sm">x</Container>);
|
|
46
|
+
expect(container.firstElementChild?.className).toContain('max-w-screen-sm');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('size="lg"', () => {
|
|
50
|
+
const { container } = render(<Container size="lg">x</Container>);
|
|
51
|
+
expect(container.firstElementChild?.className).toContain('max-w-screen-lg');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('size="2xl"', () => {
|
|
55
|
+
const { container } = render(<Container size="2xl">x</Container>);
|
|
56
|
+
expect(container.firstElementChild?.className).toContain('max-w-screen-2xl');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('size="fluid" emits no max-w-* class', () => {
|
|
60
|
+
const { container } = render(<Container size="fluid">x</Container>);
|
|
61
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
62
|
+
expect(cls).not.toContain('max-w-screen');
|
|
63
|
+
// still mx-auto + w-full baseline
|
|
64
|
+
expect(cls).toContain('mx-auto');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── Padding axis ───────────────────────────────────────────────────
|
|
69
|
+
describe('padding', () => {
|
|
70
|
+
it('px={false} skips the responsive padding ramp', () => {
|
|
71
|
+
const { container } = render(<Container px={false}>x</Container>);
|
|
72
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
73
|
+
expect(cls).not.toContain('px-4');
|
|
74
|
+
expect(cls).not.toContain('sm:px-6');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── Center content ─────────────────────────────────────────────────
|
|
79
|
+
describe('centerContent', () => {
|
|
80
|
+
it('centerContent turns into flex column items-center', () => {
|
|
81
|
+
const { container } = render(<Container centerContent>x</Container>);
|
|
82
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
83
|
+
expect(cls).toContain('flex');
|
|
84
|
+
expect(cls).toContain('flex-col');
|
|
85
|
+
expect(cls).toContain('items-center');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── Override semantics ─────────────────────────────────────────────
|
|
90
|
+
describe('override', () => {
|
|
91
|
+
it('sx wins over default size via tailwind-merge', () => {
|
|
92
|
+
const { container } = render(
|
|
93
|
+
<Container sx="max-w-3xl">x</Container>,
|
|
94
|
+
);
|
|
95
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
96
|
+
expect(cls).toContain('max-w-3xl');
|
|
97
|
+
// tailwind-merge collapses the conflicting max-w-screen-xl
|
|
98
|
+
expect(cls).not.toContain('max-w-screen-xl');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('sx can add vertical padding (Container does not expose it)', () => {
|
|
102
|
+
const { container } = render(<Container sx="py-12">x</Container>);
|
|
103
|
+
expect(container.firstElementChild?.className).toContain('py-12');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── Polymorphism ───────────────────────────────────────────────────
|
|
108
|
+
describe('polymorphism', () => {
|
|
109
|
+
it('as="main" renders <main> with Container chrome', () => {
|
|
110
|
+
const { container } = render(<Container as="main">x</Container>);
|
|
111
|
+
const el = container.firstElementChild;
|
|
112
|
+
expect(el?.tagName).toBe('MAIN');
|
|
113
|
+
expect(el?.className).toContain('mx-auto');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('asChild paints onto the child element', () => {
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<Container size="lg" asChild>
|
|
119
|
+
<article>x</article>
|
|
120
|
+
</Container>,
|
|
121
|
+
);
|
|
122
|
+
const el = container.firstElementChild;
|
|
123
|
+
expect(el?.tagName).toBe('ARTICLE');
|
|
124
|
+
expect(el?.className).toContain('max-w-screen-lg');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('asChild wins over as when both are passed', () => {
|
|
128
|
+
const { container } = render(
|
|
129
|
+
<Container as="main" asChild>
|
|
130
|
+
<section>x</section>
|
|
131
|
+
</Container>,
|
|
132
|
+
);
|
|
133
|
+
expect(container.firstElementChild?.tagName).toBe('SECTION');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── Pass-through ────────────────────────────────────────────────────
|
|
138
|
+
describe('pass-through', () => {
|
|
139
|
+
it('forwards data-* + aria-*', () => {
|
|
140
|
+
const { container } = render(
|
|
141
|
+
<Container data-testid="c" aria-label="page">x</Container>,
|
|
142
|
+
);
|
|
143
|
+
const el = container.firstElementChild;
|
|
144
|
+
expect(el?.getAttribute('data-testid')).toBe('c');
|
|
145
|
+
expect(el?.getAttribute('aria-label')).toBe('page');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── F11-bis edge cases ─────────────────────────────────────────────
|
|
150
|
+
describe('axis coexistence', () => {
|
|
151
|
+
it('size + centerContent + px=true (all three combine)', () => {
|
|
152
|
+
const { container } = render(
|
|
153
|
+
<Container size="md" centerContent px>x</Container>,
|
|
154
|
+
);
|
|
155
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
156
|
+
expect(cls).toContain('max-w-screen-md');
|
|
157
|
+
expect(cls).toContain('flex');
|
|
158
|
+
expect(cls).toContain('flex-col');
|
|
159
|
+
expect(cls).toContain('items-center');
|
|
160
|
+
expect(cls).toContain('px-4');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('fluid + centerContent (full-bleed centered hero pattern)', () => {
|
|
164
|
+
const { container } = render(
|
|
165
|
+
<Container size="fluid" centerContent>x</Container>,
|
|
166
|
+
);
|
|
167
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
168
|
+
expect(cls).not.toContain('max-w-screen');
|
|
169
|
+
expect(cls).toContain('flex-col');
|
|
170
|
+
expect(cls).toContain('items-center');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('fluid + px=false (full-bleed edge-to-edge — hero image pattern)', () => {
|
|
174
|
+
const { container } = render(
|
|
175
|
+
<Container size="fluid" px={false}>x</Container>,
|
|
176
|
+
);
|
|
177
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
178
|
+
expect(cls).not.toContain('max-w-screen');
|
|
179
|
+
expect(cls).not.toContain('px-4');
|
|
180
|
+
// still has mx-auto + w-full baseline
|
|
181
|
+
expect(cls).toContain('mx-auto');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('nested fluid-outer + capped-inner pattern', () => {
|
|
186
|
+
it('outer fluid colored band + inner capped content renders both', () => {
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<Container size="fluid" px={false} sx="bg-primary-50 py-16">
|
|
189
|
+
<Container size="lg">
|
|
190
|
+
<p>inner</p>
|
|
191
|
+
</Container>
|
|
192
|
+
</Container>,
|
|
193
|
+
);
|
|
194
|
+
const outer = container.firstElementChild;
|
|
195
|
+
const inner = outer?.querySelector('div');
|
|
196
|
+
expect(outer?.className).toContain('bg-primary-50');
|
|
197
|
+
expect(outer?.className).not.toContain('max-w-screen');
|
|
198
|
+
expect(inner?.className).toContain('max-w-screen-lg');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('every size value', () => {
|
|
203
|
+
const SIZES = ['sm', 'md', 'lg', 'xl', '2xl'] as const;
|
|
204
|
+
it.each(SIZES)('size="%s" emits max-w-screen-%s', (s) => {
|
|
205
|
+
const { container } = render(<Container size={s}>x</Container>);
|
|
206
|
+
expect(container.firstElementChild?.className).toContain(`max-w-screen-${s}`);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|