@astryxdesign/cli 0.7.12-canary.f36bd72 → 0.7.12

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/api/build/blocks/blocks.d.mts +41 -0
  3. package/api/build/blocks/blocks.mjs +118 -68
  4. package/api/doctor/doctor.d.mts +4 -4
  5. package/api/doctor/doctor.mjs +248 -78
  6. package/api/icon/_authoring.mjs +8 -1
  7. package/api/icon/add/add.mjs +45 -5
  8. package/api/icon/icon.type.d.mts +24 -0
  9. package/api/icon/icon.type.mjs +6 -0
  10. package/api/icon/iconAdd.doc.mjs +1 -1
  11. package/api/icon/map/map.mjs +11 -4
  12. package/api/integration/add-helpers.d.mts +5 -1
  13. package/api/integration/add-helpers.mjs +1 -1
  14. package/api/render/render.doc.mjs +1 -1
  15. package/api/render/render.type.d.mts +1 -1
  16. package/api/render/render.type.mjs +1 -1
  17. package/api/search/ORDERING.md +3 -0
  18. package/api/search/_adapter.d.mts +10 -2
  19. package/api/search/_adapter.mjs +13 -2
  20. package/api/search/_fit-order.d.mts +47 -0
  21. package/api/search/_fit-order.mjs +257 -0
  22. package/api/search/_main-searcher.mjs +15 -5
  23. package/api/template/skeleton/skeleton.mjs +1 -1
  24. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +1 -1
  25. package/assets/codemods/transforms/v0.1.0/index.mjs +1 -1
  26. package/assets/docs/tree/icon-sets.doc.mjs +4 -4
  27. package/assets/docs/tree/render-pages.doc.mjs +1 -1
  28. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +2 -2
  29. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +13 -7
  30. package/assets/templates/pages/table-grouped/page.tsx +30 -30
  31. package/assets/templates/themes/gothic/gothicTheme.ts +16 -0
  32. package/assets/templates/themes/matcha/matchaTheme.ts +1 -1
  33. package/assets/templates/themes/neutral/neutralTheme.ts +6 -0
  34. package/assets/templates/themes/y2k/y2kPaletteRefs.generated.ts +1 -1
  35. package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
  36. package/clients/cli/commands/experimental/icon.mjs +25 -4
  37. package/clients/cli/commands/experimental-render.doc.mjs +1 -1
  38. package/clients/cli/commands/template.mjs +204 -181
  39. package/foundation/discovery/component-discovery.mjs +1 -1
  40. package/foundation/discovery/icon-set-writer.d.mts +21 -5
  41. package/foundation/discovery/icon-set-writer.mjs +114 -13
  42. package/foundation/discovery/template-adapter.mjs +2 -2
  43. package/foundation/render/page-host.d.mts +4 -0
  44. package/foundation/render/page-host.mjs +45 -0
  45. package/foundation/render/page-worker.mjs +11 -5
  46. package/foundation/render/render-page.d.mts +11 -6
  47. package/foundation/render/render-page.mjs +138 -44
  48. package/foundation/response/error-codes.doc.mjs +1 -1
  49. package/foundation/xle/registry-core.mjs +1 -1
  50. package/package.json +6 -6
@@ -0,0 +1,257 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file A last pass over a searcher's answer: inside each run of one score
5
+ * band and kind, the entries from #2 to #10 that fit the whole query much
6
+ * worse than the others in their run move, in order, after them.
7
+ * @input the ordered response, the gathered candidates, and the query
8
+ * @output the same response with the same results, maybe in a new order
9
+ * @position api/search; rankWith (_main-searcher.mjs) runs it after
10
+ * orderMatches, with the weights the adapter loads (loadSearchData) through
11
+ * build's own loader. The fit is build's block-picker match
12
+ * (build/blocks/blocks.mjs) with its shipped weights, so an entry the weights
13
+ * never saw is scored from its own docs the same way. Without readable
14
+ * weights the order stays.
15
+ */
16
+
17
+ import {
18
+ docSideOf,
19
+ matchOf,
20
+ requestSideOf,
21
+ stemOf,
22
+ termsOf,
23
+ wordsOf,
24
+ } from '../build/blocks/blocks.mjs';
25
+ import {bandOf} from './_order.mjs';
26
+ import {searchedComponentsOf} from './_result.mjs';
27
+
28
+ /** An entry stays in place when its fit is at least this times the median. */
29
+ export const FIT_BAR = 1.7;
30
+
31
+ /** An entry whose name spells at least this many query words never moves. */
32
+ const NAME_WORDS = 2;
33
+
34
+ /** The first result stays; the pass orders the places after it up to here. */
35
+ export const FIT_PLACES = 10;
36
+
37
+ /** @typedef {import('../build/blocks/blocks.mjs').BlockPickerWeights} Weights */
38
+ /** @typedef {{vector: Map<number, number>, projected: Float64Array}} Side */
39
+
40
+ /**
41
+ * Each candidate by `domain:name`; a doc also by its topic and by
42
+ * `topic#section`. The first candidate with a key owns it.
43
+ * @type {WeakMap<any[], Map<string, any>>}
44
+ */
45
+ const keyed = new WeakMap();
46
+
47
+ /**
48
+ * Each candidate's docs side, computed when a result first needs it.
49
+ * @type {WeakMap<Weights, WeakMap<any, Side>>}
50
+ */
51
+ const sides = new WeakMap();
52
+
53
+ /**
54
+ * An entry's docs text: its name words, display name or first title,
55
+ * description, and keywords.
56
+ * @param {any} c
57
+ */
58
+ const docText = c =>
59
+ [
60
+ wordsOf(c.name).join(' '),
61
+ c._displayName ?? (Array.isArray(c.titles) ? c.titles[0] : '') ?? '',
62
+ c.description ?? '',
63
+ (Array.isArray(c.keywords) ? c.keywords : []).join(' '),
64
+ ].join(' ');
65
+
66
+ /**
67
+ * @param {any[]} candidates
68
+ * @returns {Map<string, any>}
69
+ */
70
+ function keysOf(candidates) {
71
+ let byKey = keyed.get(candidates);
72
+ if (!byKey) {
73
+ byKey = new Map();
74
+ for (const c of candidates) {
75
+ const keys = [`${c.domain}:${c._resultName ?? c.name}`];
76
+ if (c.domain === 'doc' && c._topic)
77
+ keys.push(
78
+ c._section ? `doc:${c._topic}#${c._section}` : `doc:${c._topic}`,
79
+ );
80
+ for (const k of keys) if (!byKey.has(k)) byKey.set(k, c);
81
+ }
82
+ keyed.set(candidates, byKey);
83
+ }
84
+ return byKey;
85
+ }
86
+
87
+ /**
88
+ * @param {any} c
89
+ * @param {Weights} weights
90
+ * @returns {Side}
91
+ */
92
+ function candidateSide(c, weights) {
93
+ let byCandidate = sides.get(weights);
94
+ if (!byCandidate) {
95
+ byCandidate = new WeakMap();
96
+ sides.set(weights, byCandidate);
97
+ }
98
+ let side = byCandidate.get(c);
99
+ if (!side) {
100
+ side = docSideOf(docText(c), weights);
101
+ byCandidate.set(c, side);
102
+ }
103
+ return side;
104
+ }
105
+
106
+ /**
107
+ * A result's docs side: its candidate's, else one from the result's own name,
108
+ * display name and description.
109
+ * @param {any} r
110
+ * @param {any[]} candidates
111
+ * @param {Weights} weights
112
+ * @returns {Side}
113
+ */
114
+ function resultSideOf(r, candidates, weights) {
115
+ const byKey = keysOf(candidates);
116
+ if (r.domain === 'doc' && r.section) {
117
+ const c = byKey.get(`doc:${r.name}#${r.section}`);
118
+ if (c) return candidateSide(c, weights);
119
+ }
120
+ const c = byKey.get(`${r.domain}:${r.name}`);
121
+ return c
122
+ ? candidateSide(c, weights)
123
+ : docSideOf(
124
+ `${wordsOf(r.name).join(' ')} ${r.displayName ?? ''} ${r.description ?? ''}`,
125
+ weights,
126
+ );
127
+ }
128
+
129
+ /**
130
+ * A result's run: its score band and kind (doc or not), as orderMatches reads
131
+ * them.
132
+ * @param {any} r
133
+ */
134
+ const runOf = r => `${bandOf(r.score ?? 0)}:${r.domain === 'doc'}`;
135
+
136
+ /**
137
+ * The results with the first kept. Entries #2 to #10 split into runs of one
138
+ * band and kind. Inside a run, the entries that fit the query under FIT_BAR
139
+ * times the run's median fit move, in order, after the others, unless their
140
+ * name spells NAME_WORDS or more of the query's words. A run whose
141
+ * median fit is zero or less keeps its order. Nothing crosses a run, and
142
+ * nothing is added or dropped.
143
+ * @param {any[]} results
144
+ * @param {any[]} candidates
145
+ * @param {string} query
146
+ * @param {Weights | (() => Weights | null | undefined) | null | undefined} weights
147
+ * the block picker's weights, or a function that reads them; the function
148
+ * runs only when a run can change. Without weights the order stays
149
+ * @returns {any[]}
150
+ */
151
+ export function orderByFit(results, candidates, query, weights) {
152
+ if (!weights || results.length < 3) return results;
153
+ const end = Math.min(FIT_PLACES, results.length);
154
+ /** @type {[number, number][]} */
155
+ const runs = [];
156
+ for (let i = 1; i < end;) {
157
+ let j = i;
158
+ while (j < end && runOf(results[j]) === runOf(results[i])) j++;
159
+ runs.push([i, j]);
160
+ i = j;
161
+ }
162
+ // Only a run of two or more can change, so the weights stay unread when
163
+ // there is none.
164
+ if (!runs.some(([i, j]) => j - i > 1)) return results;
165
+ const w = typeof weights === 'function' ? weights() : weights;
166
+ if (!w) return results;
167
+ const request = requestSideOf(query, w);
168
+ const queryWords = new Set(termsOf(query).filter(t => !t.includes('_')));
169
+ /** @param {any} r */
170
+ const named = r =>
171
+ new Set(
172
+ wordsOf(r.name)
173
+ .map(stemOf)
174
+ .filter(word => queryWords.has(word)),
175
+ ).size >= NAME_WORDS;
176
+ const out = results.slice();
177
+ let changed = false;
178
+ for (const [i, j] of runs) {
179
+ if (j - i < 2) continue;
180
+ const moved = demoteWeak(
181
+ results.slice(i, j),
182
+ request,
183
+ candidates,
184
+ w,
185
+ named,
186
+ );
187
+ moved.forEach((r, k) => {
188
+ if (r !== out[i + k]) changed = true;
189
+ out[i + k] = r;
190
+ });
191
+ }
192
+ return changed ? out : results;
193
+ }
194
+
195
+ /**
196
+ * One run, with the entries under FIT_BAR times its median fit moved, in
197
+ * order, after the others. An entry whose name spells NAME_WORDS or more of
198
+ * the query's words stays with the others: its name is strong evidence.
199
+ * @param {any[]} run
200
+ * @param {Side} request
201
+ * @param {any[]} candidates
202
+ * @param {Weights} weights
203
+ * @param {(r: any) => boolean} named
204
+ * @returns {any[]}
205
+ */
206
+ function demoteWeak(run, request, candidates, weights, named) {
207
+ const fits = run.map(
208
+ r => matchOf(request, resultSideOf(r, candidates, weights), weights).match,
209
+ );
210
+ const sorted = [...fits].sort((a, b) => a - b);
211
+ const n = sorted.length;
212
+ const median =
213
+ n % 2 ? sorted[(n - 1) / 2] : (sorted[n / 2 - 1] + sorted[n / 2]) / 2;
214
+ if (!(median > 0)) return run;
215
+ const bar = FIT_BAR * median;
216
+ const kept = run.map((r, k) => fits[k] >= bar || named(r));
217
+ return [...run.filter((_, k) => kept[k]), ...run.filter((_, k) => !kept[k])];
218
+ }
219
+
220
+ /**
221
+ * The search response with its results ordered by fit (`orderByFit`). Any
222
+ * other response passes through.
223
+ * @template T
224
+ * @param {T} response
225
+ * @param {any[]} candidates
226
+ * @param {string} query
227
+ * @param {Weights | (() => Weights | null | undefined) | null | undefined} weights
228
+ * @returns {T}
229
+ */
230
+ export function withFitOrder(response, candidates, query, weights) {
231
+ const r = /** @type {any} */ (response);
232
+ const results = r?.data?.results;
233
+ if (!Array.isArray(results)) return response;
234
+ const ordered = orderByFit(results, candidates, query, weights);
235
+ if (ordered === results) return response;
236
+ const out = {...r, data: {...r.data, results: ordered}};
237
+ const searched = searchedComponentsOf.get(r);
238
+ if (searched) searchedComponentsOf.set(out, searched);
239
+ return /** @type {T} */ (out);
240
+ }
241
+
242
+ /**
243
+ * The search response with at most `limit` results.
244
+ * @template T
245
+ * @param {T} response
246
+ * @param {number} limit
247
+ * @returns {T}
248
+ */
249
+ export function withLimit(response, limit) {
250
+ const r = /** @type {any} */ (response);
251
+ const results = r?.data?.results;
252
+ if (!Array.isArray(results) || results.length <= limit) return response;
253
+ const out = {...r, data: {...r.data, results: results.slice(0, limit)}};
254
+ const searched = searchedComponentsOf.get(r);
255
+ if (searched) searchedComponentsOf.set(out, searched);
256
+ return /** @type {T} */ (out);
257
+ }
@@ -15,6 +15,7 @@
15
15
  import {routeKit} from '../../foundation/route/route.mjs';
16
16
  import {rank, signals} from './_features.mjs';
17
17
  import {entryTable} from './_entries.mjs';
18
+ import {FIT_PLACES, withFitOrder, withLimit} from './_fit-order.mjs';
18
19
  import {orderMatches} from './_order.mjs';
19
20
  import {routeProbs} from './_query-words.mjs';
20
21
  import {namesWhole, rankLexical} from './_lexical.mjs';
@@ -94,13 +95,22 @@ export function rankWith(request, catalog, weights) {
94
95
  );
95
96
  const route = routeProbs(catalog.data.router, query);
96
97
  const rows = signals(table, query, lexical.data.results, route);
97
- return orderMatches(
98
- table,
99
- rank(table, rows, weights),
100
- lexical,
98
+ const limit = options.limit ?? 20;
99
+ // The fit pass reads the first FIT_PLACES results at every limit, so a
100
+ // smaller limit shows the top of that order.
101
+ const ordered = withFitOrder(
102
+ orderMatches(
103
+ table,
104
+ rank(table, rows, weights),
105
+ lexical,
106
+ query,
107
+ Math.max(limit, FIT_PLACES),
108
+ ),
109
+ catalog.all,
101
110
  query,
102
- options.limit ?? 20,
111
+ () => catalog.data.picker,
103
112
  );
113
+ return withLimit(ordered, limit);
104
114
  }
105
115
 
106
116
  /** @type {import('./tree.mjs').SearchKit} */
@@ -146,7 +146,7 @@ function extractSkeleton(source) {
146
146
 
147
147
  // Match a JSX opening tag, e.g. `<Section` or the legacy `<XDSSection`.
148
148
  // The `XDS` prefix is optional (templates author bare names post
149
- // un-prefix migration P2380608025). `tagName` is the full authored name
149
+ // un-prefix migration). `tagName` is the full authored name
150
150
  // (used for the self-closing lookahead); `comp` is the bare display name.
151
151
  const openMatch = t.match(/^<((XDS)?([A-Z]\w+))/);
152
152
  if (openMatch && !t.startsWith('</')) {
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * @file Codemod: Drop the `XDS` prefix from @xds/core imports and their usages
5
5
  *
6
- * Part of the XDS-prefix migration (P2380608025). Rewrites consumer code from
6
+ * Part of the XDS-prefix migration. Rewrites consumer code from
7
7
  * the prefixed names to their bare equivalents:
8
8
  *
9
9
  * XDSButton -> Button
@@ -24,7 +24,7 @@ import migrateXdsCssSurfaces, {
24
24
 
25
25
  export default [
26
26
  {
27
- // XDS-prefix migration (P2380608025). Mandatory in v0.1.0: the release
27
+ // XDS-prefix migration. Mandatory in v0.1.0: the release
28
28
  // dropped the prefixed compatibility aliases (useXDSTheme, XDSButton,
29
29
  // XDSIconRegistry, ...), so consumers upgrading from 0.0.x MUST rewrite
30
30
  // prefixed imports to their bare names.
@@ -61,7 +61,7 @@ export const docs = {
61
61
  },
62
62
  {
63
63
  type: 'prose',
64
- text: "`outline.doc.mjs` is the descriptor: name, display name and description (`--display-name`, `--description`), and `license`, the SPDX license of the artwork (it starts as your package's own). `outline.icons.json` starts with no icons and an empty `map`; its `module` is your package name. `outline.generated.js` is the set's generated module, with its declaration file `outline.generated.d.ts`: the CLI writes both, and every icon and the theme map are exports of it. Every field is in {@link schema:icon-set-doc} and {@link schema:icon-set-data}.",
64
+ text: "`outline.doc.mjs` is the descriptor: name, display name and description (`--display-name`, `--description`), and `license`, the SPDX license of the artwork (it starts as your package's own). `outline.icons.json` starts with no icons and an empty `map`; its `module` is your package name. After you rename the package, the next command that writes the data file (`new-set` on the existing set, with or without `--from`, `add`, `map`, or `new-definition --icon`) writes the new name and keeps any subpath (`@acme/old-icons/outline` becomes `@acme/new-icons/outline`), and its receipt says so. `outline.generated.js` is the set's generated module, with its declaration file `outline.generated.d.ts`: the CLI writes both, and every icon and the theme map are exports of it. Every field is in {@link schema:icon-set-doc} and {@link schema:icon-set-data}.",
65
65
  },
66
66
  {
67
67
  type: 'prose',
@@ -121,7 +121,7 @@ export const docs = {
121
121
  },
122
122
  {
123
123
  type: 'prose',
124
- text: "Every component the library exports becomes an icon (an alias of one component is recorded once), named `<Name>Icon`, with an id from its name (`Squares2X2Icon` is `squares-2x2`: a dimension stays one word). `map` is filled only where a definition's name is an icon's id (`delete` → icon `delete`), and the receipt lists those entries under Mapped by name: a library can draw another meaning under the same name (one library's `delete` is a backspace key), so check each. An icon whose id is only one of a definition's words (`trash` for `delete`) may mean something else in this library, so the receipt lists it under Suggestions, with the `experimental icon map <definition> <icon>` command for each, and writes nothing for it: check the meaning, then run the ones that fit. Run it again after a library release to refresh the icons that library supplies; `map` entries whose icon still exists are kept, and the receipt names any icon the library dropped, any icon `--exclude` left out, and the `map` entries each took with it. Icons you added from your own files or another library are left as they are. Icons you added one file at a time from the same package (`--from <package>/dist/XIcon`) move to the root import when you refresh from the package root and the root exports the same icon; each keeps its id, export, `map` entries, variants, and words on that refresh and every later one, and the receipt lists it under `moved`. A library icon whose id or name a moved icon already has is left out. When another icon of the set already reads the same root export, the icon stays on its own file, and the receipt says why. An icon you already have is known by the library export it re-exports, so it keeps its export name exactly, its `map` entries, variants, and words, even when a newer CLI spells its id differently or your `--exclude` lets a library prefix come off new ids; the receipt lists each changed id under `renamed`. An id you chose that is not a spelling of the library name (`dismiss` for `XIcon`) is kept. A refresh with `--dry-run` writes nothing and prints the Mapped by name and Suggestions lists again; `--display-name` and `--description` on a refresh rewrite the descriptor. Variants added with `add --variant … --all` are kept too; run that `add` again to pick up icons the release added. `experimental icon --list` shows how many definitions the set maps.",
124
+ text: "Every component the library exports becomes an icon (an alias of one component is recorded once), named `<Name>Icon`, with an id from its name (`Squares2X2Icon` is `squares-2x2`: a dimension stays one word). An export name cannot start with a digit, so an icon whose id would start with one once the library prefix comes off keeps the prefix (`fa0` is id `fa-0`, exported as `Fa0Icon`), and an id you give with `--id` must start with a letter. `map` is filled only where a definition's name is an icon's id (`delete` → icon `delete`), and the receipt lists those entries under Mapped by name: a library can draw another meaning under the same name (one library's `delete` is a backspace key), so check each. An icon whose id is only one of a definition's words (`trash` for `delete`) may mean something else in this library, so the receipt lists it under Suggestions, with the `experimental icon map <definition> <icon>` command for each, and writes nothing for it: check the meaning, then run the ones that fit. Run it again after a library release to refresh the icons that library supplies; `map` entries whose icon still exists are kept, and the receipt names any icon the library dropped, any icon `--exclude` left out, and the `map` entries each took with it. Icons you added from your own files or another library are left as they are. Icons you added one file at a time from the same package (`--from <package>/dist/XIcon`) move to the root import when you refresh from the package root and the root exports the same icon; each keeps its id, export, `map` entries, variants, and words on that refresh and every later one, and the receipt lists it under `moved`. A library icon whose id or name a moved icon already has is left out. When another icon of the set already reads the same root export, the icon stays on its own file, and the receipt says why. An icon you already have is known by the library export it re-exports, so it keeps its export name exactly, its `map` entries, variants, and words, even when a newer CLI spells its id differently or your `--exclude` lets a library prefix come off new ids; the receipt lists each changed id under `renamed`. An id you chose that is not a spelling of the library name (`dismiss` for `XIcon`) is kept. A refresh with `--dry-run` writes nothing and prints the Mapped by name and Suggestions lists again; `--display-name` and `--description` on a refresh rewrite the descriptor. Variants added with `add --variant … --all` are kept too; run that `add` again to pick up icons the release added. `experimental icon --list` shows how many definitions the set maps.",
125
125
  },
126
126
  {
127
127
  type: 'prose',
@@ -139,11 +139,11 @@ astryx experimental icon add --variant solid --from @heroicons/react/24/solid --
139
139
  },
140
140
  {
141
141
  type: 'prose',
142
- text: "A library that redraws its artwork for small sizes (Heroicons' 20px mini and 16px micro) ships each of those as its own set in its own package (their export names match the 24px ones), not as variants: a variant's size comes from CSS. `--variants` lists the variants; `--default-variant` names the one each icon's own export draws (when omitted, the look a library that names its looks draws; otherwise the set's current default, else, on a set without icons, the first variant; a set with icons changes it only with `--from`, and only in a library that names its looks). `--all` adds the variant for every icon whose id an export of `--from` has (`TrashIcon` in the solid module becomes `TrashSolidIcon`, the solid variant of `trash`), and the receipt lists the icons the module has no export for. Running it again changes nothing.",
142
+ text: "A library that redraws its artwork for small sizes (Heroicons' 20px mini and 16px micro) ships each of those as its own set in its own package (their export names match the 24px ones), not as variants: a variant's size comes from CSS. `--variants` lists the variants; `--default-variant` names the one each icon's own export draws (when omitted, the look a library that names its looks draws; otherwise the set's current default, else, on a set without icons, the first variant; a set with icons changes it only with `--from`, and only in a library that names its looks). `--all` adds the variant for every icon whose id an export of `--from` has (`TrashIcon` in the solid module becomes `TrashSolidIcon`, the solid variant of `trash`), and the receipt lists the icons the module has no export for. A library may ship each look as its own package (Font Awesome's solid and regular packages): `--from` may then name the other package, and an icon gains the variant from the export of the same name there; the receipt lists the icons that package has no export for. Running it again changes nothing.",
143
143
  },
144
144
  {
145
145
  type: 'prose',
146
- text: 'A library whose icons are data (objects or SVG strings) has no components to find: create the set, declare an `adapter` in its data file (see library shapes), and run the command again; every data export then becomes an icon through the adapter. When every icon export shares a leading word, such as `feather` in `featherEye` and `featherSun`, ids and export names leave it out: `featherEye` becomes id `eye`, exported as `EyeIcon`. The prefix is read over icon-like exports only: SCREAMING_CASE constants (`README`, `VERSION`), number and boolean exports, and, next to components or icon objects, plain strings are never icons. Components named `…Provider` or `…Context` are never icons either. Leave out any other export that is not an icon with `--exclude meta,version`: the data file keeps the list in `exclude`, so a refresh leaves them out too. When a library exports one icon under several names (`Trash2` and `Trash`), the icon is recorded once and its other names become words, so `experimental icon trash-2` finds it and `experimental icon map delete trash-2` maps it. `experimental icon add` reads the prefix the same way, and reuses the icon an export already has, so its commands need no `--id`.',
146
+ text: 'A library whose icons are data (objects or SVG strings) has no components to find: create the set, declare an `adapter` in its data file (see library shapes), and run the command again; every data export then becomes an icon through the adapter. When every icon export shares a leading word, such as `feather` in `featherEye` and `featherSun`, ids and export names leave it out: `featherEye` becomes id `eye`, exported as `EyeIcon`. The prefix is read over icon-like exports only: SCREAMING_CASE constants (`README`, `VERSION`), number and boolean exports, and, next to components or icon objects, plain strings are never icons. Components named `…Provider` or `…Context` are never icons either, and neither is a pack: an object whose keys are all other exports of the module, such as `fas` next to `faXmark`. Leave out any other export that is not an icon with `--exclude meta,version`: the data file keeps the list in `exclude`, so a refresh leaves them out too. When a library exports one icon under several names (`Trash2` and `Trash`), the icon is recorded once and its other names become words, so `experimental icon trash-2` finds it and `experimental icon map delete trash-2` maps it. `experimental icon add` reads the prefix the same way, and reuses the icon an export already has, so its commands need no `--id`.',
147
147
  },
148
148
  ],
149
149
  },
@@ -67,7 +67,7 @@ export const docs = {
67
67
  content: [
68
68
  {
69
69
  type: 'prose',
70
- text: 'Each warning names something that may look different from what you meant, and what to change. The most common is a component that ignores its children: `<ListItem>Save</ListItem>` renders an empty row because ListItem takes `label="Save"`. Others flag remote images (a sandboxed viewer cannot load them; save the image next to the page and import it), a `<script>`, `<link>` or `<iframe>` the page itself renders (the file promises to run and load nothing, so remove them), missing images, and output the page printed while rendering.',
70
+ text: 'Each warning names something that may look different from what you meant, and what to change. The most common is a component that ignores its children: `<ListItem>Save</ListItem>` renders an empty row because ListItem takes `label="Save"`. Others flag remote images (a sandboxed viewer cannot load them; save the image next to the page and import it), a `<script>`, `<link>` or `<iframe>` the page itself renders (the file promises to run and load nothing, so remove them), missing images, and output the page printed while rendering. A `node-crash` warning names a Node build that crashes in `Intl.Segmenter` (its version and path). The page rendered without it, so the output may differ slightly: initials and character counts (such as Avatar initials) can split a joined emoji, a flag, or a letter written with a separate accent mark. Names in ordinary composed letters (`José`, `Zoë`, `李小龙`) come out the same. Render with an official Node.js build to remove the difference.',
71
71
  },
72
72
  ],
73
73
  },
@@ -7,8 +7,8 @@ export const doc = {
7
7
  name: 'useTableRowsVirtualization - Fixed Height Rows',
8
8
  displayName: 'useTableRowsVirtualization - Fixed Height Rows',
9
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.',
10
+ "A Table with 1,000 product rows inside a 320px-tall parent, which bounds its scroll area. Give the Table's parent a height, fixed or relative, or a flex child, so windowing can bound the rows; with no height every row renders. Scroll vertically to mount the visible rows. The caller keeps all data and columns; this hook does not virtualize columns.",
11
11
  isReady: true,
12
12
  aspectRatio: 16 / 9,
13
- componentsUsed: ['Table'],
13
+ componentsUsed: ['Table', 'VStack'],
14
14
  };
@@ -7,6 +7,7 @@ import {
7
7
  useTableRowsVirtualization,
8
8
  type TableColumn,
9
9
  } from '@astryxdesign/core/Table';
10
+ import {VStack} from '@astryxdesign/core/Layout';
10
11
 
11
12
  interface Product extends Record<string, unknown> {
12
13
  id: string;
@@ -22,14 +23,19 @@ const columns: TableColumn<Product>[] = [{key: 'name', header: 'Product'}];
22
23
  export default function RowsVirtualizationHookUsage() {
23
24
  const virtualization = useTableRowsVirtualization({
24
25
  rowHeight: 56,
25
- viewportHeight: 320,
26
+ initialViewportHeight: 320,
26
27
  });
28
+ // Give the Table's parent a height, fixed or relative, or a flex child, so
29
+ // windowing can bound the rows; with no height every row renders.
30
+ // initialViewportHeight only sizes the first window.
27
31
  return (
28
- <Table
29
- data={products}
30
- columns={columns}
31
- idKey="id"
32
- virtualization={virtualization}
33
- />
32
+ <VStack height={320}>
33
+ <Table
34
+ data={products}
35
+ columns={columns}
36
+ idKey="id"
37
+ virtualization={virtualization}
38
+ />
39
+ </VStack>
34
40
  );
35
41
  }