astryx-dracula 0.5.1 → 0.7.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.
Files changed (120) hide show
  1. package/BRAND.md +57 -6
  2. package/README.md +16 -6
  3. package/USAGE.md +50 -50
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +49 -11
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/icons.mjs +4 -0
  8. package/icons.tsx +8 -0
  9. package/package.json +23 -18
  10. package/shared/auth-chrome-config.ts +0 -4
  11. package/shared/auth-chrome.tsx +2 -3
  12. package/shared/chaptered-doc-config.ts +3 -2
  13. package/shared/chaptered-doc.tsx +1 -2
  14. package/shared/chart-hues.ts +21 -6
  15. package/shared/chart-legend.tsx +2 -2
  16. package/shared/data-bar.tsx +2 -2
  17. package/shared/order-desk.tsx +52 -54
  18. package/shared/revenue-chart.tsx +1 -1
  19. package/shared/scene-castle.tsx +21 -13
  20. package/shared/settings-data.ts +0 -6
  21. package/shared/settings-rows.tsx +1 -23
  22. package/templates/ai-chat-landing.template.mjs +1 -0
  23. package/templates/ai-chat-landing.tsx +30 -11
  24. package/templates/ai-chat.template.mjs +1 -0
  25. package/templates/ai-chat.tsx +9 -3
  26. package/templates/blank.template.mjs +1 -0
  27. package/templates/blank.tsx +4 -2
  28. package/templates/centered-hero.template.mjs +1 -0
  29. package/templates/centered-hero.tsx +5 -3
  30. package/templates/classic-gallery.template.mjs +1 -0
  31. package/templates/classic-gallery.tsx +9 -2
  32. package/templates/contact-form.template.mjs +1 -0
  33. package/templates/contact-form.tsx +13 -2
  34. package/templates/dashboard-portfolio.template.mjs +1 -0
  35. package/templates/dashboard-portfolio.tsx +21 -6
  36. package/templates/dashboard.template.mjs +1 -0
  37. package/templates/dashboard.tsx +9 -7
  38. package/templates/detail-page.template.mjs +1 -0
  39. package/templates/detail-page.tsx +28 -36
  40. package/templates/documentation-design.template.mjs +1 -0
  41. package/templates/documentation-design.tsx +8 -3
  42. package/templates/documentation-technical.template.mjs +1 -0
  43. package/templates/documentation-technical.tsx +11 -6
  44. package/templates/documentation.template.mjs +1 -0
  45. package/templates/documentation.tsx +16 -7
  46. package/templates/editor.template.mjs +1 -0
  47. package/templates/editor.tsx +4 -2
  48. package/templates/file-explorer.template.mjs +1 -0
  49. package/templates/file-explorer.tsx +4 -2
  50. package/templates/form-two-column.template.mjs +1 -0
  51. package/templates/form-two-column.tsx +19 -4
  52. package/templates/form-wizard.template.mjs +15 -0
  53. package/templates/form-wizard.tsx +878 -0
  54. package/templates/gallery-hero.template.mjs +1 -0
  55. package/templates/gallery-hero.tsx +14 -6
  56. package/templates/ide.template.mjs +1 -0
  57. package/templates/ide.tsx +187 -101
  58. package/templates/incident-console.template.mjs +1 -0
  59. package/templates/incident-console.tsx +137 -85
  60. package/templates/kanban-board.template.mjs +1 -0
  61. package/templates/kanban-board.tsx +51 -7
  62. package/templates/library.template.mjs +1 -0
  63. package/templates/library.tsx +15 -4
  64. package/templates/login-card.template.mjs +1 -0
  65. package/templates/login-card.tsx +4 -2
  66. package/templates/login-split.template.mjs +1 -0
  67. package/templates/login-split.tsx +8 -5
  68. package/templates/login-sso.template.mjs +1 -0
  69. package/templates/login-sso.tsx +13 -19
  70. package/templates/login.template.mjs +1 -0
  71. package/templates/login.tsx +4 -2
  72. package/templates/messaging-shell.template.mjs +1 -0
  73. package/templates/messaging-shell.tsx +114 -47
  74. package/templates/mixed-gallery.template.mjs +1 -0
  75. package/templates/mixed-gallery.tsx +10 -2
  76. package/templates/payment-form.template.mjs +1 -0
  77. package/templates/payment-form.tsx +34 -31
  78. package/templates/product-detail.template.mjs +1 -0
  79. package/templates/product-detail.tsx +21 -5
  80. package/templates/product-gallery.template.mjs +1 -0
  81. package/templates/product-gallery.tsx +12 -5
  82. package/templates/product-tour.template.mjs +1 -0
  83. package/templates/product-tour.tsx +21 -14
  84. package/templates/settings-dialog.template.mjs +1 -0
  85. package/templates/settings-dialog.tsx +10 -3
  86. package/templates/settings-sidebar.template.mjs +1 -0
  87. package/templates/settings-sidebar.tsx +609 -588
  88. package/templates/settings.template.mjs +1 -0
  89. package/templates/settings.tsx +11 -12
  90. package/templates/shell-nav.template.mjs +1 -0
  91. package/templates/shell-nav.tsx +47 -29
  92. package/templates/shell-side-nav.template.mjs +1 -0
  93. package/templates/shell-side-nav.tsx +88 -70
  94. package/templates/shell-top-nav.template.mjs +1 -0
  95. package/templates/shell-top-nav.tsx +102 -88
  96. package/templates/side-gallery.template.mjs +1 -0
  97. package/templates/side-gallery.tsx +4 -2
  98. package/templates/table-grouped.template.mjs +1 -0
  99. package/templates/table-grouped.tsx +150 -134
  100. package/templates/table-page-chart.template.mjs +1 -0
  101. package/templates/table-page-chart.tsx +4 -2
  102. package/templates/table-page-heatmap-status.template.mjs +1 -0
  103. package/templates/table-page-heatmap-status.tsx +4 -2
  104. package/templates/{table-page-shoe-store-heatmap.template.mjs → table-page-shoe-store.template.mjs} +1 -0
  105. package/templates/{table-page-shoe-store-heatmap.tsx → table-page-shoe-store.tsx} +5 -3
  106. package/templates/table-page.template.mjs +1 -0
  107. package/templates/table-page.tsx +12 -2
  108. package/templates/table-tree.template.mjs +16 -0
  109. package/templates/table-tree.tsx +707 -0
  110. package/templates/table.template.mjs +1 -0
  111. package/templates/table.tsx +4 -2
  112. package/templates/tech-report.template.mjs +1 -0
  113. package/templates/tech-report.tsx +18 -13
  114. package/templates/theme-showcase.template.mjs +1 -0
  115. package/templates/theme-showcase.tsx +41 -28
  116. package/templates/work-item-detail.template.mjs +8 -0
  117. package/templates/work-item-detail.tsx +1049 -0
  118. package/theme.css +71 -78
  119. package/theme.css.d.ts +9 -0
  120. package/tokens.css +79 -46
@@ -8,7 +8,6 @@ import { VStack } from "@astryxdesign/core/Layout";
8
8
  import { Link } from "@astryxdesign/core/Link";
9
9
  import { Heading, Text } from "@astryxdesign/core/Text";
10
10
  import { TextInput } from "@astryxdesign/core/TextInput";
11
- import { inputAutoComplete } from "astryx-dracula/shared/auth-chrome-config";
12
11
  import {
13
12
  AUTH_BRAND_NAME,
14
13
  AUTH_EMAIL_PLACEHOLDER,
@@ -116,7 +115,7 @@ export function AuthLoginFields({
116
115
  onChange={onEmailChange}
117
116
  placeholder={AUTH_EMAIL_PLACEHOLDER}
118
117
  type="email"
119
- {...inputAutoComplete("email")}
118
+ autoComplete="email"
120
119
  size="lg"
121
120
  onEnter={onSubmit}
122
121
  status={error ? { type: "error", message: error } : undefined}
@@ -129,7 +128,7 @@ export function AuthLoginFields({
129
128
  onChange={onPasswordChange}
130
129
  placeholder={AUTH_PASSWORD_PLACEHOLDER}
131
130
  type="password"
132
- {...inputAutoComplete("current-password")}
131
+ autoComplete="current-password"
133
132
  size="lg"
134
133
  onEnter={onSubmit}
135
134
  status={error ? { type: "error", message: error } : undefined}
@@ -57,11 +57,12 @@ export const outlinePanel: CSSProperties = {
57
57
  top: "var(--spacing-6)",
58
58
  alignSelf: "start",
59
59
  // No paddingBlockStart. This object is applied via `style=` to the outline
60
- // LayoutPanels (chaptered-doc.tsx:316, documentation-design.tsx:592,
61
- // documentation-technical.tsx:82), and an inline style prop BEATS the stylex
60
+ // LayoutPanels, and an inline style prop BEATS the stylex
62
61
  // class -- so this 8px silently overrode the panel's own 16px block padding
63
62
  // at the top edge only, leaving every "On this page" rail in the kit
64
63
  // asymmetric by 8px. The panel already owns its block padding.
64
+ // Call sites are named by file, not by line: line numbers in a comment go
65
+ // stale the first time anyone edits above them, and these three did.
65
66
  };
66
67
 
67
68
  // Derived once per page so the Outline receives a stable array identity
@@ -11,7 +11,6 @@ import { Button } from "@astryxdesign/core/Button";
11
11
  import { CodeBlock } from "@astryxdesign/core/CodeBlock";
12
12
  import { Divider } from "@astryxdesign/core/Divider";
13
13
  import { useMediaQuery } from "@astryxdesign/core/hooks";
14
- import type { IconType } from "@astryxdesign/core/Icon";
15
14
  import { Icon } from "@astryxdesign/core/Icon";
16
15
  import {
17
16
  HStack,
@@ -39,7 +38,7 @@ import {
39
38
  sectionId,
40
39
  } from "astryx-dracula/shared/chaptered-doc-config";
41
40
  import { ChevronLeft, ChevronRight } from "lucide-react";
42
- import { type CSSProperties, type ReactNode, useState } from "react";
41
+ import { type ReactNode, useState } from "react";
43
42
 
44
43
  function ChapterRail({
45
44
  groups,
@@ -5,18 +5,33 @@
5
5
  // Purple is deliberately absent: purple means tappable, so it must never encode
6
6
  // data. `muted` is a COMMENT-blue grey because it backs the `{label: 'Other'}`
7
7
  // legend row (dashboard.tsx:229, :253) -- a labelled data category, not chrome.
8
+ //
9
+ // Every var() carries an explicit fallback equal to the theme's own pin for
10
+ // that token. Core moved the canonical `--color-data-*` defaults out of the
11
+ // generated theme CSS and into its own dataVars group, so a bare
12
+ // `var(--color-data-*)` is no longer guaranteed to resolve: the theme's pin is
13
+ // the only thing painting it. The fallback is the same value, so a theme that
14
+ // drops the token still paints the right colour instead of nothing. The
15
+ // fallback-vs-pin sync is enforced by scripts/check.ts, so the two cannot drift.
8
16
 
9
17
  export const CHART_HUES = {
10
- cyan: "var(--color-data-categorical-cyan)",
11
- orange: "var(--color-data-categorical-orange)",
12
- green: "var(--color-data-categorical-green)",
13
- pink: "var(--color-data-categorical-pink)",
14
- muted: "var(--color-data-categorical-blue)",
18
+ cyan: "var(--color-data-categorical-cyan, #8BE9FD)",
19
+ orange: "var(--color-data-categorical-orange, #FFB86C)",
20
+ green: "var(--color-data-categorical-green, #50FA7B)",
21
+ pink: "var(--color-data-categorical-pink, #FF79C6)",
22
+ muted: "var(--color-data-categorical-blue, #6272A4)",
15
23
  // Added because --color-data-categorical-red exists in the palette
16
24
  // (tokens.css:276) and heat maps need a severity colour, but no module
17
25
  // exposed it. Without this, a chart could not reach red through the
18
26
  // sanctioned source at all.
19
- red: "var(--color-data-categorical-red)",
27
+ red: "var(--color-data-categorical-red, #FF5555)",
20
28
  } as const;
21
29
 
22
30
  export type ChartHue = (typeof CHART_HUES)[keyof typeof CHART_HUES];
31
+
32
+ /**
33
+ * Any `--color-data-*` colour: a categorical hue, or a step of a sequential
34
+ * ramp. The fallback inside the var() is part of the string, which is why the
35
+ * template-literal type below still admits it.
36
+ */
37
+ export type DataColor = `var(--color-data-${string})`;
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { HStack, StackItem, VStack } from "@astryxdesign/core/Stack";
25
25
  import { Text } from "@astryxdesign/core/Text";
26
- import type { ChartHue } from "astryx-dracula/shared/chart-hues";
26
+ import type { ChartHue, DataColor } from "astryx-dracula/shared/chart-hues";
27
27
  import type { ReactNode } from "react";
28
28
 
29
29
  export interface ChartLegendEntry {
@@ -35,7 +35,7 @@ export interface ChartLegendEntry {
35
35
  * The surface arm is not optional: a legend whose first row is a recessive
36
36
  * surface is normal (the heatmap's zero cell), so a data-only type would
37
37
  * reject the value the heatmap is supposed to paint. */
38
- color: ChartHue | `var(--color-data-${string})` | `var(--color-background-${string})`;
38
+ color: ChartHue | DataColor | `var(--color-background-${string})`;
39
39
  }
40
40
 
41
41
  // Kept as a string-keyed fall-through on purpose, and DO NOT narrow it: call
@@ -39,7 +39,7 @@
39
39
  import { HStack, StackItem, VStack } from "@astryxdesign/core/Stack";
40
40
  import { Text } from "@astryxdesign/core/Text";
41
41
  import type { CSSProperties } from "react";
42
- import type { ChartHue } from "./chart-hues";
42
+ import type { ChartHue, DataColor } from "./chart-hues";
43
43
 
44
44
  export interface DataBarSegment {
45
45
  /** Stable key for the segment. */
@@ -48,7 +48,7 @@ export interface DataBarSegment {
48
48
  /** MUST be a --color-data-categorical-* or --color-data-<family>-N var.
49
49
  * Never a raw hex, never a --dracula-* primitive. Typed rather than
50
50
  * documented: `string` made the rule a comment nobody could break. */
51
- color: ChartHue | `var(--color-data-${string})`;
51
+ color: ChartHue | DataColor;
52
52
  }
53
53
 
54
54
  /** Minimum segment share, so a tiny non-zero value stays visible. */
@@ -1,5 +1,5 @@
1
1
  // Parametric order desk shared by the table-page-chart (Matcha Bar) and
2
- // table-page-shoe-store-heatmap (Midnight Kicks) templates. The two pages are
2
+ // table-page-shoe-store (Midnight Kicks) templates. The two pages are
3
3
  // one archetype — revenue line over the order log — differing only in catalogue,
4
4
  // rows, and chart scale, so the frame/columns live here once.
5
5
 
@@ -37,7 +37,7 @@ import type { CSSProperties } from "react";
37
37
  // minHeight appears in none of them.
38
38
  //
39
39
  // The wrapper is shared by all three callers (table-page-chart.tsx:410,
40
- // table-page-shoe-store-heatmap.tsx:759, and this module's own default), so
40
+ // table-page-shoe-store.tsx:759, and this module's own default), so
41
41
  // anchoring it here covers them together. Anchoring inside each caller instead
42
42
  // would put a 100dvh box inside this one and overflow by the header height.
43
43
  //
@@ -200,63 +200,61 @@ export function OrderDesk({
200
200
  gridTicks: number[];
201
201
  }) {
202
202
  return (
203
- // The anchor lives on a page-owned wrapper, never on the Layout. A bare
204
- // `height="fill"` resolves against `min-height: 100%`, which collapses when
205
- // the host is content-sized — and templates ship standalone, so that is the
206
- // common case. Putting the height on the Layout instead would trade
207
- // a soft failure (the document scrolls) for a hard one (content clipped,
208
- // because `LayoutContent` is `overflow: auto` with a definite height).
209
- <div style={pageStyle}>
210
- <Layout
211
- height="fill"
212
- header={
213
- <LayoutHeader hasDivider padding={6}>
214
- <HStack gap={2} vAlign="center">
215
- <StackItem size="fill">
216
- {/* maxLines={1}, not wrap="wrap": at 390 the 35px display-2 title
203
+ // Templates ship standalone, so the page owns the definite ancestor for
204
+ // `height="fill"` — 100dvh on the Layout itself, the same anchor every
205
+ // other page uses. It used to sit on a wrapper <div>, which contradicted
206
+ // the no-raw-div rule for a reason that does not hold: the height lands on
207
+ // the same box either way.
208
+ <Layout
209
+ style={pageStyle}
210
+ height="fill"
211
+ header={
212
+ <LayoutHeader hasDivider padding={6}>
213
+ <HStack gap={2} vAlign="center">
214
+ <StackItem size="fill">
215
+ {/* maxLines={1}, not wrap="wrap": at 390 the 35px display-2 title
217
216
  wraps 2-3 lines in a 342px header (measured: 136px tall for
218
217
  "Midnight Kicks", 180px for an 18-char title). Wrapping the
219
218
  row measured IDENTICAL header heights in both configurations
220
219
  — the controls already fit, the title is what is tall. And
221
220
  maxLines wires core's useTruncation, so the full value stays
222
221
  reachable in a Tooltip (SC 1.4.10, "truncation must reveal"). */}
223
- <Heading level={1} type="display-2" maxLines={1}>
224
- {title}
225
- </Heading>
226
- </StackItem>
227
- <IconButton
228
- label="Filter"
229
- icon={<Icon icon={Filter} size="sm" />}
230
- variant="ghost"
231
- tooltip="Filter"
232
- />
233
- <IconButton
234
- label="Export"
235
- icon={<Icon icon={Download} size="sm" />}
236
- variant="ghost"
237
- tooltip="Export"
238
- />
239
- <Button label="New order" variant="primary" icon={<Icon icon={Plus} size="sm" />} />
240
- </HStack>
241
- </LayoutHeader>
242
- }
243
- content={
244
- <LayoutContent padding={3}>
245
- <VStack gap={4}>
246
- <RevenueChart data={revenueData} chartMax={chartMax} gridTicks={gridTicks} />
247
- <Table<OrderDeskRow>
248
- data={orders}
249
- columns={columnsFor(selfHash, products)}
250
- idKey="id"
251
- density="balanced"
252
- dividers="rows"
253
- textOverflow="truncate"
254
- hasHover
255
- />
256
- </VStack>
257
- </LayoutContent>
258
- }
259
- />
260
- </div>
222
+ <Heading level={1} type="display-2" maxLines={1}>
223
+ {title}
224
+ </Heading>
225
+ </StackItem>
226
+ <IconButton
227
+ label="Filter"
228
+ icon={<Icon icon={Filter} size="sm" />}
229
+ variant="ghost"
230
+ tooltip="Filter"
231
+ />
232
+ <IconButton
233
+ label="Export"
234
+ icon={<Icon icon={Download} size="sm" />}
235
+ variant="ghost"
236
+ tooltip="Export"
237
+ />
238
+ <Button label="New order" variant="primary" icon={<Icon icon={Plus} size="sm" />} />
239
+ </HStack>
240
+ </LayoutHeader>
241
+ }
242
+ content={
243
+ <LayoutContent padding={3}>
244
+ <VStack gap={4}>
245
+ <RevenueChart data={revenueData} chartMax={chartMax} gridTicks={gridTicks} />
246
+ <Table<OrderDeskRow>
247
+ data={orders}
248
+ columns={columnsFor(selfHash, products)}
249
+ idKey="id"
250
+ density="balanced"
251
+ dividers="rows"
252
+ textOverflow="truncate"
253
+ hasHover
254
+ />
255
+ </VStack>
256
+ </LayoutContent>
257
+ }
258
+ />
261
259
  );
262
260
  }
@@ -169,7 +169,7 @@ export function RevenueChart({
169
169
  y1={y}
170
170
  x2={CHART_W - CHART_PAD_RIGHT}
171
171
  y2={y}
172
- stroke="var(--color-separator)"
172
+ stroke="var(--color-graph-gridlines)"
173
173
  strokeDasharray={tick === 0 ? undefined : "3 3"}
174
174
  opacity={tick === 0 ? 1 : 0.5}
175
175
  />
@@ -23,13 +23,13 @@ const CARD_HILLS = {
23
23
 
24
24
  const TALL_HILL = "M0 380 Q200 320 400 370 V500 H0 Z" as const;
25
25
 
26
- function WideCastle() {
26
+ function WideCastle({ label }: { label?: string }) {
27
27
  return (
28
28
  <svg
29
29
  style={galleryImage}
30
30
  viewBox="0 0 800 450"
31
31
  role="img"
32
- aria-label="Moonlit castle on a hill in Dracula theme colors"
32
+ aria-label={label ?? "Moonlit castle on a hill in Dracula theme colors"}
33
33
  >
34
34
  <rect width="800" height="450" fill="var(--dracula-bg-dark)" />
35
35
  {/* Stars */}
@@ -95,13 +95,13 @@ function WideCastle() {
95
95
  );
96
96
  }
97
97
 
98
- function CardCastle() {
98
+ function CardCastle({ label }: { label?: string }) {
99
99
  return (
100
100
  <svg
101
101
  style={galleryImage}
102
102
  viewBox="0 0 400 500"
103
103
  role="img"
104
- aria-label="Moonlit castle on midnight hills"
104
+ aria-label={label ?? "Moonlit castle on midnight hills"}
105
105
  >
106
106
  <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
107
107
  {/* Stars */}
@@ -142,13 +142,13 @@ function CardCastle() {
142
142
  );
143
143
  }
144
144
 
145
- function TallPines() {
145
+ function TallPines({ label }: { label?: string }) {
146
146
  return (
147
147
  <svg
148
148
  style={galleryImage}
149
149
  viewBox="0 0 400 500"
150
150
  role="img"
151
- aria-label="Harvest moon over midnight pines"
151
+ aria-label={label ?? "Harvest moon over midnight pines"}
152
152
  >
153
153
  <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
154
154
  {/* Stars */}
@@ -186,14 +186,14 @@ function TallPines() {
186
186
  );
187
187
  }
188
188
 
189
- function Rooftops() {
189
+ function Rooftops({ label }: { label?: string }) {
190
190
  return (
191
191
  <svg
192
192
  style={galleryImage}
193
193
  viewBox="0 0 400 300"
194
194
  preserveAspectRatio="xMidYMid slice"
195
195
  role="img"
196
- aria-label="Harvest moon over the castle rooftops"
196
+ aria-label={label ?? "Harvest moon over the castle rooftops"}
197
197
  >
198
198
  <rect width="400" height="300" fill="var(--dracula-bg)" />
199
199
  <g fill="var(--dracula-fg)">
@@ -229,9 +229,17 @@ function Rooftops() {
229
229
  );
230
230
  }
231
231
 
232
- export function SceneCastle({ variant }: { variant: SceneCastleVariant }) {
233
- if (variant === "wide") return <WideCastle />;
234
- if (variant === "card") return <CardCastle />;
235
- if (variant === "rooftops") return <Rooftops />;
236
- return <TallPines />;
232
+ export function SceneCastle({
233
+ variant,
234
+ label,
235
+ }: {
236
+ variant: SceneCastleVariant;
237
+ // Callers that keep their own alt text pass it here, so the accessible name
238
+ // cannot drift from the data the way a hardcoded label lets it.
239
+ label?: string;
240
+ }) {
241
+ if (variant === "wide") return <WideCastle label={label} />;
242
+ if (variant === "card") return <CardCastle label={label} />;
243
+ if (variant === "rooftops") return <Rooftops label={label} />;
244
+ return <TallPines label={label} />;
237
245
  }
@@ -12,18 +12,12 @@ import {
12
12
  Briefcase,
13
13
  CreditCard,
14
14
  FileText,
15
- Fingerprint,
16
15
  Globe,
17
- KeyRound,
18
- Laptop,
19
16
  Lock,
20
17
  Share2,
21
18
  ShieldCheck,
22
- Smartphone,
23
19
  SquarePen,
24
- Tablet,
25
20
  User,
26
- UserCog,
27
21
  } from "lucide-react";
28
22
  import type { CSSProperties } from "react";
29
23
 
@@ -8,29 +8,7 @@ import { Divider } from "@astryxdesign/core/Divider";
8
8
  import { HStack, VStack } from "@astryxdesign/core/Layout";
9
9
  import { Link } from "@astryxdesign/core/Link";
10
10
  import { Text } from "@astryxdesign/core/Text";
11
- import {
12
- actionNoWrap,
13
- DEVICE_ROWS,
14
- type DeviceRow,
15
- INFO_TILES,
16
- type InfoRow,
17
- type InfoTileData,
18
- iconBox,
19
- LOGIN_ROWS,
20
- type SettingsNavItem,
21
- SOCIAL_ROWS,
22
- } from "astryx-dracula/shared/settings-data";
23
- import {
24
- Bell,
25
- Briefcase,
26
- CreditCard,
27
- FileText,
28
- Globe,
29
- Lock,
30
- Share2,
31
- ShieldCheck,
32
- SquarePen,
33
- } from "lucide-react";
11
+ import { actionNoWrap, type InfoRow } from "astryx-dracula/shared/settings-data";
34
12
  import type { CSSProperties, ReactNode } from "react";
35
13
 
36
14
  // ─── Row components ──────────────────────────────────────────────────────────
@@ -2,5 +2,6 @@ export default {
2
2
  type: "page",
3
3
  name: "AI Chat Landing",
4
4
  description: "Composer, greeting, and category toggles",
5
+ keywords: ["ai chat", "assistant", "composer", "landing", "chat", "empty state"],
5
6
  category: "marketing",
6
7
  };
@@ -1,6 +1,8 @@
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
7
  import {
6
8
  ChatComposer,
@@ -215,15 +217,23 @@ function makeTrigger<TAux>({
215
217
  items,
216
218
  getDescription,
217
219
  toToken,
220
+ hasMultiWordQuery,
221
+ emptySearchText,
218
222
  }: {
219
223
  character: string;
220
224
  items: SearchableItem<TAux>[];
221
225
  getDescription: (aux: TAux | undefined) => string | undefined;
222
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;
223
231
  }): ChatComposerTrigger {
224
232
  return {
225
233
  character,
226
234
  searchSource: createStaticSource(items),
235
+ ...(hasMultiWordQuery ? { hasMultiWordQuery } : {}),
236
+ ...(emptySearchText ? { emptySearchText } : {}),
227
237
  renderItem: (item) => (
228
238
  <TypeaheadItem
229
239
  item={item}
@@ -238,6 +248,13 @@ const mentionTrigger = makeTrigger({
238
248
  character: "@",
239
249
  items: MENTION_ITEMS,
240
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.",
241
258
  toToken: (item) => ({
242
259
  value: `@${item.id}`,
243
260
  label: item.label,
@@ -257,6 +274,11 @@ const commandTrigger = makeTrigger({
257
274
  });
258
275
 
259
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.
260
282
 
261
283
  /** Composer attachment. The name is user-facing; the id is the React key, since
262
284
  * two dropped files can share a name. */
@@ -273,15 +295,10 @@ const INITIAL_ATTACHMENTS: Attachment[] = [
273
295
  "dracula_spec.pdf",
274
296
  ].map((name) => ({ id: crypto.randomUUID(), name }));
275
297
 
276
- // The composer's imperative insert methods mutate the DOM without emitting a
277
- // change, so dispatch an input event to sync its value and clear the placeholder.
278
- const syncComposerValue = () => {
279
- document.activeElement?.dispatchEvent(new Event("input", { bubbles: true }));
280
- };
281
-
282
298
  // One inserter for every composer write. `insertText` replaces the selection
283
299
  // (suggestion prompts); `insertToken` inserts a chip at the cursor without
284
- // touching the selection (mentions, mode tokens).
300
+ // touching the selection (mentions, mode tokens). Both emit a change in 0.7.10,
301
+ // so no input-event sync is needed after either call.
285
302
  function insertIntoComposer(
286
303
  input: ChatComposerInputHandle | null,
287
304
  write: { text: string } | { token: ChatComposerToken },
@@ -305,7 +322,6 @@ function insertIntoComposer(
305
322
  } else {
306
323
  input.insertToken(write.token);
307
324
  }
308
- syncComposerValue();
309
325
  }
310
326
 
311
327
  // Main component
@@ -513,7 +529,10 @@ export default function AiChatLanding() {
513
529
  <ClickableCard
514
530
  key={suggestion.heading}
515
531
  label={suggestion.heading}
516
- variant="muted"
532
+ // Raised tappable card. `muted` is a recessed well since
533
+ // the 2026-11 token change, so it would read as a hole
534
+ // in the page rather than a prompt you can press.
535
+ variant="default"
517
536
  padding={4}
518
537
  onClick={() => {
519
538
  applySuggestion(suggestion.prompt);
@@ -2,5 +2,6 @@ export default {
2
2
  type: "page",
3
3
  name: "AI Chat",
4
4
  description: "Threaded assistant chat with design-doc artifact",
5
+ keywords: ["ai chat", "assistant", "conversation", "transcript", "chat", "artifact"],
5
6
  category: "tooling",
6
7
  };
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC[p=0] > H > (SI[fill] > ChL > ChML > (ChM > ChB)*4 + ChC"Ask a follow up...") + (C.transparent > Tbar + S.section > MD) ;; Dlg#artifact[variant=fullscreen] > L > (DH"JWT Token Refresh" + (LC[p=0] > S.section > MD))
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 > LC[p=0] > H > (V > ChL > (VH"Night thread"[as=h1] + ChML > (ChSM"Today" + (ChM > ChBu)*8 + ChSM"Changes sealed in the crypt") + ChC"Ask the night anything...")) + (RH + C.transparent > Tbar + S.transparent > (V[g=2] > Hd"JWT Token Refresh: Design & Rollout"[level=2 t=display-2] + MD)) ;; Dlg#artifact[variant=fullscreen] > L > (DH"JWT Token Refresh" + (LC[p=0] > S.transparent > (V[g=2] > Hd"JWT Token Refresh: Design & Rollout"[level=2 t=display-2] + MD)))
4
6
 
5
7
  import { Avatar } from "@astryxdesign/core/Avatar";
6
8
  import { Button } from "@astryxdesign/core/Button";
@@ -229,7 +231,9 @@ function ArtifactCard({ onOpen }: { onOpen: () => void }) {
229
231
  <ClickableCard
230
232
  label={`Open ${ARTIFACT_TITLE}`}
231
233
  onClick={onOpen}
232
- variant="muted"
234
+ // A raised tappable chip, not a well: `muted` is #21222C since the
235
+ // 2026-11 token change and would sink this card below the chat body.
236
+ variant="default"
233
237
  padding={3}
234
238
  maxWidth={360}
235
239
  style={artifactCard}
@@ -372,6 +376,7 @@ export default function AiChat() {
372
376
  </ChatMessageBubble>
373
377
  <ChatToolCalls
374
378
  defaultIsExpanded
379
+ label="Explored the auth files"
375
380
  calls={[
376
381
  {
377
382
  name: "read",
@@ -478,6 +483,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
478
483
  <ChatMessage sender="assistant" avatar={<Avatar name="Agent" size="md" />}>
479
484
  <ChatToolCalls
480
485
  defaultIsExpanded
486
+ label="Added and ran the refresh test"
481
487
  calls={[
482
488
  {
483
489
  name: "read",
@@ -2,5 +2,6 @@ export default {
2
2
  type: "page",
3
3
  name: "Blank",
4
4
  description: "Empty layout shell for a new page",
5
+ keywords: ["blank", "empty", "starter", "minimal", "placeholder"],
5
6
  category: "tooling",
6
7
  };
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L[h=auto] > LC > Hd"A blank crypt awaits its story"[level=1]
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=auto] > LC > Hd"A blank crypt awaits its story"[level=1 t=display-2]
4
6
 
5
7
  import { Layout, LayoutContent } from "@astryxdesign/core/Layout";
6
8
  import { Heading } from "@astryxdesign/core/Text";
@@ -2,5 +2,6 @@ export default {
2
2
  type: "page",
3
3
  name: "Centered Hero",
4
4
  description: "Headline, CTAs, and hero visual",
5
+ keywords: ["centered hero", "landing", "hero", "marketing", "cta", "splash"],
5
6
  category: "marketing",
6
7
  };
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC[p=6] > V[g=10] > ((V[g=6 a=center] > (V[g=3 a=center] > Hd"Small joys for creatures of the night"[level=1 t=display-2] + Tx"Sometimes all it takes is one small ritual to turn the whole night around."[t=body] + Tx"astryx-dracula v0.5.1"[t=supporting]) + (H[j=center g=3] > B.primary"Embrace the night" + B.secondary"Read the lore")) + (S.transparent[p=0] > AR[ratio=16/9]))
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=auto] > LC[p=6] > V[g=10] > ((V[g=6 a=center] > (V[g=3 a=center] > Hd"Small joys for creatures of the night"[level=1 t=display-2] + Tx"Sometimes all it takes is one small ritual to turn the whole night around."[t=body] + Tx"astryx-dracula v0.7.0"[t=supporting]) + (H[j=center g=3] > B.primary"Embrace the night" + B.secondary"Read the lore")) + (S.transparent[p=0] > AR[ratio=16/9] > Scene{SceneCastle}))
4
6
 
5
7
  import { AspectRatio } from "@astryxdesign/core/AspectRatio";
6
8
  import { Button } from "@astryxdesign/core/Button";
@@ -39,7 +41,7 @@ export default function CenteredHero() {
39
41
  Sometimes all it takes is one small ritual to turn the whole night around.
40
42
  </Text>
41
43
  <Text type="supporting" color="secondary" justify="center" hasTabularNumbers>
42
- astryx-dracula v0.5.1
44
+ astryx-dracula v0.7.0
43
45
  </Text>
44
46
  </VStack>
45
47
  <HStack gap={3} wrap="wrap" justify="center">
@@ -2,5 +2,6 @@ export default {
2
2
  type: "page",
3
3
  name: "Classic Gallery",
4
4
  description: "Filterable image wall with category tabs",
5
+ keywords: ["gallery", "collection", "grid", "art", "images", "portfolio"],
5
6
  category: "marketing",
6
7
  };