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
@@ -2,7 +2,7 @@
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
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))
4
4
 
5
- import {useRef, useState, type CSSProperties} from 'react';
5
+ import {useState, type CSSProperties} from 'react';
6
6
 
7
7
  import {
8
8
  HStack,
@@ -40,6 +40,7 @@ import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
40
40
  import {MoreMenu} from '@astryxdesign/core/MoreMenu';
41
41
  import {Toolbar} from '@astryxdesign/core/Toolbar';
42
42
  import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
43
+ import {useMediaQuery} from '@astryxdesign/core/hooks';
43
44
 
44
45
  import {
45
46
  FileText,
@@ -52,19 +53,17 @@ import {
52
53
  } from 'lucide-react';
53
54
 
54
55
  // Below this container width the split-pane collapses to a single chat column
55
- // and the artifact opens as a full-screen dialog instead. Shared by the CSS
56
- // container query and the JS check in openArtifact so they can't drift. 1023
57
- // keeps tablet widths from squeezing the chat into a ~100px rail beside the
58
- // 640px default artifact panel.
56
+ // and the artifact opens as a full-screen dialog instead. Single source:
57
+ // MOBILE_MAX_WIDTH feeds useMediaQuery below, which both gates the panel and
58
+ // routes openArtifact — no container query, no manual width read to drift.
59
+ // 1023 keeps tablet widths from squeezing the chat into a ~100px rail beside
60
+ // the 640px default artifact panel.
59
61
  const MOBILE_MAX_WIDTH = 1023;
60
-
61
62
  const root: CSSProperties = {
62
63
  // The viewer owns its chrome: the root fills the viewport and the message
63
64
  // list plus artifact body scroll inside it (editor pattern).
64
65
  height: '100dvh',
65
66
  width: '100%',
66
- containerType: 'inline-size',
67
- containerName: 'artifact',
68
67
  };
69
68
  const chatColumn: CSSProperties = {
70
69
  flex: 1,
@@ -91,38 +90,16 @@ const articleBody: CSSProperties = {
91
90
  marginInline: 'auto',
92
91
  };
93
92
 
94
- // Runtime width for the artifact panel, passed in via the --artifact-panel-width
95
- // custom property so the MOBILE container query can still override it to 100%
96
- // (an inline `width` would beat the class rule). The container query lives in a
97
- // plain <style> tag below so it needs NO CSS compiler.
93
+ // Width for the artifact panel. Plain style object — the panel unmounts below
94
+ // the breakpoint (see useMediaQuery in the component), so no container query
95
+ // or <style> tag is needed to hide it.
98
96
  const artifactPanelWidthVar = (size: number | string): CSSProperties =>
99
97
  ({
100
- '--artifact-panel-width': typeof size === 'number' ? `${size}px` : size,
98
+ width: typeof size === 'number' ? `${size}px` : size,
99
+ flexShrink: 0,
100
+ overflow: 'hidden',
101
101
  }) as CSSProperties;
102
102
 
103
- const AI_CHAT_CSS = `
104
- .ai-chat-resize-handle {
105
- display: flex;
106
- }
107
- .ai-chat-artifact-panel {
108
- overflow: hidden;
109
- display: flex;
110
- flex-direction: column;
111
- width: var(--artifact-panel-width);
112
- flex-shrink: 0;
113
- }
114
- @container artifact (max-width: ${MOBILE_MAX_WIDTH}px) {
115
- .ai-chat-resize-handle {
116
- display: none;
117
- }
118
- .ai-chat-artifact-panel {
119
- display: none;
120
- width: 100%;
121
- flex-shrink: 1;
122
- }
123
- }
124
- `;
125
-
126
103
  // Artifact content
127
104
 
128
105
  const MENTION_TOKENS = [
@@ -175,6 +152,13 @@ The refresh path is covered end to end:
175
152
 
176
153
  // Artifact subviews
177
154
 
155
+ // Copy/Share live in both surfaces: desktop toolbar buttons and the mobile
156
+ // overflow menu. One items array so the two can't drift.
157
+ const ARTIFACT_ACTION_ITEMS = [
158
+ {label: 'Copy', icon: Copy},
159
+ {label: 'Share', icon: Share2},
160
+ ] as const;
161
+
178
162
  // Header actions: version menu, copy, share. Pass `onClose` for the desktop
179
163
  // close button. A fragment so each control is a direct child of the toolbar.
180
164
  function ArtifactActions({onClose}: {onClose?: () => void}) {
@@ -188,20 +172,16 @@ function ArtifactActions({onClose}: {onClose?: () => void}) {
188
172
  }}
189
173
  items={[{label: 'v2 (current)'}, {label: 'v1'}]}
190
174
  />
191
- <Button
192
- label="Copy"
193
- variant="ghost"
194
- size="sm"
195
- icon={<Icon icon={Copy} size="sm" />}
196
- isIconOnly
197
- />
198
- <Button
199
- label="Share"
200
- variant="ghost"
201
- size="sm"
202
- icon={<Icon icon={Share2} size="sm" />}
203
- isIconOnly
204
- />
175
+ {ARTIFACT_ACTION_ITEMS.map(item => (
176
+ <Button
177
+ key={item.label}
178
+ label={item.label}
179
+ variant="ghost"
180
+ size="sm"
181
+ icon={<Icon icon={item.icon} size="sm" />}
182
+ isIconOnly
183
+ />
184
+ ))}
205
185
  {onClose != null && (
206
186
  <Button
207
187
  label="Close document"
@@ -226,23 +206,23 @@ function MobileArtifactActions() {
226
206
  {
227
207
  type: 'section',
228
208
  title: 'Version',
229
- items: [
230
- {label: 'v2 (current)', onClick: () => {}},
231
- {label: 'v1', onClick: () => {}},
232
- ],
209
+ items: [{label: 'v2 (current)'}, {label: 'v1'}],
233
210
  },
234
211
  {type: 'divider'},
235
- {label: 'Copy', icon: Copy},
236
- {label: 'Share', icon: Share2},
212
+ ...ARTIFACT_ACTION_ITEMS,
237
213
  ]}
238
214
  />
239
215
  );
240
216
  }
241
217
 
242
- // Scrollable artifact body — the formatted document. The mobile dialog renders
243
- // the title in its DialogHeader, so it drops the in-body heading rather than
244
- // announcing the same h1 twice.
245
- function ArtifactBody({hasOwnTitle = true}: {hasOwnTitle?: boolean}) {
218
+ // Scrollable artifact content — the formatted document, no heading. The
219
+ // desktop panel renders ArtifactBody above it; the mobile dialog renders its
220
+ // title in DialogHeader instead, so both share this rather than a flag prop.
221
+ function ArtifactContent() {
222
+ return <Markdown>{ARTIFACT_CONTENT}</Markdown>;
223
+ }
224
+ // Desktop body: in-body heading plus the shared content.
225
+ function ArtifactBody() {
246
226
  return (
247
227
  <Section
248
228
  variant="transparent"
@@ -251,13 +231,10 @@ function ArtifactBody({hasOwnTitle = true}: {hasOwnTitle?: boolean}) {
251
231
  aria-label={ARTIFACT_TITLE}
252
232
  tabIndex={0}>
253
233
  <VStack gap={2} style={articleBody}>
254
- {/* Page owns the h1 (VisuallyHidden "Night thread"); the panel title is h2. */}
255
- {hasOwnTitle && (
256
- <Heading level={2} type="display-2">
257
- {ARTIFACT_TITLE}
258
- </Heading>
259
- )}
260
- <Markdown>{ARTIFACT_CONTENT}</Markdown>
234
+ <Heading level={2} type="display-2">
235
+ {ARTIFACT_TITLE}
236
+ </Heading>
237
+ <ArtifactContent />
261
238
  </VStack>
262
239
  </Section>
263
240
  );
@@ -298,18 +275,19 @@ export default function AiChat() {
298
275
  // Mobile shows the artifact as a full-screen dialog; desktop as a side panel.
299
276
  const [isArtifactDialogOpen, setIsArtifactDialogOpen] = useState(false);
300
277
  const [isArtifactOpen, setIsArtifactOpen] = useState(true);
301
- const rootRef = useRef<HTMLElement>(null);
302
278
  const artifactResize = useResizable({
303
279
  defaultSize: 640,
304
- minSizePx: 480,
305
- maxSizePx: 960,
280
+ minSize: 480,
281
+ maxSize: 960,
306
282
  autoSaveId: 'ai-chat-artifact-panel',
307
283
  });
308
284
 
309
- // Match the container-query breakpoint by measuring the root, not the viewport.
285
+ // Single source for the breakpoint: the media query both gates the panel
286
+ // below and routes openArtifact, so the split-pane and the dialog target
287
+ // can't disagree about which surface is live.
288
+ const isMobileWidth = useMediaQuery(`(max-width: ${MOBILE_MAX_WIDTH}px)`);
310
289
  const openArtifact = () => {
311
- const width = rootRef.current?.offsetWidth ?? Infinity;
312
- if (width <= MOBILE_MAX_WIDTH) {
290
+ if (isMobileWidth) {
313
291
  setIsArtifactDialogOpen(true);
314
292
  } else {
315
293
  setIsArtifactOpen(true);
@@ -317,8 +295,7 @@ export default function AiChat() {
317
295
  };
318
296
 
319
297
  return (
320
- <VStack ref={rootRef} style={root}>
321
- <style>{AI_CHAT_CSS}</style>
298
+ <VStack style={root}>
322
299
  <Layout
323
300
  height="fill"
324
301
  content={
@@ -700,8 +677,11 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
700
677
  </ChatLayout>
701
678
  </VStack>
702
679
 
703
- {/* Desktop split-pane: resize handle + artifact panel */}
704
- {isArtifactOpen && (
680
+ {/* Desktop split-pane: resize handle + artifact panel. Gated on
681
+ the same media query that routes openArtifact, so shrinking
682
+ the window while open moves to the dialog instead of
683
+ squeezing the chat into a rail. */}
684
+ {isArtifactOpen && !isMobileWidth && (
705
685
  <>
706
686
  <ResizeHandle
707
687
  direction="horizontal"
@@ -710,14 +690,12 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
710
690
  pillPlacement="start"
711
691
  hasDivider
712
692
  label="Resize artifact panel"
713
- className="ai-chat-resize-handle"
714
693
  />
715
694
 
716
695
  {/* Toolbar as the card header, body below */}
717
696
  <Card
718
697
  variant="transparent"
719
698
  height="100%"
720
- className="ai-chat-artifact-panel"
721
699
  style={artifactPanelWidthVar(artifactResize.size)}>
722
700
  <Toolbar
723
701
  label="Artifact actions"
@@ -773,7 +751,16 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
773
751
  }
774
752
  content={
775
753
  <LayoutContent padding={0}>
776
- <ArtifactBody hasOwnTitle={false} />
754
+ <Section
755
+ variant="transparent"
756
+ style={artifactScroll}
757
+ role="region"
758
+ aria-label={ARTIFACT_TITLE}
759
+ tabIndex={0}>
760
+ <VStack gap={2} style={articleBody}>
761
+ <ArtifactContent />
762
+ </VStack>
763
+ </Section>
777
764
  </LayoutContent>
778
765
  }
779
766
  />
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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.3.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]))
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.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]))
4
4
 
5
5
  import type {CSSProperties} from 'react';
6
6
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -52,7 +52,7 @@ export default function CenteredHero() {
52
52
  color="secondary"
53
53
  justify="center"
54
54
  hasTabularNumbers>
55
- astryx-dracula v0.3.1
55
+ astryx-dracula v0.5.0
56
56
  </Text>
57
57
  </VStack>
58
58
  <HStack gap={3} wrap="wrap" justify="center">
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // Ctr > V[a=center] > S.transparent[p=6] > V[g=6] > (V[g=2 a=center] > Hd"Let's brew after dark"[level=1] + Tx"Tell us a bit"[t=body]) + (V[g=4] > Hd"Why work with us"[level=2] + (G[c={min:200} g=4] > (C > V[g=3] > Ic + (V[g=1] > Hd"Title"[level=3] + Tx"Desc"[t=body]))*3)) + (V[g=4] > Hd"Your details"[level=2] + (G[c={min:260} g=4] > TI"Full name" + TI"Email") + (G[c={min:260} g=4] > TI"Company" + TI"Phone")) + D + (V[g=4] > Hd"Your project"[level=2] + (Fd"Goals"[req] > (H[g=2] > Tk"Goal"*10)) + SE"Timeline" + SE"Budget" + (RL"Source" > RLI*5) + TA"Notes" + CB"Decision maker") + (V[g=3] > B.primary"Send it into the night" + (H[j=center g=1] > Tx[t=supporting] > Lk"Privacy Policy"))
3
+ // Ctr > V[a=center] > S.transparent[p=6] > V[g=6] > (V[g=2 a=center] > Hd"Let's brew after dark"[level=1] + Tx"Tell us a bit"[t=body]) + (V[g=4] > Hd"Why work with us"[level=2] + (G[c={min:200} g=4] > (C > V[g=3] > Ic + (V[g=1] > Hd"Title"[level=3] + Tx"Desc"[t=body]))*3)) + (V[g=4] > Hd"Your details"[level=2] + (G[c={min:260} g=4] > TI"Full name" + TI"Email") + (G[c={min:260} g=4] > TI"Company" + TI"Phone")) + D + (V[g=4] > Hd"Your project"[level=2] + (Fd"Goals"[req] > (H[g=2] > Tk"Goal"*10)) + SE"Timeline" + SE"Budget" + (RL"Source" > RLI*5) + TA"Notes" + CB"Decision maker") + (V[g=3] > B.primary"Send it into the night" + (Tx[t=supporting] > Lk"Privacy Policy"))
4
4
 
5
5
  import {useState} from 'react';
6
6
  import {VStack, HStack} from '@astryxdesign/core/Layout';
@@ -296,15 +296,16 @@ export default function ContactForm() {
296
296
  variant="primary"
297
297
  onClick={() => setSubmitted(true)}
298
298
  />
299
- <HStack gap={1} hAlign="center">
300
- <Text type="supporting" color="secondary">
301
- By submitting you agree to our{' '}
302
- <Link href="#/templates/contact-form" type="supporting">
303
- Privacy Policy
304
- </Link>
305
- .
306
- </Text>
307
- </HStack>
299
+ <Text type="supporting" color="secondary">
300
+ By submitting you agree to our{' '}
301
+ <Link
302
+ href="#/templates/contact-form"
303
+ type="supporting"
304
+ hasUnderline>
305
+ Privacy Policy
306
+ </Link>
307
+ .
308
+ </Text>
308
309
  </VStack>
309
310
  </VStack>
310
311
  </Section>