From dd802b6881043207ecdb9e700e228af98352c0cf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:41:35 +0000 Subject: [PATCH] docs(components): spell the inherited `disabled` as `boolean | string` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectui#7087 (maintainer ruling 2026-09-01, option 1 scoped to `disabled`) removed the 18 `disabled?: boolean` narrowings from the concrete schemas, so the member is now inherited from `BaseSchema` as `boolean | string` — a boolean, or a predicate expression on the same evaluated path as `visible`. 13 component pages went on spelling the removed narrowing in their illustrative interfaces, teaching a type the shipped tree no longer has. Each of the 22 `disabled?:` rows under `content/docs/components` was attributed to its enclosing interface before editing: 13 belong to schemas that inherit the member and were corrected; 8 belong to independent item/option shapes that declare their own `disabled?: boolean` and are left at plain `boolean`; `basic/button-group.mdx` was already correct (objectui#6347) and is the spelling the other 13 converged on. No gate can see this drift — the rows live in `plaintext`/`text` fences and `check-doc-snippet-types` compiles only `ts`/`tsx` (objectui#6143) — so the invariant is pinned instead: `packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts` holds all 14 inherited rows to the union, cross-checks against the shipped interfaces so a re-narrowing fails pointing at `packages/types`, keeps the 8 independent rows at `boolean` as a blanket-replace control, and asserts the two tables account for every documented row. Also corrects the now-false 13-of-14 paragraph in the header of `button-group-doc-surface-6347.test.ts` (comment text only; its assertions are untouched and stay green). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho --- .../component-docs-disabled-inherited-7239.md | 11 + .../components/disclosure/toggle-group.mdx | 2 +- content/docs/components/form/button.mdx | 2 +- content/docs/components/form/calendar.mdx | 2 +- content/docs/components/form/checkbox.mdx | 2 +- content/docs/components/form/combobox.mdx | 2 +- content/docs/components/form/date-picker.mdx | 2 +- content/docs/components/form/file-upload.mdx | 2 +- content/docs/components/form/input-otp.mdx | 2 +- content/docs/components/form/input.mdx | 2 +- content/docs/components/form/radio-group.mdx | 2 +- content/docs/components/form/select.mdx | 2 +- content/docs/components/form/switch.mdx | 2 +- content/docs/components/form/textarea.mdx | 2 +- .../button-group-doc-surface-6347.test.ts | 27 +- ...onent-docs-disabled-inherited-7239.test.ts | 245 ++++++++++++++++++ 16 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 .changeset/component-docs-disabled-inherited-7239.md create mode 100644 packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts diff --git a/.changeset/component-docs-disabled-inherited-7239.md b/.changeset/component-docs-disabled-inherited-7239.md new file mode 100644 index 0000000000..a09347042f --- /dev/null +++ b/.changeset/component-docs-disabled-inherited-7239.md @@ -0,0 +1,11 @@ +--- +--- + +Docs-only: 13 `content/docs/components/**` pages spelled an inherited +`disabled?: boolean` in their illustrative interfaces, which went stale when +objectui#7087 removed the 18 narrowings and left the member inherited from +`BaseSchema` as `boolean | string`. The pages now spell the shipped union +(objectui#7239), and a new test-only pin +(`packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts`) +holds all 14 inherited rows to it while keeping the 8 independent item/option +rows at plain `boolean`. No published package behaviour changes. diff --git a/content/docs/components/disclosure/toggle-group.mdx b/content/docs/components/disclosure/toggle-group.mdx index 022427bffa..b0f117876c 100644 --- a/content/docs/components/disclosure/toggle-group.mdx +++ b/content/docs/components/disclosure/toggle-group.mdx @@ -41,7 +41,7 @@ interface ToggleGroupSchema { onValueChange?: (value: string | string[]) => void; // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression // Styling className?: string; diff --git a/content/docs/components/form/button.mdx b/content/docs/components/form/button.mdx index ec33db2324..2455e17d33 100644 --- a/content/docs/components/form/button.mdx +++ b/content/docs/components/form/button.mdx @@ -57,7 +57,7 @@ interface ButtonSchema { iconPosition?: 'left' | 'right'; // Icon placement // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression loading?: boolean; // Actions diff --git a/content/docs/components/form/calendar.mdx b/content/docs/components/form/calendar.mdx index 7cec8d7ff1..b86f54454a 100644 --- a/content/docs/components/form/calendar.mdx +++ b/content/docs/components/form/calendar.mdx @@ -36,7 +36,7 @@ interface CalendarSchema { // Constraints minDate?: Date | string; // Minimum selectable date maxDate?: Date | string; // Maximum selectable date - disabled?: boolean; // Whether calendar is disabled + disabled?: boolean | string; // boolean, or a predicate expression // Events onChange?: (date: Date | Date[] | undefined) => void; // Date change handler diff --git a/content/docs/components/form/checkbox.mdx b/content/docs/components/form/checkbox.mdx index a084fc9ea1..f6b874d337 100644 --- a/content/docs/components/form/checkbox.mdx +++ b/content/docs/components/form/checkbox.mdx @@ -18,6 +18,6 @@ interface CheckboxSchema { label?: string; defaultChecked?: boolean; required?: boolean; - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression } ``` diff --git a/content/docs/components/form/combobox.mdx b/content/docs/components/form/combobox.mdx index e04575735a..a792335541 100644 --- a/content/docs/components/form/combobox.mdx +++ b/content/docs/components/form/combobox.mdx @@ -42,7 +42,7 @@ interface ComboboxSchema { onChange?: (value: string) => void; // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression // Styling className?: string; diff --git a/content/docs/components/form/date-picker.mdx b/content/docs/components/form/date-picker.mdx index 34d195b50f..23aa1c262a 100644 --- a/content/docs/components/form/date-picker.mdx +++ b/content/docs/components/form/date-picker.mdx @@ -34,7 +34,7 @@ interface DatePickerSchema { onChange?: (date: Date | undefined) => void; // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression // Styling className?: string; diff --git a/content/docs/components/form/file-upload.mdx b/content/docs/components/form/file-upload.mdx index 5469628dcc..d4baecf6c7 100644 --- a/content/docs/components/form/file-upload.mdx +++ b/content/docs/components/form/file-upload.mdx @@ -43,7 +43,7 @@ interface FileUploadSchema { maxFiles?: number; // Maximum number of files (for multiple) // States - disabled?: boolean; // Whether field is disabled + disabled?: boolean | string; // boolean, or a predicate expression // Help text description?: string; // Help text or description diff --git a/content/docs/components/form/input-otp.mdx b/content/docs/components/form/input-otp.mdx index 5a2ac21f51..0598b05721 100644 --- a/content/docs/components/form/input-otp.mdx +++ b/content/docs/components/form/input-otp.mdx @@ -36,7 +36,7 @@ interface InputOTPSchema { onComplete?: (value: string) => void; // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression // Styling className?: string; diff --git a/content/docs/components/form/input.mdx b/content/docs/components/form/input.mdx index d876e6a891..e5de3f2b97 100644 --- a/content/docs/components/form/input.mdx +++ b/content/docs/components/form/input.mdx @@ -22,7 +22,7 @@ interface InputSchema { placeholder?: string; defaultValue?: string | number; required?: boolean; - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression readonly?: boolean; } ``` diff --git a/content/docs/components/form/radio-group.mdx b/content/docs/components/form/radio-group.mdx index fabf92601b..dab355e15c 100644 --- a/content/docs/components/form/radio-group.mdx +++ b/content/docs/components/form/radio-group.mdx @@ -50,7 +50,7 @@ interface RadioGroupSchema { onChange?: (value: string | number) => void; // States - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression // Styling className?: string; diff --git a/content/docs/components/form/select.mdx b/content/docs/components/form/select.mdx index 2e6a306894..8499dab8cc 100644 --- a/content/docs/components/form/select.mdx +++ b/content/docs/components/form/select.mdx @@ -19,6 +19,6 @@ interface SelectSchema { options: { label: string; value: string | number }[]; placeholder?: string; required?: boolean; - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression } ``` diff --git a/content/docs/components/form/switch.mdx b/content/docs/components/form/switch.mdx index ba27aa8cc6..22c2f04df5 100644 --- a/content/docs/components/form/switch.mdx +++ b/content/docs/components/form/switch.mdx @@ -17,6 +17,6 @@ interface SwitchSchema { name?: string; label?: string; defaultChecked?: boolean; - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression } ``` diff --git a/content/docs/components/form/textarea.mdx b/content/docs/components/form/textarea.mdx index d5dc8107b1..2479fc44f1 100644 --- a/content/docs/components/form/textarea.mdx +++ b/content/docs/components/form/textarea.mdx @@ -19,6 +19,6 @@ interface TextareaSchema { placeholder?: string; rows?: number; required?: boolean; - disabled?: boolean; + disabled?: boolean | string; // boolean, or a predicate expression } ``` diff --git a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts index 2b58359f7a..684c9b6478 100644 --- a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts +++ b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts @@ -58,15 +58,24 @@ * `z.union([z.boolean(), z.string()])`, the string limb being the predicate * dialect `disabledOn` also carries). * - * That reads at first like a repo-wide convention worth leaving alone — 14 - * `content/docs/components/**` pages spell a component schema's own `disabled` - * as `boolean`. It is not: 13 of those 14 component schemas REDECLARE - * `disabled?: boolean` themselves (`ButtonSchema`, `SelectSchema`, - * `SwitchSchema`, `ToggleGroupSchema` and nine more), so their pages are right. - * `ButtonGroupSchema` is the ONE that does not redeclare it, which makes this - * page the outlier rather than the convention. Measured, not assumed — and it - * is why the type TEXT of both inherited rows is asserted here rather than - * waved through as "documented centrally". + * That read at first like a repo-wide convention worth leaving alone — 14 + * `content/docs/components/**` pages spelled a component schema's own + * `disabled` as `boolean`. On the tree this file was written against it was + * not a convention but an outlier: 13 of those 14 component schemas REDECLARED + * `disabled?: boolean` themselves, so their pages were right and + * `ButtonGroupSchema` — which did not redeclare it — was the one page out of + * step. + * + * ⚠️ That 13-of-14 reading is now HISTORY, not a live fact. objectui#7087 + * (maintainer ruling 2026-09-01) removed all 18 narrowings, so NONE of the 14 + * redeclare `disabled` any more and all 14 inherit `boolean | string`; the 13 + * pages that had been right became the stale ones and were corrected by + * objectui#7239. The reasoning below still holds and is why this file asserts + * the type TEXT of both inherited rows rather than waving them through as + * "documented centrally" — only the population it was measured against moved. + * The assertions in this file are about `ButtonGroupSchema` alone and are + * unaffected; the 14-page invariant now lives in + * `component-docs-disabled-inherited-7239.test.ts`. * * ## `## Selection Mode` is gone because the renderer cannot draw it * diff --git a/packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts b/packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts new file mode 100644 index 0000000000..f3e3b3e13e --- /dev/null +++ b/packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts @@ -0,0 +1,245 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Every `content/docs/components/**` page that documents a schema INHERITING + * `disabled` spells it `boolean | string` (objectui#7239, the docs half of the + * objectui#7087 ruling of 2026-09-01). + * + * ## Why a pin rather than "the docs gates went green" + * + * No gate can see this drift. These pages declare their own illustrative + * `interface` blocks (objectui#6143): `check-doc-component-types` reads only the + * `type` STRING LITERALS out of docs code blocks, and `check-doc-snippet-types` + * compiles `ts`/`tsx` fences — a `plaintext` fence is not one. So a member row + * in those fences may name any type at all and every gate stays green. That is + * the same hole `button-group-doc-surface-6347.test.ts` records one page over, + * and it is why the correction this file accompanies could not be verified by + * running CI. + * + * ## The defect this pins shut + * + * objectui#7087 removed 18 `disabled?: boolean` narrowings from the concrete + * schemas, so the member is now inherited from `BaseSchema` as + * `boolean | string` — a boolean, or a predicate expression on the same + * evaluated path as `visible`. 13 component pages went on spelling the removed + * narrowing, teaching a type the shipped tree no longer has. A reader copying + * those rows would conclude `disabled: "${data.status === 'locked'}"` is + * invalid, which is exactly the capability the ruling restored. + * + * ## What this file asserts, and why in this shape + * + * 1. INHERITED (14 rows) — the page's `disabled` row reads `boolean | string`, + * AND the shipped interface of the same name still extends `BaseSchema` + * without redeclaring `disabled`. The second half is what keeps this pin + * honest in the other direction: if someone re-narrows the type, the pin + * goes red pointing at `packages/types`, not at the page — the page would + * then be the thing that is right. A doc-only assertion could not tell + * those two worlds apart. + * 2. INDEPENDENT (8 rows) — the item/option shapes that declare their own + * `disabled?: boolean` and do NOT extend `BaseSchema` still read exactly + * `boolean`. This is the blanket-replace control: a sweep that rewrote + * every `disabled?: boolean` under `content/docs/components` turns these + * red, and nothing else in the repo would have caught it. + * 3. COMPLETENESS — the two tables together account for EVERY `disabled?:` + * row under `content/docs/components`, by measurement rather than by the + * counts being restated. A new page carrying the stale spelling fails here + * instead of sitting unclassified, which is the failure mode a + * hand-enumerated pin has by construction. + * + * ## Boundary, stated rather than implied + * + * The three overlay menu-item rows are doc-local names with NO shipped + * counterpart (`ContextMenuCommandItem` / `DropdownMenuCommandItem` / + * `MenubarCommandItem` against a shipped `MenuCommandItem`), so only their doc + * text is asserted; that is recorded here so a later reader does not mistake + * the missing shipped half for an oversight. `ButtonGroupSchema` is listed + * INHERITED although objectui#6347 had already corrected its page — it is the + * spelling all 13 others converged on, and pinning it here keeps the model row + * from drifting away from the rows that copy it. + * + * ## Predictions, written before the first run (red-first) + * + * With the 13 corrected pages reverted to their `origin/main` @ `ebc05b4d6` + * blobs and this file in place: + * + * - the 13 INHERITED cases fail, each naming its own page and the type text + * it actually found (`boolean`); + * - the `ButtonGroupSchema` INHERITED case stays green (objectui#6347 fixed + * that page earlier, so it is untouched by the revert); + * - all 8 INDEPENDENT cases stay green — the revert does not touch them; + * - COMPLETENESS stays green: reverting changes the type text of a row, not + * which rows exist. + * + * That last pair is the point of the control: a failure that reddened the + * independent rows too would mean the sweep was indiscriminate, not that the + * inherited rows were wrong. + */ + +import { describe, expect, it } from 'vitest'; +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..', '..', '..'); +const DOC_DIR = join(REPO_ROOT, 'content', 'docs', 'components'); +const TYPES_DIR = join(REPO_ROOT, 'packages', 'types', 'src'); + +const DECL_RE = /^\s*(?:export\s+)?(?:interface|type)\s+([A-Za-z0-9_]+)/; +const DISABLED_RE = /^\s*disabled\?:\s*(.+?);/; + +/** Every `disabled?:` row under `content/docs/components`, with its owner. */ +interface DocRow { + readonly page: string; + readonly iface: string; + readonly line: number; + readonly type: string; +} + +function mdxPages(): string[] { + const out: string[] = []; + const walk = (dir: string, prefix: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => + a.name.localeCompare(b.name), + )) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) walk(join(dir, entry.name), rel); + else if (entry.name.endsWith('.mdx')) out.push(rel); + } + }; + walk(DOC_DIR, ''); + return out; +} + +function docRows(): DocRow[] { + const rows: DocRow[] = []; + for (const page of mdxPages()) { + const lines = readFileSync(join(DOC_DIR, page), 'utf8').split('\n'); + lines.forEach((raw, idx) => { + const hit = DISABLED_RE.exec(raw); + if (!hit) return; + let iface = '(unattributed)'; + for (let i = idx - 1; i >= 0; i -= 1) { + const decl = DECL_RE.exec(lines[i]); + if (decl) { + iface = decl[1]; + break; + } + } + rows.push({ page, iface, line: idx + 1, type: hit[1].trim() }); + }); + } + return rows; +} + +/** The shipped interface of that name, if `packages/types/src` declares one. */ +function shipped(name: string): { extendsBase: boolean; declaresDisabled: boolean } | null { + for (const file of readdirSync(TYPES_DIR).filter((f) => f.endsWith('.ts')).sort()) { + const lines = readFileSync(join(TYPES_DIR, file), 'utf8').split('\n'); + const head = lines.findIndex((l) => + new RegExp(`^\\s*(?:export\\s+)?interface\\s+${name}\\b`).test(l), + ); + if (head === -1) continue; + let declaresDisabled = false; + for (let i = head + 1; i < lines.length; i += 1) { + if (/^\}/.test(lines[i])) break; + if (DISABLED_RE.test(lines[i])) declaresDisabled = true; + } + return { extendsBase: /\bextends\s+BaseSchema\b/.test(lines[head]), declaresDisabled }; + } + return null; +} + +const ROWS = docRows(); +const rowFor = (page: string, iface: string): DocRow | undefined => + ROWS.find((r) => r.page === page && r.iface === iface); + +/** Schemas that INHERIT `disabled` from `BaseSchema` — the page must be wide. */ +const INHERITED = [ + { page: 'basic/button-group.mdx', iface: 'ButtonGroupSchema' }, + { page: 'disclosure/toggle-group.mdx', iface: 'ToggleGroupSchema' }, + { page: 'form/button.mdx', iface: 'ButtonSchema' }, + { page: 'form/calendar.mdx', iface: 'CalendarSchema' }, + { page: 'form/checkbox.mdx', iface: 'CheckboxSchema' }, + { page: 'form/combobox.mdx', iface: 'ComboboxSchema' }, + { page: 'form/date-picker.mdx', iface: 'DatePickerSchema' }, + { page: 'form/file-upload.mdx', iface: 'FileUploadSchema' }, + { page: 'form/input-otp.mdx', iface: 'InputOTPSchema' }, + { page: 'form/input.mdx', iface: 'InputSchema' }, + { page: 'form/radio-group.mdx', iface: 'RadioGroupSchema' }, + { page: 'form/select.mdx', iface: 'SelectSchema' }, + { page: 'form/switch.mdx', iface: 'SwitchSchema' }, + { page: 'form/textarea.mdx', iface: 'TextareaSchema' }, +] as const; + +/** Independent item/option shapes — they declare `disabled` and stay narrow. */ +const INDEPENDENT = [ + { page: 'basic/button-group.mdx', iface: 'ButtonGroupButton', shippedName: 'ButtonGroupButton' }, + { page: 'disclosure/accordion.mdx', iface: 'AccordionItem', shippedName: 'AccordionItem' }, + { page: 'disclosure/toggle-group.mdx', iface: 'ToggleGroupItem', shippedName: 'ToggleGroupItem' }, + { page: 'form/form.mdx', iface: 'FormField', shippedName: 'FormField' }, + { page: 'form/radio-group.mdx', iface: 'RadioOption', shippedName: 'RadioOption' }, + // Doc-local names; the shipped shape they illustrate is `MenuCommandItem`. + { page: 'overlay/context-menu.mdx', iface: 'ContextMenuCommandItem', shippedName: null }, + { page: 'overlay/dropdown-menu.mdx', iface: 'DropdownMenuCommandItem', shippedName: null }, + { page: 'overlay/menubar.mdx', iface: 'MenubarCommandItem', shippedName: null }, +] as const; + +describe('component pages spell the INHERITED `disabled` as `boolean | string` (objectui#7239)', () => { + it.each(INHERITED)('$page documents $iface with the inherited union', ({ page, iface }) => { + const row = rowFor(page, iface); + expect(row, `no \`disabled?:\` row attributed to ${iface} in ${page}`).toBeDefined(); + expect(`${page} ${iface} -> ${row?.type}`).toBe(`${page} ${iface} -> boolean | string`); + }); + + it.each(INHERITED)('$iface still inherits `disabled` in the shipped tree', ({ iface }) => { + const decl = shipped(iface); + expect(decl, `packages/types/src declares no interface ${iface}`).not.toBeNull(); + // Re-narrowing the type would make the PAGE right and this pin's premise + // wrong; it must fail here rather than silently keep asserting the union. + expect({ iface, ...decl }).toEqual({ iface, extendsBase: true, declaresDisabled: false }); + }); +}); + +describe('independent shapes stay `boolean` — the blanket-replace control (objectui#7239)', () => { + it.each(INDEPENDENT)('$page documents $iface as a plain boolean', ({ page, iface }) => { + const row = rowFor(page, iface); + expect(row, `no \`disabled?:\` row attributed to ${iface} in ${page}`).toBeDefined(); + expect(`${page} ${iface} -> ${row?.type}`).toBe(`${page} ${iface} -> boolean`); + }); + + it.each(INDEPENDENT.filter((e) => e.shippedName !== null))( + '$iface declares its own `disabled` and does not extend BaseSchema', + ({ shippedName }) => { + const decl = shipped(shippedName as string); + expect(decl, `packages/types/src declares no interface ${shippedName}`).not.toBeNull(); + expect({ name: shippedName, ...decl }).toEqual({ + name: shippedName, + extendsBase: false, + declaresDisabled: true, + }); + }, + ); +}); + +describe('the two tables account for every documented `disabled` row (objectui#7239)', () => { + it('classifies every `disabled?:` row under content/docs/components', () => { + const claimed = new Set( + [...INHERITED, ...INDEPENDENT].map((e) => `${e.page}#${e.iface}`), + ); + const measured = ROWS.map((r) => `${r.page}#${r.iface}`); + // Reported as sorted arrays so a failure names the stray row, not a count. + expect(measured.filter((k) => !claimed.has(k)).sort()).toEqual([]); + expect([...claimed].filter((k) => !measured.includes(k)).sort()).toEqual([]); + }); + + it('finds no unattributed row (the walk-back parser reached a declaration)', () => { + expect(ROWS.filter((r) => r.iface === '(unattributed)')).toEqual([]); + }); + + it('sees exactly the population this card measured', () => { + expect({ rows: ROWS.length, inherited: INHERITED.length, independent: INDEPENDENT.length }).toEqual( + { rows: 22, inherited: 14, independent: 8 }, + ); + }); +});