Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/accordion-item-icon-liveness.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
---
"@objectstack/spec": patch
---

docs(spec): record the live read point of `page:accordion` `items[].icon` — a `.describe()` plus an accept-pin, so a liveness sweep stops re-deriving a false retirement candidate (#9881)

`PageAccordionProps.items[].icon` parsed, rendered, and said nothing about
itself. A liveness sweep therefore read it as declared-but-unenforced and opened
a retirement candidate against it — which cost a full dispatch cycle before the
cross-repo read point was found and the candidate was closed premise-overtaken.
Nothing on the spec side recorded that liveness, so the next sweep would have
derived the same false candidate from the same absence.

**The key is live**, re-verified at the objectui pin this repo builds against
(`.objectui-sha` = `82a94170c`) rather than taken from the card:

- `packages/components/src/renderers/layout/containers.tsx:851-853` —
`PageAccordionRenderer` renders `{item.icon && <LazyIcon name={item.icon} …/>}`
inside the `AccordionTrigger`, grouped with the label in the trigger's single
wrapping span.
- `containers.tsx:898` — `ComponentRegistry.register('accordion', …)` publishes
the key to the Studio block designer in the `items` input, documented as
`[{ label, icon?, collapsed?, children }]`.

**Nothing about what parses changes.** The key was already declared and already
optional; this adds the prose that makes its liveness readable, and the test that
keeps it readable:

- a `.describe()` naming the consumer behaviourally, in the file's house idiom —
the same shape `record:alert`'s own `icon` uses ("Read on this component —
contrast …"), with the file:line anchors and the measured pin in the docblock
above the key, where this file keeps them;
- an accept-pin asserting the key parses on a `page:accordion` item and survives
to the parsed output, that an undeclared sibling on the same item is still
refused (so the accept is not vacuous on a schema that stopped being strict),
and that the `.describe()` still names the consumer — deleting it is what
re-opens the false candidate, so it is pinned rather than left to review.

The item `value` prescribed against one line above is the deliberate contrast:
the same renderer overwrites that key with `panel-<index>`, and a read point is
precisely what separates the two verdicts.
52 changes: 52 additions & 0 deletions packages/spec/src/ui/component.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -269,6 +269,58 @@ describe('PageAccordionProps variant (#6776)', () => {
});
});

// #9881 — the accept-pin for `page:accordion` items[].icon, a key a liveness
// sweep once read as declared-but-unenforced. It has a live cross-repo consumer:
// objectui's `PageAccordionRenderer` renders `{item.icon && <LazyIcon
// name={item.icon} …/>}` inside the `AccordionTrigger`
// (`packages/components/src/renderers/layout/containers.tsx:851-853`), and the
// same file's `ComponentRegistry.register('accordion', …)` publishes the key to
// the Studio block designer at `:898` (the `items` input, documented as
// `[{ label, icon?, collapsed?, children }]`). Measured at the pin this repo
// builds against — `.objectui-sha` = 82a94170c.
//
// #9397 spent a full dispatch cycle re-deriving that read point from scratch
// after the sweep proposed retiring the key. This block plus the `.describe()`
// it pins are what stop the next sweep repeating it: the liveness verdict is
// now readable from the spec side alone, with no cross-repo hunt.
describe('PageAccordionProps items[].icon liveness (#9881)', () => {
const accordion = ComponentPropsMap['page:accordion'];

it('accepts an icon on a panel item — the value objectui LazyIcon renders in the trigger', () => {
const result = accordion.safeParse({
items: [{ label: 'Details', icon: 'circle-alert', children: [] }],
});
expect(result.success).toBe(true);
const items = (result.success ? result.data : undefined) as
| { items: { icon?: string }[] }
| undefined;
// Carried through to the parsed output, not stripped: what the renderer
// reads is what an author writes.
expect(items?.items[0]?.icon).toBe('circle-alert');
});

it('still refuses an undeclared sibling on the same item — the accept above is not vacuous', () => {
// Without this the green above would also be green on a schema that had
// stopped being strict, which is the failure mode an accept-pin exists to
// exclude.
const result = accordion.safeParse({
items: [{ label: 'Details', iconName: 'circle-alert', children: [] }],
});
expect(result.success).toBe(false);
expect(JSON.stringify(result.error?.issues)).toContain('unrecognized_keys');
});

it('keeps a `.describe()` that names the consumer, so the read point survives a rename', () => {
// The describe is the artifact an auditor reads instead of hunting across
// repos; deleting it is what re-opens the false candidate, so it is pinned
// rather than left to review.
const itemShape = (PageAccordionProps as unknown as {
def: { shape: { items: { def: { element: { def: { shape: Record<string, { description?: string }> } } } } } };
}).def.shape.items.def.element.def.shape;
expect(itemShape.icon?.description).toContain('LazyIcon');
});
});

// #5775 — the two tab-item keys the renderer honours and the schema did not
// declare. `value` is the load-bearing one: it is the `?tab=` token, and the
// index-derived fallback (`tab-<i>`) silently points at a different tab as soon
Expand Down
27 changes: 26 additions & 1 deletion packages/spec/src/ui/component.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1461,7 +1461,32 @@ export const PageAccordionProps = strictObject({
},
}, {
label: I18nLabelSchema,
icon: z.string().optional(),
/**
* Panel-trigger icon, and the reason this key carries a docblock at all: a
* liveness sweep read it as declared-but-unenforced and opened a retirement
* candidate against it, which cost a full dispatch cycle before the
* cross-repo read point was found (#9397, closed premise-overtaken; #9881
* is the rider that records the liveness here so the next sweep cannot
* re-derive the same false candidate).
*
* The key is LIVE at the objectui pin this repo builds against
* (`.objectui-sha` = `82a94170c`): `containers.tsx:851-853` renders
* `{item.icon && <LazyIcon name={item.icon} …/>}` inside the
* `AccordionTrigger`, grouped with the label in the trigger's one wrapping
* span, and the renderer's registration publishes the key to the Studio
* block designer at `:898` (the `items` input, documented as
* `[{ label, icon?, collapsed?, children }]`).
*
* Vocabulary is Lucide, resolved through objectui's `LazyIcon`
* (`lib/lazy-icon.tsx` — kebab-case or PascalCase, normalised to
* kebab-case, with a fallback when the name is not a real Lucide icon), the
* same slot every other authorable icon on this surface uses. Contrast the
* item `value` prescribed against above: the same renderer OVERWRITES that
* one, and a read point is precisely what separates the two verdicts.
*/
icon: z.string().optional().describe(
'Lucide icon name rendered in the panel trigger, left of the label. Read on this component — the renderer draws it via `LazyIcon`; contrast the item `value` beside it, which the renderer overwrites with `panel-<index>`.',
),
collapsed: z.boolean().default(false),
children: z.array(z.unknown()).describe('Child components'),
})),
Expand Down
Loading