Skip to content

Commit 5bed1f6

Browse files
feat(sdui-parser): port objectui's one base-prop list into the save gate (#20799)
Fixes #20578 Clause-②: yes (widening) ## What Ports objectui's one base-prop list into the save gate, at the console pin `db11afd4967c` (objectui#11008 for `bind`/`hidden`, objectui#11044 for the single list). objectui anchors: `packages/sdui-parser/src/validate.ts:137` (`SDUI_BASE_PROPS`, docblock `:60-136`, skip `:213`) and `packages/sdui-parser/src/codegen.ts:13-32`, `:95-110`. - `validate.ts`: `BASE_PROPS` is now the `every-node` members of a 20-entry list: the 9 existing plus `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId`. Six `where-undeclared` members (`name`, `label`, `description`, `placeholder`, `data`, `ariaLabel`) skip `unknown-prop` only when the type declares no input of that name; a declared one keeps its `type-mismatch` check. - `codegen.ts`: `generateDts` emitted a hand-kept `export interface SduiBaseProps` (public through `index.ts`). It is now emitted from the same list, and a component that declares a base attribute `Omit`s it from the base it extends. This is the second consumer of the list, so it is exported from `validate.ts` for `codegen.ts` and is not re-exported from `index.ts`. - Correction: the first revision of this PR (and its report) claimed `codegen.ts` emits no base-props interface. That was false; the second commit fixes the code and the `validate.ts` LOCKSTEP paragraph. - Existing test `html-tier-manifest.test.ts` now expects `AProps extends Omit<SduiBaseProps, "className">`, the same string objectui pins in `html-tier-manifest-10735.test.ts:114`. ## Clause-② measurement Scanned every component's `inputs` in the tracked `sdui.manifest.json` (107 components). - Newly `every-node`: `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId` are declared by 0 components each, so no declared type check is removed. - `where-undeclared`: `name` 3, `label` 10, `description` 6, `data` 7, `placeholder` 0, `ariaLabel` 0 declaring components. The skip is guarded by `!byName.has(key)`, so a declared key takes the same path and severity as before. - Validator: for this manifest, undeclared uses lose a `unknown-prop` warning; no error-severity `validateTree` diagnostic is removed. - Generated types: `SduiBaseProps` goes from 8 members to 19, so a `.tsx` page authoring `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId`, `name`, `label`, `description`, `placeholder`, `data` or `ariaLabel` on a tag that does not declare it was a TypeScript error against `generateDts` output before and compiles now. That widens the accepted authoring surface and the published type output, hence `yes (widening)` and a `minor` changeset. ## Tests - New `base-props-one-list.test.ts`: each of the 13 attribute `every-node` members draws no `unknown-prop`; the list is pinned entry by entry (20 entries, `type` alone with no attribute type); each of the 6 `where-undeclared` keys draws nothing when undeclared; a declared `label` given a number still draws `type-mismatch`; a non-base key is still `unknown-prop`; the emitted `SduiBaseProps` equals the list's attribute entries in order; a component with no declared base attribute extends the whole base and one declaring `label` extends `Omit<SduiBaseProps, "label">`. `type` is not tested through `validateTree`: on a node it is the tag itself, so any such case passes through `unknown-component`. - `pnpm --filter @objectstack/sdui-parser exec vitest run`: 13 files, 224 tests passed; `typecheck` exit 0 (`os-verify-lock`, `VERDICT command-exit 0`). - No generated `.d.ts` or golden file for this output exists in the repo outside `packages/sdui-parser/` (grep for `SduiBaseProps` and `generateDts`). ## Gates (head `5cb34e77dc`) `dispatch-gates --commands` derived 61 families; 56 ran and exited 0, including `check:sdui-lockstep`, `check:published-files`, `check:query-options-erasure`, `check:type-check-coverage`, `check-changeset-no-major`, `check:doc-authoring`, `check:nul-bytes`. NOT MEASURED, exit 3 PREREQUISITE NOT MET (need a built `dist/`, left to CI): `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt`. The earlier body's count of 52 was wrong. ## Acceptance notes None outside the card. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfa9315 commit 5bed1f6

5 files changed

Lines changed: 253 additions & 28 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/sdui-parser': minor
3+
---
4+
5+
`@objectstack/sdui-parser` now reads one base-prop list, ported from objectui's `SDUI_BASE_PROPS` at the console pin `db11afd4967c` (objectui#11008, #11044). Both `validateTree` and the generated JSX types (`generateDts`'s `SduiBaseProps`) are driven by it.
6+
7+
- On every node, whatever the component declares: `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId` are newly accepted. They no longer draw `unknown-prop`, and the generated types accept them as attributes.
8+
- Only on a type whose registration declares no input of that name: `name`, `label`, `description`, `placeholder`, `data`, `ariaLabel` are newly accepted. A type that declares one keeps its declared type check and its declared attribute type; its generated interface `Omit`s that key from `SduiBaseProps`.
9+
10+
Effect for consumers: `os validate` stops warning `unknown-prop` on those keys, and a `.tsx` page that authors them now type-checks against `generateDts` output where it was a TypeScript error before. Measured on the tracked `sdui.manifest.json` (107 components), no component declares any of the five every-node keys, and every declared where-undeclared key is checked as before, so no diagnostic of error severity is removed for that manifest. The wider type surface is why this is a minor, not a patch.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* The base-prop list, ported in lockstep from objectui (objectui#11008 added
3+
* `bind` / `hidden`; objectui#11044 made ONE list with an `every-node` and a
4+
* `where-undeclared` scope) at the console pin `db11afd4967c`.
5+
*
6+
* Three facts, kept apart:
7+
* 1. every `every-node` member draws no `unknown-prop`;
8+
* 2. a `where-undeclared` member draws none where the type declares no input
9+
* of that name;
10+
* 3. a `where-undeclared` member the type DECLARES keeps its declared type
11+
* check (the reason it is not `every-node`).
12+
* Measured on the tracked `sdui.manifest.json`: no component declares any of
13+
* the five newly-`every-node` keys, so no declared type check moves.
14+
*/
15+
import { describe, expect, it } from 'vitest';
16+
import { generateDts, manifestFromConfigs, validateTree } from '../index.js';
17+
import { SDUI_BASE_PROPS } from '../validate.js';
18+
import type { Manifest, SchemaElement } from '../types.js';
19+
20+
const manifest: Manifest = manifestFromConfigs([
21+
{ type: 'leaf', namespace: 'ui', inputs: [{ name: 'content', type: 'string' }] },
22+
// `label` declared as a string, the `badge` / `button` shape
23+
{ type: 'labelled', namespace: 'ui', inputs: [{ name: 'label', type: 'string' }] },
24+
]);
25+
26+
const codes = (node: SchemaElement): string[] =>
27+
validateTree(node, manifest).diagnostics.map((d) => d.code);
28+
29+
const EVERY_NODE = [
30+
'id', 'className', 'style', 'visible', 'visibleWhen', 'visibleOn', 'hidden',
31+
'hiddenOn', 'disabled', 'disabledOn', 'bind', 'testId', 'children',
32+
];
33+
// `type` is the one `every-node` entry with no attribute form: on a node it
34+
// IS the tag, so a node cannot carry it without being the component under test.
35+
// It is pinned through the list below, not through `validateTree`.
36+
const WHERE_UNDECLARED = ['name', 'label', 'description', 'placeholder', 'data', 'ariaLabel'];
37+
38+
describe('base props — one list, two scopes (objectui#11008, #11044)', () => {
39+
it.each(EVERY_NODE)('every-node `%s` draws no unknown-prop', (key) => {
40+
const node = { type: 'leaf', [key]: key === 'children' ? [] : 'x' } as SchemaElement;
41+
expect(codes(node)).not.toContain('unknown-prop');
42+
});
43+
44+
it.each(WHERE_UNDECLARED)('undeclared `%s` draws no unknown-prop', (key) => {
45+
expect(codes({ type: 'leaf', [key]: 'x' } as SchemaElement)).toEqual([]);
46+
});
47+
48+
it('a DECLARED where-undeclared key keeps its type check', () => {
49+
const diags = validateTree({ type: 'labelled', label: 5 } as SchemaElement, manifest).diagnostics;
50+
expect(diags.map((d) => d.code)).toEqual(['type-mismatch']);
51+
expect(validateTree({ type: 'labelled', label: 'ok' } as SchemaElement, manifest).diagnostics).toEqual([]);
52+
});
53+
54+
it('the list is the 20 entries, `type` alone having no attribute type', () => {
55+
expect(SDUI_BASE_PROPS.map((p) => p.name)).toEqual(['type', ...EVERY_NODE, ...WHERE_UNDECLARED]);
56+
expect(SDUI_BASE_PROPS.filter((p) => p.tsType === null).map((p) => p.name)).toEqual(['type']);
57+
expect(SDUI_BASE_PROPS.filter((p) => p.scope === 'every-node').map((p) => p.name)).toEqual(['type', ...EVERY_NODE]);
58+
});
59+
60+
it('a non-base key is still unknown', () => {
61+
expect(codes({ type: 'leaf', nonsense: 1 } as SchemaElement)).toEqual(['unknown-prop']);
62+
});
63+
});
64+
65+
describe('generateDts — `SduiBaseProps` is emitted from the same list', () => {
66+
const dts = generateDts(manifest);
67+
const emitted = /export interface SduiBaseProps \{\n([\s\S]*?)\n\}/.exec(dts)?.[1].split('\n') ?? [];
68+
69+
it('carries every list entry that has an attribute type, in list order, and nothing else', () => {
70+
expect(emitted).toEqual(
71+
SDUI_BASE_PROPS.filter((p) => p.tsType !== null).map((p) => ` ${p.name}?: ${p.tsType};`),
72+
);
73+
expect(emitted).toContain(' bind?: string;');
74+
expect(emitted).toContain(' hidden?: boolean;');
75+
});
76+
77+
it('a component extends the whole base when it declares no base attribute', () => {
78+
expect(dts).toContain('export interface LeafProps extends SduiBaseProps {');
79+
});
80+
81+
it('a component that declares a base attribute Omits exactly that one from the base', () => {
82+
expect(dts).toContain('export interface LabelledProps extends Omit<SduiBaseProps, "label"> {\n label?: string;\n}');
83+
});
84+
});

‎packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ describe('generateDts publishes the html tier into JSX.IntrinsicElements', () =>
124124
const dts = generateDts(published);
125125
expect(dts).toContain('"p": PProps;');
126126
expect(dts).toContain('"a": AProps;');
127-
expect(dts).toContain('export interface AProps extends SduiBaseProps {\n className?: string;\n href?: string;\n}');
127+
expect(dts).toContain('export interface AProps extends Omit<SduiBaseProps, "className"> {\n className?: string;\n href?: string;\n}');
128128
expect(dts).toContain('"flex": FlexProps;');
129129
});
130130
});

‎packages/sdui-parser/src/codegen.ts‎

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@
1010

1111
import type { Manifest, ManifestComponent, ManifestInput, ManifestInputType } from './types.js';
1212
import { inputTypeArms } from './input-type.js';
13+
import { SDUI_BASE_PROPS } from './validate.js';
14+
15+
/**
16+
* `SduiBaseProps`, emitted from {@link SDUI_BASE_PROPS} — the list
17+
* `validateTree` reads — so the generated types and the validator cannot
18+
* disagree about a base prop (objectui#11044). ⛔ Never a second hand-kept
19+
* copy of the list here: that is how this interface came to refuse `bind` and
20+
* `hidden` after the validator accepted them.
21+
*/
22+
const emitBaseProps = (): string =>
23+
`export interface SduiBaseProps {\n${SDUI_BASE_PROPS.filter((prop) => prop.tsType !== null)
24+
.map((prop) => ` ${prop.name}?: ${prop.tsType};`)
25+
.join('\n')}\n}`;
26+
27+
/**
28+
* The attributes `SduiBaseProps` carries — every entry of the list but `type`,
29+
* whatever its scope. A declared input of the same name outranks each of them
30+
* in the generated types (objectui#11044, objectui#11075).
31+
*/
32+
const BASE_ATTRIBUTES = new Set(SDUI_BASE_PROPS.filter((prop) => prop.tsType !== null).map((prop) => prop.name));
1333

1434
export interface CodegenOptions {
1535
/** include a self-contained minimal JSX namespace so the d.ts type-checks
@@ -38,16 +58,7 @@ export function generateDts(manifest: Manifest, options: CodegenOptions = {}): s
3858
return `// AUTO-GENERATED by @object-ui/sdui-parser — DO NOT EDIT.
3959
// Source of truth: ComponentRegistry inputs (ADR-0080 §3). Regenerate via codegen.
4060
41-
export interface SduiBaseProps {
42-
id?: string;
43-
className?: string;
44-
style?: Record<string, unknown>;
45-
visible?: boolean;
46-
visibleOn?: string;
47-
disabled?: boolean;
48-
disabledOn?: string;
49-
children?: unknown;
50-
}
61+
${emitBaseProps()}
5162
5263
${interfaces}
5364
@@ -70,13 +81,31 @@ export {};
7081
* those arms only (objectui#3832): the old test was `i.type !== 'slot'`, which a
7182
* union like `['slot', 'string']` would have passed while `tsType` fell through
7283
* to the default and typed it `string` anyway.
84+
*
85+
* A base attribute the component declares is `Omit`ted from the base it
86+
* extends, whatever its scope: the declared input wins with its type. So a
87+
* declared type the base one does not admit is no TS2430 conflict — neither
88+
* `label?: string | Record<string, unknown>` on a `'where-undeclared'` member
89+
* (objectui#11044) nor `record:alert`'s
90+
* `visible?: boolean | string | Record<string, unknown>` on an `'every-node'`
91+
* one (objectui#11075) — and one the base admits (`className?: string`) types
92+
* the same either way.
93+
*
94+
* The scope is the VALIDATOR's distinction — `validateTree` skips an
95+
* `'every-node'` key before the declared-input lookup — and does not reach this
96+
* surface. ⛔ Every declared base attribute is `Omit`ted, not only the ones
97+
* whose types conflict: assignability is TypeScript's judgement, and a
98+
* conflicts-only rule here would re-derive it from type strings.
99+
*
100+
* A slot-only input emits no attribute, so a declared `children: 'slot'` is
101+
* not `Omit`ted and the base `children` stays.
73102
*/
74103
function emitInterface(comp: ManifestComponent): string {
75-
const lines = comp.inputs
76-
.filter((i) => valueArms(i).length > 0)
77-
.map((i) => ` ${propLine(i)}`)
78-
.join('\n');
79-
return `export interface ${propsName(comp.type)} extends SduiBaseProps {\n${lines}\n}`;
104+
const emitted = comp.inputs.filter((i) => valueArms(i).length > 0);
105+
const lines = emitted.map((i) => ` ${propLine(i)}`).join('\n');
106+
const yielded = emitted.filter((i) => BASE_ATTRIBUTES.has(i.name)).map((i) => JSON.stringify(i.name));
107+
const base = yielded.length > 0 ? `Omit<SduiBaseProps, ${yielded.join(' | ')}>` : 'SduiBaseProps';
108+
return `export interface ${propsName(comp.type)} extends ${base} {\n${lines}\n}`;
80109
}
81110

82111
/** The arms that describe a VALUE (every arm except `'slot'`). */

‎packages/sdui-parser/src/validate.ts‎

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,122 @@ export function acceptsChildren(comp: Pick<ManifestComponent, 'inputs'>): boolea
6060
}
6161

6262
/**
63-
* Base props every node may carry (mirrors BaseSchema) — never "unknown prop".
63+
* Where a base prop is legal without a declaration (objectui#11044).
64+
*
65+
* - `'every-node'` — on every node, and a registration's own input of the
66+
* same name is not consulted: {@link validateTree} skips the key before the
67+
* declared-input lookup.
68+
* - `'where-undeclared'` — on a type whose registration declares NO input of
69+
* that name. Where one does, the declared input wins, its `type-mismatch`
70+
* check included, and the generated JSX types take the declared type too.
71+
*/
72+
export type SduiBasePropScope = 'every-node' | 'where-undeclared';
73+
74+
/** One entry of {@link SDUI_BASE_PROPS}. */
75+
export interface SduiBaseProp {
76+
/** The `BaseSchema` member (`@object-ui/types`). */
77+
readonly name: string;
78+
readonly scope: SduiBasePropScope;
79+
/**
80+
* The attribute's type in the generated JSX surface (`SduiBaseProps` in
81+
* `sdui-intrinsics.d.ts`), or `null` for the one key that is no attribute:
82+
* `type`, which the tag name carries (`parse.ts` refuses a `type` attribute).
83+
*/
84+
readonly tsType: string | null;
85+
}
86+
87+
/**
88+
* The base props: the `BaseSchema` members this tier accepts on a node without
89+
* a registration declaring them. ONE list, read by BOTH of its consumers —
90+
* {@link validateTree}'s `unknown-prop` branch and `generateDts`'s
91+
* `SduiBaseProps` (objectui#11044). Before this list the two were separate
92+
* hand-kept copies and had drifted: the validator accepted `bind` and `hidden`
93+
* (objectui#11008) while the generated types still refused both.
94+
*
95+
* `bind` and `hidden` are `'every-node'` (objectui#11008). `BaseSchema` declares
96+
* both for every node and no registration declares either as an input, so
97+
* before they joined, the undeclared-key branch below answered every authored
98+
* one with `unknown-prop` — "has no prop" about a key the protocol declares,
99+
* on the nodes that honour it: `hidden` is read for every node by
100+
* `SchemaRenderer`'s hide chain, and `bind` by every renderer that calls
101+
* `useDataScope` (`list`, `tree-view`, the `object-*` widgets). The
102+
* declaration outranks the implementation, so the parser's view is the
103+
* declared type's, not a per-registration subset. The cost is accepted and
104+
* named: a `bind` on a node that does not read it — `data-table`
105+
* (objectui#6575) — draws nothing here either, and its render-time console
106+
* warning is the one signal left.
107+
*
108+
* `visibleWhen`, `hiddenOn` and `testId` are `'every-node'` for the same reason
109+
* (objectui#11044): no registration declares any of them, `SchemaRenderer`'s
110+
* hide chain reads the first two for every node, and it strips `testId` and
111+
* re-emits it as `data-testid`. `visibleWhen` is the canonical ADR-0089
112+
* predicate; before it joined, the deprecated `visibleOn` was silent while it
113+
* drew `unknown-prop`.
114+
*
115+
* The `'where-undeclared'` members (objectui#11044, triage ruling) are the
116+
* `BaseSchema` members some registrations DECLARE as typed inputs — the input
117+
* family's `placeholder`, `label`, `name`, … . Skipping them the way the
118+
* `'every-node'` members are skipped would silence those registrations'
119+
* `type-mismatch`, so they are base props only where the type declares no
120+
* input of that name. ⛔ Never move one to `'every-node'` to accept a key: that
121+
* silences a declared type check.
122+
*
123+
* Held over the live registry by `base-props-one-list-11044.test.tsx` in
124+
* `@object-ui/components` — every member a `BaseSchema` member, and `body` the
125+
* one member left out.
126+
*
127+
* ⛔ `body` is NOT here and must not be added. It was `BaseSchema`'s second
128+
* child-list spelling until objectui#6771 retired it; teaching this list the
129+
* key was the option that ruling refused, because it would have blessed a
130+
* second permanent spelling of one concept. `./body-dialect.ts` answers it by
131+
* name instead.
132+
*
133+
* LOCKSTEP: the list, its scopes and the two Sets below are the port of
134+
* objectui's `SDUI_BASE_PROPS` (objectui#11008, #11044, pin `db11afd4967c`).
135+
* Both consumers are ported too: {@link validateTree} and `codegen.ts`'s
136+
* `SduiBaseProps`. The list is exported for `codegen.ts` only; `index.ts` does
137+
* not re-export it.
64138
*
65139
* `children` IS here: the key is legal on every node, so it never draws
66140
* `unknown-prop` and its declared `slot` input is never type-checked. Whether
67141
* a given component RENDERS it is the containment question below, answered by
68142
* {@link acceptsChildren} from the declared input.
69143
*/
70-
const BASE_PROPS = new Set([
71-
'type',
72-
'id',
73-
'className',
74-
'style',
75-
'visible',
76-
'visibleOn',
77-
'disabled',
78-
'disabledOn',
79-
CHILD_LIST_KEY,
80-
]);
144+
export const SDUI_BASE_PROPS: readonly SduiBaseProp[] = Object.freeze([
145+
{ name: 'type', scope: 'every-node', tsType: null },
146+
{ name: 'id', scope: 'every-node', tsType: 'string' },
147+
{ name: 'className', scope: 'every-node', tsType: 'string' },
148+
{ name: 'style', scope: 'every-node', tsType: 'Record<string, unknown>' },
149+
{ name: 'visible', scope: 'every-node', tsType: 'boolean' },
150+
{ name: 'visibleWhen', scope: 'every-node', tsType: 'string' },
151+
{ name: 'visibleOn', scope: 'every-node', tsType: 'string' },
152+
{ name: 'hidden', scope: 'every-node', tsType: 'boolean' },
153+
{ name: 'hiddenOn', scope: 'every-node', tsType: 'string' },
154+
{ name: 'disabled', scope: 'every-node', tsType: 'boolean' },
155+
{ name: 'disabledOn', scope: 'every-node', tsType: 'string' },
156+
{ name: 'bind', scope: 'every-node', tsType: 'string' },
157+
{ name: 'testId', scope: 'every-node', tsType: 'string' },
158+
{ name: CHILD_LIST_KEY, scope: 'every-node', tsType: 'unknown' },
159+
{ name: 'name', scope: 'where-undeclared', tsType: 'string' },
160+
{ name: 'label', scope: 'where-undeclared', tsType: 'string | Record<string, string>' },
161+
{ name: 'description', scope: 'where-undeclared', tsType: 'string | Record<string, string>' },
162+
{ name: 'placeholder', scope: 'where-undeclared', tsType: 'string' },
163+
{ name: 'data', scope: 'where-undeclared', tsType: 'unknown' },
164+
{
165+
name: 'ariaLabel',
166+
scope: 'where-undeclared',
167+
tsType: 'string | { key: string; defaultValue?: string; params?: Record<string, unknown> }',
168+
},
169+
] satisfies SduiBaseProp[]);
170+
171+
const basePropNames = (scope: SduiBasePropScope): Set<string> =>
172+
new Set(SDUI_BASE_PROPS.filter((prop) => prop.scope === scope).map((prop) => prop.name));
173+
174+
/** The `'every-node'` members of {@link SDUI_BASE_PROPS}: never "unknown prop". */
175+
const BASE_PROPS = basePropNames('every-node');
176+
177+
/** The `'where-undeclared'` members of {@link SDUI_BASE_PROPS}. */
178+
const WHERE_UNDECLARED_BASE_PROPS = basePropNames('where-undeclared');
81179

82180
const isExpr = (v: unknown): boolean =>
83181
typeof v === 'object' && v !== null && '$expr' in (v as Record<string, unknown>);
@@ -116,6 +214,10 @@ export function validateTree(tree: SchemaElement | null, manifest: Manifest): Va
116214
// each provided prop
117215
for (const [key, value] of Object.entries(node)) {
118216
if (BASE_PROPS.has(key)) continue;
217+
// A declaration outranks a `'where-undeclared'` base prop
218+
// (objectui#11044): skipped only when this type declares no input of
219+
// that name, so a declared one keeps its type check below.
220+
if (WHERE_UNDECLARED_BASE_PROPS.has(key) && !byName.has(key)) continue;
119221
// The `object-kanban` Quick Add pair (objectui#8285): a key
120222
// `@objectstack/spec` still publishes and the renderer cannot honour,
121223
// because the control is gated on a RUNTIME SLOT no parsed page can

0 commit comments

Comments
 (0)