@astryxdesign/cli 0.0.15 → 0.1.0-canary.0b5b49f
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/CHANGELOG.md +88 -0
- package/README.md +1 -1
- package/docs/getting-started.doc.mjs +15 -15
- package/docs/migration.doc.mjs +4 -4
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +2 -3
- package/docs/theme.doc.dense.mjs +1 -1
- package/docs/theme.doc.mjs +39 -19
- package/docs/theme.doc.zh.mjs +1 -1
- package/package.json +9 -13
- package/src/api/doctor.mjs +4 -4
- package/src/commands/agent-docs.mjs +27 -10
- package/src/commands/agent-docs.test.mjs +21 -21
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +3 -3
- package/src/utils/package-manager.mjs +1 -1
- package/templates/pages/documentation/page.tsx +39 -52
- package/templates/pages/documentation-design/page.tsx +80 -55
- package/templates/pages/documentation-technical/page.tsx +96 -54
- package/templates/pages/form-two-column/page.tsx +13 -30
- package/templates/pages/ide/page.tsx +167 -236
|
@@ -32,8 +32,8 @@ describe('generateCompressedIndex', () => {
|
|
|
32
32
|
it('includes the version number', () => {
|
|
33
33
|
const result = generateCompressedIndex('1.2.3');
|
|
34
34
|
expect(result).toContain('Astryx v1.2.3');
|
|
35
|
-
expect(result).toContain('<!--
|
|
36
|
-
expect(result).toContain('<!--
|
|
35
|
+
expect(result).toContain('<!-- ASTRYX:START -->');
|
|
36
|
+
expect(result).toContain('<!-- ASTRYX:END -->');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('includes theme nudge rule', () => {
|
|
@@ -82,19 +82,19 @@ describe('injectXdsBlock', () => {
|
|
|
82
82
|
const filePath = path.join(tmpDir, 'test.md');
|
|
83
83
|
fs.writeFileSync(filePath, '# Existing content\n');
|
|
84
84
|
|
|
85
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
85
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
86
86
|
|
|
87
87
|
expect(result).toBe(true);
|
|
88
88
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
89
|
expect(content).toContain('# Existing content');
|
|
90
|
-
expect(content).toContain('<!--
|
|
90
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it('replaces existing markers', () => {
|
|
94
94
|
const filePath = path.join(tmpDir, 'test.md');
|
|
95
95
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
96
96
|
|
|
97
|
-
injectXdsBlock(filePath, '<!--
|
|
97
|
+
injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
98
98
|
|
|
99
99
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
100
100
|
expect(content).toContain('new');
|
|
@@ -106,7 +106,7 @@ describe('injectXdsBlock', () => {
|
|
|
106
106
|
it('returns false and does not create file when createIfMissing is false', () => {
|
|
107
107
|
const filePath = path.join(tmpDir, 'nonexistent.md');
|
|
108
108
|
|
|
109
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
109
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->');
|
|
110
110
|
|
|
111
111
|
expect(result).toBe(false);
|
|
112
112
|
expect(fs.existsSync(filePath)).toBe(false);
|
|
@@ -116,11 +116,11 @@ describe('injectXdsBlock', () => {
|
|
|
116
116
|
const filePath = path.join(tmpDir, 'test.md');
|
|
117
117
|
fs.writeFileSync(filePath, '# Existing content\n\nNo XDS markers here.\n');
|
|
118
118
|
|
|
119
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
119
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
120
120
|
|
|
121
121
|
expect(result).toBe(false);
|
|
122
122
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
123
|
-
expect(content).not.toContain('<!--
|
|
123
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
124
124
|
expect(content).toBe('# Existing content\n\nNo XDS markers here.\n');
|
|
125
125
|
});
|
|
126
126
|
|
|
@@ -128,7 +128,7 @@ describe('injectXdsBlock', () => {
|
|
|
128
128
|
const filePath = path.join(tmpDir, 'test.md');
|
|
129
129
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
130
130
|
|
|
131
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
131
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
132
132
|
|
|
133
133
|
expect(result).toBe(true);
|
|
134
134
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -139,7 +139,7 @@ describe('injectXdsBlock', () => {
|
|
|
139
139
|
it('creates file when createIfMissing is true', () => {
|
|
140
140
|
const filePath = path.join(tmpDir, 'new.md');
|
|
141
141
|
|
|
142
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
142
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->', {
|
|
143
143
|
createIfMissing: true,
|
|
144
144
|
header: '# Header',
|
|
145
145
|
});
|
|
@@ -147,7 +147,7 @@ describe('injectXdsBlock', () => {
|
|
|
147
147
|
expect(result).toBe(true);
|
|
148
148
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
149
149
|
expect(content).toContain('# Header');
|
|
150
|
-
expect(content).toContain('<!--
|
|
150
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
153
|
|
|
@@ -157,9 +157,9 @@ describe('injectAgentsMd', () => {
|
|
|
157
157
|
|
|
158
158
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
159
159
|
expect(content).toContain('# AGENTS.md');
|
|
160
|
-
expect(content).toContain('<!--
|
|
160
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
161
161
|
expect(content).toContain('Astryx v1.0.0');
|
|
162
|
-
expect(content).toContain('<!--
|
|
162
|
+
expect(content).toContain('<!-- ASTRYX:END -->');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('updates existing AGENTS.md by replacing XDS markers', () => {
|
|
@@ -195,7 +195,7 @@ Existing agent docs.
|
|
|
195
195
|
|
|
196
196
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
197
197
|
expect(content).toContain('Existing agent docs.');
|
|
198
|
-
expect(content).toContain('<!--
|
|
198
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
199
199
|
expect(content).toContain('Astryx v1.0.0');
|
|
200
200
|
});
|
|
201
201
|
});
|
|
@@ -210,7 +210,7 @@ describe('injectClaudeMd', () => {
|
|
|
210
210
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
211
211
|
expect(content).toContain('# Claude Config');
|
|
212
212
|
expect(content).toContain('Existing rules.');
|
|
213
|
-
expect(content).toContain('<!--
|
|
213
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
214
214
|
expect(content).toContain('Astryx v1.0.0');
|
|
215
215
|
});
|
|
216
216
|
|
|
@@ -322,7 +322,7 @@ describe('installAgentDocs', () => {
|
|
|
322
322
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(true);
|
|
323
323
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
324
324
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
325
|
-
expect(content).toContain('<!--
|
|
325
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
it('injects into CLAUDE.md at root when it exists', () => {
|
|
@@ -334,7 +334,7 @@ describe('installAgentDocs', () => {
|
|
|
334
334
|
expect(written).toEqual(['CLAUDE.md']);
|
|
335
335
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
336
336
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
337
|
-
expect(claudeContent).toContain('<!--
|
|
337
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
338
338
|
expect(claudeContent).toContain('Project rules.');
|
|
339
339
|
});
|
|
340
340
|
|
|
@@ -349,8 +349,8 @@ describe('installAgentDocs', () => {
|
|
|
349
349
|
expect(written).toContain('CLAUDE.md');
|
|
350
350
|
const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
351
351
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
352
|
-
expect(agentsContent).toContain('<!--
|
|
353
|
-
expect(claudeContent).toContain('<!--
|
|
352
|
+
expect(agentsContent).toContain('<!-- ASTRYX:START -->');
|
|
353
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
354
354
|
});
|
|
355
355
|
|
|
356
356
|
it('updates existing .claude/CLAUDE.md', () => {
|
|
@@ -363,7 +363,7 @@ describe('installAgentDocs', () => {
|
|
|
363
363
|
expect(written).toEqual(['.claude/CLAUDE.md']);
|
|
364
364
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
365
365
|
expect(content).toContain('Existing content.');
|
|
366
|
-
expect(content).toContain('<!--
|
|
366
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
367
367
|
});
|
|
368
368
|
|
|
369
369
|
it('respects --agent claude preset: finds existing CLAUDE.md', () => {
|
|
@@ -410,7 +410,7 @@ describe('installAgentDocs', () => {
|
|
|
410
410
|
|
|
411
411
|
expect(written).toEqual([]);
|
|
412
412
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
413
|
-
expect(content).not.toContain('<!--
|
|
413
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
414
414
|
expect(content).toBe('# Claude\n\nProject rules only.\n');
|
|
415
415
|
});
|
|
416
416
|
|
|
@@ -106,14 +106,14 @@ describe('doctor — individual checks', () => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it('themes: WARN when theme installed but not wired', () => {
|
|
109
|
-
installPkg('@astryxdesign/theme-
|
|
109
|
+
installPkg('@astryxdesign/theme-neutral', '0.0.14');
|
|
110
110
|
const res = checkThemes({cwd: tmpDir, configTheme: null});
|
|
111
111
|
expect(res.status).toBe('warn');
|
|
112
|
-
expect(res.message).toContain('@astryxdesign/theme-
|
|
112
|
+
expect(res.message).toContain('@astryxdesign/theme-neutral');
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('themes: PASS when theme installed and wired via config', () => {
|
|
116
|
-
installPkg('@astryxdesign/theme-
|
|
116
|
+
installPkg('@astryxdesign/theme-neutral', '0.0.14');
|
|
117
117
|
const res = checkThemes({cwd: tmpDir, configTheme: 'default'});
|
|
118
118
|
expect(res.status).toBe('pass');
|
|
119
119
|
});
|
package/src/commands/init.mjs
CHANGED
|
@@ -246,9 +246,9 @@ export function registerInit(program) {
|
|
|
246
246
|
humanLog(' Next steps:');
|
|
247
247
|
humanLog(" 1. Import components: import { Button } from '@astryxdesign/core'");
|
|
248
248
|
humanLog(' 2. Optionally add a theme:');
|
|
249
|
-
humanLog(" import {
|
|
250
|
-
humanLog(' <Theme theme={
|
|
251
|
-
humanLog(` 3. ${run}
|
|
249
|
+
humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
|
|
250
|
+
humanLog(' <Theme theme={neutralTheme}>...</Theme>');
|
|
251
|
+
humanLog(` 3. ${run} astryx --help for all commands`);
|
|
252
252
|
humanLog('');
|
|
253
253
|
});
|
|
254
254
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file Detect the project's package manager from lockfiles.
|
|
5
5
|
*
|
|
6
6
|
* Returns the correct command prefix for running package binaries
|
|
7
|
-
* (e.g. 'npx
|
|
7
|
+
* (e.g. 'npx astryx', 'yarn astryx', 'pnpm exec astryx').
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
@@ -3,24 +3,23 @@
|
|
|
3
3
|
'use client';
|
|
4
4
|
|
|
5
5
|
import * as stylex from '@stylexjs/stylex';
|
|
6
|
-
import {
|
|
7
|
-
SideNav,
|
|
8
|
-
SideNavHeading,
|
|
9
|
-
SideNavItem,
|
|
10
|
-
SideNavSection,
|
|
11
|
-
} from '@astryxdesign/core/SideNav';
|
|
12
|
-
import {Text} from '@astryxdesign/core/Text';
|
|
6
|
+
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
13
7
|
import {Button} from '@astryxdesign/core/Button';
|
|
14
8
|
import {Card} from '@astryxdesign/core/Card';
|
|
9
|
+
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
|
|
15
10
|
import {HStack, VStack, StackItem} from '@astryxdesign/core/Stack';
|
|
16
|
-
import {Layout, LayoutContent
|
|
11
|
+
import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
17
12
|
import {Grid} from '@astryxdesign/core/Grid';
|
|
18
13
|
import {radiusVars} from '@astryxdesign/core/theme/tokens.stylex';
|
|
19
14
|
|
|
20
15
|
const styles = stylex.create({
|
|
21
16
|
previewCard: {
|
|
22
|
-
borderRadius: radiusVars['--radius-
|
|
23
|
-
|
|
17
|
+
borderRadius: radiusVars['--radius-element'],
|
|
18
|
+
},
|
|
19
|
+
// Negative margin offsets each card's 8px padding so the grid content stays
|
|
20
|
+
// visually aligned while giving every card a padded hover/click target.
|
|
21
|
+
cardGrid: {
|
|
22
|
+
margin: -8,
|
|
24
23
|
},
|
|
25
24
|
});
|
|
26
25
|
|
|
@@ -213,30 +212,12 @@ const COMPONENT_CATEGORIES = [
|
|
|
213
212
|
export default function DocumentationOverviewPage() {
|
|
214
213
|
return (
|
|
215
214
|
<Layout
|
|
216
|
-
height="
|
|
215
|
+
height="auto"
|
|
217
216
|
contentWidth={1200}
|
|
218
|
-
start={
|
|
219
|
-
<LayoutPanel hasDivider padding={0}>
|
|
220
|
-
<SideNav header={<SideNavHeading heading="Product Name" />}>
|
|
221
|
-
<SideNavSection title="Navigation" isHeaderHidden>
|
|
222
|
-
<SideNavItem label="Home" isSelected />
|
|
223
|
-
<SideNavItem label="Getting started" />
|
|
224
|
-
</SideNavSection>
|
|
225
|
-
|
|
226
|
-
{COMPONENT_CATEGORIES.map(category => (
|
|
227
|
-
<SideNavSection key={category.label} title={category.label}>
|
|
228
|
-
{category.items.map(item => (
|
|
229
|
-
<SideNavItem key={item.key} label={item.name} />
|
|
230
|
-
))}
|
|
231
|
-
</SideNavSection>
|
|
232
|
-
))}
|
|
233
|
-
</SideNav>
|
|
234
|
-
</LayoutPanel>
|
|
235
|
-
}
|
|
236
217
|
content={
|
|
237
218
|
<LayoutContent padding={8}>
|
|
238
219
|
<VStack gap={10}>
|
|
239
|
-
<Card variant="
|
|
220
|
+
<Card variant="gray" padding={10}>
|
|
240
221
|
<HStack gap={8} vAlign="center">
|
|
241
222
|
<StackItem size="fill">
|
|
242
223
|
<VStack gap={4}>
|
|
@@ -246,11 +227,7 @@ export default function DocumentationOverviewPage() {
|
|
|
246
227
|
beautiful, accessible products.
|
|
247
228
|
</Text>
|
|
248
229
|
<HStack>
|
|
249
|
-
<Button
|
|
250
|
-
label="Get started"
|
|
251
|
-
variant="primary"
|
|
252
|
-
size="lg"
|
|
253
|
-
/>
|
|
230
|
+
<Button label="Get started" variant="primary" size="lg" />
|
|
254
231
|
</HStack>
|
|
255
232
|
</VStack>
|
|
256
233
|
</StackItem>
|
|
@@ -260,25 +237,35 @@ export default function DocumentationOverviewPage() {
|
|
|
260
237
|
|
|
261
238
|
{COMPONENT_CATEGORIES.map(category => (
|
|
262
239
|
<VStack key={category.label} gap={4}>
|
|
263
|
-
<
|
|
264
|
-
<Grid
|
|
240
|
+
<Heading level={2}>{category.label}</Heading>
|
|
241
|
+
<Grid
|
|
242
|
+
columns={{minWidth: 260}}
|
|
243
|
+
gap={2}
|
|
244
|
+
xstyle={styles.cardGrid}>
|
|
265
245
|
{category.items.map(item => (
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
246
|
+
<ClickableCard
|
|
247
|
+
key={item.key}
|
|
248
|
+
label={`Open ${item.name}`}
|
|
249
|
+
onClick={() => {}}
|
|
250
|
+
variant="transparent"
|
|
251
|
+
padding={2}>
|
|
252
|
+
<VStack gap={3}>
|
|
253
|
+
<Card
|
|
254
|
+
variant="muted"
|
|
255
|
+
padding={0}
|
|
256
|
+
minHeight={160}
|
|
257
|
+
xstyle={styles.previewCard}
|
|
258
|
+
/>
|
|
259
|
+
<VStack gap={0.5}>
|
|
260
|
+
<Text type="body" weight="bold">
|
|
261
|
+
{item.name}
|
|
262
|
+
</Text>
|
|
263
|
+
<Text type="body" color="secondary" maxLines={3}>
|
|
264
|
+
{item.desc}
|
|
265
|
+
</Text>
|
|
266
|
+
</VStack>
|
|
280
267
|
</VStack>
|
|
281
|
-
</
|
|
268
|
+
</ClickableCard>
|
|
282
269
|
))}
|
|
283
270
|
</Grid>
|
|
284
271
|
</VStack>
|
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
'use client';
|
|
4
4
|
|
|
5
|
-
import {useState, useMemo} from 'react';
|
|
5
|
+
import {useCallback, useState, useMemo} from 'react';
|
|
6
6
|
import * as stylex from '@stylexjs/stylex';
|
|
7
|
-
import {
|
|
8
|
-
SideNav,
|
|
9
|
-
SideNavHeading,
|
|
10
|
-
SideNavItem,
|
|
11
|
-
SideNavSection,
|
|
12
|
-
} from '@astryxdesign/core/SideNav';
|
|
13
7
|
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
14
8
|
import {Button} from '@astryxdesign/core/Button';
|
|
15
9
|
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
@@ -20,8 +14,10 @@ import {Token} from '@astryxdesign/core/Token';
|
|
|
20
14
|
import {Banner} from '@astryxdesign/core/Banner';
|
|
21
15
|
import {CodeBlock} from '@astryxdesign/core/CodeBlock';
|
|
22
16
|
import {TabList, Tab} from '@astryxdesign/core/TabList';
|
|
17
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
23
18
|
import {HStack, VStack, StackItem} from '@astryxdesign/core/Stack';
|
|
24
19
|
import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
|
|
20
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
25
21
|
import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
|
|
26
22
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
27
23
|
import {Tooltip} from '@astryxdesign/core/Tooltip';
|
|
@@ -29,6 +25,7 @@ import {Table, pixel} from '@astryxdesign/core/Table';
|
|
|
29
25
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
30
26
|
import {Section} from '@astryxdesign/core/Section';
|
|
31
27
|
import {Center} from '@astryxdesign/core/Center';
|
|
28
|
+
import {Outline, type OutlineItem} from '@astryxdesign/core/Outline';
|
|
32
29
|
import {
|
|
33
30
|
ArrowTopRightOnSquareIcon,
|
|
34
31
|
ArrowsPointingOutIcon,
|
|
@@ -37,8 +34,25 @@ import {
|
|
|
37
34
|
|
|
38
35
|
const styles = stylex.create({
|
|
39
36
|
tabListFlush: {marginInlineStart: '-12px'},
|
|
37
|
+
outlinePanel: {
|
|
38
|
+
position: 'sticky',
|
|
39
|
+
top: 24,
|
|
40
|
+
alignSelf: 'start',
|
|
41
|
+
paddingBlockStart: 120,
|
|
42
|
+
},
|
|
40
43
|
});
|
|
41
44
|
|
|
45
|
+
const COMPONENT_OUTLINE_ITEMS: OutlineItem[] = [
|
|
46
|
+
{id: 'usage', label: 'Usage', level: 2},
|
|
47
|
+
{id: 'best-practices', label: 'Best practices', level: 3},
|
|
48
|
+
{id: 'examples', label: 'Examples', level: 2},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const COMPONENT_OUTLINE_OPTIONS = COMPONENT_OUTLINE_ITEMS.map(item => ({
|
|
52
|
+
value: item.id,
|
|
53
|
+
label: item.label,
|
|
54
|
+
}));
|
|
55
|
+
|
|
42
56
|
// ---------------------------------------------------------------------------
|
|
43
57
|
// DialogPreview — stateful dialog preview for component previews
|
|
44
58
|
// ---------------------------------------------------------------------------
|
|
@@ -493,14 +507,21 @@ function getComponentDocs(key: string) {
|
|
|
493
507
|
// ComponentDetailView
|
|
494
508
|
// ---------------------------------------------------------------------------
|
|
495
509
|
|
|
496
|
-
function ComponentDetailView({
|
|
497
|
-
activeNav,
|
|
498
|
-
nav,
|
|
499
|
-
}: {
|
|
500
|
-
activeNav: string;
|
|
501
|
-
nav: React.ReactNode;
|
|
502
|
-
}) {
|
|
510
|
+
function ComponentDetailView({activeNav}: {activeNav: string}) {
|
|
503
511
|
const [exampleTabs, setExampleTabs] = useState<Record<string, string>>({});
|
|
512
|
+
const [activeId, setActiveId] = useState<string | undefined>(
|
|
513
|
+
COMPONENT_OUTLINE_ITEMS[0]?.id,
|
|
514
|
+
);
|
|
515
|
+
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
516
|
+
|
|
517
|
+
const scrollToId = useCallback((id: string) => {
|
|
518
|
+
setActiveId(id);
|
|
519
|
+
const target = document.getElementById(id);
|
|
520
|
+
if (target != null) {
|
|
521
|
+
target.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
522
|
+
window.history.pushState(null, '', `#${id}`);
|
|
523
|
+
}
|
|
524
|
+
}, []);
|
|
504
525
|
|
|
505
526
|
const EXAMPLE_PREVIEWS: Record<string, React.ReactNode[]> = {
|
|
506
527
|
button: [
|
|
@@ -556,25 +577,42 @@ function ComponentDetailView({
|
|
|
556
577
|
|
|
557
578
|
return (
|
|
558
579
|
<Layout
|
|
559
|
-
height="
|
|
580
|
+
height="auto"
|
|
560
581
|
contentWidth={960}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
582
|
+
end={
|
|
583
|
+
isMobile ? undefined : (
|
|
584
|
+
<LayoutPanel
|
|
585
|
+
isScrollable={false}
|
|
586
|
+
label="On this page"
|
|
587
|
+
role="complementary"
|
|
588
|
+
xstyle={styles.outlinePanel}>
|
|
589
|
+
<Outline
|
|
590
|
+
items={COMPONENT_OUTLINE_ITEMS}
|
|
591
|
+
onActiveIdChange={setActiveId}
|
|
592
|
+
/>
|
|
593
|
+
</LayoutPanel>
|
|
594
|
+
)
|
|
565
595
|
}
|
|
566
596
|
content={
|
|
567
|
-
<LayoutContent padding={8}>
|
|
597
|
+
<LayoutContent isScrollable={false} padding={8}>
|
|
568
598
|
<VStack gap={8}>
|
|
569
599
|
<VStack gap={2}>
|
|
570
600
|
<Text type="display-1">{getComponentName(activeNav)}</Text>
|
|
571
601
|
<Text type="supporting" color="secondary">
|
|
572
602
|
March 30, 2026 · Updated 5:40 p.m. PST
|
|
573
603
|
</Text>
|
|
604
|
+
{isMobile && (
|
|
605
|
+
<Selector
|
|
606
|
+
label="On this page"
|
|
607
|
+
isLabelHidden
|
|
608
|
+
options={COMPONENT_OUTLINE_OPTIONS}
|
|
609
|
+
value={activeId}
|
|
610
|
+
onChange={scrollToId}
|
|
611
|
+
width="100%"
|
|
612
|
+
/>
|
|
613
|
+
)}
|
|
574
614
|
</VStack>
|
|
575
615
|
|
|
576
|
-
<Divider />
|
|
577
|
-
|
|
578
616
|
<Card variant="muted" padding={0}>
|
|
579
617
|
<Center height={360}>
|
|
580
618
|
{COMPONENT_PREVIEWS[activeNav] ?? (
|
|
@@ -586,13 +624,18 @@ function ComponentDetailView({
|
|
|
586
624
|
</Card>
|
|
587
625
|
|
|
588
626
|
<VStack gap={4}>
|
|
589
|
-
<Heading level={2}>
|
|
627
|
+
<Heading id="usage" level={2}>
|
|
628
|
+
Usage
|
|
629
|
+
</Heading>
|
|
590
630
|
<Text type="large" weight="normal">
|
|
591
631
|
{docs.usage}
|
|
592
632
|
</Text>
|
|
593
|
-
<Heading level={3}>
|
|
633
|
+
<Heading id="best-practices" level={3}>
|
|
634
|
+
Best practices
|
|
635
|
+
</Heading>
|
|
594
636
|
<Table
|
|
595
637
|
data={docs.bestPractices as Record<string, unknown>[]}
|
|
638
|
+
dividers="none"
|
|
596
639
|
columns={[
|
|
597
640
|
{
|
|
598
641
|
key: 'type',
|
|
@@ -616,14 +659,15 @@ function ComponentDetailView({
|
|
|
616
659
|
},
|
|
617
660
|
]}
|
|
618
661
|
density="spacious"
|
|
619
|
-
dividers="rows"
|
|
620
662
|
/>
|
|
621
663
|
</VStack>
|
|
622
664
|
|
|
623
665
|
<Divider />
|
|
624
666
|
|
|
625
667
|
<VStack gap={4}>
|
|
626
|
-
<Heading level={2}>
|
|
668
|
+
<Heading id="examples" level={2}>
|
|
669
|
+
Examples
|
|
670
|
+
</Heading>
|
|
627
671
|
<Text type="large" weight="normal">
|
|
628
672
|
Explore common configurations, variations, and states for this
|
|
629
673
|
component.
|
|
@@ -675,7 +719,10 @@ function ComponentDetailView({
|
|
|
675
719
|
<TabList
|
|
676
720
|
value={activeTab}
|
|
677
721
|
onChange={value =>
|
|
678
|
-
setExampleTabs(prev => ({
|
|
722
|
+
setExampleTabs(prev => ({
|
|
723
|
+
...prev,
|
|
724
|
+
[tabKey]: value,
|
|
725
|
+
}))
|
|
679
726
|
}
|
|
680
727
|
size="sm"
|
|
681
728
|
xstyle={styles.tabListFlush}>
|
|
@@ -685,7 +732,11 @@ function ComponentDetailView({
|
|
|
685
732
|
{activeTab === 'description' ? (
|
|
686
733
|
<Text type="body">{example.description}</Text>
|
|
687
734
|
) : (
|
|
688
|
-
<CodeBlock
|
|
735
|
+
<CodeBlock
|
|
736
|
+
code={example.code}
|
|
737
|
+
language="tsx"
|
|
738
|
+
width="100%"
|
|
739
|
+
/>
|
|
689
740
|
)}
|
|
690
741
|
</VStack>
|
|
691
742
|
</Section>
|
|
@@ -705,31 +756,5 @@ function ComponentDetailView({
|
|
|
705
756
|
// ---------------------------------------------------------------------------
|
|
706
757
|
|
|
707
758
|
export default function DesignDocumentationPage() {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return (
|
|
711
|
-
<ComponentDetailView
|
|
712
|
-
activeNav={activePage}
|
|
713
|
-
nav={
|
|
714
|
-
<SideNav header={<SideNavHeading heading="Product Name" />}>
|
|
715
|
-
{COMPONENT_CATEGORIES.map(category => (
|
|
716
|
-
<SideNavSection key={category.label} title={category.label}>
|
|
717
|
-
{category.items.map(item => (
|
|
718
|
-
<SideNavItem
|
|
719
|
-
key={item.key}
|
|
720
|
-
label={item.name}
|
|
721
|
-
isSelected={activePage === item.key}
|
|
722
|
-
onClick={
|
|
723
|
-
item.key === 'button'
|
|
724
|
-
? () => setActivePage(item.key)
|
|
725
|
-
: undefined
|
|
726
|
-
}
|
|
727
|
-
/>
|
|
728
|
-
))}
|
|
729
|
-
</SideNavSection>
|
|
730
|
-
))}
|
|
731
|
-
</SideNav>
|
|
732
|
-
}
|
|
733
|
-
/>
|
|
734
|
-
);
|
|
759
|
+
return <ComponentDetailView activeNav="button" />;
|
|
735
760
|
}
|