diff --git a/.changeset/theme-types-localized-5716.md b/.changeset/theme-types-localized-5716.md new file mode 100644 index 0000000000..85b3fede0e --- /dev/null +++ b/.changeset/theme-types-localized-5716.md @@ -0,0 +1,19 @@ +--- +'@object-ui/types': minor +'@object-ui/providers': minor +--- + +Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. + +Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): + +- `Typography` — the shape lives on as the inline `Theme['typography']` member. +- `BorderRadius` — lives on as inline `Theme['borderRadius']`. +- `Shadow` — lives on as inline `Theme['shadows']`. +- `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. + +Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). + +The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. + +`@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). diff --git a/packages/providers/src/__tests__/spec-symbol-batch7.test.ts b/packages/providers/src/__tests__/spec-symbol-batch7.test.ts index 69510fdf28..a68d43412f 100644 --- a/packages/providers/src/__tests__/spec-symbol-batch7.test.ts +++ b/packages/providers/src/__tests__/spec-symbol-batch7.test.ts @@ -7,41 +7,38 @@ */ /** - * `@object-ui/providers` ↔ `@objectstack/spec` symbol-collision guard - * (objectui#3161, objectstack#4115 ledger batch 7). + * `ThemePreference` derivation pin + * (objectui#3161, objectstack#4115 ledger batch 7; re-homed by objectui#5716). * - * `Theme` → `ThemePreference`, derived from the spec's `ThemeMode` plus the one - * legacy spelling this provider still honours. + * `Theme` → `ThemePreference`, derived from the theme MODE vocabulary plus the + * one legacy spelling this provider still honours. The batch-7 naming + * judgement is recorded at the declaration in `../types.ts`. * - * The triage note on objectstack#4115 asked whether this `Theme` was the parsed - * runtime theme or the authored JSON. It was neither: it was - * `Theme['mode']` wearing `Theme`'s name — a string union standing where the - * spec has a whole document (`name`, `label`, `colors`, `typography`, …). That - * is the more misleading of the two failure shapes, because a session reading - * it would conclude an ObjectStack theme IS a string. `ThemeMode`, the spec's - * own name for what this really is, could not be taken either: the spec exports - * that too (the objectui#3169 rule — run the candidate past the guard first), - * and `'system'` would have made it a lie anyway. + * This file used to read the vocabulary from the spec's `ThemeModeSchema` and + * carried a designed tripwire: "if the spec retires `Theme`, this stops + * compiling and the rename is up for re-triage". The spec DID retire its + * theme module (objectstack#10485), the re-triage happened, and the + * objectui#5716 ruling made `@object-ui/types` the vocabulary's owner + * (`ThemeMode`, with the `THEME_MODES` runtime witness). So the pins below + * read the new owner — the tripwire fired, was triaged, and is retired. */ import { describe, it, expect } from 'vitest'; -import { ThemeModeSchema } from '@objectstack/spec/ui'; -import type { Theme as SpecTheme, ThemeModeSchema as SpecThemeModeSchema } from '@objectstack/spec/ui'; +import { THEME_MODES } from '@object-ui/types'; +import type { ThemeMode } from '@object-ui/types'; import type { ThemePreference } from '../types'; -const specModes = (ThemeModeSchema as unknown as { options: readonly string[] }).options; - -describe("'system' is this package's own legacy spelling, not the spec's", () => { - it('reads a non-empty mode enum from the spec (the probe is not vacuous)', () => { - expect(Array.isArray(specModes) && specModes.length > 0).toBe(true); +describe("'system' is this package's own legacy spelling, not the vocabulary's", () => { + it('reads a non-empty mode tuple from the owner (the probe is not vacuous)', () => { + expect(Array.isArray(THEME_MODES) && THEME_MODES.length > 0).toBe(true); }); - it('the spec never declared `system` — it spells the OS-following mode `auto`', () => { - expect(specModes).toContain('auto'); + it('the vocabulary never declares `system` — the OS-following mode is spelled `auto`', () => { + expect(THEME_MODES).toContain('auto'); expect( - specModes.includes('system'), - "the spec now declares `system` too — drop the extra union member in types.ts, it is no longer a local alias", + (THEME_MODES as readonly string[]).includes('system'), + 'the mode vocabulary now declares `system` too — drop the extra union member in types.ts, it is no longer a local alias', ).toBe(false); }); }); @@ -53,26 +50,19 @@ describe("'system' is this package's own legacy spelling, not the spec's", () => type Assert = T; type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; type IsAny = 0 extends 1 & T ? true : false; -type HasKey = K extends keyof T ? true : false; - -type SpecThemeMode = (typeof SpecThemeModeSchema)['_zod']['input']; -describe('ThemePreference is the spec mode vocabulary plus one legacy member', () => { +describe('ThemePreference is the mode vocabulary plus one legacy member', () => { it('is pinned at compile time', () => { - // Load-bearing import: if the spec retires `Theme`, this stops compiling - // and the rename is up for re-triage. - type _SpecIsReal = Assert, false>>; - - // The spec's `Theme` is a DOCUMENT — that is the whole reason this union - // could not keep the name. - type _SpecThemeIsADocument = Assert>; - type _AndItHasAModeOfItsOwn = Assert>; + // The owner's type is real (not `any`), and the runtime witness and the + // type are the same vocabulary — the pair cannot drift apart. + type _OwnerIsReal = Assert, false>>; + type _WitnessCarriesTheType = Assert>; - // …and this is exactly that mode, plus `system`. Derived, so a mode the - // spec adds appears here (and `ThemeProvider`'s branch must handle it — - // `theme-mode-spec-parity.test.tsx` fails at runtime if it does not). - type _IsTheSpecModePlusLegacy = Assert>; - type _NothingElseIsLocal = Assert, 'system'>>; + // …and this is exactly that vocabulary, plus `system`. Derived, so a mode + // the owner adds appears here (and `ThemeProvider`'s branch must handle it + // — `theme-mode-spec-parity.test.tsx` fails at runtime if it does not). + type _IsTheModeVocabularyPlusLegacy = Assert>; + type _NothingElseIsLocal = Assert, 'system'>>; expect(true).toBe(true); }); diff --git a/packages/providers/src/__tests__/theme-mode-spec-parity.test.tsx b/packages/providers/src/__tests__/theme-mode-spec-parity.test.tsx index e822c99899..b85a0d196b 100644 --- a/packages/providers/src/__tests__/theme-mode-spec-parity.test.tsx +++ b/packages/providers/src/__tests__/theme-mode-spec-parity.test.tsx @@ -7,18 +7,24 @@ */ /** - * Theme mode ↔ spec vocabulary parity + behavior (#2942). + * Theme mode ↔ vocabulary parity + behavior (#2942). * - * The spec's OS-following mode is `auto` (`ThemeModeSchema`, - * `ui/theme.zod.ts`); this provider branched on its pre-spec `system` - * spelling only, so `mode: 'auto'` fell into `classList.add('auto')` — a - * class no Tailwind variant matches. The page locked to the light theme, - * the OS preference was ignored, and nothing errored. + * The vocabulary's OS-following mode is `auto`; this provider branched on its + * pre-spec `system` spelling only, so `mode: 'auto'` fell into + * `classList.add('auto')` — a class no Tailwind variant matches. The page + * locked to the light theme, the OS preference was ignored, and nothing + * errored. + * + * The vocabulary read here came from the spec's `ThemeModeSchema` until the + * spec retired its theme module (objectstack#10485) and the objectui#5716 + * ruling moved ownership to `@object-ui/types` — `THEME_MODES` is the owner's + * runtime witness, kept as a tuple precisely so this parity coverage stays + * executable (the `SPEC_GESTURE_TYPES` precedent). */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render } from '@testing-library/react'; import React from 'react'; -import { ThemeModeSchema } from '@objectstack/spec/ui'; +import { THEME_MODES } from '@object-ui/types'; import { ThemeProvider } from '../ThemeProvider'; function mockMatchMedia(prefersDark: boolean) { @@ -37,21 +43,19 @@ function mockMatchMedia(prefersDark: boolean) { }); } -describe('ThemeProvider covers the spec theme-mode vocabulary', () => { +describe('ThemeProvider covers the theme-mode vocabulary', () => { beforeEach(() => { document.documentElement.className = ''; window.localStorage?.clear?.(); mockMatchMedia(true); }); - it('reads a non-empty enum from the spec', () => { - const raw = (ThemeModeSchema as unknown as { options?: readonly string[] }).options; - expect(Array.isArray(raw) && raw.length > 0, 'could not read ThemeModeSchema.options').toBe(true); + it('reads a non-empty mode tuple from the owner', () => { + expect(Array.isArray(THEME_MODES) && THEME_MODES.length > 0, 'could not read THEME_MODES').toBe(true); }); - it('every spec mode resolves to a real light/dark class — never a dead literal', () => { - const raw = (ThemeModeSchema as unknown as { options?: readonly string[] }).options ?? []; - for (const mode of raw) { + it('every declared mode resolves to a real light/dark class — never a dead literal', () => { + for (const mode of THEME_MODES) { document.documentElement.className = ''; const { unmount } = render( diff --git a/packages/providers/src/types.ts b/packages/providers/src/types.ts index 7553e5b207..2ca2bad186 100644 --- a/packages/providers/src/types.ts +++ b/packages/providers/src/types.ts @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import type { ThemeModeSchema } from '@objectstack/spec/ui'; +import type { ThemeMode } from '@object-ui/types'; export interface DataSourceProviderProps { dataSource: any; @@ -12,31 +12,33 @@ export interface MetadataProviderProps { } /** - * The theme MODE this provider stores and applies: the spec's `ThemeMode` - * vocabulary (`auto | light | dark`) plus the one legacy spelling this package - * still accepts. + * The theme MODE this provider stores and applies: the `ThemeMode` vocabulary + * (`auto | light | dark`) plus the one legacy spelling this package still + * accepts. * * Named `ThemePreference`, not `Theme` (objectui#3161, objectstack#4115 ledger - * batch 7). `@objectstack/spec/ui` owns `Theme` for a whole theme DOCUMENT — - * `{ name, label, mode, colors: { primary, background, … }, typography, … }` — - * so the local declaration was not a drifted copy of the spec's `Theme` at all; - * it was the spec's `Theme['mode']` under the spec's `Theme` name, which is the - * more misleading of the two failures: a session reading `Theme` here would - * conclude an ObjectStack theme is a string. The spec's own name for this - * concept, `ThemeMode`, is exported too, so THAT name could not be taken either - * (the "run the new name past the guard first" rule from objectui#3169 — it - * would have traded one collision for another); and it would be wrong anyway, - * because of `'system'`. + * batch 7). `Theme` names a whole theme DOCUMENT — `{ name, label, mode, + * colors: { primary, background, … }, typography, … }` — so the old local + * declaration was not a drifted copy of that document type at all; it was + * `Theme['mode']` under `Theme`'s name, which is the more misleading of the + * two failures: a session reading `Theme` here would conclude an ObjectStack + * theme is a string. `ThemeMode`, the vocabulary's own name for what this + * really is, could not be taken either (the "run the new name past the guard + * first" rule from objectui#3169 — it would have traded one collision for + * another); and it would be wrong anyway, because of `'system'`. * - * The three real modes are now the spec enum rather than a hand copy of it, so - * a mode the spec adds arrives here and `ThemeProvider`'s branch must handle - * it. `'system'` stays as an explicit local union member: it is the pre-spec - * spelling of `auto`, still sitting in users' `localStorage`, and the provider - * treats the two identically (#2942, pinned in `theme-mode-spec-parity`). - * Reached through the schema's `_zod` carrier so this package takes no zod - * dependency — same technique as `packages/react/src/spec-input.ts`. + * Derivation history (objectui#5716): this union was born reading the spec's + * `ThemeModeSchema` through its `_zod` input carrier. The spec then retired + * its whole theme module (objectstack#10485), objectui assumed ownership of + * the theme document types, and the mode vocabulary's owner is now + * `@object-ui/types` (`ThemeMode`, with the `THEME_MODES` runtime witness) — + * so the derivation reads from there. A mode the owner adds still arrives + * here and `ThemeProvider`'s branch must handle it. `'system'` stays as an + * explicit local union member: it is the pre-spec spelling of `auto`, still + * sitting in users' `localStorage`, and the provider treats the two + * identically (#2942, pinned in `theme-mode-spec-parity`). */ -export type ThemePreference = (typeof ThemeModeSchema)['_zod']['input'] | 'system'; +export type ThemePreference = ThemeMode | 'system'; export interface ThemeProviderProps { defaultTheme?: ThemePreference; 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 43175daeee..f3f0644747 100644 --- a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts +++ b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts @@ -105,8 +105,14 @@ const DROPPED_SCHEMA_EXPORTS = [ 'SpecResponsiveConfigSchema', 'BreakpointColumnMapSchema', 'BreakpointOrderMapSchema', - // theme.ts - 'ThemeModeSchema', + // 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 + // now — `theme.ts` no longer re-exports anything from `@objectstack/spec/ui`, + // so once the pin moves past the retirement the row would have asserted + // nothing. The mode vocabulary's runtime value is `THEME_MODES` (a local + // tuple, deliberately exported as a VALUE — the #2561 erasure cannot recur + // for it). ]; /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f8ef39af9f..1a7f793e75 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -699,23 +699,30 @@ import type { AppComponentSchema } from './app.js'; // Phase 2 Schemas - New Additions // ============================================================================ export type { - // Theme System (aligned with @objectstack/spec) + // Theme System — the document vocabulary is owned HERE since objectui#5716 + // (maintainer ruling 2026-08-23, option A — localize): the spec retired its + // theme module (objectstack#10485) while objectui RETAINED the theme system, + // so the types moved into `./theme` with the last-published 17.1.0 shapes as + // the blueprint. `Typography` / `BorderRadius` / `Shadow` / + // `ThemeDefinition` were DELETED under the same ruling's zero-reader rider + // (the first three live on as inline members of `Theme`). Theme, // `ThemeComponentSchema` RETIRED in objectui#5489 — the `type: 'theme'` // component kind no renderer implemented. See `./theme` for the tombstone. ThemeMode, ColorPalette, - Typography, - BorderRadius, - Shadow, // `Animation` / `ZIndex` retired with `theme.animation` / `theme.zIndex` in // @objectstack/spec 17.0.0-rc.3 (objectstack#5021) — see `./theme`. ThemeSwitcherSchema, ThemePreviewSchema, - // Legacy aliases - ThemeDefinition, } from './theme.js'; +// Runtime witness for the theme mode vocabulary — a VALUE, so it must not sit +// inside the `export type` block above (#2561: inside one it is value-erased +// and resolves to `undefined` at runtime). Same pattern as +// `SPEC_GESTURE_TYPES` below. +export { THEME_MODES } from './theme.js'; + export type { // Report Presentation Layer (ObjectUI-specific UX enhancements: // sections, schedule, export presets, conditional formatting, etc.). @@ -972,10 +979,13 @@ export { * ``` */ export type * as Data from '@objectstack/spec/data'; -// Deliberate public namespace export of the spec vocabulary. Note the #4171 -// caveat: `UI.FormField` erases to `any` until the spec types its unions. -// eslint-disable-next-line no-restricted-imports -export type * as UI from '@objectstack/spec/ui'; +// Deliberate public namespace export of the spec vocabulary — via the local +// shim module since objectui#5716, so the `UI.Theme`-family members are +// re-pointed at their owner (`./theme.ts`) and survive the spec's theme +// retirement instead of narrowing silently on the pin refresh. See the shim's +// header for the full reasoning. The #4171 caveat still applies: +// `UI.FormField` erases to `any` until the spec types its unions. +export type * as UI from './spec-ui-namespace.js'; export type * as System from '@objectstack/spec/system'; export type * as AI from '@objectstack/spec/ai'; export type * as API from '@objectstack/spec/api'; diff --git a/packages/types/src/spec-ui-namespace.ts b/packages/types/src/spec-ui-namespace.ts new file mode 100644 index 0000000000..986b3cdc72 --- /dev/null +++ b/packages/types/src/spec-ui-namespace.ts @@ -0,0 +1,37 @@ +/** + * 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. + */ + +/** + * The `UI` protocol namespace of `@object-ui/types` — the module behind + * `export type * as UI` in `index.ts`. + * + * Existence note (objectui#5716). `index.ts` used to point that namespace + * export straight at `@objectstack/spec/ui`, so the namespace tracked + * whatever the installed pin publishes — and on the dependency refresh past + * the spec's theme retirement (objectstack#10485), the `UI.Theme`-family + * members would have vanished with no error anywhere in this repo. The + * objectui#5716 ruling says that silent narrowing must stop for the theme + * family. This shim is the mechanism: an explicit re-export beats a star + * re-export of the same name, so the three theme document types below resolve + * to their owner (`./theme.ts`) — today, while the pin still publishes the + * retired names, and unchanged after the refresh removes them from the star. + * + * The REST of the namespace still tracks the spec by star — deliberately the + * same posture as the non-theme `@objectstack/spec/ui` re-export blocks in + * `index.ts`, which the same ruling left out of scope. Spec/ui names the + * refresh retires (`ThemeSchema`, `ThemeModeSchema`, `ThemeParsed`, + * `Typography`, `BorderRadius`, `Shadow`, `defineTheme`) drop out of the star + * with it; the named-type deletions on this package's own surface are + * recorded in the objectui#5716 changeset. + */ +// The star below is the sanctioned namespace re-export of the spec vocabulary +// (moved here from `index.ts:978`); the no-restricted-imports entry it trips +// guards named `FormField` imports, which a star cannot prove absent. +// eslint-disable-next-line no-restricted-imports +export type * from '@objectstack/spec/ui'; +export type { Theme, ThemeMode, ColorPalette } from './theme.js'; diff --git a/packages/types/src/theme.ts b/packages/types/src/theme.ts index df42f66e50..e1ee1573be 100644 --- a/packages/types/src/theme.ts +++ b/packages/types/src/theme.ts @@ -9,9 +9,9 @@ /** * @object-ui/types - Theme Schema * - * Defines theme configuration aligned with @objectstack/spec. - * Provides the complete design token system: colors, typography, - * spacing, borders, shadows, breakpoints, animation, z-index. + * The theme document vocabulary (`Theme`, `ThemeMode`, `ColorPalette`) — + * owned by this package since objectui#5716 — plus the objectui component + * schemas that render themes. * * @module theme * @packageDocumentation @@ -20,93 +20,187 @@ import type { BaseSchema } from './base.js'; // ============================================================================ -// Spec-Canonical Theme Sub-types — imported from @objectstack/spec/ui -// Rule: "Never Redefine Types. ALWAYS import them." +// Theme Document Vocabulary (formerly `@objectstack/spec/ui`) // ============================================================================ +// `@objectstack/spec` retired its whole theme module: `ui/theme.zod.ts` went +// in objectstack#10485 (PR objectstack#10695), and the objectstack#10856 +// ruling had objectui drop the dangling VALUE re-exports (objectui#5710). +// This block is the TYPE half. The maintainer ruling on objectui#5716 +// (2026-08-23, option A — localize) makes objectui the owner of the theme +// document types, because the theme SYSTEM stays: `ThemeEngine` +// (`packages/core/src/theme/ThemeEngine.ts`) turns a `Theme` into CSS +// variables, and `ThemeProvider` (`packages/react/src/context/ThemeContext.tsx`) +// applies it. +// +// The declarations below are hand-written from the LAST-PUBLISHED shapes — +// `@objectstack/spec` 17.1.0, the pin the engine was actually built against. +// That release is the blueprint, not a starting point: every key and its +// optionality below is the 17.1.0 `z.input` reading, including the `?: never` +// retirement tombstones. There is deliberately no spec import left in this +// file for the dependency refresh to break. +// +// Under the same ruling's zero-reader rider, four published names did NOT +// make the move and were DELETED from this package's surface: `Typography`, +// `BorderRadius`, `Shadow` (their shapes live on as the inline `typography` / +// `borderRadius` / `shadows` members of `Theme` below) and the deprecated +// `ThemeDefinition` alias of `Theme`. Each had zero in-repo readers — census +// in the objectui#5716 PR body. Re-adding one is a published-API decision, +// not a convenience. +// +// Drift story, stated plainly (objectui#5716). While the installed pin still +// publishes the retired names: `check:spec-symbols` carries ALLOW entries for +// the three names below, and `__tests__/page-nav-misc-spec-parity.test.ts` +// still proves the local `Theme` and the 17.1.0 authoring `Theme` accept each +// other — both turn stale/red on the pin refresh, which is how that refresh +// announces itself here. AFTER the refresh the shapes are documented, not +// pinned upstream — nothing is left upstream to pin against. What stays +// executable forever: `THEME_MODES` (runtime witness of the mode vocabulary) +// and `ThemeEngine`'s `Record` map, which the compiler +// keeps in lockstep with the palette key set. /** - * Color Palette Definition - * Canonical definition from @objectstack/spec/ui. - */ -export type { ColorPalette } from '@objectstack/spec/ui'; - -/** - * Typography Configuration - * Canonical definition from @objectstack/spec/ui. + * The theme mode vocabulary, as a runtime tuple. + * + * A runtime `as const` tuple rather than a bare union, deliberately — the + * `SPEC_GESTURE_TYPES` precedent in `./mobile.ts`: the parity pins in + * `@object-ui/providers` (`theme-mode-spec-parity.test.tsx`, + * `spec-symbol-batch7.test.ts`) read the mode vocabulary at runtime, and this + * tuple keeps those pins executable against the vocabulary's owner. A + * type-only union would have left them nothing to read, which is how a + * retirement quietly takes working coverage with it. */ -export type { Typography } from '@objectstack/spec/ui'; +export const THEME_MODES = ['auto', 'light', 'dark'] as const; /** - * Border Radius Configuration - * Canonical definition from @objectstack/spec/ui. + * Theme display mode. `auto` follows the OS preference. + * + * Owned here since the upstream theme retirement (objectui#5716). Note the + * pre-spec `'system'` spelling is NOT part of this vocabulary — it is + * `@object-ui/providers`' own legacy alias of `auto`, declared there on + * `ThemePreference` (objectui#2942). */ -export type { BorderRadius } from '@objectstack/spec/ui'; +export type ThemeMode = (typeof THEME_MODES)[number]; /** - * Shadow Configuration - * Canonical definition from @objectstack/spec/ui. - */ -export type { Shadow } from '@objectstack/spec/ui'; - -// `Animation` / `ZIndex` RETIRED in @objectstack/spec 17.0.0-rc.3 -// (objectstack#5021 option 2, PR objectstack#5289). `ThemeSchema.animation` and -// `ThemeSchema.zIndex` became tombstones whose prescription points at -// `customVars`, and the spec DELETED the two value schemas outright rather than -// leave them standing beside the tombstones — an exported value schema with no -// consumer reads as a capability to whoever finds it. A `--z-modal` or a -// `--duration-fast` is authored through `theme.customVars` now. -// -// Removing the DEAD EMISSION side in `ThemeEngine.generateThemeVars()` is -// objectui#3361 and deliberately NOT done here; this is only the type surface -// that stopped compiling, which that card pre-declared would ride with the -// dependency refresh. - -/** - * Theme Mode - * Canonical definition from @objectstack/spec/ui. + * Color palette of a theme document. `primary` is the only required color. + * + * Owned here since the upstream theme retirement (objectui#5716); the key set + * is the last-published one. `ThemeEngine.generateColorVars()` maps every key + * onto a Shadcn CSS variable, and its `Record` + * `COLOR_TO_CSS_MAP` makes the compiler reject a key added or removed here + * without the mapping moving with it. */ -export type { ThemeMode } from '@objectstack/spec/ui'; - -// `ThemeModeSchema` (the zod value) is intentionally not re-exported — under -// `export type` it was value-erased (#2561). It can no longer be imported from -// `@objectstack/spec/ui` either: objectstack#10485 (PR objectstack#10695) -// retired the spec's whole `ui/theme.zod.ts` module, and the objectstack#10856 -// ruling had objectui remove its dangling re-exports (objectui#5710). The -// installed `@objectstack/spec` pin still publishes the type re-exports in -// this file; re-homing the theme TYPE surface once the pin moves past the -// retirement is a separate, un-ruled decision — see the tripwire note in -// `packages/providers/src/__tests__/spec-symbol-batch7.test.ts`. +export interface ColorPalette { + primary: string; + secondary?: string; + accent?: string; + success?: string; + warning?: string; + error?: string; + info?: string; + background?: string; + surface?: string; + text?: string; + textSecondary?: string; + border?: string; + disabled?: string; + primaryLight?: string; + primaryDark?: string; + secondaryLight?: string; + secondaryDark?: string; +} /** - * Complete Theme Definition — the spec's **authoring** theme shape, re-exported - * by reference (objectstack#4115) rather than restated. + * Complete theme definition — the AUTHORING shape of a theme document: theme + * JSON as stored, edited, and handed to `ThemeProvider`. * - * The interface this replaces was member-for-member identical to the spec's - * authoring theme shape (verified by the mutual-assignability probe in - * `__tests__/page-nav-misc-spec-parity.test.ts`), so the only thing the copy - * added was a second place to drift from — under the spec's own symbol name, - * which is what makes such a copy read as canonical to the next reader. + * Owned here since the upstream theme retirement (objectui#5716). Two + * provenance notes carried over from the retired module, because both are + * live contracts rather than history: * - * **The `z.input` shape, not the parsed one**, and in `@objectstack/spec` - * 17.0.0-rc.6 that shape is spelled `Theme`. Up to rc.5 the spec published - * three names — `Theme` (= `z.infer`), `ThemeParsed` (= `z.infer`) and - * `ThemeInput` (= `z.input`) — and this binding took `ThemeInput`. rc.6 retired - * every `…Input` alias and made the bare name the input: `Theme` is now - * `z.input` and `ThemeParsed` is the `z.infer` side. So - * `ThemeInput` → `Theme` is a rename that preserves this binding's meaning - * exactly; re-pointing at `ThemeParsed` would be the silent semantic change. - * The reason the input side is the true one here is unchanged: `mode` is - * required in the parsed shape because `.default('auto')` has already run, and - * everything in objectui that carries a `Theme` is on the authoring side — - * theme JSON as stored, edited and handed to `ThemeProvider`. This is the - * `.default()`/`z.input` rule from objectui#3169: writing side → input type. + * - **This is the input side, so `mode` is optional.** The retired zod schema + * ran `.default('auto')` at parse time, which made `mode` required only in + * the PARSED shape; everything in objectui that carries a `Theme` is on the + * writing side (the `.default()`/`z.input` rule from objectui#3169), and + * re-declaring the parsed side here would have made every stored theme + * unrepresentable. The objectui#4167 rc.6 re-pointing settled this binding + * on the input side by SIDE, not by name. * - * This is the canonical JSON shape for a theme. It can be serialized, stored, - * and applied at runtime via ThemeProvider. + * - **The `?: never` members are retirement tombstones, not accidents.** + * `animation` / `zIndex` (and, inside `typography`, the size/weight/ + * line-height/letter-spacing scales and `fontFamily.heading` / `.mono`) + * were retired upstream (objectstack#5021, ADR-0049 emitted-but-read-by- + * nobody) — the engine-side dead emission is objectui#3361. `customVars` is + * the byte-for-byte replacement: the engine emits `customVars` entries + * verbatim as `--: `, so `customVars: { 'font-size-lg': + * '1.125rem' }` puts the SAME `--font-size-lg` on the document the retired + * scale keys used to. Keeping the keys as `?: never` keeps authoring one a + * compile-time rejection instead of a silent strip. The earlier prune of + * `spacing` / `breakpoints` / `logo` / `density` / `wcagContrast` / `rtl` / + * `touchTarget` / `keyboardNavigation` (objectstack#3494, keys the engine + * never consumed) predates the tombstone convention, so those keys are + * simply absent. */ -export type { Theme } from '@objectstack/spec/ui'; - -import type { Theme } from '@objectstack/spec/ui'; +export interface Theme { + /** Unique theme name (identifier). */ + name: string; + /** Human-readable label. */ + label: string; + /** Optional description. */ + description?: string; + /** Display mode. Optional when authoring; the system treats absence as `auto`. */ + mode?: ThemeMode; + /** Color palette — the only required token group. */ + colors: ColorPalette; + /** Typography. Only `fontFamily.base` is live; the rest are tombstones (see above). */ + typography?: { + fontFamily?: { + base?: string; + /** Retired (objectstack#5021) — author `customVars` instead. */ + heading?: never; + /** Retired (objectstack#5021) — author `customVars` instead. */ + mono?: never; + }; + /** Retired (objectstack#5021) — author `customVars` instead. */ + fontSize?: never; + /** Retired (objectstack#5021) — author `customVars` instead. */ + fontWeight?: never; + /** Retired (objectstack#5021) — author `customVars` instead. */ + lineHeight?: never; + /** Retired (objectstack#5021) — author `customVars` instead. */ + letterSpacing?: never; + }; + /** Rounded-corner scale. */ + borderRadius?: { + none?: string; + sm?: string; + base?: string; + md?: string; + lg?: string; + xl?: string; + '2xl'?: string; + full?: string; + }; + /** Box-shadow scale. */ + shadows?: { + none?: string; + sm?: string; + base?: string; + md?: string; + lg?: string; + xl?: string; + '2xl'?: string; + inner?: string; + }; + /** Retired (objectstack#5021) — author `customVars` instead. */ + animation?: never; + /** Retired (objectstack#5021) — author `customVars` instead. */ + zIndex?: never; + /** Escape hatch: entries are emitted verbatim as `--: `. */ + customVars?: Record; + /** Name of a theme to inherit from. */ + extends?: string; +} // ============================================================================ // ObjectUI Component Schemas (UI rendering) @@ -116,7 +210,7 @@ import type { Theme } from '@objectstack/spec/ui'; // maintainer ruling of 2026-08-21 on objectstack#10485 (option B, quoted // verbatim and untranslated): // -// 「B:退役授权面 —— 收掉 `themes` 载体键与 schema,`app.branding` 留作唯一颜色面; +// 「B:退役授权面 —— 收掉 `themes` 载体键与 schema,`app.branding` 留作唯一颜色面; // objectui 引擎代码与单测保留」 // // It declared a COMPONENT kind — a theme-manager node carrying `themes[]`, @@ -131,10 +225,11 @@ import type { Theme } from '@objectstack/spec/ui'; // unenforced, the ADR-0078 class. // // The theme SYSTEM is untouched and is explicitly RETAINED by the same ruling: -// `Theme` above is the spec's authoring theme document, `ThemeEngine` -// (`packages/core/src/theme/ThemeEngine.ts`) turns it into CSS variables, and -// `ThemeProvider` (`packages/react/src/context/ThemeContext.tsx`) applies it. -// Retiring the dead component kind is not retiring theming. +// `Theme` above is the authoring theme document (owned here since +// objectui#5716), `ThemeEngine` (`packages/core/src/theme/ThemeEngine.ts`) +// turns it into CSS variables, and `ThemeProvider` +// (`packages/react/src/context/ThemeContext.tsx`) applies it. Retiring the +// dead component kind is not retiring theming. // // The two siblings below — `ThemeSwitcherSchema` / `ThemePreviewSchema` — are // unregistered in exactly the same way but were NOT named by the ruling; they @@ -185,11 +280,6 @@ export interface ThemePreviewSchema extends BaseSchema { showComponents?: boolean; } -// ============================================================================ -// Legacy Aliases (Backward Compatibility) -// ============================================================================ - -/** - * @deprecated Use `Theme` instead. Kept for backward compatibility. - */ -export type ThemeDefinition = Theme; +// `ThemeDefinition` (the deprecated legacy alias of `Theme`) was DELETED here +// under the objectui#5716 zero-reader rider — zero in-repo readers (census in +// that PR). Use `Theme`. diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index e84e943117..c1b6bf5b75 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -329,6 +329,45 @@ const ALLOW = { "authored `UserFilters`.", issue: 4115, }, + // The three theme document types, localized under the objectui#5716 ruling + // (2026-08-23, option A): the spec RETIRED its whole theme module + // (objectstack#10485 / PR objectstack#10695) while objectui retained the + // theme SYSTEM, so this package now OWNS the vocabulary — hand-written from + // the last-published 17.1.0 shapes, with no spec import left for the pin + // refresh to break. The names still collide only because the installed pin + // is that last publishing release. Deliberate consequence of this list's + // shrink-only staleness check: when the pin moves past the retirement, rule + // 1 stops flagging these names, all three entries turn STALE and this gate + // fails loudly — the refresh announces itself here instead of the theme + // surface narrowing silently, which is the objectui#5716 ruling's + // silent-half axis. The refresh PR deletes these three entries. + "@object-ui/types:Theme": { + reason: + "Owned here since the upstream theme retirement (objectui#5716 ruling, option A — " + + "localize): the retired module's authoring theme document, hand-written from the " + + "17.1.0 z.input reading. One-time exact-equality probe (both directions, all six " + + "shapes) ran against the installed 17.1.0 in the objectui#5716 PR; " + + "page-nav-misc-spec-parity.test.ts keeps mutual assignability with the spec's Theme " + + "compiled until the pin refresh retires its spec leg.", + issue: 5716, + }, + "@object-ui/types:ThemeMode": { + reason: + "Owned here since the upstream theme retirement (objectui#5716 ruling, option A). " + + "Derived locally from the THEME_MODES runtime witness tuple, which keeps the " + + "@object-ui/providers parity pins executable (theme-mode-spec-parity.test.tsx, " + + "spec-symbol-batch7.test.ts — both fail if the vocabulary gains, loses, or " + + "misspells a member the provider does not handle).", + issue: 5716, + }, + "@object-ui/types:ColorPalette": { + reason: + "Owned here since the upstream theme retirement (objectui#5716 ruling, option A), " + + "hand-written from the 17.1.0 z.input reading. Drift guard is structural: " + + "ThemeEngine's COLOR_TO_CSS_MAP is Record, so the compiler " + + "rejects a key added or removed here without the CSS-variable mapping moving with it.", + issue: 5716, + }, }; // ── Untriaged collisions (the ledger) ────────────────────────────────────────