@starci/hfs 4.16.5 → 4.17.2

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 (76) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.md +4 -4
  3. package/codemod/loading-states.d.mts +50 -0
  4. package/codemod/loading-states.mjs +384 -0
  5. package/codemod/main.d.mts +1 -1
  6. package/codemod/main.mjs +46 -5
  7. package/codemod/page-navigation.d.mts +50 -0
  8. package/codemod/page-navigation.mjs +278 -0
  9. package/codemod/section-frame.d.mts +50 -0
  10. package/codemod/section-frame.mjs +483 -0
  11. package/codemod/small-renames.d.mts +54 -0
  12. package/codemod/small-renames.mjs +614 -0
  13. package/codemod/surface-card-kinds.d.mts +49 -0
  14. package/codemod/surface-card-kinds.mjs +404 -0
  15. package/codemod/theme-tokens.d.mts +45 -0
  16. package/codemod/theme-tokens.mjs +114 -0
  17. package/codemod/tier-binding.d.mts +50 -0
  18. package/codemod/tier-binding.mjs +214 -0
  19. package/package.json +1 -1
  20. package/runtime/knowledge/grammars/core/playbook.yaml +41 -51
  21. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +5 -5
  22. package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
  23. package/runtime/knowledge/hfs/rules.yaml +255 -30
  24. package/runtime/knowledge/hfs/slots.yaml +12 -5
  25. package/runtime/knowledge/patterns/fe/folder.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +23 -13
  27. package/runtime/scripts/hfs/architecture/clones.mjs +12 -2
  28. package/runtime/scripts/hfs/architecture/component-tiers.d.mts +46 -0
  29. package/runtime/scripts/hfs/architecture/component-tiers.mjs +161 -0
  30. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  31. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/frontend.mjs +2 -2
  33. package/runtime/scripts/hfs/architecture/index.mjs +6 -0
  34. package/runtime/scripts/hfs/architecture/render-graph.d.mts +125 -0
  35. package/runtime/scripts/hfs/architecture/render-graph.mjs +449 -0
  36. package/runtime/scripts/hfs/architecture/render-prop-across-server.d.mts +23 -0
  37. package/runtime/scripts/hfs/architecture/render-prop-across-server.mjs +58 -0
  38. package/runtime/scripts/hfs/architecture/surface-nesting.d.mts +44 -0
  39. package/runtime/scripts/hfs/architecture/surface-nesting.mjs +127 -0
  40. package/runtime/scripts/hfs/architecture/unit-spec-providers.d.mts +2 -1
  41. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +12 -5
  42. package/runtime/scripts/hfs/slot-manifest-shape.mjs +16 -1
  43. package/runtime/scripts/work/draw/draw-dna.mjs +1 -1
  44. package/scaffold/add-fe-layout.mjs +3 -3
  45. package/scaffold/add-fe-overlay.mjs +4 -4
  46. package/scaffold/add-fe-page.mjs +1 -1
  47. package/scaffold/add-fe-unit.d.mts +2 -1
  48. package/scaffold/add-fe-unit.mjs +14 -12
  49. package/scaffold/add.d.mts +1 -1
  50. package/scaffold/add.mjs +1 -1
  51. package/src/main.mjs +4 -4
  52. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +3 -4
  53. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +8 -5
  54. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +1 -3
  55. package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +3 -3
  56. package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +3 -5
  57. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -1
  58. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +1 -1
  59. package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +1 -2
  60. package/templates/fe/units/block/component.tsx.tpl +2 -3
  61. package/templates/fe/units/branch/index.tsx.tpl +4 -3
  62. package/templates/fe/units/composite/index.tsx.tpl +3 -3
  63. package/templates/fe/units/frame/index.tsx.tpl +10 -0
  64. package/templates/fe/units/layout/index.tsx.tpl +2 -2
  65. package/templates/fe/units/leaf/index.tsx.tpl +3 -3
  66. package/templates/fe/units/page/component.tsx.tpl +2 -2
  67. package/templates/fe/units/page/index.tsx.tpl +2 -2
  68. package/templates/fe/units/page/stories.tsx.tpl +2 -2
  69. package/templates/fe/units/slot/SlotView.tsx.tpl +11 -10
  70. package/templates/fe/units/slot/data-slot.ts.tpl +20 -8
  71. package/templates/fe/units/slot/slot.shared.ts.tpl +52 -11
  72. package/templates/fe/units/slot/useSlotOfReads.ts.tpl +19 -0
  73. /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/classNames.ts +0 -0
  74. /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/index.tsx +0 -0
  75. /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/classNames.ts +0 -0
  76. /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/index.tsx +0 -0
@@ -0,0 +1,127 @@
1
+ import { RenderGraph } from './render-graph.mjs';
2
+ /**
3
+ * R356 `surface-nesting` (FE_SURFACE_NESTING), the whole-program half of the one-file card laws (R340, R351, R354, R348, R366).
4
+ * ESLint judges one file; an app draws a screen across several, so the check walks the render graph (render-graph.mts) from every
5
+ * app component and reports, at the inner element, what stands wrongly inside what once every component is replaced by what it
6
+ * draws and every `children` by what its caller handed it:
7
+ *
8
+ * card-in-card a card inside a card (surface in surface is banned totally, owner decision 2026-11-01);
9
+ * card-in-pane a card inside `Pane surface`;
10
+ * card-in-overlay a card inside a Dialog, Drawer, AlertDialog or Popover;
11
+ * table-in-card a `DataTable` inside a card (a table stands on the page);
12
+ * grid-outside-panes a `PaneGrid` inside a card or a Page whose chrome is not `panes`;
13
+ * heading-over-card a `Heading`, or a region `Section`, whose only neighbour or child is one card (a card carries its own label);
14
+ * label-in-grid a labelled card repeated by a `.map` in a `Grid kind="cards"` (a repeated tile has no label).
15
+ *
16
+ * A finding carries `crossFile: true` when the two elements are drawn in different files, which is what one-file lint cannot see.
17
+ */
18
+ export const SURFACE_NESTING_RULE_IDS = ['FE_SURFACE_NESTING'];
19
+ const RULE = 'FE_SURFACE_NESTING';
20
+ /** The grammar vocabulary this check reads, in one place so a rename in the grammar is one edit here. */
21
+ export const SURFACE_VOCABULARY = Object.freeze({
22
+ cards: new Set(['SurfaceCard']),
23
+ overlays: new Set(['Dialog', 'Drawer', 'AlertDialog', 'Popover']),
24
+ tables: new Set(['DataTable']),
25
+ headings: new Set(['Heading']),
26
+ sections: new Set(['Section']),
27
+ paneGrids: new Set(['PaneGrid']),
28
+ pane: 'Pane',
29
+ page: 'Page',
30
+ grids: new Set(['Grid']),
31
+ });
32
+ const grammarName = (element) => (element.tag.kind === 'grammar' ? element.tag.name : null);
33
+ const isGrammarIn = (element, names) => {
34
+ const name = grammarName(element);
35
+ return name !== null && names.has(name);
36
+ };
37
+ const isTrue = (element, attribute) => {
38
+ const found = element.attributes.get(attribute);
39
+ return found !== undefined && found.text !== 'false' && found.text !== null;
40
+ };
41
+ /** The card an element is, or draws at its root through one app component: the one wrapper level the audit allows. */
42
+ const cardOf = (graph, element) => {
43
+ if (isGrammarIn(element, SURFACE_VOCABULARY.cards))
44
+ return element;
45
+ const root = element.tag.kind === 'component' ? graph.rootOf(element) : null;
46
+ return root && isGrammarIn(root, SURFACE_VOCABULARY.cards) ? root : null;
47
+ };
48
+ const describe = (element) => (element.tag.kind === 'component' ? `${element.tag.name} (${element.tag.file})` : element.tag.name);
49
+ const chainOf = (ancestors, stack) => `${ancestors.map(entry => entry.element.tag.name).join(' > ')}${stack.length > 1 ? ` via ${stack.map(entry => entry.split('#')[1]).join(' > ')}` : ''}`;
50
+ export function checkSurfaceNesting(input) {
51
+ const graph = new RenderGraph(input.context.ts, input.graph);
52
+ const found = new Map();
53
+ const report = (element, outer, kind, message, ancestors, stack) => {
54
+ const where = graph.locate(element);
55
+ const key = `${where.path}:${where.line}:${where.column}:${kind}`;
56
+ if (found.has(key))
57
+ return;
58
+ found.set(key, {
59
+ ruleId: RULE, ...where, kind, crossFile: outer !== null && outer.file !== element.file,
60
+ message: `${message} (${chainOf(ancestors, stack)}). A card carries its own label and stands on the page or on a canvas pane; a table stands on the page; a grid stands in Page chrome="panes" (contexts/ui/components/card.md).`,
61
+ });
62
+ };
63
+ const visitor = {
64
+ enter(element, ancestors, stack) {
65
+ const name = grammarName(element);
66
+ const nearestCard = [...ancestors].reverse().find(entry => isGrammarIn(entry.element, SURFACE_VOCABULARY.cards));
67
+ if (name !== null && SURFACE_VOCABULARY.cards.has(name)) {
68
+ const inPane = [...ancestors].reverse().find(entry => grammarName(entry.element) === SURFACE_VOCABULARY.pane && isTrue(entry.element, 'surface'));
69
+ const inOverlay = [...ancestors].reverse().find(entry => isGrammarIn(entry.element, SURFACE_VOCABULARY.overlays));
70
+ if (nearestCard)
71
+ report(element, nearestCard.element, 'card-in-card', `${name} is drawn inside ${describe(nearestCard.element)}: surface in surface is banned`, ancestors, stack);
72
+ else if (inPane)
73
+ report(element, inPane.element, 'card-in-pane', `${name} is drawn inside Pane surface`, ancestors, stack);
74
+ else if (inOverlay)
75
+ report(element, inOverlay.element, 'card-in-overlay', `${name} is drawn inside ${describe(inOverlay.element)}: an overlay is a surface already`, ancestors, stack);
76
+ }
77
+ if (name !== null && SURFACE_VOCABULARY.tables.has(name) && nearestCard) {
78
+ report(element, nearestCard.element, 'table-in-card', `${name} is drawn inside ${describe(nearestCard.element)}`, ancestors, stack);
79
+ }
80
+ if (name !== null && SURFACE_VOCABULARY.paneGrids.has(name)) {
81
+ const frame = [...ancestors].reverse().find(entry => {
82
+ const frameName = grammarName(entry.element);
83
+ return frameName === SURFACE_VOCABULARY.pane || frameName === SURFACE_VOCABULARY.page || SURFACE_VOCABULARY.cards.has(frameName ?? '');
84
+ });
85
+ const frameName = frame ? grammarName(frame.element) : null;
86
+ if (frame && frameName !== SURFACE_VOCABULARY.pane) {
87
+ const chrome = frame.element.attributes.get('chrome');
88
+ const open = frameName === SURFACE_VOCABULARY.page && (chrome === undefined ? hasSpread(frame.element) : chrome.text === null);
89
+ const panes = frameName === SURFACE_VOCABULARY.page && chrome?.text === 'panes';
90
+ if (!open && !panes)
91
+ report(element, frame.element, 'grid-outside-panes', `${name} is drawn inside ${describe(frame.element)}`, ancestors, stack);
92
+ }
93
+ }
94
+ if (element.repeated) {
95
+ const inGrid = [...ancestors].reverse().find(entry => isGrammarIn(entry.element, SURFACE_VOCABULARY.grids));
96
+ const card = inGrid?.element.attributes.get('kind')?.text === 'cards' ? cardOf(graph, element) : null;
97
+ if (inGrid && card && card.attributes.has('label')) {
98
+ report(element, inGrid.element, 'label-in-grid', `${describe(element)} repeats a labelled card in a Grid kind="cards": a repeated tile has no label`, ancestors, stack);
99
+ }
100
+ }
101
+ },
102
+ parent(element, drawn, ancestors, stack) {
103
+ if (!isGrammarIn(element, SURFACE_VOCABULARY.sections) || element.attributes.get('kind')?.text === 'list' || drawn.length !== 1)
104
+ return;
105
+ const only = drawn[0];
106
+ if (cardOf(graph, only))
107
+ report(only, element, 'heading-over-card', `${describe(only)} is the only thing in a Section: the card has two titles`, ancestors, stack);
108
+ },
109
+ siblings(elements, ancestors, stack) {
110
+ for (let index = 0; index < elements.length - 1; index += 1) {
111
+ const heading = elements[index];
112
+ if (!isGrammarIn(heading, SURFACE_VOCABULARY.headings))
113
+ continue;
114
+ const next = elements[index + 1];
115
+ const after = elements[index + 2];
116
+ if (!cardOf(graph, next) || (after && cardOf(graph, after)))
117
+ continue;
118
+ report(next, heading, 'heading-over-card', `${describe(next)} stands directly under a ${heading.tag.name}, so the card has two titles`, ancestors, stack);
119
+ }
120
+ },
121
+ };
122
+ const components = graph.allComponents();
123
+ for (const component of components)
124
+ graph.walkFrom(component, visitor);
125
+ return { violations: [...found.values()], coverage: { status: 'checked', components: components.length } };
126
+ }
127
+ const hasSpread = (element) => element.attributes.has('...');
@@ -4,7 +4,8 @@ import type { MachineContext } from './machine-ast.mjs';
4
4
  /**
5
5
  * R48 `unit-spec-providers` (BE_SPEC_QUALITY), the unit test standard's rule 3: the unit spec of a service builds its
6
6
  * subject with `Test.createTestingModule({ providers: [...] })`, and that providers array EQUALS the constructor
7
- * dependencies of the service under test: the service class first, then one provider for each constructor parameter.
7
+ * dependencies of the service under test: the service class first, then one provider for each required constructor parameter.
8
+ * A parameter decorated by `Optional()` imported from `@nestjs/common` may be omitted to exercise its absent-provider branch.
8
9
  *
9
10
  * The service under test is the sibling `<name>.service.ts` of every `<name>.service.spec.ts` under src/ or apps/ (a spec is
10
11
  * outside the production program, so it is read from disk). A parameter decorated with a custom `Inject<Thing>()` provides
@@ -9,7 +9,8 @@ import { checkFunctionSubject, functionSubjectExports, keyOf as literalKey, valu
9
9
  /**
10
10
  * R48 `unit-spec-providers` (BE_SPEC_QUALITY), the unit test standard's rule 3: the unit spec of a service builds its
11
11
  * subject with `Test.createTestingModule({ providers: [...] })`, and that providers array EQUALS the constructor
12
- * dependencies of the service under test: the service class first, then one provider for each constructor parameter.
12
+ * dependencies of the service under test: the service class first, then one provider for each required constructor parameter.
13
+ * A parameter decorated by `Optional()` imported from `@nestjs/common` may be omitted to exercise its absent-provider branch.
13
14
  *
14
15
  * The service under test is the sibling `<name>.service.ts` of every `<name>.service.spec.ts` under src/ or apps/ (a spec is
15
16
  * outside the production program, so it is read from disk). A parameter decorated with a custom `Inject<Thing>()` provides
@@ -80,6 +81,7 @@ function serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report
80
81
  }
81
82
  function checkDependencies(kit, rel, service, serviceFile, list, provided, report) {
82
83
  const { serviceName, serviceClass } = service;
84
+ const checker = kit.checkerOf(serviceFile.sourceFile);
83
85
  const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
84
86
  const expected = [];
85
87
  for (const dependency of dependencies) {
@@ -87,18 +89,23 @@ function checkDependencies(kit, rel, service, serviceFile, list, provided, repor
87
89
  report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
88
90
  continue;
89
91
  }
90
- expected.push(dependency.name);
92
+ const optional = kit.decorators(dependency.parameter).some(decorator => {
93
+ const expression = decorator.expression;
94
+ return kit.ts.isCallExpression(expression) && expression.arguments.length === 0
95
+ && kit.isImportOf(checker, expression.expression, 'Optional', '@nestjs/common');
96
+ });
97
+ expected.push({ name: dependency.name, optional });
91
98
  }
92
99
  const remaining = [...expected];
93
100
  for (const item of provided.slice(1)) {
94
- const index = remaining.indexOf(item.name);
101
+ const index = remaining.findIndex(dependency => dependency.name === item.name);
95
102
  if (index === -1)
96
103
  report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
97
104
  else
98
105
  remaining.splice(index, 1);
99
106
  }
100
- for (const name of remaining)
101
- report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
107
+ for (const dependency of remaining.filter(item => !item.optional))
108
+ report(list, `${rel}: ${dependency.name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${dependency.name}, useValue: double }\` (or the class itself) to the providers.`);
102
109
  }
103
110
  function checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report }) {
104
111
  const list = providerList(ts, call, rel, report);
@@ -49,13 +49,27 @@ function checkProfileMaps(m, bad) {
49
49
  continue;
50
50
  }
51
51
  for (const extra of Object.keys(m[key]))
52
- if (!isSide(extra))
52
+ if (!isSide(extra) && !(key === 'tiers' && extra === COMPONENT_LIBRARY))
53
53
  bad.push(`${key}.${extra} is not a profile`);
54
54
  for (const profile of PROFILES)
55
55
  if (!(profile in m[key]))
56
56
  bad.push(`${key}.${profile} is missing`);
57
57
  }
58
58
  }
59
+ /** The tier matrix of a component library (packages/grammar), beside the be and fe profiles: tiers.grammar. */
60
+ const COMPONENT_LIBRARY = 'grammar';
61
+ function checkComponentLibraryTiers(m, bad) {
62
+ const tiers = m.tiers?.[COMPONENT_LIBRARY];
63
+ if (tiers === undefined)
64
+ return;
65
+ bad.push(...tierMapProblems(COMPONENT_LIBRARY, tiers));
66
+ if (!isPlainObject(tiers))
67
+ return;
68
+ for (const [tier, def] of Object.entries(tiers))
69
+ for (const target of def?.mayImport ?? [])
70
+ if (!(target in tiers))
71
+ bad.push(`tiers.${COMPONENT_LIBRARY}.${tier}.mayImport names ${target}, which is not a ${COMPONENT_LIBRARY} tier`);
72
+ }
59
73
  function checkAppKindAndTierShapes(m, bad) {
60
74
  for (const profile of PROFILES) {
61
75
  const kinds = m.appKinds?.[profile];
@@ -66,6 +80,7 @@ function checkAppKindAndTierShapes(m, bad) {
66
80
  continue;
67
81
  bad.push(...tierMapProblems(profile, tiers));
68
82
  }
83
+ checkComponentLibraryTiers(m, bad);
69
84
  }
70
85
  function checkSlotShapes(m, bad) {
71
86
  if (!Array.isArray(m.slots) || !m.slots.length) {
@@ -348,7 +348,7 @@ export function proposalNamesIn(files) {
348
348
  const RATIO_RX = /(?<![\d./])(\d{1,4})\s*\/\s*(\d{1,4})(?![\d./])/;
349
349
  const BAR_CLASS = /(?:^|[-_])(bar|bars|track|fill|segment|segments|progress|meter|gauge)(?:$|[-_])/i;
350
350
  const NOTICE_CLASS = /(?:^|[-_])(notice|callout|banner|attention|alert|toast|announcement)(?:$|[-_])/i;
351
- const CONTAINERS = new Set(['SurfaceCard', 'SurfaceListCard', 'SurfaceAccordionCard', 'SurfaceCopyGroup', 'PageContainer', 'EmptyNotice', 'Fieldset']);
351
+ const CONTAINERS = new Set(['SurfaceCard', 'SurfaceCopyGroup', 'PageContainer', 'EmptyNotice', 'Fieldset']);
352
352
  const ACTION_COMPONENTS = new Set(['Button', 'IconButton', 'TextAction', 'Link', 'ButtonGroup']);
353
353
  const isAction = (el) => ACTION_COMPONENTS.has(componentNameOf(el)) || el.tag === 'button' || (el.tag === 'a' && el.attrs.href != null);
354
354
  // ---------------------------------------------------------------------------------------------------------
@@ -4,7 +4,7 @@
4
4
  // starci app add layout settings --blocks settings-nav-block the blocks fill the frame's rail
5
5
  //
6
6
  // The route file only mounts the layout owner (contexts/code/fe architecture.md FE-ARCHITECTURE-1). The owner is a server
7
- // component that composes a Grammar layout frame, `PrimaryRailLayout`: the routed page is its primary region and the `--blocks`
7
+ // component that composes a Grammar layout frame, `Grid kind="main-rail"`: the routed page is its primary region and the `--blocks`
8
8
  // (existing blocks of components/blocks) its rail. A frame places regions only and never insets a page or draws a page head;
9
9
  // `Page` owns both (contexts/ui/components page-header.md). The owner has no copy to resolve, so it has no pure half and no story:
10
10
  // the frame's look is the Grammar's and the regions are the page's and the blocks'. The bodies are templates/fe/units/layout/.
@@ -36,8 +36,8 @@ export function addFeLayout({ repoRoot, segment, options = {} }) {
36
36
  blockImports: parts.imports,
37
37
  railDoc: blocks.length === 0 ? 'leaves its rail empty until a block fills it' : `fills its rail with ${blocks.join(', ')}`,
38
38
  frame: blocks.length === 0
39
- ? '<PrimaryRailLayout primary={props.content} />'
40
- : ['(', ' <PrimaryRailLayout', ' primary={props.content}', ' rail={', ' <>', parts.jsx, ' </>', ' }', ' />', ')'].join('\n'),
39
+ ? '<Grid kind="main-rail" primary={props.content} />'
40
+ : ['(', ' <Grid', ' kind="main-rail"', ' primary={props.content}', ' rail={', ' <>', parts.jsx, ' </>', ' }', ' />', ')'].join('\n'),
41
41
  };
42
42
  const created = [];
43
43
  writeNew(created, repoRoot, route, fill('layout/route-layout.tsx.tpl', vars));
@@ -7,7 +7,7 @@
7
7
  // dialog-drawer.md). The connected half (`"use client"`, an overlay entry of FE-BOUNDARY-2) owns whether it is open and resolves
8
8
  // every label; the pure half takes `{ props, on }` of atoms (FE-FUNCTION-2). The decision group puts the way out first and the
9
9
  // commit last (action-group.md `decision`, CTA-5), always in the shell's footer. With `--form`, the body holds a Form with an `id`
10
- // whose fields sit in a one-column `FormGrid` (text-field.md `narrow`), and the footer's commit is `type="submit"` with
10
+ // whose fields sit in a one-column `Grid kind="form"` (text-field.md `narrow`), and the footer's commit is `type="submit"` with
11
11
  // `form="<id>"`, so it submits that form from outside it. The bodies are templates/fe/units/overlay/; the stories draw it closed and open. A page or layout owner mounts it.
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
@@ -42,7 +42,7 @@ function overlayVars({ nameKebab, shell, form }) {
42
42
  if (form) {
43
43
  const formId = `${nameKebab}-form`;
44
44
  return {
45
- grammarImports: ['Button', shell, 'Form', 'FormGrid', 'Input'].sort().join(', '),
45
+ grammarImports: ['Button', shell, 'Form', 'Grid', 'Input'].sort().join(', '),
46
46
  labelMembers: ' readonly nameLabel: string\n',
47
47
  labelsResolved: ' nameLabel: t("name"),\n',
48
48
  labelsFixture: ' nameLabel: "Name",\n',
@@ -54,9 +54,9 @@ function overlayVars({ nameKebab, shell, form }) {
54
54
  footer: footer(`<Button type="submit" form="${formId}" intent="commit">`),
55
55
  body: [
56
56
  ` <Form id="${formId}" label={props.props.title} onSubmit={props.on.submit}>`,
57
- ' <FormGrid columns={1}>',
57
+ ' <Grid kind="form" columns={1}>',
58
58
  ` <Input id="${nameKebab}-name" name="name" label={props.props.nameLabel} isRequired />`,
59
- ' </FormGrid>',
59
+ ' </Grid>',
60
60
  ' </Form>',
61
61
  ].join('\n'),
62
62
  };
@@ -11,7 +11,7 @@
11
11
  // `--tabs`, the header carries the page tabs as link tabs (contexts/ui/components tabs.md `level:page`): the first tab is the route
12
12
  // itself and every other one its sub-route `<route>/<tab>`, which the author adds as its own page; Grammar selects the tab of the
13
13
  // current path, so the server owner passes no state and no handler. The
14
- // owner also exports the route's loading view, the same page drawn with `isSkeleton` (contexts/ui composition.md STATE-8), and
14
+ // owner also exports the route's loading view, the same page drawn with `isLoading` (contexts/ui composition.md STATE-8), and
15
15
  // `<Name>ErrorPage` keeps the page and puts one negative Alert where the first region would be. The bodies are
16
16
  // templates/fe/units/page/; the stories pair the page with its loading view for the DNA gate.
17
17
  import fs from 'node:fs';
@@ -10,7 +10,7 @@ export interface FeDeclaration {
10
10
  };
11
11
  };
12
12
  }
13
- /** The options of `add block|composite|branch|leaf`. */
13
+ /** The options of `add block|frame|composite|branch|leaf`. */
14
14
  export interface FeUnitOptions {
15
15
  readonly app?: string | undefined;
16
16
  readonly shapes?: string | readonly string[] | false | undefined;
@@ -19,6 +19,7 @@ export interface FeUnitOptions {
19
19
  /** The nouns this module answers, and the components layer each one writes into. */
20
20
  export declare const FE_UNIT_LAYERS: Readonly<{
21
21
  block: "blocks";
22
+ frame: "frames";
22
23
  composite: "composites";
23
24
  branch: "branches";
24
25
  leaf: "leaves";
@@ -1,18 +1,18 @@
1
- // starci app add <block|composite|branch|leaf> <name> - one front-end unit, already in the shape-slot pattern.
1
+ // starci app add <block|frame|composite|branch|leaf> <name> - one front-end unit, already in the shape-slot pattern.
2
2
  //
3
3
  // The bodies are templates/fe/units/<tier>/; this module only fills them and places them (contexts/code/fe function.md
4
4
  // FE-FUNCTION-2..4 and FE-FUNCTION-7, README.md FE-ERROR-1).
5
5
  //
6
6
  // starci app add block handoff-block HandoffBlock: one read folded into a DataSlot, no shape
7
7
  // starci app add block handoff-block --shapes prepared,sent,returned plus HANDOFF_BLOCK_SHAPES, HandoffBlockShape and an exhaustive switch
8
- // starci app add composite term-value | branch disclosure | leaf status-label
8
+ // starci app add frame page-body | composite term-value | branch disclosure | leaf status-label
9
9
  //
10
10
  // A block reads `useQuery<Read>Swr` of hooks/swr (`--read`, default: the block name without its `Block` suffix; `add table --fe`
11
11
  // writes that hook). A block with shapes takes its shape from the read's `status` (the placeholder's own while the read loads), so
12
12
  // the read names its shape in that field. The first block of an app also writes the app's one copy of the status owners
13
- // (FE-ERROR-1): DataSlot and SlotLabels in modules/types, slotOfSwr/mapSlot/loadingSlot in hooks/slot/slot.shared.ts,
13
+ // (FE-ERROR-1): DataSlot and SlotLabels in modules/types, slotOfSwr/slotOfReads/mapSlot/loadingSlot in hooks/slot/slot.shared.ts (and useSlotOfReads),
14
14
  // useSlotLabels, the SlotView composite and the shared `status` namespace of every catalog. Nothing that exists is overwritten.
15
- // A composite, branch or leaf, SlotView among them, is vocabulary that knows no feature: in a front end of two or more apps with
15
+ // A frame, composite, branch or leaf, SlotView among them, is vocabulary that knows no feature: in a front end of two or more apps with
16
16
  // the shared ui package (slot fe.package.ui) it lives in that package and its barrel, DataSlot and SlotLabels are declared beside
17
17
  // SlotView there and modules/types re-exports them (R91 `monorepo-tier-belongs-to-its-side`); else it lives in the app's components.
18
18
  // A shape that names a data status is refused by the lint (R92 `no-data-status-shape`), which owns that word list.
@@ -23,7 +23,7 @@ import { ScaffoldError, camelOf, pascalOf } from './service.mjs';
23
23
  import { coreFeApps } from './app.mjs';
24
24
  const UNITS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates', 'fe', 'units');
25
25
  /** The nouns this module answers, and the components layer each one writes into. */
26
- export const FE_UNIT_LAYERS = Object.freeze({ block: 'blocks', composite: 'composites', branch: 'branches', leaf: 'leaves' });
26
+ export const FE_UNIT_LAYERS = Object.freeze({ block: 'blocks', frame: 'frames', composite: 'composites', branch: 'branches', leaf: 'leaves' });
27
27
  /** The shared status copy of FE-ERROR-1, by catalog locale; any other locale takes the English sentences. */
28
28
  const STATUS_COPY = Object.freeze({
29
29
  en: { forbidden: 'You do not have access to this.', error: 'This could not be loaded.', retry: 'Try again' },
@@ -133,7 +133,9 @@ function ensureSlotOwners(created, { repoRoot, appBase, ui }) {
133
133
  }
134
134
  writeOnce(created, repoRoot, `${appBase}/src/hooks/slot/slot.shared.ts`, fill('slot/slot.shared.ts.tpl', {}));
135
135
  writeOnce(created, repoRoot, `${appBase}/src/hooks/slot/useSlotLabels.ts`, fill('slot/useSlotLabels.ts.tpl', {}));
136
- exportFrom(created, repoRoot, `${appBase}/src/hooks/slot/index.ts`, 'export { loadingSlot, mapSlot, slotOfSwr } from "./slot.shared"');
136
+ writeOnce(created, repoRoot, `${appBase}/src/hooks/slot/useSlotOfReads.ts`, fill('slot/useSlotOfReads.ts.tpl', {}));
137
+ exportFrom(created, repoRoot, `${appBase}/src/hooks/slot/index.ts`, 'export { loadingSlot, mapSlot, slotOfReads, slotOfSwr } from "./slot.shared"');
138
+ exportFrom(created, repoRoot, `${appBase}/src/hooks/slot/index.ts`, 'export { useSlotOfReads } from "./useSlotOfReads"');
137
139
  exportFrom(created, repoRoot, `${appBase}/src/hooks/slot/index.ts`, 'export { useSlotLabels } from "./useSlotLabels"');
138
140
  }
139
141
  /**
@@ -173,8 +175,8 @@ function shapeParts({ Name, NAME_UPPER, shapes }) {
173
175
  ` case "${shape}":`,
174
176
  ' return (',
175
177
  ' <>',
176
- ` <Text isSkeleton={isSkeleton}>{shapeLabels.${shape}}</Text>`,
177
- ' <Text isSkeleton={isSkeleton}>{item.id}</Text>',
178
+ ` <Text isLoading={isLoading}>{shapeLabels.${shape}}</Text>`,
179
+ ' <Text isLoading={isLoading}>{item.id}</Text>',
178
180
  ' </>',
179
181
  ' )',
180
182
  ].join('\n')).join('\n');
@@ -191,7 +193,7 @@ function shapeParts({ Name, NAME_UPPER, shapes }) {
191
193
  shapeBody: [
192
194
  '',
193
195
  `/** The ready tree of one drawn shape: one case per member of ${Name}Shape, so a member nobody draws fails the lint. */`,
194
- `const readyTreeOf = (shape: ${Name}Shape, shapeLabels: ${Name}BaseProps["props"]["shapeLabels"], item: ${Name}Item, isSkeleton: boolean) => {`,
196
+ `const readyTreeOf = (shape: ${Name}Shape, shapeLabels: ${Name}BaseProps["props"]["shapeLabels"], item: ${Name}Item, isLoading: boolean) => {`,
195
197
  ' switch (shape) {',
196
198
  cases,
197
199
  ' }',
@@ -223,12 +225,12 @@ function blockVars({ Name, nameCamel, nameKebab, Read, shapes, title, ui }) {
223
225
  shapeLabelsMember: shaped ? ` readonly shapeLabels: { ${shapes.map((shape) => `readonly ${shape}: string`).join('; ')} }\n` : '',
224
226
  placeholderShapeDoc: shaped ? ', and its shape is the block\'s while the read loads' : '',
225
227
  placeholderShape: shaped ? `, shape: "${shapes[0]}"` : '',
226
- readyTree: shaped ? 'readyTreeOf(props.shape, props.props.shapeLabels, item, isSkeleton)' : '<Text isSkeleton={isSkeleton}>{item.id}</Text>',
228
+ readyTree: shaped ? 'readyTreeOf(props.shape, props.props.shapeLabels, item, isLoading)' : '<Text isLoading={isLoading}>{item.id}</Text>',
227
229
  componentImports: shaped ? `${NAME_UPPER}_PLACEHOLDER, ${NAME_UPPER}_SHAPES, ${Name}Base` : `${Name}Base`,
228
230
  componentTypeImport: shaped ? `import type { ${Name}Shape } from "./component"\n` : '',
229
231
  connectedDoc: shaped ? 'takes its shape from the read (the placeholder\'s while it loads), ' : '',
230
232
  itemShapeMapped: shaped ? ', shape: shapeOf(value.status)' : '',
231
- shapeAttribute: shaped ? ` shape={(item.items ?? ${NAME_UPPER}_PLACEHOLDER).shape}\n` : '',
233
+ shapeAttribute: shaped ? ` shape={("items" in item ? item.items : ${NAME_UPPER}_PLACEHOLDER).shape}\n` : '',
232
234
  shapeLabelsResolved: shaped ? ` shapeLabels: { ${shapes.map((shape) => `${shape}: t("shapes.${shape}")`).join(', ')} },\n` : '',
233
235
  shapeLabelsFixture: shaped ? ` shapeLabels: { ${shapes.map((shape) => `${shape}: "${humanOf(shape)}"`).join(', ')} },\n` : '',
234
236
  fixtureShape: shaped ? `, shape: "${shapes[0]}"` : '',
@@ -256,7 +258,7 @@ function addBlock({ created, place, names, options }) {
256
258
  writeNew(created, place.repoRoot, stories, fill('block/stories.tsx.tpl', vars));
257
259
  addCatalogKeys(created, place, { nameCamel: names.nameCamel, title: humanOf(names.Name), shapes });
258
260
  }
259
- /** Writes one composite, branch or leaf: into the shared ui package and its barrel, or into the app's components. */
261
+ /** Writes one frame, composite, branch or leaf: into the shared ui package and its barrel, or into the app's components. */
260
262
  function addVocabulary({ created, place, noun, Name }) {
261
263
  const layer = FE_UNIT_LAYERS[noun];
262
264
  const body = fill(`${noun}/index.tsx.tpl`, { Name });
@@ -22,7 +22,7 @@ export interface AddResult {
22
22
  /**
23
23
  * Generates the tree of one noun in the app at `repoRoot` (the folder of hfs.json).
24
24
  *
25
- * @param {{ repoRoot: string, noun: string, name: string, options?: object, now?: () => number }} input - The app root, the noun (job, reactor, queue, projection, ..., or a front-end unit: block, composite, branch, leaf, page, overlay, layout), the instance name and the options the noun needs.
25
+ * @param {{ repoRoot: string, noun: string, name: string, options?: object, now?: () => number }} input - The app root, the noun (job, reactor, queue, projection, ..., or a front-end unit: block, frame, composite, branch, leaf, page, overlay, layout), the instance name and the options the noun needs.
26
26
  * @returns {{ created: Array<string>, registered: { patterns: Array<string>, kinds: Array<string> } }} The files written (app-root relative) and what hfs.json gained.
27
27
  */
28
28
  export declare function addKind({ repoRoot, noun, name, options, now }: {
package/scaffold/add.mjs CHANGED
@@ -45,7 +45,7 @@ const FE_OWNERS = Object.freeze({
45
45
  /**
46
46
  * Generates the tree of one noun in the app at `repoRoot` (the folder of hfs.json).
47
47
  *
48
- * @param {{ repoRoot: string, noun: string, name: string, options?: object, now?: () => number }} input - The app root, the noun (job, reactor, queue, projection, ..., or a front-end unit: block, composite, branch, leaf, page, overlay, layout), the instance name and the options the noun needs.
48
+ * @param {{ repoRoot: string, noun: string, name: string, options?: object, now?: () => number }} input - The app root, the noun (job, reactor, queue, projection, ..., or a front-end unit: block, frame, composite, branch, leaf, page, overlay, layout), the instance name and the options the noun needs.
49
49
  * @returns {{ created: Array<string>, registered: { patterns: Array<string>, kinds: Array<string> } }} The files written (app-root relative) and what hfs.json gained.
50
50
  */
51
51
  export function addKind({ repoRoot, noun, name, options = {}, now = Date.now }) {
package/src/main.mjs CHANGED
@@ -72,7 +72,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
72
72
  // exactly that kind's file tree, generated FROM the files: tree of its pattern topic (knowledge/patterns/be) with the
73
73
  // one template body of each entry (templates/be/patterns), plus the platform capabilities it needs when they are missing;
74
74
  // it registers the patterns and the trigger kind in hfs.json (sides.be.patterns, sides.be.kinds). Never overwrites a file (scaffold/add.mjs). In a lite app a noun whose slots lite does not have refuses: `add <noun>: full edition only`.
75
- // starci app add <block|composite|branch|leaf> <name> [--shapes a,b,c] [--read <read>] [--app <fe app>] [--cwd <dir>]
75
+ // starci app add <block|frame|composite|branch|leaf> <name> [--shapes a,b,c] [--read <read>] [--app <fe app>] [--cwd <dir>]
76
76
  // starci app add <page|layout> <route> [--blocks a,b] [--tabs a,b (page)] | overlay <name> --kind modal|drawer [--form] a route page, segment layout
77
77
  // or overlay owner (scaffold/add-fe-page.mjs, add-fe-layout.mjs, add-fe-overlay.mjs).
78
78
  // one front-end unit in the shape-slot pattern from templates/fe/units (scaffold/add-fe-unit.mjs); the
@@ -129,7 +129,7 @@ import { BOOLEAN as CI_BOOLEAN, COMMANDS as CI_COMMANDS } from '../ci/cli.mjs';
129
129
  import { LOCAL_FLAGS as CI_LOCAL_FLAGS } from '../ci/local.mjs';
130
130
  import { USAGE as COVERAGE_USAGE } from '../coverage/cli.mjs';
131
131
  import { SYNC_USAGE } from '../sync/cli.mjs';
132
- import { codemodMain } from '../codemod/main.mjs';
132
+ import { CODEMODS, codemodMain } from '../codemod/main.mjs';
133
133
  /** A value flag as parse stores it: the string given, else undefined. */
134
134
  const valueFlag = (opts, flag) => { const given = opts[flag]; return typeof given === 'string' ? given : undefined; };
135
135
  /** A boolean flag as parse stores it: true when given, else undefined. */
@@ -152,7 +152,7 @@ const PARSED = {
152
152
  add: {
153
153
  run: addMain, ...ADD_FLAGS, list: ['--inject'],
154
154
  forms: [
155
- { operands: '<api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app|block|composite|branch|leaf|page|overlay|layout> <name>', only: ADD_FLAGS },
155
+ { operands: '<api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app|block|frame|composite|branch|leaf|page|overlay|layout> <name>', only: ADD_FLAGS },
156
156
  { operands: 'service <dir> <name>', only: { list: ['--inject'] } },
157
157
  { operands: 'spec <file>.service.ts', flags: [] },
158
158
  { operands: 'image', flags: [] },
@@ -192,7 +192,7 @@ const DATABASE_USAGE = [['new', '<kebab-name>', DB_NEW_FLAGS], ['baseline', '',
192
192
  const VERB_USAGE = {
193
193
  ...Object.fromEntries(Object.entries(PARSED).map(([verb, spec]) => [verb, spec.forms.map((form) => line(`starci app ${verb}`, form.operands, specUsage(form.only ?? (form.flags === undefined ? spec : { value: form.flags })), spec.edition && form.edition !== false ? flagUsage('--edition', { value: true }) : ''))])),
194
194
  lint: ['starci app lint [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--full] [--full] [--json] [--out <file>]'],
195
- codemod: ['starci app codemod button-intent [--execute] [--json] [--format json|markdown] [--out <file.json|file.md>]', 'starci app codemod spacing-roles [--execute] [--json] [--format json|markdown] [--out <file.json|file.md>]'],
195
+ codemod: CODEMODS.map((name) => `starci app codemod ${name} [--execute] [--json] [--format json|markdown] [--out <file.json|file.md>]`),
196
196
  upgrade: ['starci app upgrade [--execute]'],
197
197
  sync: [SYNC_USAGE.sync],
198
198
  world: ['starci app world <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--edition full|lite]'],
@@ -1,4 +1,4 @@
1
- import { Button, Form, Input, SectionHeader, Text } from "@starci/grammar/common"
1
+ import { Button, Form, Input, Section, Text } from "@starci/grammar/common"
2
2
 
3
3
  /** Copy, the pending flag, the first error message and the action supplied by the connected sign-in block. */
4
4
  export interface SignInFormBaseProps {
@@ -17,8 +17,7 @@ export interface SignInFormBaseProps {
17
17
 
18
18
  /** Draws the sign-in form without reading navigation, copy or database state. */
19
19
  export const SignInFormBase = (props: SignInFormBaseProps) => (
20
- <>
21
- <SectionHeader title={props.props.title} level={2} />
20
+ <Section title={props.props.title}>
22
21
  <Form label={props.props.title} isPending={props.props.pending} onSubmit={props.on.submit}>
23
22
  <Input id="sign-in-email" name="email" label={props.props.emailLabel} kind="email" isRequired />
24
23
  <Input id="sign-in-password" name="password" label={props.props.passwordLabel} kind="password" isRequired />
@@ -27,5 +26,5 @@ export const SignInFormBase = (props: SignInFormBaseProps) => (
27
26
  {props.props.submitLabel}
28
27
  </Button>
29
28
  </Form>
30
- </>
29
+ </Section>
31
30
  )
@@ -15,11 +15,14 @@ export const generateMetadata = async (props: LayoutProps) => appLayoutMetadata(
15
15
 
16
16
  /** The locale segment's layout slot: the document of the address's language, the app's client providers and its chrome. */
17
17
  const Layout = async (props: LayoutProps) => (
18
- <LocaleShell {...await appI18n.readLocaleSegment(props.params)}>
19
- <AppProviders>
20
- <AppLayout content={props.children} />
21
- </AppProviders>
22
- </LocaleShell>
18
+ <LocaleShell
19
+ {...await appI18n.readLocaleSegment(props.params)}
20
+ content={
21
+ <AppProviders>
22
+ <AppLayout content={props.children} />
23
+ </AppProviders>
24
+ }
25
+ />
23
26
  )
24
27
 
25
28
  export default Layout
@@ -14,9 +14,7 @@ export const generateMetadata = async (props: LayoutProps) => landingLayoutMetad
14
14
 
15
15
  /** The locale segment's layout slot: the document of the address's language around the landing's chrome. */
16
16
  const Layout = async (props: LayoutProps) => (
17
- <LocaleShell {...await landingI18n.readLocaleSegment(props.params)}>
18
- <LandingLayout content={props.children} />
19
- </LocaleShell>
17
+ <LocaleShell {...await landingI18n.readLocaleSegment(props.params)} content={<LandingLayout content={props.children} />} />
20
18
  )
21
19
 
22
20
  export default Layout
@@ -4,12 +4,12 @@ import { I18nProvider } from "@heroui/react"
4
4
  import { NextIntlClientProvider } from "next-intl"
5
5
  import type { ReactNode } from "react"
6
6
 
7
- /** What the provider stack takes: the locale resolved on the server, its catalog, the product time zone and the tree it wraps. */
7
+ /** What the provider stack takes: the locale resolved on the server, its catalog, the product time zone and the tree it wraps (`content`). */
8
8
  type ProvidersProps = {
9
9
  readonly locale: string
10
10
  readonly messages: Record<string, unknown>
11
11
  readonly timeZone: string
12
- readonly children: ReactNode
12
+ readonly content: ReactNode
13
13
  }
14
14
 
15
15
  /**
@@ -18,6 +18,6 @@ type ProvidersProps = {
18
18
  */
19
19
  export const Providers = (props: ProvidersProps) => (
20
20
  <NextIntlClientProvider locale={props.locale} messages={props.messages} timeZone={props.timeZone}>
21
- <I18nProvider locale={props.locale}>{props.children}</I18nProvider>
21
+ <I18nProvider locale={props.locale}>{props.content}</I18nProvider>
22
22
  </NextIntlClientProvider>
23
23
  )
@@ -6,16 +6,14 @@ type LocaleShellProps = {
6
6
  readonly locale: string
7
7
  readonly messages: Record<string, unknown>
8
8
  readonly timeZone: string
9
- readonly children: ReactNode
9
+ readonly content: ReactNode
10
10
  }
11
11
 
12
- /** The document of one route language: `<html lang>` from the address and the provider stack; the app's own chrome is the children. */
12
+ /** The document of one route language: `<html lang>` from the address and the provider stack; the app's own chrome is the content. */
13
13
  export const LocaleShell = (props: LocaleShellProps) => (
14
14
  <html lang={props.locale}>
15
15
  <body>
16
- <Providers locale={props.locale} messages={props.messages} timeZone={props.timeZone}>
17
- {props.children}
18
- </Providers>
16
+ <Providers locale={props.locale} messages={props.messages} timeZone={props.timeZone} content={props.content} />
19
17
  </body>
20
18
  </html>
21
19
  )
@@ -3,5 +3,5 @@ export { GlobalErrorNotice } from "./composites/GlobalErrorNotice"
3
3
  export { LoadingNotice } from "./composites/LoadingNotice"
4
4
  export { LocaleShell } from "./composites/LocaleShell"
5
5
  export { NotFoundNotice } from "./composites/NotFoundNotice"
6
- export { SiteShell } from "./composites/SiteShell"
6
+ export { SiteShell } from "./frames/SiteShell"
7
7
  export { Icon } from "./leaves/Icon"
@@ -1,7 +1,7 @@
1
1
  import type { Metadata } from "next"
2
2
  import { getTranslations } from "next-intl/server"
3
3
  import type { ReactNode } from "react"
4
- import { SiteShell } from "@/components/composites/SiteShell"
4
+ import { SiteShell } from "@/components/frames/SiteShell"
5
5
  import { siteUrl } from "@/modules/config"
6
6
  import { APP_ROUTES } from "@/modules/routes"
7
7
 
@@ -2,8 +2,7 @@
2
2
  :root,
3
3
  .light,
4
4
  .dark {
5
- --starci-core-accent: #0d9488;
6
- --accent: var(--starci-core-accent);
5
+ --accent: #0d9488;
7
6
  --accent-foreground: #051817;
8
7
  --font-sans: ui-sans-serif, system-ui, sans-serif;
9
8
  }
@@ -28,8 +28,7 @@ export const {{Name}}Base = (props: {{Name}}BaseProps) => (
28
28
  placeholder={{{NAME_UPPER}}_PLACEHOLDER}
29
29
  labels={props.props.slotLabels}
30
30
  onRetry={props.on.retry}
31
- >
32
- {(item, isSkeleton) => {{readyTree}}}
33
- </SlotView>
31
+ render={(item, isLoading) => {{readyTree}}}
32
+ />
34
33
  </SurfaceCard>
35
34
  )