@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.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/fragments.json +1 -1
- package/package.json +21 -2
- package/src/assets/fragments-logo.tsx +37 -0
- package/src/assets/fragments_logo.svg +1 -0
- package/src/assets/fragments_logo_text.svg +1 -0
- package/src/blocks/AccountSettings.block.ts +1 -1
- package/src/blocks/ActivityFeed.block.ts +7 -7
- package/src/blocks/ChatInterface.block.ts +36 -80
- package/src/blocks/DashboardLayout.block.ts +85 -66
- package/src/blocks/DashboardPage.block.ts +297 -0
- package/src/blocks/EmptyState.block.ts +5 -3
- package/src/blocks/FeatureGrid.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +21 -26
- package/src/blocks/PricingComparison.block.ts +1 -1
- package/src/blocks/ShoppingCart.block.ts +2 -2
- package/src/components/Input/Input.test.tsx +35 -0
- package/src/components/Input/index.tsx +47 -2
- package/src/components/Menu/Menu.module.scss +2 -0
- package/src/components/Table/Table.fragment.tsx +1 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +32 -0
- package/src/index.ts +3 -0
- package/src/tokens/_derive.scss +32 -8
- package/src/blocks/AIChat.block.ts +0 -266
- package/src/blocks/AppShell.block.ts +0 -175
- package/src/blocks/CTABanner.block.ts +0 -24
- package/src/blocks/CardGrid.block.ts +0 -22
- package/src/blocks/CodeExamples.block.ts +0 -66
- package/src/blocks/ConfirmDialog.block.ts +0 -19
- package/src/blocks/ConversationWithHistory.block.ts +0 -45
- package/src/blocks/DashboardNav.block.ts +0 -183
- package/src/blocks/ForgotPassword.block.ts +0 -26
- package/src/blocks/FormLayout.block.ts +0 -31
- package/src/blocks/InsetDashboardLayout.block.ts +0 -79
- package/src/blocks/MetricDashboard.block.ts +0 -38
- package/src/blocks/NewsletterSignup.block.ts +0 -26
- package/src/blocks/NotificationList.block.ts +0 -39
- package/src/blocks/NotificationPreferences.block.ts +0 -40
- package/src/blocks/OrderSummary.block.ts +0 -52
- package/src/blocks/ProfileEditForm.block.ts +0 -51
- package/src/blocks/SearchResults.block.ts +0 -39
- package/src/blocks/SettingsPage.block.ts +0 -58
- package/src/blocks/StreamingMessage.block.ts +0 -24
- package/src/blocks/TestimonialCard.block.ts +0 -27
- package/src/blocks/UserProfileCard.block.ts +0 -29
- package/src/recipes/AIChat.recipe.ts +0 -266
- package/src/recipes/AppShell.recipe.ts +0 -175
- package/src/recipes/CardGrid.recipe.ts +0 -22
- package/src/recipes/ChatInterface.recipe.ts +0 -87
- package/src/recipes/CodeExamples.recipe.ts +0 -66
- package/src/recipes/ConfirmDialog.recipe.ts +0 -19
- package/src/recipes/DashboardLayout.recipe.ts +0 -73
- package/src/recipes/DashboardNav.recipe.ts +0 -183
- package/src/recipes/FormLayout.recipe.ts +0 -31
- package/src/recipes/LoginForm.recipe.ts +0 -33
- 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={
|
|
133
|
+
ref={mergedRef}
|
|
89
134
|
type={type}
|
|
90
135
|
value={value}
|
|
91
136
|
defaultValue={defaultValue}
|
|
@@ -58,7 +58,7 @@ export default defineFragment({
|
|
|
58
58
|
],
|
|
59
59
|
whenNot: [
|
|
60
60
|
'Simple lists (use List component)',
|
|
61
|
-
'Card-based layouts (use
|
|
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
package/src/tokens/_derive.scss
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
});
|