@fragments-sdk/ui 0.3.0 → 0.5.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/README.md +98 -2
- package/fragments.json +1 -1
- package/package.json +11 -5
- package/src/components/Accordion/Accordion.fragment.tsx +186 -0
- package/src/components/Accordion/Accordion.module.scss +111 -0
- package/src/components/Accordion/index.tsx +271 -0
- package/src/components/Alert/Alert.fragment.tsx +67 -42
- package/src/components/Alert/Alert.module.scss +31 -21
- package/src/components/Alert/index.tsx +202 -73
- package/src/components/AppShell/AppShell.fragment.tsx +315 -0
- package/src/components/AppShell/AppShell.module.scss +213 -0
- package/src/components/AppShell/index.tsx +398 -0
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Avatar/index.tsx +8 -9
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +16 -10
- package/src/components/Badge/index.tsx +20 -6
- package/src/components/Box/Box.fragment.tsx +168 -0
- package/src/components/Box/Box.module.scss +84 -0
- package/src/components/Box/index.tsx +78 -0
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/Button/Button.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +52 -26
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
- package/src/components/CodeBlock/index.tsx +599 -0
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
- package/src/components/ColorPicker/index.tsx +129 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +12 -3
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
- package/src/components/EmptyState/EmptyState.module.scss +9 -9
- package/src/components/EmptyState/index.tsx +104 -69
- package/src/components/Field/Field.fragment.tsx +165 -0
- package/src/components/Field/Field.module.scss +31 -0
- package/src/components/Field/index.tsx +143 -0
- package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
- package/src/components/Fieldset/Fieldset.module.scss +22 -0
- package/src/components/Fieldset/index.tsx +47 -0
- package/src/components/Form/Form.fragment.tsx +286 -0
- package/src/components/Form/Form.module.scss +8 -0
- package/src/components/Form/index.tsx +53 -0
- package/src/components/Grid/Grid.fragment.tsx +18 -18
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +208 -0
- package/src/components/Header/index.tsx +363 -0
- package/src/components/Icon/Icon.fragment.tsx +138 -0
- package/src/components/Icon/Icon.module.scss +38 -0
- package/src/components/Icon/index.tsx +58 -0
- package/src/components/Image/Image.fragment.tsx +195 -0
- package/src/components/Image/Image.module.scss +77 -0
- package/src/components/Image/index.tsx +95 -0
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +75 -2
- package/src/components/Input/index.tsx +60 -21
- package/src/components/Link/Link.fragment.tsx +132 -0
- package/src/components/Link/Link.module.scss +67 -0
- package/src/components/Link/index.tsx +57 -0
- package/src/components/List/List.fragment.tsx +152 -0
- package/src/components/List/List.module.scss +71 -0
- package/src/components/List/index.tsx +106 -0
- package/src/components/Listbox/Listbox.fragment.tsx +191 -0
- package/src/components/Listbox/Listbox.module.scss +97 -0
- package/src/components/Listbox/index.tsx +121 -0
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +12 -3
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +13 -4
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Progress/Progress.module.scss +11 -11
- package/src/components/Progress/index.tsx +34 -7
- package/src/components/Prompt/Prompt.fragment.tsx +231 -0
- package/src/components/Prompt/Prompt.module.scss +243 -0
- package/src/components/Prompt/index.tsx +439 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +10 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +91 -12
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
- package/src/components/Sidebar/Sidebar.module.scss +91 -47
- package/src/components/Sidebar/index.tsx +57 -14
- package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Stack/Stack.module.scss +120 -0
- package/src/components/Stack/index.tsx +148 -0
- package/src/components/Table/Table.fragment.tsx +10 -3
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +10 -1
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Textarea/index.tsx +3 -7
- package/src/components/Theme/Theme.fragment.tsx +128 -0
- package/src/components/Theme/ThemeToggle.module.scss +82 -0
- package/src/components/Theme/index.tsx +343 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +6 -6
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/Tooltip/Tooltip.module.scss +4 -4
- package/src/components/Tooltip/index.tsx +4 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
- package/src/components/VisuallyHidden/index.tsx +29 -0
- package/src/index.ts +278 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/recipes/AppShell.recipe.ts +175 -0
- package/src/recipes/CardGrid.recipe.ts +6 -2
- package/src/recipes/ChatInterface.recipe.ts +87 -0
- package/src/recipes/CodeExamples.recipe.ts +66 -0
- package/src/recipes/DashboardLayout.recipe.ts +46 -12
- package/src/recipes/DashboardNav.recipe.ts +183 -0
- package/src/recipes/LoginForm.recipe.ts +8 -1
- package/src/recipes/SettingsPage.recipe.ts +37 -20
- package/src/styles/globals.scss +31 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +41 -0
- package/src/tokens/_mixins.scss +95 -1
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +507 -101
- package/src/utils/a11y.tsx +439 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Stack } from '.';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
import { Badge } from '../Badge';
|
|
6
|
+
|
|
7
|
+
export default defineSegment({
|
|
8
|
+
component: Stack,
|
|
9
|
+
|
|
10
|
+
meta: {
|
|
11
|
+
name: 'Stack',
|
|
12
|
+
description: 'Flexible layout component for arranging children in rows or columns with consistent spacing. Supports responsive direction and gap.',
|
|
13
|
+
category: 'layout',
|
|
14
|
+
status: 'stable',
|
|
15
|
+
tags: ['stack', 'layout', 'flex', 'spacing', 'responsive'],
|
|
16
|
+
since: '0.2.0',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
usage: {
|
|
20
|
+
when: [
|
|
21
|
+
'Arranging elements in a row or column',
|
|
22
|
+
'Creating consistent spacing between items',
|
|
23
|
+
'Building responsive layouts',
|
|
24
|
+
'Simple flexbox-based arrangements',
|
|
25
|
+
],
|
|
26
|
+
whenNot: [
|
|
27
|
+
'Complex grid layouts (use Grid)',
|
|
28
|
+
'Button-specific grouping (use ButtonGroup)',
|
|
29
|
+
'Page-level layout (use AppShell)',
|
|
30
|
+
],
|
|
31
|
+
guidelines: [
|
|
32
|
+
'Use semantic elements via the "as" prop when appropriate',
|
|
33
|
+
'Leverage responsive props for mobile-first layouts',
|
|
34
|
+
'Keep spacing consistent within related sections',
|
|
35
|
+
'Consider alignment for visual balance',
|
|
36
|
+
],
|
|
37
|
+
accessibility: [
|
|
38
|
+
'Use semantic elements (nav, section, etc.) via "as" prop',
|
|
39
|
+
'Maintains source order for screen readers',
|
|
40
|
+
'No accessibility concerns with visual arrangement',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
props: {
|
|
45
|
+
children: {
|
|
46
|
+
type: 'node',
|
|
47
|
+
description: 'Elements to arrange',
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
direction: {
|
|
51
|
+
type: 'string | object',
|
|
52
|
+
description: 'Stack direction: "row", "column", or responsive object',
|
|
53
|
+
default: 'column',
|
|
54
|
+
},
|
|
55
|
+
gap: {
|
|
56
|
+
type: 'string | object',
|
|
57
|
+
description: 'Spacing between items: "none", "xs", "sm", "md", "lg", "xl", or responsive object',
|
|
58
|
+
default: 'md',
|
|
59
|
+
},
|
|
60
|
+
align: {
|
|
61
|
+
type: 'enum',
|
|
62
|
+
description: 'Cross-axis alignment',
|
|
63
|
+
values: ['start', 'center', 'end', 'stretch', 'baseline'],
|
|
64
|
+
},
|
|
65
|
+
justify: {
|
|
66
|
+
type: 'enum',
|
|
67
|
+
description: 'Main-axis alignment',
|
|
68
|
+
values: ['start', 'center', 'end', 'between'],
|
|
69
|
+
},
|
|
70
|
+
wrap: {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
description: 'Allow items to wrap',
|
|
73
|
+
default: 'false',
|
|
74
|
+
},
|
|
75
|
+
as: {
|
|
76
|
+
type: 'enum',
|
|
77
|
+
description: 'HTML element to render',
|
|
78
|
+
values: ['div', 'section', 'nav', 'article', 'aside', 'header', 'footer', 'main', 'ul', 'ol'],
|
|
79
|
+
default: 'div',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
relations: [
|
|
84
|
+
{ component: 'Grid', relationship: 'alternative', note: 'Use Grid for complex 2D layouts' },
|
|
85
|
+
{ component: 'ButtonGroup', relationship: 'sibling', note: 'ButtonGroup is specialized for buttons' },
|
|
86
|
+
{ component: 'Box', relationship: 'sibling', note: 'Box for single-element styling' },
|
|
87
|
+
],
|
|
88
|
+
|
|
89
|
+
contract: {
|
|
90
|
+
propsSummary: [
|
|
91
|
+
'direction: row|column|{responsive} - stack direction',
|
|
92
|
+
'gap: none|xs|sm|md|lg|xl|{responsive} - spacing',
|
|
93
|
+
'align: start|center|end|stretch|baseline - cross-axis',
|
|
94
|
+
'justify: start|center|end|between - main-axis',
|
|
95
|
+
'wrap: boolean - allow wrapping',
|
|
96
|
+
'as: string - HTML element',
|
|
97
|
+
],
|
|
98
|
+
scenarioTags: [
|
|
99
|
+
'layout.flex',
|
|
100
|
+
'spacing.consistent',
|
|
101
|
+
'responsive.layout',
|
|
102
|
+
],
|
|
103
|
+
a11yRules: ['A11Y_SEMANTIC_ELEMENTS'],
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
variants: [
|
|
107
|
+
{
|
|
108
|
+
name: 'Vertical Stack',
|
|
109
|
+
description: 'Default column layout',
|
|
110
|
+
render: () => (
|
|
111
|
+
<Stack gap="sm">
|
|
112
|
+
<Badge>Item 1</Badge>
|
|
113
|
+
<Badge>Item 2</Badge>
|
|
114
|
+
<Badge>Item 3</Badge>
|
|
115
|
+
</Stack>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Horizontal Stack',
|
|
120
|
+
description: 'Row layout',
|
|
121
|
+
render: () => (
|
|
122
|
+
<Stack direction="row" gap="sm">
|
|
123
|
+
<Badge>Item 1</Badge>
|
|
124
|
+
<Badge>Item 2</Badge>
|
|
125
|
+
<Badge>Item 3</Badge>
|
|
126
|
+
</Stack>
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Gap Sizes',
|
|
131
|
+
description: 'Different spacing options',
|
|
132
|
+
render: () => (
|
|
133
|
+
<Stack gap="lg">
|
|
134
|
+
<Stack direction="row" gap="xs">
|
|
135
|
+
<Badge variant="info">XS</Badge>
|
|
136
|
+
<Badge variant="info">Gap</Badge>
|
|
137
|
+
</Stack>
|
|
138
|
+
<Stack direction="row" gap="sm">
|
|
139
|
+
<Badge variant="info">SM</Badge>
|
|
140
|
+
<Badge variant="info">Gap</Badge>
|
|
141
|
+
</Stack>
|
|
142
|
+
<Stack direction="row" gap="md">
|
|
143
|
+
<Badge variant="info">MD</Badge>
|
|
144
|
+
<Badge variant="info">Gap</Badge>
|
|
145
|
+
</Stack>
|
|
146
|
+
<Stack direction="row" gap="lg">
|
|
147
|
+
<Badge variant="info">LG</Badge>
|
|
148
|
+
<Badge variant="info">Gap</Badge>
|
|
149
|
+
</Stack>
|
|
150
|
+
</Stack>
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Alignment',
|
|
155
|
+
description: 'Cross-axis and main-axis alignment',
|
|
156
|
+
render: () => (
|
|
157
|
+
<Stack gap="md">
|
|
158
|
+
<Stack direction="row" gap="sm" justify="between" style={{ width: '200px', padding: '8px', background: 'var(--fui-bg-secondary)', borderRadius: '4px' }}>
|
|
159
|
+
<Badge>Start</Badge>
|
|
160
|
+
<Badge>End</Badge>
|
|
161
|
+
</Stack>
|
|
162
|
+
<Stack direction="row" gap="sm" justify="center" style={{ width: '200px', padding: '8px', background: 'var(--fui-bg-secondary)', borderRadius: '4px' }}>
|
|
163
|
+
<Badge>Centered</Badge>
|
|
164
|
+
</Stack>
|
|
165
|
+
</Stack>
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'Responsive',
|
|
170
|
+
description: 'Direction changes at breakpoints',
|
|
171
|
+
render: () => (
|
|
172
|
+
<Stack
|
|
173
|
+
direction={{ base: 'column', md: 'row' }}
|
|
174
|
+
gap={{ base: 'sm', md: 'lg' }}
|
|
175
|
+
>
|
|
176
|
+
<Button variant="secondary">First</Button>
|
|
177
|
+
<Button variant="secondary">Second</Button>
|
|
178
|
+
<Button variant="secondary">Third</Button>
|
|
179
|
+
</Stack>
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'Semantic Element',
|
|
184
|
+
description: 'Using nav element for navigation',
|
|
185
|
+
render: () => (
|
|
186
|
+
<Stack as="nav" direction="row" gap="md">
|
|
187
|
+
<Button variant="ghost" size="sm">Home</Button>
|
|
188
|
+
<Button variant="ghost" size="sm">About</Button>
|
|
189
|
+
<Button variant="ghost" size="sm">Contact</Button>
|
|
190
|
+
</Stack>
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
.stack {
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Direction
|
|
8
|
+
.row {
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.column {
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Responsive direction
|
|
17
|
+
.directionResponsive {
|
|
18
|
+
flex-direction: var(--fui-stack-direction, column);
|
|
19
|
+
|
|
20
|
+
@media (min-width: 640px) {
|
|
21
|
+
flex-direction: var(--fui-stack-direction-sm, var(--fui-stack-direction, column));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (min-width: 768px) {
|
|
25
|
+
flex-direction: var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (min-width: 1024px) {
|
|
29
|
+
flex-direction: var(--fui-stack-direction-lg, var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column))));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (min-width: 1280px) {
|
|
33
|
+
flex-direction: var(--fui-stack-direction-xl, var(--fui-stack-direction-lg, var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column)))));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Gap sizes
|
|
38
|
+
.gap-xs {
|
|
39
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.gap-sm {
|
|
43
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.gap-md {
|
|
47
|
+
gap: var(--fui-space-3, $fui-space-3);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.gap-lg {
|
|
51
|
+
gap: var(--fui-space-4, $fui-space-4);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.gap-xl {
|
|
55
|
+
gap: var(--fui-space-6, $fui-space-6);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Responsive gap
|
|
59
|
+
.gapResponsive {
|
|
60
|
+
gap: var(--fui-stack-gap, 0);
|
|
61
|
+
|
|
62
|
+
@media (min-width: 640px) {
|
|
63
|
+
gap: var(--fui-stack-gap-sm, var(--fui-stack-gap, 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (min-width: 768px) {
|
|
67
|
+
gap: var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media (min-width: 1024px) {
|
|
71
|
+
gap: var(--fui-stack-gap-lg, var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0))));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media (min-width: 1280px) {
|
|
75
|
+
gap: var(--fui-stack-gap-xl, var(--fui-stack-gap-lg, var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0)))));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Align (cross-axis)
|
|
80
|
+
.align-start {
|
|
81
|
+
align-items: flex-start;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.align-center {
|
|
85
|
+
align-items: center;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.align-end {
|
|
89
|
+
align-items: flex-end;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.align-stretch {
|
|
93
|
+
align-items: stretch;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.align-baseline {
|
|
97
|
+
align-items: baseline;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Justify (main-axis)
|
|
101
|
+
.justify-start {
|
|
102
|
+
justify-content: flex-start;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.justify-center {
|
|
106
|
+
justify-content: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.justify-end {
|
|
110
|
+
justify-content: flex-end;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.justify-between {
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Wrap
|
|
118
|
+
.wrap {
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Stack.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
type Direction = 'row' | 'column';
|
|
6
|
+
type Gap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
|
|
8
|
+
/** Responsive value — either a single value or per-breakpoint overrides */
|
|
9
|
+
export interface ResponsiveDirection {
|
|
10
|
+
/** Default (mobile-first) */
|
|
11
|
+
base?: Direction;
|
|
12
|
+
/** ≥640px */
|
|
13
|
+
sm?: Direction;
|
|
14
|
+
/** ≥768px */
|
|
15
|
+
md?: Direction;
|
|
16
|
+
/** ≥1024px */
|
|
17
|
+
lg?: Direction;
|
|
18
|
+
/** ≥1280px */
|
|
19
|
+
xl?: Direction;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Responsive gap value */
|
|
23
|
+
export interface ResponsiveGap {
|
|
24
|
+
/** Default (mobile-first) */
|
|
25
|
+
base?: Gap;
|
|
26
|
+
/** ≥640px */
|
|
27
|
+
sm?: Gap;
|
|
28
|
+
/** ≥768px */
|
|
29
|
+
md?: Gap;
|
|
30
|
+
/** ≥1024px */
|
|
31
|
+
lg?: Gap;
|
|
32
|
+
/** ≥1280px */
|
|
33
|
+
xl?: Gap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StackProps {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Stack direction.
|
|
40
|
+
* - A string for fixed direction: `"row"` or `"column"`
|
|
41
|
+
* - An object for responsive direction: `{ base: "column", md: "row" }`
|
|
42
|
+
*/
|
|
43
|
+
direction?: Direction | ResponsiveDirection;
|
|
44
|
+
/**
|
|
45
|
+
* Gap between items.
|
|
46
|
+
* - A string for fixed gap: `"sm"`, `"md"`, etc.
|
|
47
|
+
* - An object for responsive gap: `{ base: "sm", md: "lg" }`
|
|
48
|
+
*/
|
|
49
|
+
gap?: Gap | ResponsiveGap;
|
|
50
|
+
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
51
|
+
justify?: 'start' | 'center' | 'end' | 'between';
|
|
52
|
+
wrap?: boolean;
|
|
53
|
+
as?: 'div' | 'section' | 'nav' | 'article' | 'aside' | 'header' | 'footer' | 'main' | 'ul' | 'ol';
|
|
54
|
+
className?: string;
|
|
55
|
+
style?: React.CSSProperties;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isResponsiveDirection(
|
|
59
|
+
direction: StackProps['direction']
|
|
60
|
+
): direction is ResponsiveDirection {
|
|
61
|
+
return typeof direction === 'object' && direction !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isResponsiveGap(gap: StackProps['gap']): gap is ResponsiveGap {
|
|
65
|
+
return typeof gap === 'object' && gap !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Stack = React.forwardRef<HTMLElement, StackProps>(
|
|
69
|
+
function Stack(
|
|
70
|
+
{
|
|
71
|
+
children,
|
|
72
|
+
direction = 'column',
|
|
73
|
+
gap = 'md',
|
|
74
|
+
align,
|
|
75
|
+
justify,
|
|
76
|
+
wrap = false,
|
|
77
|
+
as: Component = 'div',
|
|
78
|
+
className,
|
|
79
|
+
style,
|
|
80
|
+
},
|
|
81
|
+
ref
|
|
82
|
+
) {
|
|
83
|
+
let directionClass: string;
|
|
84
|
+
let gapClass: string | false;
|
|
85
|
+
let inlineStyle: React.CSSProperties | undefined;
|
|
86
|
+
|
|
87
|
+
// Handle responsive direction
|
|
88
|
+
if (isResponsiveDirection(direction)) {
|
|
89
|
+
directionClass = styles.directionResponsive;
|
|
90
|
+
const vars: Record<string, string> = {};
|
|
91
|
+
if (direction.base) vars['--fui-stack-direction'] = direction.base;
|
|
92
|
+
if (direction.sm) vars['--fui-stack-direction-sm'] = direction.sm;
|
|
93
|
+
if (direction.md) vars['--fui-stack-direction-md'] = direction.md;
|
|
94
|
+
if (direction.lg) vars['--fui-stack-direction-lg'] = direction.lg;
|
|
95
|
+
if (direction.xl) vars['--fui-stack-direction-xl'] = direction.xl;
|
|
96
|
+
inlineStyle = vars as unknown as React.CSSProperties;
|
|
97
|
+
} else {
|
|
98
|
+
directionClass = styles[direction];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle responsive gap
|
|
102
|
+
if (isResponsiveGap(gap)) {
|
|
103
|
+
gapClass = styles.gapResponsive;
|
|
104
|
+
const gapVars: Record<string, string> = {};
|
|
105
|
+
if (gap.base && gap.base !== 'none') gapVars['--fui-stack-gap'] = `var(--fui-space-${gapToSpace(gap.base)})`;
|
|
106
|
+
if (gap.sm && gap.sm !== 'none') gapVars['--fui-stack-gap-sm'] = `var(--fui-space-${gapToSpace(gap.sm)})`;
|
|
107
|
+
if (gap.md && gap.md !== 'none') gapVars['--fui-stack-gap-md'] = `var(--fui-space-${gapToSpace(gap.md)})`;
|
|
108
|
+
if (gap.lg && gap.lg !== 'none') gapVars['--fui-stack-gap-lg'] = `var(--fui-space-${gapToSpace(gap.lg)})`;
|
|
109
|
+
if (gap.xl && gap.xl !== 'none') gapVars['--fui-stack-gap-xl'] = `var(--fui-space-${gapToSpace(gap.xl)})`;
|
|
110
|
+
inlineStyle = { ...inlineStyle, ...gapVars } as React.CSSProperties;
|
|
111
|
+
} else {
|
|
112
|
+
gapClass = gap !== 'none' && styles[`gap-${gap}`];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const classes = [
|
|
116
|
+
styles.stack,
|
|
117
|
+
directionClass,
|
|
118
|
+
gapClass,
|
|
119
|
+
align && styles[`align-${align}`],
|
|
120
|
+
justify && styles[`justify-${justify}`],
|
|
121
|
+
wrap && styles.wrap,
|
|
122
|
+
className,
|
|
123
|
+
]
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(' ');
|
|
126
|
+
|
|
127
|
+
const mergedStyle = inlineStyle ? { ...inlineStyle, ...style } : style;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Component ref={ref as React.Ref<never>} className={classes} style={mergedStyle}>
|
|
131
|
+
{children}
|
|
132
|
+
</Component>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Map gap prop values to space variable numbers
|
|
138
|
+
function gapToSpace(gap: Gap): string {
|
|
139
|
+
const map: Record<Gap, string> = {
|
|
140
|
+
none: '0',
|
|
141
|
+
xs: '1',
|
|
142
|
+
sm: '2',
|
|
143
|
+
md: '3',
|
|
144
|
+
lg: '4',
|
|
145
|
+
xl: '6',
|
|
146
|
+
};
|
|
147
|
+
return map[gap];
|
|
148
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Table, createColumns } from '
|
|
4
|
-
import { Badge } from '../Badge
|
|
3
|
+
import { Table, createColumns } from '.';
|
|
4
|
+
import { Badge } from '../Badge';
|
|
5
5
|
|
|
6
6
|
// Sample data types
|
|
7
7
|
interface User {
|
|
@@ -43,7 +43,7 @@ export default defineSegment({
|
|
|
43
43
|
meta: {
|
|
44
44
|
name: 'Table',
|
|
45
45
|
description: 'Data table with sorting and row selection. Use for displaying structured data that needs to be scanned, compared, or acted upon.',
|
|
46
|
-
category: '
|
|
46
|
+
category: 'display',
|
|
47
47
|
status: 'stable',
|
|
48
48
|
tags: ['table', 'data', 'grid', 'list', 'sorting'],
|
|
49
49
|
since: '0.1.0',
|
|
@@ -134,6 +134,13 @@ export default defineSegment({
|
|
|
134
134
|
a11yRules: ['A11Y_TABLE_HEADERS', 'A11Y_TABLE_SORT'],
|
|
135
135
|
},
|
|
136
136
|
|
|
137
|
+
ai: {
|
|
138
|
+
compositionPattern: 'simple',
|
|
139
|
+
commonPatterns: [
|
|
140
|
+
'<Table columns={[{header:"Name",accessorKey:"name"},{header:"Status",accessorKey:"status"}]} data={[{name:"Item 1",status:"Active"}]} />',
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
|
|
137
144
|
variants: [
|
|
138
145
|
{
|
|
139
146
|
name: 'Default',
|
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
-moz-osx-font-smoothing: grayscale;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Caption for accessibility
|
|
18
|
+
.caption {
|
|
19
|
+
padding: var(--fui-space-3, $fui-space-3) 0;
|
|
20
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
21
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
22
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
23
|
+
text-align: left;
|
|
24
|
+
caption-side: top;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Visually hidden caption (screen readers only)
|
|
28
|
+
.captionHidden {
|
|
29
|
+
@include visually-hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
// Size variants
|
|
18
33
|
.sm {
|
|
19
34
|
.th,
|
|
@@ -66,6 +81,16 @@
|
|
|
66
81
|
gap: var(--fui-space-1, $fui-space-1);
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
// Sortable header cell (for focus styles)
|
|
85
|
+
.thSortable {
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
|
|
88
|
+
&:focus-visible {
|
|
89
|
+
@include focus-ring;
|
|
90
|
+
outline-offset: -2px;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
.sortable {
|
|
70
95
|
cursor: pointer;
|
|
71
96
|
transition: color var(--fui-transition-fast, $fui-transition-fast);
|
|
@@ -150,3 +175,35 @@
|
|
|
150
175
|
padding-right: var(--fui-space-4, $fui-space-4);
|
|
151
176
|
}
|
|
152
177
|
}
|
|
178
|
+
|
|
179
|
+
// ============================================
|
|
180
|
+
// Accessibility: High Contrast Mode
|
|
181
|
+
// ============================================
|
|
182
|
+
|
|
183
|
+
@media (prefers-contrast: more) {
|
|
184
|
+
.headerRow {
|
|
185
|
+
border-bottom-width: 2px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.row {
|
|
189
|
+
border-bottom-width: 2px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.thSortable:focus-visible {
|
|
193
|
+
outline-width: 3px;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================
|
|
198
|
+
// Accessibility: Reduced Motion
|
|
199
|
+
// ============================================
|
|
200
|
+
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
.row {
|
|
203
|
+
transition: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.sortable {
|
|
207
|
+
transition: none;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -16,7 +16,7 @@ import styles from './Table.module.scss';
|
|
|
16
16
|
// Column definition helper type
|
|
17
17
|
export type TableColumn<T> = ColumnDef<T, unknown>;
|
|
18
18
|
|
|
19
|
-
export interface TableProps<T> {
|
|
19
|
+
export interface TableProps<T> extends Omit<React.HTMLAttributes<HTMLTableElement>, 'onClick'> {
|
|
20
20
|
/** Column definitions */
|
|
21
21
|
columns: TableColumn<T>[];
|
|
22
22
|
/** Data array */
|
|
@@ -41,8 +41,10 @@ export interface TableProps<T> {
|
|
|
41
41
|
emptyMessage?: string;
|
|
42
42
|
/** Size variant */
|
|
43
43
|
size?: 'sm' | 'md';
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/** Visible caption for the table (recommended for accessibility) */
|
|
45
|
+
caption?: string;
|
|
46
|
+
/** Hide the caption visually but keep it for screen readers */
|
|
47
|
+
captionHidden?: boolean;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export function Table<T>({
|
|
@@ -59,6 +61,11 @@ export function Table<T>({
|
|
|
59
61
|
emptyMessage = 'No data available',
|
|
60
62
|
size = 'md',
|
|
61
63
|
className,
|
|
64
|
+
caption,
|
|
65
|
+
captionHidden = false,
|
|
66
|
+
'aria-label': ariaLabel,
|
|
67
|
+
'aria-describedby': ariaDescribedBy,
|
|
68
|
+
...htmlProps
|
|
62
69
|
}: TableProps<T>) {
|
|
63
70
|
// Internal sorting state when uncontrolled
|
|
64
71
|
const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
|
|
@@ -100,29 +107,57 @@ export function Table<T>({
|
|
|
100
107
|
);
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
// Keyboard handler for sortable headers
|
|
111
|
+
const handleHeaderKeyDown = (
|
|
112
|
+
event: React.KeyboardEvent<HTMLTableCellElement>,
|
|
113
|
+
toggleSorting: ((event: unknown) => void) | undefined
|
|
114
|
+
) => {
|
|
115
|
+
if (toggleSorting && (event.key === 'Enter' || event.key === ' ')) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
toggleSorting(event);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
103
121
|
return (
|
|
104
122
|
<div className={styles.wrapper}>
|
|
105
|
-
<table
|
|
123
|
+
<table
|
|
124
|
+
{...htmlProps}
|
|
125
|
+
className={rootClasses}
|
|
126
|
+
aria-label={ariaLabel}
|
|
127
|
+
aria-describedby={ariaDescribedBy}
|
|
128
|
+
>
|
|
129
|
+
{caption && (
|
|
130
|
+
<caption className={captionHidden ? styles.captionHidden : styles.caption}>
|
|
131
|
+
{caption}
|
|
132
|
+
</caption>
|
|
133
|
+
)}
|
|
106
134
|
<thead className={styles.thead}>
|
|
107
135
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
108
136
|
<tr key={headerGroup.id} className={styles.headerRow}>
|
|
109
137
|
{headerGroup.headers.map((header) => {
|
|
110
138
|
const canSort = sortable && header.column.getCanSort();
|
|
111
139
|
const sortDirection = header.column.getIsSorted();
|
|
140
|
+
const toggleSorting = canSort ? header.column.getToggleSortingHandler() : undefined;
|
|
112
141
|
|
|
113
142
|
return (
|
|
114
143
|
<th
|
|
115
144
|
key={header.id}
|
|
116
|
-
className={styles.th}
|
|
145
|
+
className={[styles.th, canSort && styles.thSortable].filter(Boolean).join(' ')}
|
|
117
146
|
style={{
|
|
118
147
|
width: header.getSize() !== 150 ? header.getSize() : undefined,
|
|
119
148
|
}}
|
|
120
|
-
|
|
149
|
+
scope="col"
|
|
150
|
+
tabIndex={canSort ? 0 : undefined}
|
|
151
|
+
role={canSort ? 'columnheader' : undefined}
|
|
152
|
+
onClick={toggleSorting}
|
|
153
|
+
onKeyDown={canSort ? (e) => handleHeaderKeyDown(e, toggleSorting) : undefined}
|
|
121
154
|
aria-sort={
|
|
122
155
|
sortDirection
|
|
123
156
|
? sortDirection === 'asc'
|
|
124
157
|
? 'ascending'
|
|
125
158
|
: 'descending'
|
|
159
|
+
: canSort
|
|
160
|
+
? 'none'
|
|
126
161
|
: undefined
|
|
127
162
|
}
|
|
128
163
|
>
|
|
@@ -199,6 +234,7 @@ function SortIcon() {
|
|
|
199
234
|
viewBox="0 0 12 12"
|
|
200
235
|
fill="none"
|
|
201
236
|
xmlns="http://www.w3.org/2000/svg"
|
|
237
|
+
aria-hidden="true"
|
|
202
238
|
>
|
|
203
239
|
<path
|
|
204
240
|
d="M6 2L8.5 5H3.5L6 2Z"
|
|
@@ -222,6 +258,7 @@ function SortAscIcon() {
|
|
|
222
258
|
viewBox="0 0 12 12"
|
|
223
259
|
fill="none"
|
|
224
260
|
xmlns="http://www.w3.org/2000/svg"
|
|
261
|
+
aria-hidden="true"
|
|
225
262
|
>
|
|
226
263
|
<path d="M6 2L8.5 5H3.5L6 2Z" fill="currentColor" />
|
|
227
264
|
</svg>
|
|
@@ -236,6 +273,7 @@ function SortDescIcon() {
|
|
|
236
273
|
viewBox="0 0 12 12"
|
|
237
274
|
fill="none"
|
|
238
275
|
xmlns="http://www.w3.org/2000/svg"
|
|
276
|
+
aria-hidden="true"
|
|
239
277
|
>
|
|
240
278
|
<path d="M6 10L3.5 7H8.5L6 10Z" fill="currentColor" />
|
|
241
279
|
</svg>
|