Skip to content

Commit e46ee77

Browse files
feat(types,components): declare and draw input-otp's separator (objectui#11365) (#11469)
Fixes #11365 Clause-②: yes ## What this changes Triage ruled the enforce arm of ADR-0049 (answer `5943561653` on the card). The shipped primitive already exports `InputOTPSeparator` (`role="separator"`), so the key is declared and drawn. The docs page and both catalog entries stay as they are, and they are now valid. - **Types (both faces).** `InputOTPSchema` gains `separator?: boolean` on the TypeScript face (`packages/types/src/form.ts`) and on the zod mirror (`packages/types/src/zod/form.zod.ts`). The node's `body` / `children` refusal messages list what the node renders instead, and that list now names `separator`. - **Renderer.** With `separator: true`, `packages/components/src/renderers/form/input-otp.tsx` splits the `length` slots into two `InputOTPGroup`s at the midpoint and draws one `InputOTPSeparator` between them, the shadcn "with separator" layout. - **Odd `length`:** the extra slot goes in the first group, so 5 slots draw as 3 and 2, and 7 as 4 and 3. - **`length: 1`:** one slot has nothing to separate, so it draws no separator. - **Absent or `false`:** one group holding every slot, byte-identical to before (proof below). - **Registration.** The `input-otp` registration publishes `{ name: 'separator', type: 'boolean' }` in `inputs`. - **Changeset.** `minor` for `@object-ui/types` and `@object-ui/components`: the published face gains a member. ## Accept-set change: before and after Before is BASE `5988b6b53`; after is head `da4d38ef`. | Reading | Before | After | |---|---|---| | `components-form-input-otp/with-visual-separator` through the real `SchemaRenderer`: `role="separator"` count | 0 | 1 | | `components-form-input-otp/verification-form` (its `input-otp` sits at `properties.children.1` of a `flex` bag): `role="separator"` count | 0 | 1 | | `with-visual-separator` slot groups | one group of 6 | 3 and 3 | | `components-form-input-otp/6-digit-otp` (no `separator`) | no separator, one group of 6 | unchanged | | `safeValidateSchema` on both authoring entries | green, because `.passthrough()` left the key unjudged | green, and the key is judged | | Strict authoring face (`StrictAnyComponentSchema`) on `with-visual-separator` | `unrecognized_keys` at the root, key `separator` | green | | Strict authoring face on `verification-form` | `unrecognized_keys` at `properties.children.1`, key `separator` | green | | `{ type: 'input-otp', separator: 'yes' }` under `safeValidateSchema` | green (passthrough) | `invalid_type` at `separator` | | The same value on an `input-otp` inside a `flex` bag | green | `invalid_type` at `properties.children.0.separator` | | TS face: an `InputOTPSchema` literal with `separator: 'yes'` | compiles: `BaseSchema`'s index signature is `any` | compile error, pinned by a `@ts-expect-error` | | html/jsx page tier: an `input-otp` tag written with `separator={true}` | `unknown-prop` warning, "has no prop separator" | no diagnostic | The html/jsx tier row was measured once with a throwaway probe. It ran `compile` with the same `manifestFromConfigs` mapping as `page.tsx`'s `getJsxManifest`, on head and on head with only the `inputs` row removed. BASE has the same `inputs`. No gate requires the row: `check:component-surface-parity` is report-only and `inputs` stays inside its union either way, and `registry-inputs-spec-parity` covers only types with a spec row, which `input-otp` lacks. The row is there so that tier stops warning off a key the renderer honours. ## Corpus re-judged Every document that authors `input-otp`: - **Catalog**, all five `examples/schema-catalog/src/schemas/components-form-input-otp/*` entries: - `with-visual-separator` and `verification-form` author `separator: true`. Both are now drawn, and both are green on both faces (table above). - `4-digit`, `6-digit-otp` and `8-digit` do not author it. Their rendered markup is byte-identical to BASE (proof below). - **Docs page** `content/docs/components/form/input-otp.mdx`: its interface block lists `separator?: boolean; // Show visual separator`, which is now true. The page is unedited. - **The installed `@objectstack/spec` 17.5.0 has no `input-otp` row**, so there is nothing to conflict with: - a recursive grep of its `dist/` for `input-otp`, `InputOTP` and their spellings exits 1, while a control grep for `'input'` under `dist/ui` exits 0; - `separator` has 0 hits in `dist/ui/index.d.mts`. - **objectstack** (`1d0600bf`) authors no `input-otp` anywhere: `git grep -c input-otp` exits 1, and a control grep in `packages/spec/src/ui` exits 0. ## Pins - `examples/schema-catalog/test/input-otp-separator-11365.test.tsx` renders through the real `SchemaRenderer` and checks: - both authoring entries draw exactly one `role="separator"`; - the 3 and 3 split; - an odd `length` gives 3 and 2, and one slot draws no separator; - the control: `6-digit-otp` draws none, in one group; - `separator: false` gives the same `innerHTML` as no `separator` at all; - the registration publishes the boolean input. It also checks parsing: `safeValidateSchema` and the strict face are green on both entries, and a non-boolean `separator` is refused with `invalid_type` at its path, flat and nested in a bag. - `packages/types/src/__tests__/input-otp-separator-11365.test.ts`: - compile-time: `InputOTPSchema['separator']` is exactly `boolean | undefined`, the declared spelling compiles, and `separator: 'yes'` is a `@ts-expect-error`; - runtime: the mirror keeps a boolean `separator`, and refuses a string with `invalid_type` at `separator`. A control shows an undeclared key still passes through. ## Reverse verification (each run from a committed head) - **Red first.** The catalog pin was run on the untouched BASE tree before any source edit. Both authoring entries read 0 `role="separator"`. The strict face reported `unrecognized_keys` for `separator`, at the root for `with-visual-separator` and at `properties.children.1` for `verification-form`. - **Ablation of the three source files.** I checked out `form.ts`, `zod/form.zod.ts` and the renderer from BASE under a restore trap, then confirmed on disk that `InputOTPSeparator` was 0 in the renderer, `separator?: boolean` was 0 in `form.ts` and `separator: z` was 0 in the zod file. - vitest: 7 failed and 6 passed. The 6 passing were the controls, the passthrough rows, the boolean round-trip and the `false` equivalence. - `tsc -p tsconfig.test.json` for `packages/types` exited 2: TS2344 on the `boolean | undefined` equality, and TS2578, an unused `@ts-expect-error`. - Restored with `git checkout HEAD -- ...`. `git diff HEAD` is 0 bytes, and every hash matches its HEAD blob. - **Ablation of the `inputs` row.** The row count read 0 on disk. The registration pin went red: 1 failed, 10 passed. Restored, and the hash matches HEAD. - **Byte-identity without `separator`.** I rendered `4-digit`, `6-digit-otp` and `8-digit` through `SchemaRenderer` with the head renderer and with the BASE renderer. Both outputs are 6201 bytes and `cmp` exits 0. ## Gates run locally Node 22.23.3 was used, the latest 22.x that CI's `22.x` resolves to; see the report for why. | Gate | Result | |---|---| | `pnpm --filter '@object-ui/components^...' build`, then `@object-ui/components` itself | exit 0 | | `pnpm --filter @object-ui/types --filter @object-ui/components run type-check` (both scripts echoed) | exit 0 | | `pnpm --filter @object-ui/example-schema-catalog run type-check`, on the doc-gate build closure | exit 0 | | `pnpm exec vitest run packages/types/` | 331 files, 8765 tests passed | | `pnpm exec vitest run packages/components/ examples/schema-catalog/` | 391 passed, 1 skipped; 5808 tests passed, 24 skipped | | `pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx` (readers of `inputs` and of `ui:input-otp`) | 2 files, 476 tests passed | | New pins at head `da4d38ef` | 2 files, 15 tests passed | | `check:doc-snippets`, after the scoped `--build-filter` turbo build | exit 0; 776 of 776 blocks judged, 0 failed | | `check:doc-examples` | exit 0 | | `check:doc-types` | exit 0 | | `check:component-surface-parity` (report-only) | `input-otp`: reads 3 to 4, interface 9 to 10, inputs 2 to 3. Findings stay at the same 2 (see Acceptance notes), and the full report is otherwise identical to BASE | | `node scripts/check-changeset-no-major.mjs`, `check:new-line-citations`, `check:control-bytes`, `check-changeset-presence` | exit 0 each, at head `da4d38ef` | **Declared narrowing.** `check:sdui-registration-pins` was not run locally: - It weighs a built console bundle for registrations the `sideEffects` arrays promise. - This diff adds and removes no registration and touches no `sideEffects` array. - CI's `performance-budget.yml` runs it. The repo-wide lint and the full farm also belong to CI. ## Acceptance notes - `check:component-surface-parity` still reports two older `input-otp` findings, unchanged by this PR: `InputOTPSchema` declares `defaultValue` and `error`, and the renderer reads neither by name. They belong to that census's family (objectui#4631). They are recorded here, not filed. - The docs page does not describe the midpoint split or the odd-length rule; the changeset and the type's docblock do. No sentence on the page became false, so the page is unedited. Session: `https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC` --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3f6efd6 commit e46ee77

6 files changed

Lines changed: 248 additions & 11 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/components': minor
3+
'@object-ui/types': minor
4+
---
5+
6+
`input-otp` draws the separator its docs page teaches (objectui#11365). The "With Separator" example on the `input-otp` docs page and two catalog entries (`with-visual-separator`, and the `input-otp` inside `verification-form`) author `separator: true`, but no type declared the key and the renderer read nothing for it, so they drew no separator at all. Triage ruled the enforce arm of ADR-0049: the shipped primitive already exports `InputOTPSeparator`, so the key is declared and drawn, and the docs page and catalog entries stay as they are.
7+
8+
**Components.** With `separator: true`, the `input-otp` renderer splits its `length` slots into two groups at the midpoint and draws one `InputOTPSeparator` (`role="separator"`) between them, the shadcn "with separator" layout. An odd `length` puts the extra slot in the first group (5 slots draw as 3 and 2). One slot has nothing to separate and draws no separator. Without `separator`, or with `separator: false`, the markup is unchanged: one group holding every slot. The `input-otp` registration publishes `separator` as a boolean input.
9+
10+
**Types.** `InputOTPSchema` gains `separator?: boolean` on both faces. The TypeScript face now refuses a non-boolean `separator`; before, `BaseSchema`'s index signature admitted any value. The zod mirror refuses a non-boolean `separator` with `invalid_type` at the key; before, `.passthrough()` kept it unjudged. The strict authoring face now accepts `separator`, where it used to report it as an unrecognized key. The "What it renders instead" list in the node's `body` / `children` refusal messages now names `separator`.
11+
12+
**minor, not patch — the published face gains a member.** `separator` is a new member of the shipped `.d.ts` and of the mirror's `.shape`, and the mirror refuses a non-boolean value by name.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11365 — `input-otp` draws the separator its docs page and two
11+
* catalog entries author.
12+
*
13+
* The docs page's "With Separator" section shows
14+
* `components-form-input-otp/with-visual-separator`, and
15+
* `components-form-input-otp/verification-form` authors the same key on an
16+
* `input-otp` nested in a `flex` node's `properties` bag. Before this card no
17+
* type declared `separator` on `InputOTPSchema` and the renderer read nothing
18+
* for it, so both entries rendered ZERO `role="separator"` elements through the
19+
* real `SchemaRenderer`. Triage ruled the enforce arm (ADR-0049): declare the
20+
* key on both faces and draw the primitive's `InputOTPSeparator`.
21+
*
22+
* ## What each block pins
23+
*
24+
* - The render block counts the `role="separator"` elements the renderer
25+
* actually emitted, on the two authoring entries and on the entry that does
26+
* NOT author the key (the control: without `separator` nothing changes).
27+
* - The parse block asks both faces. `safeValidateSchema` is `.passthrough()`,
28+
* so a green there says nothing about an UNDECLARED key; the strict authoring
29+
* face closes unknown keys, so a green there is the declaration's evidence.
30+
* The value probe (a non-boolean `separator`) is what proves the declared
31+
* member judges its value rather than merely being tolerated.
32+
*/
33+
import { describe, it, expect } from 'vitest';
34+
import { render } from '@testing-library/react';
35+
// Module scope, not a hook: registering the renderers is an unbounded module
36+
// load (AGENTS.md, test discipline — flaky tests: find the race).
37+
import '@object-ui/components';
38+
import { ComponentRegistry } from '@object-ui/core';
39+
import { SchemaRenderer, toRenderableSchema } from '@object-ui/react';
40+
import { safeValidateSchema, StrictAnyComponentSchema } from '@object-ui/types/zod';
41+
import { getExample } from '../src/index.js';
42+
43+
const AUTHORING = [
44+
'components-form-input-otp/with-visual-separator',
45+
'components-form-input-otp/verification-form',
46+
] as const;
47+
48+
/** Every issue as `code path: message`, so a red run says what broke. */
49+
function issuesOf(result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[]; message: string }> } }): string[] {
50+
return result.success || !result.error
51+
? []
52+
: result.error.issues.map((i) => `${i.code} ${i.path.map(String).join('.')}: ${i.message}`);
53+
}
54+
55+
function renderNode(schema: unknown): HTMLElement {
56+
const { container } = render(<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />);
57+
return container;
58+
}
59+
60+
/**
61+
* The slot count of each `InputOTPGroup`, in document order. The container's
62+
* other children are the separator and the library's own wrapper around the
63+
* hidden `<input>`, which is not a group.
64+
*/
65+
function groupSizes(container: HTMLElement): number[] {
66+
const host = container.querySelector('[data-input-otp-container]');
67+
expect(host, 'no input-otp container rendered: nothing below would be a reading').not.toBeNull();
68+
return Array.from((host as Element).children)
69+
.filter((el) => el.getAttribute('role') !== 'separator' && !el.querySelector('input'))
70+
.map((group) => group.children.length);
71+
}
72+
73+
describe('objectui#11365 — the authored `separator` draws one InputOTPSeparator', () => {
74+
it.each(AUTHORING)('%s renders exactly one role="separator"', (id) => {
75+
const container = renderNode(getExample(id).schema);
76+
expect(container.querySelector('[data-input-otp-container]'), 'no input-otp rendered').not.toBeNull();
77+
expect(container.querySelectorAll('[role="separator"]')).toHaveLength(1);
78+
});
79+
80+
it('the separator splits the six slots at the midpoint, between two groups', () => {
81+
const container = renderNode(getExample('components-form-input-otp/with-visual-separator').schema);
82+
expect(groupSizes(container)).toEqual([3, 3]);
83+
});
84+
85+
it('an odd `length` puts the extra slot in the first group; one slot draws no separator', () => {
86+
const five = renderNode({ type: 'input-otp', length: 5, separator: true });
87+
expect(groupSizes(five)).toEqual([3, 2]);
88+
expect(five.querySelectorAll('[role="separator"]')).toHaveLength(1);
89+
90+
const one = renderNode({ type: 'input-otp', length: 1, separator: true });
91+
expect(groupSizes(one)).toEqual([1]);
92+
expect(one.querySelectorAll('[role="separator"]')).toHaveLength(0);
93+
});
94+
95+
it('the control: the entry without `separator` draws none, in one group', () => {
96+
const container = renderNode(getExample('components-form-input-otp/6-digit-otp').schema);
97+
expect(container.querySelectorAll('[role="separator"]')).toHaveLength(0);
98+
expect(groupSizes(container)).toEqual([6]);
99+
});
100+
101+
it('the registration publishes `separator` as a boolean input', () => {
102+
// `inputs` is the html/jsx page tier's prop whitelist: without this row,
103+
// `separator` on an `input-otp` tag draws an `unknown-prop` warning on a
104+
// key the renderer honours (measured once on objectui#11365's PR).
105+
const inputs = ComponentRegistry.getMeta('input-otp')?.inputs ?? [];
106+
expect(inputs.filter((i) => i.name === 'separator')).toEqual([
107+
expect.objectContaining({ name: 'separator', type: 'boolean' }),
108+
]);
109+
});
110+
111+
it('`separator: false` renders the same markup as no `separator` at all', () => {
112+
const absent = renderNode({ type: 'input-otp', length: 6 }).innerHTML;
113+
const off = renderNode({ type: 'input-otp', length: 6, separator: false }).innerHTML;
114+
expect(off).toBe(absent);
115+
});
116+
});
117+
118+
describe('objectui#11365 — both authoring entries validate, on both faces', () => {
119+
it.each(AUTHORING)('%s validates under safeValidateSchema', (id) => {
120+
expect(issuesOf(safeValidateSchema(getExample(id).schema))).toEqual([]);
121+
});
122+
123+
it.each(AUTHORING)('%s validates under the strict authoring face (no unrecognized `separator`)', (id) => {
124+
expect(issuesOf(StrictAnyComponentSchema.safeParse(getExample(id).schema))).toEqual([]);
125+
});
126+
127+
it('a non-boolean `separator` is refused by value, at its path, in a bag child too', () => {
128+
// Counter-probe: a passthrough hole would accept this; a declared boolean
129+
// member refuses it with `invalid_type` at the key.
130+
const flat = safeValidateSchema({ type: 'input-otp', separator: 'yes' });
131+
expect(issuesOf(flat).map((s) => s.split(':')[0])).toEqual(['invalid_type separator']);
132+
133+
const nested = safeValidateSchema({
134+
type: 'flex',
135+
properties: { children: [{ type: 'input-otp', separator: 'yes' }] },
136+
});
137+
expect(nested.success).toBe(false);
138+
expect(issuesOf(nested).map((s) => s.split(':')[0])).toEqual(['invalid_type properties.children.0.separator']);
139+
});
140+
});

‎packages/components/src/renderers/form/input-otp.tsx‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { ComponentRegistry } from '@object-ui/core';
1010
import type { InputOTPSchema } from '@object-ui/types';
11-
import { InputOTP, InputOTPGroup, InputOTPSlot } from '../../ui';
11+
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '../../ui';
1212
import { toFormControlDomProps } from '../../lib/form-control-dom-props';
1313

1414
ComponentRegistry.register('input-otp',
@@ -22,7 +22,15 @@ ComponentRegistry.register('input-otp',
2222
// published 4- and 8-digit examples drew six slots (objectui#11347). One
2323
// spelling, no alias.
2424
const length = schema.length || 6;
25-
const slots = Array.from({ length });
25+
// `separator` (`InputOTPSchema.separator`, objectui#11365) splits the slots
26+
// into two groups at the midpoint and draws the primitive's separator
27+
// between them, the shadcn "with separator" layout. An odd `length` puts
28+
// the extra slot in the first group; a single slot has nothing to separate.
29+
// Absent or `false`, `split === length` and the markup is the one group
30+
// it always was.
31+
const split = schema.separator === true && length > 1 ? Math.ceil(length / 2) : length;
32+
const slotsFrom = (from: number, to: number) =>
33+
Array.from({ length: to - from }, (_, k) => <InputOTPSlot key={from + k} index={from + k} />);
2634

2735
const handleChange = (val: string) => {
2836
if (onChange) {
@@ -39,11 +47,13 @@ ComponentRegistry.register('input-otp',
3947
{...toFormControlDomProps(otpProps)}
4048
style={style}
4149
>
42-
<InputOTPGroup>
43-
{slots.map((_, i) => (
44-
<InputOTPSlot key={i} index={i} />
45-
))}
46-
</InputOTPGroup>
50+
<InputOTPGroup>{slotsFrom(0, split)}</InputOTPGroup>
51+
{split < length && (
52+
<>
53+
<InputOTPSeparator />
54+
<InputOTPGroup>{slotsFrom(split, length)}</InputOTPGroup>
55+
</>
56+
)}
4757
</InputOTP>
4858
);
4959
},
@@ -52,6 +62,7 @@ ComponentRegistry.register('input-otp',
5262
label: 'Input OTP',
5363
inputs: [
5464
{ name: 'length', type: 'number' },
65+
{ name: 'separator', type: 'boolean' },
5566
{ name: 'className', type: 'string' }
5667
],
5768
defaultProps: {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11365 — `InputOTPSchema.separator` is declared on BOTH faces, as a
11+
* boolean (triage's enforce arm under ADR-0049).
12+
*
13+
* The TypeScript face carries `BaseSchema`'s `[key: string]: any`, so an
14+
* undeclared `separator` already compiled whatever its value; the declared
15+
* member is what makes a non-boolean refused. The compile-time rows below are
16+
* checked by `tsc -p tsconfig.test.json`; the `describe` block reads the zod
17+
* mirror at runtime, value probe and control included. The rendering half
18+
* (one `role="separator"`, and none without the key) is pinned beside the
19+
* catalog entries in `examples/schema-catalog/test/input-otp-separator-11365.test.tsx`.
20+
*/
21+
22+
import { describe, it, expect } from 'vitest';
23+
import { InputOTPSchema } from '../zod/form.zod';
24+
import type { InputOTPSchema as TsInputOTPSchema } from '../form';
25+
26+
/** Compile-time truth assertion, erased at runtime — only `tsc` checks these. */
27+
type Expect<T extends true> = T;
28+
/** Compile-time equality, exact in both directions; `any` equals nothing but `any`. */
29+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
30+
31+
export type _SeparatorIsABoolean = Expect<Equal<TsInputOTPSchema['separator'], boolean | undefined>>;
32+
33+
/** CONTROL — the declared spelling compiles. */
34+
export const _separatorAccepted: TsInputOTPSchema = { type: 'input-otp', length: 6, separator: true };
35+
// @ts-expect-error -- `separator` is a declared boolean, so the index signature no longer admits a string
36+
export const _separatorRefused: TsInputOTPSchema = { type: 'input-otp', length: 6, separator: 'yes' };
37+
38+
describe('InputOTPSchema.separator on the zod mirror (objectui#11365)', () => {
39+
it('a boolean `separator` parses and survives', () => {
40+
const result = InputOTPSchema.safeParse({ type: 'input-otp', separator: true });
41+
expect(result.success).toBe(true);
42+
expect(result.success && result.data.separator).toBe(true);
43+
});
44+
45+
it('a non-boolean `separator` is refused with `invalid_type` at the key', () => {
46+
const result = InputOTPSchema.safeParse({ type: 'input-otp', separator: 'yes' });
47+
expect(result.success).toBe(false);
48+
const found = result.success ? [] : result.error.issues.map((i) => [i.code, i.path.join('.')]);
49+
expect(found).toEqual([['invalid_type', 'separator']]);
50+
});
51+
52+
it('CONTROL — the same probe on an undeclared key passes through, so the refusal above is the declaration', () => {
53+
expect(InputOTPSchema.safeParse({ type: 'input-otp', notAKey11365: 'yes' }).success).toBe(true);
54+
});
55+
});

‎packages/types/src/form.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,19 @@ export interface InputOTPSchema extends BaseSchema {
14531453
* @default 6
14541454
*/
14551455
length?: number;
1456+
/**
1457+
* Draw a visual separator between two halves of the slots
1458+
*
1459+
* READ SITE: `packages/components/src/renderers/form/input-otp.tsx`. `true`
1460+
* splits the {@link InputOTPSchema.length} slots into two groups at the
1461+
* midpoint and draws one separator (`role="separator"`) between them; an odd
1462+
* `length` puts the extra slot in the first group, and a single slot has
1463+
* nothing to separate. Absent or `false` draws one group. Declared by
1464+
* objectui#11365: the docs page and two catalog entries authored it while no
1465+
* type declared it and the renderer read nothing for it.
1466+
* @default false
1467+
*/
1468+
separator?: boolean;
14561469
/**
14571470
* Default value
14581471
*/
@@ -1501,7 +1514,8 @@ export interface InputOTPSchema extends BaseSchema {
15011514
* `maxLength`, `value` (in
15021515
* `packages/components/src/renderers/form/input-otp.tsx`). The undeclared
15031516
* `maxLength` read has since moved to the declared {@link InputOTPSchema.length}
1504-
* (objectui#11347).
1517+
* (objectui#11347), and objectui#11365 added a read of the declared
1518+
* {@link InputOTPSchema.separator}.
15051519
*
15061520
* Before objectui#9256 tombstoned them here, `body` and `children` were both
15071521
* inherited-and-optional from {@link BaseSchema} — so authoring either here
@@ -1530,7 +1544,8 @@ export interface InputOTPSchema extends BaseSchema {
15301544
* `maxLength`, `value` (in
15311545
* `packages/components/src/renderers/form/input-otp.tsx`). The undeclared
15321546
* `maxLength` read has since moved to the declared {@link InputOTPSchema.length}
1533-
* (objectui#11347).
1547+
* (objectui#11347), and objectui#11365 added a read of the declared
1548+
* {@link InputOTPSchema.separator}.
15341549
*
15351550
* Before objectui#9256 tombstoned them here, `body` and `children` were both
15361551
* inherited-and-optional from {@link BaseSchema} — so authoring either here

‎packages/types/src/zod/form.zod.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,10 @@ export const InputOTPSchema = BaseSchema.extend({
748748
name: z.string().optional().describe('Field name for form submission'),
749749
label: z.string().optional().describe('OTP input label'),
750750
length: z.number().optional().describe('Number of OTP digits'),
751+
separator: z
752+
.boolean()
753+
.optional()
754+
.describe('Draw a visual separator between two halves of the slots (objectui#11365)'),
751755
defaultValue: z.string().optional().describe('Default value'),
752756
value: z.string().optional().describe('Controlled value'),
753757
description: z.string().optional().describe('Help text'),
@@ -759,14 +763,14 @@ export const InputOTPSchema = BaseSchema.extend({
759763
+ 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or '
760764
+ '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An '
761765
+ 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. '
762-
+ 'What it renders instead: `length`, `value`.',
766+
+ 'What it renders instead: `length`, `separator`, `value`.',
763767
),
764768
children: retirementTombstone(
765769
'REFUSED (objectui#9256, ADR-0049) — `input-otp` reads NEITHER content channel: measured with the '
766770
+ 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or '
767771
+ '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An '
768772
+ 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. '
769-
+ 'What it renders instead: `length`, `value`.',
773+
+ 'What it renders instead: `length`, `separator`, `value`.',
770774
),
771775
});
772776

0 commit comments

Comments
 (0)