astryx-dracula 0.4.0 → 0.5.1
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/astryx.integration.mjs +1 -1
- package/icons.mjs +17 -17
- package/icons.tsx +61 -62
- package/package.json +120 -116
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +168 -19
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +270 -292
- package/shared/chart-hues.ts +10 -15
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -57
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +3 -0
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +123 -0
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -25
- package/shared/scene-tile.tsx +88 -100
- package/shared/settings-data.ts +132 -104
- package/shared/settings-rows.tsx +143 -163
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +19 -0
- package/templates/ai-chat-landing.template.mjs +4 -4
- package/templates/ai-chat-landing.tsx +468 -447
- package/templates/ai-chat.template.mjs +4 -4
- package/templates/ai-chat.tsx +561 -645
- package/templates/blank.template.mjs +4 -4
- package/templates/blank.tsx +14 -12
- package/templates/centered-hero.template.mjs +4 -4
- package/templates/centered-hero.tsx +52 -67
- package/templates/classic-gallery.template.mjs +4 -4
- package/templates/classic-gallery.tsx +92 -101
- package/templates/contact-form.template.mjs +4 -4
- package/templates/contact-form.tsx +243 -280
- package/templates/dashboard-portfolio.template.mjs +4 -4
- package/templates/dashboard-portfolio.tsx +723 -634
- package/templates/dashboard.template.mjs +4 -4
- package/templates/dashboard.tsx +592 -682
- package/templates/detail-page.template.mjs +4 -4
- package/templates/detail-page.tsx +537 -598
- package/templates/documentation-design.template.mjs +4 -4
- package/templates/documentation-design.tsx +242 -709
- package/templates/documentation-technical.template.mjs +4 -4
- package/templates/documentation-technical.tsx +233 -258
- package/templates/documentation.template.mjs +4 -4
- package/templates/documentation.tsx +263 -277
- package/templates/editor.template.mjs +4 -4
- package/templates/editor.tsx +847 -899
- package/templates/file-explorer.template.mjs +4 -4
- package/templates/file-explorer.tsx +562 -602
- package/templates/form-two-column.template.mjs +4 -4
- package/templates/form-two-column.tsx +187 -208
- package/templates/gallery-hero.template.mjs +4 -4
- package/templates/gallery-hero.tsx +89 -112
- package/templates/ide.template.mjs +4 -4
- package/templates/ide.tsx +405 -462
- package/templates/incident-console.template.mjs +4 -4
- package/templates/incident-console.tsx +559 -581
- package/templates/kanban-board.template.mjs +4 -4
- package/templates/kanban-board.tsx +660 -729
- package/templates/library.template.mjs +4 -4
- package/templates/library.tsx +405 -452
- package/templates/login-card.template.mjs +4 -4
- package/templates/login-card.tsx +59 -163
- package/templates/login-split.template.mjs +4 -4
- package/templates/login-split.tsx +196 -232
- package/templates/login-sso.template.mjs +4 -4
- package/templates/login-sso.tsx +253 -277
- package/templates/login.template.mjs +4 -4
- package/templates/login.tsx +51 -149
- package/templates/messaging-shell.template.mjs +4 -4
- package/templates/messaging-shell.tsx +596 -624
- package/templates/mixed-gallery.template.mjs +4 -4
- package/templates/mixed-gallery.tsx +56 -56
- package/templates/payment-form.template.mjs +4 -4
- package/templates/payment-form.tsx +877 -990
- package/templates/product-detail.template.mjs +4 -4
- package/templates/product-detail.tsx +251 -302
- package/templates/product-gallery.template.mjs +4 -4
- package/templates/product-gallery.tsx +122 -135
- package/templates/product-tour.template.mjs +4 -4
- package/templates/product-tour.tsx +737 -794
- package/templates/settings-dialog.template.mjs +4 -4
- package/templates/settings-dialog.tsx +694 -747
- package/templates/settings-sidebar.template.mjs +4 -4
- package/templates/settings-sidebar.tsx +768 -848
- package/templates/settings.template.mjs +4 -4
- package/templates/settings.tsx +306 -230
- package/templates/shell-nav.template.mjs +4 -4
- package/templates/shell-nav.tsx +217 -277
- package/templates/shell-side-nav.template.mjs +4 -4
- package/templates/shell-side-nav.tsx +130 -231
- package/templates/shell-top-nav.template.mjs +4 -4
- package/templates/shell-top-nav.tsx +192 -248
- package/templates/side-gallery.template.mjs +4 -4
- package/templates/side-gallery.tsx +87 -94
- package/templates/table-grouped.template.mjs +4 -4
- package/templates/table-grouped.tsx +999 -1201
- package/templates/table-page-chart.template.mjs +4 -4
- package/templates/table-page-chart.tsx +398 -397
- package/templates/table-page-heatmap-status.template.mjs +4 -4
- package/templates/table-page-heatmap-status.tsx +479 -527
- package/templates/table-page-shoe-store-heatmap.template.mjs +4 -4
- package/templates/table-page-shoe-store-heatmap.tsx +746 -746
- package/templates/table-page.template.mjs +4 -4
- package/templates/table-page.tsx +393 -431
- package/templates/table.template.mjs +4 -4
- package/templates/table.tsx +93 -99
- package/templates/tech-report.template.mjs +4 -4
- package/templates/tech-report.tsx +1320 -1324
- package/templates/theme-showcase.template.mjs +4 -4
- package/templates/theme-showcase.tsx +869 -1246
- package/tokens.css +340 -343
package/shared/auth-chrome.tsx
CHANGED
|
@@ -1,25 +1,174 @@
|
|
|
1
1
|
// Shared login chrome: page wash + 400px column + Moon brand row + the
|
|
2
2
|
// autocomplete widen-record TextInput needs (prop type omits input attrs).
|
|
3
|
-
import type {CSSProperties} from 'react';
|
|
4
|
-
import {Moon} from 'lucide-react';
|
|
5
|
-
import {VStack} from '@astryxdesign/core/Layout';
|
|
6
|
-
import {Text} from '@astryxdesign/core/Text';
|
|
7
|
-
import {Icon} from '@astryxdesign/core/Icon';
|
|
8
|
-
import {AUTH_BRAND_NAME} from 'astryx-dracula/shared/auth-copy';
|
|
9
|
-
import {
|
|
10
|
-
authContentStyle,
|
|
11
|
-
authPageStyle,
|
|
12
|
-
inputAutoComplete,
|
|
13
|
-
} from 'astryx-dracula/shared/auth-chrome-config';
|
|
14
3
|
|
|
4
|
+
import { Button } from "@astryxdesign/core/Button";
|
|
5
|
+
import { Card } from "@astryxdesign/core/Card";
|
|
6
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
7
|
+
import { VStack } from "@astryxdesign/core/Layout";
|
|
8
|
+
import { Link } from "@astryxdesign/core/Link";
|
|
9
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
10
|
+
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
11
|
+
import { inputAutoComplete } from "astryx-dracula/shared/auth-chrome-config";
|
|
12
|
+
import {
|
|
13
|
+
AUTH_BRAND_NAME,
|
|
14
|
+
AUTH_EMAIL_PLACEHOLDER,
|
|
15
|
+
AUTH_FORGOT_PASSWORD,
|
|
16
|
+
AUTH_HEADING,
|
|
17
|
+
AUTH_PASSWORD_PLACEHOLDER,
|
|
18
|
+
AUTH_PRIMARY_CTA,
|
|
19
|
+
AUTH_SIGNUP_LINK,
|
|
20
|
+
AUTH_SIGNUP_PROMPT,
|
|
21
|
+
AUTH_SUBTITLE,
|
|
22
|
+
AUTH_TERMS_PREFIX,
|
|
23
|
+
AUTH_TERMS_PRIVACY,
|
|
24
|
+
AUTH_TERMS_SERVICE,
|
|
25
|
+
} from "astryx-dracula/shared/auth-copy";
|
|
26
|
+
import { Moon } from "lucide-react";
|
|
27
|
+
import type { ReactNode } from "react";
|
|
15
28
|
|
|
16
29
|
export function LoginBrand() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
return (
|
|
31
|
+
<VStack gap={2} hAlign="center">
|
|
32
|
+
<Icon icon={Moon} size="lg" color="secondary" />
|
|
33
|
+
<Text type="body" weight="semibold" size="lg">
|
|
34
|
+
{AUTH_BRAND_NAME}
|
|
35
|
+
</Text>
|
|
36
|
+
</VStack>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Shared password-login core: the centered header, credential fields, primary
|
|
41
|
+
// CTA, signup line and terms line every password page in the family carries.
|
|
42
|
+
// The three pages differed only in what sits between the CTA and the signup
|
|
43
|
+
// line (nothing, or the SSO divider + provider buttons) and in their outer
|
|
44
|
+
// shell — login-split's two-column grid and login-sso's step machine keep
|
|
45
|
+
// their own markup and take the pieces below instead of this whole card.
|
|
46
|
+
export function AuthCard({
|
|
47
|
+
selfHash,
|
|
48
|
+
signupLink = AUTH_SIGNUP_LINK,
|
|
49
|
+
children,
|
|
50
|
+
}: {
|
|
51
|
+
selfHash: string;
|
|
52
|
+
signupLink?: string;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Card padding={4} width="100%">
|
|
58
|
+
<VStack gap={4} hAlign="stretch">
|
|
59
|
+
<VStack gap={1} hAlign="center">
|
|
60
|
+
<Heading level={1} type="display-2" justify="center">
|
|
61
|
+
{AUTH_HEADING}
|
|
62
|
+
</Heading>
|
|
63
|
+
<Text type="body" color="secondary">
|
|
64
|
+
{AUTH_SUBTITLE}
|
|
65
|
+
</Text>
|
|
66
|
+
</VStack>
|
|
67
|
+
{children}
|
|
68
|
+
{/* Prose link: colour alone is not a cue inside a sentence
|
|
69
|
+
(WCAG 1.4.1 / F73), so it asks for the underline the kit
|
|
70
|
+
only gives on hover. */}
|
|
71
|
+
<VStack hAlign="center">
|
|
72
|
+
<Text type="supporting" color="secondary">
|
|
73
|
+
{AUTH_SIGNUP_PROMPT}{" "}
|
|
74
|
+
<Link href={selfHash} type="supporting" hasUnderline>
|
|
75
|
+
{signupLink}
|
|
76
|
+
</Link>
|
|
77
|
+
</Text>
|
|
78
|
+
</VStack>
|
|
79
|
+
</VStack>
|
|
80
|
+
</Card>
|
|
81
|
+
<AuthTerms selfHash={selfHash} />
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Email + password fields with the shared error/forgot-password rhythm, plus
|
|
87
|
+
// the primary CTA that always follows them. The field gap is 8px (gap={2}),
|
|
88
|
+
// not the outer 16px: without the group the two inputs sat twice as far
|
|
89
|
+
// apart as the fields below them.
|
|
90
|
+
export function AuthLoginFields({
|
|
91
|
+
email,
|
|
92
|
+
password,
|
|
93
|
+
error,
|
|
94
|
+
selfHash,
|
|
95
|
+
isLoading,
|
|
96
|
+
onEmailChange,
|
|
97
|
+
onPasswordChange,
|
|
98
|
+
onSubmit,
|
|
99
|
+
}: {
|
|
100
|
+
email: string;
|
|
101
|
+
password: string;
|
|
102
|
+
error: string | null;
|
|
103
|
+
selfHash: string;
|
|
104
|
+
isLoading: boolean;
|
|
105
|
+
onEmailChange: (value: string) => void;
|
|
106
|
+
onPasswordChange: (value: string) => void;
|
|
107
|
+
onSubmit: () => void;
|
|
108
|
+
}) {
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<VStack gap={2}>
|
|
112
|
+
<TextInput
|
|
113
|
+
label="Email"
|
|
114
|
+
isLabelHidden
|
|
115
|
+
value={email}
|
|
116
|
+
onChange={onEmailChange}
|
|
117
|
+
placeholder={AUTH_EMAIL_PLACEHOLDER}
|
|
118
|
+
type="email"
|
|
119
|
+
{...inputAutoComplete("email")}
|
|
120
|
+
size="lg"
|
|
121
|
+
onEnter={onSubmit}
|
|
122
|
+
status={error ? { type: "error", message: error } : undefined}
|
|
123
|
+
/>
|
|
124
|
+
<VStack gap={1}>
|
|
125
|
+
<TextInput
|
|
126
|
+
label="Password"
|
|
127
|
+
isLabelHidden
|
|
128
|
+
value={password}
|
|
129
|
+
onChange={onPasswordChange}
|
|
130
|
+
placeholder={AUTH_PASSWORD_PLACEHOLDER}
|
|
131
|
+
type="password"
|
|
132
|
+
{...inputAutoComplete("current-password")}
|
|
133
|
+
size="lg"
|
|
134
|
+
onEnter={onSubmit}
|
|
135
|
+
status={error ? { type: "error", message: error } : undefined}
|
|
136
|
+
/>
|
|
137
|
+
{error && (
|
|
138
|
+
<VStack hAlign="end">
|
|
139
|
+
<Link href={selfHash} color="secondary" isStandalone>
|
|
140
|
+
{AUTH_FORGOT_PASSWORD}
|
|
141
|
+
</Link>
|
|
142
|
+
</VStack>
|
|
143
|
+
)}
|
|
144
|
+
</VStack>
|
|
145
|
+
</VStack>
|
|
146
|
+
<Button
|
|
147
|
+
label={AUTH_PRIMARY_CTA}
|
|
148
|
+
variant="primary"
|
|
149
|
+
size="lg"
|
|
150
|
+
isLoading={isLoading}
|
|
151
|
+
onClick={onSubmit}
|
|
152
|
+
/>
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The family terms line, rendered below the card in the page column.
|
|
158
|
+
export function AuthTerms({ selfHash }: { selfHash: string }) {
|
|
159
|
+
return (
|
|
160
|
+
<VStack hAlign="center" width="100%">
|
|
161
|
+
<Text type="supporting" color="secondary" justify="center">
|
|
162
|
+
{AUTH_TERMS_PREFIX}{" "}
|
|
163
|
+
<Link href={selfHash} type="supporting" hasUnderline>
|
|
164
|
+
{AUTH_TERMS_SERVICE}
|
|
165
|
+
</Link>{" "}
|
|
166
|
+
and{" "}
|
|
167
|
+
<Link href={selfHash} type="supporting" hasUnderline>
|
|
168
|
+
{AUTH_TERMS_PRIVACY}
|
|
169
|
+
</Link>
|
|
170
|
+
.
|
|
171
|
+
</Text>
|
|
172
|
+
</VStack>
|
|
173
|
+
);
|
|
25
174
|
}
|
package/shared/auth-copy.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
// Auth copy shared by the four login templates. Single-sourced so the family
|
|
2
2
|
// line cannot drift again. SSO-specific strings (Work email, Whisper your
|
|
3
3
|
// details, Continue with SSO, provider names) stay per-template in login-sso.
|
|
4
|
-
export const AUTH_HEADING =
|
|
5
|
-
export const AUTH_SUBTITLE =
|
|
6
|
-
export const AUTH_PRIMARY_CTA =
|
|
7
|
-
export const AUTH_SIGNUP_PROMPT =
|
|
8
|
-
export const AUTH_SIGNUP_LINK =
|
|
9
|
-
export const AUTH_SSO_DIVIDER =
|
|
10
|
-
export const AUTH_FORGOT_PASSWORD =
|
|
4
|
+
export const AUTH_HEADING = "Welcome back to the night";
|
|
5
|
+
export const AUTH_SUBTITLE = "Sign in to your crypt";
|
|
6
|
+
export const AUTH_PRIMARY_CTA = "Sign in";
|
|
7
|
+
export const AUTH_SIGNUP_PROMPT = "New to the castle?";
|
|
8
|
+
export const AUTH_SIGNUP_LINK = "Sign up";
|
|
9
|
+
export const AUTH_SSO_DIVIDER = "Or continue with";
|
|
10
|
+
export const AUTH_FORGOT_PASSWORD = "Forgot password?";
|
|
11
11
|
// Names the problem without apologising, and does not blame one field: a
|
|
12
12
|
// mistyped email used to be reported as a wrong password, because this string
|
|
13
13
|
// was attached to the password input only.
|
|
14
|
-
export const AUTH_ERROR_MESSAGE =
|
|
15
|
-
export const AUTH_EMAIL_PLACEHOLDER =
|
|
14
|
+
export const AUTH_ERROR_MESSAGE = "Email or password is wrong. Try again.";
|
|
15
|
+
export const AUTH_EMAIL_PLACEHOLDER = "you@castle.dracula";
|
|
16
16
|
// The placeholder is the only visible name on the credential inputs, so it has
|
|
17
17
|
// to actually name the field. "Whisper your password" named nothing.
|
|
18
|
-
export const AUTH_PASSWORD_PLACEHOLDER =
|
|
19
|
-
export const AUTH_BRAND_NAME =
|
|
20
|
-
export const AUTH_TERMS_PREFIX =
|
|
21
|
-
export const AUTH_TERMS_SERVICE =
|
|
22
|
-
export const AUTH_TERMS_PRIVACY =
|
|
18
|
+
export const AUTH_PASSWORD_PLACEHOLDER = "Your password";
|
|
19
|
+
export const AUTH_BRAND_NAME = "Castle Dracula";
|
|
20
|
+
export const AUTH_TERMS_PREFIX = "By clicking continue, you agree to our";
|
|
21
|
+
export const AUTH_TERMS_SERVICE = "Terms of service";
|
|
22
|
+
export const AUTH_TERMS_PRIVACY = "Privacy policy";
|
|
@@ -9,84 +9,81 @@
|
|
|
9
9
|
// documentation-technical, product-tour, tech-report) share lives here; the
|
|
10
10
|
// outline-building and scroll helpers that only chaptered-doc itself calls stay
|
|
11
11
|
// unexported in that file.
|
|
12
|
-
|
|
13
|
-
import type {IconType} from
|
|
14
|
-
import type {OutlineItem} from
|
|
12
|
+
|
|
13
|
+
import type { IconType } from "@astryxdesign/core/Icon";
|
|
14
|
+
import type { OutlineItem } from "@astryxdesign/core/Outline";
|
|
15
|
+
import type { CSSProperties } from "react";
|
|
15
16
|
|
|
16
17
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
17
18
|
|
|
18
19
|
export interface DocSection {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// Unique within the chapter; the DOM id is derived from it so the outline
|
|
21
|
+
// links and the headings can never drift apart.
|
|
22
|
+
key: string;
|
|
23
|
+
heading: string;
|
|
24
|
+
body: string;
|
|
25
|
+
bullets?: string[];
|
|
26
|
+
code?: { language: string; title: string; source: string };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface DocChapter {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
icon: IconType;
|
|
33
|
+
intro: string;
|
|
34
|
+
hasArt?: boolean;
|
|
35
|
+
sections: DocSection[];
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export type ChapterGroup = {title: string; chapters: DocChapter[]};
|
|
38
|
+
export type ChapterGroup = { title: string; chapters: DocChapter[] };
|
|
38
39
|
|
|
39
|
-
export const sectionId = (chapterId: string, key: string) =>
|
|
40
|
-
`${chapterId}--${key}`;
|
|
40
|
+
export const sectionId = (chapterId: string, key: string) => `${chapterId}--${key}`;
|
|
41
41
|
|
|
42
42
|
// Astryx has no image primitive: AspectRatio exposes no objectFit or radius
|
|
43
43
|
// props, so the scene fill and the corner clip live in these two styles.
|
|
44
44
|
export const sceneFill: CSSProperties = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
width: "100%",
|
|
46
|
+
height: "100%",
|
|
47
|
+
display: "block",
|
|
48
48
|
};
|
|
49
49
|
export const sceneClip: CSSProperties = {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
borderRadius: "var(--radius-container)",
|
|
51
|
+
overflow: "clip",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
// The outline is sticky so it tracks the chapter as the document scrolls.
|
|
55
55
|
export const outlinePanel: CSSProperties = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
65
|
};
|
|
66
66
|
|
|
67
67
|
// Derived once per page so the Outline receives a stable array identity
|
|
68
68
|
// and does not re-register its scroll spy on every render.
|
|
69
|
-
export function buildOutlineByChapter(
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
]),
|
|
81
|
-
);
|
|
69
|
+
export function buildOutlineByChapter(chapters: DocChapter[]): Record<string, OutlineItem[]> {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
chapters.map((chapter) => [
|
|
72
|
+
chapter.id,
|
|
73
|
+
chapter.sections.map((section) => ({
|
|
74
|
+
id: sectionId(chapter.id, section.key),
|
|
75
|
+
label: section.heading,
|
|
76
|
+
level: 2,
|
|
77
|
+
})),
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
export function scrollToSection(id: string) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const target = document.getElementById(id);
|
|
84
|
+
if (target != null) {
|
|
85
|
+
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
86
|
+
}
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
// ─── Rail ────────────────────────────────────────────────────────────────────
|
|
92
|
-
|