@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.
Files changed (175) hide show
  1. package/README.md +98 -2
  2. package/fragments.json +1 -1
  3. package/package.json +11 -5
  4. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  5. package/src/components/Accordion/Accordion.module.scss +111 -0
  6. package/src/components/Accordion/index.tsx +271 -0
  7. package/src/components/Alert/Alert.fragment.tsx +67 -42
  8. package/src/components/Alert/Alert.module.scss +31 -21
  9. package/src/components/Alert/index.tsx +202 -73
  10. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  11. package/src/components/AppShell/AppShell.module.scss +213 -0
  12. package/src/components/AppShell/index.tsx +398 -0
  13. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  14. package/src/components/Avatar/index.tsx +8 -9
  15. package/src/components/Badge/Badge.fragment.tsx +2 -2
  16. package/src/components/Badge/Badge.module.scss +16 -10
  17. package/src/components/Badge/index.tsx +20 -6
  18. package/src/components/Box/Box.fragment.tsx +168 -0
  19. package/src/components/Box/Box.module.scss +84 -0
  20. package/src/components/Box/index.tsx +78 -0
  21. package/src/components/Button/Button.fragment.tsx +2 -2
  22. package/src/components/Button/Button.module.scss +42 -0
  23. package/src/components/Button/index.tsx +67 -33
  24. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
  25. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  26. package/src/components/ButtonGroup/index.tsx +40 -0
  27. package/src/components/Card/Card.fragment.tsx +52 -26
  28. package/src/components/Card/Card.module.scss +52 -5
  29. package/src/components/Card/index.tsx +154 -53
  30. package/src/components/Chart/Chart.fragment.tsx +213 -0
  31. package/src/components/Chart/Chart.module.scss +123 -0
  32. package/src/components/Chart/index.tsx +267 -0
  33. package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
  34. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  35. package/src/components/Checkbox/index.tsx +3 -4
  36. package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
  37. package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
  38. package/src/components/CodeBlock/index.tsx +599 -0
  39. package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
  40. package/src/components/Collapsible/Collapsible.module.scss +117 -0
  41. package/src/components/Collapsible/index.tsx +219 -0
  42. package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
  43. package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
  44. package/src/components/ColorPicker/index.tsx +129 -0
  45. package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
  46. package/src/components/ConversationList/ConversationList.module.scss +160 -0
  47. package/src/components/ConversationList/index.tsx +254 -0
  48. package/src/components/Dialog/Dialog.fragment.tsx +12 -3
  49. package/src/components/Dialog/Dialog.module.scss +26 -7
  50. package/src/components/Dialog/index.tsx +12 -15
  51. package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
  52. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  53. package/src/components/EmptyState/index.tsx +104 -69
  54. package/src/components/Field/Field.fragment.tsx +165 -0
  55. package/src/components/Field/Field.module.scss +31 -0
  56. package/src/components/Field/index.tsx +143 -0
  57. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  58. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  59. package/src/components/Fieldset/index.tsx +47 -0
  60. package/src/components/Form/Form.fragment.tsx +286 -0
  61. package/src/components/Form/Form.module.scss +8 -0
  62. package/src/components/Form/index.tsx +53 -0
  63. package/src/components/Grid/Grid.fragment.tsx +18 -18
  64. package/src/components/Grid/index.tsx +6 -1
  65. package/src/components/Header/Header.fragment.tsx +192 -0
  66. package/src/components/Header/Header.module.scss +208 -0
  67. package/src/components/Header/index.tsx +363 -0
  68. package/src/components/Icon/Icon.fragment.tsx +138 -0
  69. package/src/components/Icon/Icon.module.scss +38 -0
  70. package/src/components/Icon/index.tsx +58 -0
  71. package/src/components/Image/Image.fragment.tsx +195 -0
  72. package/src/components/Image/Image.module.scss +77 -0
  73. package/src/components/Image/index.tsx +95 -0
  74. package/src/components/Input/Input.fragment.tsx +1 -1
  75. package/src/components/Input/Input.module.scss +75 -2
  76. package/src/components/Input/index.tsx +60 -21
  77. package/src/components/Link/Link.fragment.tsx +132 -0
  78. package/src/components/Link/Link.module.scss +67 -0
  79. package/src/components/Link/index.tsx +57 -0
  80. package/src/components/List/List.fragment.tsx +152 -0
  81. package/src/components/List/List.module.scss +71 -0
  82. package/src/components/List/index.tsx +106 -0
  83. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  84. package/src/components/Listbox/Listbox.module.scss +97 -0
  85. package/src/components/Listbox/index.tsx +121 -0
  86. package/src/components/Loading/Loading.fragment.tsx +153 -0
  87. package/src/components/Loading/Loading.module.scss +256 -0
  88. package/src/components/Loading/index.tsx +236 -0
  89. package/src/components/Menu/Menu.fragment.tsx +12 -3
  90. package/src/components/Menu/Menu.module.scss +17 -1
  91. package/src/components/Menu/index.tsx +3 -3
  92. package/src/components/Message/Message.fragment.tsx +200 -0
  93. package/src/components/Message/Message.module.scss +224 -0
  94. package/src/components/Message/index.tsx +278 -0
  95. package/src/components/Popover/Popover.fragment.tsx +13 -4
  96. package/src/components/Popover/Popover.module.scss +33 -10
  97. package/src/components/Popover/index.tsx +9 -11
  98. package/src/components/Progress/Progress.fragment.tsx +1 -1
  99. package/src/components/Progress/Progress.module.scss +11 -11
  100. package/src/components/Progress/index.tsx +34 -7
  101. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  102. package/src/components/Prompt/Prompt.module.scss +243 -0
  103. package/src/components/Prompt/index.tsx +439 -0
  104. package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
  105. package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
  106. package/src/components/RadioGroup/index.tsx +3 -4
  107. package/src/components/Select/Select.fragment.tsx +10 -1
  108. package/src/components/Select/Select.module.scss +8 -0
  109. package/src/components/Select/index.tsx +91 -12
  110. package/src/components/Separator/Separator.fragment.tsx +1 -1
  111. package/src/components/Separator/index.tsx +7 -3
  112. package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
  113. package/src/components/Sidebar/Sidebar.module.scss +91 -47
  114. package/src/components/Sidebar/index.tsx +57 -14
  115. package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
  116. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  117. package/src/components/Slider/Slider.fragment.tsx +201 -0
  118. package/src/components/Slider/Slider.module.scss +87 -0
  119. package/src/components/Slider/index.tsx +88 -0
  120. package/src/components/Stack/Stack.fragment.tsx +194 -0
  121. package/src/components/Stack/Stack.module.scss +120 -0
  122. package/src/components/Stack/index.tsx +148 -0
  123. package/src/components/Table/Table.fragment.tsx +10 -3
  124. package/src/components/Table/Table.module.scss +57 -0
  125. package/src/components/Table/index.tsx +44 -6
  126. package/src/components/Tabs/Tabs.fragment.tsx +10 -1
  127. package/src/components/Tabs/Tabs.module.scss +25 -10
  128. package/src/components/Tabs/index.tsx +11 -8
  129. package/src/components/Text/Text.fragment.tsx +188 -0
  130. package/src/components/Text/Text.module.scss +82 -0
  131. package/src/components/Text/index.tsx +58 -0
  132. package/src/components/Textarea/Textarea.fragment.tsx +1 -1
  133. package/src/components/Textarea/index.tsx +3 -7
  134. package/src/components/Theme/Theme.fragment.tsx +128 -0
  135. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  136. package/src/components/Theme/index.tsx +343 -0
  137. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
  138. package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
  139. package/src/components/ThinkingIndicator/index.tsx +258 -0
  140. package/src/components/Toast/Toast.fragment.tsx +6 -6
  141. package/src/components/Toast/Toast.module.scss +16 -1
  142. package/src/components/Toast/index.tsx +27 -11
  143. package/src/components/Toggle/Toggle.fragment.tsx +1 -1
  144. package/src/components/Toggle/Toggle.module.scss +25 -10
  145. package/src/components/Toggle/index.tsx +12 -0
  146. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
  147. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  148. package/src/components/ToggleGroup/index.tsx +144 -0
  149. package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
  150. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  151. package/src/components/Tooltip/index.tsx +4 -2
  152. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  153. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  154. package/src/components/VisuallyHidden/index.tsx +29 -0
  155. package/src/index.ts +278 -3
  156. package/src/recipes/AIChat.recipe.ts +266 -0
  157. package/src/recipes/AppShell.recipe.ts +175 -0
  158. package/src/recipes/CardGrid.recipe.ts +6 -2
  159. package/src/recipes/ChatInterface.recipe.ts +87 -0
  160. package/src/recipes/CodeExamples.recipe.ts +66 -0
  161. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  162. package/src/recipes/DashboardNav.recipe.ts +183 -0
  163. package/src/recipes/LoginForm.recipe.ts +8 -1
  164. package/src/recipes/SettingsPage.recipe.ts +37 -20
  165. package/src/styles/globals.scss +31 -0
  166. package/src/tokens/_computed.scss +212 -0
  167. package/src/tokens/_density.scss +171 -0
  168. package/src/tokens/_derive.scss +287 -0
  169. package/src/tokens/_index.scss +41 -0
  170. package/src/tokens/_mixins.scss +95 -1
  171. package/src/tokens/_palettes.scss +185 -0
  172. package/src/tokens/_radius.scss +107 -0
  173. package/src/tokens/_seeds.scss +59 -0
  174. package/src/tokens/_variables.scss +507 -101
  175. 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 './index.js';
4
- import { Badge } from '../Badge/index.js';
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: 'data-display',
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
- /** Additional class name */
45
- className?: string;
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 className={rootClasses}>
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
- onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
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>