@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,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:
|
|
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)
|
|
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:
|
|
54
|
-
height:
|
|
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:
|
|
60
|
-
height:
|
|
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
|
|
74
|
-
font-size:
|
|
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:
|
|
84
|
+
border-radius: var(--fui-space-0-5, $fui-space-0-5);
|
|
79
85
|
|
|
80
86
|
.sm & {
|
|
81
|
-
font-size:
|
|
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
|
|
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=
|
|
60
|
+
aria-label={`Remove ${typeof children === 'string' ? children : 'badge'}`}
|
|
47
61
|
className={styles.remove}
|
|
48
62
|
>
|
|
49
63
|
×
|