@micha.bigler/ui-core-micha 3.1.2 → 3.2.1

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.
@@ -153,6 +153,29 @@ export const PACKAGE_DEFAULT_MARGIN = {
153
153
  // as clipped even when nothing is actually cut off).
154
154
  const ROTATED_TICK_CLEARANCE_PX = 8;
155
155
 
156
+ // UCM-CHART-16: the FLAT (angle 0) tick band used to be the unconditional TICK_BAND_BASE_PX
157
+ // regardless of the actual tick font size -- fine for MUI's own generic default, but MUI's
158
+ // internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks EVERY tick label the
159
+ // moment the real rendered text is even slightly taller than what that flat floor leaves room
160
+ // for once MUI's own tick/gap overhead (and, when an axis title shares the band, its own gap) is
161
+ // subtracted. `TICK_BAND_BASE_PX` alone was a near-miss with essentially no margin at any font
162
+ // size above whatever MUI itself implicitly assumed -- two hram consumers independently patched
163
+ // around it with the same empirically-measured `height: 56`, and a third, titleless case broke at
164
+ // a tick font just 1px above the untitled default.
165
+ //
166
+ // These three constants are DERIVED, not guessed: binary-searched against the real, unmocked
167
+ // `BarChart` rendered in this repo's own dev harness (`dev/entries.jsx`'s `chart-probe` entry,
168
+ // Chrome, this package's own DM Sans default -- the taller of the two fonts checked; MUI's Roboto
169
+ // fallback needs visibly less and was never the trigger) at tick font sizes 11-18px, with and
170
+ // without an axis title:
171
+ // required flat tickBand ~= ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
172
+ // required tickBand+title ~= that, plus TICK_BAND_TITLE_EXTRA_PX, plus AXIS_TITLE_BAND_PX
173
+ // matched every measured threshold exactly or over by at most 1px, never under, across ten
174
+ // font-size/title combinations -- see UCM-CHART-16's work order for the full measurement table.
175
+ const TICK_TEXT_HEIGHT_FACTOR = 1.3;
176
+ const TICK_BAND_MUI_OVERHEAD_PX = 9;
177
+ const TICK_BAND_TITLE_EXTRA_PX = 2;
178
+
156
179
  // THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
157
180
  // 20px (label, if present) = 65px regardless of what the ticks actually say
158
181
  // -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
@@ -291,13 +314,18 @@ export function defaultNumericTickFormatter(locale) {
291
314
  : new Intl.NumberFormat(locale).format(value));
292
315
  }
293
316
 
294
- // UCM-CHART-12: named size tokens, resolved through the theme's 8px spacing unit
295
- // (`src/theme/tokens.js`'s `spacing: 8`). `standard` is pinned to the pre-existing deployed
296
- // default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this migration does not also
297
- // silently redraw every already-shipped default-sized chart; `compact`/`tall` are proportional.
298
- // Documented plainly in `docs/CHART-LAYOUT.md` -- these are visible product decisions, not
299
- // internal plumbing.
300
- export const CHART_SIZE_SPACING_UNITS = { compact: 30, standard: 40, tall: 50 };
317
+ // UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
318
+ // (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
319
+ // the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
320
+ // migration did not also silently redraw every already-shipped default-sized chart -- that pin
321
+ // was a migration guardrail, not a design verdict. The migration it protected is complete, and
322
+ // the operator judged the whole scale too cramped (`tall` had already forced hram to use the
323
+ // `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
324
+ // adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
325
+ // `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
326
+ export const CHART_SIZE_SPACING_UNITS = {
327
+ compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
328
+ };
301
329
 
302
330
  // UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
303
331
  // constant taken on faith. These two ARE constants, but only for the "content is present" case;
@@ -384,7 +412,18 @@ function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextW
384
412
  angle = needsRotation ? -45 : 0;
385
413
  } // "horizontal" always stays at angle 0
386
414
 
387
- if (angle === 0) return { tickBand: TICK_BAND_BASE_PX, angle: 0 };
415
+ if (angle === 0) {
416
+ // UCM-CHART-16: derive the flat band from the actual tick font size instead of the
417
+ // unconditional floor -- `axis?.label` is the SAME signal `resolveChartLayout` uses to decide
418
+ // whether an axis title also shares this band (`xTitleBand`), so a titled axis reserves the
419
+ // extra room its own gap costs, precisely where the near-miss was.
420
+ const hasTitle = Boolean(axis?.label);
421
+ const flatTickBand = fontSize > 0
422
+ ? Math.ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
423
+ + (hasTitle ? TICK_BAND_TITLE_EXTRA_PX : 0)
424
+ : TICK_BAND_BASE_PX;
425
+ return { tickBand: Math.max(TICK_BAND_BASE_PX, flatTickBand), angle: 0 };
426
+ }
388
427
  // Rotation is a visual instruction independent of whether a font size is knowable here -- an
389
428
  // "angled" axis still rotates even when `fontSize` can't be resolved (no theme fallback given),
390
429
  // it just can't project a tighter band than the floor without one.
@@ -404,11 +443,11 @@ function resolveChartHeightPx(size, height, spacing) {
404
443
  const units = CHART_SIZE_SPACING_UNITS[size];
405
444
  if (units == null) {
406
445
  throw new Error(
407
- `[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall", `
408
- + 'or pass height (in px) for the documented escape.',
446
+ `[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
447
+ + '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.',
409
448
  );
410
449
  }
411
- // MUI's `theme.spacing(n)` returns a CSS length STRING ("240px"), not a number -- the chart
450
+ // MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
412
451
  // height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
413
452
  // back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
414
453
  return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
@@ -31,10 +31,10 @@ describe('BarChart preset', () => {
31
31
  legend: { position: { vertical: 'bottom', horizontal: 'start' } },
32
32
  });
33
33
  expect(props.hideLegend).toBe(true);
34
- // UCM-CHART-12: size defaults to "standard" (320px, the historical default) -- the wrapper
35
- // always carries an explicit height now, width stays responsive.
36
- expect(props.height).toBe(320);
37
- expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('320px');
34
+ // UCM-CHART-12/UCM-CHART-15: size defaults to "standard" (400px) -- the wrapper always
35
+ // carries an explicit height now, width stays responsive.
36
+ expect(props.height).toBe(400);
37
+ expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('400px');
38
38
  });
39
39
 
40
40
  it.each([
@@ -78,11 +78,13 @@ describe('BarChart preset', () => {
78
78
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
79
79
  // deleted `minHeight`/`aspect` CHART-8 trio -- see the removed-prop assertions below for the
80
80
  // migration side of this.
81
- describe('size/height resolution (UCM-CHART-12)', () => {
81
+ describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
82
82
  it.each([
83
- ['compact', 240],
84
- ['standard', 320],
85
- ['tall', 400],
83
+ ['compact', 320],
84
+ ['standard', 400],
85
+ ['tall', 480],
86
+ ['extra_tall', 560],
87
+ ['super_tall', 640],
86
88
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
87
89
  renderChart({ series: [{ data: [1] }], size });
88
90
  expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -36,7 +36,7 @@ describe('LineChart preset', () => {
36
36
 
37
37
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
38
38
  // deleted `minHeight`/`aspect` CHART-8 trio.
39
- describe('size/height resolution (UCM-CHART-12)', () => {
39
+ describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
40
40
  function renderLine(props) {
41
41
  return render(
42
42
  <ThemeProvider theme={createTheme()}>
@@ -48,9 +48,11 @@ describe('LineChart preset', () => {
48
48
  afterEach(cleanup);
49
49
 
50
50
  it.each([
51
- ['compact', 240],
52
- ['standard', 320],
53
- ['tall', 400],
51
+ ['compact', 320],
52
+ ['standard', 400],
53
+ ['tall', 480],
54
+ ['extra_tall', 560],
55
+ ['super_tall', 640],
54
56
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
55
57
  renderLine({ size });
56
58
  expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -235,13 +235,15 @@ describe('ScatterReferenceCurve / ScatterReferenceLine (THEME-10)', () => {
235
235
 
236
236
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
237
237
  // deleted `minHeight`/`aspect` CHART-8 trio.
238
- describe('ScatterChart size/height resolution (UCM-CHART-12)', () => {
238
+ describe('ScatterChart size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
239
239
  afterEach(() => { cleanup(); vi.clearAllMocks(); });
240
240
 
241
241
  it.each([
242
- ['compact', 240],
243
- ['standard', 320],
244
- ['tall', 400],
242
+ ['compact', 320],
243
+ ['standard', 400],
244
+ ['tall', 480],
245
+ ['extra_tall', 560],
246
+ ['super_tall', 640],
245
247
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
246
248
  renderScatter({ size });
247
249
  expect(scatterSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -0,0 +1,95 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import {
4
+ afterAll, afterEach, beforeAll, describe, expect, it,
5
+ } from 'vitest';
6
+ import { cleanup, render } from '@testing-library/react';
7
+ import { ThemeProvider, createTheme } from '@mui/material/styles';
8
+
9
+ import { BarChart } from '../src/components/charts/BarChart';
10
+
11
+ // UCM-CHART-16: MUI's own tick-label fit check (`shortenLabels`, weighed against
12
+ // `tickLabelsMaxHeight`) depends on a REAL SVG text measurement (`getBBox`), which jsdom does not
13
+ // implement -- `SVGElement.prototype.getBBox` is not a function here, so MUI's own
14
+ // `getStringSize` falls back to `getBoundingClientRect`, which jsdom always reports as 0x0. Every
15
+ // measured height therefore comes back 0, MUI's fit check trivially "fits" any text, and the real
16
+ // defect (every tick label blanked) can never be observed by rendering the unmocked chart as-is --
17
+ // a test that skips this setup would pass whether or not the resolver's band is correct, i.e. be
18
+ // exactly the "mocked prop-assertion" trap this package's own tests warn against elsewhere.
19
+ //
20
+ // This installs a deterministic `getBBox` instead, so the SAME code MUI itself runs is actually
21
+ // exercised: height scaled off the measurement span's own `fontSize`, at a ratio (1.28) chosen to
22
+ // sit inside the real near-miss window this WO's own work order measured against a real browser
23
+ // (`resolveXAxisGeometry`'s `TICK_TEXT_HEIGHT_FACTOR` comment) -- large enough that the pre-fix
24
+ // unconditional floor already fails a titled axis at the theme's own default tick font, small
25
+ // enough that the untitled default (never broken) still passes. Width is deliberately near-zero so
26
+ // only the HEIGHT dimension of MUI's fit check is exercised; each chart also sets an explicit
27
+ // `width` so MUI's real responsive sizing (ResizeObserver, unavailable in jsdom) never enters into
28
+ // it.
29
+ function installHeightOnlyTextMeasurement() {
30
+ const original = SVGElement.prototype.getBBox;
31
+ // eslint-disable-next-line func-names
32
+ SVGElement.prototype.getBBox = function getBBox() {
33
+ const fontSize = Number.parseFloat(this.style.fontSize) || 12;
34
+ return { x: 0, y: 0, width: 1, height: fontSize * 1.28 };
35
+ };
36
+ return () => {
37
+ SVGElement.prototype.getBBox = original;
38
+ };
39
+ }
40
+
41
+ describe('x-axis tick band leaves MUI room for one real line of tick text (UCM-CHART-16)', () => {
42
+ let restoreGetBBox;
43
+
44
+ beforeAll(() => {
45
+ restoreGetBBox = installHeightOnlyTextMeasurement();
46
+ });
47
+
48
+ afterAll(() => {
49
+ restoreGetBBox();
50
+ });
51
+
52
+ afterEach(cleanup);
53
+
54
+ // WO scope item 1's own signature: an axis title AND horizontal tick labels sharing the same
55
+ // band, at the theme's own default tick font -- no explicit `height`/`tickLabelStyle` overrides
56
+ // anywhere, so this exercises the resolver's own default reservation end to end, the exact path
57
+ // two hram consumers had to work around by hand.
58
+ it('renders non-empty tick labels when the axis has both a title and horizontal ticks', () => {
59
+ const { container } = render(
60
+ <ThemeProvider theme={createTheme()}>
61
+ <BarChart
62
+ series={[{ data: [24, 36, 28], label: 'Opened' }]}
63
+ xAxis={[{ data: ['A', 'B', 'C'] }]}
64
+ xAxisLabel="Category"
65
+ width={600}
66
+ />
67
+ </ThemeProvider>,
68
+ );
69
+ const tickTexts = Array.from(container.querySelectorAll('.MuiChartsAxis-bottom .MuiChartsAxis-tickLabel'))
70
+ .map((node) => node.textContent);
71
+ expect(tickTexts).toHaveLength(3);
72
+ expect(tickTexts.every((text) => text.length > 0)).toBe(true);
73
+ });
74
+
75
+ // Non-goal guardrail: the rotated path projects its own band from real measured/estimated text
76
+ // width and must stay exactly as before -- this WO only touches the flat (angle 0) case.
77
+ it('leaves the rotated tick path unaffected', () => {
78
+ const { container } = render(
79
+ <ThemeProvider theme={createTheme()}>
80
+ <BarChart
81
+ series={[{ data: [24, 36, 28, 12, 9, 7, 15] }]}
82
+ xAxis={[{
83
+ data: Array.from({ length: 7 }, (_, i) => `A rather long category label ${i}`),
84
+ }]}
85
+ xLabels="angled"
86
+ width={600}
87
+ />
88
+ </ThemeProvider>,
89
+ );
90
+ const tickTexts = Array.from(container.querySelectorAll('.MuiChartsAxis-bottom .MuiChartsAxis-tickLabel'))
91
+ .map((node) => node.textContent);
92
+ expect(tickTexts).toHaveLength(7);
93
+ expect(tickTexts.every((text) => text.length > 0)).toBe(true);
94
+ });
95
+ });
@@ -115,8 +115,8 @@ describe('chart chrome defaults', () => {
115
115
  expect(timeSeriesProps.grid).toEqual({ horizontal: true });
116
116
  expect(timeSeriesProps.skipAnimation).toBe(true);
117
117
  // UCM-CHART-12: TimeSeriesChart's inner BarChart now takes size="standard" instead of a
118
- // fixed height prop -- the resolved height must still land on the historical 320px default.
119
- expect(timeSeriesProps.height).toBe(320);
118
+ // fixed height prop. UCM-CHART-15 shifted "standard" from 320px to 400px.
119
+ expect(timeSeriesProps.height).toBe(400);
120
120
  });
121
121
 
122
122
  it('keeps caller tick font, grid, showMark, and legendPosition values instead of the defaults', () => {
@@ -551,15 +551,22 @@ describe('resolveChartLayout composition invariant (UCM-CHART-12)', () => {
551
551
  });
552
552
  });
553
553
 
554
- describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () => {
554
+ describe('resolveChartLayout size tokens and height escape (UCM-CHART-12, UCM-CHART-15)', () => {
555
555
  it.each(Object.entries(CHART_SIZE_SPACING_UNITS))('resolves size="%s" through the theme spacing scale', (size, units) => {
556
556
  const layout = resolveChartLayout({ size, spacing: theme.spacing });
557
557
  expect(layout.chartHeight).toBe(units * 8);
558
558
  });
559
559
 
560
- it('pins "standard" to the historical 320px default', () => {
561
- const layout = resolveChartLayout({ size: 'standard', spacing: theme.spacing });
562
- expect(layout.chartHeight).toBe(320);
560
+ // UCM-CHART-15: hardcoded independently of CHART_SIZE_SPACING_UNITS -- the test above computes
561
+ // its expectation from the same source under test and would pass unchanged against either the
562
+ // old or the new scale. This one is written to fail against the pre-CHART-15 values (240/320/400).
563
+ it('resolves the five size tokens to their documented pixel heights', () => {
564
+ const px = Object.fromEntries(['compact', 'standard', 'tall', 'extra_tall', 'super_tall'].map(
565
+ (size) => [size, resolveChartLayout({ size, spacing: theme.spacing }).chartHeight],
566
+ ));
567
+ expect(px).toEqual({
568
+ compact: 320, standard: 400, tall: 480, extra_tall: 560, super_tall: 640,
569
+ });
563
570
  });
564
571
 
565
572
  it('lets height override the size token entirely', () => {
@@ -567,13 +574,63 @@ describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () =
567
574
  expect(layout.chartHeight).toBe(111);
568
575
  });
569
576
 
570
- it('throws on an unknown size token', () => {
577
+ // UCM-CHART-15: the height escape still wins for a token added in this WO, not only a
578
+ // pre-existing one.
579
+ it('lets height override a token that has no call site yet', () => {
580
+ const layout = resolveChartLayout({ size: 'super_tall', height: 111, spacing: theme.spacing });
581
+ expect(layout.chartHeight).toBe(111);
582
+ });
583
+
584
+ it('throws on an unknown size token, naming all five valid tokens', () => {
571
585
  expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(/Unknown chart size/);
586
+ expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(
587
+ /"compact" \| "standard" \| "tall" \| "extra_tall" \| "super_tall"/,
588
+ );
572
589
  });
573
590
 
574
591
  it('falls back to a numeric 8px unit when no spacing function is given', () => {
575
592
  const layout = resolveChartLayout({ size: 'standard' });
576
- expect(layout.chartHeight).toBe(320);
593
+ expect(layout.chartHeight).toBe(400);
594
+ });
595
+ });
596
+
597
+ describe('resolveChartLayout flat x-axis tick band leaves MUI room to render (UCM-CHART-16)', () => {
598
+ // Third occurrence (WO Part A): no title, just a tick font 1px above the untitled default --
599
+ // already enough to blank every label under the pre-fix, unconditional TICK_BAND_BASE_PX=25.
600
+ // 26 is not invented: binary-searched against the real, unmocked `BarChart` rendered in this
601
+ // repo's own dev harness (Chrome, this package's own DM Sans default) -- 25 still blanked every
602
+ // tick, 26 was the smallest axis height that didn't. A test computing its own expectation from
603
+ // `CHART_TICK_TEXT_HEIGHT_FACTOR`-style constants would pass whatever those constants say,
604
+ // proving nothing; this number came from outside the code under test.
605
+ it('reserves enough room for a tick font one pixel above the untitled default (measured threshold: 26px)', () => {
606
+ const layout = resolveChartLayout({
607
+ xAxis: [{
608
+ scaleType: 'band',
609
+ data: ['Urban', 'Peri-urban', 'Rural', 'Access', 'Village'],
610
+ tickLabelStyle: { fontSize: 13 },
611
+ }],
612
+ yAxis: [{ scaleType: 'linear' }],
613
+ xLabels: 'horizontal',
614
+ spacing: theme.spacing,
615
+ });
616
+ expect(layout.xAxis[0].height).toBeGreaterThanOrEqual(26);
617
+ });
618
+
619
+ // Rule 2 (WO non-goal guardrail): an axis without a title still reserves only the tick band --
620
+ // this WO's extra title-driven margin must not leak in when there is no title to share it with.
621
+ // Reviewer finding: asserting `xTitleBand === 0` alone passes whether or not the +2px title
622
+ // margin leaked into `xAxisBand` instead -- the band/height values themselves have to be
623
+ // checked against the same tick font's no-title formula (`ceil(fontSize * 1.3) + 9`).
624
+ it('does not add the title margin when the axis has no label', () => {
625
+ const layout = resolveChartLayout({
626
+ xAxis: [{ scaleType: 'band', data: ['A', 'B'], tickLabelStyle: { fontSize: 13 } }],
627
+ yAxis: [{ scaleType: 'linear' }],
628
+ xLabels: 'horizontal',
629
+ spacing: theme.spacing,
630
+ });
631
+ expect(layout.bands.xTitleBand).toBe(0);
632
+ expect(layout.bands.xAxisBand).toBe(26);
633
+ expect(layout.xAxis[0].height).toBe(26);
577
634
  });
578
635
  });
579
636
 
@@ -0,0 +1,187 @@
1
+ # UCM-CHART-15 — Shift the chart size scale up one step, and raise the ceiling
2
+
3
+ ## Part A — Envelope
4
+
5
+ *Authored by the Expertenchat. Authoritative WHAT/WHY.*
6
+
7
+ ### Goal
8
+
9
+ The named chart sizes become one step taller, and the scale gains two steps above `tall`.
10
+
11
+ | token | today | new |
12
+ |---|---|---|
13
+ | `compact` | 30 → 240 px | **40 → 320 px** |
14
+ | `standard` | 40 → 320 px | **50 → 400 px** |
15
+ | `tall` | 50 → 400 px | **60 → 480 px** |
16
+ | `extra_tall` | — | **70 → 560 px** |
17
+ | `super_tall` | — | **80 → 640 px** |
18
+
19
+ The 10-unit step is preserved throughout, and every value stays a clean multiple of the theme's 8 px
20
+ spacing unit.
21
+
22
+ ### Why
23
+
24
+ Two independent pressures, both from real call sites rather than speculation.
25
+
26
+ **The ceiling is too low.** `tall` (400 px) is the largest token, and hram has already had to use the
27
+ documented `height` escape to get past it — `StructuralReachabilityPanel` sits at `height={480}` with
28
+ a comment recording exactly this: *"no size token goes taller than `tall`"*. An escape used because
29
+ the scale ends is a scale that ends too early.
30
+
31
+ **The whole scale is too cramped.** The operator's judgement, on existing plots and not only the new
32
+ ones. That judgement lands on a value that was never chosen on its merits: the comment at
33
+ `src/components/charts/chartDefaults.js:295` says `standard` is
34
+
35
+ > "pinned to the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this
36
+ > migration does not also silently redraw every already-shipped default-sized chart"
37
+
38
+ That was a **migration guardrail, not a design verdict** — 320 was kept because it was the previous
39
+ value. The migration it protected is complete, so the pin has served its purpose and what remains is
40
+ a height inherited rather than decided.
41
+
42
+ ### The blast radius is one app, and that is why now
43
+
44
+ Measured across the workspace on 2026-08-24:
45
+
46
+ ```
47
+ hram 3.0.1 <- the only 3.x consumer
48
+ 14 other apps 2.41.3 / 2.37.0
49
+ ```
50
+
51
+ The size tokens are a **3.0.0 feature** (this repo's own CHANGELOG marks 3.0.0 "Breaking": `size`
52
+ replaced `minHeight`/`height`/`aspect`). On 2.x the scale does not exist. So no chart outside hram
53
+ changes, and the verification surface is one application.
54
+
55
+ **That inverts the usual caution into urgency:** fourteen apps are still to migrate onto 3.x. Every
56
+ app that migrates onto a scale already known to be too cramped has to be revisited afterwards.
57
+ Correcting the scale before they migrate is strictly cheaper than after.
58
+
59
+ ### Version: minor, by operator decision — and what that obliges
60
+
61
+ Redefining what an existing public token renders is breaking under semver, and the Expertenchat
62
+ recommended a major. **The operator decided minor on 2026-08-24; that decision stands and this WO
63
+ implements it.**
64
+
65
+ The consequence has to be carried somewhere, so it is carried here: a minor version can arrive
66
+ through automation. Renovate's weekly run bumps minors without a human reading the diff, and on this
67
+ estate that triage is also the vehicle that opens `develop → main`. A silent minor could therefore
68
+ make every hram chart 80 px taller in a PR nobody read as a visual change.
69
+
70
+ Two obligations follow, and they are not optional decoration:
71
+
72
+ 1. **The CHANGELOG entry must lead with the redraw**, in the plainest possible words: every existing
73
+ `compact`/`standard`/`tall` chart becomes 80 px taller. Not a bullet at the bottom — the first
74
+ line of the entry.
75
+ 2. **Release and consumption are coordinated**, not left to the weekly bump. `hram`'s pin bump and
76
+ its own verification are `HRAM-CHT-5` and land deliberately.
77
+
78
+ ### Scope
79
+
80
+ **`src/components/charts/chartDefaults.js`**
81
+
82
+ 1. `CHART_SIZE_SPACING_UNITS` becomes `{ compact: 40, standard: 50, tall: 60, extra_tall: 70,
83
+ super_tall: 80 }`.
84
+ 2. **Rewrite the pin comment at `:294-299`, do not delete it.** It explains why 320 was there; without
85
+ a successor the next reader sees an unexplained shift. The new text should say what the old pin
86
+ was for, that its migration is complete, and that the scale is now chosen rather than inherited.
87
+
88
+ **The enumeration sites — all of them.** The valid-size list is written out in more places than the
89
+ one that matters, and a half-updated enumeration is this change's most likely defect:
90
+
91
+ 3. `resolveChartHeightPx`'s error message (`:407`) — `"compact" | "standard" | "tall"`.
92
+ 4. `assertRemovedChartProp`'s guidance string in **each** chart component that carries it
93
+ (`LineChart.jsx:72`, `BarChart.jsx:54`, and the same call in `ScatterChart`).
94
+ 5. The `size` JSDoc in **each** preset (`LineChart.jsx:44`, `BarChart.jsx:27`, `ScatterChart.jsx:155`).
95
+ 6. `docs/CHART-LAYOUT.md`.
96
+ 7. The CHANGELOG entry described above.
97
+
98
+ Grep for the literal `"tall"` before declaring this done; the list above is what was found on
99
+ 2026-08-24 and is not guaranteed complete.
100
+
101
+ ### Non-goals / do not touch
102
+
103
+ - `resolveChartLayout`'s geometry, the axis-band resolution, margins, `xLabels`, rotation logic.
104
+ - The `height` px escape — it stays exactly as documented.
105
+ - `ChartFrame`'s `minHeight` semantics.
106
+ - Any consuming app. hram's adoption is `HRAM-CHT-5`.
107
+ - Adding a sixth step. If 640 px proves too small later, the scale extends by another 10 units; do
108
+ not pre-add one now.
109
+
110
+ ### Risks
111
+
112
+ - **A half-updated enumeration.** Adding the tokens to the resolver but not to the error message
113
+ produces a runtime error that lists only three valid values while five work — which reads as a bug
114
+ in the caller's code, not in this package.
115
+ - **A test that asserts the token names but not their values** would pass against both the old and
116
+ new scale and prove nothing. See test 1.
117
+ - The two new tokens have **no call site yet** in this repo. That is intended — `HRAM-CHT-5` adopts
118
+ them — but it means nothing here exercises them beyond the resolver test.
119
+
120
+ ### Tests to write
121
+
122
+ 1. `resolveChartHeightPx` returns **320 / 400 / 480 / 560 / 640** for the five tokens — asserting the
123
+ pixel values, not just that a number comes back. Written so it fails against the current scale.
124
+ 2. An unknown size still throws, and the message names **all five** valid tokens.
125
+ 3. The `height` escape still wins over `size`, for a new token as well as an old one.
126
+ 4. Existing chart-layout tests that hard-code 240/320/400 are updated deliberately, each one seen
127
+ and changed rather than mass-replaced — one of them may be asserting the old value *on purpose*.
128
+
129
+ ---
130
+
131
+ ## Part B — Implementation map
132
+
133
+ `.claude/models.local.json` has no local override, so the default applies: `implementation.runtime`
134
+ is `claude`/`sonnet` — the Orchestrator implemented directly, no Codex dispatch, no preamble/progress
135
+ contract needed.
136
+
137
+ Context package used:
138
+
139
+ - `src/components/charts/chartDefaults.js:294-300` (the pin comment + `CHART_SIZE_SPACING_UNITS`)
140
+ and `:402-419` (`resolveChartHeightPx`, its error message, and the `theme.spacing` unwrap comment).
141
+ - Enumeration sites confirmed by grep for the literal `"tall"` (matched the WO's own 2026-08-24
142
+ list exactly, nothing extra found): `LineChart.jsx:44,72`, `BarChart.jsx:27,54`,
143
+ `ScatterChart.jsx:155,198`, `docs/CHART-LAYOUT.md:66,73-75`.
144
+ - Existing hardcoded-value tests located via grep for `240|320|400` across `tests/*.test.{js,jsx}`:
145
+ `chartDefaults.test.js:119,562,576`, `BarChart.test.jsx:36,83-85`, `LineChart.test.jsx:51-53`,
146
+ `ScatterChart.test.jsx:242-244`. Distinguished from unrelated same-valued `minHeight`/`ChartFrame`
147
+ test fixtures (`chartDefaults.test.js:291,298,308,315,318`, `ChartFrame.test.jsx:294,311`,
148
+ `chart-api-census.test.js:196`), which were left untouched — they assert an arbitrary prop value,
149
+ not a size-token pixel height.
150
+
151
+ ---
152
+
153
+ ## Part C — Orchestrator only
154
+
155
+ > **STOP — everything below addresses the Orchestrator.**
156
+
157
+ **Tier 3 · tests: the four resolver cases plus the updated layout tests**
158
+
159
+ Shared-core — a change *inside* `ui-core-micha` is Tier 3 by the tiering table regardless of diff
160
+ size, and this one redefines rendered output for every consumer that reaches 3.x.
161
+
162
+ - `reviewer` — the enumeration completeness and the test's ability to fail are its brief.
163
+ - `ui_reviewer` — the scale as a design decision: does the 10-unit step still read as a coherent
164
+ scale with five members, and is `super_tall` distinguishable from `extra_tall` in practice.
165
+ - `sec_reviewer` — not run. No auth, no data, no exposure surface.
166
+ - Concurrent, full context.
167
+
168
+ ### Release
169
+
170
+ Minor bump per the operator's decision → **3.2.0** from the current 3.1.1. Follow this repo's release
171
+ procedure. The CHANGELOG obligation in Part A is a release gate, not a nicety: if the entry does not
172
+ lead with "every existing chart becomes 80 px taller", the release is not ready.
173
+
174
+ Do **not** bump hram from here. `HRAM-CHT-5` owns that, and it carries the visual verification.
175
+
176
+ ### Register
177
+
178
+ Row `UCM-CHART-15`, review notes in the required shape, the published version, landing SHA(s) in a
179
+ second commit before the push. Record explicitly that a **major was recommended and minor was chosen
180
+ by the operator** — a future reader tracing a silent redraw should find that decision, not infer it.
181
+
182
+ ### Log
183
+
184
+ - **2026-08-24** — Envelope authored. Operator judged all three existing tokens too cramped and chose
185
+ a full one-step shift over adding a token at the top, accepting the breaking redraw; added
186
+ `super_tall` to the two steps proposed. Version set to minor against the Expertenchat's
187
+ recommendation of major, recorded above with its consequence.