@fragments-sdk/ui 0.2.3 → 0.4.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 (133) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +9 -4
  3. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  4. package/src/components/Accordion/Accordion.module.scss +111 -0
  5. package/src/components/Accordion/index.tsx +271 -0
  6. package/src/components/Alert/Alert.fragment.tsx +66 -41
  7. package/src/components/Alert/Alert.module.scss +31 -21
  8. package/src/components/Alert/index.tsx +202 -73
  9. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  10. package/src/components/AppShell/AppShell.module.scss +213 -0
  11. package/src/components/AppShell/index.tsx +398 -0
  12. package/src/components/Avatar/index.tsx +8 -9
  13. package/src/components/Badge/Badge.module.scss +16 -10
  14. package/src/components/Badge/index.tsx +20 -6
  15. package/src/components/Box/Box.fragment.tsx +168 -0
  16. package/src/components/Box/Box.module.scss +84 -0
  17. package/src/components/Box/index.tsx +78 -0
  18. package/src/components/Button/Button.module.scss +42 -0
  19. package/src/components/Button/index.tsx +67 -33
  20. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  21. package/src/components/ButtonGroup/index.tsx +40 -0
  22. package/src/components/Card/Card.fragment.tsx +51 -25
  23. package/src/components/Card/Card.module.scss +52 -5
  24. package/src/components/Card/index.tsx +154 -53
  25. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  26. package/src/components/Checkbox/index.tsx +3 -4
  27. package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
  28. package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
  29. package/src/components/CodeBlock/index.tsx +385 -0
  30. package/src/components/ColorChip/ColorChip.module.scss +165 -0
  31. package/src/components/ColorChip/index.tsx +157 -0
  32. package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
  33. package/src/components/ColorPicker/index.tsx +107 -0
  34. package/src/components/Dialog/Dialog.fragment.tsx +9 -0
  35. package/src/components/Dialog/Dialog.module.scss +26 -7
  36. package/src/components/Dialog/index.tsx +12 -15
  37. package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
  38. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  39. package/src/components/EmptyState/index.tsx +104 -69
  40. package/src/components/Field/Field.fragment.tsx +165 -0
  41. package/src/components/Field/Field.module.scss +31 -0
  42. package/src/components/Field/index.tsx +143 -0
  43. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  44. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  45. package/src/components/Fieldset/index.tsx +47 -0
  46. package/src/components/Form/Form.fragment.tsx +286 -0
  47. package/src/components/Form/Form.module.scss +8 -0
  48. package/src/components/Form/index.tsx +53 -0
  49. package/src/components/Grid/Grid.fragment.tsx +17 -17
  50. package/src/components/Grid/index.tsx +6 -1
  51. package/src/components/Header/Header.fragment.tsx +192 -0
  52. package/src/components/Header/Header.module.scss +209 -0
  53. package/src/components/Header/index.tsx +363 -0
  54. package/src/components/Icon/Icon.fragment.tsx +138 -0
  55. package/src/components/Icon/Icon.module.scss +38 -0
  56. package/src/components/Icon/index.tsx +58 -0
  57. package/src/components/Image/Image.fragment.tsx +195 -0
  58. package/src/components/Image/Image.module.scss +77 -0
  59. package/src/components/Image/index.tsx +95 -0
  60. package/src/components/Input/Input.module.scss +75 -2
  61. package/src/components/Input/index.tsx +60 -21
  62. package/src/components/Link/Link.fragment.tsx +132 -0
  63. package/src/components/Link/Link.module.scss +67 -0
  64. package/src/components/Link/index.tsx +57 -0
  65. package/src/components/List/List.fragment.tsx +152 -0
  66. package/src/components/List/List.module.scss +71 -0
  67. package/src/components/List/index.tsx +106 -0
  68. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  69. package/src/components/Listbox/Listbox.module.scss +97 -0
  70. package/src/components/Listbox/index.tsx +121 -0
  71. package/src/components/Menu/Menu.fragment.tsx +9 -0
  72. package/src/components/Menu/Menu.module.scss +17 -1
  73. package/src/components/Menu/index.tsx +3 -3
  74. package/src/components/Popover/Popover.fragment.tsx +9 -0
  75. package/src/components/Popover/Popover.module.scss +33 -10
  76. package/src/components/Popover/index.tsx +9 -11
  77. package/src/components/Progress/Progress.module.scss +11 -11
  78. package/src/components/Progress/index.tsx +34 -7
  79. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  80. package/src/components/Prompt/Prompt.module.scss +243 -0
  81. package/src/components/Prompt/index.tsx +439 -0
  82. package/src/components/RadioGroup/RadioGroup.module.scss +3 -3
  83. package/src/components/RadioGroup/index.tsx +3 -4
  84. package/src/components/Select/Select.fragment.tsx +9 -0
  85. package/src/components/Select/index.tsx +6 -7
  86. package/src/components/Separator/index.tsx +7 -3
  87. package/src/components/Sidebar/Sidebar.fragment.tsx +783 -0
  88. package/src/components/Sidebar/Sidebar.module.scss +586 -0
  89. package/src/components/Sidebar/index.tsx +1013 -0
  90. package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
  91. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  92. package/src/components/Slider/Slider.module.scss +87 -0
  93. package/src/components/Slider/index.tsx +88 -0
  94. package/src/components/Stack/Stack.module.scss +120 -0
  95. package/src/components/Stack/index.tsx +148 -0
  96. package/src/components/Table/Table.fragment.tsx +7 -0
  97. package/src/components/Table/Table.module.scss +57 -0
  98. package/src/components/Table/index.tsx +44 -6
  99. package/src/components/Tabs/Tabs.fragment.tsx +9 -0
  100. package/src/components/Tabs/Tabs.module.scss +25 -10
  101. package/src/components/Tabs/index.tsx +11 -8
  102. package/src/components/Text/Text.module.scss +82 -0
  103. package/src/components/Text/index.tsx +58 -0
  104. package/src/components/Textarea/index.tsx +3 -7
  105. package/src/components/Theme/Theme.fragment.tsx +128 -0
  106. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  107. package/src/components/Theme/index.tsx +343 -0
  108. package/src/components/Toast/Toast.fragment.tsx +5 -5
  109. package/src/components/Toast/Toast.module.scss +16 -1
  110. package/src/components/Toast/index.tsx +27 -11
  111. package/src/components/Toggle/Toggle.module.scss +25 -10
  112. package/src/components/Toggle/index.tsx +12 -0
  113. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  114. package/src/components/ToggleGroup/index.tsx +144 -0
  115. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  116. package/src/components/Tooltip/index.tsx +4 -2
  117. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  118. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  119. package/src/components/VisuallyHidden/index.tsx +29 -0
  120. package/src/index.ts +241 -3
  121. package/src/recipes/AppShell.recipe.ts +175 -0
  122. package/src/recipes/CardGrid.recipe.ts +6 -2
  123. package/src/recipes/ChatInterface.recipe.ts +87 -0
  124. package/src/recipes/CodeExamples.recipe.ts +66 -0
  125. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  126. package/src/recipes/DashboardNav.recipe.ts +183 -0
  127. package/src/recipes/LoginForm.recipe.ts +8 -1
  128. package/src/recipes/SettingsPage.recipe.ts +37 -20
  129. package/src/styles/globals.scss +31 -0
  130. package/src/tokens/_index.scss +3 -0
  131. package/src/tokens/_mixins.scss +54 -1
  132. package/src/tokens/_variables.scss +429 -64
  133. package/src/utils/a11y.tsx +439 -0
@@ -0,0 +1,168 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Box } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Box,
7
+
8
+ meta: {
9
+ name: 'Box',
10
+ description: 'Primitive layout component for applying spacing, backgrounds, and borders. A flexible container for building custom layouts.',
11
+ category: 'layout',
12
+ status: 'stable',
13
+ tags: ['layout', 'container', 'spacing', 'primitive', 'box'],
14
+ since: '0.1.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Applying consistent padding or margin to content sections',
20
+ 'Creating bordered or elevated containers',
21
+ 'Wrapping content with semantic HTML elements',
22
+ 'Building custom layouts not covered by Stack or Grid',
23
+ ],
24
+ whenNot: [
25
+ 'Horizontal or vertical stacking (use Stack)',
26
+ 'Grid-based layouts (use Grid)',
27
+ 'Card-like containers with header/body/footer (use Card)',
28
+ 'Simple text styling (use Text)',
29
+ ],
30
+ guidelines: [
31
+ 'Use padding props instead of inline styles for consistency',
32
+ 'Choose semantic HTML elements (section, article) where appropriate',
33
+ 'Combine with Stack or Grid for complex layouts',
34
+ 'Use background variants from the design system, not custom colors',
35
+ ],
36
+ accessibility: [
37
+ 'Choose semantic as prop values for proper document structure',
38
+ 'Avoid div-soup; use meaningful elements like section, article',
39
+ 'Ensure proper heading hierarchy within Box containers',
40
+ ],
41
+ },
42
+
43
+ props: {
44
+ children: {
45
+ type: 'node',
46
+ description: 'Content to render inside the box',
47
+ },
48
+ as: {
49
+ type: 'enum',
50
+ description: 'HTML element to render',
51
+ values: ['div', 'section', 'article', 'aside', 'main', 'header', 'footer', 'nav', 'span'],
52
+ default: 'div',
53
+ },
54
+ padding: {
55
+ type: 'enum',
56
+ description: 'Padding on all sides',
57
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
58
+ },
59
+ paddingX: {
60
+ type: 'enum',
61
+ description: 'Horizontal padding',
62
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
63
+ },
64
+ paddingY: {
65
+ type: 'enum',
66
+ description: 'Vertical padding',
67
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
68
+ },
69
+ margin: {
70
+ type: 'enum',
71
+ description: 'Margin on all sides',
72
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl', 'auto'],
73
+ },
74
+ marginX: {
75
+ type: 'enum',
76
+ description: 'Horizontal margin',
77
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl', 'auto'],
78
+ },
79
+ marginY: {
80
+ type: 'enum',
81
+ description: 'Vertical margin',
82
+ values: ['none', 'xs', 'sm', 'md', 'lg', 'xl', 'auto'],
83
+ },
84
+ background: {
85
+ type: 'enum',
86
+ description: 'Background color',
87
+ values: ['none', 'primary', 'secondary', 'tertiary', 'elevated'],
88
+ },
89
+ rounded: {
90
+ type: 'enum',
91
+ description: 'Border radius',
92
+ values: ['none', 'sm', 'md', 'lg', 'full'],
93
+ },
94
+ border: {
95
+ type: 'boolean',
96
+ description: 'Show border',
97
+ default: 'false',
98
+ },
99
+ display: {
100
+ type: 'enum',
101
+ description: 'Display type',
102
+ values: ['block', 'inline', 'inline-block', 'flex', 'inline-flex', 'grid', 'none'],
103
+ },
104
+ },
105
+
106
+ relations: [
107
+ { component: 'Stack', relationship: 'alternative', note: 'Use Stack for directional layouts with gap' },
108
+ { component: 'Grid', relationship: 'alternative', note: 'Use Grid for column-based layouts' },
109
+ { component: 'Card', relationship: 'alternative', note: 'Use Card for content containers with structure' },
110
+ ],
111
+
112
+ contract: {
113
+ propsSummary: [
114
+ 'as: div|section|article|... - HTML element',
115
+ 'padding: none|xs|sm|md|lg|xl - all-sides padding',
116
+ 'paddingX/paddingY: directional padding overrides',
117
+ 'margin: none|xs|sm|md|lg|xl|auto - margin',
118
+ 'background: none|primary|secondary|tertiary|elevated',
119
+ 'rounded: none|sm|md|lg|full - border radius',
120
+ 'border: boolean - show border',
121
+ ],
122
+ scenarioTags: [
123
+ 'layout.container',
124
+ 'spacing.wrapper',
125
+ 'structure.section',
126
+ ],
127
+ a11yRules: ['A11Y_SEMANTIC_HTML'],
128
+ },
129
+
130
+ variants: [
131
+ {
132
+ name: 'Default',
133
+ description: 'Basic box with padding',
134
+ render: () => (
135
+ <Box padding="md" background="secondary" rounded="md">
136
+ Content with padding and background
137
+ </Box>
138
+ ),
139
+ },
140
+ {
141
+ name: 'With Border',
142
+ description: 'Bordered container',
143
+ render: () => (
144
+ <Box padding="lg" border rounded="md">
145
+ Bordered content area
146
+ </Box>
147
+ ),
148
+ },
149
+ {
150
+ name: 'Directional Padding',
151
+ description: 'Different horizontal and vertical padding',
152
+ render: () => (
153
+ <Box paddingX="xl" paddingY="sm" background="tertiary" rounded="sm">
154
+ Wide horizontal padding, short vertical
155
+ </Box>
156
+ ),
157
+ },
158
+ {
159
+ name: 'Centered with Auto Margin',
160
+ description: 'Centered content using margin auto',
161
+ render: () => (
162
+ <Box padding="md" marginX="auto" background="elevated" rounded="lg" style={{ maxWidth: '300px' }}>
163
+ Centered content
164
+ </Box>
165
+ ),
166
+ },
167
+ ],
168
+ });
@@ -0,0 +1,84 @@
1
+ @use '../../tokens/variables' as *;
2
+
3
+ .box {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ // Padding
8
+ .p-none { padding: 0; }
9
+ .p-xs { padding: var(--fui-space-1, $fui-space-1); }
10
+ .p-sm { padding: var(--fui-space-2, $fui-space-2); }
11
+ .p-md { padding: var(--fui-space-4, $fui-space-4); }
12
+ .p-lg { padding: var(--fui-space-6, $fui-space-6); }
13
+ .p-xl { padding: var(--fui-space-8, $fui-space-8); }
14
+
15
+ // Padding X
16
+ .px-none { padding-left: 0; padding-right: 0; }
17
+ .px-xs { padding-left: var(--fui-space-1, $fui-space-1); padding-right: var(--fui-space-1, $fui-space-1); }
18
+ .px-sm { padding-left: var(--fui-space-2, $fui-space-2); padding-right: var(--fui-space-2, $fui-space-2); }
19
+ .px-md { padding-left: var(--fui-space-4, $fui-space-4); padding-right: var(--fui-space-4, $fui-space-4); }
20
+ .px-lg { padding-left: var(--fui-space-6, $fui-space-6); padding-right: var(--fui-space-6, $fui-space-6); }
21
+ .px-xl { padding-left: var(--fui-space-8, $fui-space-8); padding-right: var(--fui-space-8, $fui-space-8); }
22
+
23
+ // Padding Y
24
+ .py-none { padding-top: 0; padding-bottom: 0; }
25
+ .py-xs { padding-top: var(--fui-space-1, $fui-space-1); padding-bottom: var(--fui-space-1, $fui-space-1); }
26
+ .py-sm { padding-top: var(--fui-space-2, $fui-space-2); padding-bottom: var(--fui-space-2, $fui-space-2); }
27
+ .py-md { padding-top: var(--fui-space-4, $fui-space-4); padding-bottom: var(--fui-space-4, $fui-space-4); }
28
+ .py-lg { padding-top: var(--fui-space-6, $fui-space-6); padding-bottom: var(--fui-space-6, $fui-space-6); }
29
+ .py-xl { padding-top: var(--fui-space-8, $fui-space-8); padding-bottom: var(--fui-space-8, $fui-space-8); }
30
+
31
+ // Margin
32
+ .m-none { margin: 0; }
33
+ .m-xs { margin: var(--fui-space-1, $fui-space-1); }
34
+ .m-sm { margin: var(--fui-space-2, $fui-space-2); }
35
+ .m-md { margin: var(--fui-space-4, $fui-space-4); }
36
+ .m-lg { margin: var(--fui-space-6, $fui-space-6); }
37
+ .m-xl { margin: var(--fui-space-8, $fui-space-8); }
38
+ .m-auto { margin: auto; }
39
+
40
+ // Margin X
41
+ .mx-none { margin-left: 0; margin-right: 0; }
42
+ .mx-xs { margin-left: var(--fui-space-1, $fui-space-1); margin-right: var(--fui-space-1, $fui-space-1); }
43
+ .mx-sm { margin-left: var(--fui-space-2, $fui-space-2); margin-right: var(--fui-space-2, $fui-space-2); }
44
+ .mx-md { margin-left: var(--fui-space-4, $fui-space-4); margin-right: var(--fui-space-4, $fui-space-4); }
45
+ .mx-lg { margin-left: var(--fui-space-6, $fui-space-6); margin-right: var(--fui-space-6, $fui-space-6); }
46
+ .mx-xl { margin-left: var(--fui-space-8, $fui-space-8); margin-right: var(--fui-space-8, $fui-space-8); }
47
+ .mx-auto { margin-left: auto; margin-right: auto; }
48
+
49
+ // Margin Y
50
+ .my-none { margin-top: 0; margin-bottom: 0; }
51
+ .my-xs { margin-top: var(--fui-space-1, $fui-space-1); margin-bottom: var(--fui-space-1, $fui-space-1); }
52
+ .my-sm { margin-top: var(--fui-space-2, $fui-space-2); margin-bottom: var(--fui-space-2, $fui-space-2); }
53
+ .my-md { margin-top: var(--fui-space-4, $fui-space-4); margin-bottom: var(--fui-space-4, $fui-space-4); }
54
+ .my-lg { margin-top: var(--fui-space-6, $fui-space-6); margin-bottom: var(--fui-space-6, $fui-space-6); }
55
+ .my-xl { margin-top: var(--fui-space-8, $fui-space-8); margin-bottom: var(--fui-space-8, $fui-space-8); }
56
+ .my-auto { margin-top: auto; margin-bottom: auto; }
57
+
58
+ // Background
59
+ .bg-none { background-color: transparent; }
60
+ .bg-primary { background-color: var(--fui-bg-primary, $fui-bg-primary); }
61
+ .bg-secondary { background-color: var(--fui-bg-secondary, $fui-bg-secondary); }
62
+ .bg-tertiary { background-color: var(--fui-bg-tertiary, $fui-bg-tertiary); }
63
+ .bg-elevated { background-color: var(--fui-bg-elevated, $fui-bg-elevated); }
64
+
65
+ // Border radius
66
+ .rounded-none { border-radius: 0; }
67
+ .rounded-sm { border-radius: var(--fui-radius-sm, $fui-radius-sm); }
68
+ .rounded-md { border-radius: var(--fui-radius-md, $fui-radius-md); }
69
+ .rounded-lg { border-radius: var(--fui-radius-lg, $fui-radius-lg); }
70
+ .rounded-full { border-radius: var(--fui-radius-full, $fui-radius-full); }
71
+
72
+ // Border
73
+ .border {
74
+ border: 1px solid var(--fui-border, $fui-border);
75
+ }
76
+
77
+ // Display
78
+ .display-block { display: block; }
79
+ .display-inline { display: inline; }
80
+ .display-inline-block { display: inline-block; }
81
+ .display-flex { display: flex; }
82
+ .display-inline-flex { display: inline-flex; }
83
+ .display-grid { display: grid; }
84
+ .display-none { display: none; }
@@ -0,0 +1,78 @@
1
+ import * as React from 'react';
2
+ import styles from './Box.module.scss';
3
+ import '../../styles/globals.scss';
4
+
5
+ export interface BoxProps {
6
+ children?: React.ReactNode;
7
+ /** HTML element to render */
8
+ as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer' | 'nav' | 'span';
9
+ /** Padding on all sides */
10
+ padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ /** Horizontal padding (overrides padding) */
12
+ paddingX?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
13
+ /** Vertical padding (overrides padding) */
14
+ paddingY?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
15
+ /** Margin on all sides */
16
+ margin?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto';
17
+ /** Horizontal margin (overrides margin) */
18
+ marginX?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto';
19
+ /** Vertical margin (overrides margin) */
20
+ marginY?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto';
21
+ /** Background color */
22
+ background?: 'none' | 'primary' | 'secondary' | 'tertiary' | 'elevated';
23
+ /** Border radius */
24
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';
25
+ /** Border */
26
+ border?: boolean;
27
+ /** Display type */
28
+ display?: 'block' | 'inline' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none';
29
+ /** Additional class name */
30
+ className?: string;
31
+ /** Inline styles */
32
+ style?: React.CSSProperties;
33
+ }
34
+
35
+ export const Box = React.forwardRef<HTMLElement, BoxProps>(
36
+ function Box(
37
+ {
38
+ children,
39
+ as: Component = 'div',
40
+ padding,
41
+ paddingX,
42
+ paddingY,
43
+ margin,
44
+ marginX,
45
+ marginY,
46
+ background,
47
+ rounded,
48
+ border,
49
+ display,
50
+ className,
51
+ style,
52
+ },
53
+ ref
54
+ ) {
55
+ const classes = [
56
+ styles.box,
57
+ padding && styles[`p-${padding}`],
58
+ paddingX && styles[`px-${paddingX}`],
59
+ paddingY && styles[`py-${paddingY}`],
60
+ margin && styles[`m-${margin}`],
61
+ marginX && styles[`mx-${marginX}`],
62
+ marginY && styles[`my-${marginY}`],
63
+ background && styles[`bg-${background}`],
64
+ rounded && styles[`rounded-${rounded}`],
65
+ border && styles.border,
66
+ display && styles[`display-${display}`],
67
+ className,
68
+ ]
69
+ .filter(Boolean)
70
+ .join(' ');
71
+
72
+ return (
73
+ <Component ref={ref as React.Ref<never>} className={classes} style={style}>
74
+ {children}
75
+ </Component>
76
+ );
77
+ }
78
+ );
@@ -95,3 +95,45 @@
95
95
  background-color: var(--fui-color-danger-hover, $fui-color-danger-hover);
96
96
  }
97
97
  }
98
+
99
+ // Icon-only button (square aspect ratio)
100
+ .icon {
101
+ aspect-ratio: 1;
102
+ padding: 0;
103
+
104
+ &.sm {
105
+ width: var(--fui-button-height-sm, $fui-button-height-sm);
106
+ }
107
+
108
+ &.md {
109
+ width: var(--fui-button-height-md, $fui-button-height-md);
110
+ }
111
+
112
+ &.lg {
113
+ width: var(--fui-button-height-lg, $fui-button-height-lg);
114
+ }
115
+ }
116
+
117
+ // Full width
118
+ .fullWidth {
119
+ width: 100%;
120
+ }
121
+
122
+ // ============================================
123
+ // Accessibility: High Contrast Mode
124
+ // ============================================
125
+
126
+ @media (prefers-contrast: more) {
127
+ .secondary {
128
+ border-width: 2px;
129
+ border-color: var(--fui-text-primary, $fui-text-primary);
130
+ }
131
+
132
+ .ghost {
133
+ border-width: 2px;
134
+
135
+ &:hover:not(:disabled) {
136
+ border-color: var(--fui-text-primary, $fui-text-primary);
137
+ }
138
+ }
139
+ }
@@ -4,48 +4,82 @@ import styles from './Button.module.scss';
4
4
  // Import globals to ensure CSS variables are defined
5
5
  import '../../styles/globals.scss';
6
6
 
7
- export interface ButtonProps {
7
+ type ButtonBaseProps = {
8
8
  children: React.ReactNode;
9
9
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
10
10
  size?: 'sm' | 'md' | 'lg';
11
- disabled?: boolean;
12
- onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
13
- type?: 'button' | 'submit' | 'reset';
14
- className?: string;
11
+ /** Render as icon-only button (square aspect ratio) */
12
+ icon?: boolean;
13
+ /** Make button full width of container */
14
+ fullWidth?: boolean;
15
+ };
16
+
17
+ // Button as native button element
18
+ export interface ButtonAsButtonProps
19
+ extends ButtonBaseProps,
20
+ Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
21
+ as?: 'button';
22
+ }
23
+
24
+ // Button as anchor element
25
+ export interface ButtonAsAnchorProps
26
+ extends ButtonBaseProps,
27
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> {
28
+ as: 'a';
15
29
  }
16
30
 
17
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
18
- function Button(
19
- {
20
- children,
21
- variant = 'primary',
22
- size = 'md',
23
- disabled = false,
24
- onClick,
25
- type = 'button',
26
- className,
27
- },
28
- ref
29
- ) {
30
- const classNames = [
31
- styles.button,
32
- styles[size],
33
- styles[variant],
34
- className,
35
- ]
36
- .filter(Boolean)
37
- .join(' ');
31
+ export type ButtonProps = ButtonAsButtonProps | ButtonAsAnchorProps;
38
32
 
33
+ export const Button = React.forwardRef<
34
+ HTMLButtonElement | HTMLAnchorElement,
35
+ ButtonProps
36
+ >(function Button(props, ref) {
37
+ const {
38
+ children,
39
+ variant = 'primary',
40
+ size = 'md',
41
+ icon = false,
42
+ fullWidth = false,
43
+ className,
44
+ ...rest
45
+ } = props;
46
+
47
+ const classNames = [
48
+ styles.button,
49
+ styles[size],
50
+ styles[variant],
51
+ icon && styles.icon,
52
+ fullWidth && styles.fullWidth,
53
+ className,
54
+ ]
55
+ .filter(Boolean)
56
+ .join(' ');
57
+
58
+ // Render as anchor
59
+ if (props.as === 'a') {
60
+ const { as: _as, ...anchorProps } = rest as ButtonAsAnchorProps & { as?: 'a' };
39
61
  return (
40
- <BaseButton
41
- ref={ref}
42
- type={type}
43
- disabled={disabled}
44
- onClick={onClick}
62
+ <a
63
+ ref={ref as React.Ref<HTMLAnchorElement>}
45
64
  className={classNames}
65
+ {...anchorProps}
46
66
  >
47
67
  {children}
48
- </BaseButton>
68
+ </a>
49
69
  );
50
70
  }
51
- );
71
+
72
+ // Render as button (default)
73
+ const { as: _as, ...buttonProps } = rest as ButtonAsButtonProps;
74
+ return (
75
+ <BaseButton
76
+ ref={ref as React.Ref<HTMLButtonElement>}
77
+ type={(buttonProps as ButtonAsButtonProps).type || 'button'}
78
+ disabled={(buttonProps as ButtonAsButtonProps).disabled || false}
79
+ className={classNames}
80
+ {...buttonProps}
81
+ >
82
+ {children}
83
+ </BaseButton>
84
+ );
85
+ });
@@ -0,0 +1,37 @@
1
+ @use '../../tokens/variables' as *;
2
+
3
+ .group {
4
+ display: flex;
5
+ flex-direction: row;
6
+ }
7
+
8
+ // Gap sizes
9
+ .gap-xs {
10
+ gap: var(--fui-space-1, $fui-space-1);
11
+ }
12
+
13
+ .gap-sm {
14
+ gap: var(--fui-space-2, $fui-space-2);
15
+ }
16
+
17
+ .gap-md {
18
+ gap: var(--fui-space-3, $fui-space-3);
19
+ }
20
+
21
+ // Wrap
22
+ .wrap {
23
+ flex-wrap: wrap;
24
+ }
25
+
26
+ // Alignment
27
+ .align-start {
28
+ justify-content: flex-start;
29
+ }
30
+
31
+ .align-center {
32
+ justify-content: center;
33
+ }
34
+
35
+ .align-end {
36
+ justify-content: flex-end;
37
+ }
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+ import styles from './ButtonGroup.module.scss';
3
+ import '../../styles/globals.scss';
4
+
5
+ export interface ButtonGroupProps {
6
+ children: React.ReactNode;
7
+ gap?: 'none' | 'xs' | 'sm' | 'md';
8
+ wrap?: boolean;
9
+ align?: 'start' | 'center' | 'end';
10
+ className?: string;
11
+ }
12
+
13
+ export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
14
+ function ButtonGroup(
15
+ {
16
+ children,
17
+ gap = 'sm',
18
+ wrap = false,
19
+ align,
20
+ className,
21
+ },
22
+ ref
23
+ ) {
24
+ const classes = [
25
+ styles.group,
26
+ gap !== 'none' && styles[`gap-${gap}`],
27
+ wrap && styles.wrap,
28
+ align && styles[`align-${align}`],
29
+ className,
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' ');
33
+
34
+ return (
35
+ <div ref={ref} className={classes}>
36
+ {children}
37
+ </div>
38
+ );
39
+ }
40
+ );