astryx-dracula 0.6.2 → 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 (59) hide show
  1. package/BRAND.md +54 -3
  2. package/README.md +11 -4
  3. package/USAGE.md +1 -1
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +26 -4
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/package.json +11 -11
  8. package/shared/auth-chrome-config.ts +0 -4
  9. package/shared/auth-chrome.tsx +2 -3
  10. package/shared/chaptered-doc-config.ts +3 -2
  11. package/shared/order-desk.tsx +52 -54
  12. package/shared/revenue-chart.tsx +1 -1
  13. package/shared/scene-castle.tsx +21 -13
  14. package/templates/ai-chat-landing.tsx +6 -9
  15. package/templates/ai-chat.tsx +5 -1
  16. package/templates/centered-hero.tsx +2 -2
  17. package/templates/classic-gallery.tsx +5 -0
  18. package/templates/contact-form.tsx +9 -0
  19. package/templates/dashboard-portfolio.tsx +17 -4
  20. package/templates/dashboard.tsx +5 -5
  21. package/templates/detail-page.tsx +20 -28
  22. package/templates/documentation-design.tsx +3 -0
  23. package/templates/documentation-technical.tsx +8 -5
  24. package/templates/documentation.tsx +13 -6
  25. package/templates/form-two-column.tsx +11 -2
  26. package/templates/form-wizard.template.mjs +15 -0
  27. package/templates/form-wizard.tsx +878 -0
  28. package/templates/gallery-hero.tsx +10 -4
  29. package/templates/ide.tsx +177 -96
  30. package/templates/incident-console.tsx +15 -5
  31. package/templates/kanban-board.tsx +46 -4
  32. package/templates/library.tsx +12 -3
  33. package/templates/login-split.tsx +3 -3
  34. package/templates/login-sso.tsx +9 -17
  35. package/templates/messaging-shell.tsx +109 -46
  36. package/templates/mixed-gallery.tsx +6 -0
  37. package/templates/payment-form.tsx +29 -28
  38. package/templates/product-detail.tsx +12 -3
  39. package/templates/product-gallery.tsx +8 -3
  40. package/templates/product-tour.tsx +11 -7
  41. package/templates/settings-dialog.tsx +5 -0
  42. package/templates/settings-sidebar.tsx +603 -584
  43. package/templates/settings.tsx +5 -2
  44. package/templates/shell-nav.tsx +23 -3
  45. package/templates/shell-side-nav.tsx +94 -81
  46. package/templates/shell-top-nav.tsx +98 -86
  47. package/templates/table-grouped.tsx +146 -132
  48. package/templates/{table-page-shoe-store-heatmap.template.mjs → table-page-shoe-store.template.mjs} +1 -1
  49. package/templates/{table-page-shoe-store-heatmap.tsx → table-page-shoe-store.tsx} +1 -1
  50. package/templates/table-page.tsx +8 -0
  51. package/templates/table-tree.template.mjs +16 -0
  52. package/templates/table-tree.tsx +707 -0
  53. package/templates/tech-report.tsx +8 -5
  54. package/templates/theme-showcase.tsx +18 -11
  55. package/templates/work-item-detail.template.mjs +8 -0
  56. package/templates/work-item-detail.tsx +1049 -0
  57. package/theme.css +38 -3
  58. package/theme.css.d.ts +2 -2
  59. package/tokens.css +41 -4
@@ -31,6 +31,10 @@ const frameClip: CSSProperties = {
31
31
  const textCenter: CSSProperties = {
32
32
  textAlign: "center",
33
33
  };
34
+ // Definite ancestor for `Layout height="fill"`: 100dvh, not minHeight: '100%'
35
+ // (a percentage min-height against an indefinite block computes to 0 — CSS 2.1
36
+ // 10.5). Same shape as side-gallery.tsx.
37
+ const pageStyle: CSSProperties = { height: "100dvh" };
34
38
 
35
39
  // ─── Gallery Data ───────────────────────────────────────────────────────────
36
40
 
@@ -80,6 +84,7 @@ export default function ClassicGallery() {
80
84
 
81
85
  return (
82
86
  <Layout
87
+ style={pageStyle}
83
88
  height="fill"
84
89
  content={
85
90
  <LayoutContent padding={0}>
@@ -146,6 +146,7 @@ export default function ContactForm() {
146
146
  label="Full name"
147
147
  isRequired
148
148
  placeholder="Full name"
149
+ autoComplete="name"
149
150
  value={fullName}
150
151
  onChange={setFullName}
151
152
  status={errors.fullName ? { type: "error", message: errors.fullName } : undefined}
@@ -153,7 +154,9 @@ export default function ContactForm() {
153
154
  <TextInput
154
155
  label="Email"
155
156
  isRequired
157
+ type="email"
156
158
  placeholder="you@company.com"
159
+ autoComplete="email"
157
160
  value={email}
158
161
  onChange={setEmail}
159
162
  status={errors.email ? { type: "error", message: errors.email } : undefined}
@@ -164,6 +167,7 @@ export default function ContactForm() {
164
167
  label="Company"
165
168
  isRequired
166
169
  placeholder="Company"
170
+ autoComplete="organization"
167
171
  value={company}
168
172
  onChange={setCompany}
169
173
  status={errors.company ? { type: "error", message: errors.company } : undefined}
@@ -171,7 +175,9 @@ export default function ContactForm() {
171
175
  <TextInput
172
176
  label="Phone"
173
177
  isRequired
178
+ type="tel"
174
179
  placeholder="Phone number"
180
+ autoComplete="tel"
175
181
  value={phone}
176
182
  onChange={setPhone}
177
183
  status={errors.phone ? { type: "error", message: errors.phone } : undefined}
@@ -190,6 +196,9 @@ export default function ContactForm() {
190
196
  labelID="campaign-goals-label"
191
197
  isGroupLabel
192
198
  isRequired
199
+ // The token row is a custom, non-bordered control, so the
200
+ // message box must sit below it (attached overlaps the input).
201
+ statusVariant="detached"
193
202
  status={errors.goals ? { type: "error", message: errors.goals } : undefined}
194
203
  >
195
204
  <HStack gap={2} wrap="wrap" role="group" aria-labelledby="campaign-goals-label">
@@ -18,11 +18,13 @@ import { List, ListItem } from "@astryxdesign/core/List";
18
18
  import type { PixelWidth, TableColumn } from "@astryxdesign/core/Table";
19
19
  import { pixel, proportional } from "@astryxdesign/core/Table";
20
20
  import { Heading, Text } from "@astryxdesign/core/Text";
21
+ import { CHART_HUES } from "astryx-dracula/shared/chart-hues";
21
22
  import { ChartLabel } from "astryx-dracula/shared/chart-labels";
22
23
  import { CHART_PANEL_STYLE } from "astryx-dracula/shared/chart-panel-style";
23
24
  import { MetricCard, TableCard } from "astryx-dracula/shared/metric-card";
24
25
  import { MetricDelta } from "astryx-dracula/shared/metric-delta";
25
26
  import { Sparkline, type SparkPoint } from "astryx-dracula/shared/sparkline";
27
+ import type { CSSProperties } from "react";
26
28
 
27
29
  // ============= DATA =============
28
30
 
@@ -530,6 +532,10 @@ const PORTFOLIO_STEP = PORTFOLIO_WIDTH / portfolioBars.length;
530
532
  const portfolioYFor = (v: number) =>
531
533
  PORTFOLIO_BASE - ((v - PORTFOLIO_MIN) / (PORTFOLIO_MAX - PORTFOLIO_MIN)) * PORTFOLIO_PLOT;
532
534
  const PORTFOLIO_Y_TICKS = [200000, 240000, 280000, 320000];
535
+ // The chart annotates its own last point. Reading it from the series rather
536
+ // than restating the anchor keeps the label true when the generator's noise
537
+ // moves the closing value off the 294,200 anchor it is interpolated toward.
538
+ const PORTFOLIO_LAST = portfolioData[portfolioData.length - 1].value;
533
539
 
534
540
  function PortfolioChart() {
535
541
  return (
@@ -548,7 +554,7 @@ function PortfolioChart() {
548
554
  y1={portfolioYFor(t)}
549
555
  x2={PORTFOLIO_LEFT + PORTFOLIO_WIDTH}
550
556
  y2={portfolioYFor(t)}
551
- stroke="var(--color-separator)"
557
+ stroke="var(--color-graph-gridlines)"
552
558
  strokeDasharray="3 3"
553
559
  opacity={0.5}
554
560
  />
@@ -567,7 +573,7 @@ function PortfolioChart() {
567
573
  width={Math.max(2, PORTFOLIO_STEP - 2)}
568
574
  height={h}
569
575
  rx={4}
570
- fill="var(--dracula-green)"
576
+ fill={CHART_HUES.green}
571
577
  />
572
578
  );
573
579
  })}
@@ -583,11 +589,11 @@ function PortfolioChart() {
583
589
  ))}
584
590
  <ChartLabel
585
591
  x={PORTFOLIO_LEFT + PORTFOLIO_WIDTH - 4}
586
- y={portfolioYFor(294200) - 8}
592
+ y={portfolioYFor(PORTFOLIO_LAST) - 8}
587
593
  textAnchor="end"
588
594
  fill="var(--color-text-highlight)"
589
595
  >
590
- $294k
596
+ ${Math.round(PORTFOLIO_LAST / 1000)}k
591
597
  </ChartLabel>
592
598
  </svg>
593
599
  </Card>
@@ -732,11 +738,18 @@ function AssetRow({
732
738
  );
733
739
  }
734
740
 
741
+ // A definite ancestor for `Layout height="fill"`. 100dvh, not minHeight: '100%':
742
+ // a percentage min-height against an indefinite containing block computes to 0
743
+ // (CSS 2.1 10.5), so the document would keep the scroll instead of the content
744
+ // pane. Same shape as dashboard.tsx and heatmap-status.
745
+ const pageStyle: CSSProperties = { height: "100dvh" };
746
+
735
747
  // ============= MAIN COMPONENT =============
736
748
 
737
749
  export default function DashboardPortfolio() {
738
750
  return (
739
751
  <Layout
752
+ style={pageStyle}
740
753
  height="fill"
741
754
  content={
742
755
  <LayoutContent padding={6}>
@@ -25,7 +25,7 @@ const SELF_HASH = "#/templates/dashboard";
25
25
 
26
26
  // ============= ICONS =============
27
27
 
28
- import { CHART_HUES } from "astryx-dracula/shared/chart-hues";
28
+ import { CHART_HUES, type DataColor } from "astryx-dracula/shared/chart-hues";
29
29
  import { ChartLegend } from "astryx-dracula/shared/chart-legend";
30
30
  import { CHART_PANEL_STYLE } from "astryx-dracula/shared/chart-panel-style";
31
31
  import { BarCell, CountCell, MetricCard, TableCard } from "astryx-dracula/shared/metric-card";
@@ -460,7 +460,7 @@ function ActiveUsersChart() {
460
460
  y1="30"
461
461
  x2="520"
462
462
  y2="30"
463
- stroke="var(--color-separator)"
463
+ stroke="var(--color-graph-gridlines)"
464
464
  strokeDasharray="3 3"
465
465
  opacity={0.5}
466
466
  />
@@ -469,11 +469,11 @@ function ActiveUsersChart() {
469
469
  y1="80"
470
470
  x2="520"
471
471
  y2="80"
472
- stroke="var(--color-separator)"
472
+ stroke="var(--color-graph-gridlines)"
473
473
  strokeDasharray="3 3"
474
474
  opacity={0.5}
475
475
  />
476
- <line x1="20" y1="130" x2="520" y2="130" stroke="var(--color-separator)" />
476
+ <line x1="20" y1="130" x2="520" y2="130" stroke="var(--color-graph-gridlines)" />
477
477
  {bars.map((d, i) => {
478
478
  const hAll = (d.allUsers / max) * 110;
479
479
  const hDesktop = (d.desktop / max) * 110;
@@ -548,7 +548,7 @@ function StackedBarCard({
548
548
  data,
549
549
  }: {
550
550
  title: string;
551
- data: Array<{ label: string; value: number; color: string }>;
551
+ data: Array<{ label: string; value: number; color: DataColor }>;
552
552
  }) {
553
553
  const total = data.reduce((sum, d) => sum + d.value, 0);
554
554
  const clipId = `stacked-${title.toLowerCase().replace(/[^a-z]+/g, "-")}`;
@@ -2,10 +2,11 @@
2
2
  // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
3
  // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
4
  // human drift check — update it with the file, never alone):
5
- // L[h=fill mw=1000] > (LH[divider p=6] > V[g=3] > ((H[g=4 a=start] > (SI[fill] > V[g=2] > (Lk"All orders" + (V[g=0] > Hd"#1001"[level=1] + (H[g=1 a=center wrap] > Tx"5 ordered items"[t=supporting] + (H[g=1 a=center] > Bu + Av"Vlad Dracul" + Tx"Vlad Dracul"[t=supporting]) + (H[g=1 a=center] > Bu + SD"Unfulfilled" + Tx"Unfulfilled"[t=supporting]) + (H[g=1 a=center] > Bu + Ic + Tx"02/23/2026"[t=supporting]) + (H[g=1 a=center] > Bu + Ic + Tx"Needs attention"[t=supporting]) + (H[g=1 a=center] > Bu + Lk"See all")))) + (H[g=2] > B.secondary"Restock" + B.secondary"Edit")) + (H[g=2] > (SI[fill] > V[a=stretch] > B.secondary"Restock") + (SI[fill] > V[a=stretch] > B.secondary"Edit")) + (H[a=center] > (SI[fill] > TL[size=lg] > (Tab"Details"! + Tab"Invoices" + Tab"Timeline" + TabMenu"More")) + B.ghost))) + (LC > V[g=4] > ((S > V[g=4] > ((H[g=2 a=center wrap] > (SI[fill] > (H[g=2 a=center] > Hd"Items"[level=2] + SD"Unfulfilled" + Tx"Unfulfilled"[t=supporting])) + (H[g=2] > B.ghost"Fulfill item" + B.secondary"Create label")) + (UL[density=spacious edge=outset] > LI*5)) + (S > V[g=4] > ((H[g=2 a=center wrap] > (SI[fill] > (H[g=2 a=center] > Hd"Invoice"[level=2] + SD"Paid" + Tx"Paid"[t=supporting])) + (H[g=2] > B.ghost"Refund" + B.secondary"Send Invoice")) + (ML > MLI*5) + D + (ML > MLI*1))) + (S > V[g=4] > ((H[g=2 a=center] > (SI[fill] > Hd"Timeline"[level=2]) + B.ghost"Filters") + (UL[density=spacious edge=outset] > LI*4)))) + (LP[w=320 p=4] > V[g=4] > Col"Notes" + Col"Customer" + Col"Fraud analysis")) ;; Dlg[variant=fullscreen] > L[h=auto] > (DH"Order details" + (LC[p=4] > V[g=4] > Col"Notes" + Col"Customer" + Col"Fraud analysis"))
5
+ // L[h=fill mw=1000] > (LH[divider p=6 pb=0] > V[g=3] > ((H[g=4 a=start] > (SI[fill] > V[g=2] > (Lk"All orders" + (V[g=0] > Hd"#1001"[level=1] + (H[g=1 a=center wrap] > Tx"5 ordered items"[t=supporting] + (H[g=1 a=center] > Bu + Av"Vlad Dracul" + Tx"Vlad Dracul"[t=supporting]) + (H[g=1 a=center] > Bu + SD"Unfulfilled" + Tx"Unfulfilled"[t=supporting]) + (H[g=1 a=center] > Bu + Ic + Tx"02/23/2026"[t=supporting]) + (H[g=1 a=center] > Bu + Ic + Tx"Needs attention"[t=supporting]) + (H[g=1 a=center] > Bu + Lk"See all")))) + (H[g=2] > B.secondary"Restock" + B.secondary"Edit")) + (H[g=2] > (SI[fill] > V[a=stretch] > B.secondary"Restock") + (SI[fill] > V[a=stretch] > B.secondary"Edit")) + (H[a=center] > (SI[fill] > TL[size=lg fullBleed] > (Tab"Details"! + Tab"Invoices" + Tab"Timeline" + TabMenu"More")) + B.ghost))) + (LC > V[g=4] > ((S > V[g=4] > ((H[g=2 a=center wrap] > (SI[fill] > (H[g=2 a=center] > Hd"Items"[level=2] + SD"Unfulfilled" + Tx"Unfulfilled"[t=supporting])) + (H[g=2] > B.ghost"Fulfill item" + B.secondary"Create label")) + (UL[density=spacious edge=outset] > LI*5)) + (S > V[g=4] > ((H[g=2 a=center wrap] > (SI[fill] > (H[g=2 a=center] > Hd"Invoice"[level=2] + SD"Paid" + Tx"Paid"[t=supporting])) + (H[g=2] > B.ghost"Refund" + B.secondary"Send Invoice")) + (ML > MLI*5) + D + (ML > MLI*1))) + (S > V[g=4] > ((H[g=2 a=center] > (SI[fill] > Hd"Timeline"[level=2]) + B.ghost"Filters") + (UL[density=spacious edge=outset] > LI*4)))) + (LP[w=320 p=4] > V[g=4] > Col"Notes" + Col"Customer" + Col"Fraud analysis")) ;; Dlg[variant=fullscreen] > L[h=auto] > (DH"Order details" + (LC[p=4] > V[g=4] > Col"Notes" + Col"Customer" + Col"Fraud analysis"))
6
6
 
7
7
  import { Avatar } from "@astryxdesign/core/Avatar";
8
8
  import { Button } from "@astryxdesign/core/Button";
9
+ import { Center } from "@astryxdesign/core/Center";
9
10
  import { Collapsible } from "@astryxdesign/core/Collapsible";
10
11
  import { Dialog, DialogHeader } from "@astryxdesign/core/Dialog";
11
12
  import { Divider } from "@astryxdesign/core/Divider";
@@ -43,23 +44,20 @@ import {
43
44
  } from "lucide-react";
44
45
  // ─── Styles ─────────────────────────────────────────────────────────────────
45
46
  import type { CSSProperties } from "react";
46
- import { useState } from "react";
47
47
 
48
- // The only custom CSS in this template is small optical-alignment negative
49
- // margins: LayoutHeader/TabList have no edge-dock prop (#2622) and List
50
- // has no "bleed to container edge" prop (#2626). Everything else uses props.
51
- // Plain inline styles — no StyleX compiler required.
48
+ // Definite ancestor for `height="fill"`: 100dvh, not minHeight: '100%' (a
49
+ // percentage min-height against an indefinite block computes to 0 — CSS 2.1
50
+ // 10.5). Templates ship standalone, so the page owns this, not the host.
51
+ const pageStyle: CSSProperties = { height: "100dvh" };
52
+
53
+ import { useState } from "react";
52
54
 
53
- // Bleed the tab bar to the header's content edges so the active-tab underline
54
- // meets the header divider. No edge-dock prop on TabList (#2622).
55
- //
56
- // These are written as spacing-token arithmetic, not raw pixels: core's
57
- // --spacing-* scale is `rem` since 0.7.0, so a literal -12px stops matching
58
- // the padding it cancels at any root font size other than 16px.
59
- const tabsRow: CSSProperties = {
60
- marginInline: "calc(var(--spacing-3) * -1)",
61
- marginBottom: "calc(var(--spacing-4) * -1)",
62
- marginTop: "var(--spacing-3)",
55
+ // The only custom CSS in this template is the sigil plate's shrink rule: the
56
+ // List row supplies the box, and the plate must not squash beside a long name.
57
+ // Everything else uses props.
58
+ const sigilClip: CSSProperties = {
59
+ flexShrink: 0,
60
+ overflow: "clip",
63
61
  };
64
62
 
65
63
  // ─── Product data ───────────────────────────────────────────────────────────
@@ -111,17 +109,10 @@ const PRODUCT_HUES: readonly SceneHue[] = [
111
109
  "var(--dracula-green)",
112
110
  ];
113
111
 
114
- const sigilClip: CSSProperties = {
115
- width: 40,
116
- height: 40,
117
- overflow: "clip",
118
- flexShrink: 0,
119
- };
120
-
121
112
  function ProductSigil({ index }: { name: string; index: number }) {
122
113
  const hue = PRODUCT_HUES[index % PRODUCT_HUES.length];
123
114
  return (
124
- <div style={sigilClip}>
115
+ <Center width={40} height={40} style={sigilClip}>
125
116
  <SceneFrame
126
117
  label={`${PRODUCTS[index % PRODUCTS.length].name} sigil`}
127
118
  viewBox="0 0 40 40"
@@ -132,7 +123,7 @@ function ProductSigil({ index }: { name: string; index: number }) {
132
123
  <circle cx={25} cy={9} r={5.5} fill="var(--dracula-bg-light)" opacity={0.5} />
133
124
  <path d="M0 28 Q10 22 20 26 T40 24 V40 H0 Z" fill="var(--dracula-current-line)" />
134
125
  </SceneFrame>
135
- </div>
126
+ </Center>
136
127
  );
137
128
  }
138
129
 
@@ -199,7 +190,7 @@ function PageHeader({
199
190
  isNarrow: boolean;
200
191
  }) {
201
192
  return (
202
- <LayoutHeader hasDivider padding={6}>
193
+ <LayoutHeader hasDivider padding={6} paddingBlockEnd={0}>
203
194
  <VStack gap={3}>
204
195
  <HStack gap={4} vAlign="start">
205
196
  <StackItem size="fill">
@@ -284,9 +275,9 @@ function PageHeader({
284
275
  </HStack>
285
276
  )}
286
277
 
287
- <HStack vAlign="center" style={tabsRow}>
278
+ <HStack vAlign="center">
288
279
  <StackItem size="fill">
289
- <TabList value={activeTab} onChange={onTabChange} size="lg">
280
+ <TabList value={activeTab} onChange={onTabChange} size="lg" isFullBleed>
290
281
  <Tab value="details" label="Details" />
291
282
  <Tab value="invoices" label="Invoices" />
292
283
  <Tab value="timeline" label="Timeline" />
@@ -601,6 +592,7 @@ export default function DetailPage() {
601
592
  return (
602
593
  <>
603
594
  <Layout
595
+ style={pageStyle}
604
596
  height="fill"
605
597
  contentWidth={1000}
606
598
  defaultHasDividers
@@ -143,6 +143,9 @@ function ComponentDetailView() {
143
143
  height="auto"
144
144
  contentWidth={960}
145
145
  end={
146
+ // Dropped below 768 on purpose: this rail is in-page navigation, not
147
+ // content. Every heading stays reachable by scrolling, and two rails on a
148
+ // phone would eat the column the prose needs.
146
149
  isMobile ? undefined : (
147
150
  <LayoutPanel
148
151
  isScrollable={false}
@@ -2,7 +2,7 @@
2
2
  // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
3
  // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
4
  // human drift check — update it with the file, never alone):
5
- // L[h=auto mw=960] > (LC[p=8 !scroll] > V[g=8] > (V[g=2] > Hd"Getting started with Astryx Dracula"[level=1] + Tx"Last updated March 30, 2026"[t=supporting] + SE{Selector}"On this page") + (C > V[g=3] > (H[g=2 a=center] > (SI[fill] > (H[g=2 a=center] > Ic + Tx"AI Assistance"[t=body])) + B.ghost"Copy prompt") + Tx"Help me get set up"[t=body]) + (V[g=4] > Hd"Prerequisites"[level=2] + (UL[density=compact] > LI"Node.js 18+" + LI"React 18 or 19" + LI"A package manager")) + D + (V[g=4] > Hd"Install the package"[level=2] + Tx"Description"[t=body] + (V[g=2] > Tx"Step 1"[t=body] + Cd) + (V[g=2] > Tx"Step 2"[t=body] + Tx"Astryx ships precompiled CSS"[t=body] + Cd) + (V[g=2] > Tx"Step 3"[t=body] + Cd)) + D + (V[g=4] > Hd"Configure theming"[level=2] + Tx"Description"[t=body] + Cd + Tx"Note"[t=body]) + D + (V[g=4] > Hd"Next steps"[level=2] + (UL[density=compact] > LI"Fundamental concepts" + LI"Component API reference" + LI"Accessibility" + LI"CLI tools" + LI"Design tokens"))) + (LP[!scroll] > Outline)
5
+ // L[h=auto mw=960] > (LC[p=8 !scroll] > V[g=8] > (V[g=2] > Hd"Getting started with Astryx Dracula"[level=1 t=display-2] + Tx"Last updated March 30, 2026"[t=supporting] + SE{Selector}"On this page") + (C > V[g=3] > (H[g=2 a=center] > (SI[fill] > (H[g=2 a=center] > Ic + Tx"AI Assistance"[t=body])) + B.ghost"Copy prompt") + Tx"Help me get set up"[t=body]) + (V[g=4] > Hd"Prerequisites"[level=2] + (UL[density=compact] > LI"Node.js 18+" + LI"React 18 or 19" + LI"A package manager")) + D + (V[g=4] > Hd"Install the package"[level=2] + Tx"Description"[t=body] + (V[g=2] > Tx"Step 1"[t=body] + Cd) + (V[g=2] > Tx"Step 2"[t=body] + Tx"Astryx ships precompiled CSS"[t=body] + Cd) + (V[g=2] > Tx"Step 3"[t=body] + Cd)) + D + (V[g=4] > Hd"Configure theming"[level=2] + Tx"Description"[t=body] + Cd + Tx"Note"[t=body]) + D + (V[g=4] > Hd"Next steps"[level=2] + (UL[density=compact] > LI"Fundamental concepts" + LI"Component API reference" + LI"Accessibility" + LI"CLI tools" + LI"Design tokens"))) + (LP[!scroll] > Outline)
6
6
 
7
7
  import { Button } from "@astryxdesign/core/Button";
8
8
  import { Card } from "@astryxdesign/core/Card";
@@ -63,6 +63,9 @@ export default function DocumentationTechnical() {
63
63
  height="auto"
64
64
  contentWidth={960}
65
65
  end={
66
+ // Dropped below 768 on purpose: this rail is in-page navigation, not
67
+ // content. Every heading stays reachable by scrolling, and two rails on a
68
+ // phone would eat the column the prose needs.
66
69
  isMobile ? undefined : (
67
70
  <LayoutPanel
68
71
  isScrollable={false}
@@ -78,7 +81,9 @@ export default function DocumentationTechnical() {
78
81
  <LayoutContent isScrollable={false} padding={8}>
79
82
  <VStack gap={8}>
80
83
  <VStack gap={2}>
81
- <Heading level={1}>Getting started with Astryx Dracula</Heading>
84
+ <Heading level={1} type="display-2">
85
+ Getting started with Astryx Dracula
86
+ </Heading>
82
87
  <Text type="supporting" color="secondary" hasTabularNumbers>
83
88
  Last updated March 30, 2026
84
89
  </Text>
@@ -116,9 +121,7 @@ export default function DocumentationTechnical() {
116
121
  />
117
122
  </HStack>
118
123
  <Text type="body" color="secondary">
119
- Help me get set up with Astryx Dracula. Based on my project, do the following: 1.
120
- Install @astryxdesign/core and the StyleX compiler. 2. Wrap my app in the Theme
121
- component. 3. Replace one existing component with an Astryx equivalent.
124
+ {SETUP_PROMPT}
122
125
  </Text>
123
126
  </VStack>
124
127
  </Card>
@@ -2,11 +2,12 @@
2
2
  // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
3
  // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
4
  // human drift check — update it with the file, never alone):
5
- // Counts: 31 total, Core 18 / Layout 4 / Navigation 4 / Form 5. Derived at
6
- // render from COMPONENT_CATEGORIES, so these numerals must move together with
7
- // the array -- the gate in scripts/check.ts reads the rendered count, not this
8
- // comment.
9
- // L[h=fill mw=1200] > LC[p=8] > V[g=10] > (C.transparent[p=10] > (H[g=8 a=center] > (SI[fill] > V[g=4] > Hd"The coven grimoire"[level=1 t=display-1] + Tx.lg"Every incantation in the Astryx spellbook"[t=large] + (H > B.primary"Enter the grimoire")))) + (V[g=4] > (H[j=between a=center] > Hd"Core"[level=2] + Tx"18 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 h=160] > Scene{SceneTile}) + (V[g=1] > Tx"AppShell"[t=body] + Tx"AppShell provides"[t=body]))*18)) + (V[g=4] > (H[j=between a=center] > Hd"Layout"[level=2] + Tx"4 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 h=160] > Scene{SceneTile}) + (V[g=1] > Tx"Card"[t=body] + Tx"Cards group"[t=body]))*4)) + (V[g=4] > (H[j=between a=center] > Hd"Navigation"[level=2] + Tx"4 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 h=160] > Scene{SceneTile}) + (V[g=1] > Tx"Breadcrumbs"[t=body] + Tx"Breadcrumbs show"[t=body]))*4)) + (V[g=4] > (H[j=between a=center] > Hd"Form"[level=2] + Tx"5 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 h=160] > Scene{SceneTile}) + (V[g=1] > Tx"CheckboxInput"[t=body] + Tx"CheckboxInput renders"[t=body]))*5))
5
+ // Counts: 31 total, Core 18 / Layout 4 / Navigation 4 / Form 5. The page derives
6
+ // every rendered count from COMPONENT_CATEGORIES; the numerals here are the
7
+ // header's copy of that count and must be moved by hand with the array — no
8
+ // gate reads them (scripts/check.ts gates component names in a header, not
9
+ // counts).
10
+ // L[h=fill mw=1200] > LC[p=8] > V[g=10] > (C.transparent[p=10] > (H[g=8 a=center] > (SI[fill] > V[g=4] > Hd"The coven grimoire"[level=1 t=display-2] + Tx.lg"Every incantation in the Astryx spellbook"[t=large] + (H > B.primary"Enter the grimoire")))) + (V[g=4] > (H[j=between a=center] > Hd"Core"[level=2] + Tx"18 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 minH=160] > Scene{SceneTile}) + (V[g=1] > Tx"AppShell"[t=body] + Tx"AppShell provides"[t=body]))*18)) + (V[g=4] > (H[j=between a=center] > Hd"Layout"[level=2] + Tx"4 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 minH=160] > Scene{SceneTile}) + (V[g=1] > Tx"Card"[t=body] + Tx"Cards group"[t=body]))*4)) + (V[g=4] > (H[j=between a=center] > Hd"Navigation"[level=2] + Tx"4 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 minH=160] > Scene{SceneTile}) + (V[g=1] > Tx"Breadcrumbs"[t=body] + Tx"Breadcrumbs show"[t=body]))*4)) + (V[g=4] > (H[j=between a=center] > Hd"Form"[level=2] + Tx"5 spells"[t=body]) + (G[c={min:260} g=4] > (CC.transparent[p=4] > V[g=3] > (C.muted[p=0 minH=160] > Scene{SceneTile}) + (V[g=1] > Tx"CheckboxInput"[t=body] + Tx"CheckboxInput renders"[t=body]))*5))
10
11
 
11
12
  /**
12
13
  * Documentation catalog — every component shelf in the grimoire. Frame-first layout (see `bunx astryx docs layout`): (Frame/responsive/container: see XLE header above.)
@@ -24,6 +25,11 @@ import type { SceneHue } from "astryx-dracula/shared/scene-hues";
24
25
  import { SceneTile } from "astryx-dracula/shared/scene-tile";
25
26
  import type { CSSProperties } from "react";
26
27
 
28
+ // Definite ancestor for `height="fill"`: 100dvh, not minHeight: '100%' (a
29
+ // percentage min-height against an indefinite block computes to 0 — CSS 2.1
30
+ // 10.5). Templates ship standalone, so the page owns this, not the host.
31
+ const pageStyle: CSSProperties = { height: "100dvh" };
32
+
27
33
  // Negative margin offsets each card's 8px padding so the grid content stays
28
34
  // visually aligned while giving every card a padded hover/click target.
29
35
  const cardGrid: CSSProperties = {
@@ -244,6 +250,7 @@ const COMPONENT_TOTAL = COMPONENT_CATEGORIES.reduce(
244
250
  export default function DocumentationCatalog() {
245
251
  return (
246
252
  <Layout
253
+ style={pageStyle}
247
254
  height="fill"
248
255
  contentWidth={1200}
249
256
  content={
@@ -253,7 +260,7 @@ export default function DocumentationCatalog() {
253
260
  <HStack gap={8} vAlign="center">
254
261
  <StackItem size="fill">
255
262
  <VStack gap={4}>
256
- <Heading level={1} type="display-1">
263
+ <Heading level={1} type="display-2">
257
264
  The coven grimoire
258
265
  </Heading>
259
266
  <Text type="large" weight="normal" color="secondary">
@@ -121,6 +121,7 @@ export default function TwoColumnForm() {
121
121
  label="Full name"
122
122
  isRequired
123
123
  placeholder="Full name"
124
+ autoComplete="name"
124
125
  value={fullName}
125
126
  onChange={setFullName}
126
127
  status={errors.fullName ? { type: "error", message: errors.fullName } : undefined}
@@ -129,7 +130,9 @@ export default function TwoColumnForm() {
129
130
  <TextInput
130
131
  label="Email"
131
132
  isRequired
133
+ type="email"
132
134
  placeholder="Email"
135
+ autoComplete="email"
133
136
  value={email}
134
137
  onChange={setEmail}
135
138
  status={errors.email ? { type: "error", message: errors.email } : undefined}
@@ -137,6 +140,7 @@ export default function TwoColumnForm() {
137
140
  <TextInput
138
141
  label="Company name"
139
142
  placeholder="Company name"
143
+ autoComplete="organization"
140
144
  value={company}
141
145
  onChange={setCompany}
142
146
  />
@@ -145,20 +149,25 @@ export default function TwoColumnForm() {
145
149
  <TextInput
146
150
  label="Job title"
147
151
  placeholder="Job title"
152
+ autoComplete="organization-title"
148
153
  value={jobTitle}
149
154
  onChange={setJobTitle}
150
155
  />
151
156
  <TextInput
152
157
  label="Phone number"
158
+ type="tel"
153
159
  placeholder="Phone number"
160
+ autoComplete="tel"
154
161
  value={phone}
155
162
  onChange={setPhone}
156
163
  />
157
164
  </Grid>
158
165
 
159
166
  <VStack gap={2}>
160
- <Text type="label">What are you reaching out about?</Text>
161
- <HStack gap={2} wrap="wrap">
167
+ <Text type="label" id="inquiry-reasons-label">
168
+ What are you reaching out about?
169
+ </Text>
170
+ <HStack gap={2} wrap="wrap" role="group" aria-labelledby="inquiry-reasons-label">
162
171
  {INQUIRY_REASONS.map((reason) => {
163
172
  const isSelected = inquiryReason === reason;
164
173
  return (
@@ -0,0 +1,15 @@
1
+ export default {
2
+ type: "page",
3
+ name: "Form Wizard",
4
+ description: "Four-step workspace setup with a stepper, per-step validation, and a review",
5
+ keywords: [
6
+ "wizard",
7
+ "stepper",
8
+ "multi-step form",
9
+ "onboarding",
10
+ "validation",
11
+ "review step",
12
+ "workspace setup",
13
+ ],
14
+ category: "forms",
15
+ };