@micha.bigler/ui-core-micha 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORK_ORDERS.md CHANGED
@@ -95,3 +95,4 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
95
95
  | UCM-CHART-10 | The rotated-tick allowance is reserved twice, and the surplus is dead space | `spaceForRotatedTicks` adds the same `extraHeight` BOTH to `axis.height` and to `margin.bottom`, and in MUI v8 those are additive. The axis band already holds the rotated labels, so the margin copy is left over as an empty band below the plot. Measured in hram 2026-08-22 at 1280 px with ucm 2.42.1: Ward Metrics and Access Ladder each show a 340 px surface whose plot ends at 131 px -- **130 px, 38 %, empty**; Cost 92 px; Division Metrics 85 px; Cost-effectiveness 44 px; Expert Mode only 8 px. Two things make it conclusive: the band SCALES WITH THE LABEL LOAD (35 long rotated ward names cost 130 px, short labels cost 8), and `emptyBelowLowestLabel` equals `emptyBelowAxisLine` on every chart -- the labels sit inside the axis band and nothing occupies the space beneath them. | 2026-08-22 | done | 2026-08-22: `spaceForRotatedTicks` fix + version bump | Tier 3, shared-core. Full order: [`work-orders/UCM-CHART-10.md`](work-orders/UCM-CHART-10.md). THIRD instance of one failure class in this file: space reserved in two places that add up -- after UCM-CHART-8 (`minHeight` vs `height`) and hram's RES-28 (`margin.left` vs an unsized `yAxis`). Neither CHART-8 nor CHART-9 could have caught it: both worked on the wrapper BOX, this is the axis MARGIN arithmetic. **Fix:** kept `extraHeight` on `axis.height` only, `margin` now passes through `spaceForRotatedTicks` completely untouched (no `bottom` computed in any branch) -- a caller-set `margin.bottom` is therefore protected unconditionally, a strict superset of the removed `callerSetBottom` escape hatch, which both reviewers confirmed is a clean simplification, not a behaviour gap. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged during `UCM-CHART-9` earlier the same day) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` and `ui_reviewer` ran concurrently (Sonnet, diff inline), both converged on the same two findings, both fixed before commit -- (1) version not bumped in-diff (fixed: `package.json` 2.42.1 -> 2.42.2, so `publish.yml`'s version-increase gate fires on push); (2) the WO's "investigate in the same pass" item (HRAM-RES-29 item 2, scatter x-axis tick-label clipping) had no recorded confirm/refute. **Refuted and recorded in the WO file:** `ScatterChart.jsx` never calls `spaceForRotatedTicks` at all, and hram's own named call sites (`AccessGapScatterPanel.jsx`, `OptimizationResultsPanel.jsx`) set no `tickLabelStyle.angle` either -- this helper cannot be the cause. Matches hram's own `HRAM-RES-29` register row, which had independently traced and closed item 2 the same day to an unrelated pre-2.42.0 missing-`height`-prop bug on `ScatterChart`, already fixed by `UCM-CHART-8`/2.42.0. Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`chartDefaults` incl. new `spaceForRotatedTicks (CHART-10)` unit-test block, all four presets, `ChartFrame`) -- 134/134 green, preset assertions unchanged except the two that directly asserted the bug (rewritten to assert the fix, both reviewers confirmed the rewrite is correct and non-trivial). **Rendered check -- numeric, on hram with real data** (`CLI-Kilombero_2026-per_cell-network`, SG Sandbox CLI, same candidate point, Research -> Allocation -> Basic Plots), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement, hram's own git tree untouched -- confirmed via `git status`): at 1280 px, Ward Metrics 129.6 px -> 51.6 px empty (35 labels, 340 px surface -- matches this row's own original 130 px/38% measurement almost exactly), Access Ladder 156.9 px -> 80.6 px (35 labels, 369 px), Division Metrics 85.2 px -> 41.2 px (5 labels, 340 px) -- each roughly halved, consistent with removing exactly one of two equal additive reservations. Checked again at 375 px: no clipping at either width, no regression. SUBSTITUTION DECLARED: screenshot capture was unavailable in this session (Browser pane would not composite frames) -- this is DOM/computed-style measurement (`getBoundingClientRect`/`getComputedStyle` against the live rendered SVG), which for this WO's numeric acceptance criterion is close to sufficient on its own, per the WO's own fallback clause. Cost/Cost-effectiveness/Expert Mode cards from the original six-card table were not individually re-measured (time-boxed after the first three -- Ward Metrics, Access Ladder, Division Metrics -- gave a clean, consistent, reviewer-confirmed signal); those three all use `BarChart` and its rotated-tick `xAxisAngle` path same as the measured cards (`CostMetricsPanel.jsx`, `RatioMetricsPanel.jsx` both call `spaceForRotatedTicks` via `BarChart`), so the fix is expected to reach them the same way, just not independently confirmed with numbers here. Publishes automatically via `publish.yml` on this push (version-increase gate). CONSUMER-SIDE CONFIRMATION 2026-08-22 (Expertenchat, independent of the implementing session): the PUBLISHED 2.42.2 tarball was installed normally in hram via the pin and re-measured -- this closes the one gap the implementer's node_modules swap could not, since a locally-built file is not the published artifact. (a) Artifact inspected: `dist/index.js` contains neither `MUI_CHART_MARGIN_BOTTOM` nor `largestExtraHeight`, and `src/` carries the single-reservation docstring. (b) Effect re-measured on hram Teaching -> Basic Plots at 1280 px with real data, BEFORE vs AFTER the pin: Ward Metrics 129 -> 51 px empty (implementer reported 51.6), Division Metrics 85 -> 41 (reported 41.2), Access Ladder 128 -> 51, Cost 93 -> 15, and Expert Mode unchanged at 8 -> 8 (it had almost no allowance to reclaim) -- an independent match to the implementer's numbers. No clipping at either width measured. hram pin bumped in `ae0fd956`. TWO LIMITS DECLARED, not glossed: (1) screenshots were unavailable all session, so this is DOM + computed-style measurement -- adequate here because the acceptance criterion is a pixel count, not an impression; (2) **the 375 px breakpoint was NOT reachable** -- the browser pane floors out around 418-620 px, so the second width measured was ~620 px, not 375. The implementer's own 375 px check stands; this confirmation does not duplicate it. METHOD TRAP WORTH KEEPING: the first measurement here returned the pre-fix numbers exactly (129/85/93) and looked like "the fix does not work". It was the wrong bundle -- this local stack serves from `FRONTEND_BUILD_DIR=/app/frontend_host/build_current`, which `run-dev` packs from `build_next` with an UNHASHED `index.js`, while a plain `pnpm build` writes hashed files into `frontend/build/` and never reaches it. Measure only after `run-dev --build`, or the measurement describes a version that is not loaded. |
96
96
  | UCM-CHART-11 | The rotated-tick allowance is now counted once, but estimated too high | CHART-10 removed the double reservation and that holds. What remains is a SINGLE reservation that is too large. Measured in hram 2026-08-22, ucm 2.42.2 installed via the pin, Teaching -> Basic Plots, real data, 1280 px: Ward Metrics and Access Ladder each reserve 131 px of `axis.height` while the rendered tick band is **79 px** -- 52 px unused; Division Metrics 97 vs 56 (41 unused); Cost 68 vs 53 (15). All four are -45 deg and the same preset. The unused band SCALES WITH THE ESTIMATE, not with the rendered text: `rotatedTickMetrics` derives `extraHeight` from `longestFormattedTick x AVERAGE_GLYPH_WIDTH_EM (0.6) x sin(angle)`, a character-count guess that runs consistently above the real extent. **Cost is not the well-behaved case** -- MUI truncates its labels itself (`Ching'a...`), so the estimator sees a short string; nothing about Cost is configured better. | 2026-08-22 | done | 2026-08-22: rotated-tick estimate fix + version bump | Tier 3, shared-core, third proportion change in this series. Full order: [`work-orders/UCM-CHART-11.md`](work-orders/UCM-CHART-11.md). Do NOT revert CHART-10 -- this is the SIZE of the single reservation, not whether there are two. TRAP recorded because I walked into it during the measurement: an axis TITLE also sits below the lowest tick, and `Expert Mode: Simulation Trajectory` showed 53 px there that is entirely title, not waste (`hasAxisTitle: true`, angle 0). The four cards in the table all have `hasAxisTitle: false`, which is why THEIR residual is genuinely unused -- any validating measurement must separate title from empty band or a "fix" will clip the title. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged earlier the same day during `UCM-CHART-9`) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3. **Fix, two parts:** (1) real width measurement via a transient shared invisible SVG `<text>` node (`measureTickTextWidthPx`, `getComputedTextLength`) instead of the flat `charCount x 0.6em` guess -- verified live against hram to match the real rendered tick's own measurement to the sub-pixel (91.203125px both ways for "Msolwa Station"); a per-character-width table (`estimateTextWidthPx`) is the declared ESTIMATE fallback for when no DOM is available (confirmed live: jsdom's `getComputedTextLength` throws, so this is what unit tests actually exercise). (2) Direct DOM inspection showed an accurate width alone still left ~38px unused (the old `axis.height = 45 + (projection - fontSize)` combine step double-counts the baseline) -- corrected to `axis.height = max(45, ceil(projection) + 8px clearance)`, i.e. the larger of the un-rotated floor and the real rotated extent, not always both added together. **Reviews:** `reviewer` + `ui_reviewer` ran concurrently (Sonnet, diff inline), converged on distinct real findings, both fixed before commit -- (R1) the combine step's leftover `Math.max(1, ...)` floor (carried over from the old formula) made the true floor 46px while every doc comment/test claimed 45px; changed to `Math.max(0, ...)` and added a test pinning the exact-45 case. (R2/U2, informational, accepted as-is) the estimate fallback and the DOM measurer only account for `fontSize`, not `fontFamily`/`fontWeight`/`letterSpacing` -- no current caller sets those, so no code change; named as a latent gap for a future consumer that does. (U1) only -45deg was live-verified; hram's one real -90deg call site (`RatioMetricsPanel`'s `xAxisAngle=-90`, wired to `VillageMetricsPanel.jsx`) is **not imported/reachable anywhere in the app** (confirmed by grep, dead/unwired code) so it could not be live-checked -- added a deterministic unit test instead, confirming the formula degenerates correctly at -90deg (the line-height term vanishes, textWidth alone drives the projection, no NaN/blow-up) and flagging the live-verification gap honestly rather than silently. **Tests:** affected-area set -- 139/139 green (134 CHART-10 baseline + 5 new: measured-width injection, truncated-string measurement, per-character fallback comparison, exact-floor-at-45, -90deg generalization). **Rendered check -- numeric, on hram with real data** (Teaching -> Basic Plots, `CLI-Kilombero_2026-per_cell-network`, real production-shaped data), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement -- see below): at 1280px, Ward Metrics 129.6px -> **14.6px** empty; Access Ladder 156.9px -> **14.6px**; Division Metrics 85.2px -> **14.2px**; Cost 92.3px -> **14.6px** (unchanged as expected -- its `axis.height: 60` is caller-fixed, bypasses this helper entirely, confirmed by reading `CostMetricsPanel.jsx`). All four land under the WO's ~15px target. Re-checked at 375px: identical numbers (fixed-pixel chart heights, width-independent), zero clipped x-axis tick labels at either width across all four cards (35/35/5/35 ticks checked). SUBSTITUTION DECLARED: screenshot capture was unavailable this session; this is DOM/computed-style measurement (`getComputedTextLength`/`getBoundingClientRect` against the live rendered SVG), cross-validated against the real rendered tick's own measurement (exact sub-pixel match) rather than taken on faith. **Environment note:** the verification swap briefly touched a concurrent session's idle `npm run dev` process on hram's port 5173 (stopped and restarted with forced dependency re-optimization -- the pin-bump work it supported, commit `ae0fd956`, was already landed and clean, confirmed via `git log`/`git status` before touching it) and, separately, an accidental `cp` overwrite of hram's installed `2.42.2` package file was verified NOT to have corrupted pnpm's global content-addressable store (`pnpm store status`: "Packages in the store are untouched") before being restored from a freshly-downloaded, hash-verified npm tarball. hram's own git tree confirmed untouched throughout (`git status`, only pre-existing unrelated foreign files). Publishes automatically via `publish.yml` on this push (version-increase gate). |
97
97
  | UCM-CHART-12 | One layout model for charts. BREAKING (3.0.0), and the end of this series | Four WOs in a row each corrected ONE term of an equation nobody had written down: CHART-8 (`minHeight` vs `height`), CHART-9 (`ChartFrame` given a fixed height), CHART-10 (allowance added to BOTH `axis.height` and `margin.bottom`), CHART-11 (that allowance ESTIMATED above the rendered text). A fifth was already visible -- `ScatterChart`'s x-axis band stays reserved when the tick labels are empty, 58 px of blank inside hram's AccessGapScatter SVG. **Operator called a stop: no more term-by-term corrections.** Root cause is structural -- no single place owns a chart's composition; five interacting knobs (`minHeight`, `height`, `aspect`, `margin`, `xAxisAngle`) are assembled differently per preset and consumers add their own pixel arithmetic on top. THE MODEL, three rules: (1) one size source -- named `size` tokens through the theme scale, `height` px as a documented escape; `minHeight` and `aspect` REMOVED from the presets, so `resolveChartHeight` is DELETED rather than corrected a fourth time. (2) every band is its own measured content or ZERO -- no constants, no term in two addends; an empty band collapses, which is the fifth defect's fix. (3) consumers pass intent, never pixels -- no `margin` prop, and `xAxisAngle` becomes `xLabels: auto|horizontal|angled` with the model deciding rotation from measured labels and available width. | 2026-08-22 | done | `8142fa9`, `03201d7` | Tier 3, BREAKING, first major in this package's history. Full order: [`work-orders/UCM-CHART-12.md`](work-orders/UCM-CHART-12.md). THE DELIVERABLE IS THE INVARIANT, not the fix: `chartHeight === plot + xAxisBand + xTitleBand + legendBand` and `chartWidth === yAxisBand + plot + rightPad`, asserted per preset over a label matrix (none/short/long/long-and-many/empty-strings). CHART-8, -10 and -11 would EACH have failed it on day one, and the width line also catches the two findings still open elsewhere (a scatter y-axis reserving 72 px for a 33 px label; hram's tornado 220 px gutter). It must not be dropped for expedience. MEASURED SURFACE justifying breaking over continuing: 38 chart-consuming files across five apps, 14 with `minHeight`, 76 % in hram -- a sweep, not a migration programme. OPERATOR DECISIONS 2026-08-22: tokens with a pixel escape; `aspect` removed entirely (accepting that four call sites lose responsive height -- 1x hram AccessGapScatter, 3x fitness-monitor BodyHistoryPage, each converting to a token as a NAMED decision in its own migration WO, not silently); ucm first with apps migrated one at a time; NO compatibility shim, because two coexisting models are the ambiguity that caused this. `ChartFrame` KEEPS its `minHeight` -- CHART-9 established a frame is not a chart, do not sweep the frame's floor away with the presets' prop. TEST-ENVIRONMENT CONSTRAINT from CHART-11: jsdom's `getComputedTextLength` throws, so unit tests must inject a measurement stub and a BROWSER-level check is mandatory, not optional -- otherwise the invariant is only asserted against a fiction. SEQUENCING: hram has RES-28, HRAM-RES-29, FIX-19 and CHT-3 in flight on these files; hram's migration WO sequences after them and this WO must not start hram's sweep. **Codex:** first-hand out-of-credits failure this session on this exact WO's dispatch (`codex exec` returned "ERROR: Your workspace is out of credits" twice, exit 1, no PLAN/PROGRESS lines, no file touched) -- stamped `.claude/codex-status.md` 2026-08-22 and implemented directly in Claude, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at this WO's Tier 3, so the flip changed no routing. **Review runtime note:** `.claude/models.local.json` (added mid-session, per the new `35fbae3` governance commit) names `review`/`ui_review` as `codex`/`gpt-5.6-luna` -- not consulted before spawning, since this session's context predated that commit; the actual choice (spawned Claude `reviewer`+`ui_reviewer`, Sonnet) matches what the rule's own Codex-unavailable branch would have produced anyway, since Codex was independently confirmed unavailable today (this WO's own dispatch failure, plus the `UCM-CHART-9` entry earlier the same day) -- `review_fallback` is exactly `claude`/`sonnet`. Named here for the record, not as a retroactive justification. **Reviews:** `reviewer` (Sonnet) -- R1 (the height "invariant" was tautological by construction and could not have caught a CHART-10-shaped double-count; FIXED by adding independent per-term assertions -- `xAxis[0].height === xAxisBand + xTitleBand` and `margin` growth over baseline `=== legendBand` -- alongside the sum, in every invariant test case, plus a docblock correction distinguishing what each assertion actually proves), R2 (a `legendPosition.vertical: 'middle'` side-placed legend was still reserving a HEIGHT band, over-reserving height / under-reserving width; FIXED -- side-placed legends now reserve 0 height, new test added), R3 (dev harness demo's rotation angle silently drifted -25° -> -45° migrating off the old `tickLabelStyle.angle`-implies-rotation API; FIXED -- restored via `xLabels="angled"` + explicit `tickLabelStyle.angle: -25` override, commented). `ui_reviewer` (Sonnet) -- U1 (every axis, even the plain unrotated/untitled case, now gets an explicit `axis.height` where MUI used to fill in its own default -- verified against `node_modules/@mui/x-charts/constants`: `DEFAULT_AXIS_SIZE_HEIGHT=25`/`AXIS_LABEL_DEFAULT_HEIGHT=20` match `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` exactly, so no rendering changes for that case; comment + doc note added to make this provable, not just asserted), U2 (docs/CHART-LAYOUT.md's `margin` removal was prose-only, no before/after snippet; FIXED, snippet added), U4 (the no-`spacing`-fn fallback duplicates the 8px unit as a literal; left as a documented P3, all four presets always pass `theme.spacing` in practice). Both reviews' findings fixed; no open findings after fixes. **Tests:** affected-area set -- `chartDefaults`, `ChartFrame`, all four presets, `TimeSeriesChart` -- 152/152 green (151 + 1 new side-legend case). Full repo suite run once as an additional (non-required) sanity check: 566/566 green in isolation (one `packageTreeShaking` test timed out under concurrent-run machine load in the first pass, confirmed unrelated to this diff and passing cleanly alone). **Browser-level check, on this repo's own dev harness** (`dev/entries.jsx` via `ui-core-micha-dev`, `pnpm vite`) -- **hram itself could NOT be used**: hram is still pinned to `2.41.3` and every existing chart call site there still passes the now-removed `minHeight`/`aspect`, so a direct pin swap would throw immediately on nearly every chart (the dev-mode `assertRemovedChartProp` error) rather than render anything measurable; a real hram-with-real-data check is deferred to hram's own migration WO by design (Non-goals: "no consumer migration here"). Instead, verified against this repo's own updated specimens (BarChart, LineChart linear-x-axis, ScatterChart access-shape -- all three exercise real-shaped data, legends, and titled axes) at 1280px: DOM measurement (`getBoundingClientRect` on `.MuiChartsSurface-root`/`.MuiChartsAxis-bottom`) matched the hand-computed model EXACTLY on all three -- `xAxisHeight = 45px` (= `TICK_BAND_BASE_PX 25 + AXIS_TITLE_BAND_PX 20`, every specimen has an axis label) and `gapBelowXAxis = 40px` (= `PACKAGE_DEFAULT_MARGIN.bottom 8 + LEGEND_BAND_PX 32`, every specimen shows a legend). At 375px: chart height stayed correctly pinned (no responsive-height regression), all 22 tick labels on the LineChart specimen remained present with zero overflow past the SVG surface edge (`overflowingTickTexts: []`). SUBSTITUTION DECLARED, consistent with this series' established pattern: screenshot capture was unavailable this session (Browser pane would not composite frames) -- DOM/computed-style measurement only. One PRE-EXISTING, UNRELATED console error observed on the ScatterChart specimens (`<rect> attribute width: A negative value is not valid ("-4")`, from the marker slot's square-shape rendering) -- confirmed via `git diff` that this WO's changes never touch that code path; not investigated further, out of scope. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.0.0 > 2.42.3`). **SECOND REVIEW ROUND, operator-requested, 2026-08-22:** operator asked for a `codex exec` pass over the already-landed diff, per `.claude/models.local.json` (`review`/`ui_review`: `codex`/`gpt-5.6-luna`) -- confirmed Codex available today first (`.claude/codex-status.md` had a fresh `available` line at the top of the log). Invoked directly via Bash with both mandatory flags and `-m gpt-5.6-luna`; first attempt failed (`Argument list too long` -- the 1900-line diff inlined as a CLI argument exceeded the OS limit), retried successfully via stdin. Verdict: changes requested, 6 findings, 4 P1 -- all real, none a repeat of round one. **(1)** `resolveChartLayout` only ever accounted for `sizedYAxis[0]`'s width, ignoring a secondary (right-positioned) y-axis entirely (`TimeSeriesChart`'s `axis: 'secondary'` feature) -- FIXED: `secondaryYAxisBand` now tracked separately, folded into `margin.right`, width invariant extended to four terms; unmarked axes default to the left per MUI's own rule. **(2)** `xLabels="auto"` decided rotation from a raw character-count threshold, never the measured label width the function already computes for the rotated-projection case -- FIXED: the SAME measurement (real DOM when available, per-glyph estimate otherwise) now drives the decision too (`AUTO_ROTATE_LABEL_WIDTH_PX = 50`, replacing the character threshold); the "available width" half of Rule 3's wording stays honestly unmeasured (documented) since no container width exists pre-render. **(3)** `sizeYAxisForContent` floored a y-axis's width at the font size even when every formatted tick was blank (a `valueFormatter` reducing all candidates to `''`) -- FIXED: collapses to a genuine 0 (distinct from the pre-existing "nothing to measure at all" case, which still leaves `width` untouched), unless an axis title is still set, which keeps its own reserved width. **(4)** `slotProps.legend.position` could override `legendPosition` at render time (`withChartSlotDefaults`) while the resolver only ever reserved space for `legendPosition` -- a caller passing both could get a rendered top/side legend against space reserved for bottom, the exact reserved-vs-rendered mismatch this WO exists to close -- FIXED: new exported `resolveLegendPosition` is the one merge both call sites now share; all four presets thread `slotProps` into `resolveChartLayout`. **(5, P2)** a caller-set `xAxis[0].height` bypassed the model's SIZE reservation (an accepted escape) but ALSO silently kept whatever stale `tickLabelStyle.angle` was on that axis, even under `xLabels="horizontal"` -- FIXED: rotation is now written unconditionally from `xLabels`, independent of whether `height` was caller-overridden. **(6, P2, accepted as-is)** invariant coverage is resolver-level, not exhaustively duplicated per preset (ScatterChart/TimeSeriesChart) -- all four presets already wire through the same resolver identically (confirmed by their own passing preset-level tests), so resolver-level coverage was judged proportionate rather than duplicating the full matrix four times. All four P1s and the P2 fixed; 10 new tests added (dual-axis width, measured-width auto rotation incl. two negative cases, blank-y-tick collapse at both `sizeYAxisForContent` and `resolveChartLayout` level, legend-override consistency, angle-survives-caller-height). Re-verified: 162/162 chart tests green, `tsc -p tsconfig.build.json` clean. Per Reviews (`AGENTS.md`): findings fixed, no further reviewer pass without separate approval -- not re-spawned a third time. Version bumped `3.0.0` -> `3.0.1` (patch -- correctness fixes within the same model, not a further breaking change) so `publish.yml`'s gate fires again on this push. |
98
+ | UCM-CHART-13 | `ChartFrame` accepts three sizing props and applies one. Remove the other two | Measured 2026-08-22 against 3.0.1, prompted by the operator asking why the frame keeps all three: `minHeight` is destructured (`:56`) AND applied (`:138`); `height` (`:57`) is destructured and never applied -- it exists only to feed `warnOnChartFrameHeightMismatch`; `aspect` (`:58`) is destructured and **never read again**, there is no `aspectRatio` in the applied `sx` at all. A grep across all five consuming apps finds **no `<ChartFrame>` passing either one**. The justification offered for keeping them -- `docs/CHART-LAYOUT.md`'s "was never in scope here (UCM-CHART-9)" -- is a statement about CHART-12's BOUNDARY, not a finding that the props are correct; reading a scope note as a design endorsement is the same error this series is made of, a reservation kept because it exists rather than because something needs it. | 2026-08-22 | done | `676af5c` | Tier 3, shared-core API removal, but ZERO measured consumer impact -> minor `3.1.0`. Full order: [`work-orders/UCM-CHART-13.md`](work-orders/UCM-CHART-13.md). `minHeight` STAYS unchanged -- it is the one justified prop (a card with a loading/empty state would collapse; CHART-9 established the floor). `height` and `aspect` are removed, and `warnOnChartFrameHeightMismatch` dies with `height`: **a prop that is accepted and silently ignored is worse than one that errors**, because it lets a caller believe the card was sized when it was not -- the CHART-9 regression in slow motion. Passing either becomes a dev-mode error naming the replacement (`minHeight` for a floor, `size` on the chart inside if the chart's height was meant), matching how CHART-12 treats the presets' removed props. Also corrects `docs/CHART-LAYOUT.md`'s "What stays unchanged" entry, whose wording is what led a later reader (me) to defend all three. Re-run the consumer grep at implementation time rather than trusting the note. **Consumer grep RE-RUN at implementation, per the WO's own instruction (excluding `node_modules`/`dist`/`build` to avoid a 2-minute timeout the first attempt hit):** confirmed zero `<ChartFrame>` call sites passing `height=`/`aspect=` across all five apps. **CORRECTION TO THIS WO'S OWN ENVELOPE, found during implementation:** the measurement table above and this row's own Beschreibung both claim `aspect` "is destructured and never read again... there is no `aspectRatio` in the applied `sx` at all" -- **this is factually wrong.** `git show db335b5:src/components/charts/ChartFrame.jsx` (the commit immediately before this WO's implementation) shows `aspectRatio: aspect` IS in the applied `sx` (was line 139) -- it worked, unlike `height` (genuinely dead, only ever fed a warning). Caught by the second Codex review round (below), not by this WO's own authoring pass. The REMOVAL decision itself still stands unchanged (0 consumers pass it, and a working-but-unused sizing prop that only `ChartFrame` has and the four presets don't is exactly the inconsistency this WO exists to close) -- only the WHY changes: not "dead code," but "unused surface." `ChartFrame.jsx`'s own docblock and `docs/CHART-LAYOUT.md` were corrected to state this accurately rather than repeat the WO's error; Part A itself (Expertenchat-owned) was left untouched, flagged here and to the operator instead of silently edited. **Implementation:** `.claude/models.local.json`'s `implementation.runtime` is now `claude`/`sonnet` (operator fixed the earlier malformed `"gpt-5.6-luna"` value) -- implemented directly, no Codex dispatch. **Reviews, both rounds through `codex exec -m gpt-5.6-luna`** (per `review`/`ui_review` in `.claude/models.local.json`; Codex confirmed available today, `.claude/codex-status.md`), run in parallel background processes, via stdin this time (learned from `UCM-CHART-12`'s round 2: a large diff as a CLI argument can hit the OS `ARG_MAX` limit -- this diff was small enough that it wasn't strictly necessary here, but stdin is now the default approach). `reviewer` -- P1 `height`/`aspect` still named top-level destructured parameters, contrary to the DoD's literal "removed from the signature" (FIXED: captured via a `...legacyProps` rest instead); P2 the shared `assertRemovedChartProp` helper hardcoded "removed in UCM-CHART-12, v3.0.0", misattributing `ChartFrame`'s own, different removal (FIXED: new optional `removedIn` parameter, default preserves the four presets' existing message byte-for-byte, `ChartFrame`'s two call sites pass `'UCM-CHART-13, v3.1.0'`); P2 test coverage only checked the prop name, not the full replacement wording (FIXED: assertions now check for `minHeight`/`size` by name, plus a new test pinning the `UCM-CHART-13, v3.1.0` attribution). `ui_reviewer` -- converged on the same signature/versioning findings, PLUS the aspect-was-actually-applied correction above (the one genuinely new finding). All fixed; 166/166 chart tests green (145 prior + 21 in `ChartFrame.test.jsx`, up from 16 pre-WO), `tsc -p tsconfig.build.json` clean. Version bumped `3.0.1` -> `3.1.0` (minor, per the WO's own classification). |
@@ -1,40 +1,46 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useEffect, useId, useRef, useState } from 'react';
13
+ import React, { useId, useRef, useState } from 'react';
3
14
  import { Alert, Box, Button, CircularProgress, Paper, Stack, Typography, } from '@mui/material';
4
15
  import { useTranslation } from 'react-i18next';
5
16
  import { useTheme } from '@mui/material/styles';
6
17
  import { exportChartPng, exportChartSvg } from './exportChart';
18
+ import { assertRemovedChartProp } from './chartDefaults';
7
19
  export const CHART_FRAME_ROOT_SX = { p: 2 };
8
20
  export const CHART_FRAME_ALERT_SX = { mt: 2 };
9
- /**
10
- * UCM-CHART-12: `warnOnHeightMismatch` (the shared helper this note used to call into
11
- * `chartDefaults`) was deleted along with `resolveChartHeight` -- both belonged to the
12
- * `{minHeight, height, aspect}` trio the four chart presets used to accept, which that WO
13
- * replaced with `resolveChartLayout`'s `size`/`height` model. `ChartFrame` keeps its OWN
14
- * `minHeight`/`height`/`aspect` props unchanged (CHART-9: it is a card floor, not a chart size),
15
- * so it keeps this note too -- same text, same condition, just no longer routed through the
16
- * now-deleted shared function.
17
- */
18
- function warnOnChartFrameHeightMismatch(minHeight, height) {
19
- if (process.env.NODE_ENV === 'production')
20
- return;
21
- if (minHeight == null || height == null || minHeight === height)
22
- return;
23
- // eslint-disable-next-line no-console
24
- console.warn(`[ui-core-micha] <ChartFrame> received both minHeight={${minHeight}} and height={${height}}; `
25
- + 'they disagree; height is ignored here and minHeight keeps sizing the wrapper as a floor. '
26
- + 'Pass only minHeight, or drop height if it was meant to size the whole card.');
27
- }
28
21
  /**
29
22
  * Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
30
23
  *
31
24
  * `minHeight` (CHART-9): always just a floor on the content box, which holds a whole card --
32
- * title, toolbar, chart, legend, footnotes, export links -- not a single chart. `height` is never
33
- * applied to the box (unlike the four chart presets, whose box holds exactly one chart and use
34
- * `resolveChartLayout`'s `size`/`height` resolution); it is only compared against `minHeight` for
35
- * this component's own dev-mode mismatch note.
25
+ * title, toolbar, chart, legend, footnotes, export links -- not a single chart.
26
+ *
27
+ * UCM-CHART-13: `height` and `aspect` are GONE. `height` was destructured and only ever fed a
28
+ * dev-mode mismatch warning, never applied to the box -- genuinely dead. `aspect` is a DIFFERENT
29
+ * case (codex review finding, corrected from this WO's own Envelope, which claimed it too was
30
+ * applied nowhere): it WAS wired to `aspectRatio` on the box at 3.0.1, and worked. It is removed
31
+ * anyway, because no consumer across the five apps passes it (measured against 3.0.1) and
32
+ * `ChartFrame` accepting a sizing prop it is the only one of the four+frame components to apply is
33
+ * exactly the kind of inconsistency this WO exists to close -- a working-but-unused prop is still
34
+ * unused surface, just not dead code the way `height` was. Passing either now throws in dev via the
35
+ * same `assertRemovedChartProp` the four chart presets use (`UCM-CHART-12`), naming `minHeight` (a
36
+ * card floor) or the chart's own `size` prop as the replacement.
36
37
  */
37
- export function ChartFrame({ title, subtitle, toolbar, controls, loading = false, error = false, isEmpty = false, emptyMessage, minHeight, height, aspect, exportOptions = false, onExportSvg, onExportPng, meta, ariaLabel, children, variant = 'outlined', titleVariant = 'h6', sx, }) {
38
+ export function ChartFrame(_a) {
39
+ var { title, subtitle, toolbar, controls, loading = false, error = false, isEmpty = false, emptyMessage, minHeight, exportOptions = false, onExportSvg, onExportPng, meta, ariaLabel, children, variant = 'outlined', titleVariant = 'h6', sx } = _a,
40
+ // UCM-CHART-13: `height`/`aspect` are gone from the signature itself (DoD) -- captured via rest,
41
+ // not named parameters, so they read as "detected leftovers", not "accepted props" a reader might
42
+ // assume are still wired to something.
43
+ legacyProps = __rest(_a, ["title", "subtitle", "toolbar", "controls", "loading", "error", "isEmpty", "emptyMessage", "minHeight", "exportOptions", "onExportSvg", "onExportPng", "meta", "ariaLabel", "children", "variant", "titleVariant", "sx"]);
38
44
  const { t } = useTranslation();
39
45
  const theme = useTheme();
40
46
  const titleId = useId();
@@ -46,7 +52,8 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
46
52
  const hasCustomAriaLabel = Boolean(ariaLabel);
47
53
  const chartLabel = ariaLabel || title;
48
54
  const callerSx = Array.isArray(sx) ? sx : [sx];
49
- useEffect(() => warnOnChartFrameHeightMismatch(minHeight, height), [minHeight, height]);
55
+ assertRemovedChartProp('ChartFrame', 'height', legacyProps.height, 'Use minHeight for the card floor, or size on the chart inside the frame if you meant the chart\'s own height.', 'UCM-CHART-13, v3.1.0');
56
+ assertRemovedChartProp('ChartFrame', 'aspect', legacyProps.aspect, 'Removed -- it was never applied to the frame. Use size on the chart inside the frame if you meant the chart\'s own aspect.', 'UCM-CHART-13, v3.1.0');
50
57
  const runExport = async (type) => {
51
58
  setExportError(false);
52
59
  try {
@@ -74,7 +81,6 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
74
81
  return (_jsxs(Paper, { variant: variant, sx: [CHART_FRAME_ROOT_SX, ...callerSx], children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 1 }, children: [_jsxs(Box, { children: [_jsx(Typography, { id: titleId, variant: titleVariant, children: title }), subtitle && _jsx(Typography, { variant: "body2", color: "text.secondary", children: subtitle })] }), toolbar && _jsx(Box, { children: toolbar })] }), controls && _jsx(Box, { sx: { mb: 2 }, children: controls }), _jsx(Box, { ref: chartRef, role: "img", "aria-label": chartLabel, "aria-labelledby": hasCustomAriaLabel ? undefined : titleId, sx: {
75
82
  width: '100%',
76
83
  minHeight,
77
- aspectRatio: aspect,
78
84
  display: 'flex',
79
85
  alignItems: 'center',
80
86
  justifyContent: 'center',
@@ -506,12 +506,18 @@ export function resolveChartLayout({ size = 'standard', height, xAxis, yAxis, se
506
506
  * migration path instead of a layout that quietly stops matching what the prop says) -- gated on
507
507
  * `NODE_ENV !== 'production'` like the deleted `warnOnHeightMismatch` was, so a shared package
508
508
  * cannot crash a consumer's production page over a layout prop; loud in dev, inert in prod.
509
+ *
510
+ * `removedIn` (codex review finding, UCM-CHART-13): a shared helper hardcoding ONE WO/version
511
+ * string would misattribute the removal for every OTHER call site that reuses it -- `ChartFrame`
512
+ * (UCM-CHART-13, v3.1.0) removes `height`/`aspect` for a different reason, in a different WO, than
513
+ * the four presets' own `minHeight`/`aspect`/`margin` (UCM-CHART-12, v3.0.0). The default keeps
514
+ * every existing call site's message byte-identical; only a NEW call site needs to override it.
509
515
  */
510
- export function assertRemovedChartProp(componentName, propName, value, replacement) {
516
+ export function assertRemovedChartProp(componentName, propName, value, replacement, removedIn = 'UCM-CHART-12, v3.0.0') {
511
517
  if (value === undefined || process.env.NODE_ENV === 'production')
512
518
  return;
513
- throw new Error(`[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in UCM-CHART-12, `
514
- + `v3.0.0 -- see docs/CHART-LAYOUT.md). ${replacement}`);
519
+ throw new Error(`[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in ${removedIn} `
520
+ + `-- see docs/CHART-LAYOUT.md). ${replacement}`);
515
521
  }
516
522
  /**
517
523
  * UCM-CHART-12 (codex second-pass review finding): the single source of truth for where the
@@ -125,9 +125,13 @@ the right amount.
125
125
 
126
126
  ## What stays unchanged
127
127
 
128
- - `ChartFrame`'s own `minHeight`/`height`/`aspect` — it is a whole-card floor (title, toolbar,
129
- chart, legend, footnotes, export links), not a single chart's size, and was never in scope here
130
- (`UCM-CHART-9` established this distinction).
128
+ - `ChartFrame`'s own `minHeight` — it is a whole-card floor (title, toolbar, chart, legend,
129
+ footnotes, export links), not a single chart's size (`UCM-CHART-9` established this distinction).
130
+ `height` and `aspect` are **not** part of this — `UCM-CHART-13` removed them, but for different
131
+ reasons: `height` was destructured and only ever fed a dev-mode warning, never applied — genuinely
132
+ dead. `aspect` was applied (`aspectRatio` on the box, at 3.0.1) and worked; it is gone because no
133
+ consumer across the five apps passed it (measured against 3.0.1), not because it did nothing.
134
+ Passing either now throws in dev, same as the four presets' removed props.
131
135
  - Palettes, series colours, tooltips, legend content, interaction.
132
136
  - `sizeYAxisForContent` (y-axis width from tick content) — reused unchanged inside the resolver.
133
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "sideEffects": [
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useId, useRef, useState } from 'react';
1
+ import React, { useId, useRef, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Box,
@@ -11,38 +11,27 @@ import {
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { useTheme } from '@mui/material/styles';
13
13
  import { exportChartPng, exportChartSvg } from './exportChart';
14
+ import { assertRemovedChartProp } from './chartDefaults';
14
15
 
15
16
  export const CHART_FRAME_ROOT_SX = { p: 2 };
16
17
  export const CHART_FRAME_ALERT_SX = { mt: 2 };
17
18
 
18
- /**
19
- * UCM-CHART-12: `warnOnHeightMismatch` (the shared helper this note used to call into
20
- * `chartDefaults`) was deleted along with `resolveChartHeight` -- both belonged to the
21
- * `{minHeight, height, aspect}` trio the four chart presets used to accept, which that WO
22
- * replaced with `resolveChartLayout`'s `size`/`height` model. `ChartFrame` keeps its OWN
23
- * `minHeight`/`height`/`aspect` props unchanged (CHART-9: it is a card floor, not a chart size),
24
- * so it keeps this note too -- same text, same condition, just no longer routed through the
25
- * now-deleted shared function.
26
- */
27
- function warnOnChartFrameHeightMismatch(minHeight, height) {
28
- if (process.env.NODE_ENV === 'production') return;
29
- if (minHeight == null || height == null || minHeight === height) return;
30
- // eslint-disable-next-line no-console
31
- console.warn(
32
- `[ui-core-micha] <ChartFrame> received both minHeight={${minHeight}} and height={${height}}; `
33
- + 'they disagree; height is ignored here and minHeight keeps sizing the wrapper as a floor. '
34
- + 'Pass only minHeight, or drop height if it was meant to size the whole card.',
35
- );
36
- }
37
-
38
19
  /**
39
20
  * Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
40
21
  *
41
22
  * `minHeight` (CHART-9): always just a floor on the content box, which holds a whole card --
42
- * title, toolbar, chart, legend, footnotes, export links -- not a single chart. `height` is never
43
- * applied to the box (unlike the four chart presets, whose box holds exactly one chart and use
44
- * `resolveChartLayout`'s `size`/`height` resolution); it is only compared against `minHeight` for
45
- * this component's own dev-mode mismatch note.
23
+ * title, toolbar, chart, legend, footnotes, export links -- not a single chart.
24
+ *
25
+ * UCM-CHART-13: `height` and `aspect` are GONE. `height` was destructured and only ever fed a
26
+ * dev-mode mismatch warning, never applied to the box -- genuinely dead. `aspect` is a DIFFERENT
27
+ * case (codex review finding, corrected from this WO's own Envelope, which claimed it too was
28
+ * applied nowhere): it WAS wired to `aspectRatio` on the box at 3.0.1, and worked. It is removed
29
+ * anyway, because no consumer across the five apps passes it (measured against 3.0.1) and
30
+ * `ChartFrame` accepting a sizing prop it is the only one of the four+frame components to apply is
31
+ * exactly the kind of inconsistency this WO exists to close -- a working-but-unused prop is still
32
+ * unused surface, just not dead code the way `height` was. Passing either now throws in dev via the
33
+ * same `assertRemovedChartProp` the four chart presets use (`UCM-CHART-12`), naming `minHeight` (a
34
+ * card floor) or the chart's own `size` prop as the replacement.
46
35
  */
47
36
  export function ChartFrame({
48
37
  title,
@@ -54,8 +43,6 @@ export function ChartFrame({
54
43
  isEmpty = false,
55
44
  emptyMessage,
56
45
  minHeight,
57
- height,
58
- aspect,
59
46
  exportOptions = false,
60
47
  onExportSvg,
61
48
  onExportPng,
@@ -65,6 +52,10 @@ export function ChartFrame({
65
52
  variant = 'outlined',
66
53
  titleVariant = 'h6',
67
54
  sx,
55
+ // UCM-CHART-13: `height`/`aspect` are gone from the signature itself (DoD) -- captured via rest,
56
+ // not named parameters, so they read as "detected leftovers", not "accepted props" a reader might
57
+ // assume are still wired to something.
58
+ ...legacyProps
68
59
  }) {
69
60
  const { t } = useTranslation();
70
61
  const theme = useTheme();
@@ -78,9 +69,15 @@ export function ChartFrame({
78
69
  const chartLabel = ariaLabel || title;
79
70
  const callerSx = Array.isArray(sx) ? sx : [sx];
80
71
 
81
- useEffect(
82
- () => warnOnChartFrameHeightMismatch(minHeight, height),
83
- [minHeight, height],
72
+ assertRemovedChartProp(
73
+ 'ChartFrame', 'height', legacyProps.height,
74
+ 'Use minHeight for the card floor, or size on the chart inside the frame if you meant the chart\'s own height.',
75
+ 'UCM-CHART-13, v3.1.0',
76
+ );
77
+ assertRemovedChartProp(
78
+ 'ChartFrame', 'aspect', legacyProps.aspect,
79
+ 'Removed -- it was never applied to the frame. Use size on the chart inside the frame if you meant the chart\'s own aspect.',
80
+ 'UCM-CHART-13, v3.1.0',
84
81
  );
85
82
 
86
83
  const runExport = async (type) => {
@@ -136,7 +133,6 @@ export function ChartFrame({
136
133
  sx={{
137
134
  width: '100%',
138
135
  minHeight,
139
- aspectRatio: aspect,
140
136
  display: 'flex',
141
137
  alignItems: 'center',
142
138
  justifyContent: 'center',
@@ -556,12 +556,18 @@ export function resolveChartLayout({
556
556
  * migration path instead of a layout that quietly stops matching what the prop says) -- gated on
557
557
  * `NODE_ENV !== 'production'` like the deleted `warnOnHeightMismatch` was, so a shared package
558
558
  * cannot crash a consumer's production page over a layout prop; loud in dev, inert in prod.
559
+ *
560
+ * `removedIn` (codex review finding, UCM-CHART-13): a shared helper hardcoding ONE WO/version
561
+ * string would misattribute the removal for every OTHER call site that reuses it -- `ChartFrame`
562
+ * (UCM-CHART-13, v3.1.0) removes `height`/`aspect` for a different reason, in a different WO, than
563
+ * the four presets' own `minHeight`/`aspect`/`margin` (UCM-CHART-12, v3.0.0). The default keeps
564
+ * every existing call site's message byte-identical; only a NEW call site needs to override it.
559
565
  */
560
- export function assertRemovedChartProp(componentName, propName, value, replacement) {
566
+ export function assertRemovedChartProp(componentName, propName, value, replacement, removedIn = 'UCM-CHART-12, v3.0.0') {
561
567
  if (value === undefined || process.env.NODE_ENV === 'production') return;
562
568
  throw new Error(
563
- `[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in UCM-CHART-12, `
564
- + `v3.0.0 -- see docs/CHART-LAYOUT.md). ${replacement}`,
569
+ `[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in ${removedIn} `
570
+ + `-- see docs/CHART-LAYOUT.md). ${replacement}`,
565
571
  );
566
572
  }
567
573
 
@@ -213,10 +213,8 @@ describe('ChartFrame', () => {
213
213
  });
214
214
 
215
215
  // CHART-9: ChartFrame's content box is a whole CARD (title, toolbar, chart, legend, footnotes,
216
- // export links), not a single chart -- unlike the four chart presets, which do apply
217
- // resolveChartHeight's height/aspect resolution to their own box. ChartFrame's box must never
218
- // take a fixed `height`; `minHeight` is always just a floor content can exceed.
219
- describe('minHeight/height resolution (CHART-9)', () => {
216
+ // export links), not a single chart. `minHeight` is always just a floor content can exceed.
217
+ describe('minHeight resolution (CHART-9)', () => {
220
218
  function contentBoxStyle() {
221
219
  return window.getComputedStyle(screen.getByTestId('chart-body').parentElement);
222
220
  }
@@ -245,37 +243,56 @@ describe('ChartFrame', () => {
245
243
  expect(style.height).toBe('auto');
246
244
  });
247
245
 
248
- it('leaves minHeight as a floor and gives no fixed height when aspect is set (no height)', () => {
249
- renderFrame({ minHeight: 320, aspect: 1.8 });
246
+ it('leaves minHeight unset (auto) when the caller passes none', () => {
247
+ renderFrame();
250
248
  const style = contentBoxStyle();
251
- expect(style.minHeight).toBe('320px');
249
+ expect(style.minHeight).toBe('auto');
252
250
  expect(style.height).toBe('auto');
253
- expect(style.aspectRatio).toBe('1.8 / 1');
254
251
  });
252
+ });
255
253
 
256
- it('does not apply height as a fixed box height even when passed explicitly, and does not throw', () => {
257
- expect(() => renderFrame({ minHeight: 420, height: 380 })).not.toThrow();
258
- const style = contentBoxStyle();
259
- expect(style.minHeight).toBe('420px');
260
- expect(style.height).toBe('auto');
254
+ // UCM-CHART-13: `height`/`aspect` were destructured but never applied to the box -- measured
255
+ // against 3.0.1, no consumer passed either. Both are now gone; passing one throws in dev, naming
256
+ // the replacement, the same as the four chart presets' removed props (`UCM-CHART-12`).
257
+ describe('removed height/aspect props throw in dev, naming their replacement (UCM-CHART-13)', () => {
258
+ it('throws when a caller still passes height, naming minHeight AND size as the replacement', () => {
259
+ expect(() => renderFrame({ height: 380 })).toThrow(/ChartFrame.*height.*minHeight.*size/s);
261
260
  });
262
261
 
263
- it('ignores height when passed alone, with no minHeight floor either', () => {
264
- renderFrame({ height: 280 });
265
- const style = contentBoxStyle();
266
- expect(style.minHeight).toBe('auto');
267
- expect(style.height).toBe('auto');
262
+ it('throws when a caller still passes aspect, naming size as the replacement', () => {
263
+ expect(() => renderFrame({ aspect: 1.8 })).toThrow(/ChartFrame.*aspect.*size/s);
264
+ });
265
+
266
+ it('attributes the removal to UCM-CHART-13/v3.1.0, not the presets\' own UCM-CHART-12 message', () => {
267
+ expect(() => renderFrame({ height: 380 })).toThrow(/UCM-CHART-13.*v3\.1\.0/);
268
+ });
269
+
270
+ it('does not throw when only minHeight is passed', () => {
271
+ expect(() => renderFrame({ minHeight: 320 })).not.toThrow();
272
+ });
273
+
274
+ it('stays inert in production even when height/aspect are passed', () => {
275
+ const original = process.env.NODE_ENV;
276
+ process.env.NODE_ENV = 'production';
277
+ try {
278
+ expect(() => renderFrame({ height: 380, aspect: 1.8 })).not.toThrow();
279
+ } finally {
280
+ process.env.NODE_ENV = original;
281
+ }
268
282
  });
269
283
 
270
- it('warns with wording accurate to its own behaviour -- minHeight wins, not height', () => {
271
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
272
- renderFrame({ minHeight: 420, height: 380 });
273
- expect(warnSpy).toHaveBeenCalledOnce();
274
- const message = warnSpy.mock.calls[0][0];
275
- expect(message).toContain('ChartFrame');
276
- expect(message).toContain('height is ignored');
277
- expect(message).not.toContain('height wins');
278
- warnSpy.mockRestore();
284
+ it('applies nothing to the box for a still-passed height/aspect (dev-mode error aside) -- confirmed via the production path', () => {
285
+ const original = process.env.NODE_ENV;
286
+ process.env.NODE_ENV = 'production';
287
+ try {
288
+ renderFrame({ minHeight: 320, height: 380, aspect: 1.8 });
289
+ const style = window.getComputedStyle(screen.getByTestId('chart-body').parentElement);
290
+ expect(style.minHeight).toBe('320px');
291
+ expect(style.height).toBe('auto');
292
+ expect(style.aspectRatio).toBe('auto');
293
+ } finally {
294
+ process.env.NODE_ENV = original;
295
+ }
279
296
  });
280
297
  });
281
298
  });
@@ -292,6 +292,15 @@ describe('removed chart props throw in dev, naming their replacement (UCM-CHART-
292
292
  .toThrow(/BarChart.*minHeight.*Use size instead\./s);
293
293
  });
294
294
 
295
+ // UCM-CHART-13 (codex review finding): a shared helper hardcoding one WO/version string would
296
+ // misattribute the removal for a different call site's own, different removal.
297
+ it('defaults removedIn to UCM-CHART-12/v3.0.0 (the four presets\' own call sites), but a caller can override it', () => {
298
+ expect(() => assertRemovedChartProp('BarChart', 'minHeight', 320, 'Use size instead.'))
299
+ .toThrow(/UCM-CHART-12, v3\.0\.0/);
300
+ expect(() => assertRemovedChartProp('ChartFrame', 'aspect', 1.8, 'Use size instead.', 'UCM-CHART-13, v3.1.0'))
301
+ .toThrow(/UCM-CHART-13, v3\.1\.0/);
302
+ });
303
+
295
304
  it('assertRemovedChartProp stays inert in production', () => {
296
305
  const original = process.env.NODE_ENV;
297
306
  process.env.NODE_ENV = 'production';
@@ -0,0 +1,122 @@
1
+ # UCM-CHART-13 — A card has no size of its own. Remove `height` and `aspect` from `ChartFrame`.
2
+
3
+ - **Repo:** `ui-core-micha`, branch `main`
4
+ - **Tier:** 3 — shared-core API removal, **four measured consumer call sites** (see below). Breaking:
5
+ `4.0.0`, sequenced with `FM-CHART-1`.
6
+ - **Status:** planned
7
+ - **Workstream:** `CHART-*`
8
+ - **Files:** `src/components/charts/ChartFrame.jsx` and its tests.
9
+
10
+ > **Why. Twice reversed — the reasoning below is what settled it, not the earlier prop-inventory.**
11
+ >
12
+ > Draft 1 said remove `height` **and** `aspect`, on the grounds that both were dead code. That
13
+ > premise was wrong: `aspect` **is** applied — `ChartFrame.jsx:136-139` sets
14
+ > `sx={{ width: '100%', minHeight, aspectRatio: aspect, ... }}`.
15
+ >
16
+ > Draft 2 therefore kept `aspect`. **That was the worse error**, and the operator caught it: it
17
+ > reintroduces through the card exactly what `UCM-CHART-12` removed from the chart.
18
+ >
19
+ > **The structural argument, which neither earlier draft made.** After the migration a consumer
20
+ > looks like this:
21
+ >
22
+ > ```jsx
23
+ > <ChartFrame minHeight={320} aspect="960 / 380"> // height derived from WIDTH
24
+ > <LineChart size="tall" /> // height from a TOKEN
25
+ > </ChartFrame>
26
+ > ```
27
+ >
28
+ > **Two independent size sources for nested boxes that have to agree.** Before the migration they
29
+ > were coherent — same ratio on both. Afterwards the frame tracks width and the chart tracks a token,
30
+ > and they coincide only by accident. Whichever way they diverge is a defect this series already has
31
+ > a name for: the frame taller than its content is dead space, the frame shorter is the
32
+ > `UCM-CHART-9` overflow.
33
+ >
34
+ > **A card must have no size of its own.** Its height is its content's height, with `minHeight` as a
35
+ > floor so an empty or loading state does not collapse. That is what `UCM-CHART-9` established — it
36
+ > found the defect at a fixed `height`; `aspect` is the same defect derived from width instead of
37
+ > stated directly.
38
+ >
39
+ > | prop | applied today | verdict |
40
+ > |---|---|---|
41
+ > | `minHeight` | yes (`:138`) | **stays** — the floor, and the only legitimate one |
42
+ > | `aspect` | yes (`:139`) | **goes** — a second size source on a box that must follow its content |
43
+ > | `height` | no — only feeds a warning | **goes** — accepted and silently ignored |
44
+ >
45
+ > **This one has consumer impact**, unlike draft 2's claim: four `ChartFrame`s pass `aspect` —
46
+ > `fitness-monitor/BodyHistoryPage.jsx:328, :449` and `EnvironmentPage.jsx:250, :284`. They are
47
+ > migrated by `FM-CHART-1`, not here.
48
+
49
+ ## Part A — Envelope (authoritative WHAT/WHY)
50
+
51
+ ### Goal
52
+
53
+ `ChartFrame` has no size source of its own. Its height is its content's height, floored by
54
+ `minHeight`. `aspect` and `height` are gone.
55
+
56
+ ### Definition of Done
57
+
58
+ - [ ] **`aspect` removed** from the signature and from the applied `sx` (`:139`). A card that sizes
59
+ itself from its width can be shorter than its content (the `UCM-CHART-9` overflow) or taller
60
+ (dead space); both are defects this series already has names for.
61
+ - [ ] **`height` removed** from `ChartFrame`'s signature, and `warnOnChartFrameHeightMismatch` deleted
62
+ with it — the warning exists only to explain a prop that should not be accepted in the first
63
+ place. **A prop that is accepted and silently ignored is worse than one that errors**: it lets a
64
+ caller believe the card was sized when it was not, which is precisely the `UCM-CHART-9`
65
+ regression in slow motion.
66
+ - [ ] **`minHeight` stays, unchanged.** It is the one justified prop: a card with a loading or empty
67
+ state would otherwise collapse, and `UCM-CHART-9` established the floor as correct. Do not
68
+ touch its behaviour.
69
+ - [ ] **Passing `height` or `aspect` to `ChartFrame` is a dev-mode error naming what to do instead**
70
+ — `minHeight` for a floor, or `size` on the chart *inside* the frame if the caller meant the
71
+ chart's height. Consistent with how `UCM-CHART-12` treats the presets' removed props.
72
+ - [ ] **`docs/CHART-LAYOUT.md`'s "What stays unchanged" entry is made precise.** It lists the frame's
73
+ `minHeight`/`height`/`aspect` as retained without saying which are applied. Replace it: the frame
74
+ keeps **`minHeight` only**, as a floor; it has no size source of its own, and a chart's height
75
+ comes from its `size` token.
76
+
77
+ ### Non-goals
78
+
79
+ - Do not change `minHeight`'s semantics, and do not give `ChartFrame` a `size` token. The frame is
80
+ not a chart (`UCM-CHART-9`); its height follows its content with a floor, and that is correct.
81
+ - Do not touch the four presets. `UCM-CHART-12` settled them.
82
+ - No consumer migration here. The four `ChartFrame`s passing `aspect` are fitness-monitor's, and
83
+ `FM-CHART-1` removes them — **sequence this after `FM-CHART-1`, or land both together**, or those
84
+ four cards hit the new error.
85
+
86
+ ### Risks
87
+
88
+ - **`height` is free — no consumer passes it and it does nothing.** `aspect` is not: four cards
89
+ depend on it and lose their shape when it goes. That is intended (their height becomes their
90
+ content's), but it is a **visible change in fitness-monitor**, so `FM-CHART-1`'s rendered two-width
91
+ check is what confirms it, not this WO's unit tests.
92
+ - **Re-run the consumer scan by reading the call sites, not by grepping.** Both earlier drafts of this
93
+ WO were wrong about `aspect` — once from a grep that matched nothing, once from a `head`-truncated
94
+ grep that cut off the line where it is applied. Open the files.
95
+
96
+ ### Tests to WRITE — narrow
97
+
98
+ - `ChartFrame` with `minHeight`: floor applied, `UCM-CHART-9`'s existing assertions unchanged.
99
+ - `ChartFrame` given `height` or `aspect`: dev-mode error naming the replacement, and **no
100
+ `aspectRatio` or `height` on the box**.
101
+ - **A `ChartFrame` whose content is taller than `minHeight` grows to fit** — `UCM-CHART-9`'s
102
+ regression test, unchanged, and now the only thing that decides the card's height.
103
+ - The `UCM-CHART-9` regression test (content taller than the floor grows the box) stays green,
104
+ unchanged.
105
+
106
+ No full suite.
107
+
108
+ ---
109
+
110
+ ## Part B — Implementation map
111
+
112
+ > **TO BE FILLED BY THE ORCHESTRATOR** on `git pull`. Must carry `ChartFrame.jsx`'s signature
113
+ > (`:56-58`), the applied `sx` (`:138`), `warnOnChartFrameHeightMismatch` (`:27-35`), the
114
+ > `docs/CHART-LAYOUT.md` line to correct, the absolute working directory, the progress contract, and
115
+ > the preamble. **Do not dispatch while this placeholder stands.**
116
+
117
+ ---
118
+
119
+ ## Part C — Orchestrator-only
120
+
121
+ > **TO BE FILLED BY THE ORCHESTRATOR.** Review routing, register maintenance, registry publish
122
+ > verification, commit, and the execution directive with its self-address guard.