@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,398 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import styles from './AppShell.module.scss';
5
+ import {
6
+ SidebarProvider,
7
+ Sidebar,
8
+ useSidebar,
9
+ type SidebarCollapsible,
10
+ } from '../Sidebar';
11
+ // Import globals to ensure CSS variables are defined
12
+ import '../../styles/globals.scss';
13
+
14
+ // ============================================
15
+ // Types
16
+ // ============================================
17
+
18
+ export type AppShellLayout = 'stacked' | 'sidebar-inset' | 'inset';
19
+
20
+ export interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {
21
+ children: React.ReactNode;
22
+ /**
23
+ * Layout mode:
24
+ * - 'stacked': Header spans full width above sidebar (default)
25
+ * - 'sidebar-inset': Sidebar is full height, header sits next to it
26
+ */
27
+ layout?: AppShellLayout;
28
+ }
29
+
30
+ export interface AppShellHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
31
+ children: React.ReactNode;
32
+ /** Header height (default: '56px') */
33
+ height?: string;
34
+ }
35
+
36
+ export interface AppShellSidebarProps extends React.HTMLAttributes<HTMLDivElement> {
37
+ children: React.ReactNode;
38
+ /** Width of expanded sidebar (default: '240px') */
39
+ width?: string;
40
+ /** Width when collapsed (default: '64px') */
41
+ collapsedWidth?: string;
42
+ /** Collapse behavior */
43
+ collapsible?: SidebarCollapsible;
44
+ /** Sidebar position */
45
+ position?: 'left' | 'right';
46
+ /** Default collapsed state */
47
+ defaultCollapsed?: boolean;
48
+ }
49
+
50
+ export interface AppShellMainProps extends React.HTMLAttributes<HTMLElement> {
51
+ children: React.ReactNode;
52
+ /** Content padding */
53
+ padding?: 'none' | 'sm' | 'md' | 'lg';
54
+ }
55
+
56
+ export interface AppShellAsideProps extends React.HTMLAttributes<HTMLElement> {
57
+ children: React.ReactNode;
58
+ /** Aside width (default: '280px') */
59
+ width?: string;
60
+ /** Control visibility */
61
+ visible?: boolean;
62
+ }
63
+
64
+ // ============================================
65
+ // Context
66
+ // ============================================
67
+
68
+ interface AppShellContextValue {
69
+ layout: AppShellLayout;
70
+ headerHeight: string;
71
+ sidebarWidth: string;
72
+ sidebarCollapsedWidth: string;
73
+ asideWidth: string;
74
+ asideVisible: boolean;
75
+ }
76
+
77
+ const AppShellContext = React.createContext<AppShellContextValue>({
78
+ layout: 'stacked',
79
+ headerHeight: '56px',
80
+ sidebarWidth: '240px',
81
+ sidebarCollapsedWidth: '64px',
82
+ asideWidth: '280px',
83
+ asideVisible: false,
84
+ });
85
+
86
+ function useAppShell() {
87
+ return React.useContext(AppShellContext);
88
+ }
89
+
90
+ // ============================================
91
+ // Hooks
92
+ // ============================================
93
+
94
+ function useIsMobile() {
95
+ const [isMobile, setIsMobile] = React.useState(false);
96
+
97
+ React.useEffect(() => {
98
+ if (typeof window === 'undefined') return;
99
+
100
+ const mq = window.matchMedia('(max-width: 767px)');
101
+ setIsMobile(mq.matches);
102
+
103
+ const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
104
+ mq.addEventListener('change', handler);
105
+ return () => mq.removeEventListener('change', handler);
106
+ }, []);
107
+
108
+ return isMobile;
109
+ }
110
+
111
+ // ============================================
112
+ // Helper to extract config from children
113
+ // ============================================
114
+
115
+ interface ExtractedConfig {
116
+ headerHeight: string;
117
+ sidebarWidth: string;
118
+ sidebarCollapsedWidth: string;
119
+ sidebarCollapsible: SidebarCollapsible;
120
+ sidebarDefaultCollapsed: boolean;
121
+ asideWidth: string;
122
+ asideVisible: boolean;
123
+ }
124
+
125
+ function extractConfigFromChildren(children: React.ReactNode): ExtractedConfig {
126
+ const config: ExtractedConfig = {
127
+ headerHeight: '56px',
128
+ sidebarWidth: '240px',
129
+ sidebarCollapsedWidth: '64px',
130
+ sidebarCollapsible: 'icon',
131
+ sidebarDefaultCollapsed: false,
132
+ asideWidth: '280px',
133
+ asideVisible: false,
134
+ };
135
+
136
+ React.Children.forEach(children, child => {
137
+ if (!React.isValidElement(child)) return;
138
+
139
+ if (child.type === AppShellHeader) {
140
+ const props = child.props as AppShellHeaderProps;
141
+ if (props.height) config.headerHeight = props.height;
142
+ }
143
+
144
+ if (child.type === AppShellSidebar) {
145
+ const props = child.props as AppShellSidebarProps;
146
+ if (props.width) config.sidebarWidth = props.width;
147
+ if (props.collapsedWidth) config.sidebarCollapsedWidth = props.collapsedWidth;
148
+ if (props.collapsible) config.sidebarCollapsible = props.collapsible;
149
+ if (props.defaultCollapsed !== undefined) config.sidebarDefaultCollapsed = props.defaultCollapsed;
150
+ }
151
+
152
+ if (child.type === AppShellAside) {
153
+ const props = child.props as AppShellAsideProps;
154
+ if (props.width) config.asideWidth = props.width;
155
+ if (props.visible !== false) config.asideVisible = true;
156
+ }
157
+ });
158
+
159
+ return config;
160
+ }
161
+
162
+ // ============================================
163
+ // Internal component to apply CSS variables
164
+ // ============================================
165
+
166
+ function AppShellInner({
167
+ children,
168
+ className,
169
+ layout,
170
+ ...htmlProps
171
+ }: {
172
+ children: React.ReactNode;
173
+ className?: string;
174
+ layout: AppShellLayout;
175
+ } & React.HTMLAttributes<HTMLDivElement>) {
176
+ const appShell = useAppShell();
177
+ const { collapsed, isMobile, collapsible } = useSidebar();
178
+
179
+ const classes = [
180
+ styles.root,
181
+ (layout === 'sidebar-inset' || layout === 'inset') && styles.sidebarInset,
182
+ layout === 'inset' && styles.insetLayout,
183
+ className,
184
+ ].filter(Boolean).join(' ');
185
+
186
+ // Calculate actual sidebar width based on state
187
+ const actualSidebarWidth = isMobile
188
+ ? '0px'
189
+ : (collapsible === 'icon' && collapsed)
190
+ ? appShell.sidebarCollapsedWidth
191
+ : (collapsible === 'offcanvas' && collapsed)
192
+ ? '0px'
193
+ : appShell.sidebarWidth;
194
+
195
+ const style: React.CSSProperties = {
196
+ '--appshell-header-height': appShell.headerHeight,
197
+ '--appshell-sidebar-width': actualSidebarWidth,
198
+ '--appshell-sidebar-expanded-width': appShell.sidebarWidth,
199
+ '--appshell-sidebar-collapsed-width': appShell.sidebarCollapsedWidth,
200
+ '--appshell-aside-width': appShell.asideVisible ? appShell.asideWidth : '0px',
201
+ } as React.CSSProperties;
202
+
203
+ return (
204
+ <div {...htmlProps} className={classes} style={style} data-layout={layout} data-mobile={isMobile || undefined}>
205
+ {children}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ // ============================================
211
+ // Components
212
+ // ============================================
213
+
214
+ /**
215
+ * AppShell - Root layout wrapper
216
+ * Automatically wraps children with SidebarProvider
217
+ */
218
+ function AppShellRoot({
219
+ children,
220
+ layout = 'stacked',
221
+ className,
222
+ ...htmlProps
223
+ }: AppShellProps) {
224
+ // Extract config from children using useMemo to avoid re-renders
225
+ const config = React.useMemo(() => extractConfigFromChildren(children), [children]);
226
+
227
+ const contextValue: AppShellContextValue = {
228
+ layout,
229
+ headerHeight: config.headerHeight,
230
+ sidebarWidth: config.sidebarWidth,
231
+ sidebarCollapsedWidth: config.sidebarCollapsedWidth,
232
+ asideWidth: config.asideWidth,
233
+ asideVisible: config.asideVisible,
234
+ };
235
+
236
+ return (
237
+ <AppShellContext.Provider value={contextValue}>
238
+ <SidebarProvider
239
+ width={config.sidebarWidth}
240
+ collapsedWidth={config.sidebarCollapsedWidth}
241
+ collapsible={config.sidebarCollapsible}
242
+ defaultCollapsed={config.sidebarDefaultCollapsed}
243
+ >
244
+ <AppShellInner className={className} layout={layout} {...htmlProps}>
245
+ {children}
246
+ </AppShellInner>
247
+ </SidebarProvider>
248
+ </AppShellContext.Provider>
249
+ );
250
+ }
251
+
252
+ /**
253
+ * AppShell.Header - Fixed header slot
254
+ */
255
+ function AppShellHeader({
256
+ children,
257
+ height = '56px',
258
+ className,
259
+ style: styleProp,
260
+ ...htmlProps
261
+ }: AppShellHeaderProps) {
262
+ const { layout } = useAppShell();
263
+
264
+ const classes = [
265
+ styles.header,
266
+ (layout === 'sidebar-inset' || layout === 'inset') && styles.headerInset,
267
+ layout === 'inset' && styles.headerInsetRounded,
268
+ className,
269
+ ].filter(Boolean).join(' ');
270
+
271
+ const style: React.CSSProperties = {
272
+ '--header-height': height,
273
+ ...styleProp,
274
+ } as React.CSSProperties;
275
+
276
+ return (
277
+ <div {...htmlProps} className={classes} style={style}>
278
+ {children}
279
+ </div>
280
+ );
281
+ }
282
+
283
+ /**
284
+ * AppShell.Sidebar - Sidebar slot (delegates to Sidebar component)
285
+ */
286
+ function AppShellSidebar({
287
+ children,
288
+ width = '240px',
289
+ collapsedWidth = '64px',
290
+ collapsible = 'icon',
291
+ position = 'left',
292
+ defaultCollapsed = false,
293
+ className,
294
+ ...htmlProps
295
+ }: AppShellSidebarProps) {
296
+ const isMobile = useIsMobile();
297
+ const { layout } = useAppShell();
298
+
299
+ const classes = [
300
+ styles.sidebar,
301
+ (layout === 'sidebar-inset' || layout === 'inset') && styles.sidebarFullHeight,
302
+ layout === 'inset' && styles.sidebarInsetRounded,
303
+ className,
304
+ ].filter(Boolean).join(' ');
305
+
306
+ return (
307
+ <div {...htmlProps} className={classes}>
308
+ <Sidebar
309
+ width={width}
310
+ collapsedWidth={collapsedWidth}
311
+ position={position}
312
+ collapsible={collapsible}
313
+ defaultCollapsed={defaultCollapsed}
314
+ >
315
+ {children}
316
+ </Sidebar>
317
+ {isMobile && <Sidebar.Overlay />}
318
+ </div>
319
+ );
320
+ }
321
+
322
+ /**
323
+ * AppShell.Main - Scrollable main content area
324
+ */
325
+ function AppShellMain({
326
+ children,
327
+ padding = 'md',
328
+ className,
329
+ id = 'main-content',
330
+ ...htmlProps
331
+ }: AppShellMainProps) {
332
+ const { layout } = useAppShell();
333
+
334
+ const classes = [
335
+ styles.main,
336
+ padding !== 'none' && styles[`padding${padding.charAt(0).toUpperCase() + padding.slice(1)}`],
337
+ layout === 'inset' && styles.mainInset,
338
+ className,
339
+ ].filter(Boolean).join(' ');
340
+
341
+ return (
342
+ <main {...htmlProps} className={classes} id={id}>
343
+ {children}
344
+ </main>
345
+ );
346
+ }
347
+
348
+ /**
349
+ * AppShell.Aside - Optional right sidebar panel
350
+ */
351
+ function AppShellAside({
352
+ children,
353
+ width = '280px',
354
+ visible = true,
355
+ className,
356
+ style: styleProp,
357
+ ...htmlProps
358
+ }: AppShellAsideProps) {
359
+ const isMobile = useIsMobile();
360
+
361
+ // Hide aside on mobile
362
+ if (isMobile || !visible) {
363
+ return null;
364
+ }
365
+
366
+ const classes = [styles.aside, className].filter(Boolean).join(' ');
367
+
368
+ const style: React.CSSProperties = {
369
+ '--aside-width': width,
370
+ ...styleProp,
371
+ } as React.CSSProperties;
372
+
373
+ return (
374
+ <aside {...htmlProps} className={classes} style={style}>
375
+ {children}
376
+ </aside>
377
+ );
378
+ }
379
+
380
+ // ============================================
381
+ // Export compound component
382
+ // ============================================
383
+
384
+ export const AppShell = Object.assign(AppShellRoot, {
385
+ Header: AppShellHeader,
386
+ Sidebar: AppShellSidebar,
387
+ Main: AppShellMain,
388
+ Aside: AppShellAside,
389
+ });
390
+
391
+ export {
392
+ AppShellRoot,
393
+ AppShellHeader,
394
+ AppShellSidebar,
395
+ AppShellMain,
396
+ AppShellAside,
397
+ useAppShell,
398
+ };
@@ -9,7 +9,7 @@ import '../../styles/globals.scss';
9
9
 
10
10
  export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
11
 
12
- export interface AvatarProps {
12
+ export interface AvatarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'> {
13
13
  /** Image source URL */
14
14
  src?: string;
15
15
  /** Alt text for the image */
@@ -24,19 +24,15 @@ export interface AvatarProps {
24
24
  shape?: 'circle' | 'square';
25
25
  /** Custom background color for fallback */
26
26
  color?: string;
27
- /** Additional class name */
28
- className?: string;
29
27
  }
30
28
 
31
- export interface AvatarGroupProps {
29
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
32
30
  /** Maximum number of avatars to display */
33
31
  max?: number;
34
32
  /** Size for all avatars in the group */
35
33
  size?: AvatarSize;
36
34
  /** Children (Avatar components) */
37
35
  children: React.ReactNode;
38
- /** Additional class name */
39
- className?: string;
40
36
  }
41
37
 
42
38
  // ============================================
@@ -76,6 +72,8 @@ const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
76
72
  shape = 'circle',
77
73
  color,
78
74
  className,
75
+ style: styleProp,
76
+ ...htmlProps
79
77
  },
80
78
  ref
81
79
  ) {
@@ -97,13 +95,13 @@ const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
97
95
  className,
98
96
  ].filter(Boolean).join(' ');
99
97
 
100
- const style: React.CSSProperties = {};
98
+ const style: React.CSSProperties = { ...styleProp };
101
99
  if (showFallback && fallbackColor) {
102
100
  style.backgroundColor = fallbackColor;
103
101
  }
104
102
 
105
103
  return (
106
- <div ref={ref} className={avatarClasses} style={style}>
104
+ <div ref={ref} {...htmlProps} className={avatarClasses} style={style}>
107
105
  {!showFallback && (
108
106
  <img
109
107
  src={src}
@@ -140,6 +138,7 @@ function AvatarGroup({
140
138
  size = 'md',
141
139
  children,
142
140
  className,
141
+ ...htmlProps
143
142
  }: AvatarGroupProps) {
144
143
  const childArray = React.Children.toArray(children);
145
144
  const displayCount = max && max < childArray.length ? max : childArray.length;
@@ -148,7 +147,7 @@ function AvatarGroup({
148
147
  const groupClasses = [styles.group, className].filter(Boolean).join(' ');
149
148
 
150
149
  return (
151
- <div className={groupClasses}>
150
+ <div {...htmlProps} className={groupClasses}>
152
151
  {childArray.slice(0, displayCount).map((child, index) => {
153
152
  if (React.isValidElement<AvatarProps>(child)) {
154
153
  return React.cloneElement(child, {
@@ -14,12 +14,12 @@
14
14
 
15
15
  // Sizes
16
16
  .sm {
17
- padding: 2px var(--fui-space-2, $fui-space-2);
17
+ padding: var(--fui-space-0-5, $fui-space-0-5) var(--fui-space-2, $fui-space-2);
18
18
  font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
19
19
  }
20
20
 
21
21
  .md {
22
- padding: var(--fui-space-1, $fui-space-1) 10px;
22
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-1, $fui-space-1);
23
23
  font-size: var(--fui-font-size-xs, $fui-font-size-xs);
24
24
  }
25
25
 
@@ -49,15 +49,21 @@
49
49
  color: var(--fui-color-info, $fui-color-info);
50
50
  }
51
51
 
52
+ .outline {
53
+ background-color: transparent;
54
+ color: var(--fui-text-primary, $fui-text-primary);
55
+ border: 1px solid var(--fui-border-strong, $fui-border-strong);
56
+ }
57
+
52
58
  .dot {
53
- width: 6px;
54
- height: 6px;
59
+ width: $fui-badge-dot-md;
60
+ height: $fui-badge-dot-md;
55
61
  border-radius: 50%;
56
62
  background-color: currentColor;
57
63
 
58
64
  .sm & {
59
- width: 5px;
60
- height: 5px;
65
+ width: $fui-badge-dot-sm;
66
+ height: $fui-badge-dot-sm;
61
67
  }
62
68
  }
63
69
 
@@ -70,15 +76,15 @@
70
76
  @include button-reset;
71
77
  @include interactive-base;
72
78
 
73
- padding: 0 2px;
74
- font-size: 14px;
79
+ padding: 0 var(--fui-space-0-5, $fui-space-0-5);
80
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
75
81
  color: inherit;
76
82
  opacity: 0.6;
77
83
  line-height: 1;
78
- border-radius: 2px;
84
+ border-radius: var(--fui-space-0-5, $fui-space-0-5);
79
85
 
80
86
  .sm & {
81
- font-size: 12px;
87
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
82
88
  }
83
89
 
84
90
  &:hover {
@@ -4,14 +4,13 @@ import styles from './Badge.module.scss';
4
4
  // Import globals to ensure CSS variables are defined
5
5
  import '../../styles/globals.scss';
6
6
 
7
- export interface BadgeProps {
7
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
8
8
  children: React.ReactNode;
9
- variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
9
+ variant?: 'default' | 'success' | 'warning' | 'error' | 'info' | 'outline';
10
10
  size?: 'sm' | 'md';
11
11
  dot?: boolean;
12
12
  icon?: React.ReactNode;
13
13
  onRemove?: () => void;
14
- className?: string;
15
14
  }
16
15
 
17
16
  export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
@@ -24,6 +23,8 @@ export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
24
23
  icon,
25
24
  onRemove,
26
25
  className,
26
+ 'aria-label': ariaLabel,
27
+ ...htmlProps
27
28
  },
28
29
  ref
29
30
  ) {
@@ -31,19 +32,32 @@ export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
31
32
  .filter(Boolean)
32
33
  .join(' ');
33
34
 
35
+ // For status badges, include the status in the aria-label if not provided
36
+ const effectiveAriaLabel = ariaLabel || (
37
+ variant !== 'default' && variant !== 'outline'
38
+ ? `${variant}: ${typeof children === 'string' ? children : ''}`
39
+ : undefined
40
+ );
41
+
34
42
  return (
35
- <span ref={ref} className={classes}>
43
+ <span
44
+ ref={ref}
45
+ {...htmlProps}
46
+ className={classes}
47
+ role={effectiveAriaLabel ? 'status' : undefined}
48
+ aria-label={effectiveAriaLabel}
49
+ >
36
50
  {dot && <span className={styles.dot} aria-hidden="true" />}
37
51
  {icon && (
38
52
  <span className={styles.icon} aria-hidden="true">
39
53
  {icon}
40
54
  </span>
41
55
  )}
42
- {children}
56
+ <span>{children}</span>
43
57
  {onRemove && (
44
58
  <BaseButton
45
59
  onClick={onRemove}
46
- aria-label="Remove"
60
+ aria-label={`Remove ${typeof children === 'string' ? children : 'badge'}`}
47
61
  className={styles.remove}
48
62
  >
49
63
  &times;