astryx-dracula 0.2.1 → 0.3.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.
- package/BRAND.md +30 -18
- package/README.md +17 -12
- package/USAGE.md +29 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +174 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +94 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +66 -33
- package/templates/ai-chat.tsx +36 -19
- package/templates/blank.tsx +4 -3
- package/templates/centered-hero.tsx +14 -125
- package/templates/classic-gallery.tsx +20 -66
- package/templates/contact-form.tsx +59 -39
- package/templates/dashboard-portfolio.tsx +143 -226
- package/templates/dashboard.tsx +122 -122
- package/templates/detail-page.tsx +95 -91
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +83 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +29 -19
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +52 -74
- package/templates/editor.tsx +112 -84
- package/templates/file-explorer.tsx +131 -84
- package/templates/form-two-column.tsx +61 -125
- package/templates/gallery-hero.tsx +15 -171
- package/templates/ide.tsx +22 -20
- package/templates/incident-console.tsx +104 -70
- package/templates/kanban-board.tsx +99 -42
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +37 -42
- package/templates/login-card.tsx +58 -87
- package/templates/login-split.tsx +88 -126
- package/templates/login-sso.tsx +105 -64
- package/templates/login.tsx +107 -53
- package/templates/messaging-shell.tsx +171 -144
- package/templates/mixed-gallery.tsx +29 -130
- package/templates/payment-form.tsx +368 -290
- package/templates/product-detail.tsx +39 -58
- package/templates/product-gallery.tsx +30 -41
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +135 -282
- package/templates/settings-sidebar.tsx +178 -268
- package/templates/settings.tsx +41 -14
- package/templates/shell-nav.tsx +31 -24
- package/templates/shell-side-nav.tsx +29 -20
- package/templates/shell-top-nav.tsx +80 -44
- package/templates/side-gallery.tsx +38 -94
- package/templates/table-grouped.tsx +62 -34
- package/templates/table-page-chart.tsx +23 -302
- package/templates/table-page-heatmap-status.tsx +156 -92
- package/templates/table-page-shoe-store-heatmap.tsx +18 -293
- package/templates/table-page.tsx +23 -7
- package/templates/table.tsx +23 -8
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +299 -237
- package/theme.css +76 -14
- package/tokens.css +425 -30
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Chaptered-doc TYPES and FRAME STYLES, split from the components.
|
|
2
|
+
//
|
|
3
|
+
// WHY: chaptered-doc.tsx exports components, and a .tsx exporting
|
|
4
|
+
// non-components breaks React Fast Refresh.
|
|
5
|
+
// `react-doctor/only-export-components` catches it. See shared/scene-hues.ts
|
|
6
|
+
// for the same split, same reason.
|
|
7
|
+
//
|
|
8
|
+
// Everything the four callers (chaptered-doc, documentation-design,
|
|
9
|
+
// documentation-technical, product-tour, tech-report) share lives here; the
|
|
10
|
+
// outline-building and scroll helpers that only chaptered-doc itself calls stay
|
|
11
|
+
// unexported in that file.
|
|
12
|
+
import type {CSSProperties} from 'react';
|
|
13
|
+
import type {IconType} from '@astryxdesign/core/Icon';
|
|
14
|
+
import type {OutlineItem} from '@astryxdesign/core/Outline';
|
|
15
|
+
|
|
16
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface DocSection {
|
|
19
|
+
// Unique within the chapter; the DOM id is derived from it so the outline
|
|
20
|
+
// links and the headings can never drift apart.
|
|
21
|
+
key: string;
|
|
22
|
+
heading: string;
|
|
23
|
+
body: string;
|
|
24
|
+
bullets?: string[];
|
|
25
|
+
code?: {language: string; title: string; source: string};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DocChapter {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
icon: IconType;
|
|
32
|
+
intro: string;
|
|
33
|
+
hasArt?: boolean;
|
|
34
|
+
sections: DocSection[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ChapterGroup = {title: string; chapters: DocChapter[]};
|
|
38
|
+
|
|
39
|
+
export const sectionId = (chapterId: string, key: string) =>
|
|
40
|
+
`${chapterId}--${key}`;
|
|
41
|
+
|
|
42
|
+
// Astryx has no image primitive: AspectRatio exposes no objectFit or radius
|
|
43
|
+
// props, so the scene fill and the corner clip live in these two styles.
|
|
44
|
+
export const sceneFill: CSSProperties = {
|
|
45
|
+
width: '100%',
|
|
46
|
+
height: '100%',
|
|
47
|
+
display: 'block',
|
|
48
|
+
};
|
|
49
|
+
export const sceneClip: CSSProperties = {
|
|
50
|
+
borderRadius: 'var(--radius-container)',
|
|
51
|
+
overflow: 'clip',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// The outline is sticky so it tracks the chapter as the document scrolls.
|
|
55
|
+
export const outlinePanel: CSSProperties = {
|
|
56
|
+
position: 'sticky',
|
|
57
|
+
top: 'var(--spacing-6)',
|
|
58
|
+
alignSelf: 'start',
|
|
59
|
+
// No paddingBlockStart. This object is applied via `style=` to the outline
|
|
60
|
+
// LayoutPanels (chaptered-doc.tsx:316, documentation-design.tsx:592,
|
|
61
|
+
// documentation-technical.tsx:82), and an inline style prop BEATS the stylex
|
|
62
|
+
// class -- so this 8px silently overrode the panel's own 16px block padding
|
|
63
|
+
// at the top edge only, leaving every "On this page" rail in the kit
|
|
64
|
+
// asymmetric by 8px. The panel already owns its block padding.
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Derived once per page so the Outline receives a stable array identity
|
|
68
|
+
// and does not re-register its scroll spy on every render.
|
|
69
|
+
export function buildOutlineByChapter(
|
|
70
|
+
chapters: DocChapter[],
|
|
71
|
+
): Record<string, OutlineItem[]> {
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
chapters.map(chapter => [
|
|
74
|
+
chapter.id,
|
|
75
|
+
chapter.sections.map(section => ({
|
|
76
|
+
id: sectionId(chapter.id, section.key),
|
|
77
|
+
label: section.heading,
|
|
78
|
+
level: 2,
|
|
79
|
+
})),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function scrollToSection(id: string) {
|
|
85
|
+
const target = document.getElementById(id);
|
|
86
|
+
if (target != null) {
|
|
87
|
+
target.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Rail ────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Identical scaffold shared by the product-tour and tech-report templates:
|
|
2
|
+
// chapter rail (SideNav) + Layout content + sticky Outline that collapses to
|
|
3
|
+
// an "On this page" Selector below 1024px (the AppShell drawer owns the rail
|
|
4
|
+
// there behind the MobileNavToggle). Chapter data stays in each template.
|
|
5
|
+
// Scene systems stay local too: product-tour keeps BAT_WING/BAT_BODY/FIR +
|
|
6
|
+
// NightScene/PaletteScene/TypeScene/PagesScene, tech-report keeps its own.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildOutlineByChapter,
|
|
10
|
+
outlinePanel,
|
|
11
|
+
sceneClip,
|
|
12
|
+
sceneFill,
|
|
13
|
+
scrollToSection,
|
|
14
|
+
sectionId,
|
|
15
|
+
type ChapterGroup,
|
|
16
|
+
type DocChapter,
|
|
17
|
+
type DocSection,
|
|
18
|
+
} from 'astryx-dracula/shared/chaptered-doc-config';
|
|
19
|
+
import {useState, type CSSProperties, type ReactNode} from 'react';
|
|
20
|
+
|
|
21
|
+
import {AppShell} from '@astryxdesign/core/AppShell';
|
|
22
|
+
import {MobileNavToggle} from '@astryxdesign/core/MobileNav';
|
|
23
|
+
import {
|
|
24
|
+
SideNav,
|
|
25
|
+
SideNavHeading,
|
|
26
|
+
SideNavItem,
|
|
27
|
+
SideNavSection,
|
|
28
|
+
} from '@astryxdesign/core/SideNav';
|
|
29
|
+
import {NavIcon} from '@astryxdesign/core/NavIcon';
|
|
30
|
+
import {
|
|
31
|
+
HStack,
|
|
32
|
+
Layout,
|
|
33
|
+
LayoutContent,
|
|
34
|
+
LayoutPanel,
|
|
35
|
+
StackItem,
|
|
36
|
+
VStack,
|
|
37
|
+
} from '@astryxdesign/core/Layout';
|
|
38
|
+
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
39
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
40
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
41
|
+
import type {IconType} from '@astryxdesign/core/Icon';
|
|
42
|
+
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
43
|
+
import {CodeBlock} from '@astryxdesign/core/CodeBlock';
|
|
44
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
45
|
+
import {List, ListItem} from '@astryxdesign/core/List';
|
|
46
|
+
import {Outline} from '@astryxdesign/core/Outline';
|
|
47
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
48
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
49
|
+
|
|
50
|
+
import {ChevronLeft, ChevronRight} from 'lucide-react';
|
|
51
|
+
|
|
52
|
+
export function ChapterRail({
|
|
53
|
+
groups,
|
|
54
|
+
activeId,
|
|
55
|
+
onSelect,
|
|
56
|
+
heading,
|
|
57
|
+
subheading,
|
|
58
|
+
headingHref,
|
|
59
|
+
headingIcon,
|
|
60
|
+
}: {
|
|
61
|
+
groups: ChapterGroup[];
|
|
62
|
+
activeId: string;
|
|
63
|
+
onSelect: (id: string) => void;
|
|
64
|
+
heading: string;
|
|
65
|
+
subheading: string;
|
|
66
|
+
headingHref: string;
|
|
67
|
+
headingIcon: ReactNode;
|
|
68
|
+
}) {
|
|
69
|
+
// Resizable like the shell-side-nav template: 264 default, 220-400 range.
|
|
70
|
+
// The AppShell drawer owns the rail below 1024px (see MobileNavToggle),
|
|
71
|
+
// so the handle only matters at desktop widths.
|
|
72
|
+
return (
|
|
73
|
+
<SideNav
|
|
74
|
+
collapsible
|
|
75
|
+
resizable={{defaultWidth: 264, minWidth: 220, maxWidth: 400}}
|
|
76
|
+
header={
|
|
77
|
+
<SideNavHeading
|
|
78
|
+
icon={<NavIcon icon={headingIcon} />}
|
|
79
|
+
heading={heading}
|
|
80
|
+
subheading={subheading}
|
|
81
|
+
headingHref={headingHref}
|
|
82
|
+
/>
|
|
83
|
+
}>
|
|
84
|
+
{groups.map(group => (
|
|
85
|
+
<SideNavSection key={group.title} title={group.title}>
|
|
86
|
+
{group.chapters.map(chapter => (
|
|
87
|
+
<SideNavItem
|
|
88
|
+
key={chapter.id}
|
|
89
|
+
label={chapter.title}
|
|
90
|
+
icon={chapter.icon}
|
|
91
|
+
isSelected={chapter.id === activeId}
|
|
92
|
+
onClick={() => onSelect(chapter.id)}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</SideNavSection>
|
|
96
|
+
))}
|
|
97
|
+
</SideNav>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Chapter body ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function SectionBlock({
|
|
104
|
+
chapterId,
|
|
105
|
+
section,
|
|
106
|
+
}: {
|
|
107
|
+
chapterId: string;
|
|
108
|
+
section: DocSection;
|
|
109
|
+
}) {
|
|
110
|
+
return (
|
|
111
|
+
<VStack gap={3}>
|
|
112
|
+
<Heading level={2} id={sectionId(chapterId, section.key)}>
|
|
113
|
+
{section.heading}
|
|
114
|
+
</Heading>
|
|
115
|
+
<Text type="body" color="secondary" display="block">
|
|
116
|
+
{section.body}
|
|
117
|
+
</Text>
|
|
118
|
+
{section.bullets != null && (
|
|
119
|
+
<List listStyle="disc">
|
|
120
|
+
{section.bullets.map(bullet => (
|
|
121
|
+
<ListItem key={bullet} label={bullet} />
|
|
122
|
+
))}
|
|
123
|
+
</List>
|
|
124
|
+
)}
|
|
125
|
+
{section.code != null && (
|
|
126
|
+
<CodeBlock
|
|
127
|
+
code={section.code.source}
|
|
128
|
+
language={section.code.language}
|
|
129
|
+
title={section.code.title}
|
|
130
|
+
width="100%"
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</VStack>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function ChapterNav({
|
|
138
|
+
chapters,
|
|
139
|
+
chapter,
|
|
140
|
+
onSelect,
|
|
141
|
+
}: {
|
|
142
|
+
chapters: DocChapter[];
|
|
143
|
+
chapter: DocChapter;
|
|
144
|
+
onSelect: (id: string) => void;
|
|
145
|
+
}) {
|
|
146
|
+
const index = chapters.findIndex(entry => entry.id === chapter.id);
|
|
147
|
+
const previous = index > 0 ? chapters[index - 1] : undefined;
|
|
148
|
+
const next = index < chapters.length - 1 ? chapters[index + 1] : undefined;
|
|
149
|
+
return (
|
|
150
|
+
<HStack gap={3} hAlign="between" vAlign="center">
|
|
151
|
+
{previous != null ? (
|
|
152
|
+
<Button
|
|
153
|
+
label={previous.title}
|
|
154
|
+
variant="secondary"
|
|
155
|
+
icon={<Icon icon={ChevronLeft} size="sm" />}
|
|
156
|
+
onClick={() => onSelect(previous.id)}
|
|
157
|
+
/>
|
|
158
|
+
) : (
|
|
159
|
+
<StackItem size="fill" />
|
|
160
|
+
)}
|
|
161
|
+
{next != null ? (
|
|
162
|
+
<Button
|
|
163
|
+
label={next.title}
|
|
164
|
+
variant="secondary"
|
|
165
|
+
endContent={<Icon icon={ChevronRight} size="sm" />}
|
|
166
|
+
onClick={() => onSelect(next.id)}
|
|
167
|
+
/>
|
|
168
|
+
) : (
|
|
169
|
+
<StackItem size="fill" />
|
|
170
|
+
)}
|
|
171
|
+
</HStack>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Page ────────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export function ChapterArtFrame({children}: {children: ReactNode}) {
|
|
178
|
+
return (
|
|
179
|
+
<AspectRatio ratio={16 / 9} style={sceneClip}>
|
|
180
|
+
{children}
|
|
181
|
+
</AspectRatio>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default function ChapteredDoc({
|
|
186
|
+
groups,
|
|
187
|
+
defaultChapter,
|
|
188
|
+
railHeading,
|
|
189
|
+
railSubheading,
|
|
190
|
+
railHref,
|
|
191
|
+
railIcon,
|
|
192
|
+
eyebrow,
|
|
193
|
+
badge,
|
|
194
|
+
art,
|
|
195
|
+
footer,
|
|
196
|
+
}: {
|
|
197
|
+
groups: ChapterGroup[];
|
|
198
|
+
defaultChapter: string;
|
|
199
|
+
railHeading: string;
|
|
200
|
+
railSubheading: string;
|
|
201
|
+
railHref: string;
|
|
202
|
+
railIcon: ReactNode;
|
|
203
|
+
eyebrow: string;
|
|
204
|
+
badge: (chapter: DocChapter, index: number, total: number) => ReactNode;
|
|
205
|
+
art: (chapterId: string) => ReactNode;
|
|
206
|
+
footer?: ReactNode;
|
|
207
|
+
}) {
|
|
208
|
+
const chapters = groups.flatMap(group => group.chapters);
|
|
209
|
+
const [outlineByChapter] = useState(() => buildOutlineByChapter(chapters));
|
|
210
|
+
const [chapterId, setChapterId] = useState(defaultChapter);
|
|
211
|
+
const [activeSection, setActiveSection] = useState(
|
|
212
|
+
outlineByChapter[defaultChapter]?.[0]?.id ?? '',
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Responsive contract: below 1024px the outline stops being a column, since
|
|
216
|
+
// a narrow viewport has nothing to outline against.
|
|
217
|
+
const isNarrow = useMediaQuery('(max-width: 1024px)');
|
|
218
|
+
|
|
219
|
+
const chapter = chapters.find(entry => entry.id === chapterId) ?? chapters[0];
|
|
220
|
+
const outlineItems = outlineByChapter[chapter.id] ?? [];
|
|
221
|
+
|
|
222
|
+
const openChapter = (id: string) => {
|
|
223
|
+
setChapterId(id);
|
|
224
|
+
setActiveSection(outlineByChapter[id]?.[0]?.id ?? '');
|
|
225
|
+
// A chapter reads as a new page, so the document goes back to the top
|
|
226
|
+
// instead of keeping the previous chapter's scroll offset.
|
|
227
|
+
window.scrollTo({top: 0});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<AppShell
|
|
232
|
+
height="auto"
|
|
233
|
+
contentPadding={0}
|
|
234
|
+
mobileNav={{hasToggle: false}}
|
|
235
|
+
sideNav={
|
|
236
|
+
<ChapterRail
|
|
237
|
+
groups={groups}
|
|
238
|
+
activeId={chapter.id}
|
|
239
|
+
onSelect={openChapter}
|
|
240
|
+
heading={railHeading}
|
|
241
|
+
subheading={railSubheading}
|
|
242
|
+
headingHref={railHref}
|
|
243
|
+
headingIcon={railIcon}
|
|
244
|
+
/>
|
|
245
|
+
}>
|
|
246
|
+
<Layout
|
|
247
|
+
height="auto"
|
|
248
|
+
contentWidth={960}
|
|
249
|
+
end={
|
|
250
|
+
isNarrow ? undefined : (
|
|
251
|
+
<LayoutPanel
|
|
252
|
+
isScrollable={false}
|
|
253
|
+
label="On this page"
|
|
254
|
+
role="complementary"
|
|
255
|
+
style={outlinePanel}>
|
|
256
|
+
<Outline
|
|
257
|
+
items={outlineItems}
|
|
258
|
+
onActiveIdChange={setActiveSection}
|
|
259
|
+
/>
|
|
260
|
+
</LayoutPanel>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
content={
|
|
264
|
+
<LayoutContent isScrollable={false} padding={8}>
|
|
265
|
+
<VStack gap={8}>
|
|
266
|
+
<VStack gap={2}>
|
|
267
|
+
<HStack gap={2} vAlign="center">
|
|
268
|
+
<MobileNavToggle />
|
|
269
|
+
<Text type="supporting" color="secondary">
|
|
270
|
+
{eyebrow}
|
|
271
|
+
</Text>
|
|
272
|
+
</HStack>
|
|
273
|
+
<Heading level={1} type="display-2">
|
|
274
|
+
{chapter.title}
|
|
275
|
+
</Heading>
|
|
276
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
277
|
+
{badge(
|
|
278
|
+
chapter,
|
|
279
|
+
chapters.findIndex(entry => entry.id === chapter.id),
|
|
280
|
+
chapters.length,
|
|
281
|
+
)}
|
|
282
|
+
</Text>
|
|
283
|
+
{isNarrow && (
|
|
284
|
+
<Selector
|
|
285
|
+
label="On this page"
|
|
286
|
+
isLabelHidden
|
|
287
|
+
options={outlineItems.map(item => ({
|
|
288
|
+
value: item.id,
|
|
289
|
+
label: item.label,
|
|
290
|
+
}))}
|
|
291
|
+
value={activeSection}
|
|
292
|
+
onChange={(id: string) => {
|
|
293
|
+
setActiveSection(id);
|
|
294
|
+
scrollToSection(id);
|
|
295
|
+
}}
|
|
296
|
+
width="100%"
|
|
297
|
+
/>
|
|
298
|
+
)}
|
|
299
|
+
</VStack>
|
|
300
|
+
|
|
301
|
+
<Text type="large" color="secondary" display="block">
|
|
302
|
+
{chapter.intro}
|
|
303
|
+
</Text>
|
|
304
|
+
|
|
305
|
+
{art(chapter.id)}
|
|
306
|
+
|
|
307
|
+
<VStack gap={8}>
|
|
308
|
+
{chapter.sections.map(section => (
|
|
309
|
+
<SectionBlock
|
|
310
|
+
key={section.key}
|
|
311
|
+
chapterId={chapter.id}
|
|
312
|
+
section={section}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
</VStack>
|
|
316
|
+
|
|
317
|
+
<Divider />
|
|
318
|
+
|
|
319
|
+
<ChapterNav
|
|
320
|
+
chapters={chapters}
|
|
321
|
+
chapter={chapter}
|
|
322
|
+
onSelect={openChapter}
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
{footer}
|
|
326
|
+
</VStack>
|
|
327
|
+
</LayoutContent>
|
|
328
|
+
}
|
|
329
|
+
/>
|
|
330
|
+
</AppShell>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared categorical chart hues: the visual.md categorical ramp as importable
|
|
2
|
+
// constants. Every value is a role token var -- never a raw --dracula-* primitive
|
|
3
|
+
// and never a raw hex -- so the vocabulary is the sanctioned one by construction
|
|
4
|
+
// rather than by convention, and a gate can read the object and enforce it.
|
|
5
|
+
// Purple is deliberately absent: purple means tappable, so it must never encode
|
|
6
|
+
// data. `muted` is a COMMENT-blue grey because it backs the `{label: 'Other'}`
|
|
7
|
+
// legend row (dashboard.tsx:229, :253) -- a labelled data category, not chrome.
|
|
8
|
+
|
|
9
|
+
export const CHART_HUES = {
|
|
10
|
+
cyan: 'var(--color-data-categorical-cyan)',
|
|
11
|
+
orange: 'var(--color-data-categorical-orange)',
|
|
12
|
+
green: 'var(--color-data-categorical-green)',
|
|
13
|
+
pink: 'var(--color-data-categorical-pink)',
|
|
14
|
+
muted: 'var(--color-data-categorical-blue)',
|
|
15
|
+
// Added because --color-data-categorical-red exists in the palette
|
|
16
|
+
// (tokens.css:276) and heat maps need a severity colour, but no module
|
|
17
|
+
// exposed it. Without this, a chart could not reach red through the
|
|
18
|
+
// sanctioned source at all.
|
|
19
|
+
red: 'var(--color-data-categorical-red)',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type ChartHue = (typeof CHART_HUES)[keyof typeof CHART_HUES];
|
|
23
|
+
|
|
24
|
+
// The value list exists for the gate: it reads the object's VALUES, never a
|
|
25
|
+
// grep across shared/. A text-level filter cannot separate the sanctioned rows
|
|
26
|
+
// from a drifting neighbour, which is why the check is here and not in a lint.
|
|
27
|
+
export const CHART_HUE_VALUES: readonly ChartHue[] = Object.values(CHART_HUES);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Shared SVG chart-axis/label primitives. Every hand-drawn chart label goes
|
|
2
|
+
// through ChartLabel: token fill plus the 13px mono floor from visual.md
|
|
3
|
+
// (nothing meaningful below 12px, ever). The mono family resolves from the
|
|
4
|
+
// `--font-family-mono` theme token, single-sourced with the body/code faces.
|
|
5
|
+
|
|
6
|
+
import type {ReactNode} from 'react';
|
|
7
|
+
|
|
8
|
+
export const CHART_LABEL_FILL = 'var(--color-text-paragraph)';
|
|
9
|
+
export const CHART_LABEL_SIZE = 13;
|
|
10
|
+
|
|
11
|
+
interface ChartLabelProps {
|
|
12
|
+
x: number | string;
|
|
13
|
+
y: number | string;
|
|
14
|
+
textAnchor?: 'start' | 'middle' | 'end';
|
|
15
|
+
/** Defaults to the paragraph token; pass an explicit fill for values drawn
|
|
16
|
+
* on colored fills (e.g. in-cell heatmap counts).
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT the chart-legend union: this paints TEXT, so its arms
|
|
19
|
+
* are a text role and the on-fill role, and neither is a --color-data-*
|
|
20
|
+
* var. Reusing the legend type here would reject the one use it exists
|
|
21
|
+
* for. */
|
|
22
|
+
fill?: `var(--color-text-${string})` | `var(--color-on-${string})`;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ChartLabel({
|
|
27
|
+
x,
|
|
28
|
+
y,
|
|
29
|
+
textAnchor = 'middle',
|
|
30
|
+
fill = CHART_LABEL_FILL,
|
|
31
|
+
children,
|
|
32
|
+
}: ChartLabelProps) {
|
|
33
|
+
return (
|
|
34
|
+
<text
|
|
35
|
+
x={x}
|
|
36
|
+
y={y}
|
|
37
|
+
textAnchor={textAnchor}
|
|
38
|
+
fontSize={CHART_LABEL_SIZE}
|
|
39
|
+
fill={fill}
|
|
40
|
+
fontFamily="var(--font-family-mono)">
|
|
41
|
+
{children}
|
|
42
|
+
</text>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Shared chart legend: swatch + label, always paired.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS: the legend was hand-rolled three incompatible ways before
|
|
4
|
+
// this module — dashboard.tsx used an Icon swatch, table-page-heatmap-status
|
|
5
|
+
// used a 12px rx=4 rect, tech-report used an 8px rx=1 rect — so three files
|
|
6
|
+
// disagreed on what a legend swatch even looks like. Meanwhile demo/Bento.tsx
|
|
7
|
+
// and demo/Dashboard.tsx render seven bars in seven hues with no legend at all,
|
|
8
|
+
// so those hues encoded nothing. This module makes that state unrepresentable.
|
|
9
|
+
//
|
|
10
|
+
// COLOUR CONTRACT (UIAuditColorMotion, from references/visual.md): "Legends pair
|
|
11
|
+
// spectral badges with dots; never rely on color alone, labels travel with
|
|
12
|
+
// hues." So `label` is REQUIRED and not optional — a dot without a label is
|
|
13
|
+
// colour-alone encoding, and WCAG 1.4.1 plus 1.4.11 both fail on that. Making
|
|
14
|
+
// the label required closes both in one shape instead of leaving every call site
|
|
15
|
+
// to remember.
|
|
16
|
+
//
|
|
17
|
+
// Swatch colour MUST be a --color-data-categorical-* or --color-data-<family>-N
|
|
18
|
+
// var. Never a raw hex, never a --dracula-* primitive: that discipline is what
|
|
19
|
+
// makes the 56 --color-data-* tokens load-bearing rather than decorative, and
|
|
20
|
+
// shared/chart-hues.ts is the sanctioned categorical source.
|
|
21
|
+
//
|
|
22
|
+
// Not interactive. A legend is a key, not a control: no hover, no focus ring.
|
|
23
|
+
|
|
24
|
+
import type {ReactNode} from 'react';
|
|
25
|
+
import {HStack, StackItem, VStack} from '@astryxdesign/core/Stack';
|
|
26
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
27
|
+
import {CHART_HUES, type ChartHue} from 'astryx-dracula/shared/chart-hues';
|
|
28
|
+
|
|
29
|
+
export interface ChartLegendEntry {
|
|
30
|
+
/** REQUIRED. The hue never travels alone — this is the 1.4.1 contract. */
|
|
31
|
+
label: string;
|
|
32
|
+
/** A --color-data-* var, or a --color-background-* surface. Never a raw
|
|
33
|
+
* hex, never a --dracula-* primitive.
|
|
34
|
+
*
|
|
35
|
+
* The surface arm is not optional: a legend whose first row is a recessive
|
|
36
|
+
* surface is normal (the heatmap's zero cell), so a data-only type would
|
|
37
|
+
* reject the value the heatmap is supposed to paint. */
|
|
38
|
+
color: ChartHue | `var(--color-data-${string})` | `var(--color-background-${string})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Kept as a string-keyed fall-through on purpose, and DO NOT narrow it: call
|
|
42
|
+
// sites pass an already-RESOLVED value (CHART_HUES.x is a property access, so
|
|
43
|
+
// it yields the value, never the key), so the lookup misses at every site and
|
|
44
|
+
// the `?? entry.color` is the only path that paints. Narrowing the key type
|
|
45
|
+
// would render `background: undefined` at every call site, compile clean, and
|
|
46
|
+
// break at runtime. The old comment here claimed call sites may pass a KEY;
|
|
47
|
+
// none does. The behaviour is correct; only the comment was wrong.
|
|
48
|
+
const HUE_LOOKUP: Record<string, string> = CHART_HUES;
|
|
49
|
+
|
|
50
|
+
export interface ChartLegendProps {
|
|
51
|
+
entries: readonly ChartLegendEntry[];
|
|
52
|
+
/** Spacing step between entries. @default 2 */
|
|
53
|
+
gap?: 1 | 2 | 3 | 4 | 6;
|
|
54
|
+
/** Accessible name for the legend as a group. */
|
|
55
|
+
label?: string;
|
|
56
|
+
/** Swatch size in px. @default 10 */
|
|
57
|
+
swatchSize?: number;
|
|
58
|
+
/** Caption under the legend, e.g. "Hourly intervals · trailing 24 hours". */
|
|
59
|
+
caption?: ReactNode;
|
|
60
|
+
/** Orientation. @default 'row' */
|
|
61
|
+
direction?: 'row' | 'column';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export function ChartLegend({
|
|
66
|
+
entries,
|
|
67
|
+
gap = 2,
|
|
68
|
+
swatchSize = 10,
|
|
69
|
+
label,
|
|
70
|
+
caption,
|
|
71
|
+
direction = 'row',
|
|
72
|
+
}: ChartLegendProps) {
|
|
73
|
+
const Stack = direction === 'row' ? HStack : VStack;
|
|
74
|
+
return (
|
|
75
|
+
<VStack gap={1}>
|
|
76
|
+
<Stack gap={gap} vAlign="center" role="list" aria-label={label}>
|
|
77
|
+
{entries.map(entry => (
|
|
78
|
+
<HStack key={entry.label} gap={1} vAlign="center" role="listitem">
|
|
79
|
+
<StackItem
|
|
80
|
+
style={{
|
|
81
|
+
width: `${swatchSize}px`,
|
|
82
|
+
height: `${swatchSize}px`,
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
background: HUE_LOOKUP[entry.color] ?? entry.color,
|
|
85
|
+
borderRadius: 'var(--radius-inner)',
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
{/* supporting, not body: a legend label is metadata by the
|
|
89
|
+
typography doctrine, and it never carries a sentence the page
|
|
90
|
+
needs the reader to have. */}
|
|
91
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
92
|
+
{entry.label}
|
|
93
|
+
</Text>
|
|
94
|
+
</HStack>
|
|
95
|
+
))}
|
|
96
|
+
</Stack>
|
|
97
|
+
{caption}
|
|
98
|
+
</VStack>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The one chart-panel look, shared by every hand-drawn SVG chart panel in the
|
|
2
|
+
// kit (dashboard, portfolio, chart, heatmap-status, shoe-store). Split from
|
|
3
|
+
// shared/revenue-chart.tsx so that file exports only components and React Fast
|
|
4
|
+
// Refresh stays intact; see shared/scene-hues.ts for the same reason.
|
|
5
|
+
import type {CSSProperties} from 'react';
|
|
6
|
+
|
|
7
|
+
// One chart-panel look for every hand-drawn SVG chart panel in this wave's
|
|
8
|
+
// templates (dashboard, portfolio, chart, heatmap-status, shoe-store). Restate
|
|
9
|
+
// nowhere: theme Card variant work lands in wave 2.
|
|
10
|
+
export const CHART_PANEL_STYLE: CSSProperties = {
|
|
11
|
+
backgroundColor: 'var(--color-background)',
|
|
12
|
+
border: 'var(--border-width) solid var(--color-separator)',
|
|
13
|
+
};
|