@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,363 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import styles from './Header.module.scss';
5
+ import { useSidebar } from '../Sidebar';
6
+ // Import globals to ensure CSS variables are defined
7
+ import '../../styles/globals.scss';
8
+
9
+ // ============================================
10
+ // Types
11
+ // ============================================
12
+
13
+ export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
14
+ children: React.ReactNode;
15
+ /** Header height (default: '56px') */
16
+ height?: string;
17
+ /** Position behavior */
18
+ position?: 'static' | 'fixed' | 'sticky';
19
+ }
20
+
21
+ export interface HeaderBrandProps {
22
+ children: React.ReactNode;
23
+ /** Link destination */
24
+ href?: string;
25
+ /** Additional class name */
26
+ className?: string;
27
+ }
28
+
29
+ export interface HeaderNavProps {
30
+ children: React.ReactNode;
31
+ /** Accessible label for navigation */
32
+ 'aria-label'?: string;
33
+ /** Additional class name */
34
+ className?: string;
35
+ }
36
+
37
+ export interface HeaderNavItemProps {
38
+ children: React.ReactNode;
39
+ /** Whether this item is active/current */
40
+ active?: boolean;
41
+ /** Link destination */
42
+ href?: string;
43
+ /** Render as child element (polymorphic) */
44
+ asChild?: boolean;
45
+ /** Click handler */
46
+ onClick?: () => void;
47
+ /** Additional class name */
48
+ className?: string;
49
+ }
50
+
51
+ export interface HeaderSearchProps {
52
+ children: React.ReactNode;
53
+ /** Whether search expands on mobile */
54
+ expandable?: boolean;
55
+ /** Additional class name */
56
+ className?: string;
57
+ }
58
+
59
+ export interface HeaderActionsProps {
60
+ children: React.ReactNode;
61
+ /** Additional class name */
62
+ className?: string;
63
+ }
64
+
65
+ export interface HeaderTriggerProps {
66
+ /** Custom trigger content */
67
+ children?: React.ReactNode;
68
+ /** Accessible label */
69
+ 'aria-label'?: string;
70
+ /** Additional class name */
71
+ className?: string;
72
+ }
73
+
74
+ // ============================================
75
+ // Icons
76
+ // ============================================
77
+
78
+ function MenuIcon() {
79
+ return (
80
+ <svg
81
+ xmlns="http://www.w3.org/2000/svg"
82
+ width="24"
83
+ height="24"
84
+ viewBox="0 0 256 256"
85
+ fill="currentColor"
86
+ aria-hidden="true"
87
+ >
88
+ <path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z" />
89
+ </svg>
90
+ );
91
+ }
92
+
93
+ function CloseIcon() {
94
+ return (
95
+ <svg
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ width="24"
98
+ height="24"
99
+ viewBox="0 0 256 256"
100
+ fill="currentColor"
101
+ aria-hidden="true"
102
+ >
103
+ <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" />
104
+ </svg>
105
+ );
106
+ }
107
+
108
+ // ============================================
109
+ // Hooks
110
+ // ============================================
111
+
112
+ function useIsMobile() {
113
+ const [isMobile, setIsMobile] = React.useState(false);
114
+
115
+ React.useEffect(() => {
116
+ if (typeof window === 'undefined') return;
117
+
118
+ const mq = window.matchMedia('(max-width: 767px)');
119
+ setIsMobile(mq.matches);
120
+
121
+ const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
122
+ mq.addEventListener('change', handler);
123
+ return () => mq.removeEventListener('change', handler);
124
+ }, []);
125
+
126
+ return isMobile;
127
+ }
128
+
129
+ // ============================================
130
+ // Components
131
+ // ============================================
132
+
133
+ /**
134
+ * Header - Root header element
135
+ */
136
+ function HeaderRoot({
137
+ children,
138
+ height = '56px',
139
+ position = 'static',
140
+ className,
141
+ style: styleProp,
142
+ ...htmlProps
143
+ }: HeaderProps) {
144
+ const classes = [
145
+ styles.header,
146
+ position === 'fixed' && styles.fixed,
147
+ position === 'sticky' && styles.sticky,
148
+ className,
149
+ ].filter(Boolean).join(' ');
150
+
151
+ const style: React.CSSProperties = {
152
+ '--header-height': height,
153
+ ...styleProp,
154
+ } as React.CSSProperties;
155
+
156
+ return (
157
+ <header {...htmlProps} className={classes} style={style} data-position={position}>
158
+ <div className={styles.container}>
159
+ {children}
160
+ </div>
161
+ </header>
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Header.Brand - Logo/brand slot
167
+ */
168
+ function HeaderBrand({ children, href, className }: HeaderBrandProps) {
169
+ const classes = [styles.brand, className].filter(Boolean).join(' ');
170
+
171
+ if (href) {
172
+ return (
173
+ <a href={href} className={classes}>
174
+ {children}
175
+ </a>
176
+ );
177
+ }
178
+
179
+ return <div className={classes}>{children}</div>;
180
+ }
181
+
182
+ /**
183
+ * Header.Nav - Navigation container (hidden on mobile)
184
+ */
185
+ function HeaderNav({
186
+ children,
187
+ 'aria-label': ariaLabel = 'Main navigation',
188
+ className,
189
+ }: HeaderNavProps) {
190
+ const classes = [styles.nav, className].filter(Boolean).join(' ');
191
+
192
+ return (
193
+ <nav className={classes} aria-label={ariaLabel}>
194
+ <ul className={styles.navList}>
195
+ {children}
196
+ </ul>
197
+ </nav>
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Header.NavItem - Navigation link
203
+ */
204
+ function HeaderNavItem({
205
+ children,
206
+ active = false,
207
+ href,
208
+ asChild = false,
209
+ onClick,
210
+ className,
211
+ }: HeaderNavItemProps) {
212
+ const classes = [
213
+ styles.navItem,
214
+ active && styles.navItemActive,
215
+ className,
216
+ ].filter(Boolean).join(' ');
217
+
218
+ const itemProps = {
219
+ className: classes,
220
+ onClick,
221
+ 'aria-current': active ? 'page' as const : undefined,
222
+ };
223
+
224
+ if (asChild && React.isValidElement(children)) {
225
+ return (
226
+ <li>
227
+ {React.cloneElement(children, {
228
+ ...itemProps,
229
+ className: [classes, (children.props as { className?: string }).className].filter(Boolean).join(' '),
230
+ } as React.HTMLAttributes<HTMLElement>)}
231
+ </li>
232
+ );
233
+ }
234
+
235
+ if (href) {
236
+ return (
237
+ <li>
238
+ <a {...itemProps} href={href}>
239
+ {children}
240
+ </a>
241
+ </li>
242
+ );
243
+ }
244
+
245
+ return (
246
+ <li>
247
+ <button {...itemProps} type="button">
248
+ {children}
249
+ </button>
250
+ </li>
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Header.Search - Search input slot (hidden on mobile unless expandable)
256
+ */
257
+ function HeaderSearch({
258
+ children,
259
+ expandable = false,
260
+ className,
261
+ }: HeaderSearchProps) {
262
+ const classes = [
263
+ styles.search,
264
+ expandable && styles.searchExpandable,
265
+ className,
266
+ ].filter(Boolean).join(' ');
267
+
268
+ return <div className={classes}>{children}</div>;
269
+ }
270
+
271
+ /**
272
+ * Header.Actions - Right-side actions container
273
+ */
274
+ function HeaderActions({ children, className }: HeaderActionsProps) {
275
+ const classes = [styles.actions, className].filter(Boolean).join(' ');
276
+ return <div className={classes}>{children}</div>;
277
+ }
278
+
279
+ /**
280
+ * Header.Trigger - Mobile menu trigger (integrates with SidebarProvider)
281
+ */
282
+ function HeaderTrigger({
283
+ children,
284
+ 'aria-label': ariaLabel = 'Toggle navigation',
285
+ className,
286
+ }: HeaderTriggerProps) {
287
+ const isMobile = useIsMobile();
288
+ const { open, setOpen } = useSidebar();
289
+
290
+ // Only render on mobile
291
+ if (!isMobile) {
292
+ return null;
293
+ }
294
+
295
+ const classes = [styles.trigger, className].filter(Boolean).join(' ');
296
+
297
+ return (
298
+ <button
299
+ type="button"
300
+ className={classes}
301
+ onClick={() => setOpen(!open)}
302
+ aria-label={ariaLabel}
303
+ aria-expanded={open}
304
+ >
305
+ {children || (open ? <CloseIcon /> : <MenuIcon />)}
306
+ </button>
307
+ );
308
+ }
309
+
310
+ /**
311
+ * Header.Spacer - Flexible spacer to push items apart
312
+ */
313
+ function HeaderSpacer({ className }: { className?: string }) {
314
+ const classes = [styles.spacer, className].filter(Boolean).join(' ');
315
+ return <div className={classes} />;
316
+ }
317
+
318
+ /**
319
+ * Header.SkipLink - Skip to main content link (accessibility)
320
+ */
321
+ function HeaderSkipLink({
322
+ children = 'Skip to main content',
323
+ href = '#main-content',
324
+ className,
325
+ }: {
326
+ children?: React.ReactNode;
327
+ href?: string;
328
+ className?: string;
329
+ }) {
330
+ const classes = [styles.skipLink, className].filter(Boolean).join(' ');
331
+ return (
332
+ <a href={href} className={classes}>
333
+ {children}
334
+ </a>
335
+ );
336
+ }
337
+
338
+ // ============================================
339
+ // Export compound component
340
+ // ============================================
341
+
342
+ export const Header = Object.assign(HeaderRoot, {
343
+ Brand: HeaderBrand,
344
+ Nav: HeaderNav,
345
+ NavItem: HeaderNavItem,
346
+ Search: HeaderSearch,
347
+ Actions: HeaderActions,
348
+ Trigger: HeaderTrigger,
349
+ Spacer: HeaderSpacer,
350
+ SkipLink: HeaderSkipLink,
351
+ });
352
+
353
+ export {
354
+ HeaderRoot,
355
+ HeaderBrand,
356
+ HeaderNav,
357
+ HeaderNavItem,
358
+ HeaderSearch,
359
+ HeaderActions,
360
+ HeaderTrigger,
361
+ HeaderSpacer,
362
+ HeaderSkipLink,
363
+ };
@@ -0,0 +1,138 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Icon } from './index.js';
4
+ import { Heart, Star, Check, Warning, Info } from '@phosphor-icons/react';
5
+
6
+ export default defineSegment({
7
+ component: Icon,
8
+
9
+ meta: {
10
+ name: 'Icon',
11
+ description: 'Wrapper for Phosphor icons with consistent sizing and semantic colors. Provides standardized icon rendering across the design system.',
12
+ category: 'data-display',
13
+ status: 'stable',
14
+ tags: ['icon', 'phosphor', 'visual', 'symbol', 'graphic'],
15
+ since: '0.1.0',
16
+ },
17
+
18
+ usage: {
19
+ when: [
20
+ 'Displaying UI icons alongside text or in buttons',
21
+ 'Indicating status or state visually',
22
+ 'Adding visual hierarchy to feature lists',
23
+ 'Decorating cards or stats with relevant symbols',
24
+ ],
25
+ whenNot: [
26
+ 'Large decorative illustrations (use Image or custom SVG)',
27
+ 'Logo display (use dedicated Logo component)',
28
+ 'Complex graphics with multiple colors',
29
+ 'Animated icons (use custom implementation)',
30
+ ],
31
+ guidelines: [
32
+ 'Use semantic color variants (success, error, warning) for status indication',
33
+ 'Pair icons with text labels for accessibility',
34
+ 'Match icon weight to surrounding text weight for visual consistency',
35
+ 'Use consistent sizes within the same context',
36
+ ],
37
+ accessibility: [
38
+ 'Icons are decorative by default (aria-hidden)',
39
+ 'Always pair with visible or visually-hidden text for meaning',
40
+ 'Do not rely on color alone to convey information',
41
+ 'Consider using VisuallyHidden for icon-only buttons',
42
+ ],
43
+ },
44
+
45
+ props: {
46
+ icon: {
47
+ type: 'component',
48
+ description: 'Phosphor icon component to render',
49
+ required: true,
50
+ },
51
+ size: {
52
+ type: 'enum',
53
+ description: 'Icon size',
54
+ values: ['xs', 'sm', 'md', 'lg', 'xl'],
55
+ default: 'md',
56
+ },
57
+ weight: {
58
+ type: 'enum',
59
+ description: 'Icon stroke weight/style',
60
+ values: ['thin', 'light', 'regular', 'bold', 'fill', 'duotone'],
61
+ default: 'regular',
62
+ },
63
+ variant: {
64
+ type: 'enum',
65
+ description: 'Semantic color variant',
66
+ values: ['default', 'primary', 'secondary', 'tertiary', 'accent', 'success', 'warning', 'error'],
67
+ default: 'default',
68
+ },
69
+ },
70
+
71
+ relations: [
72
+ { component: 'Button', relationship: 'child', note: 'Use inside icon-only buttons with VisuallyHidden label' },
73
+ { component: 'VisuallyHidden', relationship: 'sibling', note: 'Pair with VisuallyHidden for accessible icon-only elements' },
74
+ { component: 'Badge', relationship: 'child', note: 'Can be used as badge icon prop' },
75
+ ],
76
+
77
+ contract: {
78
+ propsSummary: [
79
+ 'icon: PhosphorIcon - icon component (required)',
80
+ 'size: xs|sm|md|lg|xl - icon size',
81
+ 'weight: thin|light|regular|bold|fill|duotone - stroke style',
82
+ 'variant: default|primary|secondary|tertiary|accent|success|warning|error - color',
83
+ ],
84
+ scenarioTags: [
85
+ 'display.icon',
86
+ 'feedback.status',
87
+ 'decoration.visual',
88
+ ],
89
+ a11yRules: ['A11Y_ICON_LABEL', 'A11Y_COLOR_CONTRAST'],
90
+ },
91
+
92
+ variants: [
93
+ {
94
+ name: 'Default',
95
+ description: 'Basic icon with default styling',
96
+ render: () => <Icon icon={Heart} />,
97
+ },
98
+ {
99
+ name: 'Sizes',
100
+ description: 'Available size options',
101
+ render: () => (
102
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
103
+ <Icon icon={Star} size="xs" />
104
+ <Icon icon={Star} size="sm" />
105
+ <Icon icon={Star} size="md" />
106
+ <Icon icon={Star} size="lg" />
107
+ <Icon icon={Star} size="xl" />
108
+ </div>
109
+ ),
110
+ },
111
+ {
112
+ name: 'Semantic Colors',
113
+ description: 'Status and semantic color variants',
114
+ render: () => (
115
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
116
+ <Icon icon={Check} variant="success" />
117
+ <Icon icon={Warning} variant="warning" />
118
+ <Icon icon={Warning} variant="error" />
119
+ <Icon icon={Info} variant="accent" />
120
+ </div>
121
+ ),
122
+ },
123
+ {
124
+ name: 'Weights',
125
+ description: 'Icon weight/style options',
126
+ render: () => (
127
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
128
+ <Icon icon={Heart} weight="thin" />
129
+ <Icon icon={Heart} weight="light" />
130
+ <Icon icon={Heart} weight="regular" />
131
+ <Icon icon={Heart} weight="bold" />
132
+ <Icon icon={Heart} weight="fill" />
133
+ <Icon icon={Heart} weight="duotone" />
134
+ </div>
135
+ ),
136
+ },
137
+ ],
138
+ });
@@ -0,0 +1,38 @@
1
+ @use '../../tokens/variables' as *;
2
+
3
+ .icon {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ color: currentColor;
8
+ line-height: 1;
9
+ }
10
+
11
+ // Color variants (matching Badge/Alert pattern)
12
+ .primary {
13
+ color: var(--fui-text-primary, $fui-text-primary);
14
+ }
15
+
16
+ .secondary {
17
+ color: var(--fui-text-secondary, $fui-text-secondary);
18
+ }
19
+
20
+ .tertiary {
21
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
22
+ }
23
+
24
+ .accent {
25
+ color: var(--fui-color-accent, $fui-color-accent);
26
+ }
27
+
28
+ .success {
29
+ color: var(--fui-color-success, $fui-color-success);
30
+ }
31
+
32
+ .warning {
33
+ color: var(--fui-color-warning, $fui-color-warning);
34
+ }
35
+
36
+ .error {
37
+ color: var(--fui-color-danger, $fui-color-danger);
38
+ }
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+ import type { IconProps as PhosphorIconProps } from '@phosphor-icons/react';
3
+ import styles from './Icon.module.scss';
4
+ import '../../styles/globals.scss';
5
+
6
+ export interface IconProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'color'> {
7
+ /** The Phosphor icon component to render */
8
+ icon: React.ComponentType<PhosphorIconProps>;
9
+ /** Size of the icon */
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ /** Weight/style of the icon */
12
+ weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
13
+ /** Semantic color variant */
14
+ variant?: 'default' | 'primary' | 'secondary' | 'tertiary' | 'accent' | 'success' | 'warning' | 'error';
15
+ /** @deprecated Use variant instead */
16
+ color?: 'primary' | 'secondary' | 'tertiary' | 'accent' | 'success' | 'warning' | 'error';
17
+ }
18
+
19
+ const sizeMap: Record<NonNullable<IconProps['size']>, number> = {
20
+ xs: 12,
21
+ sm: 16,
22
+ md: 20,
23
+ lg: 24,
24
+ xl: 32,
25
+ };
26
+
27
+ export const Icon = React.forwardRef<HTMLSpanElement, IconProps>(
28
+ function Icon(
29
+ {
30
+ icon: IconComponent,
31
+ size = 'md',
32
+ weight = 'regular',
33
+ variant,
34
+ color,
35
+ className,
36
+ style,
37
+ ...htmlProps
38
+ },
39
+ ref
40
+ ) {
41
+ // Support deprecated color prop (variant takes precedence)
42
+ const colorVariant = variant || color;
43
+
44
+ const classes = [
45
+ styles.icon,
46
+ colorVariant && colorVariant !== 'default' && styles[colorVariant],
47
+ className,
48
+ ]
49
+ .filter(Boolean)
50
+ .join(' ');
51
+
52
+ return (
53
+ <span ref={ref} {...htmlProps} className={classes} style={style}>
54
+ <IconComponent size={sizeMap[size]} weight={weight} />
55
+ </span>
56
+ );
57
+ }
58
+ );