Skip to content

feat(types,providers): localize the theme document types — objectui owns Theme, ThemeMode, ColorPalette (#5716) - #5752

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5716-localize-theme-types
Aug 23, 2026
Merged

feat(types,providers): localize the theme document types — objectui owns Theme, ThemeMode, ColorPalette (#5716)#5752
os-zhuang merged 2 commits into
mainfrom
claude/issue-5716-localize-theme-types

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#5716

Maintainer ruling (2026-08-23, live PM chat, adopting recommendation A — localize): objectui assumes ownership of the theme document types. The spec retired its whole theme module (objectstack-ai/objectstack#10485, PR objectstack-ai/objectstack#10695); objectui retains the theme SYSTEM (ThemeEngine, ThemeProvider, theme documents), so the type home moves here, with the last-published @objectstack/spec 17.1.0 shapes as the authority. This unblocks the post-release spec-pin refresh chain (#5668 Restart-when exit; #5601 / #4935 ride the same bump).

Published-API decisions, stated by name (contract-review clause)

1. Deleted published names (zero-reader rider: not migrated — deleted)

Each name below leaves the @object-ui/types surface. The changeset states all four plainly.

  • Typography — shape lives on as the inline Theme.typography member.
  • BorderRadius — lives on as inline Theme.borderRadius.
  • Shadow — lives on as inline Theme.shadows.
  • ThemeDefinition — deprecated alias of Theme; deleted outright.

Zero-reader census. Five instruments, each with a positive control (a control that came back empty would have meant a broken instrument, not a proved zero):

  • I1 — named-import grep across packages apps examples e2e for imports of each name from @object-ui/types or a relative theme path. Zero hits for all four. Control: Theme returns 5 real import sites (ThemeEngine.ts, ThemeContext.tsx, two test files, page-nav parity test).
  • I2 — word-boundary usage grep (grep -w) across packages apps examples e2e scripts eslint-rules, declaration files excluded. Typography: 3 hits, all prose comments. BorderRadius: 1 comment. Shadow: 3 comments about DOM shadowing in plugin-form tests, unrelated. ThemeDefinition: zero hits anywhere in code. Control: ColorPalette returns live code hits in ThemeEngine.ts including a keyof position.
  • I3 — namespace-member-access grep for UI. followed by any theme-family name, and for any-alias member access of the four names — this is exactly the shape that hid clientValidation.ts theme: entry reads spec.ThemeSchema, which the spec retired — dangling dynamic read on a metadata type that is not registered #5715's dangling entry. Zero hits. Control: the broad UI. member pattern catches UI.ListView in the index.ts docblock at line 971, so the instrument sees the access shape.
  • I4 — re-export-chain grep for export blocks carrying any of the names. Zero rows. Control: the same pattern family finds the real chain plugin-form/src/successBehavior.ts:16 re-exporting SubmitBehavior from @object-ui/types.
  • I5 — the checker (authoritative for types). With the four names deleted and @object-ui/types rebuilt, type-check of the dependents that the census names as the only theme-family importers (types, providers, core, react) is green. Reverse control: deleting the KNOWN-READ ColorPalette from the surface, rebuilding, and re-running the consumer sweep goes red at exactly ThemeEngine.ts(20,22) error TS2305 — so a green sweep over the deletions is a measurement, not silence. (Consumers resolve the package through dist/index.d.ts, so both control legs rebuilt dist and confirmed the mutation landed there by anchored counts before reading any result; restore was trapped and re-verified.)

Stated instrument limits: plain grep cannot tell a comment from code (every nonzero I2 hit was read and classified by hand); text search cannot see a namespace member access (I3 + I5 cover that shape); the census is in-repo only — external consumers of the four names are unknowable from here, which is why the changeset names every removal. Doc snippets under content/ declare their own inline interfaces (not imports), so nothing there reads the deleted names either.

2. Hand-written shapes and how each was established

Theme, ThemeMode, ColorPalette are hand-written in packages/types/src/theme.ts, following the house shape this file's own pointer names (the localized touch vocabulary block in mobile.ts, including the runtime-witness-tuple pattern: THEME_MODES keeps the providers parity pins executable, as SPEC_GESTURE_TYPES did).

How parity with the 17.1.0 blueprint was established, per shape:

  • One-time exact-equality probe against the installed 17.1.0 pin, compiled with the package's own tsconfig (not committed): conditional-type exact equality in both directions for Theme, ThemeMode, ColorPalette, plus the deleted shapes via Theme.typography / Theme.borderRadius / Theme.shadows against the spec's Typography / BorderRadius / Shadow, with is-any guards proving the spec side is real. Forward leg: exit 0, zero diagnostics. Reverse leg (probe must be falsifiable): flipping one ColorPalette key from optional to required went red at exactly the two assertions that read the palette; restore leg exit 0. Mutations confirmed on disk by anchored counts in both directions.
  • Ongoing until the refresh:page-nav-misc-spec-parity.test.ts (untouched, outside the fence) keeps compiling mutual assignability between the local Theme and the spec's authoring Theme in CI on every commit — it is now a live drift gate for the hand-written shape, and its spec leg retires with the pin refresh.
  • After the refresh, stated plainly: the shapes are documented, not pinned upstream — there is deliberately nothing left upstream to pin against, and a committed pin reading @objectstack/spec/ui would turn red on the very upgrade this card unblocks. What stays executable forever: THEME_MODES (runtime witness; both providers pins fail if the vocabulary gains, loses, or misspells a member — ablation-proven below), and ThemeEngine's color map, declared as a Record keyed by keyof ColorPalette, which makes the compiler reject any palette-key change that does not move the CSS-variable mapping with it.
  • The input-side reading (mode optional; the retired zod default ran only at parse time) and the tombstone members (animation, zIndex, the typography scales — kept as optional-never) are carried over with their provenance comments condensed at the declarations.

3. The UI namespace clause (index.ts line 978)

Measured first: zero in-repo readers of any UI. member (I3 above); the spec/ui surface is 230 runtime keys plus type-only names, so a full explicit list would be a second copy of the spec and was rejected. Chosen shape: a shim module packages/types/src/spec-ui-namespace.ts — star re-export of spec/ui plus explicit re-exports of Theme / ThemeMode / ColorPalette from the local owner (an explicit export beats a star export of the same name). index.ts line 978 now points at the shim. Result: the surviving theme members can no longer narrow silently — they resolve locally today and unchanged after the refresh. The non-theme members keep tracking the spec by star, deliberately the same posture as the fifteen non-theme re-export blocks the ruling left out of scope. Spec/ui theme names the refresh retires (ThemeSchema, ThemeModeSchema, ThemeParsed, the three deleted shape names, defineTheme) drop out of the namespace with it; the changeset says so.

4. Retirement-era tripwires cleaned (ruled in-scope)

  • spec-ui-schema-reexports.test.ts: the ThemeModeSchema deny-list row removed ahead of its ratchet firing, with the reason recorded in place.
  • spec-symbol-batch7.test.ts: the designed tripwire fired its purpose ("if the spec retires Theme, the rename is up for re-triage" — the re-triage was this ruling). Rewritten with zero spec imports: pins ThemePreference equals ThemeMode | 'system', the witness-tuple/type identity, and the system-not-in-vocabulary exclusion, all against the new owner.
  • theme-mode-spec-parity.test.tsx: behavioral coverage kept intact; only the vocabulary read re-pointed from the spec schema to THEME_MODES.
  • providers/types.ts: ThemePreference derives from the local ThemeMode; the batch-7 naming history is preserved at the declaration. Providers src now has zero @objectstack/spec imports.

Ablations (tests bite; runs resolve @object-ui/types to src via the root vitest alias table, vitest.config.mts line 261, so the mutation legs are source-level by measured configuration): adding system to THEME_MODES fails exactly the batch7 exclusion pin (1 failed); adding an unhandled bogus member fails exactly the parity loop with "mode 'bogus' must resolve to light/dark, got [bogus]". Both mutations confirmed on disk by anchored counts, both restored under a trap and re-verified.

Fence deviations — both reported on #5716 before editing (mid-task report)

  1. scripts/check-spec-symbol-derivation.mjs: check:spec-symbols reds on the ruled outcome (measured: exit 1, naming exactly the three hand-written names) because the installed pin still publishes them. Three ALLOW entries added citing this ruling — the gate's own designed route. Deliberate property: the ALLOW list is shrink-only and stale-checked, so the pin refresh turns all three entries stale and fails the gate LOUDLY — a second gauge for the ruling's silent-half axis. The refresh PR deletes them.
  2. packages/types/src/spec-ui-namespace.ts: new file, exists solely to implement the ruled line-978 clause — TypeScript cannot compose a namespace export from two sources inside one file.

Verification (union at head cf05dd125, zero uncommitted files)

  • type-check of @object-ui/types, @object-ui/providers, @object-ui/core, @object-ui/react — the DEPENDENTS direction, and per the census the only in-repo theme-family importers: 4 of 4 Done, exit 0. Declared narrowing: the full dependent farm is CI's run.
  • vitest from the repo root (per AGENTS invocation rules): 48 files, 604 tests passed — types, providers, core theme, react ThemeProvider, and the edited gate's own 34-test suite.
  • check:spec-symbols exit 0: "16 declared dialects, 4 untriaged collisions in 2 packages" / claims leg unchanged at 18 pre-existing.
  • check:control-bytes OK (4799 files); changeset presence / no-major / fixed-group all green; type-check:coverage green (45 of 46 plus 41 of 41 test projects, pre-existing ledger unchanged).
  • check:self-import, check:phantom-deps green; check:node-esm-load green (load leg 34 of 39 entries evaluated) — ran one tree-state before the changeset file was added, which that gate does not read.
  • Declared narrowing: repo-wide pnpm lint is CI's run; eslint on the 9 changed files exits 0 (two pre-existing warnings on untouched lines of providers/types.ts).
  • Known-broken-in-worktree gauges not run locally (per seat gauge list): check-eager-closure-budget, check-doc-snippet-types, check-published-dist-tooling.

Out of scope, noted

Generated by Claude Code


Generated by Claude Code

The spec retired its theme module (objectstack#10485); objectui retains the
theme SYSTEM, so under the #5716 ruling (option A) @object-ui/types now OWNS
Theme / ThemeMode / ColorPalette, hand-written from the last-published
@objectstack/spec 17.1.0 shapes. Typography / BorderRadius / Shadow /
ThemeDefinition are DELETED under the zero-reader rider (shapes live on as
inline Theme members). ThemePreference derives from the local ThemeMode;
THEME_MODES is the vocabulary's runtime witness. The UI namespace re-points
its theme members at the local owner via a shim so the pin refresh cannot
narrow them silently. Retirement-era tripwires cleaned: batch7 deny-list row,
batch7 spec imports, parity test re-pointed at the new owner. check:spec-symbols
carries three ALLOW entries that turn stale (loud) on the pin refresh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3917.1 KB3990.2 KB
Main entry chunk (gzip)152.5 KB350 KB
Entry fileindex-DGuN2Oi9.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)16.66KB6.35KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)33.99KB8.57KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)510.39KB114.67KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.41KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.98KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.61KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)3.40KB1.68KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 04:57
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit c9327c9Aug 23, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5716-localize-theme-types branch August 23, 2026 04:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-zhuang@claude