@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.
- package/fragments.json +1 -1
- package/package.json +9 -4
- 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 +66 -41
- 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/index.tsx +8 -9
- 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.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +51 -25
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
- package/src/components/CodeBlock/index.tsx +385 -0
- package/src/components/ColorChip/ColorChip.module.scss +165 -0
- package/src/components/ColorChip/index.tsx +157 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
- package/src/components/ColorPicker/index.tsx +107 -0
- package/src/components/Dialog/Dialog.fragment.tsx +9 -0
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
- 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 +17 -17
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +209 -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.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/Menu/Menu.fragment.tsx +9 -0
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Popover/Popover.fragment.tsx +9 -0
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- 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.module.scss +3 -3
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +9 -0
- package/src/components/Select/index.tsx +6 -7
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +783 -0
- package/src/components/Sidebar/Sidebar.module.scss +586 -0
- package/src/components/Sidebar/index.tsx +1013 -0
- package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -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 +7 -0
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +9 -0
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- 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/Toast/Toast.fragment.tsx +5 -5
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- 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 +241 -3
- 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/_index.scss +3 -0
- package/src/tokens/_mixins.scss +54 -1
- package/src/tokens/_variables.scss +429 -64
- 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
|
+
);
|