astryx-dracula 0.3.1 → 0.5.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/USAGE.md +25 -0
  2. package/astryx-dracula.d.ts +2 -2
  3. package/astryx-dracula.js +384 -3
  4. package/astryx-dracula.variants.d.ts +2 -14
  5. package/icons.mjs +125 -0
  6. package/package.json +21 -9
  7. package/shared/auth-chrome.tsx +156 -6
  8. package/shared/chaptered-doc.tsx +3 -4
  9. package/shared/chart-hues.ts +0 -5
  10. package/shared/chart-legend.tsx +2 -3
  11. package/shared/data-bar.tsx +5 -5
  12. package/shared/format-money.ts +3 -0
  13. package/shared/metric-card.tsx +127 -0
  14. package/shared/revenue-chart.tsx +63 -7
  15. package/shared/scene-hues.ts +0 -8
  16. package/shared/scene-tile.tsx +3 -11
  17. package/shared/settings-data.ts +71 -1
  18. package/shared/settings-rows.tsx +76 -30
  19. package/shared/stars.tsx +28 -0
  20. package/templates/ai-chat-landing.tsx +105 -67
  21. package/templates/ai-chat.tsx +67 -80
  22. package/templates/centered-hero.tsx +2 -2
  23. package/templates/contact-form.tsx +11 -10
  24. package/templates/dashboard-portfolio.tsx +77 -155
  25. package/templates/dashboard.tsx +24 -137
  26. package/templates/detail-page.tsx +25 -22
  27. package/templates/documentation-design.tsx +39 -478
  28. package/templates/documentation-technical.tsx +13 -31
  29. package/templates/editor.tsx +168 -185
  30. package/templates/file-explorer.tsx +91 -93
  31. package/templates/form-two-column.tsx +1 -1
  32. package/templates/ide.tsx +18 -24
  33. package/templates/incident-console.tsx +67 -42
  34. package/templates/kanban-board.tsx +36 -85
  35. package/templates/library.tsx +42 -66
  36. package/templates/login-card.tsx +33 -123
  37. package/templates/login-split.tsx +96 -119
  38. package/templates/login-sso.tsx +69 -74
  39. package/templates/login.tsx +21 -118
  40. package/templates/messaging-shell.tsx +162 -168
  41. package/templates/payment-form.tsx +63 -71
  42. package/templates/product-detail.tsx +11 -46
  43. package/templates/product-tour.tsx +38 -60
  44. package/templates/settings-dialog.tsx +93 -118
  45. package/templates/settings-sidebar.tsx +122 -143
  46. package/templates/settings.tsx +208 -121
  47. package/templates/shell-nav.tsx +20 -75
  48. package/templates/shell-side-nav.tsx +16 -90
  49. package/templates/shell-top-nav.tsx +20 -53
  50. package/templates/side-gallery.tsx +19 -19
  51. package/templates/table-grouped.tsx +86 -136
  52. package/templates/table-page-chart.tsx +12 -11
  53. package/templates/table-page-heatmap-status.tsx +0 -17
  54. package/templates/table-page-shoe-store-heatmap.tsx +12 -12
  55. package/templates/table-page.tsx +4 -31
  56. package/templates/table.tsx +18 -2
  57. package/templates/tech-report.tsx +59 -73
  58. package/templates/theme-showcase.tsx +124 -415
  59. package/theme.css +211 -119
@@ -1,12 +1,11 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > (LC > V[g=8] > (V[g=2] > Hd"Getting started with Astryx Dracula"[level=1] + Tx"Last updated March 30, 2026"[t=supporting]) + (C > V[g=3] > (H[j=between] > (H[g=2] > Ic + Tx"AI Assistance"[t=body]) + (H > B"Copy prompt" + DM)) + Tx"Prompt description"[t=body]) + (V[g=4] > Hd"Prerequisites"[level=2] + UL) + D + (V[g=4] > Hd"Install the package"[level=2] + Tx"Description"[t=body] + (V[g=2] > Tx"Step"[t=body] + Cd)*3) + 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)) + (LP > Outline)
3
+ // L > (LC > V[g=8] > (V[g=2] > Hd"Getting started with Astryx Dracula"[level=1] + Tx"Last updated March 30, 2026"[t=supporting]) + (C > V[g=3] > (H[j=between] > (H[g=2] > Ic + Tx"AI Assistance"[t=body]) + B"Copy prompt") + Tx"Prompt description"[t=body]) + (V[g=4] > Hd"Prerequisites"[level=2] + UL) + D + (V[g=4] > Hd"Install the package"[level=2] + Tx"Description"[t=body] + (V[g=2] > Tx"Step"[t=body] + Cd)*3) + 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)) + (LP > Outline)
4
4
 
5
5
  import {useCallback, useState} from 'react';
6
6
  import {Heading, Text} from '@astryxdesign/core/Text';
7
7
  import {Button} from '@astryxdesign/core/Button';
8
8
  import {Card} from '@astryxdesign/core/Card';
9
- import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
10
9
  import {List, ListItem} from '@astryxdesign/core/List';
11
10
  import {CodeBlock} from '@astryxdesign/core/CodeBlock';
12
11
  import {Selector} from '@astryxdesign/core/Selector';
@@ -16,7 +15,7 @@ import {Divider} from '@astryxdesign/core/Divider';
16
15
  import {Icon} from '@astryxdesign/core/Icon';
17
16
  import {Outline, type OutlineItem} from '@astryxdesign/core/Outline';
18
17
  import {useMediaQuery} from '@astryxdesign/core/hooks';
19
- import {Sparkles, ClipboardCopy, ChevronDown} from 'lucide-react';
18
+ import {Sparkles, ClipboardCopy} from 'lucide-react';
20
19
  import {outlinePanel} from 'astryx-dracula/shared/chaptered-doc-config';
21
20
 
22
21
  // ---------------------------------------------------------------------------
@@ -30,17 +29,8 @@ import {outlinePanel} from 'astryx-dracula/shared/chaptered-doc-config';
30
29
  // Same prompt in the copy action and the visible body: one string, so the
31
30
  // two can never drift.
32
31
  const SETUP_PROMPT =
33
- 'Help me get set up with Astryx Dracula. Based on my project, do the following: 1. Install @astryxdesign/core and the StyleX compiler. 2. Wrap my app in ThemeProvider. 3. Replace one existing component with an Astryx equivalent. After setup, suggest relevant next steps based on my project.';
32
+ 'Help me get set up with Astryx Dracula. Based on my project, do the following: 1. Install @astryxdesign/core and the StyleX compiler. 2. Wrap my app in the Theme component. 3. Replace one existing component with an Astryx equivalent. After setup, suggest relevant next steps based on my project.';
34
33
 
35
- // Copies the setup prompt in the shape each tool wants to receive it. The
36
- // menu cannot navigate anywhere from a static template, so the action says
37
- // what it does.
38
- const PROMPT_TARGETS = ['v0', 'Claude', 'ChatGPT', 'Cursor'].map(target => ({
39
- label: `Copy for ${target}`,
40
- onClick: () => {
41
- void navigator.clipboard.writeText(SETUP_PROMPT);
42
- },
43
- }));
44
34
 
45
35
  const OUTLINE_ITEMS: OutlineItem[] = [
46
36
  {id: 'prerequisites', label: 'Prerequisites', level: 2},
@@ -124,22 +114,12 @@ export default function DocumentationTechnical() {
124
114
  void navigator.clipboard.writeText(SETUP_PROMPT);
125
115
  }}
126
116
  />
127
- <DropdownMenu
128
- button={{
129
- label: 'More options',
130
- variant: 'ghost',
131
- size: 'sm',
132
- isIconOnly: true,
133
- icon: <Icon icon={ChevronDown} />,
134
- }}
135
- items={PROMPT_TARGETS}
136
- />
137
117
  </HStack>
138
118
  <Text type="body" color="secondary">
139
119
  Help me get set up with Astryx Dracula. Based on my project,
140
120
  do the following: 1. Install @astryxdesign/core and the
141
- StyleX compiler. 2. Wrap my app in ThemeProvider. 3. Replace
142
- one existing component with an Astryx equivalent.
121
+ StyleX compiler. 2. Wrap my app in the Theme component. 3.
122
+ Replace one existing component with an Astryx equivalent.
143
123
  </Text>
144
124
  </VStack>
145
125
  </Card>
@@ -225,17 +205,19 @@ export default function App() {
225
205
  </Heading>
226
206
  <Text type="body">
227
207
  Astryx ships with a default theme that works out of the box. To
228
- customize colors, typography, and spacing, wrap your app in a
229
- theme provider.
208
+ customize colors, typography, and spacing, wrap your app in the
209
+ Theme component.
230
210
  </Text>
231
211
  <CodeBlock
232
- code={`import { ThemeProvider } from '@astryxdesign/core/Theme';
212
+ code={`import { type ReactNode } from 'react';
213
+ import { Theme } from '@astryxdesign/core/theme';
214
+ import { astryxDraculaTheme } from 'astryx-dracula';
233
215
 
234
- export default function App({ children }) {
216
+ export default function App({ children }: { children: ReactNode }) {
235
217
  return (
236
- <ThemeProvider theme="default">
218
+ <Theme theme={astryxDraculaTheme} mode="dark">
237
219
  {children}
238
- </ThemeProvider>
220
+ </Theme>
239
221
  );
240
222
  }`}
241
223
  language="tsx"
@@ -9,6 +9,7 @@ import {
9
9
  type CSSProperties,
10
10
  type KeyboardEvent,
11
11
  type MouseEvent,
12
+ type ReactNode,
12
13
  type SVGProps,
13
14
  } from 'react';
14
15
  import {useMediaQuery} from '@astryxdesign/core/hooks';
@@ -158,10 +159,6 @@ const VIEWPORT_MAX: Record<ViewportSize, number> = {
158
159
  phone: 375,
159
160
  };
160
161
 
161
- let nextId = 5;
162
- function uid() {
163
- return String(nextId++);
164
- }
165
162
 
166
163
  const DEFAULT_BLOCKS: Block[] = [
167
164
  {
@@ -328,6 +325,36 @@ const iconCircle: CSSProperties = {
328
325
  // Properties Form
329
326
  // ---------------------------------------------------------------------------
330
327
 
328
+ // Heading/description pair shared by the hero/text/features/cta branches.
329
+ // Hero labels it "Subheading" and adds its own button/alignment controls;
330
+ // the merge keeps the shared pair, not the per-branch extras.
331
+ function HeadingDescriptionGroup({
332
+ props,
333
+ onUpdate,
334
+ descriptionKey,
335
+ descriptionLabel,
336
+ }: {
337
+ props: Record<string, unknown>;
338
+ onUpdate: (key: string, value: unknown) => void;
339
+ descriptionKey: string;
340
+ descriptionLabel: string;
341
+ }) {
342
+ return (
343
+ <>
344
+ <TextInput
345
+ label="Heading"
346
+ value={(props.heading as string) ?? ''}
347
+ onChange={(v: string) => onUpdate('heading', v)}
348
+ />
349
+ <TextArea
350
+ label={descriptionLabel}
351
+ value={(props[descriptionKey] as string) ?? ''}
352
+ onChange={(v: string) => onUpdate(descriptionKey, v)}
353
+ />
354
+ </>
355
+ );
356
+ }
357
+
331
358
  function PropertiesForm({
332
359
  block,
333
360
  onUpdate,
@@ -341,15 +368,11 @@ function PropertiesForm({
341
368
  case 'hero':
342
369
  return (
343
370
  <VStack gap={4}>
344
- <TextInput
345
- label="Heading"
346
- value={(props.heading as string) ?? ''}
347
- onChange={(v: string) => onUpdate('heading', v)}
348
- />
349
- <TextArea
350
- label="Subheading"
351
- value={(props.subheading as string) ?? ''}
352
- onChange={(v: string) => onUpdate('subheading', v)}
371
+ <HeadingDescriptionGroup
372
+ props={props}
373
+ onUpdate={onUpdate}
374
+ descriptionKey="subheading"
375
+ descriptionLabel="Subheading"
353
376
  />
354
377
  <TextInput
355
378
  label="Button Label"
@@ -372,15 +395,11 @@ function PropertiesForm({
372
395
  case 'text':
373
396
  return (
374
397
  <VStack gap={4}>
375
- <TextInput
376
- label="Heading"
377
- value={(props.heading as string) ?? ''}
378
- onChange={(v: string) => onUpdate('heading', v)}
379
- />
380
- <TextArea
381
- label="Description"
382
- value={(props.description as string) ?? ''}
383
- onChange={(v: string) => onUpdate('description', v)}
398
+ <HeadingDescriptionGroup
399
+ props={props}
400
+ onUpdate={onUpdate}
401
+ descriptionKey="description"
402
+ descriptionLabel="Description"
384
403
  />
385
404
  <TextInput
386
405
  label="Button Label"
@@ -389,20 +408,15 @@ function PropertiesForm({
389
408
  />
390
409
  </VStack>
391
410
  );
392
-
393
411
  case 'features':
394
412
  case 'cta':
395
413
  return (
396
414
  <VStack gap={4}>
397
- <TextInput
398
- label="Heading"
399
- value={(props.heading as string) ?? ''}
400
- onChange={(v: string) => onUpdate('heading', v)}
401
- />
402
- <TextArea
403
- label="Description"
404
- value={(props.description as string) ?? ''}
405
- onChange={(v: string) => onUpdate('description', v)}
415
+ <HeadingDescriptionGroup
416
+ props={props}
417
+ onUpdate={onUpdate}
418
+ descriptionKey="description"
419
+ descriptionLabel="Description"
406
420
  />
407
421
  </VStack>
408
422
  );
@@ -458,187 +472,156 @@ function BlockPreview({
458
472
  }) {
459
473
  const {type, props} = block;
460
474
 
475
+ // One SelectableCard wrapper for every block; only the inner content
476
+ // switches. Keeps padding/label/selection in one place.
477
+ let content: ReactNode;
461
478
  switch (type) {
462
479
  case 'hero':
463
- return (
464
- <SelectableCard
465
- padding={4}
466
- label={block.label}
467
- isSelected={isSelected}
468
- onChange={onSelect}>
469
- <VStack gap={4}>
470
- <Heading level={3}>
471
- {(props.heading as string) || 'Hero Heading'}
472
- </Heading>
473
- <Text type="body" color="secondary">
474
- {(props.subheading as string) || 'A whisper from the crypt…'}
475
- </Text>
476
- {(props.buttonLabel as string) && (
477
- <Button label={props.buttonLabel as string} />
478
- )}
479
- </VStack>
480
- </SelectableCard>
480
+ content = (
481
+ <VStack gap={4}>
482
+ <Heading level={3}>
483
+ {(props.heading as string) || 'Hero Heading'}
484
+ </Heading>
485
+ <Text type="body" color="secondary">
486
+ {(props.subheading as string) || 'A whisper from the crypt…'}
487
+ </Text>
488
+ {(props.buttonLabel as string) && (
489
+ <Button label={props.buttonLabel as string} />
490
+ )}
491
+ </VStack>
481
492
  );
493
+ break;
482
494
 
483
495
  case 'text':
484
- if (props.heading) {
485
- return (
486
- <SelectableCard
487
- padding={4}
488
- label={block.label}
489
- isSelected={isSelected}
490
- onChange={onSelect}>
491
- <EmptyState
492
- title={props.heading as string}
493
- description={props.description as string}
494
- icon={<Icon icon={FileText} color="secondary" />}
495
- actions={
496
- (props.buttonLabel as string) ? (
497
- <Button
498
- label={props.buttonLabel as string}
499
- variant="secondary"
500
- />
501
- ) : undefined
502
- }
503
- />
504
- </SelectableCard>
505
- );
506
- }
507
- return (
508
- <SelectableCard
509
- padding={4}
510
- label={block.label}
511
- isSelected={isSelected}
512
- onChange={onSelect}>
513
- <Text type="body">
514
- {(props.content as string) || 'Ink your midnight thoughts here…'}
515
- </Text>
516
- </SelectableCard>
496
+ content = props.heading ? (
497
+ <EmptyState
498
+ title={props.heading as string}
499
+ description={props.description as string}
500
+ icon={<Icon icon={FileText} color="secondary" />}
501
+ actions={
502
+ (props.buttonLabel as string) ? (
503
+ <Button
504
+ label={props.buttonLabel as string}
505
+ variant="secondary"
506
+ />
507
+ ) : undefined
508
+ }
509
+ />
510
+ ) : (
511
+ <Text type="body">
512
+ {(props.content as string) || 'Ink your midnight thoughts here…'}
513
+ </Text>
517
514
  );
518
-
515
+ break;
519
516
  case 'image':
520
- return (
521
- <SelectableCard
522
- padding={4}
523
- label={block.label}
524
- isSelected={isSelected}
525
- onChange={onSelect}>
526
- <EmptyState
527
- title="Image Block"
528
- description="Drop an image or enter a URL"
529
- icon={<Icon icon={Image} color="secondary" />}
530
- isCompact
531
- />
532
- </SelectableCard>
517
+ content = (
518
+ <EmptyState
519
+ title="Image Block"
520
+ description="Drop an image or enter a URL"
521
+ icon={<Icon icon={Image} color="secondary" />}
522
+ isCompact
523
+ />
533
524
  );
525
+ break;
534
526
 
535
527
  case 'button':
536
- return (
537
- <SelectableCard
538
- padding={4}
539
- label={block.label}
540
- isSelected={isSelected}
541
- onChange={onSelect}>
542
- <Center>
543
- <Button
544
- label={(props.label as string) || 'Button'}
545
- variant={
546
- (props.variant as 'primary' | 'secondary' | 'ghost') ||
547
- 'primary'
548
- }
549
- size={(props.size as 'sm' | 'md' | 'lg') || 'md'}
550
- />
551
- </Center>
552
- </SelectableCard>
528
+ content = (
529
+ <Center>
530
+ <Button
531
+ label={(props.label as string) || 'Button'}
532
+ variant={
533
+ (props.variant as 'primary' | 'secondary' | 'ghost') ||
534
+ 'primary'
535
+ }
536
+ size={(props.size as 'sm' | 'md' | 'lg') || 'md'}
537
+ />
538
+ </Center>
553
539
  );
540
+ break;
541
+
554
542
 
555
543
  case 'features': {
556
544
  const items = (props.items as Transaction[]) || [];
557
- return (
558
- <SelectableCard
559
- padding={4}
560
- label={block.label}
561
- isSelected={isSelected}
562
- onChange={onSelect}>
563
- <VStack gap={4}>
564
- <HStack gap={3} vAlign="start" hAlign="between">
565
- <VStack gap={1}>
566
- <Heading level={3}>
567
- {(props.heading as string) || 'Features'}
568
- </Heading>
569
- {(props.description as string) && (
570
- <Text type="body" color="secondary">
571
- {props.description as string}
572
- </Text>
573
- )}
574
- </VStack>
575
- <Button label="View All" variant="secondary" />
576
- </HStack>
577
- <Table
578
- data={items}
579
- columns={TRANSACTION_COLUMNS}
580
- idKey="id"
581
- hasHover
582
- textOverflow="truncate"
583
- />
584
- </VStack>
585
- </SelectableCard>
545
+ content = (
546
+ <VStack gap={4}>
547
+ <HStack gap={3} vAlign="start" hAlign="between">
548
+ <VStack gap={1}>
549
+ <Heading level={3}>
550
+ {(props.heading as string) || 'Features'}
551
+ </Heading>
552
+ {(props.description as string) && (
553
+ <Text type="body" color="secondary">
554
+ {props.description as string}
555
+ </Text>
556
+ )}
557
+ </VStack>
558
+ <Button label="View All" variant="secondary" />
559
+ </HStack>
560
+ <Table
561
+ data={items}
562
+ columns={TRANSACTION_COLUMNS}
563
+ idKey="id"
564
+ hasHover
565
+ textOverflow="truncate"
566
+ />
567
+ </VStack>
586
568
  );
569
+ break;
587
570
  }
588
571
 
589
572
  case 'cards': {
590
573
  const cardItems =
591
574
  (props.cards as Array<{title: string; description: string}>) || [];
592
- return (
593
- <SelectableCard
594
- padding={4}
595
- label={block.label}
596
- isSelected={isSelected}
597
- onChange={onSelect}>
598
- <VStack gap={4}>
599
- <Heading level={3}>Cards</Heading>
600
- <Divider />
601
- <List density="balanced" hasDividers={false}>
602
- {cardItems.map((card) => (
603
- <ListItem
604
- key={card.title}
605
- label={card.title}
606
- description={card.description}
607
- />
608
- ))}
609
- </List>
610
- </VStack>
611
- </SelectableCard>
575
+ content = (
576
+ <VStack gap={4}>
577
+ <Heading level={3}>Cards</Heading>
578
+ <Divider />
579
+ <List density="balanced" hasDividers={false}>
580
+ {cardItems.map(card => (
581
+ <ListItem
582
+ key={card.title}
583
+ label={card.title}
584
+ description={card.description}
585
+ />
586
+ ))}
587
+ </List>
588
+ </VStack>
612
589
  );
590
+ break;
613
591
  }
614
592
 
615
593
  case 'cta':
616
- return (
617
- <SelectableCard
618
- padding={4}
619
- label={block.label}
620
- isSelected={isSelected}
621
- onChange={onSelect}>
622
- <HStack gap={4} vAlign="start">
623
- <Center width={40} height={40} style={iconCircle}>
624
- <Icon icon={Lock} color="secondary" />
625
- </Center>
626
- <VStack gap={1}>
627
- <Heading level={3}>
628
- {(props.heading as string) || 'Notice'}
629
- </Heading>
630
- <Text type="body" color="secondary">
631
- {(props.description as string) ||
632
- 'Words from the crypt gather here…'}
633
- </Text>
634
- </VStack>
635
- </HStack>
636
- </SelectableCard>
594
+ content = (
595
+ <HStack gap={4} vAlign="start">
596
+ <Center width={40} height={40} style={iconCircle}>
597
+ <Icon icon={Lock} color="secondary" />
598
+ </Center>
599
+ <VStack gap={1}>
600
+ <Heading level={3}>
601
+ {(props.heading as string) || 'Notice'}
602
+ </Heading>
603
+ <Text type="body" color="secondary">
604
+ {(props.description as string) ||
605
+ 'Words from the crypt gather here…'}
606
+ </Text>
607
+ </VStack>
608
+ </HStack>
637
609
  );
610
+ break;
638
611
 
639
612
  default:
640
613
  return null;
641
614
  }
615
+
616
+ return (
617
+ <SelectableCard
618
+ padding={4}
619
+ label={block.label}
620
+ isSelected={isSelected}
621
+ onChange={onSelect}>
622
+ {content}
623
+ </SelectableCard>
624
+ );
642
625
  }
643
626
 
644
627
  // ---------------------------------------------------------------------------
@@ -701,7 +684,7 @@ export default function PageEditor() {
701
684
  );
702
685
 
703
686
  const addBlock = useCallback((type: BlockType) => {
704
- const id = uid();
687
+ const id = crypto.randomUUID();
705
688
  const newBlock: Block = {
706
689
  id,
707
690
  type,