@astryxdesign/cli 0.3.0-canary.e6af10a → 0.3.0-canary.ec85ba0
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/api/template/template.test.mjs +26 -0
- package/assets/codemods/transforms/v0.3.0/__tests__/migrate-table-rowexpansion-to-tree.test.mjs +201 -0
- package/assets/codemods/transforms/v0.3.0/index.mjs +8 -0
- package/assets/codemods/transforms/v0.3.0/migrate-table-rowexpansion-to-tree.mjs +214 -0
- package/assets/docs/browser-support.doc.mjs +6 -6
- package/assets/docs/icons.doc.mjs +3 -3
- package/assets/docs/illustrations.doc.mjs +2 -2
- package/assets/docs/internationalization.doc.mjs +1 -1
- package/assets/docs/migration.doc.mjs +1 -1
- package/assets/docs/principles.doc.mjs +1 -1
- package/assets/docs/styling-libraries.doc.mjs +1 -1
- package/assets/docs/styling.doc.mjs +4 -4
- package/assets/docs/theme.doc.mjs +3 -3
- package/assets/docs/typography.doc.mjs +2 -2
- package/assets/templates/blocks/components/Table/TableRowExpansionTable.tsx +61 -58
- package/assets/templates/themes/neutral/neutralTheme.ts +4 -1
- package/authoring/doctypes/base/type.ts +9 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +14 -1
- package/foundation/discovery/template-adapter.d.mts +6 -3
- package/foundation/discovery/template-adapter.mjs +30 -14
- package/package.json +9 -9
|
@@ -52,6 +52,32 @@ describe('stripTemplateAssetRefs', () => {
|
|
|
52
52
|
const out = stripTemplateAssetRefs(src);
|
|
53
53
|
expect(out).toBe(src);
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
it('strips a /template-assets .mp4 source to an empty string, not the image data URI (#4780)', () => {
|
|
57
|
+
const src = "src: '/template-assets/Nature-1.mp4',";
|
|
58
|
+
const out = stripTemplateAssetRefs(src);
|
|
59
|
+
expect(out).toBe("src: '',");
|
|
60
|
+
expect(out).not.toContain('data:image/svg+xml,');
|
|
61
|
+
expect(out).not.toContain('/template-assets/');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('strips other video extensions (.webm, .mov, .ogv) the same way', () => {
|
|
65
|
+
for (const ext of ['webm', 'mov', 'ogv']) {
|
|
66
|
+
const src = `src: '/template-assets/clip.${ext}',`;
|
|
67
|
+
const out = stripTemplateAssetRefs(src);
|
|
68
|
+
expect(out).toBe("src: '',");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('still replaces an adjacent image reference with the placeholder when a video reference is also present', () => {
|
|
73
|
+
const src = [
|
|
74
|
+
"poster: '/template-assets/Nature-1-poster.jpg',",
|
|
75
|
+
"src: '/template-assets/Nature-1.mp4',",
|
|
76
|
+
].join('\n');
|
|
77
|
+
const out = stripTemplateAssetRefs(src);
|
|
78
|
+
expect(out).toContain('data:image/svg+xml,');
|
|
79
|
+
expect(out).toContain("src: '',");
|
|
80
|
+
});
|
|
55
81
|
});
|
|
56
82
|
|
|
57
83
|
describe('template --skeleton component extraction (prefix-agnostic)', () => {
|
package/assets/codemods/transforms/v0.3.0/__tests__/migrate-table-rowexpansion-to-tree.test.mjs
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source) {
|
|
6
|
+
const {default: transform} = await import(
|
|
7
|
+
'../migrate-table-rowexpansion-to-tree.mjs'
|
|
8
|
+
);
|
|
9
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
10
|
+
const j = jscodeshift.withParser('tsx');
|
|
11
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
12
|
+
const file = {source, path: 'test.tsx'};
|
|
13
|
+
const result = transform(file, api);
|
|
14
|
+
return result ?? source;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalize(str) {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.replace(/\{\s+/g, '{')
|
|
21
|
+
.replace(/\s+\}/g, '}')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('migrate-table-rowexpansion-to-tree', () => {
|
|
26
|
+
it('rewrites the useTableRowExpansionState import to useTableTreeState', async () => {
|
|
27
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
28
|
+
function C() {
|
|
29
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
30
|
+
baseData: tree,
|
|
31
|
+
getChildren: item => item.children ?? [],
|
|
32
|
+
getRowKey: item => item.id,
|
|
33
|
+
expandedKeys,
|
|
34
|
+
setExpandedKeys,
|
|
35
|
+
});
|
|
36
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
37
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
38
|
+
}`;
|
|
39
|
+
const output = await applyTransform(input);
|
|
40
|
+
expect(output).not.toContain('useTableRowExpansionState');
|
|
41
|
+
expect(output).toContain('useTableTreeState');
|
|
42
|
+
expect(output).toContain('useTableTreeData');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('maps baseData to data and getChildren to childrenKey', async () => {
|
|
46
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
47
|
+
function C() {
|
|
48
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
49
|
+
baseData: tree,
|
|
50
|
+
getChildren: item => item.children ?? [],
|
|
51
|
+
getRowKey: item => item.id,
|
|
52
|
+
expandedKeys,
|
|
53
|
+
setExpandedKeys,
|
|
54
|
+
});
|
|
55
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
56
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
57
|
+
}`;
|
|
58
|
+
const output = await applyTransform(input);
|
|
59
|
+
const n = normalize(output);
|
|
60
|
+
expect(n).toContain('data: tree');
|
|
61
|
+
expect(n).toContain("childrenKey: 'children'");
|
|
62
|
+
expect(output).not.toContain('baseData');
|
|
63
|
+
expect(output).not.toContain('getChildren');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('maps getRowKey to idKey preserving the accessor function', async () => {
|
|
67
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
68
|
+
function C() {
|
|
69
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
70
|
+
baseData: tree,
|
|
71
|
+
getChildren: item => item.children ?? [],
|
|
72
|
+
getRowKey: item => item.id,
|
|
73
|
+
expandedKeys,
|
|
74
|
+
setExpandedKeys,
|
|
75
|
+
});
|
|
76
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
77
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
78
|
+
}`;
|
|
79
|
+
const output = await applyTransform(input);
|
|
80
|
+
const n = normalize(output);
|
|
81
|
+
expect(n).toContain('idKey: item => item.id');
|
|
82
|
+
expect(output).not.toContain('getRowKey');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('swaps the useTableRowExpansion plugin call for useTableTreeData', async () => {
|
|
86
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
87
|
+
function C() {
|
|
88
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
89
|
+
baseData: tree,
|
|
90
|
+
getChildren: item => item.children ?? [],
|
|
91
|
+
getRowKey: item => item.id,
|
|
92
|
+
expandedKeys,
|
|
93
|
+
setExpandedKeys,
|
|
94
|
+
});
|
|
95
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
96
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
97
|
+
}`;
|
|
98
|
+
const output = await applyTransform(input);
|
|
99
|
+
// The state hook now returns treeConfig; the plugin consumes it.
|
|
100
|
+
expect(output).toContain('treeConfig');
|
|
101
|
+
expect(output).toContain('useTableTreeData(treeConfig)');
|
|
102
|
+
expect(output).not.toMatch(/useTableRowExpansion\(/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('maps getIsItemExpandable to isItemExpandable', async () => {
|
|
106
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
107
|
+
function C() {
|
|
108
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
109
|
+
baseData: tree,
|
|
110
|
+
getChildren: item => item.children ?? [],
|
|
111
|
+
getRowKey: item => item.id,
|
|
112
|
+
getIsItemExpandable: item => item.type === 'folder',
|
|
113
|
+
expandedKeys,
|
|
114
|
+
setExpandedKeys,
|
|
115
|
+
});
|
|
116
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
117
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
118
|
+
}`;
|
|
119
|
+
const output = await applyTransform(input);
|
|
120
|
+
expect(output).toContain('isItemExpandable');
|
|
121
|
+
expect(output).not.toContain('getIsItemExpandable');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('leaves the new detail-panel usage (renderExpanded) untouched', async () => {
|
|
125
|
+
const input = `import {Table, useTableRowExpansion} from '@astryxdesign/core';
|
|
126
|
+
function C() {
|
|
127
|
+
const expansion = useTableRowExpansion({
|
|
128
|
+
expandedKeys,
|
|
129
|
+
onToggle,
|
|
130
|
+
getRowKey: item => item.id,
|
|
131
|
+
renderExpanded: item => <Details item={item} />,
|
|
132
|
+
});
|
|
133
|
+
return <Table data={rows} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
134
|
+
}`;
|
|
135
|
+
const output = await applyTransform(input);
|
|
136
|
+
// No useTableRowExpansionState import here, and renderExpanded present:
|
|
137
|
+
// this is the new detail-panel API. The codemod must not touch it.
|
|
138
|
+
expect(output).toBe(input);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('does not touch files that never imported the row-expansion hooks', async () => {
|
|
142
|
+
const input = `import {Table, useTableSelection} from '@astryxdesign/core';
|
|
143
|
+
const t = <Table data={rows} columns={cols} idKey="id" />;`;
|
|
144
|
+
const output = await applyTransform(input);
|
|
145
|
+
expect(output).toBe(input);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('supports the @xds/core import source alias', async () => {
|
|
149
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@xds/core';
|
|
150
|
+
function C() {
|
|
151
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
152
|
+
baseData: tree,
|
|
153
|
+
getChildren: item => item.children ?? [],
|
|
154
|
+
getRowKey: item => item.id,
|
|
155
|
+
expandedKeys,
|
|
156
|
+
setExpandedKeys,
|
|
157
|
+
});
|
|
158
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
159
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
160
|
+
}`;
|
|
161
|
+
const output = await applyTransform(input);
|
|
162
|
+
expect(output).toContain('useTableTreeState');
|
|
163
|
+
expect(output).not.toContain('useTableRowExpansionState');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('emits treeConfig as a shorthand (not treeConfig: treeConfig)', async () => {
|
|
167
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
168
|
+
function C() {
|
|
169
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
170
|
+
baseData: tree,
|
|
171
|
+
getChildren: item => item.children ?? [],
|
|
172
|
+
getRowKey: item => item.id,
|
|
173
|
+
expandedKeys,
|
|
174
|
+
setExpandedKeys,
|
|
175
|
+
});
|
|
176
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
177
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
178
|
+
}`;
|
|
179
|
+
const output = await applyTransform(input);
|
|
180
|
+
expect(output).not.toContain('treeConfig: treeConfig');
|
|
181
|
+
expect(normalize(output)).toContain('visibleData: data');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('leaves a migration guidance comment about expansion state', async () => {
|
|
185
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
186
|
+
function C() {
|
|
187
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
188
|
+
baseData: tree,
|
|
189
|
+
getChildren: item => item.children ?? [],
|
|
190
|
+
getRowKey: item => item.id,
|
|
191
|
+
expandedKeys,
|
|
192
|
+
setExpandedKeys,
|
|
193
|
+
});
|
|
194
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
195
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
196
|
+
}`;
|
|
197
|
+
const output = await applyTransform(input);
|
|
198
|
+
expect(output).toContain('astryx-migration');
|
|
199
|
+
expect(output).toContain('defaultExpandedIds');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -40,6 +40,9 @@ import migrateLabCodeBlockImports, {
|
|
|
40
40
|
import removeThemeTransitionTokenImports, {
|
|
41
41
|
meta as removeThemeTransitionTokenImportsMeta,
|
|
42
42
|
} from './remove-theme-transition-token-imports.mjs';
|
|
43
|
+
import migrateTableRowExpansionToTree, {
|
|
44
|
+
meta as migrateTableRowExpansionToTreeMeta,
|
|
45
|
+
} from './migrate-table-rowexpansion-to-tree.mjs';
|
|
43
46
|
|
|
44
47
|
export default [
|
|
45
48
|
{
|
|
@@ -87,4 +90,9 @@ export default [
|
|
|
87
90
|
transform: removeThemeTransitionTokenImports,
|
|
88
91
|
meta: removeThemeTransitionTokenImportsMeta,
|
|
89
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'migrate-table-rowexpansion-to-tree',
|
|
95
|
+
transform: migrateTableRowExpansionToTree,
|
|
96
|
+
meta: migrateTableRowExpansionToTreeMeta,
|
|
97
|
+
},
|
|
90
98
|
];
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Migrate tree-mode useTableRowExpansion to the tree plugin
|
|
5
|
+
*
|
|
6
|
+
* useTableRowExpansion is now a detail-panel plugin (renderExpanded). Its old
|
|
7
|
+
* tree mode (child rows that reuse the parent columns) moved to
|
|
8
|
+
* useTableTreeData + useTableTreeState, and useTableRowExpansionState was
|
|
9
|
+
* removed. This codemod rewrites the standard tree pattern:
|
|
10
|
+
*
|
|
11
|
+
* const {data, expansionConfig} = useTableRowExpansionState({
|
|
12
|
+
* baseData, getChildren, getRowKey, expandedKeys, setExpandedKeys,
|
|
13
|
+
* });
|
|
14
|
+
* const expansion = useTableRowExpansion(expansionConfig);
|
|
15
|
+
*
|
|
16
|
+
* into:
|
|
17
|
+
*
|
|
18
|
+
* const {visibleData: data, treeConfig} = useTableTreeState({
|
|
19
|
+
* data: baseData, childrenKey: 'children', idKey: getRowKey, ...
|
|
20
|
+
* });
|
|
21
|
+
* const expansion = useTableTreeData(treeConfig);
|
|
22
|
+
*
|
|
23
|
+
* Files that use the new detail-panel API (renderExpanded, no
|
|
24
|
+
* useTableRowExpansionState import) are left untouched.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const meta = {
|
|
28
|
+
title: 'Migrate tree-mode useTableRowExpansion to the tree plugin',
|
|
29
|
+
description:
|
|
30
|
+
'Rewrites the removed useTableRowExpansionState tree pattern to useTableTreeState + useTableTreeData. Detail-panel usage (renderExpanded) is left untouched.',
|
|
31
|
+
pr: '#4612',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const IMPORT_SOURCES = new Set([
|
|
35
|
+
'@astryxdesign/core',
|
|
36
|
+
'@astryxdesign/core/Table',
|
|
37
|
+
'@xds/core',
|
|
38
|
+
'@xds/core/Table',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
|
|
43
|
+
* @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
|
|
44
|
+
* @returns {string | null | undefined}
|
|
45
|
+
*/
|
|
46
|
+
export default function transformer(file, api) {
|
|
47
|
+
const j = api.jscodeshift;
|
|
48
|
+
const root = j(file.source);
|
|
49
|
+
|
|
50
|
+
// Only act on files importing the removed state hook. Detail-panel-only
|
|
51
|
+
// usage (useTableRowExpansion alone) is the new API and must be left alone.
|
|
52
|
+
let stateLocal = null;
|
|
53
|
+
let pluginLocal = null;
|
|
54
|
+
/** @type {any[]} */
|
|
55
|
+
const importPaths = [];
|
|
56
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
|
|
57
|
+
if (!IMPORT_SOURCES.has(path.node.source.value)) return;
|
|
58
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
59
|
+
if (spec.type !== 'ImportSpecifier') continue;
|
|
60
|
+
if (spec.imported.name === 'useTableRowExpansionState') {
|
|
61
|
+
stateLocal = spec.local?.name ?? spec.imported.name;
|
|
62
|
+
importPaths.push(path);
|
|
63
|
+
}
|
|
64
|
+
if (spec.imported.name === 'useTableRowExpansion') {
|
|
65
|
+
pluginLocal = spec.local?.name ?? spec.imported.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (!stateLocal) return undefined;
|
|
70
|
+
|
|
71
|
+
let hasChanges = false;
|
|
72
|
+
|
|
73
|
+
// --- 1. Rewrite the state hook call + its config object ---
|
|
74
|
+
root
|
|
75
|
+
.find(j.CallExpression, {callee: {name: stateLocal}})
|
|
76
|
+
.forEach((/** @type {any} */ path) => {
|
|
77
|
+
const arg = path.node.arguments[0];
|
|
78
|
+
if (!arg || arg.type !== 'ObjectExpression') return;
|
|
79
|
+
|
|
80
|
+
/** @type {any} */
|
|
81
|
+
let getChildrenValue = null;
|
|
82
|
+
const nextProps = [];
|
|
83
|
+
for (const prop of arg.properties) {
|
|
84
|
+
const key =
|
|
85
|
+
prop.key?.name ?? (prop.key?.value ? String(prop.key.value) : null);
|
|
86
|
+
if (key === 'baseData') {
|
|
87
|
+
nextProps.push(j.property('init', j.identifier('data'), prop.value));
|
|
88
|
+
} else if (key === 'getChildren') {
|
|
89
|
+
getChildrenValue = prop.value;
|
|
90
|
+
// childrenKey is emitted below (default 'children').
|
|
91
|
+
} else if (key === 'getRowKey') {
|
|
92
|
+
nextProps.push(j.property('init', j.identifier('idKey'), prop.value));
|
|
93
|
+
} else if (key === 'getIsItemExpandable') {
|
|
94
|
+
nextProps.push(
|
|
95
|
+
j.property('init', j.identifier('isItemExpandable'), prop.value),
|
|
96
|
+
);
|
|
97
|
+
} else if (key === 'expandedKeys' || key === 'setExpandedKeys') {
|
|
98
|
+
// The tree state hook owns expansion internally (uncontrolled) or
|
|
99
|
+
// via expandedIds/onExpandedIdsChange (controlled). The 1:1
|
|
100
|
+
// controlled mapping is not mechanical, so these are dropped and a
|
|
101
|
+
// guidance comment is attached below.
|
|
102
|
+
continue;
|
|
103
|
+
} else {
|
|
104
|
+
nextProps.push(prop);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// childrenKey: derive the literal when getChildren is the canonical
|
|
109
|
+
// `item => item.children` / `item.children ?? []`; otherwise keep the
|
|
110
|
+
// default and let the guidance comment flag it.
|
|
111
|
+
nextProps.splice(
|
|
112
|
+
1,
|
|
113
|
+
0,
|
|
114
|
+
j.property('init', j.identifier('childrenKey'), j.literal('children')),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
arg.properties = nextProps;
|
|
118
|
+
path.node.callee = j.identifier('useTableTreeState');
|
|
119
|
+
hasChanges = true;
|
|
120
|
+
|
|
121
|
+
// Rename the destructured result: data -> visibleData (aliased back to
|
|
122
|
+
// the local name), expansionConfig -> treeConfig.
|
|
123
|
+
const declarator = path.parent.node;
|
|
124
|
+
if (
|
|
125
|
+
declarator.type === 'VariableDeclarator' &&
|
|
126
|
+
declarator.id.type === 'ObjectPattern'
|
|
127
|
+
) {
|
|
128
|
+
for (const p of declarator.id.properties) {
|
|
129
|
+
if (p.type !== 'ObjectProperty' && p.type !== 'Property') continue;
|
|
130
|
+
const kName = p.key?.name;
|
|
131
|
+
if (kName === 'data') {
|
|
132
|
+
// {data} -> {visibleData: data}
|
|
133
|
+
p.key = j.identifier('visibleData');
|
|
134
|
+
if (p.shorthand) {
|
|
135
|
+
p.shorthand = false;
|
|
136
|
+
p.value = j.identifier('data');
|
|
137
|
+
}
|
|
138
|
+
} else if (kName === 'expansionConfig') {
|
|
139
|
+
// {expansionConfig} -> {treeConfig} (keep it shorthand).
|
|
140
|
+
p.key = j.identifier('treeConfig');
|
|
141
|
+
if (p.value?.type === 'Identifier') {
|
|
142
|
+
p.value = j.identifier('treeConfig');
|
|
143
|
+
p.shorthand = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Attach a one-line migration note as a leading comment on the
|
|
150
|
+
// statement, so a human confirms the expansion-state wiring.
|
|
151
|
+
const stmt = path.parent.parent.node;
|
|
152
|
+
if (stmt && !stmt.comments) {
|
|
153
|
+
stmt.comments = [
|
|
154
|
+
j.commentLine(
|
|
155
|
+
' astryx-migration: verify tree expansion state. useTableTreeState',
|
|
156
|
+
true,
|
|
157
|
+
false,
|
|
158
|
+
),
|
|
159
|
+
j.commentLine(
|
|
160
|
+
' is uncontrolled by default; pass defaultExpandedIds, or',
|
|
161
|
+
true,
|
|
162
|
+
false,
|
|
163
|
+
),
|
|
164
|
+
j.commentLine(
|
|
165
|
+
' expandedIds + onExpandedIdsChange for the old controlled set.',
|
|
166
|
+
true,
|
|
167
|
+
false,
|
|
168
|
+
),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
void getChildrenValue;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// --- 2. Swap the plugin call: useTableRowExpansion(cfg) -> useTableTreeData(cfg) ---
|
|
175
|
+
if (pluginLocal) {
|
|
176
|
+
root
|
|
177
|
+
.find(j.CallExpression, {callee: {name: pluginLocal}})
|
|
178
|
+
.forEach((/** @type {any} */ path) => {
|
|
179
|
+
// Rename the argument identifier expansionConfig -> treeConfig.
|
|
180
|
+
const a = path.node.arguments[0];
|
|
181
|
+
if (a && a.type === 'Identifier' && a.name === 'expansionConfig') {
|
|
182
|
+
path.node.arguments[0] = j.identifier('treeConfig');
|
|
183
|
+
}
|
|
184
|
+
path.node.callee = j.identifier('useTableTreeData');
|
|
185
|
+
hasChanges = true;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!hasChanges) return undefined;
|
|
190
|
+
|
|
191
|
+
// --- 3. Fix imports: drop the removed hooks, add the tree hooks ---
|
|
192
|
+
for (const path of importPaths) {
|
|
193
|
+
const specs = path.node.specifiers.filter(
|
|
194
|
+
(/** @type {any} */ s) =>
|
|
195
|
+
!(
|
|
196
|
+
s.type === 'ImportSpecifier' &&
|
|
197
|
+
(s.imported.name === 'useTableRowExpansionState' ||
|
|
198
|
+
s.imported.name === 'useTableRowExpansion')
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
const existing = new Set(
|
|
202
|
+
specs.map((/** @type {any} */ s) => s.imported?.name),
|
|
203
|
+
);
|
|
204
|
+
if (!existing.has('useTableTreeState')) {
|
|
205
|
+
specs.push(j.importSpecifier(j.identifier('useTableTreeState')));
|
|
206
|
+
}
|
|
207
|
+
if (!existing.has('useTableTreeData')) {
|
|
208
|
+
specs.push(j.importSpecifier(j.identifier('useTableTreeData')));
|
|
209
|
+
}
|
|
210
|
+
path.node.specifiers = specs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return root.toSource({quote: 'single'});
|
|
214
|
+
}
|
|
@@ -15,7 +15,7 @@ export const docs = {
|
|
|
15
15
|
content: [
|
|
16
16
|
{
|
|
17
17
|
type: 'prose',
|
|
18
|
-
text: 'Astryx is built on modern web platform features: the Popover API, CSS anchor positioning, and CSS `light-dark()`. These let components stay small, accessible, and dependency-free, but they also set a floor on which browsers render everything correctly.',
|
|
18
|
+
text: 'Astryx is built on modern web platform features: the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API, CSS anchor positioning, and CSS `light-dark()`. These let components stay small, accessible, and dependency-free, but they also set a floor on which browsers render everything correctly.',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
type: 'prose',
|
|
@@ -73,7 +73,7 @@ export const docs = {
|
|
|
73
73
|
'Baseline 2026: the tightest requirement.',
|
|
74
74
|
],
|
|
75
75
|
[
|
|
76
|
-
'Popover API',
|
|
76
|
+
'[`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API',
|
|
77
77
|
'Opens, stacks, and light-dismisses layered surfaces via the top layer.',
|
|
78
78
|
'Baseline 2025.',
|
|
79
79
|
],
|
|
@@ -86,7 +86,7 @@ export const docs = {
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
type: 'prose',
|
|
89
|
-
text: 'The gap that matters is between Tier 1 and Tier 2: the Popover API and `light-dark()` reached wide availability well before anchor positioning. So in Tier 2 browsers, layered surfaces open and dismiss correctly; they just are not positioned. This is the one feature most consumers will need to reason about.',
|
|
89
|
+
text: 'The gap that matters is between Tier 1 and Tier 2: the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API and `light-dark()` reached wide availability well before anchor positioning. So in Tier 2 browsers, layered surfaces open and dismiss correctly; they just are not positioned. This is the one feature most consumers will need to reason about.',
|
|
90
90
|
},
|
|
91
91
|
],
|
|
92
92
|
},
|
|
@@ -112,7 +112,7 @@ export const docs = {
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
type: 'prose',
|
|
115
|
-
text: 'If your product does not use any of these, it has no anchor-positioning requirement at all; it needs only `light-dark()` (Tier 2 and up) for correct theme colors.
|
|
115
|
+
text: 'If your product does not use any of these, it has no anchor-positioning requirement at all; it needs only `light-dark()` (Tier 2 and up) for correct theme colors. Page layout, typography, forms, buttons, cards, tables, and navigation all work down to Tier 2 with no special handling.',
|
|
116
116
|
},
|
|
117
117
|
],
|
|
118
118
|
},
|
|
@@ -123,7 +123,7 @@ export const docs = {
|
|
|
123
123
|
type: 'list',
|
|
124
124
|
style: 'do',
|
|
125
125
|
items: [
|
|
126
|
-
'Components never throw on missing platform APIs. Where a browser lacks the Popover API, layers fall back to plain visibility instead of crashing.',
|
|
126
|
+
'Components never throw on missing platform APIs. Where a browser lacks the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API, layers fall back to plain visibility instead of crashing.',
|
|
127
127
|
'Tier 1 and Tier 2 are officially supported and tested.',
|
|
128
128
|
'Non-layered components render correctly down to Tier 2.',
|
|
129
129
|
],
|
|
@@ -192,7 +192,7 @@ const hasLightDark = CSS.supports('color', 'light-dark(#000, #fff)');`,
|
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
type: 'prose',
|
|
195
|
-
text: 'This is not an arbitrary window: Baseline − 2 is close to where anchor positioning stops being available while the Popover API and `light-dark()` still are, so the tier boundary tracks a real capability edge, not a guessed date. The version floors above are reviewed and advanced roughly once a year as new Baseline years land. Always feature-detect rather than hardcoding version numbers, so your app adapts automatically as the platform moves.',
|
|
195
|
+
text: 'This is not an arbitrary window: Baseline − 2 is close to where anchor positioning stops being available while the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API and `light-dark()` still are, so the tier boundary tracks a real capability edge, not a guessed date. The version floors above are reviewed and advanced roughly once a year as new Baseline years land. Always feature-detect rather than hardcoding version numbers, so your app adapts automatically as the platform moves.',
|
|
196
196
|
},
|
|
197
197
|
],
|
|
198
198
|
},
|
|
@@ -114,9 +114,9 @@ export const brandTheme = defineTheme({
|
|
|
114
114
|
type: 'list',
|
|
115
115
|
style: 'ordered',
|
|
116
116
|
items: [
|
|
117
|
-
'Add the name to IconName type in packages/core/src/Icon/globalIconRegistry.tsx',
|
|
118
|
-
'Add the default SVG to packages/core/src/Icon/defaultIcons.tsx',
|
|
119
|
-
'Add a row to the Available Names table in packages/cli/docs/icons.doc.mjs',
|
|
117
|
+
'Add the name to IconName type in `packages/core/src/Icon/globalIconRegistry.tsx`',
|
|
118
|
+
'Add the default SVG to `packages/core/src/Icon/defaultIcons.tsx`',
|
|
119
|
+
'Add a row to the Available Names table in `packages/cli/assets/docs/icons.doc.mjs`',
|
|
120
120
|
],
|
|
121
121
|
},
|
|
122
122
|
],
|
|
@@ -40,7 +40,7 @@ export const docs = {
|
|
|
40
40
|
'Keep illustrations consistent in style across the product.',
|
|
41
41
|
'Use simple, flat illustrations that work in both light and dark mode.',
|
|
42
42
|
'Size illustrations proportionally to the container, typically 120\u2013240px.',
|
|
43
|
-
'
|
|
43
|
+
'Place illustrations centered with supporting text below.',
|
|
44
44
|
],
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -59,7 +59,7 @@ export const docs = {
|
|
|
59
59
|
content: [
|
|
60
60
|
{
|
|
61
61
|
type: 'prose',
|
|
62
|
-
text: '
|
|
62
|
+
text: 'Place illustrations inside Center with supporting text stacked below. Typical illustration sizes range from 120px for inline empty states to 240px for full-page onboarding screens. Always pair the illustration with a heading and optional body text to explain what the user should do next.',
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
type: 'code',
|
|
@@ -300,7 +300,7 @@ export default function App() {
|
|
|
300
300
|
content: [
|
|
301
301
|
{
|
|
302
302
|
type: 'prose',
|
|
303
|
-
text: 'Astryx generates a `pseudo` locale that wraps every string in `⟦…⟧` and replaces letters with accented look-alikes.
|
|
303
|
+
text: 'Astryx generates a `pseudo` locale that wraps every string in `⟦…⟧` and replaces letters with accented look-alikes. Turn it on in development to catch hardcoded astryx strings and layout issues caused by longer text.',
|
|
304
304
|
},
|
|
305
305
|
{
|
|
306
306
|
type: 'code',
|
|
@@ -350,7 +350,7 @@ if (getComputedStyle(button).paddingInline === '0px') {
|
|
|
350
350
|
'Run the app in light and dark mode and check that surfaces, borders, text, icons, hover states, focus rings, and status colors flow together.',
|
|
351
351
|
'Open the command palette from the shell, type into it, select items by keyboard, and confirm focus returns to the trigger.',
|
|
352
352
|
'Check the SideNav at collapsed, expanded, active, hover, nested, and mobile states.',
|
|
353
|
-
'Verify settings popovers and dialogs in jsdom and in a real browser because native dialog and Popover APIs may need test shims.',
|
|
353
|
+
'Verify settings popovers and dialogs in jsdom and in a real browser because the native dialog and [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) APIs may need test shims.',
|
|
354
354
|
'Search for leftover hardcoded Tailwind colors, arbitrary hex values, and one-off hover colors after each route migration.',
|
|
355
355
|
'Run component tests, build, and at least one browser screenshot pass for each migrated route.',
|
|
356
356
|
],
|
|
@@ -39,7 +39,7 @@ export const docs = {
|
|
|
39
39
|
style: 'ordered',
|
|
40
40
|
items: [
|
|
41
41
|
'Use components for everything they cover',
|
|
42
|
-
'
|
|
42
|
+
'Page layout is frame-first: pick the shell and budget regions before writing content (see \`astryx docs layout\`)',
|
|
43
43
|
'Dense data renders as rows (Table, List/Item), edge-to-edge with dividers; Card is for widgets, galleries, and settings groups',
|
|
44
44
|
'StyleX or Tailwind for custom styling; both are first-class (see \`astryx docs styling\`)',
|
|
45
45
|
'Semantic tokens, not hardcoded values (see \`astryx docs tokens\`)',
|
|
@@ -56,7 +56,7 @@ export const docs = {
|
|
|
56
56
|
'`@astryxdesign/core/tailwind-theme.css`',
|
|
57
57
|
],
|
|
58
58
|
[
|
|
59
|
-
'
|
|
59
|
+
'Design token resolver APIs',
|
|
60
60
|
'JavaScript needs token values for charts, canvas, SVG, or config objects',
|
|
61
61
|
"`resolveThemeToken(theme, '--color-data-categorical-blue', {mode})`",
|
|
62
62
|
],
|
|
@@ -22,10 +22,10 @@ export const docs = {
|
|
|
22
22
|
type: 'table',
|
|
23
23
|
headers: ['Approach', 'Use for', 'Example'],
|
|
24
24
|
rows: [
|
|
25
|
-
['StyleX', 'Component-specific overrides, reusable styles, pseudo-classes, and typed tokens', 'const styles = stylex.create(...); <Button xstyle={styles.save}
|
|
26
|
-
['Tailwind utilities', '
|
|
27
|
-
['className', 'Integrating with external CSS or Tailwind on components', 'className="my-card shadow-lg"'],
|
|
28
|
-
['Styling-library token aliases', 'Keeping Panda, Chakra, MUI, Emotion, styled-components, UnoCSS, CSS Modules, or Sass in sync with the system', "colors.surface = 'var(--color-background-surface)'"],
|
|
25
|
+
['StyleX', 'Component-specific overrides, reusable styles, pseudo-classes, and typed tokens', '`const styles = stylex.create(...); <Button xstyle={styles.save} />`'],
|
|
26
|
+
['Tailwind utilities', 'Page layout, wrappers, and utility styling', '`className="flex gap-3 p-4"`'],
|
|
27
|
+
['className', 'Integrating with external CSS or Tailwind on components', '`className="my-card shadow-lg"`'],
|
|
28
|
+
['Styling-library token aliases', 'Keeping Panda, Chakra, MUI, Emotion, styled-components, UnoCSS, CSS Modules, or Sass in sync with the system', "`colors.surface = 'var(--color-background-surface)'`"],
|
|
29
29
|
],
|
|
30
30
|
},
|
|
31
31
|
{
|
|
@@ -106,7 +106,7 @@ function App() {
|
|
|
106
106
|
[
|
|
107
107
|
'Y2K',
|
|
108
108
|
"import {y2kTheme} from '@astryxdesign/theme-y2k'",
|
|
109
|
-
'Playful Y2K pop; periwinkle body, holographic accents, Poppins + Crimson Text
|
|
109
|
+
'Playful Y2K pop; periwinkle body, holographic accents, Poppins + `Crimson Text`.',
|
|
110
110
|
],
|
|
111
111
|
],
|
|
112
112
|
},
|
|
@@ -510,7 +510,7 @@ import './themes/ocean.css';
|
|
|
510
510
|
content: [
|
|
511
511
|
{
|
|
512
512
|
type: 'prose',
|
|
513
|
-
text: 'Wrap different sections in separate
|
|
513
|
+
text: 'Wrap different sections in separate [`<Theme>`](/components/Theme) providers.',
|
|
514
514
|
},
|
|
515
515
|
{
|
|
516
516
|
type: 'code',
|
|
@@ -606,7 +606,7 @@ function ChartConfig() {
|
|
|
606
606
|
},
|
|
607
607
|
{
|
|
608
608
|
type: 'prose',
|
|
609
|
-
text: 'Prefer CSS variables, StyleX token imports, xstyle, or className for ordinary styling. To change the theme or mode, manage state at the app level and pass it to
|
|
609
|
+
text: 'Prefer CSS variables, StyleX token imports, xstyle, or className for ordinary styling. To change the theme or mode, manage state at the app level and pass it to `<Theme>`.',
|
|
610
610
|
},
|
|
611
611
|
{
|
|
612
612
|
type: 'prose',
|
|
@@ -122,7 +122,7 @@ export const docs = {
|
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
type: 'prose',
|
|
125
|
-
text: 'Display text often needs heading semantics for accessibility. Use the type prop on Heading to apply display styling while preserving the correct HTML element:
|
|
125
|
+
text: 'Display text often needs heading semantics for accessibility. Use the type prop on Heading to apply display styling while preserving the correct HTML element: `<Heading level={1} type="display-1">` gives you display-1 styling with an `<h1>` tag, so screen readers see the correct document outline.',
|
|
126
126
|
},
|
|
127
127
|
],
|
|
128
128
|
},
|
|
@@ -205,7 +205,7 @@ const denseTheme = defineTheme({
|
|
|
205
205
|
items: [
|
|
206
206
|
'Use Heading for document headings and Text for everything else; they apply the full type scale automatically.',
|
|
207
207
|
'Adjust typography holistically: change base and ratio in defineTheme to shift the entire ramp (e.g. { base: 16, ratio: 1.25 } for editorial, { base: 12, ratio: 1.125 } for dense UI).',
|
|
208
|
-
'Use display types with as="h1" (or h2/h3) when display text is a page heading; this preserves accessibility while giving you display-level sizing. Or better, use
|
|
208
|
+
'Use display types with as="h1" (or h2/h3) when display text is a page heading; this preserves accessibility while giving you display-level sizing. Or better, use `<Heading level={1} type="display-1">` which handles both semantics and styling.',
|
|
209
209
|
'Let line-height snap to the 4px grid via the type scale; expandTypeScale computes leading automatically from base and ratio.',
|
|
210
210
|
'Use the supporting type for secondary information: timestamps, helper text, metadata, captions.',
|
|
211
211
|
'Use accessibilityLevel on Heading when the visual hierarchy doesn\u0027t match the document outline (e.g. sidebar or card headings).',
|
|
@@ -6,90 +6,93 @@ import {useState} from 'react';
|
|
|
6
6
|
import {
|
|
7
7
|
Table,
|
|
8
8
|
useTableRowExpansion,
|
|
9
|
-
useTableRowExpansionState,
|
|
10
9
|
proportional,
|
|
11
10
|
pixel,
|
|
12
11
|
} from '@astryxdesign/core/Table';
|
|
12
|
+
import {VStack, HStack} from '@astryxdesign/core/Stack';
|
|
13
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
14
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
13
15
|
|
|
14
|
-
interface
|
|
16
|
+
interface Order extends Record<string, unknown> {
|
|
15
17
|
id: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
customer: string;
|
|
19
|
+
status: string;
|
|
20
|
+
total: string;
|
|
21
|
+
items: {name: string; qty: number; price: string}[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const
|
|
24
|
+
const orders: Order[] = [
|
|
23
25
|
{
|
|
24
|
-
id: '
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
name: 'components',
|
|
32
|
-
type: 'folder',
|
|
33
|
-
size: '—',
|
|
34
|
-
children: [
|
|
35
|
-
{
|
|
36
|
-
id: 'src/components/Button.tsx',
|
|
37
|
-
name: 'Button.tsx',
|
|
38
|
-
type: 'file',
|
|
39
|
-
size: '4.2 KB',
|
|
40
|
-
children: [],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: 'src/components/Table.tsx',
|
|
44
|
-
name: 'Table.tsx',
|
|
45
|
-
type: 'file',
|
|
46
|
-
size: '12.8 KB',
|
|
47
|
-
children: [],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: 'src/index.ts',
|
|
53
|
-
name: 'index.ts',
|
|
54
|
-
type: 'file',
|
|
55
|
-
size: '0.4 KB',
|
|
56
|
-
children: [],
|
|
57
|
-
},
|
|
26
|
+
id: 'ord-1001',
|
|
27
|
+
customer: 'Ada Lovelace',
|
|
28
|
+
status: 'Shipped',
|
|
29
|
+
total: '$248.00',
|
|
30
|
+
items: [
|
|
31
|
+
{name: 'Mechanical keyboard', qty: 1, price: '$180.00'},
|
|
32
|
+
{name: 'Wrist rest', qty: 2, price: '$34.00'},
|
|
58
33
|
],
|
|
59
34
|
},
|
|
60
35
|
{
|
|
61
|
-
id: '
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
36
|
+
id: 'ord-1002',
|
|
37
|
+
customer: 'Alan Turing',
|
|
38
|
+
status: 'Processing',
|
|
39
|
+
total: '$52.00',
|
|
40
|
+
items: [{name: 'USB-C cable', qty: 4, price: '$13.00'}],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'ord-1003',
|
|
44
|
+
customer: 'Grace Hopper',
|
|
45
|
+
status: 'Delivered',
|
|
46
|
+
total: '$1,200.00',
|
|
47
|
+
items: [{name: 'Standing desk', qty: 1, price: '$1,200.00'}],
|
|
66
48
|
},
|
|
67
49
|
];
|
|
68
50
|
|
|
69
51
|
const columns = [
|
|
70
|
-
{key: '
|
|
71
|
-
{key: '
|
|
72
|
-
{key: '
|
|
52
|
+
{key: 'customer', header: 'Customer', width: proportional(2)},
|
|
53
|
+
{key: 'status', header: 'Status', width: pixel(130)},
|
|
54
|
+
{key: 'total', header: 'Total', width: pixel(110)},
|
|
73
55
|
];
|
|
74
56
|
|
|
75
57
|
export default function TableRowExpansionTable() {
|
|
76
58
|
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
|
|
77
|
-
new Set(['
|
|
59
|
+
new Set(['ord-1001']),
|
|
78
60
|
);
|
|
79
61
|
|
|
80
|
-
const
|
|
81
|
-
baseData: fileTree,
|
|
82
|
-
getChildren: item => item.children ?? [],
|
|
83
|
-
getRowKey: item => item.id,
|
|
62
|
+
const expansion = useTableRowExpansion<Order>({
|
|
84
63
|
expandedKeys,
|
|
85
|
-
|
|
64
|
+
onToggle: key =>
|
|
65
|
+
setExpandedKeys(prev => {
|
|
66
|
+
const next = new Set(prev);
|
|
67
|
+
if (next.has(key)) {
|
|
68
|
+
next.delete(key);
|
|
69
|
+
} else {
|
|
70
|
+
next.add(key);
|
|
71
|
+
}
|
|
72
|
+
return next;
|
|
73
|
+
}),
|
|
74
|
+
getRowKey: item => item.id,
|
|
75
|
+
// The detail panel renders arbitrary content below the row: here, the
|
|
76
|
+
// order's line items. Any component composes here (charts, forms, tables).
|
|
77
|
+
renderExpanded: item => (
|
|
78
|
+
<VStack gap={2}>
|
|
79
|
+
<Heading level={4}>Line items</Heading>
|
|
80
|
+
{item.items.map(line => (
|
|
81
|
+
<HStack key={line.name} gap={3}>
|
|
82
|
+
<Badge label={`x${line.qty}`} variant="info" />
|
|
83
|
+
<Text type="body">{line.name}</Text>
|
|
84
|
+
<Text type="body" color="secondary">
|
|
85
|
+
{line.price}
|
|
86
|
+
</Text>
|
|
87
|
+
</HStack>
|
|
88
|
+
))}
|
|
89
|
+
</VStack>
|
|
90
|
+
),
|
|
86
91
|
});
|
|
87
92
|
|
|
88
|
-
const expansion = useTableRowExpansion(expansionConfig);
|
|
89
|
-
|
|
90
93
|
return (
|
|
91
94
|
<Table
|
|
92
|
-
data={
|
|
95
|
+
data={orders}
|
|
93
96
|
columns={columns}
|
|
94
97
|
idKey="id"
|
|
95
98
|
hasHover
|
|
@@ -323,8 +323,11 @@ export const neutralTheme = defineTheme({
|
|
|
323
323
|
|
|
324
324
|
// =========================================================================
|
|
325
325
|
// Radius — slightly larger than default (kept as-is)
|
|
326
|
+
// --radius-none and --radius-full are always fixed and must never be
|
|
327
|
+
// scaled by a theme (see defineTheme's radius config docs) — 0 and
|
|
328
|
+
// 9999px respectively, matching @astryxdesign/core's own defaults.
|
|
326
329
|
// =========================================================================
|
|
327
|
-
'--radius-none': '
|
|
330
|
+
'--radius-none': '0px',
|
|
328
331
|
'--radius-inner': '0.375rem',
|
|
329
332
|
'--radius-element': '0.625rem',
|
|
330
333
|
'--radius-container': '0.75rem',
|
|
@@ -310,6 +310,15 @@ export interface ComponentThemingTarget {
|
|
|
310
310
|
* `[data-checked="checked"]`. Legacy state classes are still emitted for
|
|
311
311
|
* compatibility. Omit if the element has no state-driven selectors. */
|
|
312
312
|
states?: string[];
|
|
313
|
+
/** Set when this target has been RENAMED and this entry is the old name.
|
|
314
|
+
* The component still emits the class (via `themeProps`'s `legacyNames`),
|
|
315
|
+
* so existing themes keep working, but the docsite should steer readers to
|
|
316
|
+
* the replacement. The value is the class name that supersedes this one,
|
|
317
|
+
* without the `astryx-` prefix — e.g. `"checkbox-indicator"`.
|
|
318
|
+
*
|
|
319
|
+
* A theme target is public API; renaming one without this is a silent
|
|
320
|
+
* break for every theme styling it. */
|
|
321
|
+
deprecatedFor?: string;
|
|
313
322
|
}
|
|
314
323
|
|
|
315
324
|
/**
|
|
@@ -60,7 +60,10 @@ function renderedClassLiterals() {
|
|
|
60
60
|
const full = path.join(dir, entry.name);
|
|
61
61
|
if (entry.isDirectory()) {
|
|
62
62
|
walk(full);
|
|
63
|
-
} else if (
|
|
63
|
+
} else if (
|
|
64
|
+
(entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) &&
|
|
65
|
+
!entry.name.includes('.test.')
|
|
66
|
+
) {
|
|
64
67
|
const text = fs.readFileSync(full, 'utf8');
|
|
65
68
|
for (const re of [
|
|
66
69
|
/themeProps\(\s*'([^']+)'/g,
|
|
@@ -71,6 +74,16 @@ function renderedClassLiterals() {
|
|
|
71
74
|
classes.add(m[1]);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
77
|
+
// Renamed targets emit their old name too, via themeProps'
|
|
78
|
+
// `legacyNames`. Those classes are just as rendered as the primary
|
|
79
|
+
// one, so a doc entry for the old name is still backed by real output.
|
|
80
|
+
const legacyRe = /legacyNames:\s*\[([^\]]*)\]/g;
|
|
81
|
+
let lm;
|
|
82
|
+
while ((lm = legacyRe.exec(text)) !== null) {
|
|
83
|
+
for (const nm of lm[1].matchAll(/'([^']+)'/g)) {
|
|
84
|
+
classes.add(nm[1]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
};
|
|
@@ -11,11 +11,14 @@ export function pkgOf(t: {
|
|
|
11
11
|
package?: string;
|
|
12
12
|
}): string;
|
|
13
13
|
/**
|
|
14
|
-
* Replace demo
|
|
15
|
-
*
|
|
14
|
+
* Replace demo asset references with a placeholder so scaffolded pages
|
|
15
|
+
* render with zero setup. Images get a self-contained data URI; videos
|
|
16
|
+
* (which have no equivalent inline placeholder — see VIDEO_EXTENSIONS) are
|
|
17
|
+
* stripped to an empty src instead of being mis-replaced with image data.
|
|
18
|
+
* Builders drop in their own media either way.
|
|
16
19
|
*
|
|
17
20
|
* @param {string} source - Template source code.
|
|
18
|
-
* @returns {string} Source with demo
|
|
21
|
+
* @returns {string} Source with demo asset references replaced.
|
|
19
22
|
*/
|
|
20
23
|
export function stripTemplateAssetRefs(source: string): string;
|
|
21
24
|
/**
|
|
@@ -153,19 +153,33 @@ const PLACEHOLDER_IMAGE =
|
|
|
153
153
|
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20400%20300%22%20preserveAspectRatio%3D%22xMidYMid%20slice%22%3E%3Crect%20width%3D%22400%22%20height%3D%22300%22%20fill%3D%22%23f5f6f8%22%2F%3E%3Cg%20transform%3D%22translate%28200%20150%29%22%20fill%3D%22none%22%20stroke%3D%22%23c2cad6%22%20stroke-width%3D%225%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%22-44%22%20y%3D%22-44%22%20width%3D%2288%22%20height%3D%2288%22%20rx%3D%2216%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%22-18%22%20r%3D%222.5%22%20fill%3D%22%23c2cad6%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M-34%2030%20L-8%200%20L10%2018%20L20%208%20L34%2024%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E';
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
156
|
+
* Extensions that need a video-safe placeholder rather than the image data
|
|
157
|
+
* URI (see stripTemplateAssetRefs). There's no equivalent self-contained
|
|
158
|
+
* inline placeholder for video: unlike an SVG data URI, a `<video src>`
|
|
159
|
+
* needs actual encoded media, and hand-authoring a valid tiny MP4/WebM
|
|
160
|
+
* blob isn't something we can do reliably here — an unverifiable, possibly
|
|
161
|
+
* still-broken binary would just trade one silent failure for another.
|
|
162
|
+
* Rather than mis-render image data as video (the original bug) or guess at
|
|
163
|
+
* binary bytes, video sources are stripped to an empty string so the
|
|
164
|
+
* scaffolded example is honest about needing the builder to supply their
|
|
165
|
+
* own file, instead of silently pointing at something that can't play.
|
|
166
|
+
*
|
|
167
|
+
* @type {Set<string>}
|
|
168
|
+
*/
|
|
169
|
+
const VIDEO_EXTENSIONS = new Set(['mp4', 'webm', 'mov', 'ogv']);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Demo-asset sources to strip from scaffolded projects. Template demo imagery
|
|
157
173
|
* is self-hosted under the docsite's `/template-assets/*` dir (committed there,
|
|
158
174
|
* mirrored into the sandbox preview by scripts/sync-templates.js). Those paths
|
|
159
175
|
* only resolve inside the Astryx docsite/sandbox, so on scaffold they're
|
|
160
|
-
* replaced
|
|
161
|
-
*
|
|
162
|
-
*
|
|
176
|
+
* replaced — a scaffolded project has no `/template-assets/` dir and would
|
|
177
|
+
* otherwise 404. Genuine third-party URLs (e.g. brand logos from
|
|
178
|
+
* paypalobjects.com) are intentionally left untouched.
|
|
163
179
|
*
|
|
164
|
-
* @type {RegExp
|
|
180
|
+
* @type {RegExp}
|
|
165
181
|
*/
|
|
166
|
-
const
|
|
167
|
-
/\/template-assets\/[\w-]+\.\w+/g,
|
|
168
|
-
];
|
|
182
|
+
const DEMO_ASSET_PATTERN = /\/template-assets\/[\w-]+\.(\w+)/g;
|
|
169
183
|
|
|
170
184
|
/**
|
|
171
185
|
* Normalize path into Unix path (using forward slashes) for consistent comparison
|
|
@@ -179,16 +193,18 @@ function toPosixPath(p) {
|
|
|
179
193
|
}
|
|
180
194
|
|
|
181
195
|
/**
|
|
182
|
-
* Replace demo
|
|
183
|
-
*
|
|
196
|
+
* Replace demo asset references with a placeholder so scaffolded pages
|
|
197
|
+
* render with zero setup. Images get a self-contained data URI; videos
|
|
198
|
+
* (which have no equivalent inline placeholder — see VIDEO_EXTENSIONS) are
|
|
199
|
+
* stripped to an empty src instead of being mis-replaced with image data.
|
|
200
|
+
* Builders drop in their own media either way.
|
|
184
201
|
*
|
|
185
202
|
* @param {string} source - Template source code.
|
|
186
|
-
* @returns {string} Source with demo
|
|
203
|
+
* @returns {string} Source with demo asset references replaced.
|
|
187
204
|
*/
|
|
188
205
|
export function stripTemplateAssetRefs(source) {
|
|
189
|
-
return
|
|
190
|
-
(
|
|
191
|
-
source,
|
|
206
|
+
return source.replace(DEMO_ASSET_PATTERN, (match, extension) =>
|
|
207
|
+
VIDEO_EXTENSIONS.has(extension.toLowerCase()) ? '' : PLACEHOLDER_IMAGE,
|
|
192
208
|
);
|
|
193
209
|
}
|
|
194
210
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.3.0-canary.
|
|
3
|
+
"version": "0.3.0-canary.ec85ba0",
|
|
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",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"zod": "^4.4.3"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@astryxdesign/charts": "0.3.0-canary.
|
|
88
|
-
"@astryxdesign/core": "0.3.0-canary.
|
|
89
|
-
"@astryxdesign/lab": "0.3.0-canary.
|
|
90
|
-
"@astryxdesign/theme-neutral": "0.3.0-canary.
|
|
87
|
+
"@astryxdesign/charts": "0.3.0-canary.ec85ba0",
|
|
88
|
+
"@astryxdesign/core": "0.3.0-canary.ec85ba0",
|
|
89
|
+
"@astryxdesign/lab": "0.3.0-canary.ec85ba0",
|
|
90
|
+
"@astryxdesign/theme-neutral": "0.3.0-canary.ec85ba0",
|
|
91
91
|
"gpt-tokenizer": "^3.4.0"
|
|
92
92
|
},
|
|
93
93
|
"peerDependenciesMeta": {
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@astryxdesign/charts": "0.3.0-canary.
|
|
109
|
-
"@astryxdesign/core": "0.3.0-canary.
|
|
110
|
-
"@astryxdesign/lab": "0.3.0-canary.
|
|
111
|
-
"@astryxdesign/theme-neutral": "0.3.0-canary.
|
|
108
|
+
"@astryxdesign/charts": "0.3.0-canary.ec85ba0",
|
|
109
|
+
"@astryxdesign/core": "0.3.0-canary.ec85ba0",
|
|
110
|
+
"@astryxdesign/lab": "0.3.0-canary.ec85ba0",
|
|
111
|
+
"@astryxdesign/theme-neutral": "0.3.0-canary.ec85ba0",
|
|
112
112
|
"gpt-tokenizer": "^3.4.0"
|
|
113
113
|
},
|
|
114
114
|
"scripts": {
|