From 782795c0555c2ae27b41065ee0541c31fed627cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:08:22 +0000 Subject: [PATCH] fix(types,layout,mobile,core): re-home the breakpoint vocabulary, delete the two dead responsive implementations (#7580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectstack#11027 retired `@objectstack/spec`'s whole `ui/responsive` vocabulary on the ground that its four types "had no other authorable carrier". That ground is measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component whose authorable `columns` is typed by `BreakpointColumnMap` and applied by `resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in `@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and when a renderer implements it" — is already met here, so the two types a renderer reads are re-homed rather than retired. Both halves land together: deleting only the dead implementations would leave the next spec pin bump red, which is the card's central point. Re-homed, under the same names and members: - `BreakpointName` -> `@object-ui/types` (`mobile.ts`). No consumer change: same name, same six members, same export sites. Only provenance moved. - `BreakpointColumnMap` -> `@object-ui/layout` (`ResponsiveGrid.tsx`), verbatim from the retired `$strict` schema (no index signature restored). Removed: - `BreakpointOrderMap` — ruling item 3; no read point, published only because the retired `ResponsiveConfigSchema` paired it with the column map. - `useResponsiveConfig` (@object-ui/mobile) and `ResponsiveProtocol` (@object-ui/core), both measured at zero callers on #4773, with the parity test whose subject `ResponsiveConfigSchema` the retirement deletes. - `SpecResponsiveConfig` / `SpecBreakpointName` — dead re-exports once the two implementations went; dropped rather than re-declared, the disposition the retired i18n names in that file already carry. The objectstack#4115 comment is ANSWERED, not deleted (ruling item 5): the retirement leaves no spec definition for a local declaration to be mistaken for. The pin is still spec 17.2.0, which pre-dates the retirement and still exports both names, so the collision is real for one pin interval and is held by two SELF-EXPIRING entries in `scripts/check-spec-symbol-derivation.mjs` — ratchet 3 fails an ALLOW entry that excuses nothing, so the pin bump is forced to delete them. Same mechanism objectui#5716/#5668 used for the theme trio. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .../7580-responsive-vocabulary-rehome.md | 52 +++++ .../core/src/protocols/ResponsiveProtocol.ts | 210 ------------------ packages/core/src/protocols/index.ts | 10 +- packages/layout/src/ResponsiveGrid.tsx | 58 +++-- .../responsive-config-spec-parity.test.ts | 158 ------------- packages/mobile/src/index.ts | 8 +- packages/mobile/src/useResponsiveConfig.ts | 97 -------- .../src/__tests__/spec-derived-unions.test.ts | 31 ++- .../spec-ui-schema-reexports.test.ts | 15 +- packages/types/src/complex.ts | 13 +- packages/types/src/index.ts | 27 ++- packages/types/src/mobile.ts | 64 +++++- scripts/check-spec-symbol-derivation.mjs | 40 ++++ 13 files changed, 275 insertions(+), 508 deletions(-) create mode 100644 .changeset/7580-responsive-vocabulary-rehome.md delete mode 100644 packages/core/src/protocols/ResponsiveProtocol.ts delete mode 100644 packages/mobile/src/__tests__/responsive-config-spec-parity.test.ts delete mode 100644 packages/mobile/src/useResponsiveConfig.ts diff --git a/.changeset/7580-responsive-vocabulary-rehome.md b/.changeset/7580-responsive-vocabulary-rehome.md new file mode 100644 index 0000000000..08626f0ba3 --- /dev/null +++ b/.changeset/7580-responsive-vocabulary-rehome.md @@ -0,0 +1,52 @@ +--- +'@object-ui/types': minor +'@object-ui/layout': minor +'@object-ui/mobile': minor +'@object-ui/core': minor +--- + +Re-home the breakpoint layout vocabulary and delete the two dead responsive +implementations (objectui#7580, maintainer ruling 2026-09-04, option A). + +**Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole +`ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, +`BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the +stated ground that the four types "had no other authorable carrier". That ground is +measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component +whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by +`resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in +`@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and +when a renderer implements it" — is already met here, so the two types a renderer reads +are re-homed rather than retired. + +What survives, under the same names and the same members: + +- `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead + of re-exported from the spec. **No consumer change**: same name, same six members, same + export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. +- `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), + verbatim from the retired `$strict` schema: six optional column counts, no index + signature. `responsive-grid`'s `columns` input and its resolver are unchanged. + +What is removed: + +- `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had + no read point in the package; it was published only because the retired + `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` + needed the type. With the schema gone there is no order vocabulary for it to be the type + of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. +- `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and + `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with + `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / + `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired + `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). +- `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once + the two implementations above went, dropped rather than re-declared locally, the same + disposition the retired i18n names in that file already carry. + +No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, +`ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and +`responsive-grid` itself — are untouched. + +**Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` +objectui#7580: the retirement is merged upstream and unreleased, so this must land first. diff --git a/packages/core/src/protocols/ResponsiveProtocol.ts b/packages/core/src/protocols/ResponsiveProtocol.ts deleted file mode 100644 index 3ffa4fe4e6..0000000000 --- a/packages/core/src/protocols/ResponsiveProtocol.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * @object-ui/core - Responsive Protocol Bridge - * - * Converts spec-aligned ResponsiveConfig schemas into Tailwind CSS - * utility classes for visibility, grid columns, and ordering across - * breakpoints. Also provides runtime width-based visibility checks. - * - * @module protocols/ResponsiveProtocol - * @packageDocumentation - */ - -import type { SpecResponsiveConfig } from '@object-ui/types'; - -// ============================================================================ -// Breakpoint Definitions -// ============================================================================ - -/** Breakpoint name type matching Tailwind defaults. */ -export type BreakpointKey = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; - -/** Breakpoint minimum pixel widths aligned with Tailwind CSS defaults. */ -export const BREAKPOINT_VALUES: Record = { - xs: 0, - sm: 640, - md: 768, - lg: 1024, - xl: 1280, - '2xl': 1536, -}; - -/** Ordered breakpoint keys from smallest to largest. */ -const BREAKPOINT_ORDER: BreakpointKey[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl']; - -// ============================================================================ -// Resolved Types -// ============================================================================ - -/** Fully resolved responsive configuration. */ -export interface ResolvedResponsiveConfig { - breakpoint?: BreakpointKey; - hiddenOn: BreakpointKey[]; - columns: Partial>; - order: Partial>; -} - -// ============================================================================ -// Config Resolution -// ============================================================================ - -/** - * Resolve a responsive configuration by applying defaults. - * - * @param config - SpecResponsiveConfig from the spec - * @returns Fully resolved responsive configuration - */ -export function resolveResponsiveConfig(config: SpecResponsiveConfig): ResolvedResponsiveConfig { - return { - breakpoint: config.breakpoint as BreakpointKey | undefined, - hiddenOn: (config.hiddenOn ?? []) as BreakpointKey[], - columns: (config.columns ?? {}) as Partial>, - order: (config.order ?? {}) as Partial>, - }; -} - -// ============================================================================ -// Visibility Classes -// ============================================================================ - -/** - * Generate Tailwind CSS classes for responsive visibility. - * - * If `breakpoint` is set, the element is hidden below that breakpoint - * (e.g. breakpoint "md" → `['hidden', 'md:block']`). - * - * If `hiddenOn` contains breakpoints, the element is hidden at those - * specific sizes (e.g. hiddenOn: ["sm", "lg"] → `['sm:hidden', 'md:block', 'lg:hidden', 'xl:block']`). - * - * @param config - SpecResponsiveConfig from the spec - * @returns Array of Tailwind CSS class strings - */ -export function getVisibilityClasses(config: SpecResponsiveConfig): string[] { - const classes: string[] = []; - - // Minimum breakpoint visibility - if (config.breakpoint) { - const bp = config.breakpoint as BreakpointKey; - if (bp !== 'xs') { - classes.push('hidden'); - classes.push(`${bp}:block`); - } - } - - // Per-breakpoint hidden overrides - const hiddenOn = (config.hiddenOn ?? []) as BreakpointKey[]; - if (hiddenOn.length > 0) { - for (let i = 0; i < BREAKPOINT_ORDER.length; i++) { - const bp = BREAKPOINT_ORDER[i]; - const isHidden = hiddenOn.includes(bp); - const prevHidden = i > 0 ? hiddenOn.includes(BREAKPOINT_ORDER[i - 1]) : false; - - if (isHidden && !prevHidden) { - classes.push(bp === 'xs' ? 'hidden' : `${bp}:hidden`); - } else if (!isHidden && prevHidden) { - classes.push(bp === 'xs' ? 'block' : `${bp}:block`); - } - } - } - - return classes; -} - -// ============================================================================ -// Column Classes -// ============================================================================ - -/** - * Generate Tailwind grid-cols classes for responsive column layouts. - * - * @param config - SpecResponsiveConfig from the spec - * @returns Array of Tailwind CSS grid column class strings - */ -export function getColumnClasses(config: SpecResponsiveConfig): string[] { - const classes: string[] = []; - const columns = (config.columns ?? {}) as Partial>; - - for (const bp of BREAKPOINT_ORDER) { - const cols = columns[bp]; - if (cols == null) continue; - const prefix = bp === 'xs' ? '' : `${bp}:`; - classes.push(`${prefix}grid-cols-${cols}`); - } - - return classes; -} - -// ============================================================================ -// Order Classes -// ============================================================================ - -/** - * Generate Tailwind order utility classes for responsive ordering. - * - * @param config - SpecResponsiveConfig from the spec - * @returns Array of Tailwind CSS order class strings - */ -export function getOrderClasses(config: SpecResponsiveConfig): string[] { - const classes: string[] = []; - const order = (config.order ?? {}) as Partial>; - - for (const bp of BREAKPOINT_ORDER) { - const ord = order[bp]; - if (ord == null) continue; - const prefix = bp === 'xs' ? '' : `${bp}:`; - classes.push(`${prefix}order-${ord}`); - } - - return classes; -} - -// ============================================================================ -// Runtime Width Check -// ============================================================================ - -/** - * Determine whether a component should be hidden at a given viewport width. - * - * Checks both the minimum `breakpoint` threshold and the `hiddenOn` list. - * - * @param config - SpecResponsiveConfig from the spec - * @param width - Current viewport width in pixels - * @returns `true` if the component should be hidden at the given width - */ -export function shouldHideAtBreakpoint(config: SpecResponsiveConfig, width: number): boolean { - // Check minimum breakpoint - if (config.breakpoint) { - const minWidth = BREAKPOINT_VALUES[config.breakpoint as BreakpointKey]; - if (minWidth !== undefined && width < minWidth) { - return true; - } - } - - // Check hiddenOn list - const hiddenOn = (config.hiddenOn ?? []) as BreakpointKey[]; - if (hiddenOn.length > 0) { - const currentBp = getCurrentBreakpoint(width); - return hiddenOn.includes(currentBp); - } - - return false; -} - -/** - * Determine the current breakpoint name for a given width. - */ -function getCurrentBreakpoint(width: number): BreakpointKey { - for (let i = BREAKPOINT_ORDER.length - 1; i >= 0; i--) { - if (width >= BREAKPOINT_VALUES[BREAKPOINT_ORDER[i]]) { - return BREAKPOINT_ORDER[i]; - } - } - return 'xs'; -} diff --git a/packages/core/src/protocols/index.ts b/packages/core/src/protocols/index.ts index 86b2596b26..36b2885ad2 100644 --- a/packages/core/src/protocols/index.ts +++ b/packages/core/src/protocols/index.ts @@ -16,5 +16,13 @@ export * from './KeyboardProtocol.js'; // which declares its own `NotificationSystemConfig` and is what every surface // reads. Re-typing the bridge would have meant re-declaring a vocabulary the // spec had just retired (AGENTS.md #0.1). -export * from './ResponsiveProtocol.js'; +// `ResponsiveProtocol` removed in the objectstack#11027 uptake (objectui#7580): +// it bridged `@objectstack/spec/ui`'s `ResponsiveConfig`, which that retirement +// deleted with no successor, and it had zero importers in this repo (measured on +// objectui#4773; `docs/audits/2901-spec-enum-renderer-coverage.md` had already +// recorded it as one of the zero-importer protocols). Re-typing the bridge would +// have meant re-declaring a vocabulary the spec had just retired, for a reader +// that does not exist — AGENTS.md #0.1, and the same disposition +// `NotificationProtocol` got above. The layout vocabulary that DOES have a +// renderer is re-homed instead, in `@object-ui/types` and `@object-ui/layout`. export * from './SharingProtocol.js'; diff --git a/packages/layout/src/ResponsiveGrid.tsx b/packages/layout/src/ResponsiveGrid.tsx index fe56f6b8e5..7101c12eaa 100644 --- a/packages/layout/src/ResponsiveGrid.tsx +++ b/packages/layout/src/ResponsiveGrid.tsx @@ -8,28 +8,56 @@ import React from 'react'; import { cn } from '@object-ui/components'; -import type { BreakpointColumnMap, BreakpointOrderMap } from '@objectstack/spec/ui'; /** - * Breakpoint column map (`BreakpointColumnMapSchema`) and breakpoint order map - * (`BreakpointOrderMapSchema`), both re-exported from `@objectstack/spec/ui`. + * Breakpoint column map — grid column counts per Tailwind-style breakpoint, + * owned by this package since objectui#7580 (maintainer ruling 2026-09-04, + * option A). * * These were hand copies until objectui#4167, each carrying the word "mirrors" * — the doc-comment shape objectstack#4115 catalogues, where the claim is what - * the next agent reads as canonical and the code is what actually decides. rc.6 - * publishes both names, and both copies happened to still be exact: six - * optional numbers keyed `xs`…`2xl`, on a `$strict` schema. "Still exact" is - * the argument FOR binding them, not against — the copies had nothing to - * protect, so the only thing they could do from here was drift, and a - * breakpoint the spec adds now arrives instead of silently not existing. + * the next agent reads as canonical and the code is what actually decides. They + * were then bound by reference to `@objectstack/spec/ui`, which was right for + * as long as the spec published them. * - * `BreakpointOrderMap` has no read point in this package (`ResponsiveGrid` - * resolves columns only); it is published because `ResponsiveConfigSchema` - * pairs the two and an author configuring order needs the type. Bound rather - * than deleted for that reason — deleting a published type is a separate - * decision from stopping it being a fork. + * objectstack#11027 retired the whole `ui/responsive` vocabulary upstream, so + * there is no longer anything to bind to — and, unlike the four types that left + * with the key, this one has an authorable carrier that survives the + * retirement. `responsive-grid` is a REGISTERED SDUI component (`./index.ts`), + * its `columns` input is authorable, and {@link resolveColumnClasses} applies + * it on the render path. The tombstone's own return condition — the vocabulary + * "returns if and when a renderer implements it" — is met here, which is why + * the type is re-homed rather than deleted. + * + * ⚠️ Members are the retired `BreakpointColumnMapSchema`'s, verbatim: six + * optional column counts keyed `xs`…`2xl`. The schema was `$strict`, so no + * index signature is restored here either — an unknown breakpoint key was a + * parse error upstream and stays a type error here. + * + * `BreakpointOrderMap` RETIRED with the key (same ruling, item 3) and is NOT + * re-homed. Its own comment in this file already recorded the reason: it had no + * read point in this package — `ResponsiveGrid` resolves columns only — and it + * was published solely because the spec's `ResponsiveConfigSchema` paired the + * two, so an author configuring `order` needed the type. That pairing is what + * objectstack#11027 deleted; with the schema gone there is no order vocabulary + * left for it to be the type OF, and re-homing it would have re-declared a key + * nothing reads on either side of the boundary — the declare-without-enforce + * shape ADR-0049 removes. + * + * ⏳ Interim, and it self-expires: the pin is still `@objectstack/spec` 17.2.0, + * which PRE-dates the retirement and still exports this name, so the collision + * is real today and carries a reasoned entry in + * `scripts/check-spec-symbol-derivation.mjs`. The guard's ratchet 3 fails an + * ALLOW entry that excuses nothing, so the pin bump is forced to delete it. */ -export type { BreakpointColumnMap, BreakpointOrderMap }; +export interface BreakpointColumnMap { + xs?: number; + sm?: number; + md?: number; + lg?: number; + xl?: number; + '2xl'?: number; +} export interface ResponsiveGridProps { /** Grid column map per breakpoint */ diff --git a/packages/mobile/src/__tests__/responsive-config-spec-parity.test.ts b/packages/mobile/src/__tests__/responsive-config-spec-parity.test.ts deleted file mode 100644 index 0d85de9258..0000000000 --- a/packages/mobile/src/__tests__/responsive-config-spec-parity.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * `@object-ui/mobile`'s `SpecResponsiveConfig` IS the schema's type (objectui#4598). - * - * The card: `useResponsiveConfig.ts` hand-declared an interface over the four - * responsive keys, renamed off the schema's own symbol (`ResponsiveConfig` → - * `SpecResponsiveConfig`), under a comment saying it mirrored - * `ResponsiveConfigSchema`. Nothing tied the two together — no import, no - * `z.infer`, no compile-time reference of any kind. It AGREED key-for-key when - * it was filed, which is the whole point: the agreement was maintained by - * nobody and checked by nothing, and the comment already told the next session - * the copy was canonical. `ViewNavigationConfig` (objectui#4588) read exactly - * like that until it had drifted on `mode`. - * - * The fix re-exports the type from `@object-ui/types` — this package's only - * runtime dependency, which publishes it imported straight from - * `@objectstack/spec/ui` — so no new dependency edge was needed and the - * published name did not move. - * - * ── Why this file exists, given the fix is a re-export ────────────────────── - * - * `scripts/check-spec-symbol-derivation.mjs` stops reporting the declaration - * because after the fix there is no declaration: rule 2 collects type aliases, - * interfaces, enums and variables, and a bare `export type { … }` is none of - * those. That is the right outcome — the hand copy is gone — but the gate got - * there by seeing nothing, not by following the chain. It cannot: the binding - * now runs mobile → `@object-ui/types` → `@objectstack/spec/ui`, and the gate - * reads one package at a time. - * - * So the middle link is the part no gate covers, and it is the only place this - * fix can still rot: if `@object-ui/types` ever replaces its re-export with a - * hand copy of its own, mobile inherits the copy silently and every check in - * the repo stays green. The assertions below are written against - * `@objectstack/spec/ui` DIRECTLY for that reason — pinning against - * `@object-ui/types` would compare the import to itself and pass no matter what - * either package did. - * - * The type-level half runs under `tsc -p tsconfig.test.json` (this package's - * `type-check`), so a re-grown copy fails the build rather than waiting for a - * reviewer. `@objectstack/spec` is a devDependency here — a test may import it; - * the published `.d.ts` may not, which is why the source re-exports through - * `@object-ui/types` instead of reaching for the spec directly. - */ - -import { describe, it, expect } from 'vitest'; -import { ResponsiveConfigSchema, type ResponsiveConfig } from '@objectstack/spec/ui'; -import type { SpecResponsiveConfig } from '../index'; - -/* ── Type-level helpers ──────────────────────────────────────────────────── */ - -/** - * Invariant equality. `extends` in one direction — or a `satisfies` check — - * would accept a NARROWING, so a copy that quietly dropped `order` would still - * pass. That is the exact drift this card is about, so the pin has to be - * invariant to say anything at all. - */ -type Equal< A, B > = - (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; -type Expect< T extends true > = T; - -/* ── The binding itself ──────────────────────────────────────────────────── */ - -/** - * The whole card in one line: what `@object-ui/mobile` publishes under - * `SpecResponsiveConfig` and what the schema declares are ONE type. Red on the - * day any link in mobile → `@object-ui/types` → `@objectstack/spec/ui` is - * replaced by a copy, however byte-identical it starts out. - */ -type _IsExactlyTheSchemaType = Expect< Equal< SpecResponsiveConfig, ResponsiveConfig > >; - -/** - * The four keys `ResponsiveConfigSchema` declares. Adding one here to make a - * local read compile is the defect rather than the fix: the key has to exist in - * the schema first, or the platform refuses metadata that declares it. - */ -type SchemaDeclaredKeys = 'breakpoint' | 'hiddenOn' | 'columns' | 'order'; - -type _KeysAreExactlyTheSchemaFour = Expect< Equal< keyof SpecResponsiveConfig, SchemaDeclaredKeys > >; - -/** Every key is optional on the authoring side — none of the four is required. */ -type _AllFourAreOptional = Expect< - Equal< SpecResponsiveConfig, Partial< SpecResponsiveConfig > > ->; - -/** - * The breakpoint vocabulary is the schema's six, reached through the same - * chain. `NonNullable` keeps this pointed at which names exist rather than at - * the `| undefined` the optionality puts there on purpose. - */ -type _BreakpointVocabularyIsTheSchemaSix = Expect< - Equal< - NonNullable< SpecResponsiveConfig['breakpoint'] >, - 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' - > ->; - -/* ── Runtime half ────────────────────────────────────────────────────────── */ - -// The type-level assertions above are erased before anything runs, so the -// checks below re-ask the same questions of the schema VALUE. Without them this -// file could keep compiling while the runtime contract moved underneath it. -describe('SpecResponsiveConfig is the spec responsive config (objectui#4598)', () => { - const schemaKeys = Object.keys(ResponsiveConfigSchema.shape).sort(); - - it('reads the four declared keys off the schema itself', () => { - expect(schemaKeys, 'could not read ResponsiveConfigSchema.shape').toEqual( - ['breakpoint', 'columns', 'hiddenOn', 'order'], - ); - }); - - it('accepts a fully-populated config, and the schema agrees', () => { - const full: SpecResponsiveConfig = { - breakpoint: 'md', - hiddenOn: ['xs', 'sm'], - columns: { xs: 12, sm: 6, lg: 4 }, - order: { xs: 2, lg: 1 }, - }; - - expect(Object.keys(full).sort()).toEqual(schemaKeys); - expect(ResponsiveConfigSchema.parse(full)).toEqual(full); - }); - - it('accepts the empty config — every key is optional', () => { - const empty: SpecResponsiveConfig = {}; - expect(ResponsiveConfigSchema.parse(empty)).toEqual({}); - }); - - it('carries `2xl`, the breakpoint a five-name copy would be missing', () => { - const widest: SpecResponsiveConfig = { breakpoint: '2xl', columns: { '2xl': 3 } }; - expect(ResponsiveConfigSchema.parse(widest)).toEqual(widest); - }); - - it('refuses a breakpoint outside the schema enum', () => { - const bad: SpecResponsiveConfig = { - // @ts-expect-error 'xxl' is not one of the six declared breakpoint names - breakpoint: 'xxl', - }; - expect(() => ResponsiveConfigSchema.parse(bad)).toThrow(); - }); - - it('refuses a key the schema does not declare', () => { - const bad: SpecResponsiveConfig = { - breakpoint: 'lg', - // @ts-expect-error `visibleOn` is not a declared responsive key - visibleOn: ['xs'], - }; - // The schema is strict, so the undeclared key is a runtime rejection too — - // the type and the schema refuse the same authored metadata. - expect(() => ResponsiveConfigSchema.parse(bad)).toThrow(); - }); -}); diff --git a/packages/mobile/src/index.ts b/packages/mobile/src/index.ts index 370b3f5c2e..d837454233 100644 --- a/packages/mobile/src/index.ts +++ b/packages/mobile/src/index.ts @@ -21,7 +21,13 @@ export { useBreakpoint, type BreakpointState } from './useBreakpoint.js'; export { useResponsive } from './useResponsive.js'; -export { useResponsiveConfig, type SpecResponsiveConfig, type ResolvedResponsiveState } from './useResponsiveConfig.js'; +// `useResponsiveConfig` REMOVED (objectui#7580, maintainer ruling 2026-09-04). +// It read `@objectstack/spec`'s `ResponsiveConfigSchema`, which objectstack#11027 +// retired; it was measured at zero callers on objectui#4773. Its exported +// `SpecResponsiveConfig` / `ResolvedResponsiveState` went with it — both named +// the retired schema's shape and neither had a reader. No behaviour is retired: +// the live per-breakpoint readers are `useBreakpoint` and +// `ResponsiveContainer` below, which are untouched. export { useGesture, type UseGestureOptions } from './useGesture.js'; export { useSpecGesture, type UseSpecGestureOptions } from './useSpecGesture.js'; export { useTouchTarget, type UseTouchTargetOptions, type TouchTargetResult } from './useTouchTarget.js'; diff --git a/packages/mobile/src/useResponsiveConfig.ts b/packages/mobile/src/useResponsiveConfig.ts deleted file mode 100644 index 5bacbd3177..0000000000 --- a/packages/mobile/src/useResponsiveConfig.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { useMemo } from 'react'; -import { useBreakpoint } from './useBreakpoint.js'; -import { resolveResponsiveValue } from './breakpoints.js'; -import type { BreakpointName, SpecResponsiveConfig } from '@object-ui/types'; - -/** - * The responsive layout config this hook consumes — re-exported, not re-declared - * (objectui#4598). - * - * This used to be a hand-written interface over the same four keys, under a - * comment asserting it mirrored the schema. The assertion was true on the day it - * was written and maintained by nobody after that: the interface named the - * schema without ever referring to it, so a key added or retired upstream would - * have moved the two apart in silence. `ViewNavigationConfig` (objectui#4588) - * read the same way until it had drifted on `mode`. - * - * `@object-ui/types` — already this package's only runtime dependency — publishes - * the schema's own type at `index.ts` under this exact name, imported from - * `@objectstack/spec/ui` rather than copied. Re-exporting it costs no new - * dependency edge and leaves the published name unchanged, so the four keys are - * now whatever the schema says they are rather than whatever this file last - * remembered. `@object-ui/core`'s `ResponsiveProtocol` already binds through the - * same re-export. - * - * `responsive-config-spec-parity.test.ts` pins the chain to the schema itself, - * because the one link this file cannot see is `@object-ui/types` re-growing a - * hand copy of its own. - * - * @example - * ```ts - * const config: SpecResponsiveConfig = { - * columns: { xs: 12, sm: 6, lg: 4 }, - * hiddenOn: ['xs'], - * order: { xs: 2, lg: 1 }, - * }; - * ``` - */ -export type { SpecResponsiveConfig }; - -/** - * Resolved responsive state from a SpecResponsiveConfig. - */ -export interface ResolvedResponsiveState { - /** Whether the component is hidden at the current breakpoint */ - hidden: boolean; - /** Resolved column count for the current breakpoint */ - columns: number | undefined; - /** Resolved display order for the current breakpoint */ - order: number | undefined; - /** Current active breakpoint name */ - breakpoint: BreakpointName; -} - -/** - * Hook that consumes @objectstack/spec ResponsiveConfigSchema and - * resolves breakpoint-aware layout state. - * - * @example - * ```tsx - * const { hidden, columns, order } = useResponsiveConfig({ - * columns: { xs: 12, sm: 6, lg: 4 }, - * hiddenOn: ['xs'], - * order: { xs: 2, lg: 1 }, - * }); - * if (hidden) return null; - * return
...
; - * ``` - */ -export function useResponsiveConfig(config?: SpecResponsiveConfig): ResolvedResponsiveState { - const { breakpoint } = useBreakpoint(); - - return useMemo(() => { - if (!config) { - return { hidden: false, columns: undefined, order: undefined, breakpoint }; - } - - const hidden = config.hiddenOn?.includes(breakpoint) ?? false; - - const columns = config.columns - ? resolveResponsiveValue(config.columns, breakpoint) - : undefined; - - const order = config.order - ? resolveResponsiveValue(config.order, breakpoint) - : undefined; - - return { hidden, columns, order, breakpoint }; - }, [config, breakpoint]); -} diff --git a/packages/types/src/__tests__/spec-derived-unions.test.ts b/packages/types/src/__tests__/spec-derived-unions.test.ts index cc2b6faa89..a56b404a6c 100644 --- a/packages/types/src/__tests__/spec-derived-unions.test.ts +++ b/packages/types/src/__tests__/spec-derived-unions.test.ts @@ -131,11 +131,40 @@ const _resolvableCovers = null as unknown as ActionParamFieldType satisfies Reso // since objectui#3009 made this file compile.) const _fieldBackedParam: ActionParam = { field: 'status' }; const _minimalTypedParam: ActionParam = { name: 'priority', label: 'Priority', type: 'select' }; +// `BreakpointName` LEFT the burn-down population below — objectui#7580. +// +// It was in it: a local fork PROVED equivalent to the spec's and then replaced +// by a binding. objectstack#11027 then retired the spec's whole `ui/responsive` +// vocabulary, and the maintainer ruling of 2026-09-04 (option A) re-homed this +// union into `../mobile` rather than retiring it with the key, because +// `responsive-grid` is a registered SDUI component whose authorable `columns` +// reaches a resolver on the render path — the tombstone's own return condition. +// +// So the assertion below is NOT the burn-down check any more, and this is the +// restatement rather than a deletion: it never read the spec for this symbol +// (it imports `BreakpointName` from `../mobile`, and a type alias erases, so +// there was never a runtime witness to compare). What it pins is the union's +// WIDTH, and that pin outlives the binding it was written under — the six +// members are what `BreakpointColumnMap` is keyed by, so narrowing the local +// declaration fails to compile here rather than silently dropping a breakpoint. +// Deleting the case because its ORIGINAL reason expired would have removed live +// coverage on the exact declaration the ruling just made this repo responsible +// for. +// +// Where it goes next: at the `@objectstack/spec` pin bump that lands +// objectstack#11027, this name joins the INVERSE population documented at the +// top of this file — names objectui uses that the spec does NOT have, each +// asserted absent so that the day the spec re-adopts one, this file fails. That +// absence cannot be asserted yet: the pin is still 17.2.0, which PRE-dates the +// retirement and still exports the name. Until then the collision is held by a +// self-expiring entry in `scripts/check-spec-symbol-derivation.mjs`, whose +// ratchet 3 forces the bump to come back here. +const _breakpointCovers = null as unknown as 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' satisfies BreakpointName; + // objectstack#4115 ledger burn-down: the symbols whose local declaration was // PROVED equivalent to the spec's and then replaced by a binding. Each listed // member is what the local fork carried, so re-declaring it narrower fails here // as well as at the guard. -const _breakpointCovers = null as unknown as 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' satisfies BreakpointName; const _importModeCovers = null as unknown as 'insert' | 'update' | 'upsert' satisfies ImportWriteMode; const _importStatusCovers = null as unknown as | 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' satisfies ImportJobStatus; diff --git a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts index f3f0644747..f3c3e9988d 100644 --- a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts +++ b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts @@ -102,9 +102,18 @@ const DROPPED_SCHEMA_EXPORTS = [ // per-locale record form into `I18nLabel` itself (`string` → // `string | Record`) and ships `resolveI18nLabel` for it. // Responsive Design - 'SpecResponsiveConfigSchema', - 'BreakpointColumnMapSchema', - 'BreakpointOrderMapSchema', + // The three responsive rows — `SpecResponsiveConfigSchema`, + // `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — were removed + // by objectui#7580, ahead of the ratchet below firing, on the same reading + // that took the `ThemeModeSchema` row out at objectui#5716. objectstack#11027 + // retired the spec's whole `ui/responsive` vocabulary, and this repo no + // longer re-exports ANY of it: `BreakpointName` is declared locally in + // `../mobile.ts` and `BreakpointColumnMap` in `@object-ui/layout`, while the + // prefixed `SpecResponsiveConfig` pair was dropped from `../index.ts` as a + // dead re-export once its two readers went. So each row was about to assert + // that a name is not re-exported from this package while the name itself no + // longer exists anywhere — vacuously true forever, which is precisely what + // the ratchet above this list exists to catch. // The `ThemeModeSchema` row (theme.ts) was removed by objectui#5716, ahead // of the ratchet below firing: the spec retired its whole theme module // (objectstack#10485) and the theme TYPE surface is owned by this package diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 68d79a4af8..a4b8534e6d 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -1034,9 +1034,16 @@ export interface DashboardWidgetSchema // objectui#3173's measurement found zero `widget.responsive` read points in // the whole repo and zero authored occurrences in either corpus, so that // premise was false and the override only made TS accept a key the Zod twin - // already refused. The shared `ResponsiveConfig` shape is NOT gone — it - // stays live on `page.components[].responsive`, which `useResponsiveConfig` - // really does read. + // already refused. ⚠️ This note used to add that the shared `ResponsiveConfig` + // shape was "NOT gone — it stays live on `page.components[].responsive`, + // which `useResponsiveConfig` really does read". Both halves of that have + // since expired and it is corrected rather than left standing, because a + // stale liveness claim is what the next agent reads as the measurement: + // objectstack#11027 retired `ResponsiveConfigSchema` outright (the census + // behind it measured `page.components[].responsive` inert), and objectui#7580 + // deleted `useResponsiveConfig` with it at zero callers. What survives the + // retirement is the BREAKPOINT vocabulary, re-homed into `@object-ui/types` + // and `@object-ui/layout` because `responsive-grid` renders it — not this key. // Pinned by `__tests__/report-chart-query-spec-parity.test.ts`. /** Component schema (legacy format) — objectui-only, no spec counterpart. */ component?: SchemaNode; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 95ca04dcae..cd3134a9a6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1343,15 +1343,24 @@ export type { } from '@objectstack/spec/ui'; // ============================================================================ -// v2.0.7 Spec UI Types — Responsive Design -// ============================================================================ -export type { - ResponsiveConfig as SpecResponsiveConfig, - BreakpointName as SpecBreakpointName, - // BreakpointColumnMapSchema / BreakpointOrderMapSchema dropped without a - // replacement: they are zod values (value-erased here, #2561) and the spec - // exports no companion inferred type for them. -} from '@objectstack/spec/ui'; +// v2.0.7 Spec UI Types — Responsive Design: RETIRED UPSTREAM, dropped here +// ============================================================================ +// `SpecResponsiveConfig` (the spec's `ResponsiveConfig`) and +// `SpecBreakpointName` (its `BreakpointName`) were re-exported from this +// block under deliberately prefixed names. objectstack#11027 retired the whole +// `ui/responsive` vocabulary, and objectui#7580 removed the only two things in +// this repo that read the prefixed pair: `@object-ui/core`'s +// `ResponsiveProtocol` and `@object-ui/mobile`'s `useResponsiveConfig`, both +// measured at zero callers. So both rows are DEAD re-exports and are dropped +// rather than re-declared locally — the same disposition, for the same reason, +// as the five retired i18n names in the block above. +// +// The layout vocabulary itself is NOT dropped: `BreakpointName` is declared +// locally in `./mobile.ts` and re-exported unprefixed from the Mobile block +// above, and `BreakpointColumnMap` in `@object-ui/layout`'s +// `ResponsiveGrid.tsx` — both because `responsive-grid` is a registered SDUI +// component whose authorable `columns` reaches a resolver on the render path, +// which is the tombstone's own stated return condition. // ============================================================================ // Widget System - Runtime Widget Registration (Section 1.6) diff --git a/packages/types/src/mobile.ts b/packages/types/src/mobile.ts index c27cc93c26..535a3f2179 100644 --- a/packages/types/src/mobile.ts +++ b/packages/types/src/mobile.ts @@ -16,20 +16,57 @@ * @packageDocumentation */ -import type { BreakpointName } from '@objectstack/spec/ui'; - // ============================================================================ // Responsive Configuration // ============================================================================ /** - * Breakpoint names. + * Breakpoint names — the Tailwind-style `xs`…`2xl` layout vocabulary, owned by + * this package since objectui#7580 (maintainer ruling 2026-09-04, option A). + * + * ## Answering objectstack#4115 rather than deleting it + * + * This was bound to `@objectstack/spec/ui` instead of re-declared, under the + * reason objectstack#4115 recorded here verbatim: "a local union under a spec + * export's name is read by the next reader as the spec's own definition, so a + * copy that is correct today is a planted premise tomorrow." That reason was + * correct, and it is now SPENT — not overruled. + * + * objectstack#11027 retired the whole `ui/responsive` vocabulary upstream + * (`ResponsiveConfigSchema`, `BreakpointName`, `BreakpointColumnMapSchema`, + * `BreakpointOrderMapSchema`), leaving a tombstone and the protocol-18 + * conversion in `RETIRED_DEFS_BY_MAJOR[18]`. So there is no spec definition + * left for a reader to mistake this one for: this is not a copy that may drift + * from an original, it is the only declaration of the name that will exist. + * A planted premise needs something to be wrong ABOUT. * - * Bound to the spec rather than re-declared (objectstack#4115): a local union - * under a spec export's name is read by the next reader as the spec's own - * definition, so a copy that is correct today is a planted premise tomorrow. + * ## Why re-homed and not retired with the key + * + * The retirement's stated ground — that these types "had no other authorable + * carrier" — is a claim about the whole surface, and it is measurably false on + * this side. `responsive-grid` is a REGISTERED SDUI component (see + * `@object-ui/layout`'s `index.ts`) whose authorable `columns` input is typed + * by the sibling `BreakpointColumnMap` and applied by `resolveColumnClasses` on + * the render path. This union types four live readers here: `breakpoints.ts` + * (`BREAKPOINTS`, `BREAKPOINT_ORDER`, `getCurrentBreakpoint`), + * `useBreakpoint.ts`, `ResponsiveContainer.tsx`, and {@link ResponsiveValue} + * below. The tombstone's own return condition — the vocabulary "returns if and + * when a renderer implements it" — is already met over here. + * + * ⚠️ Members are the retired enum's, verbatim, and must stay so: the six + * `xs`…`2xl` keys that `BreakpointColumnMap` is keyed by. + * `__tests__/spec-derived-unions.test.ts` pins the width, so narrowing this + * union fails to compile rather than silently dropping a breakpoint. + * + * ⏳ Interim, and it self-expires: the pin is still `@objectstack/spec` 17.2.0, + * which PRE-dates the retirement and so still exports this name. The collision + * is therefore real today and carries a reasoned entry in + * `scripts/check-spec-symbol-derivation.mjs`. That entry cannot outlive the + * interval — the guard's ratchet 3 fails an ALLOW entry that excuses nothing — + * so the pin bump is forced to delete it and pin the vacancy instead, exactly + * as objectui#5716's theme localization was on the 17.2.0 refresh. */ -export type { BreakpointName }; +export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; /** Responsive value - different values for different breakpoints */ export type ResponsiveValue = T | Partial>; @@ -38,9 +75,16 @@ export type ResponsiveValue = T | Partial>; * Responsive layout configuration for the mobile renderer's box layout. * * Renamed off the spec's `ResponsiveConfig` name (objectstack#4115): the two - * configure responsiveness through different vocabularies, and this package - * already re-exports the spec's own under `SpecResponsiveConfig`, so the bare - * name claimed an authority it did not have. + * configure responsiveness through different vocabularies, so the bare name + * claimed an authority it did not have. + * + * ⚠️ The rename's second stated ground — that this package "already re-exports + * the spec's own under `SpecResponsiveConfig`" — expired at objectui#7580 and is + * recorded rather than silently dropped, because it is the kind of sentence a + * later reader mistakes for a live measurement. objectstack#11027 retired the + * spec `ResponsiveConfig` outright and that prefixed re-export went with its two + * readers. The rename still stands on the FIRST ground alone: the two + * vocabularies genuinely differ, as the paragraph below sets out key by key. * * The spec's `ResponsiveConfig` is the SDUI grid contract — * `{ breakpoint, hiddenOn, columns: {xs..2xl}, order: {xs..2xl} }` — arranging a diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index 9b6d6ef963..eba8bcac3a 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -579,6 +579,46 @@ const ALLOW = { "case this map exists to make someone write a reason for.", issue: 4115, }, + // ── Re-homed layout vocabulary, objectui#7580 (ruling 2026-09-04, option A) ── + // Both entries below are SELF-EXPIRING, and deliberately so. They are the same + // shape as the three theme document types noted at the end of this map: a + // maintainer ruling localized a vocabulary whose upstream retirement is MERGED + // but not yet RELEASED, so for the length of one pin interval the local + // declaration and a live spec export share a name. Ratchet 3 fails an ALLOW + // entry that excuses nothing, so the pin bump that lands objectstack#11027 + // cannot leave either entry behind — it must delete them and pin the vacancy + // where it can execute, exactly as objectui#5668 did for the theme trio. + // + // ⛔ Neither is a deliberate-divergence waiver, and neither may be renewed on + // that reading: the local declarations are the retired spec members VERBATIM. + // The reason they are here is the interval, not a difference. + "@object-ui/types:BreakpointName": { + reason: + "Re-homed from `@objectstack/spec/ui` by the objectui#7580 ruling, NOT a fork: " + + "objectstack#11027 retired the whole `ui/responsive` vocabulary upstream " + + "(tombstone + `RETIRED_DEFS_BY_MAJOR[18]`), and this repo keeps the union because " + + "`responsive-grid` is a REGISTERED SDUI component whose authorable `columns` reaches " + + "`resolveColumnClasses` on the render path — the tombstone's own stated return " + + "condition, met on the renderer side. The collision is an INTERVAL, not a divergence: " + + "the pin is still 17.2.0, which pre-dates the retirement. Members are the retired " + + "enum's verbatim (`xs`…`2xl`), width-pinned by " + + "packages/types/src/__tests__/spec-derived-unions.test.ts. ⛔ Delete this entry on the " + + "pin bump — ratchet 3 will force it — and move the name to the absence pin in " + + "page-nav-misc-spec-parity.test.ts, the disposition objectui#5716/#5668 used for the " + + "theme trio.", + issue: 7580, + }, + "@object-ui/layout:BreakpointColumnMap": { + reason: + "The `BreakpointName` entry above, one package over and for the same interval: the " + + "authorable `columns` input of the registered `responsive-grid` component. Declared " + + "verbatim from the retired `BreakpointColumnMapSchema` — six optional column counts " + + "keyed `xs`…`2xl`, and no index signature, because that schema was `$strict`. Its " + + "twin `BreakpointOrderMap` was NOT re-homed (ruling item 3): it had no read point in " + + "the package and existed only because the retired `ResponsiveConfigSchema` paired the " + + "two. ⛔ Delete this entry on the pin bump; ratchet 3 will force it.", + issue: 7580, + }, // The three theme document types (`Theme`, `ThemeMode`, `ColorPalette`, // objectui#5716 ruling, option A — localize) carried ALLOW entries here from // the localization until the `@objectstack/spec` 17.2.0 refresh