@fragments-sdk/ui 0.8.8 → 0.9.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/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.module.scss +1 -1
- package/src/components/Accordion/Accordion.test.tsx +1 -2
- package/src/components/Avatar/Avatar.fragment.tsx +18 -0
- package/src/components/Avatar/Avatar.test.tsx +18 -0
- package/src/components/Avatar/index.tsx +16 -0
- package/src/components/Badge/index.tsx +2 -0
- package/src/components/BentoGrid/BentoGrid.fragment.tsx +147 -0
- package/src/components/BentoGrid/BentoGrid.module.scss +123 -0
- package/src/components/BentoGrid/BentoGrid.test.tsx +140 -0
- package/src/components/BentoGrid/index.tsx +150 -0
- package/src/components/Button/index.tsx +2 -0
- package/src/components/Card/index.tsx +2 -0
- package/src/components/Chart/Chart.test.tsx +2 -2
- package/src/components/Checkbox/index.tsx +2 -0
- package/src/components/CodeBlock/index.tsx +1 -1
- package/src/components/Command/Command.test.tsx +1 -1
- package/src/components/Command/index.tsx +1 -1
- package/src/components/DatePicker/index.tsx +1 -1
- package/src/components/Dialog/index.tsx +2 -0
- package/src/components/Drawer/index.tsx +2 -0
- package/src/components/EmptyState/index.tsx +2 -0
- package/src/components/Field/index.tsx +2 -0
- package/src/components/Fieldset/index.tsx +2 -0
- package/src/components/Form/index.tsx +2 -0
- package/src/components/Header/Header.module.scss +4 -0
- package/src/components/Icon/index.tsx +2 -0
- package/src/components/List/index.tsx +2 -0
- package/src/components/Menu/index.tsx +2 -0
- package/src/components/NavigationMenu/NavigationMenu.module.scss +1 -2
- package/src/components/NavigationMenu/NavigationMenuContext.ts +2 -0
- package/src/components/NavigationMenu/index.tsx +51 -24
- package/src/components/NavigationMenu/useNavigationMenu.ts +3 -0
- package/src/components/Pagination/index.tsx +2 -0
- package/src/components/Popover/index.tsx +2 -0
- package/src/components/Progress/index.tsx +2 -0
- package/src/components/RadioGroup/index.tsx +2 -0
- package/src/components/Separator/index.tsx +2 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Theme/index.tsx +4 -3
- package/src/components/Toggle/index.tsx +2 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +96 -79
- package/src/components/ToggleGroup/index.tsx +2 -0
- package/src/components/Tooltip/index.tsx +2 -0
- package/src/index.ts +3 -2
- package/src/styles/globals.scss +5 -0
- package/src/tokens/_variables.scss +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Customizable UI components built on Base UI headless primitives",
|
|
6
6
|
"author": "Conan McNicholl",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@tanstack/react-table": "^8.21.3",
|
|
114
114
|
"vitest": "^2.1.8",
|
|
115
115
|
"vitest-axe": "^0.1.0",
|
|
116
|
-
"@fragments-sdk/cli": "0.7.
|
|
116
|
+
"@fragments-sdk/cli": "0.7.15"
|
|
117
117
|
},
|
|
118
118
|
"files": [
|
|
119
119
|
"src",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
align-items: center;
|
|
39
39
|
justify-content: space-between;
|
|
40
40
|
width: 100%;
|
|
41
|
-
padding: var(--fui-padding-item-
|
|
41
|
+
padding: var(--fui-padding-item-sm, $fui-padding-item-sm) 0;
|
|
42
42
|
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
43
43
|
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
44
44
|
color: var(--fui-text-primary, $fui-text-primary);
|
|
@@ -71,7 +71,6 @@ describe('Accordion', () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('links trigger aria-controls to content id', async () => {
|
|
74
|
-
const user = userEvent.setup();
|
|
75
74
|
renderAccordion({ defaultValue: 'one' });
|
|
76
75
|
|
|
77
76
|
const trigger = screen.getByRole('button', { name: /item one/i });
|
|
@@ -110,7 +109,7 @@ describe('Accordion', () => {
|
|
|
110
109
|
|
|
111
110
|
it('supports controlled value prop', async () => {
|
|
112
111
|
const onValueChange = vi.fn();
|
|
113
|
-
|
|
112
|
+
render(
|
|
114
113
|
<Accordion value="one" onValueChange={onValueChange}>
|
|
115
114
|
<Accordion.Item value="one">
|
|
116
115
|
<Accordion.Trigger>Item One</Accordion.Trigger>
|
|
@@ -62,6 +62,10 @@ export default defineFragment({
|
|
|
62
62
|
default: 'md',
|
|
63
63
|
description: 'Size variant',
|
|
64
64
|
},
|
|
65
|
+
customSize: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Custom avatar size (number in px or CSS size string like "2.25rem"), overrides size width/height',
|
|
68
|
+
},
|
|
65
69
|
shape: {
|
|
66
70
|
type: 'enum',
|
|
67
71
|
values: ['circle', 'square'],
|
|
@@ -72,6 +76,10 @@ export default defineFragment({
|
|
|
72
76
|
type: 'string',
|
|
73
77
|
description: 'Custom background color for fallback avatar',
|
|
74
78
|
},
|
|
79
|
+
imageStyle: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Inline style object applied to the underlying image element',
|
|
82
|
+
},
|
|
75
83
|
},
|
|
76
84
|
|
|
77
85
|
relations: [
|
|
@@ -87,6 +95,8 @@ export default defineFragment({
|
|
|
87
95
|
'src: string - image URL',
|
|
88
96
|
'name: string - used for initials fallback',
|
|
89
97
|
'size: xs|sm|md|lg|xl (default: md)',
|
|
98
|
+
'customSize: number|string - custom size override',
|
|
99
|
+
'imageStyle: CSSProperties - inline image styling',
|
|
90
100
|
'shape: circle|square (default: circle)',
|
|
91
101
|
],
|
|
92
102
|
scenarioTags: [
|
|
@@ -150,6 +160,14 @@ import { Stack } from '@/components/Stack';
|
|
|
150
160
|
</Stack>
|
|
151
161
|
),
|
|
152
162
|
},
|
|
163
|
+
{
|
|
164
|
+
name: 'Custom Size',
|
|
165
|
+
description: 'Set an exact avatar size',
|
|
166
|
+
code: `import { Avatar } from '@/components/Avatar';
|
|
167
|
+
|
|
168
|
+
<Avatar name="Conan McNicholl" customSize={36} />`,
|
|
169
|
+
render: () => <Avatar name="Conan McNicholl" customSize={36} />,
|
|
170
|
+
},
|
|
153
171
|
{
|
|
154
172
|
name: 'Square Shape',
|
|
155
173
|
description: 'Square variant for app icons or brands',
|
|
@@ -33,6 +33,24 @@ describe('Avatar', () => {
|
|
|
33
33
|
expect(screen.getByText('+2')).toBeInTheDocument();
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('applies a custom avatar size when customSize is provided', () => {
|
|
37
|
+
render(<Avatar name="Jane Doe" customSize={36} data-testid="avatar" />);
|
|
38
|
+
const avatar = screen.getByTestId('avatar');
|
|
39
|
+
expect(avatar).toHaveStyle({ width: '36px', height: '36px' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('applies imageStyle to the avatar image element', () => {
|
|
43
|
+
render(
|
|
44
|
+
<Avatar
|
|
45
|
+
src="https://example.com/photo.jpg"
|
|
46
|
+
alt="Jane Doe"
|
|
47
|
+
imageStyle={{ objectPosition: 'center 24%', transform: 'scale(1.4)' }}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
const img = screen.getByRole('img');
|
|
51
|
+
expect(img).toHaveStyle({ objectPosition: 'center 24%', transform: 'scale(1.4)' });
|
|
52
|
+
});
|
|
53
|
+
|
|
36
54
|
it('has no accessibility violations', async () => {
|
|
37
55
|
const { container } = render(<Avatar name="Jane Doe" />);
|
|
38
56
|
await expectNoA11yViolations(container);
|
|
@@ -22,10 +22,14 @@ export interface AvatarProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
|
|
22
22
|
name?: string;
|
|
23
23
|
/** Size variant */
|
|
24
24
|
size?: AvatarSize;
|
|
25
|
+
/** Custom avatar size (overrides size width/height) */
|
|
26
|
+
customSize?: number | string;
|
|
25
27
|
/** Shape variant */
|
|
26
28
|
shape?: 'circle' | 'square';
|
|
27
29
|
/** Custom background color for fallback */
|
|
28
30
|
color?: string;
|
|
31
|
+
/** Inline style for the underlying image element */
|
|
32
|
+
imageStyle?: React.CSSProperties;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -103,8 +107,10 @@ const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
|
103
107
|
initials,
|
|
104
108
|
name,
|
|
105
109
|
size = 'md',
|
|
110
|
+
customSize,
|
|
106
111
|
shape = 'circle',
|
|
107
112
|
color,
|
|
113
|
+
imageStyle,
|
|
108
114
|
className,
|
|
109
115
|
style: styleProp,
|
|
110
116
|
...htmlProps
|
|
@@ -137,6 +143,15 @@ const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
|
137
143
|
].filter(Boolean).join(' ');
|
|
138
144
|
|
|
139
145
|
const style: React.CSSProperties = { ...styleProp };
|
|
146
|
+
if (customSize !== undefined) {
|
|
147
|
+
const resolvedSize = typeof customSize === 'number' ? `${customSize}px` : customSize;
|
|
148
|
+
style.width = resolvedSize;
|
|
149
|
+
style.height = resolvedSize;
|
|
150
|
+
if (style.fontSize === undefined) {
|
|
151
|
+
style.fontSize = `calc(${resolvedSize} * 0.4)`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
if (showFallback && fallbackColor) {
|
|
141
156
|
style.backgroundColor = fallbackColor;
|
|
142
157
|
const textColor = getContrastTextColor(fallbackColor);
|
|
@@ -162,6 +177,7 @@ const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
|
162
177
|
alt={alt}
|
|
163
178
|
className={styles.image}
|
|
164
179
|
onError={() => setImageError(true)}
|
|
180
|
+
style={{ ...imageStyle }}
|
|
165
181
|
/>
|
|
166
182
|
)}
|
|
167
183
|
{showFallback && displayInitials && (
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineFragment } from '@fragments-sdk/cli/core';
|
|
3
|
+
import { BentoGrid } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineFragment({
|
|
6
|
+
component: BentoGrid,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'BentoGrid',
|
|
10
|
+
description: 'Asymmetric grid layout with responsive spans and built-in surface styling for bento-style feature sections',
|
|
11
|
+
category: 'layout',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['bento', 'grid', 'layout', 'responsive', 'feature-grid', 'asymmetric'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
usage: {
|
|
17
|
+
when: [
|
|
18
|
+
'Asymmetric feature grids with hero + supporting items',
|
|
19
|
+
'Dashboard overviews where cards have different visual weight',
|
|
20
|
+
'Marketing pages with mixed-size content blocks',
|
|
21
|
+
'Any layout where items need per-breakpoint column/row spans',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Uniform grids where all items are the same size (use Grid)',
|
|
25
|
+
'Form layouts with labeled fields (use Grid)',
|
|
26
|
+
'Simple stacked content (use Stack)',
|
|
27
|
+
],
|
|
28
|
+
guidelines: [
|
|
29
|
+
'Use colSpan/rowSpan with responsive objects for layouts that adapt across breakpoints',
|
|
30
|
+
'The hero item typically uses colSpan={2} rowSpan={2} at lg and above',
|
|
31
|
+
'Items include built-in surface styling — no need to wrap children in Card',
|
|
32
|
+
'Grid auto-collapses: 3→2 columns below lg, all→1 column below sm',
|
|
33
|
+
],
|
|
34
|
+
accessibility: [
|
|
35
|
+
'Grid is purely visual — it does not affect reading order or semantics',
|
|
36
|
+
'Ensure logical source order matches visual order for screen readers',
|
|
37
|
+
'Hover effects are disabled when prefers-reduced-motion is set',
|
|
38
|
+
'Border thickness increases in prefers-contrast: more mode',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
children: {
|
|
44
|
+
type: 'node',
|
|
45
|
+
description: 'BentoGrid.Item children',
|
|
46
|
+
},
|
|
47
|
+
columns: {
|
|
48
|
+
type: 'enum',
|
|
49
|
+
values: ['2', '3', '4'],
|
|
50
|
+
default: '3',
|
|
51
|
+
description: 'Number of columns — auto-collapses responsively (3→2→1)',
|
|
52
|
+
},
|
|
53
|
+
gap: {
|
|
54
|
+
type: 'enum',
|
|
55
|
+
values: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
56
|
+
default: 'md',
|
|
57
|
+
description: 'Gap between grid items, mapped to spacing tokens',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
relations: [
|
|
62
|
+
{
|
|
63
|
+
component: 'Grid',
|
|
64
|
+
relationship: 'alternative',
|
|
65
|
+
note: 'Use Grid for uniform layouts; BentoGrid for asymmetric spans with built-in surfaces',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
component: 'Card',
|
|
69
|
+
relationship: 'sibling',
|
|
70
|
+
note: 'BentoGrid.Item has built-in surface styling similar to Card',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
contract: {
|
|
75
|
+
propsSummary: [
|
|
76
|
+
'columns: 2|3|4 (default: 3) — auto-collapses responsively',
|
|
77
|
+
'gap: none|xs|sm|md|lg|xl (default: md)',
|
|
78
|
+
'BentoGrid.Item colSpan: 1|2|3 | { base, sm, md, lg, xl }',
|
|
79
|
+
'BentoGrid.Item rowSpan: 1|2|3 | { base, sm, md, lg, xl }',
|
|
80
|
+
],
|
|
81
|
+
scenarioTags: [
|
|
82
|
+
'layout.bento',
|
|
83
|
+
'layout.asymmetric',
|
|
84
|
+
'layout.responsive',
|
|
85
|
+
'pattern.feature-grid',
|
|
86
|
+
'pattern.dashboard',
|
|
87
|
+
'pattern.marketing',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
variants: [
|
|
92
|
+
{
|
|
93
|
+
name: 'Default',
|
|
94
|
+
description: 'Basic 3-column uniform bento grid',
|
|
95
|
+
render: () => (
|
|
96
|
+
<BentoGrid columns={3} gap="md">
|
|
97
|
+
<BentoGrid.Item>Item 1</BentoGrid.Item>
|
|
98
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
99
|
+
<BentoGrid.Item>Item 3</BentoGrid.Item>
|
|
100
|
+
<BentoGrid.Item>Item 4</BentoGrid.Item>
|
|
101
|
+
<BentoGrid.Item>Item 5</BentoGrid.Item>
|
|
102
|
+
<BentoGrid.Item>Item 6</BentoGrid.Item>
|
|
103
|
+
</BentoGrid>
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'Hero Layout',
|
|
108
|
+
description: '2×2 hero item with 4 supporting items in a 3-column grid',
|
|
109
|
+
render: () => (
|
|
110
|
+
<BentoGrid columns={3} gap="md">
|
|
111
|
+
<BentoGrid.Item colSpan={{ base: 1, lg: 2 }} rowSpan={{ base: 1, lg: 2 }}>
|
|
112
|
+
Hero content
|
|
113
|
+
</BentoGrid.Item>
|
|
114
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
115
|
+
<BentoGrid.Item>Item 3</BentoGrid.Item>
|
|
116
|
+
<BentoGrid.Item>Item 4</BentoGrid.Item>
|
|
117
|
+
<BentoGrid.Item>Item 5</BentoGrid.Item>
|
|
118
|
+
</BentoGrid>
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Two Column',
|
|
123
|
+
description: '2-column bento with a wide item',
|
|
124
|
+
render: () => (
|
|
125
|
+
<BentoGrid columns={2} gap="md">
|
|
126
|
+
<BentoGrid.Item>Item 1</BentoGrid.Item>
|
|
127
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
128
|
+
<BentoGrid.Item colSpan={2}>Wide item</BentoGrid.Item>
|
|
129
|
+
<BentoGrid.Item>Item 4</BentoGrid.Item>
|
|
130
|
+
<BentoGrid.Item>Item 5</BentoGrid.Item>
|
|
131
|
+
</BentoGrid>
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'Full-Width Banner',
|
|
136
|
+
description: '3-column grid with a full-width item spanning all columns',
|
|
137
|
+
render: () => (
|
|
138
|
+
<BentoGrid columns={3} gap="md">
|
|
139
|
+
<BentoGrid.Item>Item 1</BentoGrid.Item>
|
|
140
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
141
|
+
<BentoGrid.Item>Item 3</BentoGrid.Item>
|
|
142
|
+
<BentoGrid.Item colSpan={3}>Full-width banner</BentoGrid.Item>
|
|
143
|
+
</BentoGrid>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// BentoGrid Container
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
.grid {
|
|
9
|
+
display: grid;
|
|
10
|
+
width: 100%;
|
|
11
|
+
grid-auto-flow: dense;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// Column counts
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
.columns2 { grid-template-columns: repeat(2, 1fr); }
|
|
19
|
+
.columns3 { grid-template-columns: repeat(3, 1fr); }
|
|
20
|
+
.columns4 { grid-template-columns: repeat(4, 1fr); }
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Responsive collapse
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
@include below-lg {
|
|
27
|
+
.columns3,
|
|
28
|
+
.columns4 {
|
|
29
|
+
grid-template-columns: repeat(2, 1fr);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@include below-sm {
|
|
34
|
+
.columns2,
|
|
35
|
+
.columns3,
|
|
36
|
+
.columns4 {
|
|
37
|
+
grid-template-columns: 1fr;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// Gap
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
.gapNone { gap: 0; }
|
|
46
|
+
.gapXs { gap: var(--fui-space-1, $fui-space-1); }
|
|
47
|
+
.gapSm { gap: var(--fui-space-2, $fui-space-2); }
|
|
48
|
+
.gapMd { gap: var(--fui-space-4, $fui-space-4); }
|
|
49
|
+
.gapLg { gap: var(--fui-space-6, $fui-space-6); }
|
|
50
|
+
.gapXl { gap: var(--fui-space-8, $fui-space-8); }
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// BentoGrid Item
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
.item {
|
|
57
|
+
@include surface-elevated;
|
|
58
|
+
|
|
59
|
+
min-width: 0;
|
|
60
|
+
padding: var(--fui-padding-container-md, $fui-padding-container-md);
|
|
61
|
+
transition:
|
|
62
|
+
box-shadow var(--fui-transition-fast, $fui-transition-fast),
|
|
63
|
+
border-color var(--fui-transition-fast, $fui-transition-fast),
|
|
64
|
+
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
65
|
+
|
|
66
|
+
&:hover {
|
|
67
|
+
border-color: var(--fui-border-strong, $fui-border-strong);
|
|
68
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
69
|
+
transform: translateY(-2px);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- Responsive span cascade ----
|
|
73
|
+
--_col-span: var(--bento-col-span, 1);
|
|
74
|
+
--_row-span: var(--bento-row-span, 1);
|
|
75
|
+
|
|
76
|
+
grid-column: span var(--_col-span);
|
|
77
|
+
grid-row: span var(--_row-span);
|
|
78
|
+
|
|
79
|
+
@include breakpoint-sm {
|
|
80
|
+
--_col-span: var(--bento-col-span-sm, var(--bento-col-span, 1));
|
|
81
|
+
--_row-span: var(--bento-row-span-sm, var(--bento-row-span, 1));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@include breakpoint-md {
|
|
85
|
+
--_col-span: var(--bento-col-span-md, var(--bento-col-span-sm, var(--bento-col-span, 1)));
|
|
86
|
+
--_row-span: var(--bento-row-span-md, var(--bento-row-span-sm, var(--bento-row-span, 1)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@include breakpoint-lg {
|
|
90
|
+
--_col-span: var(--bento-col-span-lg, var(--bento-col-span-md, var(--bento-col-span-sm, var(--bento-col-span, 1))));
|
|
91
|
+
--_row-span: var(--bento-row-span-lg, var(--bento-row-span-md, var(--bento-row-span-sm, var(--bento-row-span, 1))));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@include breakpoint-xl {
|
|
95
|
+
--_col-span: var(--bento-col-span-xl, var(--bento-col-span-lg, var(--bento-col-span-md, var(--bento-col-span-sm, var(--bento-col-span, 1)))));
|
|
96
|
+
--_row-span: var(--bento-row-span-xl, var(--bento-row-span-lg, var(--bento-row-span-md, var(--bento-row-span-sm, var(--bento-row-span, 1)))));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// Accessibility: Reduced Motion
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
@media (prefers-reduced-motion: reduce) {
|
|
105
|
+
.item {
|
|
106
|
+
transition: none;
|
|
107
|
+
|
|
108
|
+
&:hover {
|
|
109
|
+
transform: none;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================
|
|
115
|
+
// Accessibility: High Contrast Mode
|
|
116
|
+
// ============================================
|
|
117
|
+
|
|
118
|
+
@media (prefers-contrast: more) {
|
|
119
|
+
.item {
|
|
120
|
+
border-width: 2px;
|
|
121
|
+
border-color: var(--fui-text-primary, $fui-text-primary);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { BentoGrid } from './index';
|
|
4
|
+
|
|
5
|
+
describe('BentoGrid', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<BentoGrid>
|
|
9
|
+
<BentoGrid.Item>Item 1</BentoGrid.Item>
|
|
10
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
11
|
+
</BentoGrid>
|
|
12
|
+
);
|
|
13
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
14
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('applies default columns3 class', () => {
|
|
18
|
+
const { container } = render(<BentoGrid>Content</BentoGrid>);
|
|
19
|
+
expect(container.firstChild).toHaveClass('columns3');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('applies columns2 class', () => {
|
|
23
|
+
const { container } = render(<BentoGrid columns={2}>Content</BentoGrid>);
|
|
24
|
+
expect(container.firstChild).toHaveClass('columns2');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('applies columns4 class', () => {
|
|
28
|
+
const { container } = render(<BentoGrid columns={4}>Content</BentoGrid>);
|
|
29
|
+
expect(container.firstChild).toHaveClass('columns4');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('applies default gapMd class', () => {
|
|
33
|
+
const { container } = render(<BentoGrid>Content</BentoGrid>);
|
|
34
|
+
expect(container.firstChild).toHaveClass('gapMd');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies gap classes', () => {
|
|
38
|
+
const { container } = render(<BentoGrid gap="lg">Content</BentoGrid>);
|
|
39
|
+
expect(container.firstChild).toHaveClass('gapLg');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sets CSS custom properties for simple colSpan', () => {
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<BentoGrid>
|
|
45
|
+
<BentoGrid.Item colSpan={2}>Wide</BentoGrid.Item>
|
|
46
|
+
</BentoGrid>
|
|
47
|
+
);
|
|
48
|
+
const item = container.querySelector('[class*="item"]') as HTMLElement;
|
|
49
|
+
expect(item.style.getPropertyValue('--bento-col-span')).toBe('2');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sets CSS custom properties for simple rowSpan', () => {
|
|
53
|
+
const { container } = render(
|
|
54
|
+
<BentoGrid>
|
|
55
|
+
<BentoGrid.Item rowSpan={2}>Tall</BentoGrid.Item>
|
|
56
|
+
</BentoGrid>
|
|
57
|
+
);
|
|
58
|
+
const item = container.querySelector('[class*="item"]') as HTMLElement;
|
|
59
|
+
expect(item.style.getPropertyValue('--bento-row-span')).toBe('2');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('sets per-breakpoint CSS custom properties for responsive colSpan', () => {
|
|
63
|
+
const { container } = render(
|
|
64
|
+
<BentoGrid>
|
|
65
|
+
<BentoGrid.Item colSpan={{ base: 1, lg: 2 }}>Responsive</BentoGrid.Item>
|
|
66
|
+
</BentoGrid>
|
|
67
|
+
);
|
|
68
|
+
const item = container.querySelector('[class*="item"]') as HTMLElement;
|
|
69
|
+
// base=1 should not set --bento-col-span (only values > 1)
|
|
70
|
+
expect(item.style.getPropertyValue('--bento-col-span')).toBe('');
|
|
71
|
+
expect(item.style.getPropertyValue('--bento-col-span-lg')).toBe('2');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('sets per-breakpoint CSS custom properties for responsive rowSpan', () => {
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<BentoGrid>
|
|
77
|
+
<BentoGrid.Item rowSpan={{ base: 1, md: 2, xl: 3 }}>Responsive</BentoGrid.Item>
|
|
78
|
+
</BentoGrid>
|
|
79
|
+
);
|
|
80
|
+
const item = container.querySelector('[class*="item"]') as HTMLElement;
|
|
81
|
+
expect(item.style.getPropertyValue('--bento-row-span')).toBe('');
|
|
82
|
+
expect(item.style.getPropertyValue('--bento-row-span-md')).toBe('2');
|
|
83
|
+
expect(item.style.getPropertyValue('--bento-row-span-xl')).toBe('3');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not set vars for default span value (1)', () => {
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<BentoGrid>
|
|
89
|
+
<BentoGrid.Item colSpan={1} rowSpan={1}>Default</BentoGrid.Item>
|
|
90
|
+
</BentoGrid>
|
|
91
|
+
);
|
|
92
|
+
const item = container.querySelector('[class*="item"]') as HTMLElement;
|
|
93
|
+
expect(item.style.getPropertyValue('--bento-col-span')).toBe('');
|
|
94
|
+
expect(item.style.getPropertyValue('--bento-row-span')).toBe('');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('forwards ref on root', () => {
|
|
98
|
+
const ref = vi.fn();
|
|
99
|
+
render(<BentoGrid ref={ref}>Content</BentoGrid>);
|
|
100
|
+
expect(ref).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('forwards ref on item', () => {
|
|
104
|
+
const ref = vi.fn();
|
|
105
|
+
render(
|
|
106
|
+
<BentoGrid>
|
|
107
|
+
<BentoGrid.Item ref={ref}>Content</BentoGrid.Item>
|
|
108
|
+
</BentoGrid>
|
|
109
|
+
);
|
|
110
|
+
expect(ref).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('accepts className on root', () => {
|
|
114
|
+
const { container } = render(<BentoGrid className="custom-root">Content</BentoGrid>);
|
|
115
|
+
expect(container.firstChild).toHaveClass('custom-root');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('accepts className on item', () => {
|
|
119
|
+
const { container } = render(
|
|
120
|
+
<BentoGrid>
|
|
121
|
+
<BentoGrid.Item className="custom-item">Content</BentoGrid.Item>
|
|
122
|
+
</BentoGrid>
|
|
123
|
+
);
|
|
124
|
+
const item = container.querySelector('.custom-item');
|
|
125
|
+
expect(item).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('has no accessibility violations', async () => {
|
|
129
|
+
const { container } = render(
|
|
130
|
+
<BentoGrid columns={3}>
|
|
131
|
+
<BentoGrid.Item colSpan={2} rowSpan={2}>Hero</BentoGrid.Item>
|
|
132
|
+
<BentoGrid.Item>Item 2</BentoGrid.Item>
|
|
133
|
+
<BentoGrid.Item>Item 3</BentoGrid.Item>
|
|
134
|
+
<BentoGrid.Item>Item 4</BentoGrid.Item>
|
|
135
|
+
<BentoGrid.Item>Item 5</BentoGrid.Item>
|
|
136
|
+
</BentoGrid>
|
|
137
|
+
);
|
|
138
|
+
await expectNoA11yViolations(container);
|
|
139
|
+
});
|
|
140
|
+
});
|