astryx-dracula 0.1.3 → 0.2.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/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
package/BRAND.md
CHANGED
|
@@ -74,4 +74,4 @@ Radii flat and crisp (5px elements, 4px inner). Everything below is a core defau
|
|
|
74
74
|
|
|
75
75
|
## Dims
|
|
76
76
|
|
|
77
|
-
Gap
|
|
77
|
+
Gap 24px, viewport 16px, content 15px vertical / 16px horizontal, widget-gap 24px, tile-row 96px, radius 5px everywhere, JetBrains Mono, type scale base 14 ratio 1.2: h1 24 / h2 20 / h3 17 / h4 14 / body 14 / supporting 12 / h6 10. Outer Card padding 4, nested inset 3. Touch targets 24px floor, 44 where touch matters.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Live showcase: https://yuzu-octopus.github.io/astryx-dracula/
|
|
|
16
16
|
- WCAG contrast gates enforced in CI via `bun run audit`
|
|
17
17
|
- Vendored JetBrains Mono fonts and Lucide icons, zero theme dependencies
|
|
18
18
|
- Agent skill, `llms.txt`, and copy-paste quickstart for AI-assisted adoption
|
|
19
|
+
- 43 themed pages in `templates/`, published as an Astryx integration pack and viewable live at `/astryx-dracula/#/templates`
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -48,6 +49,7 @@ theme.css prebuilt output of bun run theme:build
|
|
|
48
49
|
tokens.css plain-CSS :root fallback for any stack
|
|
49
50
|
icons.tsx vendored Lucide icon registry
|
|
50
51
|
fonts/ JetBrains Mono woff2 files
|
|
52
|
+
templates/ 43 themed pages plus integration specs
|
|
51
53
|
scripts/check.ts palette plus contrast gates
|
|
52
54
|
demo/ showcase source, deployed to GitHub Pages
|
|
53
55
|
```
|
package/USAGE.md
CHANGED
|
@@ -71,6 +71,19 @@ Then use `var(--color-primary)`, `var(--dracula-purple)`, `var(--space-gap)`.
|
|
|
71
71
|
3. Replace: delete the old theme provider and `:root` overrides, point imports at the kit prebuilt path, swap raw elements for Astryx components.
|
|
72
72
|
4. Verify: build, screenshot key pages, confirm no stray hexes remain in `src/`.
|
|
73
73
|
|
|
74
|
+
## Templates
|
|
75
|
+
|
|
76
|
+
43 themed pages ship in `templates/`, each with a `<id>.template.mjs` spec, published
|
|
77
|
+
as an Astryx integration pack (`astryx.integration.mjs`). List this package in your
|
|
78
|
+
`astryx.config`, then scaffold any of them:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bunx astryx template dashboard --package astryx-dracula
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
View all 43 live at `/astryx-dracula/#/templates` on the showcase. Pack rules: templates
|
|
85
|
+
import React plus `@astryxdesign/core` only.
|
|
86
|
+
|
|
74
87
|
## Fonts
|
|
75
88
|
|
|
76
89
|
Copy `fonts/` to your app's served static dir (Vite: `public/fonts/`).
|
package/astryx-dracula.js
CHANGED
|
@@ -123,11 +123,11 @@ export const astryxDraculaTheme = {
|
|
|
123
123
|
"--color-warning-muted": "var(--color-background-yellow)",
|
|
124
124
|
"--color-error-muted": "var(--color-background-red)",
|
|
125
125
|
"--color-info-muted": "var(--color-background-cyan)",
|
|
126
|
-
"--space-gap": "
|
|
127
|
-
"--space-viewport": "
|
|
126
|
+
"--space-gap": "24px",
|
|
127
|
+
"--space-viewport": "16px",
|
|
128
128
|
"--widget-content-vertical": "15px",
|
|
129
|
-
"--widget-content-horizontal": "
|
|
130
|
-
"--widget-gap": "
|
|
129
|
+
"--widget-content-horizontal": "16px",
|
|
130
|
+
"--widget-gap": "24px",
|
|
131
131
|
"--tile-row": "96px",
|
|
132
132
|
"--border-radius": "5px",
|
|
133
133
|
"--color-background-body": "light-dark(#282A36, #282A36)",
|
package/package.json
CHANGED
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"fonts/",
|
|
41
41
|
"icons.tsx",
|
|
42
42
|
"theme.css",
|
|
43
|
-
"tokens.css"
|
|
43
|
+
"tokens.css",
|
|
44
|
+
"templates/",
|
|
45
|
+
"astryx.integration.mjs"
|
|
44
46
|
],
|
|
45
47
|
"license": "MIT",
|
|
46
48
|
"name": "astryx-dracula",
|
|
@@ -62,5 +64,5 @@
|
|
|
62
64
|
"@astryxdesign/cli"
|
|
63
65
|
],
|
|
64
66
|
"type": "module",
|
|
65
|
-
"version": "0.
|
|
67
|
+
"version": "0.2.0"
|
|
66
68
|
}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useRef, useState, type CSSProperties} from 'react';
|
|
4
|
+
|
|
5
|
+
import {Layout, LayoutContent, VStack, HStack} from '@astryxdesign/core/Layout';
|
|
6
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
7
|
+
import {
|
|
8
|
+
ChatComposer,
|
|
9
|
+
ChatComposerDrawer,
|
|
10
|
+
ChatComposerInput,
|
|
11
|
+
ChatDictationButton,
|
|
12
|
+
useChatDictation,
|
|
13
|
+
type ChatComposerInputHandle,
|
|
14
|
+
type ChatComposerTrigger,
|
|
15
|
+
} from '@astryxdesign/core/Chat';
|
|
16
|
+
import {
|
|
17
|
+
createStaticSource,
|
|
18
|
+
TypeaheadItem,
|
|
19
|
+
type SearchableItem,
|
|
20
|
+
} from '@astryxdesign/core/Typeahead';
|
|
21
|
+
import {ToggleButton, ToggleButtonGroup} from '@astryxdesign/core/ToggleButton';
|
|
22
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
23
|
+
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
|
|
24
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
25
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
26
|
+
import {DropdownMenu, DropdownMenuItem} from '@astryxdesign/core/DropdownMenu';
|
|
27
|
+
import {
|
|
28
|
+
Settings,
|
|
29
|
+
AtSign,
|
|
30
|
+
Sparkles,
|
|
31
|
+
SquarePen,
|
|
32
|
+
CodeXml,
|
|
33
|
+
Search,
|
|
34
|
+
Lock,
|
|
35
|
+
Clock,
|
|
36
|
+
Lightbulb,
|
|
37
|
+
} from 'lucide-react';
|
|
38
|
+
|
|
39
|
+
// Fill the content area so the greeting and composer stay vertically centered.
|
|
40
|
+
const pageStyle: CSSProperties = {minHeight: '100%'};
|
|
41
|
+
const composerInput: CSSProperties = {minHeight: 84};
|
|
42
|
+
const categories: CSSProperties = {paddingInline: 'var(--space-viewport)'};
|
|
43
|
+
|
|
44
|
+
// Suggestion cards shown once a category is selected.
|
|
45
|
+
const CATEGORY_SUGGESTIONS: Record<
|
|
46
|
+
string,
|
|
47
|
+
Array<{heading: string; body: string; prompt: string}>
|
|
48
|
+
> = {
|
|
49
|
+
writing: [
|
|
50
|
+
{
|
|
51
|
+
heading: 'Draft a release announcement',
|
|
52
|
+
body: 'Compose a clear, polished note for the night-shift crew',
|
|
53
|
+
prompt: 'Help me draft a release announcement',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
heading: 'Improve my writing',
|
|
57
|
+
body: 'Enhance the clarity, tone, and flow of my text',
|
|
58
|
+
prompt: 'Review and improve the following text:',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
heading: 'Create a palette proposal',
|
|
62
|
+
body: 'Write a proposal with hues, ramps, and contrast proofs',
|
|
63
|
+
prompt: 'Help me write a palette proposal for',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
heading: 'Summarize a document',
|
|
67
|
+
body: 'Condense a long grimoire into key takeaways',
|
|
68
|
+
prompt: 'Summarize the following document into key points:',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
coding: [
|
|
72
|
+
{
|
|
73
|
+
heading: 'Debug my code',
|
|
74
|
+
body: 'Find and fix issues in a moonlit code snippet',
|
|
75
|
+
prompt: 'Help me debug the following code:',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
heading: 'Write a function',
|
|
79
|
+
body: 'Generate a well-typed function with error handling',
|
|
80
|
+
prompt: 'Write a function that',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
heading: 'Theme my editor',
|
|
84
|
+
body: 'Port this palette to a Dracula editor theme',
|
|
85
|
+
prompt: 'Convert the following colors to a Dracula theme:',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
heading: 'Review my pull request',
|
|
89
|
+
body: 'Check for bugs, contrast, and best practices',
|
|
90
|
+
prompt: 'Review this code for bugs and improvements:',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
research: [
|
|
94
|
+
{
|
|
95
|
+
heading: 'Compare purple hues',
|
|
96
|
+
body: 'Analyze the spectral trade-offs of each shade',
|
|
97
|
+
prompt: 'Compare the pros and cons of',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
heading: 'Explain a concept',
|
|
101
|
+
body: 'Break down a complex topic in simple terms',
|
|
102
|
+
prompt: 'Explain the concept of',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
heading: 'Find contrast proofs',
|
|
106
|
+
body: 'Research AA pairings for text on dark surfaces',
|
|
107
|
+
prompt: 'What are the best practices for',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
heading: 'Summarize findings',
|
|
111
|
+
body: 'Compile research into a structured overview',
|
|
112
|
+
prompt: 'Summarize the key findings on',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
creative: [
|
|
116
|
+
{
|
|
117
|
+
heading: 'Brainstorm theme variants',
|
|
118
|
+
body: 'Generate nocturnal concepts for a new variant',
|
|
119
|
+
prompt: 'Brainstorm ideas for',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
heading: 'Write a dark tale',
|
|
123
|
+
body: 'Create an engaging narrative with creatures',
|
|
124
|
+
prompt: 'Write a short story about',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
heading: 'Design a concept',
|
|
128
|
+
body: 'Explore product or visual design ideas',
|
|
129
|
+
prompt: 'Help me design a concept for',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
heading: 'Name a new shade',
|
|
133
|
+
body: 'Craft a memorable name for a color or token',
|
|
134
|
+
prompt: 'Create a catchy name for',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Category filters, shared by the toggle group and the composer mode menu.
|
|
140
|
+
const CATEGORIES = [
|
|
141
|
+
{key: 'writing', label: 'Writing', icon: SquarePen},
|
|
142
|
+
{key: 'coding', label: 'Coding', icon: CodeXml},
|
|
143
|
+
{key: 'research', label: 'Research', icon: Search},
|
|
144
|
+
{key: 'creative', label: 'Creative', icon: Lightbulb},
|
|
145
|
+
] as const;
|
|
146
|
+
|
|
147
|
+
// Composer mode menu: categories plus special modes.
|
|
148
|
+
const MODE_OPTIONS = [
|
|
149
|
+
{key: 'auto', label: 'Auto', icon: Sparkles},
|
|
150
|
+
...CATEGORIES,
|
|
151
|
+
{key: 'sensitive', label: 'Sensitive', icon: Lock},
|
|
152
|
+
{key: 'deep', label: 'Deep Mode', icon: Clock},
|
|
153
|
+
] as const;
|
|
154
|
+
|
|
155
|
+
// Modes that insert a composer token instead of switching the active category.
|
|
156
|
+
const TOKEN_MODES: Record<string, string> = {
|
|
157
|
+
sensitive: '/sensitive',
|
|
158
|
+
deep: '/deep-mode',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Composer trigger data: @ mentions and / commands.
|
|
162
|
+
const MENTION_ITEMS: SearchableItem<{role: string}>[] = [
|
|
163
|
+
{id: 'vesper', label: 'Vesper Lin', auxiliaryData: {role: 'Design Systems'}},
|
|
164
|
+
{id: 'alex', label: 'Alex Nocturne', auxiliaryData: {role: 'Frontend'}},
|
|
165
|
+
{id: 'sam', label: 'Sam Sable', auxiliaryData: {role: 'Backend'}},
|
|
166
|
+
{id: 'jordan', label: 'Jordan Hex', auxiliaryData: {role: 'Product'}},
|
|
167
|
+
{id: 'taylor', label: 'Taylor Twilight', auxiliaryData: {role: 'Design'}},
|
|
168
|
+
{id: 'morgan', label: 'Morgan Morrow', auxiliaryData: {role: 'Infrastructure'}},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const COMMAND_ITEMS: SearchableItem<{description: string}>[] = [
|
|
172
|
+
{
|
|
173
|
+
id: 'summarize',
|
|
174
|
+
label: 'summarize',
|
|
175
|
+
auxiliaryData: {description: 'Summarize the conversation'},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'translate',
|
|
179
|
+
label: 'translate',
|
|
180
|
+
auxiliaryData: {description: 'Translate text to another language'},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'search',
|
|
184
|
+
label: 'search',
|
|
185
|
+
auxiliaryData: {description: 'Search the web or documents'},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'code',
|
|
189
|
+
label: 'code',
|
|
190
|
+
auxiliaryData: {description: 'Generate or explain code'},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'help',
|
|
194
|
+
label: 'help',
|
|
195
|
+
auxiliaryData: {description: 'Show available commands'},
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
const mentionTrigger: ChatComposerTrigger = {
|
|
200
|
+
character: '@',
|
|
201
|
+
searchSource: createStaticSource(MENTION_ITEMS),
|
|
202
|
+
renderItem: item => (
|
|
203
|
+
<TypeaheadItem
|
|
204
|
+
item={item}
|
|
205
|
+
description={(item.auxiliaryData as {role: string})?.role}
|
|
206
|
+
/>
|
|
207
|
+
),
|
|
208
|
+
onSelect: item => ({
|
|
209
|
+
value: `@${item.id}`,
|
|
210
|
+
label: item.label,
|
|
211
|
+
variant: 'cyan',
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const commandTrigger: ChatComposerTrigger = {
|
|
216
|
+
character: '/',
|
|
217
|
+
searchSource: createStaticSource(COMMAND_ITEMS),
|
|
218
|
+
renderItem: item => (
|
|
219
|
+
<TypeaheadItem
|
|
220
|
+
item={item}
|
|
221
|
+
description={(item.auxiliaryData as {description: string})?.description}
|
|
222
|
+
/>
|
|
223
|
+
),
|
|
224
|
+
onSelect: item => ({
|
|
225
|
+
value: `/${item.label}`,
|
|
226
|
+
label: `/${item.label}`,
|
|
227
|
+
variant: 'yellow',
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const composerTriggers = [mentionTrigger, commandTrigger];
|
|
232
|
+
|
|
233
|
+
// Main component
|
|
234
|
+
|
|
235
|
+
export default function AiChatLanding() {
|
|
236
|
+
const [mode, setMode] = useState<string | null>('auto');
|
|
237
|
+
const [category, setCategory] = useState<string | null>(null);
|
|
238
|
+
const [attachments, setAttachments] = useState<string[]>([
|
|
239
|
+
'palette_brief.pdf',
|
|
240
|
+
'nocturne_v2.fig',
|
|
241
|
+
'api_spec.yaml',
|
|
242
|
+
'contrast_audit.csv',
|
|
243
|
+
'dracula_spec.pdf',
|
|
244
|
+
]);
|
|
245
|
+
const [isModeMenuOpen, setIsModeMenuOpen] = useState(false);
|
|
246
|
+
const composerInputRef = useRef<ChatComposerInputHandle>(null);
|
|
247
|
+
const shouldFocusComposerRef = useRef(false);
|
|
248
|
+
const dictation = useChatDictation({inputRef: composerInputRef});
|
|
249
|
+
|
|
250
|
+
const activeMode = MODE_OPTIONS.find(m => m.key === mode) ?? MODE_OPTIONS[0];
|
|
251
|
+
const suggestions = category ? CATEGORY_SUGGESTIONS[category] : null;
|
|
252
|
+
|
|
253
|
+
// The composer's imperative insert methods mutate the DOM without emitting a
|
|
254
|
+
// change, so dispatch an input event to sync its value and clear the placeholder.
|
|
255
|
+
const syncComposerValue = () => {
|
|
256
|
+
document.activeElement?.dispatchEvent(new Event('input', {bubbles: true}));
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const applySuggestion = (prompt: string) => {
|
|
260
|
+
const input = composerInputRef.current;
|
|
261
|
+
if (!input) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
input.focus();
|
|
265
|
+
if (document.activeElement) {
|
|
266
|
+
window.getSelection()?.selectAllChildren(document.activeElement);
|
|
267
|
+
}
|
|
268
|
+
input.insertText(prompt);
|
|
269
|
+
syncComposerValue();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const insertMention = (item: (typeof MENTION_ITEMS)[number]) => {
|
|
273
|
+
const input = composerInputRef.current;
|
|
274
|
+
if (!input) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
input.focus();
|
|
278
|
+
if (document.activeElement) {
|
|
279
|
+
const sel = window.getSelection();
|
|
280
|
+
sel?.selectAllChildren(document.activeElement);
|
|
281
|
+
sel?.collapseToEnd();
|
|
282
|
+
}
|
|
283
|
+
input.insertToken({
|
|
284
|
+
value: `@${item.id}`,
|
|
285
|
+
label: item.label,
|
|
286
|
+
variant: 'cyan',
|
|
287
|
+
});
|
|
288
|
+
syncComposerValue();
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const insertModeToken = (label: string) => {
|
|
292
|
+
composerInputRef.current?.focus();
|
|
293
|
+
composerInputRef.current?.insertToken({
|
|
294
|
+
value: label,
|
|
295
|
+
label,
|
|
296
|
+
variant: 'orange',
|
|
297
|
+
});
|
|
298
|
+
syncComposerValue();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<Layout
|
|
303
|
+
height="fill"
|
|
304
|
+
contentWidth={720}
|
|
305
|
+
padding={6}
|
|
306
|
+
content={
|
|
307
|
+
<LayoutContent>
|
|
308
|
+
<VStack gap={8} vAlign="center" style={pageStyle}>
|
|
309
|
+
{/* Greeting */}
|
|
310
|
+
<VStack gap={1}>
|
|
311
|
+
<HStack gap={2} vAlign="center">
|
|
312
|
+
<Icon icon={Sparkles} size="md" color="accent" />
|
|
313
|
+
<Text type="large">
|
|
314
|
+
Hi, Vlad
|
|
315
|
+
</Text>
|
|
316
|
+
</HStack>
|
|
317
|
+
<Heading level={1} type="display-2">
|
|
318
|
+
Where should we start?
|
|
319
|
+
</Heading>
|
|
320
|
+
</VStack>
|
|
321
|
+
|
|
322
|
+
{/* Composer */}
|
|
323
|
+
<ChatComposer
|
|
324
|
+
onSubmit={() => {}}
|
|
325
|
+
placeholder="Ask anything"
|
|
326
|
+
input={
|
|
327
|
+
<ChatComposerInput
|
|
328
|
+
handleRef={composerInputRef}
|
|
329
|
+
triggers={composerTriggers}
|
|
330
|
+
style={composerInput}
|
|
331
|
+
onFiles={files =>
|
|
332
|
+
setAttachments(prev => [...prev, ...files.map(f => f.name)])
|
|
333
|
+
}
|
|
334
|
+
/>
|
|
335
|
+
}
|
|
336
|
+
drawer={
|
|
337
|
+
attachments.length > 0 ? (
|
|
338
|
+
<ChatComposerDrawer count={attachments.length}>
|
|
339
|
+
{attachments.map(name => (
|
|
340
|
+
<Token
|
|
341
|
+
key={name}
|
|
342
|
+
label={name}
|
|
343
|
+
onRemove={() =>
|
|
344
|
+
setAttachments(prev => prev.filter(n => n !== name))
|
|
345
|
+
}
|
|
346
|
+
/>
|
|
347
|
+
))}
|
|
348
|
+
</ChatComposerDrawer>
|
|
349
|
+
) : undefined
|
|
350
|
+
}
|
|
351
|
+
headerActions={
|
|
352
|
+
<DropdownMenu
|
|
353
|
+
button={{
|
|
354
|
+
label: 'Reference',
|
|
355
|
+
variant: 'ghost',
|
|
356
|
+
size: 'sm',
|
|
357
|
+
icon: <Icon icon={AtSign} size="sm" />,
|
|
358
|
+
isIconOnly: true,
|
|
359
|
+
}}
|
|
360
|
+
hasChevron={false}
|
|
361
|
+
menuWidth={240}>
|
|
362
|
+
{MENTION_ITEMS.map(item => (
|
|
363
|
+
<DropdownMenuItem
|
|
364
|
+
key={item.id}
|
|
365
|
+
label={item.label}
|
|
366
|
+
description={item.auxiliaryData?.role}
|
|
367
|
+
onClick={() => insertMention(item)}
|
|
368
|
+
/>
|
|
369
|
+
))}
|
|
370
|
+
</DropdownMenu>
|
|
371
|
+
}
|
|
372
|
+
footerActions={
|
|
373
|
+
<>
|
|
374
|
+
<DropdownMenu
|
|
375
|
+
button={{
|
|
376
|
+
label: activeMode.label,
|
|
377
|
+
variant: 'ghost',
|
|
378
|
+
size: 'md',
|
|
379
|
+
icon: <Icon icon={activeMode.icon} size="sm" />,
|
|
380
|
+
children: activeMode.label,
|
|
381
|
+
}}
|
|
382
|
+
menuWidth={200}
|
|
383
|
+
isMenuOpen={isModeMenuOpen}
|
|
384
|
+
onOpenChange={isOpen => {
|
|
385
|
+
setIsModeMenuOpen(isOpen);
|
|
386
|
+
// Restore focus to the composer after inserting a mode token.
|
|
387
|
+
if (!isOpen && shouldFocusComposerRef.current) {
|
|
388
|
+
shouldFocusComposerRef.current = false;
|
|
389
|
+
setTimeout(() => composerInputRef.current?.focus(), 50);
|
|
390
|
+
}
|
|
391
|
+
}}
|
|
392
|
+
items={MODE_OPTIONS.flatMap(opt => {
|
|
393
|
+
const item = {
|
|
394
|
+
label: opt.label,
|
|
395
|
+
icon: opt.icon,
|
|
396
|
+
onClick: () => {
|
|
397
|
+
const tokenLabel = TOKEN_MODES[opt.key];
|
|
398
|
+
if (tokenLabel) {
|
|
399
|
+
insertModeToken(tokenLabel);
|
|
400
|
+
shouldFocusComposerRef.current = true;
|
|
401
|
+
} else {
|
|
402
|
+
setMode(opt.key);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
return opt.key === 'sensitive'
|
|
407
|
+
? [{type: 'divider' as const}, item]
|
|
408
|
+
: [item];
|
|
409
|
+
})}
|
|
410
|
+
/>
|
|
411
|
+
<DropdownMenu
|
|
412
|
+
button={{
|
|
413
|
+
label: 'Settings',
|
|
414
|
+
variant: 'ghost',
|
|
415
|
+
size: 'md',
|
|
416
|
+
icon: <Icon icon={Settings} size="sm" />,
|
|
417
|
+
children: 'Settings',
|
|
418
|
+
}}
|
|
419
|
+
menuWidth={200}
|
|
420
|
+
items={[
|
|
421
|
+
{label: 'Preferences', onClick: () => {}},
|
|
422
|
+
{label: 'Keyboard shortcuts', onClick: () => {}},
|
|
423
|
+
{label: 'About', onClick: () => {}},
|
|
424
|
+
]}
|
|
425
|
+
/>
|
|
426
|
+
</>
|
|
427
|
+
}
|
|
428
|
+
sendActions={<ChatDictationButton dictation={dictation} />}
|
|
429
|
+
/>
|
|
430
|
+
|
|
431
|
+
{/* Category filters + suggestion cards */}
|
|
432
|
+
<VStack gap={6} style={categories}>
|
|
433
|
+
<ToggleButtonGroup
|
|
434
|
+
label="Category"
|
|
435
|
+
value={category}
|
|
436
|
+
onChange={setCategory}
|
|
437
|
+
size="lg">
|
|
438
|
+
{CATEGORIES.map(cat => (
|
|
439
|
+
<ToggleButton
|
|
440
|
+
key={cat.key}
|
|
441
|
+
value={cat.key}
|
|
442
|
+
label={cat.label}
|
|
443
|
+
icon={<Icon icon={cat.icon} size="sm" />}
|
|
444
|
+
/>
|
|
445
|
+
))}
|
|
446
|
+
</ToggleButtonGroup>
|
|
447
|
+
|
|
448
|
+
{suggestions && (
|
|
449
|
+
<Grid columns={{minWidth: 280}} gap={3}>
|
|
450
|
+
{suggestions.map(suggestion => (
|
|
451
|
+
<ClickableCard
|
|
452
|
+
key={suggestion.heading}
|
|
453
|
+
label={suggestion.heading}
|
|
454
|
+
variant="muted"
|
|
455
|
+
padding={4}
|
|
456
|
+
onClick={() => {
|
|
457
|
+
applySuggestion(suggestion.prompt);
|
|
458
|
+
setMode(category);
|
|
459
|
+
}}>
|
|
460
|
+
<VStack gap={0.5}>
|
|
461
|
+
<Heading level={4}>{suggestion.heading}</Heading>
|
|
462
|
+
<Text type="body" color="secondary">
|
|
463
|
+
{suggestion.body}
|
|
464
|
+
</Text>
|
|
465
|
+
</VStack>
|
|
466
|
+
</ClickableCard>
|
|
467
|
+
))}
|
|
468
|
+
</Grid>
|
|
469
|
+
)}
|
|
470
|
+
</VStack>
|
|
471
|
+
</VStack>
|
|
472
|
+
</LayoutContent>
|
|
473
|
+
}
|
|
474
|
+
/>
|
|
475
|
+
);
|
|
476
|
+
}
|