@fragments-sdk/ui 0.8.1 → 0.8.2

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 (57) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/fragments.json +1 -1
  4. package/package.json +21 -2
  5. package/src/assets/fragments-logo.tsx +37 -0
  6. package/src/assets/fragments_logo.svg +1 -0
  7. package/src/assets/fragments_logo_text.svg +1 -0
  8. package/src/blocks/AccountSettings.block.ts +1 -1
  9. package/src/blocks/ActivityFeed.block.ts +7 -7
  10. package/src/blocks/ChatInterface.block.ts +36 -80
  11. package/src/blocks/DashboardLayout.block.ts +85 -66
  12. package/src/blocks/DashboardPage.block.ts +297 -0
  13. package/src/blocks/EmptyState.block.ts +5 -3
  14. package/src/blocks/FeatureGrid.block.ts +1 -1
  15. package/src/blocks/LoginForm.block.ts +21 -26
  16. package/src/blocks/PricingComparison.block.ts +1 -1
  17. package/src/blocks/ShoppingCart.block.ts +2 -2
  18. package/src/components/Input/Input.test.tsx +35 -0
  19. package/src/components/Input/index.tsx +47 -2
  20. package/src/components/Menu/Menu.module.scss +2 -0
  21. package/src/components/Table/Table.fragment.tsx +1 -1
  22. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +32 -0
  23. package/src/index.ts +3 -0
  24. package/src/tokens/_derive.scss +32 -8
  25. package/src/blocks/AIChat.block.ts +0 -266
  26. package/src/blocks/AppShell.block.ts +0 -175
  27. package/src/blocks/CTABanner.block.ts +0 -24
  28. package/src/blocks/CardGrid.block.ts +0 -22
  29. package/src/blocks/CodeExamples.block.ts +0 -66
  30. package/src/blocks/ConfirmDialog.block.ts +0 -19
  31. package/src/blocks/ConversationWithHistory.block.ts +0 -45
  32. package/src/blocks/DashboardNav.block.ts +0 -183
  33. package/src/blocks/ForgotPassword.block.ts +0 -26
  34. package/src/blocks/FormLayout.block.ts +0 -31
  35. package/src/blocks/InsetDashboardLayout.block.ts +0 -79
  36. package/src/blocks/MetricDashboard.block.ts +0 -38
  37. package/src/blocks/NewsletterSignup.block.ts +0 -26
  38. package/src/blocks/NotificationList.block.ts +0 -39
  39. package/src/blocks/NotificationPreferences.block.ts +0 -40
  40. package/src/blocks/OrderSummary.block.ts +0 -52
  41. package/src/blocks/ProfileEditForm.block.ts +0 -51
  42. package/src/blocks/SearchResults.block.ts +0 -39
  43. package/src/blocks/SettingsPage.block.ts +0 -58
  44. package/src/blocks/StreamingMessage.block.ts +0 -24
  45. package/src/blocks/TestimonialCard.block.ts +0 -27
  46. package/src/blocks/UserProfileCard.block.ts +0 -29
  47. package/src/recipes/AIChat.recipe.ts +0 -266
  48. package/src/recipes/AppShell.recipe.ts +0 -175
  49. package/src/recipes/CardGrid.recipe.ts +0 -22
  50. package/src/recipes/ChatInterface.recipe.ts +0 -87
  51. package/src/recipes/CodeExamples.recipe.ts +0 -66
  52. package/src/recipes/ConfirmDialog.recipe.ts +0 -19
  53. package/src/recipes/DashboardLayout.recipe.ts +0 -73
  54. package/src/recipes/DashboardNav.recipe.ts +0 -183
  55. package/src/recipes/FormLayout.recipe.ts +0 -31
  56. package/src/recipes/LoginForm.recipe.ts +0 -33
  57. package/src/recipes/SettingsPage.recipe.ts +0 -58
@@ -15,7 +15,7 @@ export interface InputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, '
15
15
  error?: boolean;
16
16
  label?: string;
17
17
  helperText?: string;
18
- /** Keyboard shortcut hint displayed inside the input (e.g., "⌘K") */
18
+ /** Keyboard shortcut hint displayed inside the input (e.g., "⌘K"). Also registers a global keydown listener that focuses the input when the shortcut is pressed. */
19
19
  shortcut?: string;
20
20
  onChange?: (value: string) => void;
21
21
  onBlur?: () => void;
@@ -33,6 +33,20 @@ function mergeAriaIds(...ids: Array<string | undefined>): string | undefined {
33
33
  return merged.length > 0 ? merged : undefined;
34
34
  }
35
35
 
36
+ function parseShortcut(shortcut: string): { meta: boolean; shift: boolean; alt: boolean; key: string } | null {
37
+ let meta = false, shift = false, alt = false;
38
+ let remaining = shortcut;
39
+
40
+ if (remaining.includes('⌘')) { meta = true; remaining = remaining.replace('⌘', ''); }
41
+ if (remaining.includes('⇧')) { shift = true; remaining = remaining.replace('⇧', ''); }
42
+ if (remaining.includes('⌥')) { alt = true; remaining = remaining.replace('⌥', ''); }
43
+
44
+ remaining = remaining.trim();
45
+ if (!remaining) return null;
46
+
47
+ return { meta, shift, alt, key: remaining };
48
+ }
49
+
36
50
  const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
37
51
  function Input(
38
52
  {
@@ -66,6 +80,37 @@ const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
66
80
  const generatedId = React.useId();
67
81
  const helperId = helperText ? `input-helper-${generatedId}` : undefined;
68
82
 
83
+ const internalRef = React.useRef<HTMLInputElement>(null);
84
+ const mergedRef = React.useCallback(
85
+ (node: HTMLInputElement | null) => {
86
+ internalRef.current = node;
87
+ if (typeof ref === 'function') {
88
+ ref(node);
89
+ } else if (ref) {
90
+ (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
91
+ }
92
+ },
93
+ [ref]
94
+ );
95
+
96
+ // Register global keydown handler when shortcut is provided
97
+ React.useEffect(() => {
98
+ if (!shortcut) return;
99
+ const parsed = parseShortcut(shortcut);
100
+ if (!parsed) return;
101
+
102
+ const handler = (e: KeyboardEvent) => {
103
+ if (parsed.meta && !(e.metaKey || e.ctrlKey)) return;
104
+ if (parsed.shift && !e.shiftKey) return;
105
+ if (parsed.alt && !e.altKey) return;
106
+ if (e.key.toLowerCase() !== parsed.key.toLowerCase()) return;
107
+ e.preventDefault();
108
+ internalRef.current?.focus();
109
+ };
110
+ document.addEventListener('keydown', handler);
111
+ return () => document.removeEventListener('keydown', handler);
112
+ }, [shortcut]);
113
+
69
114
  const inputClasses = [
70
115
  styles.input,
71
116
  styles[size],
@@ -85,7 +130,7 @@ const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
85
130
 
86
131
  const inputElement = (
87
132
  <Field.Control
88
- ref={ref}
133
+ ref={mergedRef}
89
134
  type={type}
90
135
  value={value}
91
136
  defaultValue={defaultValue}
@@ -11,6 +11,8 @@
11
11
  .popup {
12
12
  @include surface-elevated;
13
13
 
14
+ list-style: none;
15
+ margin: 0;
14
16
  min-width: 12rem;
15
17
  padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
16
18
  box-shadow: var(--fui-shadow-md, $fui-shadow-md);
@@ -58,7 +58,7 @@ export default defineFragment({
58
58
  ],
59
59
  whenNot: [
60
60
  'Simple lists (use List component)',
61
- 'Card-based layouts (use CardGrid)',
61
+ 'Card-based layouts (use Grid with Cards)',
62
62
  'Heavily interactive data (consider DataGrid)',
63
63
  'Small screens (consider card or list view)',
64
64
  ],
@@ -194,31 +194,63 @@ export default defineFragment({
194
194
  {
195
195
  name: 'Default',
196
196
  description: 'Basic toggle group',
197
+ code: `<ToggleGroup value={value} onChange={setValue}>
198
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
199
+ <ToggleGroup.Item value="center">Center</ToggleGroup.Item>
200
+ <ToggleGroup.Item value="right">Right</ToggleGroup.Item>
201
+ </ToggleGroup>`,
197
202
  render: () => <DefaultExample />,
198
203
  },
199
204
  {
200
205
  name: 'Pills Variant',
201
206
  description: 'Pill-shaped toggle buttons',
207
+ code: `<ToggleGroup value={value} onChange={setValue} variant="pills">
208
+ <ToggleGroup.Item value="all">All</ToggleGroup.Item>
209
+ <ToggleGroup.Item value="active">Active</ToggleGroup.Item>
210
+ <ToggleGroup.Item value="completed">Completed</ToggleGroup.Item>
211
+ </ToggleGroup>`,
202
212
  render: () => <PillsExample />,
203
213
  },
204
214
  {
205
215
  name: 'Outline Variant',
206
216
  description: 'Outlined toggle buttons',
217
+ code: `<ToggleGroup value={value} onChange={setValue} variant="outline">
218
+ <ToggleGroup.Item value="day">Day</ToggleGroup.Item>
219
+ <ToggleGroup.Item value="week">Week</ToggleGroup.Item>
220
+ <ToggleGroup.Item value="month">Month</ToggleGroup.Item>
221
+ </ToggleGroup>`,
207
222
  render: () => <OutlineExample />,
208
223
  },
209
224
  {
210
225
  name: 'Sizes',
211
226
  description: 'Different size variants',
227
+ code: `<ToggleGroup value={value} onChange={setValue} size="sm">
228
+ <ToggleGroup.Item value="a">Small</ToggleGroup.Item>
229
+ <ToggleGroup.Item value="b">Size</ToggleGroup.Item>
230
+ </ToggleGroup>
231
+ <ToggleGroup value={value} onChange={setValue} size="md">
232
+ <ToggleGroup.Item value="a">Medium</ToggleGroup.Item>
233
+ <ToggleGroup.Item value="b">Size</ToggleGroup.Item>
234
+ </ToggleGroup>`,
212
235
  render: () => <SizesExample />,
213
236
  },
214
237
  {
215
238
  name: 'View Switcher',
216
239
  description: 'Common pattern for switching between views',
240
+ code: `<ToggleGroup value={view} onChange={setView} size="sm">
241
+ <ToggleGroup.Item value="grid"><GridIcon /></ToggleGroup.Item>
242
+ <ToggleGroup.Item value="list"><ListIcon /></ToggleGroup.Item>
243
+ </ToggleGroup>`,
217
244
  render: () => <ViewSwitcherExample />,
218
245
  },
219
246
  {
220
247
  name: 'With Disabled Item',
221
248
  description: 'Toggle group with a disabled option',
249
+ code: `<ToggleGroup value={value} onChange={setValue}>
250
+ <ToggleGroup.Item value="basic">Basic</ToggleGroup.Item>
251
+ <ToggleGroup.Item value="pro">Pro</ToggleGroup.Item>
252
+ <ToggleGroup.Item value="enterprise" disabled>Enterprise</ToggleGroup.Item>
253
+ </ToggleGroup>`,
222
254
  render: () => <DisabledItemExample />,
223
255
  },
224
256
  ],
package/src/index.ts CHANGED
@@ -521,6 +521,9 @@ export {
521
521
  type ChartLegendContentProps,
522
522
  } from './components/Chart';
523
523
 
524
+ // Assets
525
+ export { FragmentsLogo, type FragmentsLogoProps } from './assets/fragments-logo';
526
+
524
527
  // Accessibility Utilities
525
528
  export {
526
529
  useId,
@@ -46,30 +46,54 @@
46
46
  // --------------------------------------------
47
47
 
48
48
  /// Derive hover state for accent color
49
+ /// Handles extreme lightness values (near-black/near-white) by reversing
50
+ /// the adjustment direction to ensure a visible hover state.
49
51
  /// @param {Color} $base - Base accent color
50
52
  /// @param {Boolean} $is-dark - Whether in dark mode
51
53
  /// @return {Color} Hover state color
52
54
  @function derive-accent-hover($base, $is-dark: false) {
55
+ $l: color.channel($base, 'lightness', $space: hsl);
56
+
53
57
  @if $is-dark {
54
- // In dark mode, lighten for hover
55
- @return color.scale($base, $lightness: 5%);
58
+ @if $l > 85% {
59
+ // Very light accent on dark bg: darken for visible hover
60
+ @return color.scale($base, $lightness: -10%);
61
+ }
62
+ // Standard dark mode: lighten for hover
63
+ @return color.scale($base, $lightness: 8%);
56
64
  } @else {
57
- // In light mode, darken for hover
58
- @return color.scale($base, $lightness: -8%);
65
+ @if $l < 15% {
66
+ // Very dark accent on light bg: lighten for visible hover
67
+ @return color.scale($base, $lightness: 18%);
68
+ }
69
+ // Standard light mode: darken for hover
70
+ @return color.scale($base, $lightness: -10%);
59
71
  }
60
72
  }
61
73
 
62
74
  /// Derive active state for accent color
75
+ /// Handles extreme lightness values (near-black/near-white) by reversing
76
+ /// the adjustment direction to ensure a visible active/pressed state.
63
77
  /// @param {Color} $base - Base accent color
64
78
  /// @param {Boolean} $is-dark - Whether in dark mode
65
79
  /// @return {Color} Active state color
66
80
  @function derive-accent-active($base, $is-dark: false) {
81
+ $l: color.channel($base, 'lightness', $space: hsl);
82
+
67
83
  @if $is-dark {
68
- // In dark mode, lighten more for active
69
- @return color.scale($base, $lightness: 10%);
84
+ @if $l > 85% {
85
+ // Very light accent on dark bg: darken more for active
86
+ @return color.scale($base, $lightness: -16%);
87
+ }
88
+ // Standard dark mode: lighten more for active
89
+ @return color.scale($base, $lightness: 14%);
70
90
  } @else {
71
- // In light mode, darken more for active
72
- @return color.scale($base, $lightness: -15%);
91
+ @if $l < 15% {
92
+ // Very dark accent on light bg: lighten more for active
93
+ @return color.scale($base, $lightness: 28%);
94
+ }
95
+ // Standard light mode: darken more for active
96
+ @return color.scale($base, $lightness: -18%);
73
97
  }
74
98
  }
75
99
 
@@ -1,266 +0,0 @@
1
- import { defineBlock } from '@fragments/core';
2
-
3
- export default defineBlock({
4
- name: 'AI Chat',
5
- description: 'Complete AI chat interface with Message, ConversationList, Prompt, and ThinkingIndicator',
6
- category: 'ai',
7
- components: ['Message', 'ConversationList', 'ThinkingIndicator', 'Prompt', 'Stack'],
8
- tags: ['chat', 'ai', 'assistant', 'conversation', 'llm', 'chatbot'],
9
- code: `
10
- import { useState, useCallback } from 'react';
11
- import {
12
- Message,
13
- ConversationList,
14
- ThinkingIndicator,
15
- Prompt,
16
- Stack,
17
- } from '@fragments/ui';
18
-
19
- interface ChatMessage {
20
- id: string;
21
- role: 'user' | 'assistant' | 'system';
22
- content: string;
23
- timestamp: Date;
24
- status?: 'sending' | 'streaming' | 'complete' | 'error';
25
- }
26
-
27
- interface ThinkingStep {
28
- id: string;
29
- label: string;
30
- status: 'pending' | 'active' | 'complete' | 'error';
31
- }
32
-
33
- function AIChat() {
34
- const [messages, setMessages] = useState<ChatMessage[]>([
35
- {
36
- id: 'system-1',
37
- role: 'system',
38
- content: 'Conversation started. Model: Claude 3.5 Sonnet',
39
- timestamp: new Date(),
40
- status: 'complete',
41
- },
42
- ]);
43
- const [isThinking, setIsThinking] = useState(false);
44
- const [thinkingSteps, setThinkingSteps] = useState<ThinkingStep[]>([]);
45
- const [isLoadingHistory, setIsLoadingHistory] = useState(false);
46
-
47
- const handleSubmit = useCallback(async (value: string) => {
48
- // Add user message
49
- const userMessage: ChatMessage = {
50
- id: crypto.randomUUID(),
51
- role: 'user',
52
- content: value,
53
- timestamp: new Date(),
54
- status: 'complete',
55
- };
56
- setMessages((prev) => [...prev, userMessage]);
57
-
58
- // Start thinking state with steps
59
- setIsThinking(true);
60
- setThinkingSteps([
61
- { id: '1', label: 'Understanding your request', status: 'active' },
62
- { id: '2', label: 'Searching knowledge base', status: 'pending' },
63
- { id: '3', label: 'Generating response', status: 'pending' },
64
- ]);
65
-
66
- try {
67
- // Simulate step 1 completion
68
- await new Promise((resolve) => setTimeout(resolve, 800));
69
- setThinkingSteps((prev) =>
70
- prev.map((s) =>
71
- s.id === '1'
72
- ? { ...s, status: 'complete' }
73
- : s.id === '2'
74
- ? { ...s, status: 'active' }
75
- : s
76
- )
77
- );
78
-
79
- // Simulate step 2 completion
80
- await new Promise((resolve) => setTimeout(resolve, 600));
81
- setThinkingSteps((prev) =>
82
- prev.map((s) =>
83
- s.id === '2'
84
- ? { ...s, status: 'complete' }
85
- : s.id === '3'
86
- ? { ...s, status: 'active' }
87
- : s
88
- )
89
- );
90
-
91
- // Simulate step 3 (response generation)
92
- await new Promise((resolve) => setTimeout(resolve, 1000));
93
-
94
- // Add assistant response
95
- const assistantMessage: ChatMessage = {
96
- id: crypto.randomUUID(),
97
- role: 'assistant',
98
- content: generateMockResponse(value),
99
- timestamp: new Date(),
100
- status: 'complete',
101
- };
102
- setMessages((prev) => [...prev, assistantMessage]);
103
- } catch (error) {
104
- // Handle error
105
- const errorMessage: ChatMessage = {
106
- id: crypto.randomUUID(),
107
- role: 'assistant',
108
- content: 'Sorry, I encountered an error. Please try again.',
109
- timestamp: new Date(),
110
- status: 'error',
111
- };
112
- setMessages((prev) => [...prev, errorMessage]);
113
- } finally {
114
- setIsThinking(false);
115
- setThinkingSteps([]);
116
- }
117
- }, []);
118
-
119
- const handleLoadHistory = useCallback(async () => {
120
- if (isLoadingHistory) return;
121
-
122
- setIsLoadingHistory(true);
123
- await new Promise((resolve) => setTimeout(resolve, 1500));
124
-
125
- // Simulate loading older messages
126
- const olderMessages: ChatMessage[] = [
127
- {
128
- id: crypto.randomUUID(),
129
- role: 'user',
130
- content: 'What can you help me with?',
131
- timestamp: new Date(Date.now() - 86400000), // Yesterday
132
- status: 'complete',
133
- },
134
- {
135
- id: crypto.randomUUID(),
136
- role: 'assistant',
137
- content: 'I can help with coding, writing, analysis, and more!',
138
- timestamp: new Date(Date.now() - 86400000 + 1000),
139
- status: 'complete',
140
- },
141
- ];
142
-
143
- setMessages((prev) => [...olderMessages, ...prev]);
144
- setIsLoadingHistory(false);
145
- }, [isLoadingHistory]);
146
-
147
- const handleCopy = useCallback((content: string) => {
148
- navigator.clipboard.writeText(content);
149
- // You might want to show a toast here
150
- }, []);
151
-
152
- return (
153
- <Stack
154
- style={{
155
- height: '100vh',
156
- maxWidth: '800px',
157
- margin: '0 auto',
158
- background: 'var(--fui-bg-primary)',
159
- }}
160
- >
161
- <ConversationList
162
- autoScroll="smart"
163
- onScrollTop={handleLoadHistory}
164
- loadingHistory={isLoadingHistory}
165
- emptyState={
166
- <Stack align="center" justify="center" style={{ flex: 1, padding: '2rem' }}>
167
- <p style={{ color: 'var(--fui-text-secondary)' }}>
168
- Start a conversation with the AI assistant
169
- </p>
170
- </Stack>
171
- }
172
- >
173
- {messages.map((msg, index) => {
174
- // Add date separator if day changed
175
- const prevMsg = messages[index - 1];
176
- const showDateSeparator =
177
- !prevMsg ||
178
- new Date(msg.timestamp).toDateString() !==
179
- new Date(prevMsg.timestamp).toDateString();
180
-
181
- return (
182
- <React.Fragment key={msg.id}>
183
- {showDateSeparator && (
184
- <ConversationList.DateSeparator date={msg.timestamp} />
185
- )}
186
- <Message
187
- role={msg.role}
188
- status={msg.status}
189
- timestamp={msg.timestamp}
190
- actions={
191
- msg.role === 'assistant' && msg.status === 'complete' ? (
192
- <>
193
- <button
194
- onClick={() => handleCopy(msg.content)}
195
- style={{
196
- padding: '4px 8px',
197
- fontSize: '12px',
198
- background: 'var(--fui-bg-tertiary)',
199
- border: 'none',
200
- borderRadius: '4px',
201
- cursor: 'pointer',
202
- }}
203
- >
204
- Copy
205
- </button>
206
- </>
207
- ) : undefined
208
- }
209
- >
210
- <Message.Content>{msg.content}</Message.Content>
211
- {msg.timestamp && <Message.Timestamp />}
212
- </Message>
213
- </React.Fragment>
214
- );
215
- })}
216
-
217
- {isThinking && (
218
- <ThinkingIndicator
219
- variant="dots"
220
- label="Claude is thinking..."
221
- showElapsed
222
- steps={thinkingSteps}
223
- />
224
- )}
225
- </ConversationList>
226
-
227
- <div style={{ padding: '1rem', borderTop: '1px solid var(--fui-border)' }}>
228
- <Prompt
229
- onSubmit={handleSubmit}
230
- loading={isThinking}
231
- placeholder="Message Claude..."
232
- >
233
- <Prompt.Textarea />
234
- <Prompt.Toolbar>
235
- <Prompt.Actions>
236
- <Prompt.ActionButton aria-label="Attach file">
237
- +
238
- </Prompt.ActionButton>
239
- <Prompt.ModeButton active>Auto</Prompt.ModeButton>
240
- </Prompt.Actions>
241
- <Prompt.Info>
242
- <Prompt.Submit />
243
- </Prompt.Info>
244
- </Prompt.Toolbar>
245
- </Prompt>
246
- </div>
247
- </Stack>
248
- );
249
- }
250
-
251
- // Mock response generator
252
- function generateMockResponse(input: string): string {
253
- const responses = [
254
- "That's a great question! Let me help you with that.",
255
- "I understand what you're looking for. Here's what I can tell you...",
256
- "Based on my knowledge, I can provide some insights on this topic.",
257
- "Let me break this down for you step by step.",
258
- ];
259
- return responses[Math.floor(Math.random() * responses.length)] +
260
- " This is a simulated response. In a real implementation, this would be replaced with actual AI-generated content based on your query about: " +
261
- input.substring(0, 50) + (input.length > 50 ? '...' : '');
262
- }
263
-
264
- export default AIChat;
265
- `.trim(),
266
- });
@@ -1,175 +0,0 @@
1
- import { defineBlock } from '@fragments/core';
2
-
3
- export default defineBlock({
4
- name: 'App Shell',
5
- description: 'Full application layout with sidebar, header, and main content. Supports two layout modes: stacked (header full-width) and sidebar-inset (sidebar full-height).',
6
- category: 'layout',
7
- components: ['AppShell', 'Header', 'Sidebar', 'Theme'],
8
- tags: ['layout', 'app-shell', 'sidebar', 'navigation', 'dashboard'],
9
- code: `
10
- // App Shell - Stacked Layout (header spans full width)
11
- // Best for apps where the brand should be prominent in the header
12
-
13
- import { AppShell, Header, Input, Sidebar, ThemeToggle } from '@fragments-sdk/ui';
14
-
15
- function StackedLayout({ children }) {
16
- return (
17
- <AppShell layout="stacked">
18
- <AppShell.Header>
19
- <Header>
20
- <Header.SkipLink />
21
- <Header.Trigger />
22
- <Header.Brand href="/">MyApp</Header.Brand>
23
- <Header.Nav>
24
- <Header.NavItem href="/" active>Dashboard</Header.NavItem>
25
- <Header.NavItem href="/settings">Settings</Header.NavItem>
26
- </Header.Nav>
27
- <Header.Spacer />
28
- <Header.Actions>
29
- <ThemeToggle />
30
- </Header.Actions>
31
- </Header>
32
- </AppShell.Header>
33
-
34
- <AppShell.Sidebar width="240px" collapsible="offcanvas">
35
- <Sidebar.Nav>
36
- <Sidebar.Section label="Menu">
37
- <Sidebar.Item icon={<HomeIcon />} href="/" active>
38
- Home
39
- </Sidebar.Item>
40
- <Sidebar.Item icon={<ChartIcon />} href="/analytics">
41
- Analytics
42
- </Sidebar.Item>
43
- <Sidebar.Item icon={<GearIcon />} href="/settings">
44
- Settings
45
- </Sidebar.Item>
46
- </Sidebar.Section>
47
- </Sidebar.Nav>
48
- </AppShell.Sidebar>
49
-
50
- <AppShell.Main padding="lg">
51
- {children}
52
- </AppShell.Main>
53
- </AppShell>
54
- );
55
- }
56
-
57
- // App Shell - Sidebar Inset Layout (sidebar is full height)
58
- // Best for documentation sites or when sidebar branding is preferred
59
-
60
- function SidebarInsetLayout({ children }) {
61
- return (
62
- <AppShell layout="sidebar-inset">
63
- <AppShell.Header>
64
- <Header>
65
- <Header.SkipLink />
66
- <Header.Trigger />
67
- <Header.Search>
68
- <Input placeholder="Search..." />
69
- </Header.Search>
70
- <Header.Spacer />
71
- <Header.Actions>
72
- <ThemeToggle />
73
- </Header.Actions>
74
- </Header>
75
- </AppShell.Header>
76
-
77
- <AppShell.Sidebar width="260px" collapsible="offcanvas">
78
- <Sidebar.Header>
79
- <a href="/">MyApp</a>
80
- </Sidebar.Header>
81
- <Sidebar.Nav>
82
- <Sidebar.Section label="Getting Started">
83
- <Sidebar.Item href="/docs" active>Introduction</Sidebar.Item>
84
- <Sidebar.Item href="/docs/install">Installation</Sidebar.Item>
85
- </Sidebar.Section>
86
- <Sidebar.Section label="Components">
87
- <Sidebar.Item href="/components">Overview</Sidebar.Item>
88
- <Sidebar.Item href="/components/button">Button</Sidebar.Item>
89
- </Sidebar.Section>
90
- </Sidebar.Nav>
91
- <Sidebar.Footer>v1.0.0</Sidebar.Footer>
92
- </AppShell.Sidebar>
93
-
94
- <AppShell.Main padding="lg">
95
- {children}
96
- </AppShell.Main>
97
- </AppShell>
98
- );
99
- }
100
-
101
- // App Shell with Collapsible Icon Sidebar
102
- // Sidebar collapses to icons only - great for dashboards
103
-
104
- function CollapsibleLayout({ children }) {
105
- return (
106
- <AppShell layout="sidebar-inset">
107
- <AppShell.Header>
108
- <Header>
109
- <Header.Trigger />
110
- <Header.Spacer />
111
- <Header.Actions>
112
- <ThemeToggle />
113
- </Header.Actions>
114
- </Header>
115
- </AppShell.Header>
116
-
117
- <AppShell.Sidebar collapsible="icon" width="240px" collapsedWidth="64px">
118
- <Sidebar.Header collapsedContent={<Logo />}>
119
- <Logo /> <span>MyApp</span>
120
- </Sidebar.Header>
121
- <Sidebar.Nav>
122
- <Sidebar.Section>
123
- <Sidebar.Item icon={<HomeIcon />} active>Dashboard</Sidebar.Item>
124
- <Sidebar.Item icon={<ChartIcon />}>Analytics</Sidebar.Item>
125
- <Sidebar.Item icon={<GearIcon />}>Settings</Sidebar.Item>
126
- </Sidebar.Section>
127
- </Sidebar.Nav>
128
- <Sidebar.Footer>
129
- <Sidebar.CollapseToggle />
130
- </Sidebar.Footer>
131
- </AppShell.Sidebar>
132
-
133
- <AppShell.Main padding="lg">
134
- {children}
135
- </AppShell.Main>
136
- </AppShell>
137
- );
138
- }
139
-
140
- // App Shell with Aside Panel
141
- // Optional right panel for additional context
142
-
143
- function LayoutWithAside({ children, aside }) {
144
- return (
145
- <AppShell layout="stacked">
146
- <AppShell.Header>
147
- <Header>
148
- <Header.Brand>MyApp</Header.Brand>
149
- <Header.Spacer />
150
- <Header.Actions>
151
- <ThemeToggle />
152
- </Header.Actions>
153
- </Header>
154
- </AppShell.Header>
155
-
156
- <AppShell.Sidebar width="200px" collapsible="offcanvas">
157
- <Sidebar.Nav>
158
- <Sidebar.Section>
159
- <Sidebar.Item icon={<HomeIcon />} active>Home</Sidebar.Item>
160
- </Sidebar.Section>
161
- </Sidebar.Nav>
162
- </AppShell.Sidebar>
163
-
164
- <AppShell.Main padding="lg">
165
- {children}
166
- </AppShell.Main>
167
-
168
- <AppShell.Aside width="280px">
169
- {aside}
170
- </AppShell.Aside>
171
- </AppShell>
172
- );
173
- }
174
- `.trim(),
175
- });