@astryxdesign/cli 0.0.15 → 0.1.0-canary.08d4cf4
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 +118 -76
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +15 -15
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +6 -6
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -5
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +53 -19
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +10 -14
- package/src/api/doctor.mjs +4 -4
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +2 -1
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +4 -4
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/init.mjs +12 -4
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +353 -169
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/index.mjs +1 -0
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +22 -43
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
package/docs/theme.doc.mjs
CHANGED
|
@@ -14,16 +14,22 @@ export const docs = {
|
|
|
14
14
|
title: 'Quick Start',
|
|
15
15
|
category: 'guide',
|
|
16
16
|
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'code',
|
|
19
|
+
lang: 'bash',
|
|
20
|
+
label: 'Install a theme package',
|
|
21
|
+
code: 'npm install @astryxdesign/theme-neutral',
|
|
22
|
+
},
|
|
17
23
|
{
|
|
18
24
|
type: 'code',
|
|
19
25
|
lang: 'tsx',
|
|
20
26
|
label: 'Basic theme setup (runtime injection)',
|
|
21
27
|
code: `import {Theme} from '@astryxdesign/core';
|
|
22
|
-
import {
|
|
28
|
+
import {neutralTheme} from '@astryxdesign/theme-neutral';
|
|
23
29
|
|
|
24
30
|
function App() {
|
|
25
31
|
return (
|
|
26
|
-
<Theme theme={
|
|
32
|
+
<Theme theme={neutralTheme}>
|
|
27
33
|
<YourApp />
|
|
28
34
|
</Theme>
|
|
29
35
|
);
|
|
@@ -34,17 +40,21 @@ function App() {
|
|
|
34
40
|
lang: 'tsx',
|
|
35
41
|
label: 'Optimized setup (pre-built CSS)',
|
|
36
42
|
code: `import {Theme} from '@astryxdesign/core';
|
|
37
|
-
import {
|
|
38
|
-
import '@astryxdesign/theme-
|
|
43
|
+
import {neutralTheme} from '@astryxdesign/theme-neutral/built';
|
|
44
|
+
import '@astryxdesign/theme-neutral/theme.css';
|
|
39
45
|
|
|
40
46
|
function App() {
|
|
41
47
|
return (
|
|
42
|
-
<Theme theme={
|
|
48
|
+
<Theme theme={neutralTheme}>
|
|
43
49
|
<YourApp />
|
|
44
50
|
</Theme>
|
|
45
51
|
);
|
|
46
52
|
}`,
|
|
47
53
|
},
|
|
54
|
+
{
|
|
55
|
+
type: 'prose',
|
|
56
|
+
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` — the same pattern works for every theme; just swap the package and import name.',
|
|
57
|
+
},
|
|
48
58
|
{
|
|
49
59
|
type: 'prose',
|
|
50
60
|
text: 'The default import uses runtime style injection, which works everywhere with no build step. The `/built` import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.',
|
|
@@ -55,24 +65,48 @@ function App() {
|
|
|
55
65
|
title: 'Available Themes',
|
|
56
66
|
category: 'guide',
|
|
57
67
|
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'prose',
|
|
70
|
+
text: 'Install the theme package you want with `npm install @astryxdesign/theme-{name}`, then import its theme object as shown below.',
|
|
71
|
+
},
|
|
58
72
|
{
|
|
59
73
|
type: 'table',
|
|
60
74
|
headers: ['Theme', 'Import', 'Description'],
|
|
61
75
|
rows: [
|
|
62
|
-
[
|
|
63
|
-
'Default',
|
|
64
|
-
"import {defaultTheme} from '@astryxdesign/theme-default'",
|
|
65
|
-
'Blue accent, system fonts, light/dark',
|
|
66
|
-
],
|
|
67
76
|
[
|
|
68
77
|
'Neutral',
|
|
69
78
|
"import {neutralTheme} from '@astryxdesign/theme-neutral'",
|
|
70
|
-
'
|
|
79
|
+
'Muted, minimal aesthetic with system fonts. A good starting point.',
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
'Butter',
|
|
83
|
+
"import {butterTheme} from '@astryxdesign/theme-butter'",
|
|
84
|
+
'Golden, buttery surfaces with blue accents; Sarina + Outfit type.',
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
'Chocolate',
|
|
88
|
+
"import {chocolateTheme} from '@astryxdesign/theme-chocolate'",
|
|
89
|
+
'Warm brown tones and cozy beige; Fraunces + Albert Sans type.',
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
'Gothic',
|
|
93
|
+
"import {gothicTheme} from '@astryxdesign/theme-gothic'",
|
|
94
|
+
'Dark-only atmospheric theme; deep blue-gray surfaces, distressed display type.',
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
'Matcha',
|
|
98
|
+
"import {matchaTheme} from '@astryxdesign/theme-matcha'",
|
|
99
|
+
'Earthy green theme with Figtree typography.',
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
'Stone',
|
|
103
|
+
"import {stoneTheme} from '@astryxdesign/theme-stone'",
|
|
104
|
+
'Warm stone and slate tones; Montserrat + Figtree type.',
|
|
71
105
|
],
|
|
72
106
|
[
|
|
73
|
-
'
|
|
74
|
-
"import {
|
|
75
|
-
'
|
|
107
|
+
'Y2K',
|
|
108
|
+
"import {y2kTheme} from '@astryxdesign/theme-y2k'",
|
|
109
|
+
'Playful Y2K pop; periwinkle body, holographic accents, Poppins + Crimson Text.',
|
|
76
110
|
],
|
|
77
111
|
],
|
|
78
112
|
},
|
|
@@ -194,14 +228,14 @@ const myTheme = defineTheme({
|
|
|
194
228
|
{
|
|
195
229
|
type: 'code',
|
|
196
230
|
lang: 'tsx',
|
|
197
|
-
label: 'Extending the
|
|
231
|
+
label: 'Extending the neutral theme',
|
|
198
232
|
code: `import {defineTheme} from '@astryxdesign/core/theme';
|
|
199
|
-
import {
|
|
233
|
+
import {neutralTheme} from '@astryxdesign/theme-neutral';
|
|
200
234
|
import {myIcons} from './icons';
|
|
201
235
|
|
|
202
236
|
const brandTheme = defineTheme({
|
|
203
237
|
name: 'brand',
|
|
204
|
-
extends:
|
|
238
|
+
extends: neutralTheme,
|
|
205
239
|
icons: myIcons,
|
|
206
240
|
tokens: {
|
|
207
241
|
'--color-accent': ['#7B61FF', '#9B85FF'],
|
|
@@ -525,9 +559,9 @@ const pandaOrEmotionTheme = {
|
|
|
525
559
|
lang: 'ts',
|
|
526
560
|
label: 'Resolve token values without a hook',
|
|
527
561
|
code: `import {resolveThemeTokens} from '@astryxdesign/core/theme/tokens';
|
|
528
|
-
import {
|
|
562
|
+
import {neutralTheme} from '@astryxdesign/theme-neutral';
|
|
529
563
|
|
|
530
|
-
const lightTokens = resolveThemeTokens(
|
|
564
|
+
const lightTokens = resolveThemeTokens(neutralTheme, {mode: 'light'});
|
|
531
565
|
const chartTheme = {
|
|
532
566
|
textColor: lightTokens['--color-text-primary'],
|
|
533
567
|
seriesColor: lightTokens['--color-data-categorical-blue'],
|
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsZh = {
|
|
6
6
|
description: 'Theme 提供者、自定义主题、亮/暗模式和组件样式覆盖。',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: '快速开始', content: [null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
-
{ title: '可用主题', content: [null, { type: 'prose', text: '
|
|
8
|
+
{ title: '快速开始', content: [null, null, null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
+
{ title: '可用主题', content: [null, null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
|
|
10
10
|
{ title: 'Theme 属性', content: [null] },
|
|
11
11
|
{ title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
|
|
@@ -34,7 +34,7 @@ export const docs = {
|
|
|
34
34
|
type: 'code',
|
|
35
35
|
lang: 'text',
|
|
36
36
|
label: 'Paste this into your AI',
|
|
37
|
-
code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your
|
|
37
|
+
code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your Astryx context. Read the generated file.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'prose',
|
|
@@ -103,7 +103,7 @@ npx astryx agent-docs --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
|
103
103
|
type: 'code',
|
|
104
104
|
lang: 'text',
|
|
105
105
|
label: 'Paste this into your AI',
|
|
106
|
-
code: `Before writing any
|
|
106
|
+
code: `Before writing any Astryx code, check your knowledge:
|
|
107
107
|
|
|
108
108
|
1. What is the correct import path for Button?
|
|
109
109
|
2. How do you make an Dialog non-dismissible?
|
|
@@ -165,7 +165,7 @@ npx astryx docs tokens --dense`,
|
|
|
165
165
|
content: [
|
|
166
166
|
{
|
|
167
167
|
type: 'prose',
|
|
168
|
-
text: '
|
|
168
|
+
text: 'Astryx ships a Model Context Protocol (MCP) server that any MCP-compatible AI tool can connect to. Instead of manually pasting CLI output, the AI can query the Astryx design system directly, searching for components, reading full documentation, and pulling code examples on demand.',
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'prose',
|
|
@@ -183,7 +183,7 @@ npx astryx docs tokens --dense`,
|
|
|
183
183
|
"mcpServers": {
|
|
184
184
|
"xds": {
|
|
185
185
|
"type": "url",
|
|
186
|
-
"url": "https://astryx.
|
|
186
|
+
"url": "https://astryx.atmeta.com/mcp"
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.1.0-canary.08d4cf4",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"astryx",
|
|
19
19
|
"cli",
|
|
20
20
|
"design-system",
|
|
21
21
|
"init",
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@clack/prompts": "^1.5.1",
|
|
52
52
|
"commander": "^12.1.0",
|
|
53
|
-
"jiti": "^2.7.0"
|
|
53
|
+
"jiti": "^2.7.0",
|
|
54
|
+
"jscodeshift": "^17.3.0"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
|
-
"@astryxdesign/core": "
|
|
57
|
-
"@astryxdesign/lab": "
|
|
58
|
-
"@astryxdesign/theme-
|
|
59
|
-
"@astryxdesign/theme-neutral": "*"
|
|
57
|
+
"@astryxdesign/core": "0.1.0-canary.08d4cf4",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.08d4cf4",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.08d4cf4"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@astryxdesign/core": {
|
|
@@ -65,18 +65,14 @@
|
|
|
65
65
|
"@astryxdesign/lab": {
|
|
66
66
|
"optional": true
|
|
67
67
|
},
|
|
68
|
-
"@astryxdesign/theme-default": {
|
|
69
|
-
"optional": true
|
|
70
|
-
},
|
|
71
68
|
"@astryxdesign/theme-neutral": {
|
|
72
69
|
"optional": true
|
|
73
70
|
}
|
|
74
71
|
},
|
|
75
72
|
"devDependencies": {
|
|
76
|
-
"@astryxdesign/core": "
|
|
77
|
-
"@astryxdesign/lab": "
|
|
78
|
-
"@astryxdesign/theme-
|
|
79
|
-
"@astryxdesign/theme-neutral": "*"
|
|
73
|
+
"@astryxdesign/core": "0.1.0-canary.08d4cf4",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.08d4cf4",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.08d4cf4"
|
|
80
76
|
},
|
|
81
77
|
"scripts": {
|
|
82
78
|
"astryx": "node bin/astryx.mjs",
|
package/src/api/doctor.mjs
CHANGED
|
@@ -240,7 +240,7 @@ export function checkThemes(ctx) {
|
|
|
240
240
|
label: 'Theme packages',
|
|
241
241
|
status: 'warn',
|
|
242
242
|
message: 'No @astryxdesign/theme-* packages are installed.',
|
|
243
|
-
fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-
|
|
243
|
+
fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set xds.theme.',
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -355,8 +355,8 @@ export function checkAgentDocs(ctx) {
|
|
|
355
355
|
try {
|
|
356
356
|
const content = fs.readFileSync(path.join(ctx.cwd, rel), 'utf-8');
|
|
357
357
|
return (
|
|
358
|
-
content.includes('<!-- XDS:START -->') &&
|
|
359
|
-
content.includes('<!-- XDS:END -->')
|
|
358
|
+
(content.includes('<!-- ASTRYX:START -->') || content.includes('<!-- XDS:START -->')) &&
|
|
359
|
+
(content.includes('<!-- ASTRYX:END -->') || content.includes('<!-- XDS:END -->'))
|
|
360
360
|
);
|
|
361
361
|
} catch {
|
|
362
362
|
return false;
|
|
@@ -368,7 +368,7 @@ export function checkAgentDocs(ctx) {
|
|
|
368
368
|
id: 'agent-docs',
|
|
369
369
|
label: 'AI agent docs',
|
|
370
370
|
status: 'warn',
|
|
371
|
-
message: `Agent docs present (${present.join(', ')}) but no
|
|
371
|
+
message: `Agent docs present (${present.join(', ')}) but no Astryx section markers found.`,
|
|
372
372
|
fix: 'Add the XDS section to your agent docs with `astryx init --features agents`.',
|
|
373
373
|
};
|
|
374
374
|
}
|
package/src/api/search.mjs
CHANGED
|
@@ -41,14 +41,184 @@ import {
|
|
|
41
41
|
} from '../lib/component-discovery.mjs';
|
|
42
42
|
import {discoverHooks, findHookDoc} from '../lib/hook-discovery.mjs';
|
|
43
43
|
import {levenshteinDistance} from '../lib/string-utils.mjs';
|
|
44
|
-
import {discoverTemplates} from './template.mjs';
|
|
44
|
+
import {discoverTemplates, extractComponents} from './template.mjs';
|
|
45
45
|
import {AstryxError} from './error.mjs';
|
|
46
46
|
|
|
47
47
|
const DOCS_DIR = path.join(CLI_ROOT, 'docs');
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Synonym / intent map: product-language terms an agent is likely to type,
|
|
51
|
+
* expanded to the catalog's vocabulary so oblique queries still rank. Keys and
|
|
52
|
+
* values are matched bidirectionally (typing any value also pulls in the key
|
|
53
|
+
* and its siblings). Lowercase, single words or short phrases.
|
|
54
|
+
*/
|
|
55
|
+
const SYNONYMS = {
|
|
56
|
+
dashboard: ['overview', 'analytics', 'kpi', 'kpis', 'metrics', 'stats', 'reporting', 'insights', 'control'],
|
|
57
|
+
login: ['signin', 'auth', 'authentication', 'sso', 'credentials', 'account'],
|
|
58
|
+
signup: ['register', 'registration', 'onboarding'],
|
|
59
|
+
payment: ['checkout', 'billing', 'card', 'pay', 'purchase', 'order'],
|
|
60
|
+
pricing: ['plans', 'plan', 'tiers', 'tier', 'subscription', 'subscriptions'],
|
|
61
|
+
chat: ['messaging', 'message', 'messages', 'conversation', 'inbox', 'dm'],
|
|
62
|
+
settings: ['preferences', 'config', 'configuration', 'account'],
|
|
63
|
+
calendar: ['schedule', 'scheduling', 'events', 'event', 'month', 'agenda'],
|
|
64
|
+
table: ['list', 'rows', 'records', 'grid', 'spreadsheet', 'datatable'],
|
|
65
|
+
gallery: ['photos', 'photo', 'images', 'image', 'pictures'],
|
|
66
|
+
hero: ['banner', 'splash', 'headline', 'landing'],
|
|
67
|
+
form: ['fields', 'input', 'inputs', 'survey'],
|
|
68
|
+
profile: ['bio', 'avatar', 'user'],
|
|
69
|
+
documentation: ['docs', 'reference', 'guide', 'api'],
|
|
70
|
+
navigation: ['nav', 'menu', 'sidebar'],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Flatten into a token -> Set(expansions) lookup (bidirectional).
|
|
74
|
+
const SYNONYM_INDEX = (() => {
|
|
75
|
+
const idx = new Map();
|
|
76
|
+
const add = (a, b) => {
|
|
77
|
+
if (!idx.has(a)) idx.set(a, new Set());
|
|
78
|
+
idx.get(a).add(b);
|
|
79
|
+
};
|
|
80
|
+
for (const [key, vals] of Object.entries(SYNONYMS)) {
|
|
81
|
+
for (const v of vals) {
|
|
82
|
+
add(key, v);
|
|
83
|
+
add(v, key);
|
|
84
|
+
for (const v2 of vals) if (v2 !== v) add(v, v2);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return idx;
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Light stemmer: strips common English suffixes so "charts"/"charting" and
|
|
92
|
+
* "chart" share a root. Deliberately crude (no Porter) — good enough to bridge
|
|
93
|
+
* plural/gerund gaps without a dependency.
|
|
94
|
+
* @param {string} w
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function stem(w) {
|
|
98
|
+
let s = w;
|
|
99
|
+
for (const suf of ['ing', 'ed', 'ies', 'es', 's']) {
|
|
100
|
+
if (s.length > suf.length + 2 && s.endsWith(suf)) {
|
|
101
|
+
s = suf === 'ies' ? s.slice(0, -3) + 'y' : s.slice(0, -suf.length);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
49
109
|
/** Valid domain filters for `--type`. */
|
|
50
110
|
export const SEARCH_DOMAINS = ['component', 'hook', 'doc', 'template'];
|
|
51
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Filler words stripped from multi-word queries so natural-language phrasing
|
|
114
|
+
* ("a page where you can see business stats") ranks on its content words.
|
|
115
|
+
*/
|
|
116
|
+
const STOPWORDS = new Set([
|
|
117
|
+
'a', 'an', 'the', 'of', 'for', 'to', 'with', 'and', 'or', 'in', 'on', 'at',
|
|
118
|
+
'by', 'that', 'this', 'my', 'your', 'our', 'their', 'is', 'are', 'be', 'it',
|
|
119
|
+
'its', 'as', 'from', 'page', 'screen', 'app', 'application', 'view', 'where',
|
|
120
|
+
'you', 'can', 'some', 'like', 'just', 'basically', 'kinda', 'want', 'wants',
|
|
121
|
+
'need', 'needs', 'something', 'thing', 'things', 'build', 'make', 'create',
|
|
122
|
+
'i', 'me', 'we', 'us', 'so', 'up', 'out', 'over', 'side', 'one', 'big',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Split a query into meaningful content tokens (lowercased, stopwords + very
|
|
127
|
+
* short words removed). Empty for single-word queries (callers fall back to
|
|
128
|
+
* whole-phrase scoring).
|
|
129
|
+
* @param {string} term - Already-lowercased query.
|
|
130
|
+
* @returns {string[]}
|
|
131
|
+
*/
|
|
132
|
+
export function tokenizeQuery(term) {
|
|
133
|
+
return term
|
|
134
|
+
.split(/\s+/)
|
|
135
|
+
// Strip only leading/trailing punctuation; keep joined identifiers intact
|
|
136
|
+
// (e.g. "foo_bar" stays one token) so gibberish stays gibberish.
|
|
137
|
+
.map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ''))
|
|
138
|
+
.filter(t => t.length >= 2 && !STOPWORDS.has(t));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Score a candidate against a query, handling multi-word natural language.
|
|
143
|
+
* Tries the whole phrase (so exact/near matches still win) AND a per-token
|
|
144
|
+
* pass (so "data table with filters" matches `table-page` via table+filter),
|
|
145
|
+
* and returns whichever is stronger.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} term - Lowercased full query.
|
|
148
|
+
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
149
|
+
* @param {object} candidate
|
|
150
|
+
* @returns {{score: number, reason: string} | null}
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* Minimum per-token score (in the multi-word pass) to count as a real match.
|
|
154
|
+
* 50 = a genuine name/keyword/description hit; below that is loose Levenshtein
|
|
155
|
+
* fuzz that would otherwise turn gibberish queries into noise.
|
|
156
|
+
*/
|
|
157
|
+
const MIN_TOKEN_SCORE = 50;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Best score for a token against a candidate, fanning out through synonyms
|
|
161
|
+
* (synonym hits are discounted so a direct hit always wins).
|
|
162
|
+
* @returns {{score: number, reason: string} | null}
|
|
163
|
+
*/
|
|
164
|
+
function bestForToken(tok, candidate) {
|
|
165
|
+
let best = scoreCandidate(tok, candidate);
|
|
166
|
+
const syns = SYNONYM_INDEX.get(tok);
|
|
167
|
+
if (syns) {
|
|
168
|
+
for (const s of syns) {
|
|
169
|
+
const h = scoreCandidate(s, candidate);
|
|
170
|
+
if (h) {
|
|
171
|
+
const score = Math.round(h.score * 0.85);
|
|
172
|
+
if (!best || score > best.score) best = {score, reason: `${h.reason} (~${tok})`};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return best;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function scoreQuery(term, tokens, candidate) {
|
|
180
|
+
const full = scoreCandidate(term, candidate);
|
|
181
|
+
|
|
182
|
+
// 0–1 content tokens: keep whole-phrase fuzzy matching (typo tolerance for
|
|
183
|
+
// single words), but if stopwords left exactly one DIFFERENT token (e.g.
|
|
184
|
+
// "pricing page" → "pricing"), score that token too and take the stronger.
|
|
185
|
+
if (tokens.length <= 1) {
|
|
186
|
+
const single = tokens.length === 1 ? bestForToken(tokens[0], candidate) : null;
|
|
187
|
+
if (full && (!single || full.score >= single.score)) return full;
|
|
188
|
+
return single;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Multi-word natural language: score each content token, counting only
|
|
192
|
+
// strong hits, then reward coverage so candidates matching more terms win.
|
|
193
|
+
let sum = 0;
|
|
194
|
+
let matched = 0;
|
|
195
|
+
const hitTerms = [];
|
|
196
|
+
for (const tok of tokens) {
|
|
197
|
+
const h = bestForToken(tok, candidate);
|
|
198
|
+
if (h && h.score >= MIN_TOKEN_SCORE) {
|
|
199
|
+
sum += h.score;
|
|
200
|
+
matched++;
|
|
201
|
+
hitTerms.push(tok);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (matched === 0) return full;
|
|
205
|
+
|
|
206
|
+
// Reward the AVERAGE strength of the concepts that matched (not divided by
|
|
207
|
+
// total query length — that penalizes verbose / low-fidelity prompts), plus
|
|
208
|
+
// a bonus per additional matched concept and a coverage term. A candidate
|
|
209
|
+
// that matches several of the query's concepts beats one matching a single
|
|
210
|
+
// incidental word.
|
|
211
|
+
const avgMatched = sum / matched;
|
|
212
|
+
const coverage = matched / tokens.length;
|
|
213
|
+
const tokenScore = Math.round(avgMatched + Math.min(matched - 1, 3) * 12 + coverage * 15);
|
|
214
|
+
|
|
215
|
+
if (full && full.score >= tokenScore) return full;
|
|
216
|
+
return {
|
|
217
|
+
score: tokenScore,
|
|
218
|
+
reason: `matches ${matched}/${tokens.length} terms: ${hitTerms.join(', ')}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
52
222
|
/**
|
|
53
223
|
* Score a single candidate against the search term across name, keywords,
|
|
54
224
|
* and prose signals. Returns the best (highest) score plus a human reason,
|
|
@@ -107,10 +277,13 @@ export function scoreCandidate(term, {name, keywords = [], description = '', pro
|
|
|
107
277
|
else if (dist === 2) consider(30, `keyword "${kw}" (distance ${dist})`);
|
|
108
278
|
}
|
|
109
279
|
|
|
110
|
-
// ── Prose / description signals (whole
|
|
280
|
+
// ── Prose / description signals (stem-tolerant whole word) ──────
|
|
281
|
+
// Match the term's stem as a whole word, tolerating plural/gerund suffixes
|
|
282
|
+
// so "chart" matches "charts" and "filter" matches "filtering".
|
|
111
283
|
if (term.length >= 3) {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
284
|
+
const root = stem(term);
|
|
285
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
286
|
+
const re = new RegExp(`\\b${escaped}(s|es|ing|ed|ies)?\\b`);
|
|
114
287
|
if (description && re.test(description.toLowerCase())) {
|
|
115
288
|
consider(50, `description mentions "${term}"`);
|
|
116
289
|
} else {
|
|
@@ -240,14 +413,30 @@ async function gatherTemplates(cwd) {
|
|
|
240
413
|
} catch {
|
|
241
414
|
return [];
|
|
242
415
|
}
|
|
243
|
-
return templates.map(t =>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
416
|
+
return templates.map(t => {
|
|
417
|
+
// Blocks ship componentsUsed; page templates don't, so derive them from the
|
|
418
|
+
// source. Category words (e.g. "Dashboard - Analytics") are strong intent
|
|
419
|
+
// signal for pages, which otherwise only index on name + description.
|
|
420
|
+
let keywords = Array.isArray(t.componentsUsed) ? [...t.componentsUsed] : [];
|
|
421
|
+
if (t.type === 'page') {
|
|
422
|
+
if (t.filePath) {
|
|
423
|
+
try {
|
|
424
|
+
keywords = keywords.concat(extractComponents(t.filePath));
|
|
425
|
+
} catch {
|
|
426
|
+
// Best-effort: skip keyword enrichment if the source can't be read.
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (t.category) keywords = keywords.concat(t.category.split(/[^A-Za-z0-9]+/).filter(Boolean));
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
domain: 'template',
|
|
433
|
+
name: t.dirName,
|
|
434
|
+
keywords,
|
|
435
|
+
description: t.description || '',
|
|
436
|
+
_displayName: t.name,
|
|
437
|
+
_kind: t.type, // 'page' | 'block'
|
|
438
|
+
};
|
|
439
|
+
});
|
|
251
440
|
}
|
|
252
441
|
|
|
253
442
|
/**
|
|
@@ -325,6 +514,7 @@ export async function search(query, options = {}) {
|
|
|
325
514
|
}
|
|
326
515
|
|
|
327
516
|
const term = String(query).trim().toLowerCase();
|
|
517
|
+
const tokens = tokenizeQuery(term);
|
|
328
518
|
|
|
329
519
|
const coreDir = findCoreDir(cwd);
|
|
330
520
|
if (!coreDir) {
|
|
@@ -342,9 +532,13 @@ export async function search(query, options = {}) {
|
|
|
342
532
|
|
|
343
533
|
const all = [...components, ...hooks, ...docTopics, ...templates];
|
|
344
534
|
|
|
535
|
+
// Score every candidate on its own merits. The consumer groups results by
|
|
536
|
+
// role (page / block / component) and takes the top of each, so there's no
|
|
537
|
+
// cross-role competition to engineer — a target page only needs to be the
|
|
538
|
+
// strongest PAGE, not outrank every component.
|
|
345
539
|
const scored = [];
|
|
346
540
|
for (const candidate of all) {
|
|
347
|
-
const hit =
|
|
541
|
+
const hit = scoreQuery(term, tokens, candidate);
|
|
348
542
|
if (hit) scored.push(toResult(candidate, hit.score, hit.reason));
|
|
349
543
|
}
|
|
350
544
|
|
package/src/api/template.mjs
CHANGED
|
@@ -95,6 +95,7 @@ async function discoverPages() {
|
|
|
95
95
|
dirName: dir.name,
|
|
96
96
|
name: doc?.name || dir.name,
|
|
97
97
|
description: doc?.description || '',
|
|
98
|
+
category: doc?.category || '',
|
|
98
99
|
isReady: doc?.isReady ?? true,
|
|
99
100
|
scaffold: doc?.scaffold ?? false,
|
|
100
101
|
filePath: path.join(dirPath, 'page.tsx'),
|
|
@@ -245,7 +246,7 @@ const UBIQUITOUS = new Set([
|
|
|
245
246
|
'StackItem', 'Icon',
|
|
246
247
|
]);
|
|
247
248
|
|
|
248
|
-
function extractComponents(pagePath) {
|
|
249
|
+
export function extractComponents(pagePath) {
|
|
249
250
|
const src = fs.readFileSync(pagePath, 'utf-8');
|
|
250
251
|
// Match JSX opening tags, e.g. `<Section` or the legacy `<XDSSection`.
|
|
251
252
|
// Templates author bare component names post un-prefix migration
|
|
@@ -17,6 +17,7 @@ const registry = new Map([
|
|
|
17
17
|
['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
|
|
18
18
|
['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
|
|
19
19
|
['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
|
|
20
|
+
['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
// Re-export from the shared utility so registry callers and other consumers
|