fix(components): ui:grid renders the 2xl breakpoint its columns map accepts - #8435
Merged
Conversation
…ccepts
`columns: { xs: 1, '2xl': 6 }` type-checked, passed GridSchema's zod mirror,
emitted no class, and rendered at the `xs` count on every screen. `grid.tsx`
had neither a `2xl` read arm nor a `GRID_COLS_2XL` static class map — both
layers are added, because the read arm alone would emit a class name Tailwind
never compiles (v4 finds utilities by scanning source text, so a
`2xl:grid-cols-${n}` built at runtime is not a utility that exists).
`2xl` is a full member of the breakpoint vocabulary everywhere else in the
repo: `BreakpointName` (@object-ui/types), `BREAKPOINTS` / `BREAKPOINT_ORDER`
(@object-ui/mobile), and `BreakpointColumnMap` (@object-ui/layout), whose
`ResponsiveGrid` already emits `2xl:grid-cols-*`. Only this consumer stopped
at five.
The pin asserts the whole emitted class string for every tier, so the other
five are non-regression assertions of the same file, and derives its case list
from `BreakpointName` with a compile-time exhaustiveness gate — a seventh tier
cannot be added to the vocabulary without this file failing to compile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…able to fail The first spelling was inert: `const _uncovered: Uncovered[] = []` type-checks whether `Uncovered` is `never` or `'2xl'`, so a seventh `BreakpointName` would have slipped through the gate meant to catch it. Replaced with `_AssertNever<T extends never>`, which reddens — verified by deleting a member from `ALL_BREAKPOINTS` and observing tsc name this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
0 to 12 compiled `2xl:grid-cols-*` rules attributable to grid.tsx, with the twelve `xl:grid-cols-*` rules as the unchanged control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…peError `container.firstElementChild.className` on a renderer that produced nothing throws before any `expect` runs, so the summary would name a TypeError rather than the node that failed to render. Assert the element with a message first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Contributor
✅ Console Performance Budget
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
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7097
ui:gridrenders the2xlbreakpoint itscolumnsmap has always accepted.Re-measured on today's tree — two of the card's numbers no longer hold
The card was written against
@objectstack/spec17.2.0. This repo now pins 17.3.0, and that changes the accept surface it names.34510e0c3BreakpointColumnMapSchemain@objectstack/spec/ui, a strict six-key objectui/responsivevocabulary.node -e "require('@objectstack/spec/ui')"exports exactly oneBreakpoint*/Responsive*symbol:ResponsiveStylesSchema. The removal note is in the package's ownsrc/ui/responsive.zod.ts.GridSchema.columns, typednumberor an open string-keyed record of numbers (packages/types/src/layout.ts:385, mirrored inzod/layout.zod.ts:223asz.record(z.string(), z.number())). Not six — unbounded.grid.tsxhad five read arms and five class maps,xsthroughxl.SchemaRendererrender (see below).The six-member vocabulary is still real, just no longer spec-side:
BreakpointName(packages/types/src/mobile.ts:69),BREAKPOINTS/BREAKPOINT_ORDER(packages/mobile/src/breakpoints.ts, pinned atbreakpoints.test.ts:31), andBreakpointColumnMap(packages/layout/src/ResponsiveGrid.tsx:53, re-homed from the retired schema by objectui#7580, maintainer ruling 2026-09-04 option A). So the card's conclusion survives its premise: the vocabulary is six everywhere, and only this consumer stopped at five.Which layer dropped it — the load-bearing measurement
Both, and that decides the diff.
grid.tsxhad no2xlread arm and noGRID_COLS_2XLstatic class map. Adding the read arm alone would have emitted a class name Tailwind never compiles — v4 finds utilities by scanning source text, so a2xl:grid-cols-count assembled from a template is not a utility that exists. Green in a unit test, unstyled in the browser.Measured by compiling
packages/components/src/index.cssthrough the package's own pipeline (postcss+@tailwindcss/postcss, the same two lines aspackages/components/scripts/build-css.mjs), varying onlygrid.tsx:grid.tsxon disk2xl:grid-cols-*rulesxl:grid-cols-*rules34510e0c3blob11835179a(no map, no arm)62b735f9c(map + arm)Escape-form note, because it is a trap: Tailwind emits these selectors as
.\32 xl\:grid-cols-6, not.2xl\:grid-cols-6— a CSS identifier cannot start with a digit. Grepping the literal spelling returns zero on a stylesheet that contains all twelve.The class then survives to the DOM. Emitted
classNameon the rendereddiv, read throughSchemaRenderer:columns{ xs: 1, "2xl": 6 }grid grid-cols-1 gap-4grid grid-cols-1 2xl:grid-cols-6 gap-4{ xs: 1, xl: 5 }grid grid-cols-1 xl:grid-cols-5 gap-44grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4Not a deliberate five. The escalation fence does not fire:
2xlhas a Tailwind equivalent in this setup (default theme, no--breakpoint-*override in any@themeblock, so2xlis 96rem/1536px — the same 1536BREAKPOINTS['2xl']carries), and@object-ui/layout'sResponsiveGridalready emits2xl:grid-cols-*from its own class table. One consumer in this repo shipped the sixth tier; the other did not.Sibling responsive readers — the triage's "before closing" step
Swept, and there is no second instance.
grid.tsx:87is the only site inpackages/**orapps/**that reads.xloff a responsive object. The other two consumers of the vocabulary are keyed byBREAKPOINT_ORDERor byObject.entries, so they carry all six by construction:resolveResponsiveValue/ResponsiveContainer(@object-ui/mobile) andresolveColumnClasses(@object-ui/layout).What the pin observes, and why that is sound
packages/components/src/__tests__/grid-breakpoint-columns-7097.test.tsxasserts the whole emitted class string, not a parse and notgetComputedStyle. happy-dom does not compile Tailwind or evaluate@media (width >= 96rem)as a browser does, so a computedgrid-template-columnshere would measure the harness. The class string is the renderer's entire output on this path —grid.tsxreads nowindow, nomatchMedia, noResizeObserver— and one case pins that width-independence explicitly by rendering the same node atinnerWidth375 and 1600 and requiring identical output, rather than being green by accident on happy-dom's default desktop width.The class reader asserts its element with a message before reading
className: a renderer that produced nothing would otherwise throwCannot read properties of nullbefore anyexpectran, and the summary would name a TypeError instead of the node that failed to render (commit 4).Would an implementation strictly worse than the bug pass it? No. Every case is whole-string equality, so the other five tiers are non-regression assertions of the same file: a change that emits the
2xlclass and dropsxlreddens, and so does "delete the feature" (no responsive classes at all), which fails all eleven cases.The case list is derived, not typed out:
ALL_BREAKPOINTSis checked againstBreakpointNameby an_AssertNeveralias, so a seventh member of the vocabulary makes this file a compile error instead of silently leaving the new tier untested. That is the durable half of the card, scoped to what still exists — with the spec-sideBreakpoint*Mapschemas retired, a repo-wide "compare a strict breakpoint map's member set against its renderer's reads" gate has no remaining subject.Prove the pin can fail — three ablations, restored by state
Each from the committed implementation, with a
trap ... EXIT INT TERMand absolute paths; each restore verified bygit hash-objectequality and an emptygit diff HEAD, never by an exit code.cn()arm, leaving the class map, so the read layer is isolated. HEAD blob62b735f9c-> on-diskeede06b1c.4 failed | 7 passed, red rows by name: a fully authored six-breakpoint map emits all six classes, in order · the reported node — { xs: 1, "2xl": 6 } — now emits its 2xl class · a map naming only 2xl emits that tier and no other tier · renders identically at a phone width and at a 2xl desktop width.ed665c2e0. The DOM pin goes to1 failed | 10 passed— it catches this only incidentally, through the out-of-range case, while the compiled stylesheet loses all twelve rules. Stated plainly: the DOM pin alone does not cover the class-map layer for in-range counts. The table above is what covers it, and it is why the diff adds the map rather than a template.'2xl'fromALL_BREAKPOINTS;type-checkexits 2 withgrid-breakpoint-columns-7097.test.tsx(113,3): error TS2344: Type '"2xl"' does not satisfy the constraint 'never'. The gate's first spelling was inert (const x: Uncovered[] = []type-checks whetherUncoveredisneveror'2xl'); commit 2 replaces it and this ablation is what caught that.Verification
pnpm --filter @object-ui/components test—Test Files 241 passed (241),Tests 2235 passed (2235).pnpm --filter @object-ui/components type-check— exit 0, both programs (tsc --noEmit && tsc -p tsconfig.test.json, so the exhaustiveness gate is covered).pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build— exit 0, run first so type-check reads fresh.d.ts.pnpm exec eslint .inpackages/components— exit 0, 0 errors. The one warning ongrid.tsxis the pre-existing[key: string]: anyon the register signature, present at base. The new test file draws none.node scripts/check-changeset-presence.mjs—2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7097-grid-2xl-breakpoint.md.node scripts/check-changeset-no-major.mjs—No changeset declares a major bump.node scripts/check-governed-queue-guard.mjs --teston the three changed paths —NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.check:control-bytes,check:unreferenced-sources,check:self-import,check:esm-specifiers,check:vi-mock-specifiers— all exit 0.check:sdui-registration-pins— exit 0:All 16 registration(s) a sideEffects array promises are present in the built console (518 chunks weighed; the 3 ruled control(s) are in the derived set).It first exited 2 with a prerequisite message (it reads the console bundle at BUNDLE time);pnpm --workspace-concurrency=2 --filter '@object-ui/console...' build— exit 0 — supplied it, so this is measured, not narrowed.No authored node in any corpus changes:
grep -rn '"2xl"'across every JSON/YAML in the tree returns zero hits (control:"type": "grid"hits five catalog files). The fix is additive for future authors and explains why nobody noticed.Two findings, reported not filed
search_issuesreturnedAPI rate limit already exceeded for user ID 323634890on the second query, and the first query's zero-result read has no lit control to certify it, so per the dispatch contract these are reported in full rather than filed unsearched.packages/components/src/index.cssscans its own test files into the published stylesheet. Its@source '../src/**/*.{ts,tsx}'carries no exclusion, while the siblings do —packages/plugin-kanban/src/index.css:91-92has@source not './**/*.test.{ts,tsx}'and@source not './**/__tests__/**', andpackages/fields/src/index.css:44has the first of those. Two consequences. Bloat is the mild one. The sharp one is that a class literal written as a test's expected value compiles a real utility intodist/index.css, so a test can make the production utility it is checking for exist — measured live here: with this PR's pin file on disk and basegrid.tsx, the stylesheet contains.\32 xl\:grid-cols-6and nothing else in the family, sourced entirely from the test's own assertion strings. The table in this PR is honest only because the measurement was re-run with the pin file removed. Fix is two lines copied from the siblings; it changes published bundle contents, so it wants its own PR and changeset.GridSchema.columnsaccepts any string key, not six.packages/types/src/layout.ts:385types itnumberor an open string-keyed record of numbers, mirrored asz.record(z.string(), z.number()).columns: { banana: 3 }type-checks, parses, emits nothing — the same silent-drop shape as this card, one level more general. Narrowing to a partial record keyed byBreakpointNamewould close it, but that is a narrowing of a published accept surface across@object-ui/typesplus its zod mirror and thezod-mirror-paritypin, so it wants a ruling rather than a patch. Related and minor: theGridSchema.columnsdocblock example still reads{ xs: 1, sm: 2, md: 3, lg: 4 }, four of six — illustrative rather than wrong, and it sits underlayout-default-jsdoc-7361.test.ts, so it was left alone.Out of scope, deliberately
The designer's legacy flat channel (
smColumns…xlColumns) gains no sixth member. Naming it and declaring a sixth designerinputis a product surface decision, and this card is about thecolumnsbreakpoint object. A comment at the mobile-first ramp records whyxxlColsis absent from that condition, so it does not read as an oversight.Generated by Claude Code