astryx-dracula 0.5.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,624 +1,600 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > (Tbar[size=sm] > (H > IB"Go back" + IB"Go forward" + Tx"Folder"[t=label]) + (SG"View mode" > SGI*4) + (H > IB"Group" + IB"Share" + IB"Tags" + IB"More" + IB"Search")) + (LC[p=0 !scroll] > (Hd"File Explorer"[level=1] + H[h=fill] > (S.transparent[p=2 w=240 dv=[end]] > UL[density=compact !hasDividers] > (LI > Ic + Tx"Item"[t=body])*3)*3 + (S.transparent[p=6] > V[g=4 a=center] > Av[size=96] + (V[g=1 a=center] > Tx"Name"[t=body] + Tx"Kind"[t=supporting]) + (ML"Information" > MLI*3))))
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] > (Tbar[size=sm dv=[bottom]] > (IB"Go back" + IB"Go forward" + Tx"Folder"[t=label]) + (SG"View mode" > SGI*4) + (IB"Group" + IB"Share" + IB"Tags" + IB"More" + IB"Search")) + (LC[p=0 !scroll] > (VH"File Explorer" + H[h=100%] > (S.transparent[p=2 w=240 dv=[end]] > UL[density=compact !hasDividers] > (LI > Ic + Tx"Item"[t=body])*3)*3 + (S.transparent[p=4 w=320] > V[g=4 a=center] > Av[size=96] + (V[g=1 a=center] > Tx"Name"[t=body] + Tx"Kind"[t=supporting]) + (ML"Information" > MLI*3))))
4
6
 
5
- import {useState, useMemo} from 'react';
6
- import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
7
- import {Toolbar} from '@astryxdesign/core/Toolbar';
8
- import {List, ListItem} from '@astryxdesign/core/List';
9
- import {HStack, VStack} from '@astryxdesign/core/Layout';
10
- import {useMediaQuery} from '@astryxdesign/core/hooks';
11
- import {Text} from '@astryxdesign/core/Text';
12
- import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
13
- import {Icon} from '@astryxdesign/core/Icon';
14
- import {IconButton} from '@astryxdesign/core/IconButton';
15
- import {Section} from '@astryxdesign/core/Section';
16
- import {EmptyState} from '@astryxdesign/core/EmptyState';
17
- import {Avatar} from '@astryxdesign/core/Avatar';
18
- import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
7
+ import { Avatar } from "@astryxdesign/core/Avatar";
8
+ import { EmptyState } from "@astryxdesign/core/EmptyState";
9
+ import { useMediaQuery } from "@astryxdesign/core/hooks";
10
+ import { Icon } from "@astryxdesign/core/Icon";
11
+ import { IconButton } from "@astryxdesign/core/IconButton";
12
+ import { HStack, Layout, LayoutContent, VStack } from "@astryxdesign/core/Layout";
13
+ import { List, ListItem } from "@astryxdesign/core/List";
14
+ import { MetadataList, MetadataListItem } from "@astryxdesign/core/MetadataList";
15
+ import { Section } from "@astryxdesign/core/Section";
16
+ import { SegmentedControl, SegmentedControlItem } from "@astryxdesign/core/SegmentedControl";
17
+ import { Text } from "@astryxdesign/core/Text";
18
+ import { Toolbar } from "@astryxdesign/core/Toolbar";
19
+ import { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden";
19
20
  import {
20
- SegmentedControl,
21
- SegmentedControlItem,
22
- } from '@astryxdesign/core/SegmentedControl';
23
- import {
24
- ChevronLeft,
25
- ChevronRight,
26
- Search,
27
- Share2,
28
- Tag,
29
- Ellipsis,
30
- Columns,
31
- LayoutGrid,
32
- Rows3,
33
- Table,
34
- SlidersHorizontal,
35
- FileText,
36
- Folder,
37
- } from 'lucide-react';
21
+ ChevronLeft,
22
+ ChevronRight,
23
+ Columns,
24
+ Ellipsis,
25
+ FileText,
26
+ Folder,
27
+ LayoutGrid,
28
+ Rows3,
29
+ Search,
30
+ Share2,
31
+ SlidersHorizontal,
32
+ Table,
33
+ Tag,
34
+ } from "lucide-react";
35
+ import { useMemo, useState } from "react";
38
36
 
39
37
  interface FileSystemItem {
40
- id: string;
41
- name: string;
42
- type: 'file' | 'folder';
43
- children?: FileSystemItem[];
38
+ id: string;
39
+ name: string;
40
+ type: "file" | "folder";
41
+ children?: FileSystemItem[];
44
42
  }
45
43
 
46
44
  const FILESYSTEM: FileSystemItem[] = [
47
- {
48
- id: 'applications',
49
- name: 'Applications',
50
- type: 'folder',
51
- children: [
52
- {
53
- id: 'chrome-apps',
54
- name: 'Chrome Apps',
55
- type: 'folder',
56
- children: [
57
- {id: 'component-lab', name: 'Palette Lab.app', type: 'file'},
58
- {id: 'google-chat', name: 'Night Chat.app', type: 'file'},
59
- {id: 'workchat', name: 'Coven.app', type: 'file'},
60
- ],
61
- },
62
- {id: 'figma', name: 'Figma.app', type: 'file'},
63
- {id: 'safari', name: 'Safari.app', type: 'file'},
64
- {id: 'slack', name: 'Slack.app', type: 'file'},
65
- {id: 'terminal', name: 'Terminal.app', type: 'file'},
66
- {id: 'vscode', name: 'Visual Studio Code.app', type: 'file'},
67
- {id: 'xcode', name: 'Xcode.app', type: 'file'},
68
- ],
69
- },
70
- {id: 'debug-log', name: 'debug-nocturne.log', type: 'file'},
71
- {
72
- id: 'desktop',
73
- name: 'Desktop',
74
- type: 'folder',
75
- children: [
76
- {id: 'screenshot1', name: 'Screenshot 2026-10-31.png', type: 'file'},
77
- {id: 'notes-txt', name: 'midnight-notes.txt', type: 'file'},
78
- {
79
- id: 'projects',
80
- name: 'Projects',
81
- type: 'folder',
82
- children: [
83
- {id: 'readme-proj', name: 'README.md', type: 'file'},
84
- {
85
- id: 'src-folder',
86
- name: 'src',
87
- type: 'folder',
88
- children: [
89
- {id: 'index-ts', name: 'index.ts', type: 'file'},
90
- {id: 'app-tsx', name: 'App.tsx', type: 'file'},
91
- ],
92
- },
93
- ],
94
- },
95
- ],
96
- },
97
- {
98
- id: 'documents',
99
- name: 'Documents',
100
- type: 'folder',
101
- children: [
102
- {id: 'design-spec', name: 'dracula-spec.pdf', type: 'file'},
103
- {id: 'resume', name: 'resume.docx', type: 'file'},
104
- {
105
- id: 'work',
106
- name: 'Work',
107
- type: 'folder',
108
- children: [
109
- {id: 'q1-report', name: 'Q1-report.xlsx', type: 'file'},
110
- {id: 'presentation', name: 'team-presentation.pptx', type: 'file'},
111
- ],
112
- },
113
- ],
114
- },
115
- {
116
- id: 'downloads',
117
- name: 'Downloads',
118
- type: 'folder',
119
- children: [
120
- {id: 'archive', name: 'archive.zip', type: 'file'},
121
- {id: 'installer', name: 'installer.dmg', type: 'file'},
122
- {id: 'photo', name: 'castle-2026.jpg', type: 'file'},
123
- ],
124
- },
125
- {id: 'login-screenshot', name: 'login-midnight-screenshot.png', type: 'file'},
126
- {
127
- id: 'movies',
128
- name: 'Movies',
129
- type: 'folder',
130
- children: [{id: 'recording', name: 'screen-recording.mov', type: 'file'}],
131
- },
132
- {
133
- id: 'music',
134
- name: 'Music',
135
- type: 'folder',
136
- children: [{id: 'playlist', name: 'favorites.m3u', type: 'file'}],
137
- },
138
- {
139
- id: 'node-modules',
140
- name: 'node_modules',
141
- type: 'folder',
142
- children: [
143
- {
144
- id: 'react',
145
- name: 'react',
146
- type: 'folder',
147
- children: [{id: 'react-index', name: 'index.js', type: 'file'}],
148
- },
149
- {
150
- id: 'react-dom',
151
- name: 'react-dom',
152
- type: 'folder',
153
- children: [{id: 'react-dom-index', name: 'index.js', type: 'file'}],
154
- },
155
- ],
156
- },
157
- {
158
- id: 'pictures',
159
- name: 'Pictures',
160
- type: 'folder',
161
- children: [
162
- {
163
- id: 'vacation',
164
- name: 'transylvania-2026',
165
- type: 'folder',
166
- children: [
167
- {id: 'img1', name: 'IMG_0001.jpg', type: 'file'},
168
- {id: 'img2', name: 'IMG_0002.jpg', type: 'file'},
169
- {id: 'img3', name: 'IMG_0003.jpg', type: 'file'},
170
- ],
171
- },
172
- {
173
- id: 'screenshots-folder',
174
- name: 'Screenshots',
175
- type: 'folder',
176
- children: [
177
- {id: 'ss1', name: 'Screen Shot 1.png', type: 'file'},
178
- {id: 'ss2', name: 'Screen Shot 2.png', type: 'file'},
179
- ],
180
- },
181
- ],
182
- },
183
- {
184
- id: 'public',
185
- name: 'Public',
186
- type: 'folder',
187
- children: [
188
- {id: 'drop-box', name: 'Drop Box', type: 'folder', children: []},
189
- ],
190
- },
191
- {
192
- id: 'astryx',
193
- name: 'astryx',
194
- type: 'folder',
195
- children: [
196
- {id: 'astryx-readme', name: 'README.md', type: 'file'},
197
- {id: 'astryx-pkg', name: 'package.json', type: 'file'},
198
- {
199
- id: 'astryx-packages',
200
- name: 'packages',
201
- type: 'folder',
202
- children: [
203
- {
204
- id: 'astryx-core',
205
- name: 'core',
206
- type: 'folder',
207
- children: [
208
- {
209
- id: 'core-src',
210
- name: 'src',
211
- type: 'folder',
212
- children: [
213
- {id: 'button-tsx', name: 'Button.tsx', type: 'file'},
214
- {id: 'card-tsx', name: 'Card.tsx', type: 'file'},
215
- {id: 'text-tsx', name: 'Text.tsx', type: 'file'},
216
- ],
217
- },
218
- ],
219
- },
220
- {
221
- id: 'astryx-cli',
222
- name: 'cli',
223
- type: 'folder',
224
- children: [{id: 'cli-index', name: 'index.ts', type: 'file'}],
225
- },
226
- ],
227
- },
228
- {
229
- id: 'astryx-apps',
230
- name: 'apps',
231
- type: 'folder',
232
- children: [
233
- {
234
- id: 'storybook',
235
- name: 'storybook',
236
- type: 'folder',
237
- children: [
238
- {
239
- id: 'sb-config',
240
- name: '.storybook',
241
- type: 'folder',
242
- children: [],
243
- },
244
- ],
245
- },
246
- {
247
- id: 'sandbox-app',
248
- name: 'sandbox',
249
- type: 'folder',
250
- children: [
251
- {
252
- id: 'sandbox-src',
253
- name: 'src',
254
- type: 'folder',
255
- children: [],
256
- },
257
- ],
258
- },
259
- ],
260
- },
261
- ],
262
- },
45
+ {
46
+ id: "applications",
47
+ name: "Applications",
48
+ type: "folder",
49
+ children: [
50
+ {
51
+ id: "chrome-apps",
52
+ name: "Chrome Apps",
53
+ type: "folder",
54
+ children: [
55
+ { id: "component-lab", name: "Palette Lab.app", type: "file" },
56
+ { id: "google-chat", name: "Night Chat.app", type: "file" },
57
+ { id: "workchat", name: "Coven.app", type: "file" },
58
+ ],
59
+ },
60
+ { id: "figma", name: "Figma.app", type: "file" },
61
+ { id: "safari", name: "Safari.app", type: "file" },
62
+ { id: "slack", name: "Slack.app", type: "file" },
63
+ { id: "terminal", name: "Terminal.app", type: "file" },
64
+ { id: "vscode", name: "Visual Studio Code.app", type: "file" },
65
+ { id: "xcode", name: "Xcode.app", type: "file" },
66
+ ],
67
+ },
68
+ { id: "debug-log", name: "debug-nocturne.log", type: "file" },
69
+ {
70
+ id: "desktop",
71
+ name: "Desktop",
72
+ type: "folder",
73
+ children: [
74
+ { id: "screenshot1", name: "Screenshot 2026-10-31.png", type: "file" },
75
+ { id: "notes-txt", name: "midnight-notes.txt", type: "file" },
76
+ {
77
+ id: "projects",
78
+ name: "Projects",
79
+ type: "folder",
80
+ children: [
81
+ { id: "readme-proj", name: "README.md", type: "file" },
82
+ {
83
+ id: "src-folder",
84
+ name: "src",
85
+ type: "folder",
86
+ children: [
87
+ { id: "index-ts", name: "index.ts", type: "file" },
88
+ { id: "app-tsx", name: "App.tsx", type: "file" },
89
+ ],
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ id: "documents",
97
+ name: "Documents",
98
+ type: "folder",
99
+ children: [
100
+ { id: "design-spec", name: "dracula-spec.pdf", type: "file" },
101
+ { id: "resume", name: "resume.docx", type: "file" },
102
+ {
103
+ id: "work",
104
+ name: "Work",
105
+ type: "folder",
106
+ children: [
107
+ { id: "q1-report", name: "Q1-report.xlsx", type: "file" },
108
+ { id: "presentation", name: "team-presentation.pptx", type: "file" },
109
+ ],
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ id: "downloads",
115
+ name: "Downloads",
116
+ type: "folder",
117
+ children: [
118
+ { id: "archive", name: "archive.zip", type: "file" },
119
+ { id: "installer", name: "installer.dmg", type: "file" },
120
+ { id: "photo", name: "castle-2026.jpg", type: "file" },
121
+ ],
122
+ },
123
+ { id: "login-screenshot", name: "login-midnight-screenshot.png", type: "file" },
124
+ {
125
+ id: "movies",
126
+ name: "Movies",
127
+ type: "folder",
128
+ children: [{ id: "recording", name: "screen-recording.mov", type: "file" }],
129
+ },
130
+ {
131
+ id: "music",
132
+ name: "Music",
133
+ type: "folder",
134
+ children: [{ id: "playlist", name: "favorites.m3u", type: "file" }],
135
+ },
136
+ {
137
+ id: "node-modules",
138
+ name: "node_modules",
139
+ type: "folder",
140
+ children: [
141
+ {
142
+ id: "react",
143
+ name: "react",
144
+ type: "folder",
145
+ children: [{ id: "react-index", name: "index.js", type: "file" }],
146
+ },
147
+ {
148
+ id: "react-dom",
149
+ name: "react-dom",
150
+ type: "folder",
151
+ children: [{ id: "react-dom-index", name: "index.js", type: "file" }],
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ id: "pictures",
157
+ name: "Pictures",
158
+ type: "folder",
159
+ children: [
160
+ {
161
+ id: "vacation",
162
+ name: "transylvania-2026",
163
+ type: "folder",
164
+ children: [
165
+ { id: "img1", name: "IMG_0001.jpg", type: "file" },
166
+ { id: "img2", name: "IMG_0002.jpg", type: "file" },
167
+ { id: "img3", name: "IMG_0003.jpg", type: "file" },
168
+ ],
169
+ },
170
+ {
171
+ id: "screenshots-folder",
172
+ name: "Screenshots",
173
+ type: "folder",
174
+ children: [
175
+ { id: "ss1", name: "Screen Shot 1.png", type: "file" },
176
+ { id: "ss2", name: "Screen Shot 2.png", type: "file" },
177
+ ],
178
+ },
179
+ ],
180
+ },
181
+ {
182
+ id: "public",
183
+ name: "Public",
184
+ type: "folder",
185
+ children: [{ id: "drop-box", name: "Drop Box", type: "folder", children: [] }],
186
+ },
187
+ {
188
+ id: "astryx",
189
+ name: "astryx",
190
+ type: "folder",
191
+ children: [
192
+ { id: "astryx-readme", name: "README.md", type: "file" },
193
+ { id: "astryx-pkg", name: "package.json", type: "file" },
194
+ {
195
+ id: "astryx-packages",
196
+ name: "packages",
197
+ type: "folder",
198
+ children: [
199
+ {
200
+ id: "astryx-core",
201
+ name: "core",
202
+ type: "folder",
203
+ children: [
204
+ {
205
+ id: "core-src",
206
+ name: "src",
207
+ type: "folder",
208
+ children: [
209
+ { id: "button-tsx", name: "Button.tsx", type: "file" },
210
+ { id: "card-tsx", name: "Card.tsx", type: "file" },
211
+ { id: "text-tsx", name: "Text.tsx", type: "file" },
212
+ ],
213
+ },
214
+ ],
215
+ },
216
+ {
217
+ id: "astryx-cli",
218
+ name: "cli",
219
+ type: "folder",
220
+ children: [{ id: "cli-index", name: "index.ts", type: "file" }],
221
+ },
222
+ ],
223
+ },
224
+ {
225
+ id: "astryx-apps",
226
+ name: "apps",
227
+ type: "folder",
228
+ children: [
229
+ {
230
+ id: "storybook",
231
+ name: "storybook",
232
+ type: "folder",
233
+ children: [
234
+ {
235
+ id: "sb-config",
236
+ name: ".storybook",
237
+ type: "folder",
238
+ children: [],
239
+ },
240
+ ],
241
+ },
242
+ {
243
+ id: "sandbox-app",
244
+ name: "sandbox",
245
+ type: "folder",
246
+ children: [
247
+ {
248
+ id: "sandbox-src",
249
+ name: "src",
250
+ type: "folder",
251
+ children: [],
252
+ },
253
+ ],
254
+ },
255
+ ],
256
+ },
257
+ ],
258
+ },
263
259
  ];
264
260
 
265
261
  // Static toolbar content, hoisted out of the render path: neither element
266
262
  // reads component state, so rebuilding them per render buys nothing.
267
263
  const viewSwitcher = (
268
- <SegmentedControl value="column" onChange={() => {}} label="View mode">
269
- <SegmentedControlItem
270
- value="grid"
271
- label="Grid"
272
- icon={<Icon icon={LayoutGrid} size="sm" />}
273
- isLabelHidden
274
- />
275
- <SegmentedControlItem
276
- value="list"
277
- label="List"
278
- icon={<Icon icon={Rows3} size="sm" />}
279
- isLabelHidden
280
- />
281
- <SegmentedControlItem
282
- value="column"
283
- label="Column"
284
- icon={<Icon icon={Columns} size="sm" />}
285
- isLabelHidden
286
- />
287
- <SegmentedControlItem
288
- value="gallery"
289
- label="Gallery"
290
- icon={<Icon icon={Table} size="sm" />}
291
- isLabelHidden
292
- />
293
- </SegmentedControl>
264
+ <SegmentedControl value="column" onChange={() => {}} label="View mode">
265
+ <SegmentedControlItem
266
+ value="grid"
267
+ label="Grid"
268
+ icon={<Icon icon={LayoutGrid} size="sm" />}
269
+ isLabelHidden
270
+ />
271
+ <SegmentedControlItem
272
+ value="list"
273
+ label="List"
274
+ icon={<Icon icon={Rows3} size="sm" />}
275
+ isLabelHidden
276
+ />
277
+ <SegmentedControlItem
278
+ value="column"
279
+ label="Column"
280
+ icon={<Icon icon={Columns} size="sm" />}
281
+ isLabelHidden
282
+ />
283
+ <SegmentedControlItem
284
+ value="gallery"
285
+ label="Gallery"
286
+ icon={<Icon icon={Table} size="sm" />}
287
+ isLabelHidden
288
+ />
289
+ </SegmentedControl>
294
290
  );
295
291
  const fileActions = (
296
- <>
297
- <IconButton
298
- variant="ghost"
299
- size="sm"
300
- icon={<Icon icon={SlidersHorizontal} size="sm" />}
301
- label="Group"
302
- tooltip="Group"
303
- />
304
- <IconButton
305
- variant="ghost"
306
- size="sm"
307
- icon={<Icon icon={Share2} size="sm" />}
308
- label="Share"
309
- tooltip="Share"
310
- />
311
- <IconButton
312
- variant="ghost"
313
- size="sm"
314
- icon={<Icon icon={Tag} size="sm" />}
315
- label="Tags"
316
- tooltip="Tags"
317
- />
318
- <IconButton
319
- variant="ghost"
320
- size="sm"
321
- icon={<Icon icon={Ellipsis} size="sm" />}
322
- label="More"
323
- tooltip="More"
324
- />
325
- <IconButton
326
- variant="ghost"
327
- size="sm"
328
- icon={<Icon icon={Search} size="sm" />}
329
- label="Search"
330
- tooltip="Search"
331
- />
332
- </>
292
+ <>
293
+ <IconButton
294
+ variant="ghost"
295
+ size="sm"
296
+ icon={<Icon icon={SlidersHorizontal} size="sm" />}
297
+ label="Group"
298
+ tooltip="Group"
299
+ />
300
+ <IconButton
301
+ variant="ghost"
302
+ size="sm"
303
+ icon={<Icon icon={Share2} size="sm" />}
304
+ label="Share"
305
+ tooltip="Share"
306
+ />
307
+ <IconButton
308
+ variant="ghost"
309
+ size="sm"
310
+ icon={<Icon icon={Tag} size="sm" />}
311
+ label="Tags"
312
+ tooltip="Tags"
313
+ />
314
+ <IconButton
315
+ variant="ghost"
316
+ size="sm"
317
+ icon={<Icon icon={Ellipsis} size="sm" />}
318
+ label="More"
319
+ tooltip="More"
320
+ />
321
+ <IconButton
322
+ variant="ghost"
323
+ size="sm"
324
+ icon={<Icon icon={Search} size="sm" />}
325
+ label="Search"
326
+ tooltip="Search"
327
+ />
328
+ </>
333
329
  );
334
330
 
335
331
  function findItem(items: FileSystemItem[], id: string): FileSystemItem | null {
336
- for (const item of items) {
337
- if (item.id === id) {
338
- return item;
339
- }
340
- if (item.children) {
341
- const found = findItem(item.children, id);
342
- if (found) {
343
- return found;
344
- }
345
- }
346
- }
347
- return null;
332
+ for (const item of items) {
333
+ if (item.id === id) {
334
+ return item;
335
+ }
336
+ if (item.children) {
337
+ const found = findItem(item.children, id);
338
+ if (found) {
339
+ return found;
340
+ }
341
+ }
342
+ }
343
+ return null;
348
344
  }
349
345
 
350
346
  function getFileExtension(name: string): string {
351
- const dot = name.lastIndexOf('.');
352
- return dot > 0 ? name.substring(dot + 1).toUpperCase() : 'File';
347
+ const dot = name.lastIndexOf(".");
348
+ return dot > 0 ? name.substring(dot + 1).toUpperCase() : "File";
353
349
  }
354
350
 
355
351
  export default function FileExplorer() {
356
- const [selectedPath, setSelectedPath] = useState<string[]>([
357
- 'applications',
358
- 'chrome-apps',
359
- 'component-lab',
360
- ]);
352
+ const [selectedPath, setSelectedPath] = useState<string[]>([
353
+ "applications",
354
+ "chrome-apps",
355
+ "component-lab",
356
+ ]);
361
357
 
362
- const isNarrow = useMediaQuery('(max-width: 768px)');
358
+ const isNarrow = useMediaQuery("(max-width: 768px)");
363
359
 
364
- const columns = useMemo(() => {
365
- // `id` names the folder a column lists, so the React key stays put when
366
- // narrow mode slices the row down to the last visible column.
367
- const cols: {
368
- id: string;
369
- items: FileSystemItem[];
370
- selectedId: string | null;
371
- }[] = [];
372
- cols.push({
373
- id: 'root',
374
- items: FILESYSTEM,
375
- selectedId: selectedPath[0] ?? null,
376
- });
377
- let currentItems: FileSystemItem[] = FILESYSTEM;
378
- for (let i = 0; i < selectedPath.length; i++) {
379
- const selected = currentItems.find(item => item.id === selectedPath[i]);
380
- if (selected?.children && selected.children.length > 0) {
381
- cols.push({
382
- id: selectedPath[i],
383
- items: selected.children,
384
- selectedId: selectedPath[i + 1] ?? null,
385
- });
386
- currentItems = selected.children;
387
- } else {
388
- break;
389
- }
390
- }
391
- return cols;
392
- }, [selectedPath]);
360
+ const columns = useMemo(() => {
361
+ // `id` names the folder a column lists, so the React key stays put when
362
+ // narrow mode slices the row down to the last visible column.
363
+ const cols: {
364
+ id: string;
365
+ items: FileSystemItem[];
366
+ selectedId: string | null;
367
+ }[] = [];
368
+ cols.push({
369
+ id: "root",
370
+ items: FILESYSTEM,
371
+ selectedId: selectedPath[0] ?? null,
372
+ });
373
+ let currentItems: FileSystemItem[] = FILESYSTEM;
374
+ for (let i = 0; i < selectedPath.length; i++) {
375
+ const selected = currentItems.find((item) => item.id === selectedPath[i]);
376
+ if (selected?.children && selected.children.length > 0) {
377
+ cols.push({
378
+ id: selectedPath[i],
379
+ items: selected.children,
380
+ selectedId: selectedPath[i + 1] ?? null,
381
+ });
382
+ currentItems = selected.children;
383
+ } else {
384
+ break;
385
+ }
386
+ }
387
+ return cols;
388
+ }, [selectedPath]);
393
389
 
394
- const columnOffset = isNarrow ? Math.max(columns.length - 1, 0) : 0;
395
- const visibleColumns = isNarrow ? columns.slice(-1) : columns;
390
+ const columnOffset = isNarrow ? Math.max(columns.length - 1, 0) : 0;
391
+ const visibleColumns = isNarrow ? columns.slice(-1) : columns;
396
392
 
397
- const currentFolderName = useMemo(() => {
398
- if (selectedPath.length === 0) {
399
- return 'Home';
400
- }
401
- const lastId = selectedPath[selectedPath.length - 1];
402
- const item = findItem(FILESYSTEM, lastId);
403
- if (item?.type === 'folder') {
404
- return item.name;
405
- }
406
- if (selectedPath.length >= 2) {
407
- const parent = findItem(
408
- FILESYSTEM,
409
- selectedPath[selectedPath.length - 2],
410
- );
411
- return parent?.name ?? 'Home';
412
- }
413
- return 'Home';
414
- }, [selectedPath]);
393
+ const currentFolderName = useMemo(() => {
394
+ if (selectedPath.length === 0) {
395
+ return "Home";
396
+ }
397
+ const lastId = selectedPath[selectedPath.length - 1];
398
+ const item = findItem(FILESYSTEM, lastId);
399
+ if (item?.type === "folder") {
400
+ return item.name;
401
+ }
402
+ if (selectedPath.length >= 2) {
403
+ const parent = findItem(FILESYSTEM, selectedPath[selectedPath.length - 2]);
404
+ return parent?.name ?? "Home";
405
+ }
406
+ return "Home";
407
+ }, [selectedPath]);
415
408
 
416
- const selectedFile = useMemo(() => {
417
- if (selectedPath.length === 0) {
418
- return null;
419
- }
420
- const lastId = selectedPath[selectedPath.length - 1];
421
- const item = findItem(FILESYSTEM, lastId);
422
- return item?.type === 'file' ? item : null;
423
- }, [selectedPath]);
409
+ const selectedFile = useMemo(() => {
410
+ if (selectedPath.length === 0) {
411
+ return null;
412
+ }
413
+ const lastId = selectedPath[selectedPath.length - 1];
414
+ const item = findItem(FILESYSTEM, lastId);
415
+ return item?.type === "file" ? item : null;
416
+ }, [selectedPath]);
424
417
 
425
- // Narrow mode is one pane at a time, the same master→detail shape the
426
- // folder columns already use: the deepest column, or the file preview when
427
- // the selection ends on a file. Beside each other the column and the
428
- // preview need 240 + 320 = 560px, so in a 390px pane the preview was cut at
429
- // the viewport edge with nothing to scroll it into view. "Go back" (:464)
430
- // pops the selected file off the path, so it is also the way back from the
431
- // preview to the column.
432
- const isPreviewPane = isNarrow && selectedFile != null;
418
+ // Narrow mode is one pane at a time, the same master→detail shape the
419
+ // folder columns already use: the deepest column, or the file preview when
420
+ // the selection ends on a file. Beside each other the column and the
421
+ // preview need 240 + 320 = 560px, so in a 390px pane the preview was cut at
422
+ // the viewport edge with nothing to scroll it into view. "Go back" (:464)
423
+ // pops the selected file off the path, so it is also the way back from the
424
+ // preview to the column.
425
+ const isPreviewPane = isNarrow && selectedFile != null;
433
426
 
434
- const handleSelect = (columnIndex: number, itemId: string) => {
435
- setSelectedPath([...selectedPath.slice(0, columnIndex), itemId]);
436
- };
427
+ const handleSelect = (columnIndex: number, itemId: string) => {
428
+ setSelectedPath([...selectedPath.slice(0, columnIndex), itemId]);
429
+ };
437
430
 
438
- const titleContent = (
439
- <>
440
- <IconButton
441
- variant="ghost"
442
- size="sm"
443
- icon={<Icon icon={ChevronLeft} size="sm" />}
444
- onClick={() => {
445
- if (selectedPath.length > 0) {
446
- setSelectedPath(selectedPath.slice(0, -1));
447
- }
448
- }}
449
- isDisabled={selectedPath.length === 0}
450
- label="Go back"
451
- tooltip="Go back"
452
- />
453
- <IconButton
454
- variant="ghost"
455
- size="sm"
456
- icon={<Icon icon={ChevronRight} size="sm" />}
457
- isDisabled
458
- label="Go forward"
459
- tooltip="Go forward"
460
- />
461
- <Text type="label" maxLines={1}>
462
- {currentFolderName}
463
- </Text>
464
- </>
465
- );
431
+ const titleContent = (
432
+ <>
433
+ <IconButton
434
+ variant="ghost"
435
+ size="sm"
436
+ icon={<Icon icon={ChevronLeft} size="sm" />}
437
+ onClick={() => {
438
+ if (selectedPath.length > 0) {
439
+ setSelectedPath(selectedPath.slice(0, -1));
440
+ }
441
+ }}
442
+ isDisabled={selectedPath.length === 0}
443
+ label="Go back"
444
+ tooltip="Go back"
445
+ />
446
+ <IconButton
447
+ variant="ghost"
448
+ size="sm"
449
+ icon={<Icon icon={ChevronRight} size="sm" />}
450
+ isDisabled
451
+ label="Go forward"
452
+ tooltip="Go forward"
453
+ />
454
+ <Text type="label" maxLines={1}>
455
+ {currentFolderName}
456
+ </Text>
457
+ </>
458
+ );
466
459
 
467
- return (
468
- <Layout
469
- style={{height: '100dvh'}}
470
- height="fill"
471
- header={
472
- <>
473
- <Toolbar
474
- label="File Explorer"
475
- size="sm"
476
- dividers={isNarrow ? undefined : ['bottom']}
477
- startContent={titleContent}
478
- centerContent={isNarrow ? undefined : viewSwitcher}
479
- endContent={isNarrow ? undefined : fileActions}
480
- />
481
- {isNarrow && (
482
- <Section
483
- variant="transparent"
484
- padding={2}
485
- dividers={['bottom']}>
486
- <HStack
487
- gap={2}
488
- vAlign="center"
489
- style={{overflowX: 'auto'}}
490
- role="region"
491
- aria-label="View and file actions"
492
- tabIndex={0}>
493
- {viewSwitcher}
494
- {fileActions}
495
- </HStack>
496
- </Section>
497
- )}
498
- </>
499
- }
500
- content={
501
- <LayoutContent padding={0} isScrollable={false}>
502
- <VisuallyHidden as="h1">
503
- File Explorer
504
- </VisuallyHidden>
505
- <HStack height="100%" style={{overflowX: 'auto', overflowY: 'hidden'}}>
506
- {!isPreviewPane &&
507
- visibleColumns.map((col, colIndex) => {
508
- const trueIndex = columnOffset + colIndex;
509
- const showDivider =
510
- trueIndex < columns.length - 1 || selectedFile != null;
511
- return (
512
- <Section
513
- key={col.id}
514
- width={240}
515
- padding={2}
516
- variant="transparent"
517
- dividers={showDivider ? ['end'] : undefined}
518
- style={{overflowY: 'auto', flexShrink: 0}}
519
- role="region"
520
- aria-label={`Folder column ${trueIndex + 1}`}
521
- tabIndex={0}>
522
- {col.items.length === 0 ? (
523
- <EmptyState
524
- title="Quiet in the crypt"
525
- description="This folder holds no scrolls yet."
526
- isCompact
527
- />
528
- ) : (
529
- <List density="compact" hasDividers={false}>
530
- {col.items.map(item => {
531
- const isSelected = col.selectedId === item.id;
532
- const hasChildren =
533
- item.type === 'folder' &&
534
- item.children != null &&
535
- item.children.length > 0;
536
- return (
537
- <ListItem
538
- key={item.id}
539
- label={
540
- <Text type="body" maxLines={1}>
541
- {item.name}
542
- </Text>
543
- }
544
- startContent={
545
- <Icon
546
- icon={
547
- item.type === 'folder'
548
- ? Folder
549
- : FileText
550
- }
551
- color={
552
- item.type === 'folder'
553
- ? 'accent'
554
- : 'secondary'
555
- }
556
- size="sm"
557
- />
558
- }
559
- endContent={
560
- hasChildren ? (
561
- <Icon
562
- icon={ChevronRight}
563
- size="xsm"
564
- color="secondary"
565
- />
566
- ) : undefined
567
- }
568
- onClick={() => handleSelect(trueIndex, item.id)}
569
- isSelected={isSelected}
570
- />
571
- );
572
- })}
573
- </List>
574
- )}
575
- </Section>
576
- );
577
- })}
578
- {selectedFile && (
579
- <Section
580
- width={isPreviewPane ? '100%' : 320}
581
- // 16px, not 24. The sibling column at :529 is padding={2}
582
- // (8px) because it holds a dense compact List, and these two
583
- // sit in one HStack (:519) as columns of the same view --
584
- // 24 beside 8 made the compact column look broken rather
585
- // than compact. 16 is the outer-inset value this repo already
586
- // uses for exactly this (scaffolds.md new-site recipe).
587
- padding={4}
588
- variant="transparent"
589
- style={{overflowY: 'auto', flexGrow: 1, flexShrink: 0}}
590
- role="region"
591
- aria-label={`Details for ${selectedFile.name}`}
592
- tabIndex={0}>
593
- <VStack gap={4} hAlign="center">
594
- <Avatar name={selectedFile.name} size={96} />
595
- <VStack gap={1} hAlign="center">
596
- <Text type="body" weight="semibold" maxLines={1}>
597
- {selectedFile.name}
598
- </Text>
599
- <Text type="supporting" color="secondary">
600
- {getFileExtension(selectedFile.name)} Document
601
- </Text>
602
- </VStack>
603
- <MetadataList title="Information">
604
- <MetadataListItem label="Created">
605
- <Text type="body">March 28, 2026 at 2:15 PM</Text>
606
- </MetadataListItem>
607
- <MetadataListItem label="Modified">
608
- <Text type="body">Yesterday, 10:27 PM</Text>
609
- </MetadataListItem>
610
- <MetadataListItem label="Kind">
611
- <Text type="body">
612
- {getFileExtension(selectedFile.name)} Document
613
- </Text>
614
- </MetadataListItem>
615
- </MetadataList>
616
- </VStack>
617
- </Section>
618
- )}
619
- </HStack>
620
- </LayoutContent>
621
- }
622
- />
623
- );
460
+ return (
461
+ <Layout
462
+ style={{ height: "100dvh" }}
463
+ height="fill"
464
+ header={
465
+ <>
466
+ <Toolbar
467
+ label="File Explorer"
468
+ size="sm"
469
+ dividers={isNarrow ? undefined : ["bottom"]}
470
+ startContent={titleContent}
471
+ centerContent={isNarrow ? undefined : viewSwitcher}
472
+ endContent={isNarrow ? undefined : fileActions}
473
+ />
474
+ {isNarrow && (
475
+ <Section variant="transparent" padding={2} dividers={["bottom"]}>
476
+ <HStack
477
+ gap={2}
478
+ vAlign="center"
479
+ style={{ overflowX: "auto" }}
480
+ role="region"
481
+ aria-label="View and file actions"
482
+ tabIndex={0}
483
+ >
484
+ {viewSwitcher}
485
+ {fileActions}
486
+ </HStack>
487
+ </Section>
488
+ )}
489
+ </>
490
+ }
491
+ content={
492
+ <LayoutContent padding={0} isScrollable={false}>
493
+ <VisuallyHidden as="h1">File Explorer</VisuallyHidden>
494
+ <HStack height="100%" style={{ overflowX: "auto", overflowY: "hidden" }}>
495
+ {!isPreviewPane &&
496
+ visibleColumns.map((col, colIndex) => {
497
+ const trueIndex = columnOffset + colIndex;
498
+ const showDivider = trueIndex < columns.length - 1 || selectedFile != null;
499
+ return (
500
+ <Section
501
+ key={col.id}
502
+ width={240}
503
+ padding={2}
504
+ variant="transparent"
505
+ dividers={showDivider ? ["end"] : undefined}
506
+ style={{ overflowY: "auto", flexShrink: 0 }}
507
+ role="region"
508
+ aria-label={`Folder column ${trueIndex + 1}`}
509
+ tabIndex={0}
510
+ >
511
+ {col.items.length === 0 ? (
512
+ <EmptyState
513
+ title="Quiet in the crypt"
514
+ description="This folder holds no scrolls yet."
515
+ isCompact
516
+ />
517
+ ) : (
518
+ <List density="compact" hasDividers={false}>
519
+ {col.items.map((item) => {
520
+ const isSelected = col.selectedId === item.id;
521
+ const hasChildren =
522
+ item.type === "folder" &&
523
+ item.children != null &&
524
+ item.children.length > 0;
525
+ return (
526
+ <ListItem
527
+ key={item.id}
528
+ label={
529
+ <Text type="body" maxLines={1}>
530
+ {item.name}
531
+ </Text>
532
+ }
533
+ startContent={
534
+ <Icon
535
+ icon={item.type === "folder" ? Folder : FileText}
536
+ color={item.type === "folder" ? "accent" : "secondary"}
537
+ size="sm"
538
+ />
539
+ }
540
+ endContent={
541
+ hasChildren ? (
542
+ <Icon icon={ChevronRight} size="xsm" color="secondary" />
543
+ ) : undefined
544
+ }
545
+ onClick={() => handleSelect(trueIndex, item.id)}
546
+ isSelected={isSelected}
547
+ />
548
+ );
549
+ })}
550
+ </List>
551
+ )}
552
+ </Section>
553
+ );
554
+ })}
555
+ {selectedFile && (
556
+ <Section
557
+ width={isPreviewPane ? "100%" : 320}
558
+ // 16px, not 24. The sibling column at :529 is padding={2}
559
+ // (8px) because it holds a dense compact List, and these two
560
+ // sit in one HStack (:519) as columns of the same view --
561
+ // 24 beside 8 made the compact column look broken rather
562
+ // than compact. 16 is the outer-inset value this repo already
563
+ // uses for exactly this (scaffolds.md new-site recipe).
564
+ padding={4}
565
+ variant="transparent"
566
+ style={{ overflowY: "auto", flexGrow: 1, flexShrink: 0 }}
567
+ role="region"
568
+ aria-label={`Details for ${selectedFile.name}`}
569
+ tabIndex={0}
570
+ >
571
+ <VStack gap={4} hAlign="center">
572
+ <Avatar name={selectedFile.name} size={96} />
573
+ <VStack gap={1} hAlign="center">
574
+ <Text type="body" weight="semibold" maxLines={1}>
575
+ {selectedFile.name}
576
+ </Text>
577
+ <Text type="supporting" color="secondary">
578
+ {getFileExtension(selectedFile.name)} Document
579
+ </Text>
580
+ </VStack>
581
+ <MetadataList title="Information">
582
+ <MetadataListItem label="Created">
583
+ <Text type="body">March 28, 2026 at 2:15 PM</Text>
584
+ </MetadataListItem>
585
+ <MetadataListItem label="Modified">
586
+ <Text type="body">Yesterday, 10:27 PM</Text>
587
+ </MetadataListItem>
588
+ <MetadataListItem label="Kind">
589
+ <Text type="body">{getFileExtension(selectedFile.name)} Document</Text>
590
+ </MetadataListItem>
591
+ </MetadataList>
592
+ </VStack>
593
+ </Section>
594
+ )}
595
+ </HStack>
596
+ </LayoutContent>
597
+ }
598
+ />
599
+ );
624
600
  }