@astryxdesign/cli 0.7.12-canary.c73b9e4 → 0.7.12-canary.c86484d
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/component/__fixtures__/resolutions/modules.txt +1 -1
- package/api/template/template.mjs +1 -0
- package/assets/discover/official-packages.json +12 -0
- package/assets/docs/tree/type-scale.doc.mjs +19 -2
- package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
- package/assets/templates/pages/table-filter/page.tsx +15 -23
- package/assets/templates/pages/table-inbox/page.tsx +3 -6
- package/assets/templates/themes/butter/butterTheme.ts +1 -1
- package/foundation/discovery/hook-discovery.mjs +31 -8
- package/foundation/text/levenshtein.d.mts +6 -1
- package/foundation/text/levenshtein.mjs +25 -9
- package/foundation/text/suggest-order.d.mts +8 -1
- package/foundation/text/suggest-order.mjs +72 -6
- package/package.json +6 -6
|
@@ -298,7 +298,7 @@ useSpeechRecognition useSpeechRecognition Chat/Chat.doc.mjs Chat/useSpeechRecogn
|
|
|
298
298
|
useSwipeAction useSwipeAction Item/Item.doc.mjs Item/useSwipeAction.ts
|
|
299
299
|
useSwitchTouch useSwitchTouch Switch/Switch.doc.mjs Switch/useSwitchTouch.ts
|
|
300
300
|
useTableCellStyles useTableCellStyles Table/Table.doc.mjs Table/useTableCellStyles.ts
|
|
301
|
-
useTableColumnSettingsState useTableColumnSettingsState Table/
|
|
301
|
+
useTableColumnSettingsState useTableColumnSettingsState Table/plugins/columnSettings/useTableColumnSettingsState.doc.mjs Table/plugins/columnSettings/useTableColumnSettingsState.tsx
|
|
302
302
|
useTableSortableState useTableSortableState Table/plugins/sortable/useTableSortableState.doc.mjs Table/plugins/sortable/useTableSortableState.tsx
|
|
303
303
|
useToastGesture useToastGesture Toast/Toast.doc.mjs Toast/useToastGesture.ts
|
|
304
304
|
useTouchTrigger useTouchTrigger Layer/Layer.doc.mjs Layer/useTouchTrigger.ts
|
|
@@ -193,6 +193,7 @@ export async function template(name, options = {}) {
|
|
|
193
193
|
`Unknown template "${name}"`,
|
|
194
194
|
orderByAffinity(name, templates, t => [t.dirName, t.name], {
|
|
195
195
|
closest: true,
|
|
196
|
+
typos: true,
|
|
196
197
|
}).map(t => ({
|
|
197
198
|
name: t.dirName,
|
|
198
199
|
reason: `${t.type} template`,
|
|
@@ -128,6 +128,10 @@
|
|
|
128
128
|
"kind": "component",
|
|
129
129
|
"name": "CommentThread"
|
|
130
130
|
},
|
|
131
|
+
{
|
|
132
|
+
"kind": "component",
|
|
133
|
+
"name": "DialogHeroHeader"
|
|
134
|
+
},
|
|
131
135
|
{
|
|
132
136
|
"kind": "component",
|
|
133
137
|
"name": "DiffViewer"
|
|
@@ -136,6 +140,10 @@
|
|
|
136
140
|
"kind": "component",
|
|
137
141
|
"name": "EmojiPicker"
|
|
138
142
|
},
|
|
143
|
+
{
|
|
144
|
+
"kind": "component",
|
|
145
|
+
"name": "LineClamp"
|
|
146
|
+
},
|
|
139
147
|
{
|
|
140
148
|
"kind": "component",
|
|
141
149
|
"name": "ListInput"
|
|
@@ -252,6 +260,10 @@
|
|
|
252
260
|
"kind": "template",
|
|
253
261
|
"name": "CommentThreadShowcase"
|
|
254
262
|
},
|
|
263
|
+
{
|
|
264
|
+
"kind": "template",
|
|
265
|
+
"name": "DialogHeroHeaderShowcase"
|
|
266
|
+
},
|
|
255
267
|
{
|
|
256
268
|
"kind": "template",
|
|
257
269
|
"name": "DiffViewerShowcase"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file `astryx docs typography/type-scale`: line height, semantic type scale,
|
|
5
|
-
* display text, headings and text components,
|
|
5
|
+
* display text, headings and text components, custom type scales, and the
|
|
6
|
+
* reader's text size on iOS.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
|
|
@@ -14,13 +15,15 @@ export const docs = {
|
|
|
14
15
|
placement: {parent: 'namespace:typography', slot: 'guides', order: 20},
|
|
15
16
|
category: 'foundations',
|
|
16
17
|
description:
|
|
17
|
-
|
|
18
|
+
"Line height, semantic type scale tokens, display text, Heading and Text components, custom type scales via defineTheme, and following the reader's text size on iOS.",
|
|
18
19
|
keywords: [
|
|
19
20
|
'line height',
|
|
20
21
|
'leading',
|
|
21
22
|
'defineTheme',
|
|
22
23
|
'base',
|
|
23
24
|
'ratio',
|
|
25
|
+
'dynamic type',
|
|
26
|
+
'text size',
|
|
24
27
|
],
|
|
25
28
|
|
|
26
29
|
sections: [
|
|
@@ -143,5 +146,19 @@ const denseTheme = defineTheme({
|
|
|
143
146
|
},
|
|
144
147
|
],
|
|
145
148
|
},
|
|
149
|
+
{
|
|
150
|
+
id: 'dynamic-type',
|
|
151
|
+
title: "Following the reader's text size on iOS",
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'prose',
|
|
155
|
+
text: "On iPhone, readers pick a text size in Settings (Display & Text Size, then Larger Text), and native apps grow or shrink their text with it. Every theme does the same in Safari on iPhone, which exposes that choice through `font: -apple-system-body`: each font size follows the reader's choice on the curve of the iOS text style it plays, so body text scales like Body, `heading-1` like Title 1 and display text like Large Title, and every role keeps its order. A size the theme fixes at a length, such as a 12px supporting size, scales on the curve of the step it reads by default; a size in `em` or `%` follows its parent, and one that reads a variable follows that variable. At the default size every font size is the theme's own. The family, spacing, radius and the root font size never change, line heights keep their ratios, text inputs keep their 16px floor on iOS, and browsers without the keyword see no change. There is nothing to turn on: no theme option, prop or document attribute.",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'prose',
|
|
159
|
+
text: 'Fixed-height controls such as the text inputs keep their height, so their text can overflow at the accessibility sizes. A labelled `Button` grows with its label.',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
146
163
|
],
|
|
147
164
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'useTableRowsVirtualization',
|
|
7
|
+
name: 'useTableRowsVirtualization - Fixed Height Rows',
|
|
8
|
+
displayName: 'useTableRowsVirtualization - Fixed Height Rows',
|
|
9
|
+
description:
|
|
10
|
+
'A Table with 1,000 product rows in a fixed-height viewport. Scroll vertically to mount the visible rows. The caller keeps all data and columns; this hook does not virtualize columns.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: ['Table'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
useTableRowsVirtualization,
|
|
8
|
+
type TableColumn,
|
|
9
|
+
} from '@astryxdesign/core/Table';
|
|
10
|
+
|
|
11
|
+
interface Product extends Record<string, unknown> {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const products: Product[] = Array.from({length: 1000}, (_, index) => ({
|
|
17
|
+
id: `product-${index}`,
|
|
18
|
+
name: `Product ${index + 1}`,
|
|
19
|
+
}));
|
|
20
|
+
const columns: TableColumn<Product>[] = [{key: 'name', header: 'Product'}];
|
|
21
|
+
|
|
22
|
+
export default function RowsVirtualizationHookUsage() {
|
|
23
|
+
const virtualization = useTableRowsVirtualization({
|
|
24
|
+
rowHeight: 56,
|
|
25
|
+
viewportHeight: 320,
|
|
26
|
+
});
|
|
27
|
+
return (
|
|
28
|
+
<Table
|
|
29
|
+
data={products}
|
|
30
|
+
columns={columns}
|
|
31
|
+
idKey="id"
|
|
32
|
+
virtualization={virtualization}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'useTableColumnSettingsState',
|
|
7
|
+
name: 'useTableColumnSettingsState — Column Visibility Picker',
|
|
8
|
+
displayName: 'useTableColumnSettingsState — Column Visibility Picker',
|
|
9
|
+
description:
|
|
10
|
+
'A Table toolbar with a Columns menu of checkboxes: toggling Email updates active columns, Reset restores defaults, and the config is passed directly to useTableColumnSettings.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: ['Button', 'CheckboxInput', 'Popover', 'Table', 'Layout'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
7
|
+
import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
|
|
8
|
+
import {HStack, VStack} from '@astryxdesign/core/Layout';
|
|
9
|
+
import {Popover} from '@astryxdesign/core/Popover';
|
|
10
|
+
import {
|
|
11
|
+
Table,
|
|
12
|
+
useTableColumnSettings,
|
|
13
|
+
useTableColumnSettingsState,
|
|
14
|
+
} from '@astryxdesign/core/Table';
|
|
15
|
+
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
16
|
+
|
|
17
|
+
interface Member extends Record<string, unknown> {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
role: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const members: Member[] = [
|
|
25
|
+
{id: '1', name: 'Alice', email: 'alice@example.com', role: 'Engineer'},
|
|
26
|
+
{id: '2', name: 'Bob', email: 'bob@example.com', role: 'Designer'},
|
|
27
|
+
{id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'Manager'},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const allColumns: TableColumn<Member>[] = [
|
|
31
|
+
{key: 'name', header: 'Name'},
|
|
32
|
+
{key: 'email', header: 'Email'},
|
|
33
|
+
{key: 'role', header: 'Role'},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const columnOptions = [
|
|
37
|
+
{key: 'name' as const, label: 'Name', isAlwaysVisible: true},
|
|
38
|
+
{key: 'email' as const, label: 'Email'},
|
|
39
|
+
{key: 'role' as const, label: 'Role'},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const defaultKeys = ['name', 'email', 'role'];
|
|
43
|
+
|
|
44
|
+
export default function UseTableColumnSettingsStateHookUsage() {
|
|
45
|
+
const [activeKeys, setActiveKeys] =
|
|
46
|
+
useState<ReadonlyArray<string>>(defaultKeys);
|
|
47
|
+
|
|
48
|
+
const state = useTableColumnSettingsState({
|
|
49
|
+
columns: columnOptions,
|
|
50
|
+
activeColumnKeys: activeKeys,
|
|
51
|
+
onChangeActiveColumnKeys: setActiveKeys,
|
|
52
|
+
defaultColumnKeys: defaultKeys,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const plugin = useTableColumnSettings<Member>(state.columnSettingsConfig);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<VStack gap={3}>
|
|
59
|
+
<HStack hAlign="start">
|
|
60
|
+
<Popover
|
|
61
|
+
placement="below"
|
|
62
|
+
label="Column settings"
|
|
63
|
+
width={200}
|
|
64
|
+
content={
|
|
65
|
+
<VStack gap={2}>
|
|
66
|
+
{columnOptions.map(col => (
|
|
67
|
+
<CheckboxInput
|
|
68
|
+
key={col.key}
|
|
69
|
+
label={col.label}
|
|
70
|
+
value={state.isColumnActive(col.key)}
|
|
71
|
+
isDisabled={!state.isColumnToggleable(col.key)}
|
|
72
|
+
onChange={() => state.toggleColumn(col.key)}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
<Button
|
|
76
|
+
label="Reset"
|
|
77
|
+
size="sm"
|
|
78
|
+
variant="secondary"
|
|
79
|
+
onClick={state.resetToDefault}>
|
|
80
|
+
Reset
|
|
81
|
+
</Button>
|
|
82
|
+
</VStack>
|
|
83
|
+
}>
|
|
84
|
+
<Button label="Columns" size="sm">
|
|
85
|
+
Columns
|
|
86
|
+
</Button>
|
|
87
|
+
</Popover>
|
|
88
|
+
</HStack>
|
|
89
|
+
<Table
|
|
90
|
+
data={members}
|
|
91
|
+
columns={allColumns}
|
|
92
|
+
idKey="id"
|
|
93
|
+
plugins={{columnSettings: plugin}}
|
|
94
|
+
/>
|
|
95
|
+
</VStack>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -1129,18 +1129,6 @@ const INITIAL_SAVED_VIEWS: SavedView[] = [
|
|
|
1129
1129
|
const COMPACT_SURFACE_QUERY =
|
|
1130
1130
|
'(max-width: 768px), (max-width: 1024px) and (pointer: coarse) and (hover: none)';
|
|
1131
1131
|
|
|
1132
|
-
/**
|
|
1133
|
-
* Top padding for whatever opens a BottomSheet, on the spacing scale.
|
|
1134
|
-
*
|
|
1135
|
-
* The panel's grab pill floats over the content instead of reserving layout
|
|
1136
|
-
* space for itself: it is 4px centered in a 24px band, so it covers the top
|
|
1137
|
-
* 10-14px of the sheet. Content that starts at the usual 16px gutter clears it
|
|
1138
|
-
* by 2px, which does not read as a gap — it reads as the sheet being cut off.
|
|
1139
|
-
* Opening at the full 24px band puts real space under the pill, and every
|
|
1140
|
-
* sheet here uses this one value so they all start on the same line.
|
|
1141
|
-
*/
|
|
1142
|
-
const SHEET_TOP_INSET = 6;
|
|
1143
|
-
|
|
1144
1132
|
/**
|
|
1145
1133
|
* Width the filter row holds back, in px, for the readout that trails the
|
|
1146
1134
|
* clauses — the density toggle, the result count and Clear all.
|
|
@@ -1698,6 +1686,7 @@ const styles = stylex.create({
|
|
|
1698
1686
|
fontSize: typeScaleVars['--text-body-size'],
|
|
1699
1687
|
fontWeight: typeScaleVars['--text-body-weight'],
|
|
1700
1688
|
height: 'auto',
|
|
1689
|
+
minHeight: 0,
|
|
1701
1690
|
lineHeight: typeScaleVars['--text-body-leading'],
|
|
1702
1691
|
paddingBlock: 0,
|
|
1703
1692
|
paddingInline: 0,
|
|
@@ -2052,7 +2041,8 @@ function FilterOverflow({
|
|
|
2052
2041
|
<VStack
|
|
2053
2042
|
gap={2}
|
|
2054
2043
|
padding={isCompactSurface ? 4 : 0}
|
|
2055
|
-
|
|
2044
|
+
// In a sheet the sheet's band under its grab bar is the top inset.
|
|
2045
|
+
paddingBlockStart={0}>
|
|
2056
2046
|
{items.map(({child, index}) => (
|
|
2057
2047
|
<StackItem key={index} xstyle={styles.overflowItem}>
|
|
2058
2048
|
{child}
|
|
@@ -2145,7 +2135,8 @@ function AdaptiveSavedViewForm({
|
|
|
2145
2135
|
<Section
|
|
2146
2136
|
variant="transparent"
|
|
2147
2137
|
padding={4}
|
|
2148
|
-
|
|
2138
|
+
// The sheet's band under its grab bar is the top inset.
|
|
2139
|
+
paddingBlockStart={0}>
|
|
2149
2140
|
<VStack gap={4}>
|
|
2150
2141
|
<HStack gap={2}>
|
|
2151
2142
|
<StackItem size="fill">
|
|
@@ -4090,11 +4081,11 @@ export default function TableFilterTemplate() {
|
|
|
4090
4081
|
label="View options"
|
|
4091
4082
|
height="tall"
|
|
4092
4083
|
// The tab strip compensates against a container inset, so the sheet
|
|
4093
|
-
// publishes none
|
|
4084
|
+
// publishes none; its band under the grab bar is the only top inset,
|
|
4085
|
+
// so the strip's own box starts at the tab row and its scroll arrow
|
|
4086
|
+
// lands on the tabs rather than above them.
|
|
4094
4087
|
padding={0}>
|
|
4095
|
-
{
|
|
4096
|
-
row and its scroll arrow lands on the tabs rather than above them. */}
|
|
4097
|
-
<VStack gap={0} paddingBlockStart={SHEET_TOP_INSET}>
|
|
4088
|
+
<VStack gap={0}>
|
|
4098
4089
|
{/* The heading the popover puts above the pane is dropped here: it
|
|
4099
4090
|
restates the selected tab word for word, and the strip already
|
|
4100
4091
|
marks which one that is. On a phone that is a line of vertical
|
|
@@ -4148,14 +4139,15 @@ export default function TableFilterTemplate() {
|
|
|
4148
4139
|
const detailBody =
|
|
4149
4140
|
activeJob == null ? null : (
|
|
4150
4141
|
<VStack gap={0} xstyle={styles.detailPanel}>
|
|
4151
|
-
{/* The only part of the body that knows which host it is in: the
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
Layout drops the panel entirely
|
|
4142
|
+
{/* The only part of the body that knows which host it is in: in the
|
|
4143
|
+
sheet, the sheet's band under its grab bar is the top inset; the
|
|
4144
|
+
docked panel has no pill and keeps the 16px gutter of the panels
|
|
4145
|
+
beside it. The two never coexist — Layout drops the panel entirely
|
|
4146
|
+
on a compact surface. */}
|
|
4155
4147
|
<Section
|
|
4156
4148
|
variant="transparent"
|
|
4157
4149
|
padding={4}
|
|
4158
|
-
paddingBlockStart={isCompactSurface ?
|
|
4150
|
+
paddingBlockStart={isCompactSurface ? 0 : 4}>
|
|
4159
4151
|
<VStack gap={4}>
|
|
4160
4152
|
<HStack gap={2}>
|
|
4161
4153
|
<StackItem size="fill">
|
|
@@ -1785,12 +1785,9 @@ function ComposeWindow({
|
|
|
1785
1785
|
hasScrim={false}
|
|
1786
1786
|
// The Layout below owns the inset; the sheet adds none of its own.
|
|
1787
1787
|
padding={0}>
|
|
1788
|
-
{/* The
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
sheet buys the handle its own band of space before the content
|
|
1792
|
-
starts. */}
|
|
1793
|
-
<VStack paddingBlockStart={3}>
|
|
1788
|
+
{/* The sheet keeps a band under its grab handle at rest, so the Layout
|
|
1789
|
+
starts below the pill without a band of its own. */}
|
|
1790
|
+
<VStack>
|
|
1794
1791
|
<Layout
|
|
1795
1792
|
// auto, not fill: the sheet is sized by this Layout now rather than
|
|
1796
1793
|
// the other way round. `fill` would stretch it to a height that
|
|
@@ -394,8 +394,8 @@ export const butterTheme = defineTheme({
|
|
|
394
394
|
// Radius intentionally not pinned, so buttons keep core's
|
|
395
395
|
// `var(--_button-radius, --radius-element)`: standalone buttons stay 8px,
|
|
396
396
|
// while the chat composer (which sets --_button-radius) rounds them fully.
|
|
397
|
+
// Inline padding only. Button sets its own block padding and height.
|
|
397
398
|
base: {
|
|
398
|
-
paddingBlock: 'var(--spacing-3)',
|
|
399
399
|
paddingInline: 'var(--spacing-4)',
|
|
400
400
|
},
|
|
401
401
|
'variant:secondary': SECONDARY_BUTTON,
|
|
@@ -12,6 +12,13 @@ import {levenshteinDistance} from '../text/string-utils.mjs';
|
|
|
12
12
|
|
|
13
13
|
const SKIP_DIRS = new Set(['utils', '__tests__', 'node_modules']);
|
|
14
14
|
|
|
15
|
+
// A hook's doc outside src/hooks/: a `use…` hook, or a `create…` factory
|
|
16
|
+
// documented as one (its doc typed `HookDoc`, such as createReorderable).
|
|
17
|
+
const HOOK_DOC_FILE_RE = /^(?:use|create)[A-Z]\w*\.doc\.mjs$/;
|
|
18
|
+
|
|
19
|
+
// Matches the top-level `relatedHooks: [...]` field in a hook .doc.mjs file.
|
|
20
|
+
const RELATED_HOOKS_RE = /(?:^|\n) {0,4}relatedHooks:\s*\[([^\]]*)\]/;
|
|
21
|
+
|
|
15
22
|
// Matches the top-level `category: 'categoryName'` field in a hook .doc.mjs file.
|
|
16
23
|
const CATEGORY_RE = /(?:^|\n) {0,4}category:\s*['"]([^'"]+)['"]/;
|
|
17
24
|
|
|
@@ -32,6 +39,25 @@ function readHookMeta(docPath) {
|
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Whether a `create…` factory's doc names its own `use…` hook in
|
|
44
|
+
* `relatedHooks`, as createAnimator's names useAnimator. Such a factory is
|
|
45
|
+
* listed beside that hook, not on its own.
|
|
46
|
+
* @param {string} docPath
|
|
47
|
+
* @param {string} hookName
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function isBesideOwnHook(docPath, hookName) {
|
|
51
|
+
if (!hookName.startsWith('create')) return false;
|
|
52
|
+
const ownHook = `use${hookName.slice('create'.length)}`;
|
|
53
|
+
try {
|
|
54
|
+
const related = RELATED_HOOKS_RE.exec(fs.readFileSync(docPath, 'utf-8'));
|
|
55
|
+
return related != null && related[1].includes(`'${ownHook}'`);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
/**
|
|
36
62
|
* Extract the hook name from a doc filename.
|
|
37
63
|
* e.g. 'useFocusTrap.doc.mjs' → 'useFocusTrap'
|
|
@@ -70,7 +96,8 @@ export function discoverHooks(coreDir) {
|
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
98
|
|
|
73
|
-
// 2. Scan ALL component directories for
|
|
99
|
+
// 2. Scan ALL component directories for hook docs: use*.doc.mjs, and
|
|
100
|
+
// create*.doc.mjs for a factory documented as a hook (createReorderable)
|
|
74
101
|
if (pathExists(srcDir)) {
|
|
75
102
|
const topEntries = listDirEntries(srcDir);
|
|
76
103
|
for (const entry of topEntries) {
|
|
@@ -120,14 +147,10 @@ function scanDirForHookDocs(dirPath, hookCategories) {
|
|
|
120
147
|
for (const entry of entries) {
|
|
121
148
|
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
122
149
|
scanDirForHookDocs(path.join(dirPath, entry.name), hookCategories);
|
|
123
|
-
} else if (
|
|
124
|
-
entry.isFile() &&
|
|
125
|
-
entry.name.startsWith('use') &&
|
|
126
|
-
entry.name.endsWith('.doc.mjs')
|
|
127
|
-
) {
|
|
150
|
+
} else if (entry.isFile() && HOOK_DOC_FILE_RE.test(entry.name)) {
|
|
128
151
|
const hookName = hookNameFromFile(entry.name);
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
const docPath = path.join(dirPath, entry.name);
|
|
153
|
+
if (!hookCategories.has(hookName) && !isBesideOwnHook(docPath, hookName)) {
|
|
131
154
|
const {category} = readHookMeta(docPath);
|
|
132
155
|
hookCategories.set(hookName, category || 'Other');
|
|
133
156
|
}
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a: string, b: string, max?: number
|
|
27
|
+
export function levenshteinDistance(a: string, b: string, max?: number, { swaps }?: {
|
|
28
|
+
swaps?: boolean;
|
|
29
|
+
}): number;
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a, b, max) {
|
|
25
|
-
if (max !== undefined) return distanceWithin(a, b, max);
|
|
26
|
-
const m = a.length,
|
|
27
|
+
export function levenshteinDistance(a, b, max, {swaps = false} = {}) {
|
|
28
|
+
if (max !== undefined) return distanceWithin(a, b, max, swaps);
|
|
29
|
+
const m = a.length,
|
|
30
|
+
n = b.length;
|
|
27
31
|
// Early exit: the distance is always ≥ |m − n|, so once the length gap
|
|
28
32
|
// exceeds the loosest fuzzy-match threshold any caller uses (the hook
|
|
29
33
|
// suggester keeps distance ≤ 5) the pair can never match — skip the O(m·n)
|
|
@@ -36,9 +40,10 @@ export function levenshteinDistance(a, b, max) {
|
|
|
36
40
|
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
37
41
|
for (let i = 1; i <= m; i++) {
|
|
38
42
|
for (let j = 1; j <= n; j++) {
|
|
39
|
-
dp[i][j] =
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
dp[i][j] =
|
|
44
|
+
a[i - 1] === b[j - 1]
|
|
45
|
+
? dp[i - 1][j - 1]
|
|
46
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
return dp[m][n];
|
|
@@ -51,11 +56,13 @@ export function levenshteinDistance(a, b, max) {
|
|
|
51
56
|
* @param {string} a
|
|
52
57
|
* @param {string} b
|
|
53
58
|
* @param {number} max
|
|
59
|
+
* @param {boolean} swaps - two adjacent letters swapped cost one edit
|
|
54
60
|
* @returns {number}
|
|
55
61
|
*/
|
|
56
|
-
function distanceWithin(a, b, max) {
|
|
62
|
+
function distanceWithin(a, b, max, swaps) {
|
|
57
63
|
const over = max + 1;
|
|
58
64
|
if (Math.abs(a.length - b.length) > max) return over;
|
|
65
|
+
let before = new Array(b.length + 1).fill(over);
|
|
59
66
|
let previous = new Array(b.length + 1).fill(over);
|
|
60
67
|
let current = new Array(b.length + 1).fill(over);
|
|
61
68
|
for (let j = 0; j <= Math.min(b.length, max); j++) previous[j] = j;
|
|
@@ -67,18 +74,27 @@ function distanceWithin(a, b, max) {
|
|
|
67
74
|
let rowMin = current[0];
|
|
68
75
|
for (let j = from; j <= to; j++) {
|
|
69
76
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
70
|
-
|
|
77
|
+
let value = Math.min(
|
|
71
78
|
previous[j - 1] + cost,
|
|
72
79
|
previous[j] + 1,
|
|
73
80
|
current[j - 1] + 1,
|
|
74
81
|
over,
|
|
75
82
|
);
|
|
83
|
+
if (
|
|
84
|
+
swaps &&
|
|
85
|
+
i > 1 &&
|
|
86
|
+
j > 1 &&
|
|
87
|
+
a[i - 1] === b[j - 2] &&
|
|
88
|
+
a[i - 2] === b[j - 1]
|
|
89
|
+
) {
|
|
90
|
+
value = Math.min(value, before[j - 2] + 1);
|
|
91
|
+
}
|
|
76
92
|
current[j] = value;
|
|
77
93
|
if (value < rowMin) rowMin = value;
|
|
78
94
|
}
|
|
79
95
|
// Every later cell costs at least this row's least.
|
|
80
96
|
if (rowMin >= over) return over;
|
|
81
|
-
[previous, current] = [current,
|
|
97
|
+
[before, previous, current] = [previous, current, before];
|
|
82
98
|
}
|
|
83
99
|
return Math.min(previous[b.length], over);
|
|
84
100
|
}
|
|
@@ -18,15 +18,22 @@ export function nameWords(text: string): string[];
|
|
|
18
18
|
* when it carries some of them, the items that carry none are left out. When
|
|
19
19
|
* no item carries one, nothing is left out. The list only ever gets shorter,
|
|
20
20
|
* and its order does not change.
|
|
21
|
+
*
|
|
22
|
+
* With `typos`, a near name (one or two slips from the typed name, as
|
|
23
|
+
* `ButonGroup` or `ButtonGorup` for ButtonGroup) ranks just below the names
|
|
24
|
+
* that carry every typed word and above the rest. With `closest` too, near
|
|
25
|
+
* names stay in the list whatever the cut leaves out, and the list is never
|
|
26
|
+
* longer than it is without `typos`.
|
|
21
27
|
* @template T
|
|
22
28
|
* @param {string} typedName
|
|
23
29
|
* @param {T[]} items
|
|
24
30
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
25
|
-
* @param {{closest?: boolean}} [options]
|
|
31
|
+
* @param {{closest?: boolean, typos?: boolean}} [options]
|
|
26
32
|
* @returns {T[]}
|
|
27
33
|
*/
|
|
28
34
|
export function orderByAffinity<T>(typedName: string, items: T[], namesOf: (item: T) => Array<string | undefined>, options?: {
|
|
29
35
|
closest?: boolean;
|
|
36
|
+
typos?: boolean;
|
|
30
37
|
}): T[];
|
|
31
38
|
/**
|
|
32
39
|
* Docs route suggestions, closest to the typed route first. When every
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
|
|
20
20
|
import {levenshteinDistance} from './levenshtein.mjs';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The score of a near name (nearNameSlips), less a little per slip: above any
|
|
24
|
+
* name that carries only some of the typed words, below one that carries all
|
|
25
|
+
* of them.
|
|
26
|
+
*/
|
|
27
|
+
const NEAR_NAME_SCORE = 19.6;
|
|
22
28
|
/** The score a synonym match gets: an exact keyword's. */
|
|
23
29
|
const SYNONYM_SCORE = 90;
|
|
24
30
|
/** The score a name with exactly the typed words gets: an exact name's. */
|
|
@@ -59,17 +65,22 @@ export function nameWords(text) {
|
|
|
59
65
|
* closer. Tiers, best first: the same words; every typed word present; some
|
|
60
66
|
* typed word present; none. Within a tier, more of the typed words and more of
|
|
61
67
|
* the candidate's own words matched rank higher, and a name that starts with
|
|
62
|
-
* the typed name (or the reverse) gets a small lift.
|
|
68
|
+
* the typed name (or the reverse) gets a small lift. With `typos`, a near name
|
|
69
|
+
* (one or two slips from the typed name) ranks just below the names that
|
|
70
|
+
* carry every typed word.
|
|
63
71
|
* @param {string[]} typed - {@link nameWords} of the typed name
|
|
64
72
|
* @param {string} name
|
|
73
|
+
* @param {{typos?: boolean}} [options]
|
|
65
74
|
* @returns {number}
|
|
66
75
|
*/
|
|
67
|
-
function affinityOf(typed, name) {
|
|
76
|
+
function affinityOf(typed, name, {typos = false} = {}) {
|
|
68
77
|
const words = nameWords(name);
|
|
69
78
|
if (typed.length === 0 || words.length === 0) return 0;
|
|
70
79
|
const typedJoined = typed.join('');
|
|
71
80
|
const joined = words.join('');
|
|
72
81
|
if (joined === typedJoined) return 40;
|
|
82
|
+
const slips = typos ? nearNameSlips(typedJoined, joined) : 0;
|
|
83
|
+
const near = slips > 0 ? NEAR_NAME_SCORE - slips * 0.2 : 0;
|
|
73
84
|
const have = new Set(words);
|
|
74
85
|
const want = new Set(typed);
|
|
75
86
|
const covered = typed.filter(w => have.has(w)).length / typed.length;
|
|
@@ -77,7 +88,29 @@ function affinityOf(typed, name) {
|
|
|
77
88
|
const prefix =
|
|
78
89
|
joined.startsWith(typedJoined) || typedJoined.startsWith(joined) ? 1 : 0;
|
|
79
90
|
const tier = covered === 1 ? 2 : covered > 0 ? 1 : 0;
|
|
80
|
-
|
|
91
|
+
// A near name keeps its word score when that is higher: a name that also
|
|
92
|
+
// carries every typed word ranks among the other full matches.
|
|
93
|
+
return Math.max(
|
|
94
|
+
near,
|
|
95
|
+
tier * 10 + covered * 4 + precise * 2 + prefix + precise * 0.01,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How many slips separate a typed name from a candidate, both with their
|
|
101
|
+
* words joined, when it is a near name: an edit or two adjacent letters
|
|
102
|
+
* swapped count as one slip, one slip is allowed from five letters and two from
|
|
103
|
+
* eight. Zero when it is not a near name.
|
|
104
|
+
* @param {string} typed
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
function nearNameSlips(typed, name) {
|
|
109
|
+
const shorter = Math.min(typed.length, name.length);
|
|
110
|
+
const cap = shorter >= 8 ? 2 : shorter >= 5 ? 1 : 0;
|
|
111
|
+
if (cap === 0 || Math.abs(typed.length - name.length) > cap) return 0;
|
|
112
|
+
const slips = levenshteinDistance(typed, name, cap, {swaps: true});
|
|
113
|
+
return slips <= cap ? slips : 0;
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
/**
|
|
@@ -87,14 +120,15 @@ function affinityOf(typed, name) {
|
|
|
87
120
|
* @param {string} typedName
|
|
88
121
|
* @param {T[]} items
|
|
89
122
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
123
|
+
* @param {{typos?: boolean}} [options]
|
|
90
124
|
* @returns {Array<{item: T, best: number}>}
|
|
91
125
|
*/
|
|
92
|
-
function scoreByAffinity(typedName, items, namesOf) {
|
|
126
|
+
function scoreByAffinity(typedName, items, namesOf, options = {}) {
|
|
93
127
|
const typed = nameWords(typedName);
|
|
94
128
|
const scored = items.map((item, index) => {
|
|
95
129
|
let best = 0;
|
|
96
130
|
for (const name of namesOf(item)) {
|
|
97
|
-
if (name) best = Math.max(best, affinityOf(typed, name));
|
|
131
|
+
if (name) best = Math.max(best, affinityOf(typed, name, options));
|
|
98
132
|
}
|
|
99
133
|
return {item, index, best};
|
|
100
134
|
});
|
|
@@ -112,14 +146,46 @@ function scoreByAffinity(typedName, items, namesOf) {
|
|
|
112
146
|
* when it carries some of them, the items that carry none are left out. When
|
|
113
147
|
* no item carries one, nothing is left out. The list only ever gets shorter,
|
|
114
148
|
* and its order does not change.
|
|
149
|
+
*
|
|
150
|
+
* With `typos`, a near name (one or two slips from the typed name, as
|
|
151
|
+
* `ButonGroup` or `ButtonGorup` for ButtonGroup) ranks just below the names
|
|
152
|
+
* that carry every typed word and above the rest. With `closest` too, near
|
|
153
|
+
* names stay in the list whatever the cut leaves out, and the list is never
|
|
154
|
+
* longer than it is without `typos`.
|
|
115
155
|
* @template T
|
|
116
156
|
* @param {string} typedName
|
|
117
157
|
* @param {T[]} items
|
|
118
158
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
119
|
-
* @param {{closest?: boolean}} [options]
|
|
159
|
+
* @param {{closest?: boolean, typos?: boolean}} [options]
|
|
120
160
|
* @returns {T[]}
|
|
121
161
|
*/
|
|
122
162
|
export function orderByAffinity(typedName, items, namesOf, options = {}) {
|
|
163
|
+
if (!options.typos) return orderByWords(typedName, items, namesOf, options);
|
|
164
|
+
const scored = scoreByAffinity(typedName, items, namesOf, {typos: true});
|
|
165
|
+
if (!options.closest) return scored.map(s => s.item);
|
|
166
|
+
const kept = new Set(orderByWords(typedName, items, namesOf, options));
|
|
167
|
+
return scored
|
|
168
|
+
.filter(s => kept.has(s.item) || isNearScore(s.best))
|
|
169
|
+
.slice(0, kept.size)
|
|
170
|
+
.map(s => s.item);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether a score is a near name's.
|
|
175
|
+
* @param {number} score
|
|
176
|
+
*/
|
|
177
|
+
const isNearScore = score => score < 20 && score > NEAR_NAME_SCORE - 1;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* {@link orderByAffinity} without `typos`.
|
|
181
|
+
* @template T
|
|
182
|
+
* @param {string} typedName
|
|
183
|
+
* @param {T[]} items
|
|
184
|
+
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
185
|
+
* @param {{closest?: boolean}} options
|
|
186
|
+
* @returns {T[]}
|
|
187
|
+
*/
|
|
188
|
+
function orderByWords(typedName, items, namesOf, options) {
|
|
123
189
|
const scored = scoreByAffinity(typedName, items, namesOf);
|
|
124
190
|
if (!options.closest) return scored.map(s => s.item);
|
|
125
191
|
// One-letter words (`a` in `a11y`, `use-a-theme`) say nothing about which
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.7.12-canary.
|
|
3
|
+
"version": "0.7.12-canary.c86484d",
|
|
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",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"zod": "^4.4.3"
|
|
126
126
|
},
|
|
127
127
|
"peerDependencies": {
|
|
128
|
-
"@astryxdesign/core": "0.7.12-canary.
|
|
128
|
+
"@astryxdesign/core": "0.7.12-canary.c86484d"
|
|
129
129
|
},
|
|
130
130
|
"peerDependenciesMeta": {
|
|
131
131
|
"@astryxdesign/core": {
|
|
@@ -133,10 +133,10 @@
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
|
-
"@astryxdesign/charts": "0.7.12-canary.
|
|
137
|
-
"@astryxdesign/core": "0.7.12-canary.
|
|
138
|
-
"@astryxdesign/lab": "0.7.12-canary.
|
|
139
|
-
"@astryxdesign/theme-neutral": "0.7.12-canary.
|
|
136
|
+
"@astryxdesign/charts": "0.7.12-canary.c86484d",
|
|
137
|
+
"@astryxdesign/core": "0.7.12-canary.c86484d",
|
|
138
|
+
"@astryxdesign/lab": "0.7.12-canary.c86484d",
|
|
139
|
+
"@astryxdesign/theme-neutral": "0.7.12-canary.c86484d",
|
|
140
140
|
"@heroicons/react": "^2.2.0",
|
|
141
141
|
"@stylexjs/stylex": "^0.19.0",
|
|
142
142
|
"@types/babel__core": "^7.20.5",
|