@micha.bigler/ui-core-micha 3.0.0 → 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
@@ -94,4 +94,5 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
94
94
  | UCM-CHART-9 | `ChartFrame` is a card, not a chart: it must never take a fixed height | REGRESSION in published 2.42.0, observed on hram staging. UCM-CHART-8 wired `resolveChartHeight()` through the four chart presets AND through `ChartFrame`. Correct for the presets, wrong for the frame: in the `minHeight`-alone branch the resolver returns `chartHeight = minHeight`, and `ChartFrame` applies it as `height` on its content box. A preset's box holds one chart; **`ChartFrame`'s box holds a whole card** -- title, toolbar, chart, legend, footnotes, export links. The floor became a fixed height, the content overflows, and ADJACENT CARDS VISIBLY OVERLAP (card title drawn over the chart above it, three sets of Export SVG/PNG stacked at different offsets). UCM-CHART-8's own Files note recorded that `ChartFrame` never destructures `height` -- the implementation then gave it one. | 2026-08-21 | done | 2026-08-22: `ChartFrame` fix + version bump + accurate mismatch warning | Tier 3, shared-core, live regression in `latest`. Full order: [`work-orders/UCM-CHART-9.md`](work-orders/UCM-CHART-9.md). Operator chose FIX-FORWARD over reverting hram's pin (2026-08-21), because 2.42.0 also appears to have stopped hram's Detailed-Results React #185 crash -- plausible mechanism is that deterministic sizing removed a resize-driven setState loop, but that is a HYPOTHESIS, not a finding, and this WO must re-check whether the crash returns (if it does, that is a finding for HRAM-RES-29, not a reason to undo this). Do NOT revert UCM-CHART-8: its preset behaviour is correct and includes the `aspect` branch four live call sites depend on; this WO narrows one consumer of the resolver only, and any edit needed to a preset test means the change has leaked. **Codex:** out of credits on the one allowed probe today (`.claude/codex-status.md`, 2026-08-22) -- Orchestrator implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` -- 2 findings (R1 version not bumped in-diff, fixed by bumping `package.json` to 2.42.1 in this commit so `publish.yml`'s version-increase gate fires on push; R2 jsdom-can't-measure-layout note on one test, non-blocking, covered by the live-browser check below). `ui_reviewer` -- 2 findings (U1 `warnOnHeightMismatch`'s message was left factually wrong for `ChartFrame` post-fix -- FIXED: added a `heightWins` param, `ChartFrame` now gets its own accurate wording, test coverage added in both `chartDefaults.test.js` and `ChartFrame.test.jsx`; U2 height-alone-on-ChartFrame is now a silent no-op with no dev warning -- accepted as-is, out of scope for this regression fix). Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`ChartFrame`, `chartDefaults`, all four presets) -- 129/129 green, preset assertions byte-identical/unchanged. **Rendered check:** this repo's own dev harness (`npm run dev`, `dev/entries.jsx` "Charts / ScatterChart (allocation shape)") driven live in the internal preview browser at 375px and 1280px -- DOM/computed-style inspection (no screenshot capture available in this session; declared substitution per the gate's fallback clause), confirmed the content box carries no fixed `height` and, after forcing the inner chart element's rendered height up to 900px/1100px to reproduce the regression scenario, the box grew to match with zero scrollHeight/clientHeight overflow at both widths. The Envelope's full five-card check (Sensitivity, Access, Access Ladder, Allocation, Optimization, rendered in hram itself) is **NOT done by this WO** -- this repo's harness only has 3 of the 5 named cards and no live hram instance was linked against this build. Carried forward as a mandatory precondition on whichever WO next bumps hram's `ui-core-micha` pin past 2.42.0. 2.42.0 stays flagged do-not-adopt; 2.42.1 publishes automatically via `publish.yml` on this push (version-increase gate). PROCESS NOTE, recorded deliberately: 2.42.0 shipped on 188 green unit tests that say nothing about layout -- the rendered check above is the layout-level evidence this WO exists to add, even though it stops short of the full hram five-card scope. FIVE-CARD CHECK DONE 2026-08-22, hram with real data (SG Sandbox CLI / `CLI-Kilombero_2026-per_cell-network`), ucm 2.42.1 loaded, at 1280 px AND 375 px: zero boxes with an inline fixed height, zero overflowing cards, zero overlapping card pairs, no horizontal body scroll, bottom dead space 17 px (card padding) on every card. CHART-9 does what it set out to do. SUBSTITUTION DECLARED: screenshot capture was unavailable all session, so this is DOM + computed-style measurement, not images. hram pin bumped to 2.42.1 in `6dd00c1d`. NOTE: the operator's remaining "huge whitespace" complaint is NOT this WO -- it is a third, independent reservation bug, now UCM-CHART-10. |
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
- | 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` | 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`). |
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). |
@@ -50,6 +50,7 @@ export function BarChart(_a) {
50
50
  xLabels,
51
51
  hideLegend,
52
52
  legendPosition,
53
+ slotProps,
53
54
  tickFontSize: theme.typography.caption.fontSize,
54
55
  spacing: theme.spacing,
55
56
  defaultLineHeight: theme.typography.caption.lineHeight,
@@ -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',
@@ -65,6 +65,7 @@ export function LineChart(_a) {
65
65
  xLabels,
66
66
  hideLegend,
67
67
  legendPosition,
68
+ slotProps,
68
69
  tickFontSize: theme.typography.caption.fontSize,
69
70
  spacing: theme.spacing,
70
71
  defaultLineHeight: theme.typography.caption.lineHeight,
@@ -161,6 +161,7 @@ export function ScatterChart(_a) {
161
161
  xLabels,
162
162
  hideLegend,
163
163
  legendPosition,
164
+ slotProps,
164
165
  tickFontSize: theme.typography.caption.fontSize,
165
166
  spacing: theme.spacing,
166
167
  defaultLineHeight: theme.typography.caption.lineHeight,
@@ -243,7 +243,16 @@ export function sizeYAxisForContent(yAxis, series, tickFontSize) {
243
243
  if (candidates.length === 0)
244
244
  return axis;
245
245
  const longestTick = longestFormattedTick(candidates, axis.valueFormatter);
246
- const tickTextWidth = Math.max(tickFontSizePx, longestTick.length * tickFontSizePx * AVERAGE_GLYPH_WIDTH_EM);
246
+ // UCM-CHART-12 Rule 2 (codex second-pass review finding): candidates existed, but a caller
247
+ // `valueFormatter` reduced every one of them to a blank string -- this is the y-axis analogue
248
+ // of `xAxisHasVisibleTicks`'s Access-scatter case, and must collapse the same way. Distinct
249
+ // from `candidates.length === 0` above (nothing to measure at all, MUI's own default applies,
250
+ // untouched): here there WAS content, it just renders as nothing. With no axis title either,
251
+ // the axis has genuinely nothing to show, so the whole band collapses to 0 -- not merely the
252
+ // text-width term floored, matching the x-axis band's own all-or-nothing zero.
253
+ if (longestTick.length === 0 && !axis.label)
254
+ return Object.assign(Object.assign({}, axis), { width: 0 });
255
+ const tickTextWidth = longestTick.length === 0 ? 0 : Math.max(tickFontSizePx, longestTick.length * tickFontSizePx * AVERAGE_GLYPH_WIDTH_EM);
247
256
  const labelThickness = axis.label
248
257
  ? Math.ceil(MUI_AXIS_LABEL_FONT_SIZE_PX * Y_AXIS_LABEL_THICKNESS_FACTOR) + Y_AXIS_LABEL_TICK_GAP
249
258
  : 0;
@@ -302,11 +311,16 @@ const LEGEND_BAND_PX = 32;
302
311
  // UCM-CHART-12 `xLabels: "auto"`: with no real container-width measurement available before
303
312
  // render (the presets stay responsive -- MUI measures actual width via ResizeObserver, which this
304
313
  // pure function cannot see), "auto" is a documented HEURISTIC, not a true fit computation: rotate
305
- // once there are enough categories AND the longest one is long enough that a horizontal row would
306
- // plausibly collide. The mandatory browser-level check (WO Part C) is what actually verifies fit
307
- // against real rendered geometry; this only picks a reasonable default ahead of that.
314
+ // once there are enough categories AND the longest one, MEASURED (same mechanism as the rotated
315
+ // projection below -- real DOM measurement when available, the per-glyph estimate otherwise), is
316
+ // wide enough that a horizontal row would plausibly collide. Codex second-pass review finding:
317
+ // the first cut of this heuristic used a raw character-count threshold instead of the measured
318
+ // width this function already has available -- replaced, since "decide from the measured labels"
319
+ // (Rule 3) is achievable even without a real container width to compare against. The count
320
+ // threshold stays as the OTHER half of the heuristic: a handful of wide labels can still fit
321
+ // spread across a wide chart, so width alone should not trigger rotation either.
308
322
  const AUTO_ROTATE_TICK_COUNT = 6;
309
- const AUTO_ROTATE_LABEL_CHARS = 4;
323
+ const AUTO_ROTATE_LABEL_WIDTH_PX = 50;
310
324
  function xAxisTickContentText(axis) {
311
325
  const data = axis === null || axis === void 0 ? void 0 : axis.data;
312
326
  if (!Array.isArray(data))
@@ -336,12 +350,22 @@ function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextW
336
350
  const fontSize = cssLengthToPixels((_a = axis === null || axis === void 0 ? void 0 : axis.tickLabelStyle) === null || _a === void 0 ? void 0 : _a.fontSize) || cssLengthToPixels(fallbackTickFontSize);
337
351
  const longestLabel = xAxisTickContentText(axis) || '';
338
352
  const dataCount = Array.isArray(axis === null || axis === void 0 ? void 0 : axis.data) ? axis.data.length : 0;
353
+ const measure = measureTextWidth || measureTickTextWidthPx;
354
+ // Measured once, up front -- both "auto"'s rotation DECISION and the rotated-band PROJECTION
355
+ // below need the label's real width, and must agree on what "real" means (same measurement,
356
+ // same estimate fallback) or the two could disagree about the same label.
357
+ const labelWidthPx = fontSize > 0
358
+ ? Math.max(fontSize, (() => {
359
+ const measured = measure(longestLabel, fontSize);
360
+ return Number.isFinite(measured) && measured > 0 ? measured : estimateTextWidthPx(longestLabel, fontSize);
361
+ })())
362
+ : 0;
339
363
  let angle = 0;
340
364
  if (xLabelsMode === 'angled') {
341
365
  angle = Number((_b = axis === null || axis === void 0 ? void 0 : axis.tickLabelStyle) === null || _b === void 0 ? void 0 : _b.angle) || -45;
342
366
  }
343
367
  else if (xLabelsMode === 'auto') {
344
- const needsRotation = dataCount > AUTO_ROTATE_TICK_COUNT && longestLabel.length > AUTO_ROTATE_LABEL_CHARS;
368
+ const needsRotation = dataCount > AUTO_ROTATE_TICK_COUNT && labelWidthPx > AUTO_ROTATE_LABEL_WIDTH_PX;
345
369
  angle = needsRotation ? -45 : 0;
346
370
  } // "horizontal" always stays at angle 0
347
371
  if (angle === 0)
@@ -352,11 +376,7 @@ function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextW
352
376
  if (fontSize === 0)
353
377
  return { tickBand: TICK_BAND_BASE_PX, angle };
354
378
  const lineHeight = Number((_c = axis === null || axis === void 0 ? void 0 : axis.tickLabelStyle) === null || _c === void 0 ? void 0 : _c.lineHeight) || defaultLineHeight;
355
- const measure = measureTextWidth || measureTickTextWidthPx;
356
- const measuredWidth = measure(longestLabel, fontSize);
357
- const textWidth = Math.max(fontSize, Number.isFinite(measuredWidth) && measuredWidth > 0
358
- ? measuredWidth
359
- : estimateTextWidthPx(longestLabel, fontSize));
379
+ const textWidth = labelWidthPx;
360
380
  const radians = (angle * Math.PI) / 180;
361
381
  const projectedHeight = Math.abs(Math.cos(radians)) * fontSize * lineHeight
362
382
  + Math.abs(Math.sin(radians)) * textWidth;
@@ -404,49 +424,68 @@ function resolveChartHeightPx(size, height, spacing) {
404
424
  * `containerWidth` is optional and only meaningful for tests / the browser-level check -- the
405
425
  * presets themselves stay responsive (no explicit width) and never pass it.
406
426
  */
407
- export function resolveChartLayout({ size = 'standard', height, xAxis, yAxis, series = [], xLabels = 'auto', hideLegend = false, legendPosition = DEFAULT_LEGEND_POSITION, tickFontSize, spacing, defaultLineHeight = 1, measureTextWidth, containerWidth, } = {}) {
427
+ export function resolveChartLayout({ size = 'standard', height, xAxis, yAxis, series = [], xLabels = 'auto', hideLegend = false, legendPosition = DEFAULT_LEGEND_POSITION, slotProps, tickFontSize, spacing, defaultLineHeight = 1, measureTextWidth, containerWidth, } = {}) {
408
428
  var _a, _b, _c;
409
429
  const chartHeight = resolveChartHeightPx(size, height, spacing);
410
430
  const xAxisList = (xAxis === null || xAxis === void 0 ? void 0 : xAxis.length) ? xAxis : [{}];
411
431
  const primaryXAxis = xAxisList[0];
412
432
  const { tickBand: xAxisBand, angle } = resolveXAxisGeometry(primaryXAxis, xLabels, defaultLineHeight, measureTextWidth, tickFontSize);
413
433
  const xTitleBand = (primaryXAxis === null || primaryXAxis === void 0 ? void 0 : primaryXAxis.label) ? AXIS_TITLE_BAND_PX : 0;
434
+ // Codex second-pass review finding: a caller-set `slotProps.legend.position` overrides
435
+ // `legendPosition` at RENDER time (`withChartSlotDefaults`) but was invisible here, so the
436
+ // margin/height this function reserved could disagree with where the legend actually rendered.
437
+ // `resolveLegendPosition` is the one merge both call sites now share.
438
+ const effectiveLegendPosition = resolveLegendPosition(legendPosition, slotProps);
414
439
  // A `vertical: 'middle'` legend renders on the SIDE (consuming width, not height) -- reviewer
415
440
  // finding: only branching on 'top' vs. not-'top' always reserved the legend band vertically,
416
441
  // over-reserving height and under-reserving width for a side-placed legend. This WO's width
417
442
  // invariant does not itemize a legend-width band, so a side legend simply gets none of the
418
443
  // height reservation here rather than a wrong one -- consistent with the WO's own text ("0 when
419
444
  // legend is off or horizontal").
420
- const legendVertical = (_a = legendPosition === null || legendPosition === void 0 ? void 0 : legendPosition.vertical) !== null && _a !== void 0 ? _a : DEFAULT_LEGEND_POSITION.vertical;
445
+ const legendVertical = effectiveLegendPosition.vertical;
421
446
  const legendIsSidePlaced = legendVertical === 'middle';
422
447
  const legendHeightBand = hideLegend || legendIsSidePlaced ? 0 : LEGEND_BAND_PX;
423
448
  const legendOnTop = legendHeightBand > 0 && legendVertical === 'top';
424
449
  const sizedYAxis = sizeYAxisForContent((yAxis === null || yAxis === void 0 ? void 0 : yAxis.length) ? yAxis : [{}], series, tickFontSize);
425
- const yAxisBand = (_c = (_b = sizedYAxis[0]) === null || _b === void 0 ? void 0 : _b.width) !== null && _c !== void 0 ? _c : 0;
450
+ // Codex second-pass review finding: a dual-axis chart (TimeSeriesChart's `axis: 'secondary'`
451
+ // feature, CHART-5) has a SECOND y-axis on the right, consuming its own width -- the resolver
452
+ // only ever accounted for `sizedYAxis[0]`. `position` defaults to `'left'` for an unmarked axis
453
+ // (matching MUI's own default-assignment rule, same as `sizeYAxisForContent`'s own series-match
454
+ // logic above), so only an explicit `'right'` counts as the secondary axis.
455
+ const primaryYAxis = (_a = sizedYAxis.find((axisEntry) => axisEntry.position !== 'right')) !== null && _a !== void 0 ? _a : sizedYAxis[0];
456
+ const secondaryYAxisEntry = sizedYAxis.find((axisEntry) => axisEntry.position === 'right');
457
+ const yAxisBand = (_b = primaryYAxis === null || primaryYAxis === void 0 ? void 0 : primaryYAxis.width) !== null && _b !== void 0 ? _b : 0;
458
+ const secondaryYAxisBand = (_c = secondaryYAxisEntry === null || secondaryYAxisEntry === void 0 ? void 0 : secondaryYAxisEntry.width) !== null && _c !== void 0 ? _c : 0;
426
459
  const rightPad = PACKAGE_DEFAULT_MARGIN.right;
427
460
  const resolvedXAxis = xAxisList.map((axisEntry, index) => {
428
- if (index !== 0 || axisEntry.height != null)
461
+ var _a;
462
+ if (index !== 0)
429
463
  return axisEntry;
430
464
  // `xLabels` is the SOLE source of truth for rotation (Rule 3) -- `angle` is always written
431
- // explicitly here, even when 0, rather than only overriding a truthy value. Leaving a stale
432
- // caller-supplied `tickLabelStyle.angle` in place when `xLabels` resolved to no rotation would
433
- // let MUI still render a rotated label whose band this resolver reserved as if it were flat --
434
- // exactly the space/render mismatch this WO exists to close, just inverted (under-reservation
435
- // instead of CHART-10's over-reservation).
436
- const _a = axisEntry.tickLabelStyle || {}, { angle: _callerAngle } = _a, restTickLabelStyle = __rest(_a, ["angle"]);
437
- return Object.assign(Object.assign({}, axisEntry), { height: xAxisBand + xTitleBand, tickLabelStyle: Object.assign(Object.assign({}, restTickLabelStyle), { angle }) });
465
+ // explicitly here, even when 0, rather than only overriding a truthy value, and regardless of
466
+ // whether the caller also set an explicit `height` (own `height` bypasses the model's SIZE,
467
+ // never its ROTATION -- codex second-pass review finding: the two were previously coupled,
468
+ // so a caller-set `height` could silently keep a stale rotated `tickLabelStyle.angle` even
469
+ // under `xLabels="horizontal"`). Leaving a stale caller-supplied angle in place when `xLabels`
470
+ // resolved to no rotation would let MUI still render a rotated label whose band this resolver
471
+ // reserved as if it were flat -- exactly the space/render mismatch this WO exists to close,
472
+ // just inverted (under-reservation instead of CHART-10's over-reservation).
473
+ const _b = axisEntry.tickLabelStyle || {}, { angle: _callerAngle } = _b, restTickLabelStyle = __rest(_b, ["angle"]);
474
+ return Object.assign(Object.assign({}, axisEntry), { height: (_a = axisEntry.height) !== null && _a !== void 0 ? _a : (xAxisBand + xTitleBand), tickLabelStyle: Object.assign(Object.assign({}, restTickLabelStyle), { angle }) });
438
475
  });
439
476
  const margin = {
440
477
  top: PACKAGE_DEFAULT_MARGIN.top + (legendOnTop ? legendHeightBand : 0),
441
478
  bottom: PACKAGE_DEFAULT_MARGIN.bottom + (legendOnTop ? 0 : legendHeightBand),
442
479
  left: PACKAGE_DEFAULT_MARGIN.left,
443
- right: rightPad,
480
+ right: rightPad + secondaryYAxisBand,
444
481
  };
445
482
  // `plotHeight`/`plotWidth`: the residual of `chartHeight`/`containerWidth` minus the three named
446
483
  // furniture bands (see this function's own docblock for why this alone does not prove the
447
484
  // absence of a double-counted term, and what does).
448
485
  const plotHeight = chartHeight - xAxisBand - xTitleBand - legendHeightBand;
449
- const plotWidth = containerWidth != null ? containerWidth - yAxisBand - rightPad : undefined;
486
+ const plotWidth = containerWidth != null
487
+ ? containerWidth - yAxisBand - secondaryYAxisBand - rightPad
488
+ : undefined;
450
489
  return {
451
490
  chartHeight,
452
491
  sx: { width: '100%', height: chartHeight },
@@ -455,6 +494,7 @@ export function resolveChartLayout({ size = 'standard', height, xAxis, yAxis, se
455
494
  yAxis: sizedYAxis,
456
495
  bands: Object.assign({ plotHeight,
457
496
  xAxisBand,
497
+ secondaryYAxisBand,
458
498
  xTitleBand, legendBand: legendHeightBand, yAxisBand,
459
499
  rightPad }, (containerWidth != null ? { plotWidth, containerWidth } : {})),
460
500
  };
@@ -466,15 +506,34 @@ export function resolveChartLayout({ size = 'standard', height, xAxis, yAxis, se
466
506
  * migration path instead of a layout that quietly stops matching what the prop says) -- gated on
467
507
  * `NODE_ENV !== 'production'` like the deleted `warnOnHeightMismatch` was, so a shared package
468
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.
469
515
  */
470
- export function assertRemovedChartProp(componentName, propName, value, replacement) {
516
+ export function assertRemovedChartProp(componentName, propName, value, replacement, removedIn = 'UCM-CHART-12, v3.0.0') {
471
517
  if (value === undefined || process.env.NODE_ENV === 'production')
472
518
  return;
473
- throw new Error(`[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in UCM-CHART-12, `
474
- + `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}`);
475
521
  }
476
- export function withChartSlotDefaults(slotProps, legendPosition) {
522
+ /**
523
+ * UCM-CHART-12 (codex second-pass review finding): the single source of truth for where the
524
+ * legend actually renders -- `legendPosition` merged with a `slotProps.legend.position` override,
525
+ * same precedence `withChartSlotDefaults` already applied. Before this fix, `resolveChartLayout`
526
+ * only ever looked at `legendPosition` when deciding how much space to reserve and on which side,
527
+ * while `withChartSlotDefaults` (below) separately let `slotProps.legend.position` win at RENDER
528
+ * time -- a caller passing both could get a legend rendered top/side while the resolver had
529
+ * reserved space for bottom (or none), the exact reserved-vs-rendered mismatch this WO exists to
530
+ * close. Both call sites now merge through this one function.
531
+ */
532
+ export function resolveLegendPosition(legendPosition, slotProps) {
477
533
  var _a;
478
- const position = Object.assign(Object.assign(Object.assign({}, DEFAULT_LEGEND_POSITION), legendPosition), (_a = slotProps === null || slotProps === void 0 ? void 0 : slotProps.legend) === null || _a === void 0 ? void 0 : _a.position);
534
+ return Object.assign(Object.assign(Object.assign({}, DEFAULT_LEGEND_POSITION), legendPosition), (_a = slotProps === null || slotProps === void 0 ? void 0 : slotProps.legend) === null || _a === void 0 ? void 0 : _a.position);
535
+ }
536
+ export function withChartSlotDefaults(slotProps, legendPosition) {
537
+ const position = resolveLegendPosition(legendPosition, slotProps);
479
538
  return Object.assign(Object.assign({}, slotProps), { tooltip: Object.assign({ trigger: 'axis' }, slotProps === null || slotProps === void 0 ? void 0 : slotProps.tooltip), legend: Object.assign(Object.assign({}, slotProps === null || slotProps === void 0 ? void 0 : slotProps.legend), { position }) });
480
539
  }
@@ -25,11 +25,20 @@ chartHeight === plotHeight + xAxisBand + xTitleBand + legendBand
25
25
  - `xTitleBand` — the axis title. Zero unless the x-axis carries a `label`.
26
26
  - `legendBand` — one row for the legend. Zero when `hideLegend` is set.
27
27
  - `plotHeight` is the residual: `chartHeight` minus the three bands above.
28
-
29
- Width follows the same shape (`yAxisBand + plotWidth + rightPad`), but the presets stay
30
- responsive — they never fix a pixel width — so `plotWidth`/`containerWidth` are only meaningful
31
- when a caller explicitly supplies `containerWidth` (tests, or the browser-level check); the
32
- presets themselves never pass it.
28
+ - The y-axis band collapses to zero the same way the x-axis one does: candidates existed (the
29
+ axis has real `min`/`max` or plotted data) but a caller `valueFormatter` blanks every formatted
30
+ tick, and there is no axis title either — the axis has nothing to show, so it reserves nothing.
31
+ Distinct from "nothing to measure at all" (no `min`/`max`/data), where MUI's own default applies
32
+ untouched.
33
+
34
+ Width follows the same shape (`yAxisBand + plotWidth + secondaryYAxisBand + rightPad`), but the
35
+ presets stay responsive — they never fix a pixel width — so `plotWidth`/`containerWidth` are only
36
+ meaningful when a caller explicitly supplies `containerWidth` (tests, or the browser-level check);
37
+ the presets themselves never pass it. A **secondary (right-positioned) y-axis** — `TimeSeriesChart`'s
38
+ `axis: 'secondary'` feature, or any preset given a two-entry `yAxis` array with one `position:
39
+ 'right'` — reserves its own width on the right, added to `margin.right` alongside the fixed
40
+ breathing-room pad; an axis with no explicit `position` defaults to the left, matching MUI's own
41
+ default-assignment rule.
33
42
 
34
43
  ## Removed props, and their replacement
35
44
 
@@ -90,11 +99,13 @@ on the wrapper), only the height stops tracking width.
90
99
 
91
100
  `"auto"` (the default) is a **heuristic**, not a true fit computation — the presets stay
92
101
  responsive, so the resolver has no real container width to measure against before render. It
93
- rotates when there are more than 6 categories *and* the longest formatted label is longer than 4
94
- characters; otherwise it stays horizontal. `"horizontal"` always stays flat; `"angled"` always
95
- rotates (at the caller's own `tickLabelStyle.angle`, or -45° if unset). Verify a specific chart's
96
- real rendered fit with the browser-level check described below — a unit test cannot see real
97
- container width or real font metrics either.
102
+ rotates when there are more than 6 categories *and* the longest label's real MEASURED width
103
+ (same DOM measurement the rotated-band projection itself uses — a real browser measurement when
104
+ available, a per-glyph estimate otherwise) exceeds 50px; otherwise it stays horizontal.
105
+ `"horizontal"` always stays flat; `"angled"` always rotates (at the caller's own
106
+ `tickLabelStyle.angle`, or -45° if unset). "Measured labels" (Rule 3) is therefore real — what
107
+ remains unmeasured is "available width", since no container width exists before render. Verify a
108
+ specific chart's real rendered fit with the browser-level check described below.
98
109
 
99
110
  ## `margin` — no replacement
100
111
 
@@ -114,9 +125,13 @@ the right amount.
114
125
 
115
126
  ## What stays unchanged
116
127
 
117
- - `ChartFrame`'s own `minHeight`/`height`/`aspect` — it is a whole-card floor (title, toolbar,
118
- chart, legend, footnotes, export links), not a single chart's size, and was never in scope here
119
- (`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.
120
135
  - Palettes, series colours, tooltips, legend content, interaction.
121
136
  - `sizeYAxisForContent` (y-axis width from tick content) — reused unchanged inside the resolver.
122
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "sideEffects": [
@@ -76,6 +76,7 @@ export function BarChart({
76
76
  xLabels,
77
77
  hideLegend,
78
78
  legendPosition,
79
+ slotProps,
79
80
  tickFontSize: theme.typography.caption.fontSize,
80
81
  spacing: theme.spacing,
81
82
  defaultLineHeight: theme.typography.caption.lineHeight,