astryx-dracula 0.5.0 → 0.6.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 (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,44 +1,45 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > H[g=1 wrap] > Tg"Writing"*4) + Hd"Suggested prompts"[level=2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
2
+ // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
+ // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
+ // human drift check — update it with the file, never alone):
5
+ // L[h=fill mw=720] > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > H[g=1 wrap] > Tg"Writing"*4) + VH"Suggested prompts"[as=h2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
4
6
 
5
- import {useRef, useState, type CSSProperties} from 'react';
6
-
7
- import {Layout, LayoutContent, VStack, HStack} from '@astryxdesign/core/Layout';
8
- import {Text, Heading} from '@astryxdesign/core/Text';
9
7
  import {
10
- ChatComposer,
11
- ChatComposerDrawer,
12
- ChatComposerInput,
13
- ChatDictationButton,
14
- useChatDictation,
15
- type ChatComposerInputHandle,
16
- type ChatComposerToken,
17
- type ChatComposerTrigger,
18
- } from '@astryxdesign/core/Chat';
8
+ ChatComposer,
9
+ ChatComposerDrawer,
10
+ ChatComposerInput,
11
+ type ChatComposerInputHandle,
12
+ type ChatComposerToken,
13
+ type ChatComposerTrigger,
14
+ ChatDictationButton,
15
+ useChatDictation,
16
+ } from "@astryxdesign/core/Chat";
17
+ import { ClickableCard } from "@astryxdesign/core/ClickableCard";
18
+ import { DropdownMenu, DropdownMenuItem } from "@astryxdesign/core/DropdownMenu";
19
+ import { Grid } from "@astryxdesign/core/Grid";
20
+ import { Icon } from "@astryxdesign/core/Icon";
21
+ import { HStack, Layout, LayoutContent, VStack } from "@astryxdesign/core/Layout";
22
+ import { Heading, Text } from "@astryxdesign/core/Text";
23
+ import { ToggleButton, ToggleButtonGroup } from "@astryxdesign/core/ToggleButton";
24
+ import { Token } from "@astryxdesign/core/Token";
19
25
  import {
20
- createStaticSource,
21
- TypeaheadItem,
22
- type SearchableItem,
23
- } from '@astryxdesign/core/Typeahead';
24
- import {ToggleButton, ToggleButtonGroup} from '@astryxdesign/core/ToggleButton';
25
- import {Token} from '@astryxdesign/core/Token';
26
- import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
27
- import {ClickableCard} from '@astryxdesign/core/ClickableCard';
28
- import {Grid} from '@astryxdesign/core/Grid';
29
- import {Icon} from '@astryxdesign/core/Icon';
30
- import {DropdownMenu, DropdownMenuItem} from '@astryxdesign/core/DropdownMenu';
26
+ createStaticSource,
27
+ type SearchableItem,
28
+ TypeaheadItem,
29
+ } from "@astryxdesign/core/Typeahead";
30
+ import { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden";
31
31
  import {
32
- Settings,
33
- AtSign,
34
- Sparkles,
35
- SquarePen,
36
- CodeXml,
37
- Search,
38
- Lock,
39
- Clock,
40
- Lightbulb,
41
- } from 'lucide-react';
32
+ AtSign,
33
+ Clock,
34
+ CodeXml,
35
+ Lightbulb,
36
+ Lock,
37
+ Search,
38
+ Settings,
39
+ Sparkles,
40
+ SquarePen,
41
+ } from "lucide-react";
42
+ import { type CSSProperties, useRef, useState } from "react";
42
43
 
43
44
  // Fill the content area so the greeting and composer stay vertically centered.
44
45
  // `height`, not `minHeight`. A percentage min-height resolves against the
@@ -48,502 +49,514 @@ import {
48
49
  // Viewport units are absolute and need no ancestor, which is why every working
49
50
  // instance in this repo uses them (editor.tsx:302, file-explorer.tsx:265,
50
51
  // messaging-shell.tsx:67, ai-chat.tsx:64, kanban-board.tsx:704).
51
- const pageStyle: CSSProperties = {height: '100dvh'};
52
+ const pageStyle: CSSProperties = { height: "100dvh" };
52
53
  // Five --spacing-4 steps: the box opens at ~80px so the empty composer reads
53
54
  // as a writing surface, not a single-line field. No single token is 80px.
54
- const composerInput: CSSProperties = {minHeight: 'calc(var(--spacing-4) * 5)'};
55
+ const composerInput: CSSProperties = { minHeight: "calc(var(--spacing-4) * 5)" };
55
56
 
56
57
  // Suggestion cards shown once a category is selected.
57
58
  const CATEGORY_SUGGESTIONS: Record<
58
- string,
59
- Array<{heading: string; body: string; prompt: string}>
59
+ string,
60
+ Array<{ heading: string; body: string; prompt: string }>
60
61
  > = {
61
- writing: [
62
- {
63
- heading: 'Draft a release announcement',
64
- body: 'Compose a clear, polished note for the night-shift crew',
65
- prompt: 'Help me draft a release announcement',
66
- },
67
- {
68
- heading: 'Improve my writing',
69
- body: 'Enhance the clarity, tone, and flow of my text',
70
- prompt: 'Review and improve the following text:',
71
- },
72
- {
73
- heading: 'Create a palette proposal',
74
- body: 'Write a proposal with hues, ramps, and contrast proofs',
75
- prompt: 'Help me write a palette proposal for',
76
- },
77
- {
78
- heading: 'Summarize a document',
79
- body: 'Condense a long grimoire into key takeaways',
80
- prompt: 'Summarize the following document into key points:',
81
- },
82
- ],
83
- coding: [
84
- {
85
- heading: 'Debug my code',
86
- body: 'Find and fix issues in a moonlit code snippet',
87
- prompt: 'Help me debug the following code:',
88
- },
89
- {
90
- heading: 'Write a function',
91
- body: 'Conjure a well-typed function warded against the dark',
92
- prompt: 'Write a function that',
93
- },
94
- {
95
- heading: 'Theme my editor',
96
- body: 'Port this palette to a Dracula editor theme',
97
- prompt: 'Convert the following colors to a Dracula theme:',
98
- },
99
- {
100
- heading: 'Review my pull request',
101
- body: 'Hunt bugs by moonlight, weigh contrast, bless the rite',
102
- prompt: 'Review this code for bugs and improvements:',
103
- },
104
- ],
105
- research: [
106
- {
107
- heading: 'Compare purple hues',
108
- body: 'Analyze the spectral trade-offs of each shade',
109
- prompt: 'Compare the pros and cons of',
110
- },
111
- {
112
- heading: 'Explain a concept',
113
- body: 'Unravel a tangled grimoire into plain night-tongue',
114
- prompt: 'Explain the concept of',
115
- },
116
- {
117
- heading: 'Find contrast proofs',
118
- body: 'Research AA pairings for text on dark surfaces',
119
- prompt: 'What are the best practices for',
120
- },
121
- {
122
- heading: 'Summarize findings',
123
- body: 'Compile research into a structured overview',
124
- prompt: 'Summarize the key findings on',
125
- },
126
- ],
127
- creative: [
128
- {
129
- heading: 'Brainstorm theme variants',
130
- body: 'Generate nocturnal concepts for a new variant',
131
- prompt: 'Brainstorm ideas for',
132
- },
133
- {
134
- heading: 'Write a dark tale',
135
- body: 'Spin a moonlit tale crawling with night creatures',
136
- prompt: 'Write a short story about',
137
- },
138
- {
139
- heading: 'Design a concept',
140
- body: 'Conjure nocturnal rites for product or visual haunts',
141
- prompt: 'Help me design a concept for',
142
- },
143
- {
144
- heading: 'Name a new shade',
145
- body: 'Craft a memorable name for a color or token',
146
- prompt: 'Create a catchy name for',
147
- },
148
- ],
62
+ writing: [
63
+ {
64
+ heading: "Draft a release announcement",
65
+ body: "Compose a clear, polished note for the night-shift crew",
66
+ prompt: "Help me draft a release announcement",
67
+ },
68
+ {
69
+ heading: "Improve my writing",
70
+ body: "Enhance the clarity, tone, and flow of my text",
71
+ prompt: "Review and improve the following text:",
72
+ },
73
+ {
74
+ heading: "Create a palette proposal",
75
+ body: "Write a proposal with hues, ramps, and contrast proofs",
76
+ prompt: "Help me write a palette proposal for",
77
+ },
78
+ {
79
+ heading: "Summarize a document",
80
+ body: "Condense a long grimoire into key takeaways",
81
+ prompt: "Summarize the following document into key points:",
82
+ },
83
+ ],
84
+ coding: [
85
+ {
86
+ heading: "Debug my code",
87
+ body: "Find and fix issues in a moonlit code snippet",
88
+ prompt: "Help me debug the following code:",
89
+ },
90
+ {
91
+ heading: "Write a function",
92
+ body: "Conjure a well-typed function warded against the dark",
93
+ prompt: "Write a function that",
94
+ },
95
+ {
96
+ heading: "Theme my editor",
97
+ body: "Port this palette to a Dracula editor theme",
98
+ prompt: "Convert the following colors to a Dracula theme:",
99
+ },
100
+ {
101
+ heading: "Review my pull request",
102
+ body: "Hunt bugs by moonlight, weigh contrast, bless the rite",
103
+ prompt: "Review this code for bugs and improvements:",
104
+ },
105
+ ],
106
+ research: [
107
+ {
108
+ heading: "Compare purple hues",
109
+ body: "Analyze the spectral trade-offs of each shade",
110
+ prompt: "Compare the pros and cons of",
111
+ },
112
+ {
113
+ heading: "Explain a concept",
114
+ body: "Unravel a tangled grimoire into plain night-tongue",
115
+ prompt: "Explain the concept of",
116
+ },
117
+ {
118
+ heading: "Find contrast proofs",
119
+ body: "Research AA pairings for text on dark surfaces",
120
+ prompt: "What are the best practices for",
121
+ },
122
+ {
123
+ heading: "Summarize findings",
124
+ body: "Compile research into a structured overview",
125
+ prompt: "Summarize the key findings on",
126
+ },
127
+ ],
128
+ creative: [
129
+ {
130
+ heading: "Brainstorm theme variants",
131
+ body: "Generate nocturnal concepts for a new variant",
132
+ prompt: "Brainstorm ideas for",
133
+ },
134
+ {
135
+ heading: "Write a dark tale",
136
+ body: "Spin a moonlit tale crawling with night creatures",
137
+ prompt: "Write a short story about",
138
+ },
139
+ {
140
+ heading: "Design a concept",
141
+ body: "Conjure nocturnal rites for product or visual haunts",
142
+ prompt: "Help me design a concept for",
143
+ },
144
+ {
145
+ heading: "Name a new shade",
146
+ body: "Craft a memorable name for a color or token",
147
+ prompt: "Create a catchy name for",
148
+ },
149
+ ],
149
150
  };
150
151
 
151
152
  // Category filters, shared by the toggle group and the composer mode menu.
152
153
  const CATEGORIES = [
153
- {key: 'writing', label: 'Writing', icon: SquarePen},
154
- {key: 'coding', label: 'Coding', icon: CodeXml},
155
- {key: 'research', label: 'Research', icon: Search},
156
- {key: 'creative', label: 'Creative', icon: Lightbulb},
154
+ { key: "writing", label: "Writing", icon: SquarePen },
155
+ { key: "coding", label: "Coding", icon: CodeXml },
156
+ { key: "research", label: "Research", icon: Search },
157
+ { key: "creative", label: "Creative", icon: Lightbulb },
157
158
  ] as const;
158
159
 
159
160
  // Composer mode menu: categories plus special modes.
160
161
  const MODE_OPTIONS = [
161
- {key: 'auto', label: 'Auto', icon: Sparkles},
162
- ...CATEGORIES,
163
- {key: 'sensitive', label: 'Sensitive', icon: Lock},
164
- {key: 'deep', label: 'Deep Mode', icon: Clock},
162
+ { key: "auto", label: "Auto", icon: Sparkles },
163
+ ...CATEGORIES,
164
+ { key: "sensitive", label: "Sensitive", icon: Lock },
165
+ { key: "deep", label: "Deep Mode", icon: Clock },
165
166
  ] as const;
166
167
 
167
168
  // Modes that insert a composer token instead of switching the active category.
168
169
  const TOKEN_MODES: Record<string, string> = {
169
- sensitive: '/sensitive',
170
- deep: '/deep-mode',
170
+ sensitive: "/sensitive",
171
+ deep: "/deep-mode",
171
172
  };
172
173
 
173
174
  // Composer trigger data: @ mentions and / commands.
174
- const MENTION_ITEMS: SearchableItem<{role: string}>[] = [
175
- {id: 'vesper', label: 'Vesper Lin', auxiliaryData: {role: 'Design Systems'}},
176
- {id: 'alex', label: 'Alex Nocturne', auxiliaryData: {role: 'Frontend'}},
177
- {id: 'sam', label: 'Sam Sable', auxiliaryData: {role: 'Backend'}},
178
- {id: 'jordan', label: 'Jordan Hex', auxiliaryData: {role: 'Product'}},
179
- {id: 'taylor', label: 'Taylor Twilight', auxiliaryData: {role: 'Design'}},
180
- {id: 'morgan', label: 'Morgan Morrow', auxiliaryData: {role: 'Infrastructure'}},
175
+ const MENTION_ITEMS: SearchableItem<{ role: string }>[] = [
176
+ { id: "vesper", label: "Vesper Lin", auxiliaryData: { role: "Design Systems" } },
177
+ { id: "alex", label: "Alex Nocturne", auxiliaryData: { role: "Frontend" } },
178
+ { id: "sam", label: "Sam Sable", auxiliaryData: { role: "Backend" } },
179
+ { id: "jordan", label: "Jordan Hex", auxiliaryData: { role: "Product" } },
180
+ { id: "taylor", label: "Taylor Twilight", auxiliaryData: { role: "Design" } },
181
+ { id: "morgan", label: "Morgan Morrow", auxiliaryData: { role: "Infrastructure" } },
181
182
  ];
182
183
 
183
- const COMMAND_ITEMS: SearchableItem<{description: string}>[] = [
184
- {
185
- id: 'summarize',
186
- label: 'summarize',
187
- auxiliaryData: {description: 'Summarize the conversation'},
188
- },
189
- {
190
- id: 'translate',
191
- label: 'translate',
192
- auxiliaryData: {description: 'Translate text to another language'},
193
- },
194
- {
195
- id: 'search',
196
- label: 'search',
197
- auxiliaryData: {description: 'Search the web or documents'},
198
- },
199
- {
200
- id: 'code',
201
- label: 'code',
202
- auxiliaryData: {description: 'Generate or explain code'},
203
- },
204
- {
205
- id: 'help',
206
- label: 'help',
207
- auxiliaryData: {description: 'Show available commands'},
208
- },
184
+ const COMMAND_ITEMS: SearchableItem<{ description: string }>[] = [
185
+ {
186
+ id: "summarize",
187
+ label: "summarize",
188
+ auxiliaryData: { description: "Summarize the conversation" },
189
+ },
190
+ {
191
+ id: "translate",
192
+ label: "translate",
193
+ auxiliaryData: { description: "Translate text to another language" },
194
+ },
195
+ {
196
+ id: "search",
197
+ label: "search",
198
+ auxiliaryData: { description: "Search the web or documents" },
199
+ },
200
+ {
201
+ id: "code",
202
+ label: "code",
203
+ auxiliaryData: { description: "Generate or explain code" },
204
+ },
205
+ {
206
+ id: "help",
207
+ label: "help",
208
+ auxiliaryData: { description: "Show available commands" },
209
+ },
209
210
  ];
210
211
 
211
212
  // One factory for both composer triggers. Render (TypeaheadItem + auxiliary
212
213
  // description) and select (token value/label/variant) differ per trigger;
213
214
  // the trigger shape doesn't.
214
215
  function makeTrigger<TAux>({
215
- character,
216
- items,
217
- getDescription,
218
- toToken,
216
+ character,
217
+ items,
218
+ getDescription,
219
+ toToken,
220
+ hasMultiWordQuery,
221
+ emptySearchText,
219
222
  }: {
220
- character: string;
221
- items: SearchableItem<TAux>[];
222
- getDescription: (aux: TAux | undefined) => string | undefined;
223
- toToken: (item: SearchableItem<TAux>) => string | ChatComposerToken;
223
+ character: string;
224
+ items: SearchableItem<TAux>[];
225
+ getDescription: (aux: TAux | undefined) => string | undefined;
226
+ toToken: (item: SearchableItem<TAux>) => string | ChatComposerToken;
227
+ /** Continue the query past spaces, for names that contain them. */
228
+ hasMultiWordQuery?: boolean;
229
+ /** Shown when the query matched nothing. */
230
+ emptySearchText?: string;
224
231
  }): ChatComposerTrigger {
225
- return {
226
- character,
227
- searchSource: createStaticSource(items),
228
- renderItem: item => (
229
- <TypeaheadItem
230
- item={item}
231
- description={getDescription(item.auxiliaryData as TAux | undefined)}
232
- />
233
- ),
234
- onSelect: item => toToken(item as SearchableItem<TAux>),
235
- };
232
+ return {
233
+ character,
234
+ searchSource: createStaticSource(items),
235
+ ...(hasMultiWordQuery ? { hasMultiWordQuery } : {}),
236
+ ...(emptySearchText ? { emptySearchText } : {}),
237
+ renderItem: (item) => (
238
+ <TypeaheadItem
239
+ item={item}
240
+ description={getDescription(item.auxiliaryData as TAux | undefined)}
241
+ />
242
+ ),
243
+ onSelect: (item) => toToken(item as SearchableItem<TAux>),
244
+ };
236
245
  }
237
246
 
238
247
  const mentionTrigger = makeTrigger({
239
- character: '@',
240
- items: MENTION_ITEMS,
241
- getDescription: aux => aux?.role,
242
- toToken: item => ({
243
- value: `@${item.id}`,
244
- label: item.label,
245
- variant: 'cyan',
246
- }),
248
+ character: "@",
249
+ items: MENTION_ITEMS,
250
+ getDescription: (aux) => aux?.role,
251
+ // The mention labels are full names ("Vesper Lin", "Alex Nocturne"), so
252
+ // the query has to survive the space: without this the menu closes at the
253
+ // first space and "@Vesper L" narrows to "Vesper" and then matches
254
+ // nothing. Core hides the menu on an unmatched multi-word query unless
255
+ // the trigger sets emptySearchText, so both go together.
256
+ hasMultiWordQuery: true,
257
+ emptySearchText: "No one by that name tonight.",
258
+ toToken: (item) => ({
259
+ value: `@${item.id}`,
260
+ label: item.label,
261
+ variant: "cyan",
262
+ }),
247
263
  });
248
264
 
249
265
  const commandTrigger = makeTrigger({
250
- character: '/',
251
- items: COMMAND_ITEMS,
252
- getDescription: aux => aux?.description,
253
- toToken: item => ({
254
- value: `/${item.label}`,
255
- label: `/${item.label}`,
256
- variant: 'yellow',
257
- }),
266
+ character: "/",
267
+ items: COMMAND_ITEMS,
268
+ getDescription: (aux) => aux?.description,
269
+ toToken: (item) => ({
270
+ value: `/${item.label}`,
271
+ label: `/${item.label}`,
272
+ variant: "yellow",
273
+ }),
258
274
  });
259
275
 
260
276
  const composerTriggers = [mentionTrigger, commandTrigger];
277
+ // The upstream `rename emptySearchResultsText to emptySearchText` codemod
278
+ // leaves a TODO on any ChatComposerInput whose `triggers` value it cannot
279
+ // attribute to a literal object — these are built by `makeTrigger`, so it
280
+ // cannot see inside them. The old key appears nowhere in this file, so that
281
+ // TODO would name a rename this template does not need.
261
282
 
262
283
  /** Composer attachment. The name is user-facing; the id is the React key, since
263
284
  * two dropped files can share a name. */
264
285
  interface Attachment {
265
- id: string;
266
- name: string;
286
+ id: string;
287
+ name: string;
267
288
  }
268
289
 
269
290
  const INITIAL_ATTACHMENTS: Attachment[] = [
270
- 'palette_brief.pdf',
271
- 'nocturne_v2.fig',
272
- 'api_spec.yaml',
273
- 'contrast_audit.csv',
274
- 'dracula_spec.pdf',
275
- ].map(name => ({id: crypto.randomUUID(), name}));
291
+ "palette_brief.pdf",
292
+ "nocturne_v2.fig",
293
+ "api_spec.yaml",
294
+ "contrast_audit.csv",
295
+ "dracula_spec.pdf",
296
+ ].map((name) => ({ id: crypto.randomUUID(), name }));
276
297
 
277
298
  // The composer's imperative insert methods mutate the DOM without emitting a
278
299
  // change, so dispatch an input event to sync its value and clear the placeholder.
279
300
  const syncComposerValue = () => {
280
- document.activeElement?.dispatchEvent(new Event('input', {bubbles: true}));
301
+ document.activeElement?.dispatchEvent(new Event("input", { bubbles: true }));
281
302
  };
282
303
 
283
304
  // One inserter for every composer write. `insertText` replaces the selection
284
305
  // (suggestion prompts); `insertToken` inserts a chip at the cursor without
285
306
  // touching the selection (mentions, mode tokens).
286
307
  function insertIntoComposer(
287
- input: ChatComposerInputHandle | null,
288
- write: {text: string} | {token: ChatComposerToken},
289
- collapseToEnd = false,
308
+ input: ChatComposerInputHandle | null,
309
+ write: { text: string } | { token: ChatComposerToken },
310
+ collapseToEnd = false,
290
311
  ) {
291
- if (!input) {
292
- return;
293
- }
294
- input.focus();
295
- if (document.activeElement) {
296
- if (collapseToEnd) {
297
- const sel = window.getSelection();
298
- sel?.selectAllChildren(document.activeElement);
299
- sel?.collapseToEnd();
300
- } else {
301
- window.getSelection()?.selectAllChildren(document.activeElement);
302
- }
303
- }
304
- if ('text' in write) {
305
- input.insertText(write.text);
306
- } else {
307
- input.insertToken(write.token);
308
- }
309
- syncComposerValue();
312
+ if (!input) {
313
+ return;
314
+ }
315
+ input.focus();
316
+ if (document.activeElement) {
317
+ if (collapseToEnd) {
318
+ const sel = window.getSelection();
319
+ sel?.selectAllChildren(document.activeElement);
320
+ sel?.collapseToEnd();
321
+ } else {
322
+ window.getSelection()?.selectAllChildren(document.activeElement);
323
+ }
324
+ }
325
+ if ("text" in write) {
326
+ input.insertText(write.text);
327
+ } else {
328
+ input.insertToken(write.token);
329
+ }
330
+ syncComposerValue();
310
331
  }
311
332
 
312
333
  // Main component
313
334
 
314
335
  export default function AiChatLanding() {
315
- const [mode, setMode] = useState<string | null>('auto');
316
- const [category, setCategory] = useState<string | null>(null);
317
- const [attachments, setAttachments] = useState<Attachment[]>(
318
- INITIAL_ATTACHMENTS,
319
- );
320
- const [isModeMenuOpen, setIsModeMenuOpen] = useState(false);
321
- const composerInputRef = useRef<ChatComposerInputHandle>(null);
322
- const shouldFocusComposerRef = useRef(false);
323
- // Dictation mic stays: the composer still renders ChatDictationButton below.
324
- const dictation = useChatDictation({inputRef: composerInputRef});
336
+ const [mode, setMode] = useState<string | null>("auto");
337
+ const [category, setCategory] = useState<string | null>(null);
338
+ const [attachments, setAttachments] = useState<Attachment[]>(INITIAL_ATTACHMENTS);
339
+ const [isModeMenuOpen, setIsModeMenuOpen] = useState(false);
340
+ const composerInputRef = useRef<ChatComposerInputHandle>(null);
341
+ const shouldFocusComposerRef = useRef(false);
342
+ // Dictation mic stays: the composer still renders ChatDictationButton below.
343
+ const dictation = useChatDictation({ inputRef: composerInputRef });
325
344
 
326
- const activeMode = MODE_OPTIONS.find(m => m.key === mode) ?? MODE_OPTIONS[0];
327
- const suggestions = category ? CATEGORY_SUGGESTIONS[category] : null;
345
+ const activeMode = MODE_OPTIONS.find((m) => m.key === mode) ?? MODE_OPTIONS[0];
346
+ const suggestions = category ? CATEGORY_SUGGESTIONS[category] : null;
328
347
 
329
- const applySuggestion = (prompt: string) => {
330
- insertIntoComposer(composerInputRef.current, {text: prompt});
331
- };
332
- const insertMention = (item: (typeof MENTION_ITEMS)[number]) => {
333
- insertIntoComposer(
334
- composerInputRef.current,
335
- {
336
- token: {
337
- value: `@${item.id}`,
338
- label: item.label,
339
- variant: 'cyan',
340
- },
341
- },
342
- true,
343
- );
344
- };
345
- const insertModeToken = (label: string) => {
346
- insertIntoComposer(composerInputRef.current, {
347
- token: {
348
- value: label,
349
- label,
350
- variant: 'orange',
351
- },
352
- });
353
- };
348
+ const applySuggestion = (prompt: string) => {
349
+ insertIntoComposer(composerInputRef.current, { text: prompt });
350
+ };
351
+ const insertMention = (item: (typeof MENTION_ITEMS)[number]) => {
352
+ insertIntoComposer(
353
+ composerInputRef.current,
354
+ {
355
+ token: {
356
+ value: `@${item.id}`,
357
+ label: item.label,
358
+ variant: "cyan",
359
+ },
360
+ },
361
+ true,
362
+ );
363
+ };
364
+ const insertModeToken = (label: string) => {
365
+ insertIntoComposer(composerInputRef.current, {
366
+ token: {
367
+ value: label,
368
+ label,
369
+ variant: "orange",
370
+ },
371
+ });
372
+ };
354
373
 
355
- return (
356
- <Layout
357
- height="fill"
358
- contentWidth={720}
359
- padding={6}
360
- content={
361
- <LayoutContent>
362
- <VStack gap={8} vAlign="center" style={pageStyle}>
363
- {/* Greeting */}
364
- <VStack gap={1}>
365
- <HStack gap={2} vAlign="center">
366
- <Icon icon={Sparkles} size="md" color="secondary" />
367
- <Text type="body">
368
- Hi, Vlad
369
- </Text>
370
- </HStack>
371
- <Heading level={1} type="display-2">
372
- Where should we start?
373
- </Heading>
374
- </VStack>
374
+ return (
375
+ <Layout
376
+ height="fill"
377
+ contentWidth={720}
378
+ padding={6}
379
+ content={
380
+ <LayoutContent>
381
+ <VStack gap={8} vAlign="center" style={pageStyle}>
382
+ {/* Greeting */}
383
+ <VStack gap={1}>
384
+ <HStack gap={2} vAlign="center">
385
+ <Icon icon={Sparkles} size="md" color="secondary" />
386
+ <Text type="body">Hi, Vlad</Text>
387
+ </HStack>
388
+ <Heading level={1} type="display-2">
389
+ Where should we start?
390
+ </Heading>
391
+ </VStack>
375
392
 
376
- {/* Composer */}
377
- <ChatComposer
378
- onSubmit={() => {}}
379
- placeholder="Ask anything"
380
- input={
381
- <ChatComposerInput
382
- handleRef={composerInputRef}
383
- triggers={composerTriggers}
384
- style={composerInput}
385
- onFiles={files =>
386
- setAttachments(prev => [
387
- ...prev,
388
- ...files.map(file => ({
389
- id: crypto.randomUUID(),
390
- name: file.name,
391
- })),
392
- ])
393
- }
394
- />
395
- }
396
- drawer={
397
- attachments.length > 0 ? (
398
- <ChatComposerDrawer count={attachments.length}>
399
- {attachments.map(attachment => (
400
- <Token
401
- key={attachment.id}
402
- label={attachment.name}
403
- onRemove={() =>
404
- setAttachments(prev =>
405
- prev.filter(item => item.id !== attachment.id),
406
- )
407
- }
408
- />
409
- ))}
410
- </ChatComposerDrawer>
411
- ) : undefined
412
- }
413
- headerActions={
414
- <DropdownMenu
415
- button={{
416
- label: 'Reference',
417
- variant: 'ghost',
418
- size: 'sm',
419
- icon: <Icon icon={AtSign} size="sm" />,
420
- isIconOnly: true,
421
- }}
422
- hasChevron={false}
423
- menuWidth={240}>
424
- {MENTION_ITEMS.map(item => (
425
- <DropdownMenuItem
426
- key={item.id}
427
- label={item.label}
428
- description={item.auxiliaryData?.role}
429
- onClick={() => insertMention(item)}
430
- />
431
- ))}
432
- </DropdownMenu>
433
- }
434
- footerActions={
435
- <>
436
- <DropdownMenu
437
- button={{
438
- label: activeMode.label,
439
- variant: 'ghost',
440
- size: 'md',
441
- icon: <Icon icon={activeMode.icon} size="sm" />,
442
- children: activeMode.label,
443
- }}
444
- menuWidth={200}
445
- isMenuOpen={isModeMenuOpen}
446
- onOpenChange={isOpen => {
447
- setIsModeMenuOpen(isOpen);
448
- // Restore focus to the composer after inserting a mode token.
449
- if (!isOpen && shouldFocusComposerRef.current) {
450
- shouldFocusComposerRef.current = false;
451
- setTimeout(() => composerInputRef.current?.focus(), 50);
452
- }
453
- }}
454
- items={MODE_OPTIONS.flatMap(opt => {
455
- const item = {
456
- label: opt.label,
457
- icon: opt.icon,
458
- onClick: () => {
459
- const tokenLabel = TOKEN_MODES[opt.key];
460
- if (tokenLabel) {
461
- insertModeToken(tokenLabel);
462
- shouldFocusComposerRef.current = true;
463
- } else {
464
- setMode(opt.key);
465
- }
466
- },
467
- };
468
- return opt.key === 'sensitive'
469
- ? [{type: 'divider' as const}, item]
470
- : [item];
471
- })}
472
- />
473
- <DropdownMenu
474
- button={{
475
- label: 'Settings',
476
- variant: 'ghost',
477
- size: 'md',
478
- icon: <Icon icon={Settings} size="sm" />,
479
- children: 'Settings',
480
- }}
481
- menuWidth={200}
482
- items={[
483
- {label: 'Preferences', onClick: () => {}},
484
- {label: 'Keyboard shortcuts', onClick: () => {}},
485
- {label: 'About', onClick: () => {}},
486
- ]}
487
- />
488
- </>
489
- }
490
- sendActions={<ChatDictationButton dictation={dictation} />}
491
- />
393
+ {/* Composer */}
394
+ <ChatComposer
395
+ onSubmit={() => {}}
396
+ placeholder="Ask anything"
397
+ input={
398
+ <ChatComposerInput
399
+ handleRef={composerInputRef}
400
+ triggers={composerTriggers}
401
+ style={composerInput}
402
+ onFiles={(files) =>
403
+ setAttachments((prev) => [
404
+ ...prev,
405
+ ...files.map((file) => ({
406
+ id: crypto.randomUUID(),
407
+ name: file.name,
408
+ })),
409
+ ])
410
+ }
411
+ />
412
+ }
413
+ drawer={
414
+ attachments.length > 0 ? (
415
+ <ChatComposerDrawer count={attachments.length}>
416
+ {attachments.map((attachment) => (
417
+ <Token
418
+ key={attachment.id}
419
+ label={attachment.name}
420
+ onRemove={() =>
421
+ setAttachments((prev) => prev.filter((item) => item.id !== attachment.id))
422
+ }
423
+ />
424
+ ))}
425
+ </ChatComposerDrawer>
426
+ ) : undefined
427
+ }
428
+ headerActions={
429
+ <DropdownMenu
430
+ button={{
431
+ label: "Reference",
432
+ variant: "ghost",
433
+ size: "sm",
434
+ icon: <Icon icon={AtSign} size="sm" />,
435
+ isIconOnly: true,
436
+ }}
437
+ hasChevron={false}
438
+ menuWidth={240}
439
+ >
440
+ {MENTION_ITEMS.map((item) => (
441
+ <DropdownMenuItem
442
+ key={item.id}
443
+ label={item.label}
444
+ description={item.auxiliaryData?.role}
445
+ onClick={() => insertMention(item)}
446
+ />
447
+ ))}
448
+ </DropdownMenu>
449
+ }
450
+ footerActions={
451
+ <>
452
+ <DropdownMenu
453
+ button={{
454
+ label: activeMode.label,
455
+ variant: "ghost",
456
+ size: "md",
457
+ icon: <Icon icon={activeMode.icon} size="sm" />,
458
+ children: activeMode.label,
459
+ }}
460
+ menuWidth={200}
461
+ isMenuOpen={isModeMenuOpen}
462
+ onOpenChange={(isOpen) => {
463
+ setIsModeMenuOpen(isOpen);
464
+ // Restore focus to the composer after inserting a mode token.
465
+ if (!isOpen && shouldFocusComposerRef.current) {
466
+ shouldFocusComposerRef.current = false;
467
+ setTimeout(() => composerInputRef.current?.focus(), 50);
468
+ }
469
+ }}
470
+ items={MODE_OPTIONS.flatMap((opt) => {
471
+ const item = {
472
+ label: opt.label,
473
+ icon: opt.icon,
474
+ onClick: () => {
475
+ const tokenLabel = TOKEN_MODES[opt.key];
476
+ if (tokenLabel) {
477
+ insertModeToken(tokenLabel);
478
+ shouldFocusComposerRef.current = true;
479
+ } else {
480
+ setMode(opt.key);
481
+ }
482
+ },
483
+ };
484
+ return opt.key === "sensitive"
485
+ ? [{ type: "divider" as const }, item]
486
+ : [item];
487
+ })}
488
+ />
489
+ <DropdownMenu
490
+ button={{
491
+ label: "Settings",
492
+ variant: "ghost",
493
+ size: "md",
494
+ icon: <Icon icon={Settings} size="sm" />,
495
+ children: "Settings",
496
+ }}
497
+ menuWidth={200}
498
+ items={[
499
+ { label: "Preferences", onClick: () => {} },
500
+ { label: "Keyboard shortcuts", onClick: () => {} },
501
+ { label: "About", onClick: () => {} },
502
+ ]}
503
+ />
504
+ </>
505
+ }
506
+ sendActions={<ChatDictationButton dictation={dictation} />}
507
+ />
492
508
 
493
- {/* Category filters + suggestion cards */}
494
- <VStack gap={6}>
495
- <ToggleButtonGroup
496
- label="Category"
497
- value={category}
498
- onChange={setCategory}
499
- size="lg">
500
- {/* ToggleButtonGroup is a single nowrap flex row and the four
509
+ {/* Category filters + suggestion cards */}
510
+ <VStack gap={6}>
511
+ <ToggleButtonGroup label="Category" value={category} onChange={setCategory} size="lg">
512
+ {/* ToggleButtonGroup is a single nowrap flex row and the four
501
513
  chips measure 488px together. In a 390px pane the fourth
502
514
  chip was cut in half by the viewport edge, which reads as a
503
515
  rendering fault rather than as overflow. The wrapping
504
516
  HStack gives the row a second line instead; the group keeps
505
517
  its own selection semantics either way. */}
506
- <HStack gap={1} wrap="wrap">
507
- {CATEGORIES.map(cat => (
508
- <ToggleButton
509
- key={cat.key}
510
- value={cat.key}
511
- label={cat.label}
512
- icon={<Icon icon={cat.icon} size="sm" />}
513
- />
514
- ))}
515
- </HStack>
516
- </ToggleButtonGroup>
518
+ <HStack gap={1} wrap="wrap">
519
+ {CATEGORIES.map((cat) => (
520
+ <ToggleButton
521
+ key={cat.key}
522
+ value={cat.key}
523
+ label={cat.label}
524
+ icon={<Icon icon={cat.icon} size="sm" />}
525
+ />
526
+ ))}
527
+ </HStack>
528
+ </ToggleButtonGroup>
517
529
 
518
- {suggestions && (
519
- <>
520
- <VisuallyHidden as="h2">Suggested prompts</VisuallyHidden>
521
- <Grid columns={{minWidth: 280}} gap={3}>
522
- {suggestions.map(suggestion => (
523
- <ClickableCard
524
- key={suggestion.heading}
525
- label={suggestion.heading}
526
- variant="muted"
527
- padding={4}
528
- onClick={() => {
529
- applySuggestion(suggestion.prompt);
530
- setMode(category);
531
- }}>
532
- <VStack gap={0.5}>
533
- <Heading level={3}>{suggestion.heading}</Heading>
534
- <Text type="body" color="secondary">
535
- {suggestion.body}
536
- </Text>
537
- </VStack>
538
- </ClickableCard>
539
- ))}
540
- </Grid>
541
- </>
542
- )}
543
- </VStack>
544
- </VStack>
545
- </LayoutContent>
546
- }
547
- />
548
- );
530
+ {suggestions && (
531
+ <>
532
+ <VisuallyHidden as="h2">Suggested prompts</VisuallyHidden>
533
+ <Grid columns={{ minWidth: 280 }} gap={3}>
534
+ {suggestions.map((suggestion) => (
535
+ <ClickableCard
536
+ key={suggestion.heading}
537
+ label={suggestion.heading}
538
+ variant="muted"
539
+ padding={4}
540
+ onClick={() => {
541
+ applySuggestion(suggestion.prompt);
542
+ setMode(category);
543
+ }}
544
+ >
545
+ <VStack gap={0.5}>
546
+ <Heading level={3}>{suggestion.heading}</Heading>
547
+ <Text type="body" color="secondary">
548
+ {suggestion.body}
549
+ </Text>
550
+ </VStack>
551
+ </ClickableCard>
552
+ ))}
553
+ </Grid>
554
+ </>
555
+ )}
556
+ </VStack>
557
+ </VStack>
558
+ </LayoutContent>
559
+ }
560
+ />
561
+ );
549
562
  }