@micha.bigler/ui-core-micha 2.29.2 → 2.31.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/DESIGN.md +5 -5
- package/WORK_ORDERS.md +6 -0
- package/dev/entries.jsx +167 -1
- package/dev/main.jsx +10 -3
- package/dist/components/charts/BarChart.js +8 -6
- package/dist/components/charts/LineChart.js +23 -6
- package/dist/components/charts/TimeSeriesChart.js +12 -3
- package/dist/components/charts/chartDefaults.js +80 -0
- package/dist/components/charts/chartLabels.js +31 -0
- package/dist/components/charts/palette.js +7 -3
- package/dist/components/charts/yearTickInterval.js +7 -0
- package/dist/index.js +4 -0
- package/dist/theme/createAppTheme.js +168 -0
- package/dist/theme/index.js +4 -0
- package/dist/theme/themeCompleteness.js +320 -0
- package/dist/theme/tokens.js +325 -0
- package/package.json +4 -1
- package/src/components/charts/BarChart.jsx +37 -9
- package/src/components/charts/LineChart.jsx +61 -9
- package/src/components/charts/TimeSeriesChart.jsx +21 -0
- package/src/components/charts/chartDefaults.js +111 -0
- package/src/components/charts/chartLabels.js +36 -0
- package/src/components/charts/palette.js +6 -3
- package/src/components/charts/yearTickInterval.js +7 -0
- package/src/index.js +16 -0
- package/src/theme/createAppTheme.js +212 -0
- package/src/theme/index.js +14 -0
- package/src/theme/themeCompleteness.js +355 -0
- package/src/theme/tokens.js +376 -0
- package/tests/BarChart.test.jsx +4 -1
- package/tests/LineChart.test.jsx +5 -2
- package/tests/chartDefaults.test.js +177 -0
- package/tests/chartLabels.test.js +50 -0
- package/tests/chartsPalette.test.js +40 -0
- package/tests/createAppTheme.test.js +103 -0
- package/tests/themeCompleteness.test.js +63 -0
- package/tests/timeSeriesLegend.test.js +99 -0
- package/work-orders/CHART-6.md +428 -0
- package/work-orders/SHELL-2.md +197 -0
- package/work-orders/THEME-1.md +587 -0
- package/work-orders/assets/SHELL-2-account-navigation.html +1159 -0
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +1352 -0
package/DESIGN.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Frontend Design Principles
|
|
2
2
|
|
|
3
|
-
How we design app frontends across the estate. **Consistency here is consistency of METHOD, not one identical look** — every app
|
|
3
|
+
How we design app frontends across the estate. **Consistency here is consistency of METHOD, not one identical look** — every app starts from the shared `createAppTheme` baseline and owns its identity overrides (principle 1); the shared surface (`ui-core-micha` theme, components, charts, layout) carries the structural consistency. hram is the worked example throughout, not a look other apps must copy.
|
|
4
4
|
|
|
5
5
|
These principles are the spec a **prototype** encodes and the implementation matches. `frontend-engineering` (build) and `ui_reviewer` (review) point here; the prototype workflow is the `frontend-design` skill.
|
|
6
6
|
|
|
7
7
|
## Identity
|
|
8
8
|
|
|
9
|
-
1. **Honor the
|
|
10
|
-
2. **One accent, deliberately placed.** Interactive = the app's one accent colour. **Data colours are a separate layer.** Status (green / amber / red) is semantic and is NEVER repurposed as a series colour. Restraint over decoration.
|
|
9
|
+
1. **Honor the shared baseline and the app's identity; don't invent a parallel system.** `ui-core-micha`'s `createAppTheme` deliberately owns the complete visual baseline. Each app supplies its identity through `palette.primary` and `fontFamily`, plus reviewed overrides where its real design requires them (hram: primary blue `#468AB2`). **Match first, then elevate.** Never invent a parallel language — a made-up teal / pills / breadcrumb vocabulary was the v1 mistake.
|
|
10
|
+
2. **One accent, deliberately placed.** Interactive = the app's one accent colour. **Data colours are a separate layer with a shared default that apps may override.** Status (green / amber / red) is semantic and is NEVER repurposed as a series colour. Restraint over decoration.
|
|
11
11
|
3. **Commit to the app's theme reality — with intent.** If the app is single-theme (hram is light-only), commit to that deliberately rather than bolting on dark mode as an afterthought — a decision, not an omission. The estate does NOT mandate light-only; it is per-app — but whichever it is, it is on purpose.
|
|
12
12
|
|
|
13
13
|
## Information design (it's a tool, not a document)
|
|
@@ -19,13 +19,13 @@ These principles are the spec a **prototype** encodes and the implementation mat
|
|
|
19
19
|
|
|
20
20
|
## Charts (read by humans, executed by code)
|
|
21
21
|
|
|
22
|
-
8. **Every chart earns:** axis labels (with units), a legend only when >1 series, a tooltip, theme-token colours, container-responsive sizing, and empty/loading states. **Single-hue for magnitude; diverging (blue/red around zero) ONLY for signed data.** The concrete data palette is
|
|
22
|
+
8. **Every chart earns:** axis labels (with units), a legend only when >1 series, a tooltip, theme-token colours, container-responsive sizing, and empty/loading states. **Single-hue for magnitude; diverging (blue/red around zero) ONLY for signed data.** The concrete data palette is a separate layer with a shared `ui-core-micha` default that apps override when their domain calls for it.
|
|
23
23
|
**Dual-axis is a default-off exception, opt-in only** (CHART-5, operator decision): the shared `TimeSeriesChart` preset supports a second y-axis via a per-series `axis: 'secondary'` field specifically for two series sharing one axis at very different scales/units (e.g. a user count vs. hours) where series-toggles alone leave one series unreadable. Reach for it only under that same condition, not as a general layout choice — a single shared axis (or toggles) stays the default everywhere else.
|
|
24
24
|
9. **Ground it in the real subject matter.** Faithful structures from the real panels (hram: Morris-tornado, Pareto/knee, CI-strip, allocation-scatter with shape-encoded sources) — no lorem, no generic charts. Hence: **scout the real subject before building.**
|
|
25
25
|
|
|
26
26
|
## Craft
|
|
27
27
|
|
|
28
|
-
10. **Prototype = spec, parity = guardrail — both directions, within a DECLARED coverage.** Purely visual/UX — function stays; **"No behaviour / permission / data-contract change"** is the hard condition in the WO. Prototypes are routinely **partial** (tabs undrawn, flows stubbed), and nobody downstream can tell a deliberate omission from a required removal — so **prototype silence never authorizes a removal; the Envelope's "Replaces / removes" list does.** Every prototype therefore **declares what it covers and what it leaves untouched**. Inside the covered area each omission is resolved at authoring time into *removes* or *deliberately keeps*, leaving nothing ambiguous; outside it the prototype says nothing. Parity is then checked both ways **within that coverage**: everything drawn is present, and nothing listed as removed survives. **A redesign that ends up net-additive has failed**, however good its new parts — shipping MORE UI than before is the estate's recurring cleanup failure (status bands, duplicated badges, tip text layered on top of the very legacy the redesign was meant to replace).
|
|
28
|
+
10. **Prototype = spec, parity = guardrail — both directions, within a DECLARED coverage.** Purely visual/UX — function stays; **"No behaviour / permission / data-contract change"** is the hard condition in the WO. Prototypes are routinely **partial** (tabs undrawn, flows stubbed), and nobody downstream can tell a deliberate omission from a required removal — so **prototype silence never authorizes a removal; the Envelope's "Replaces / removes" list does.** Every prototype therefore **declares what it covers and what it leaves untouched**. Inside the covered area each omission is resolved at authoring time into *removes* or *deliberately keeps*, leaving nothing ambiguous; outside it the prototype says nothing. Parity is then checked both ways **within that coverage**: everything drawn is present, and nothing listed as removed survives. **Inventory is not enough.** Presence and absence say nothing about weight, density or colour — an implementation can pass this check completely and still read heavier and older than the prototype. Parity therefore also requires a **rendered side-by-side at two widths** before the change lands (the Orchestrator's — `AGENTS.md` → Reviews), and the token values themselves come from the artifact's `<style>` block via the Envelope's token delta list. **A redesign that ends up net-additive has failed**, however good its new parts — shipping MORE UI than before is the estate's recurring cleanup failure (status bands, duplicated badges, tip text layered on top of the very legacy the redesign was meant to replace).
|
|
29
29
|
11. **Align to the shipped reality.** When related features land, pull the prototype onto them so it is a truthful spec, not a contradicting vision.
|
|
30
30
|
12. **Avoid the generic "AI look"; take details seriously.** No cream / serif / neon default; embed the real font (CSP-safe as a data-URI, not a silent-fallback risk); `tabular-nums` for aligned numbers; a11y (role/aria on chart regions, focusable info). **Template tells to CUT** (unless the app's design system genuinely specifies them): monospace-uppercase **eyebrow / kicker labels** (e.g. `IMARA · ADVANCING RESILIENT HEALTH SYSTEMS`), decorative **pills / badges**, `FIG. 01 ·` / `F.01` **figure captions**, `↳`-prefixed **micro-links**, **oversized display** headlines, **sketchy / hand-drawn** styling, and **explanatory paragraphs that restate the UI** (help is on-demand — see #6). Restraint is the default; each of these is added only with a real reason.
|
|
31
31
|
|
package/WORK_ORDERS.md
CHANGED
|
@@ -12,6 +12,7 @@ rules are defined centrally in `webapps/AGENTS.md` → "Work-Order Register".
|
|
|
12
12
|
| `PERF-*` | Performance improvements |
|
|
13
13
|
| `NOTIF-*` | Shared notifications platform (dcm register is canonical; this repo's rows are its ucm-side WOs) |
|
|
14
14
|
| `CHART-*` | Shared chart / data-viz kit (`ChartFrame` + MUI X-Charts presets + neutral palette/formatters) |
|
|
15
|
+
| `THEME-*` | Shared MUI theme baseline (`createAppTheme` factory, token surfaces, completeness assertion) |
|
|
15
16
|
| `MSG-*` | Shared messaging platform, Phase B (dcm register is canonical; this repo's rows are its ucm-side WOs) |
|
|
16
17
|
| `DX-*` | Developer experience / repo tooling (ucm-local prefix; unrelated to `django-core-micha`'s own `DX-*`) |
|
|
17
18
|
| `AUTH-*` | Auth components UX (login/password/MFA/profile forms, auth error i18n) |
|
|
@@ -23,6 +24,8 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
23
24
|
|
|
24
25
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
25
26
|
|---|---|---|---|---|---|---|
|
|
27
|
+
| CHART-6 | Chart-chrome defaults in the three wrappers, plus two generic fixes hoisted up | Closes the chart-chrome default surface in `BarChart`/`LineChart`/`TimeSeriesChart`: baseline tick font size (from `theme.typography.caption`), a horizontal grid, `showMark:false` with a filled-marker fix when a caller opts markers back in, automatic bottom-margin growth for rotated tick labels, a new `legendPosition` prop (default bottom-start), and `TimeSeriesChart`'s own toolbar becoming the sole legend (MUI's redundant second legend removed). Hoists `chartLabels.js` (short granularity-aware tick-label formatters) and a generalised `yearTickInterval` from fitness-monitor. `2.31.0`. Full spec: [work-orders/CHART-6.md](work-orders/CHART-6.md). | 2026-08-10 | done | 6dedc62 | Envelope authored by Expertenchat (DS-4). Precondition (THEME-1 landed + published `2.30.0`) verified before start. Orchestrator filled the Implementation map with concrete MUI X-Charts source pointers (installed `8.29.2`, not the WO's stated `8.28.2` — flagged and verified against the actual installed source throughout), then ran Codex (stdin invocation, same as THEME-1). Codex implemented `src/components/charts/{chartDefaults,chartLabels,yearTickInterval}.js`, wired all three wrappers, wrote the 10 required tests, and used a wrapper-level `slots.mark` composition for the filled-marker fix (a deliberate, now-documented deviation from the WO's named `MuiMarkElement` theme-token mechanism — the wrapper-level fix applies to all 14 consumers immediately on this version bump, not only to apps that separately adopt `createAppTheme`). **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently.** `reviewer` found **R1 (P2)** — the marker-fill deviation from the WO's specified mechanism was undocumented; fixed with an explanatory code comment (functionally correct either way, this was a disclosure gap, not a bug). `ui_reviewer` found **U1 (P3)** — `SeriesToggleDot`'s 12px dot used hardcoded pixels instead of `theme.spacing()`; fixed. Both reviewers otherwise found the six highest-risk invariants (default-vs-caller-override merge order, filled markers, rotated-tick margin math, the double-legend fix, `yearTickInterval`'s generalisation, `skipAnimation` passthrough) correct and non-vacuously tested. **A third, more serious bug was found by the Orchestrator's own DS-1 two-width rendered check (not by either reviewer, since both are static-diff reviews)**: `withAxisDefaults` in `chartDefaults.js` only applied `defaults.scaleType` when the caller passed NO `xAxis`/`yAxis` array at all — a caller-supplied axis with `data` but no explicit `scaleType` (exactly what the new dev-harness "Chart defaults" `LineChart` specimen used) silently lost the wrapper's `'point'`/`'band'` scale default entirely, leaving MUI's scale undefined and every rendered line/point position `NaN` (`<path> attribute d: Expected number, "MNaN,...`, confirmed live via `read_console_messages` and only reproducible through a real MUI scale computation — a mocked prop-assertion test structurally cannot catch it, the same class of bug CHART-1/3/4/5 have each found before in this kit). Fixed by spreading `defaults` before `axis` in the per-axis merge (caller's own explicit `scaleType`, if any, still wins); a new regression test added (`tests/chartDefaults.test.js`, "keeps the default scaleType on a caller-supplied axis that does not set one"), confirmed against both `BarChart`'s `'band'` and `LineChart`'s `'point'` defaults. Re-verified after all three fixes: 44/44 scoped vitest tests green (10 new + 34 existing adjacent chart tests, no regressions), `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's "Theme / Shared baseline" entry (which now also carries the two new `LineChart` chart-chrome specimens per the WO's "at least one chart that set the props and one that did not" requirement) at 375px and 1280px — confirmed live: correct line rendering (post-fix), filled markers on the caller-override chart, horizontal grid, bottom-start vs. caller-chosen legend position, and rotated tick labels with adequate margin at 1280px. |
|
|
28
|
+
| THEME-1 | Shared MUI theme baseline: `createAppTheme` factory + completeness assertion | Ships `createAppTheme(appConfig)` (palette-first merge of a fixed light-mode baseline with the app's own `palette`/`typography`/`components`; throws without `palette.primary`; throws on a function anywhere in `appConfig.components.*.styleOverrides`), an exported `assertThemeComplete` completeness assertion with an exemption arm and contrast checks (status text-on-own-tint, `controlBorder` on white + page background), a non-breaking chart-ramp fallback in `useNeutralChartPalette` (adopters get the theme's `dataSeries` ramp, non-adopters see byte-identical output), a new "Shared theme baseline" dev-harness entry, and `DESIGN.md` principles 1/8 reworded per the accepted policy change (ucm now ships a shared theme; the data-colour layer has a shared default). Full spec: [work-orders/THEME-1.md](work-orders/THEME-1.md). | 2026-08-10 | done | b329b39, 96992cf | Envelope authored by Expertenchat from the accepted reference sheet (`work-orders/assets/THEME-1-baseline-reference-sheet.html`, DS-2 decisions 1–23). Orchestrator filled the Implementation map, then ran Codex (stdin, not positional arg — the WO text exceeded Windows' command-line length limit via the npm shim, "Argument list too long"; `codex exec ... -` reading the WO from stdin is the working invocation for large WOs on this platform). Codex implemented `src/theme/{tokens,createAppTheme,themeCompleteness,index}.js`, wrote all 13 required tests + extended `tests/chartsPalette.test.js`, updated `src/index.js` exports, `DESIGN.md`, `package.json` (2.29.2→2.30.0, new `@fontsource/dm-sans` dependency — WO-scoped, no separate approval needed), and the dev harness. **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently. `ui_reviewer`: no findings.** `reviewer` found three, all confirmed real and fixed by the Orchestrator: **R1 (P2)** — MUI's `createTheme(options, ...args)` only runs palette augmentation on the FIRST argument; every later arg (how the baseline was layered in) is deep-merged raw, so `success`/`warning`/`error`/`info` — which had `text`/`fill`/`fillText`/`bg` but no `main`/`contrastText` — silently kept MUI's own stock hues for any standard `color="success"` usage. Verified live via a Node repro (`createTheme({...},{palette:BASELINE_PALETTE},{...})` on the pre-fix baseline produced `success.main:'#2e7d32'`, MUI's stock green, not the canonical `#1B8038`). Fixed: explicit `main`/`light`/`dark`/`contrastText` added to all four status channels (`light`/`dark` computed via MUI's own `lighten`/`darken` at the same 0.2/0.3 tonal offsets `augmentColor` uses), plus a new regression assertion in `tests/createAppTheme.test.js`. **R2 (P3)** — `controlBorder.focus` was derived to clear 3:1 only against `background.paper`, while `assertThemeComplete`'s own contrast check tests it against `#FFFFFF` and `background.default` — an app overriding either background could pass the derivation and still fail the assertion. Fixed: derivation now targets both surfaces. **R3 (P3)** — `MuiOutlinedInput`'s `variants` array was nested inside `styleOverrides.root` instead of as a sibling key; MUI never reads `variants` from inside `styleOverrides` (it's matched by `props` independently), so the entry was inert dead code, and it also meant the function-in-`styleOverrides` guard's own suggested escape hatch ("use MUI variants") was structurally unreachable from inside `styleOverrides`. Fixed: moved to the correct sibling placement; updated the one test and the one completeness-surface path that referenced the old (wrong) location. Re-verified after fixes: 15/15 scoped vitest tests green (`tests/createAppTheme.test.js`, `tests/themeCompleteness.test.js`, `tests/chartsPalette.test.js`), `tsc -p tsconfig.build.json --noEmit` clean. Orchestrator's own security/scope pass: no findings (only the WO-approved `@fontsource/dm-sans` dependency addition, no secrets, no scope creep). **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's new "Theme / Shared baseline" entry at 375px and 1280px — both screenshots confirm radius (3/8), no resting shadow on Paper/Card, correct status fill/text/bg tones, and the six-colour series ramp, consistent with the reference sheet. No app pin bumps in this WO — all 14 consumers pin exact versions and pick this up on their own adoption WO. |
|
|
26
29
|
| CHART-5 | Dual y-axis + always-visible x-axis ticks on `TimeSeriesChart` | Opt-in second y-axis via a new per-series `axis: 'secondary'` field (backward-compatible, off by default) with auto-detected integer-only tick labels per axis group; deterministic x-axis `tickLabelInterval` so bucket labels never all vanish under MUI's default collision-avoidance (the live symptom: zero x-axis labels with 24 hourly buckets). Reverses CHART-2's deliberate "no second y-axis" choice on fresh operator instruction; `DESIGN.md` #8 updated to record the scoped exception. Full spec: [work-orders/CHART-5.md](work-orders/CHART-5.md). | 2026-08-07 | done | 839e6c5, 84c37ac, 0dd3bac, f4e5586, 4a12a71 | Operator-requested after seeing the shipped chart live: y-axis showed decimal ticks (0.2, 0.4, ...) for an integer user-count series sharing an axis with fractional hours; x-axis showed no time labels at all. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). Independent `reviewer` + `ui_reviewer` ran concurrently: **`reviewer` found R1 (P1, real bug)** — used `yAxisKey` (removed in MUI X-Charts v7, per the installed v8.29.2's own CHANGELOG and `useBarPlotData.mjs` source, which reads `series[i].yAxisId`) instead of `yAxisId`; the WO explicitly asked to verify this passthrough against real MUI behaviour rather than assume, and the first pass didn't — the dual-axis feature would have silently plotted every series on the primary axis in production despite all mocked tests passing (a mocked-prop test structurally cannot catch a wrong-field-name bug, since it only checks whatever the code itself sets). Fixed (`yAxisKey` → `yAxisId`, verified against `node_modules` source directly, not just the mock) and the pre-existing `tests/BarChart.test.jsx` "dual axis" case corrected too (R2 — it used the same wrong field name and was cited by the WO as precedent, giving false confidence). **`ui_reviewer` found U1 (P1, real bug)** — `tickLabelInterval`'s `index % step === 0` systematically dropped the LAST x-axis bucket unless it landed exactly on a step boundary (e.g. 24 labels/step 3 → indices 0..21, never 23), exactly the failure mode the WO's own SCOPE C explicitly warned to check for; fixed by force-including `index === lastIndex`, with a new `it.each([24,12,7,1])` regression test, confirmed non-vacuous by reverting the fix. **U2 (P2)**: `DESIGN.md` #8's "Never dual-axis" now contradicted the shipped code — updated to record dual-axis as a scoped, opt-in exception (CHART-5) rather than a general rule change. U3 (P3, fixed `MAX_X_TICKS` under-displays low-density ranges e.g. 12 monthly buckets showing only 6) and U4 (P3, no UI indication of which axis a toggle's series plots against) both accepted, not fixed — informational, no code change. Re-verified after fixes: 31/31 scoped vitest tests (`TimeSeriesChart.test.jsx` + `BarChart.test.jsx`) green, `tsc` build clean. Published: ucm 2.29.0 (minor — new optional per-series field + prop, additive, no breaking change). **Third bug found post-publish (2.29.1 patch)**: after jg-ferien adopted it (ACT-7) the operator reported live that the secondary axis was "obviously there but invisible" and x-axis labels were still missing. Investigated by adding a new DX-1 harness entry (`dev/entries.jsx` `TimeSeriesChartEntry`, previously missing — this component had NEVER been visually verified in a real browser through CHART-2/3/4/5) and reading the real rendered DOM directly. X-axis confirmed working (9 evenly-spread labels for 24 buckets) — the live report was very likely a stale PWA service-worker cache on jg-ferien's side, not a code defect. Y-axis confirmed broken: only 1 of 2 `yAxis` entries rendered in the DOM. Root cause traced to the installed MUI X-Charts source (`defaultizeAxis.js`): `defaultPosition = index === 0 ? 'left' : 'none'` — every yAxis array entry past the first silently renders nothing unless given an explicit `position`, a class of bug no mocked-`BarChart` test can catch (the mock never runs MUI's own defaulting logic). Fixed by setting `position: 'left'`/`'right'` explicitly on the primary/secondary entries; independent `reviewer` traced the fix against the same MUI source and confirmed no second gating layer exists. Live-reverified in the dev harness after the fix: both axes render with independent scales (primary `0,1,2`; secondary `0.0,0.5,1.0,1.5,2.0`). 32/32 tests green, build clean. Published: ucm 2.29.1 (patch). **Fourth report, root cause NOT in ucm**: operator reported x-axis labels still empty even after 2.29.1. Extensive live investigation (scoped DOM queries, computed-style checks, a real-browser-DOM comparison against this repo's own dev harness in both dev and production-build mode, a DM-Sans-not-loaded font test) could not reproduce it anywhere in ucm with the exact real bucket data — ruled out: stale cache, MUI axis config, container width, dev-vs-prod build, font-loading timing. Root cause found via web search: a known, unfixed upstream MUI X-Charts bug (mui/mui-x#18768, duplicate of #18399) where insufficient measured space makes its `ellipsize` logic collapse an ENTIRE tick label to `''` instead of shortening it, confirmed live via a `<tspan>` with correct styling but zero text content — triggered by jg-ferien's verbose label format ("08/05, 10:00 AM", 16 chars) combined with its actual DM Sans font metrics (not reproducible with this repo's default Roboto-based harness). Fixed in jg-ferien (ACT-9: short, granularity-aware labels), not in ucm — `TimeSeriesChart` itself has no defect here, it's a host-side label-length choice interacting with an upstream MUI bug. This repo's DX-1 harness entry (`TimeSeriesChartEntry`) updated to model the correct short-label pattern so it doesn't teach the risky verbose format. |
|
|
27
30
|
| CHART-4 | Fix `TimeSeriesChart` chart canvas rendering at zero height | Neither `ChartFrame` nor `BarChart` ever receive a height from `TimeSeriesChart` — MUI X-Charts' responsive container measures a zero-height parent and draws no bars/axes, while the legend (sized by its own content) renders regardless, which is why the live symptom was "legend shows, chart canvas doesn't". Fix supplies a preset default height (`CHART_HEIGHT = 320`, `minHeight` on `ChartFrame` + `height` forwarded to the real `MuiBarChart`), matching the working `dev/entries.jsx` reference pattern. Full spec: [work-orders/CHART-4.md](work-orders/CHART-4.md). | 2026-08-07 | done | f0953a1, fcb289b | Second, independent bug found live in jg-ferien immediately after CHART-3 + the ucm pin bump landed — CHART-3's own fix is what let this one become visible for the first time (it was previously masked by the permanently-empty-chart bug). Root cause of it shipping twice undetected: every test in `tests/TimeSeriesChart.test.jsx`/`tests/BarChart.test.jsx` mocks the real `@mui/x-charts/BarChart`, so no test has ever mounted the real chart and observed its size. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). **Test-writing pitfall found and self-corrected during implementation**: first wrote an *unmocked* render test asserting the real chart SVG's `viewBox` height was non-zero — verified vacuous (stays green even fully reverting the fix, since jsdom's `ResizeObserver`/layout handling doesn't reproduce the real zero-height-parent browser behavior this bug depends on). Deleted; replaced with a `chartSpy`-mocked prop assertion (`props.height` non-zero) matching the file's existing convention, confirmed non-vacuous (fails pre-fix with `props.height` undefined). Independent `reviewer` + `ui_reviewer` ran concurrently: `reviewer` verified the fix mechanism end-to-end (no prop-name collision between `BarChart`'s own `minHeight`/`aspect` and the forwarded `height`) and the test's non-vacuity claim, no findings. `ui_reviewer`: two P3 informational notes, neither blocking — (U1) the fixed 320px height is a deliberate, documented exception to `DESIGN.md` #14 ("responsive = container-sized, not fixed px"), needed specifically because MUI X-Charts' own `ResizeObserver`-based height measurement is what caused CHART-4 in the first place; a code comment was added warning future readers not to "fix" this back to a responsive height without re-verifying the underlying MUI bug is resolved. (U2) `CHART_HEIGHT` uses 320 for both `ChartFrame.minHeight` and `BarChart.height`, vs. `dev/entries.jsx`'s 360/320 split — functionally harmless (WO explicitly permits one fixed value), no change made. 13/13 scoped vitest tests green, `tsc` build clean. Published: ucm 2.28.2 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
|
|
28
31
|
| CHART-3 | Fix `TimeSeriesChart` rendering nothing on async data load | `visibleKeys` was a lazy `useState` initialized once from `data` at first mount; any host that fetches asynchronously after mount (the normal case, e.g. jg-ferien's `ActivitySection`) ends up with a permanently-empty `visibleKeys`, so `isDataEmpty` stays `true` forever and the chart body never renders even once real data arrives. Fix syncs newly-seen series keys to visible-by-default while preserving a user's manual toggle-off across data updates. Full spec: [work-orders/CHART-3.md](work-orders/CHART-3.md). | 2026-08-06 | done | 9a1bd9d, 48cefa8 | Found live in jg-ferien's Aktivität screen (ACT-3 consumer) — toolbar rendered, chart body stayed on `ChartFrame`'s empty state permanently. **Codex was out of workspace credits** (genuine quota error on invocation, no partial work) — orchestrator implemented directly per the Codex→Claude fallback rule; independence flip applies (author = orchestrator, so independent review was mandatory, not self-check). Independent `reviewer` + `ui_reviewer` both ran (background, concurrent): `ui_reviewer` found no findings; `reviewer` found R1 (P2, real bug — the seen-keys tracker was a `useRef` mutated during the render body alongside a paired `setVisibleKeys` call; under React concurrent rendering an interrupted/discarded render leaves the ref mutation applied while the paired state update is discarded, permanently hiding a key without it ever reaching `visibleKeys` — a different-shaped recurrence of the exact bug this WO exists to fix) and R2 (P3, test-file duplication — a second near-duplicate render helper). Both fixed: seen-keys tracking moved from a ref to a second `useState` (both states updated together in the render body, so a discarded render rolls both back together — React's documented pattern for this), and the duplicate test helper collapsed into the existing `renderChart`'s `rerenderWith`. Re-verified: 12/12 scoped vitest tests green (3 new CHART-3 regression tests, 2 of which confirmed non-vacuous by failing against the pre-fix code), `tsc` build clean. Published: ucm 2.28.1 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
|
|
@@ -53,3 +56,6 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
53
56
|
| AUTH-1 | Password forms: state the rules (helperText + inline validator) | Spesix field report 2026-08-06: the invitation set-password screen rejects first-time users without ever stating the rules. Add `helperText` (default `Auth.PASSWORD_RULES_HINT`, optional `passwordRulesHint` prop override) on the new-password field of **PasswordSetForm + PasswordChangeForm** (NOT `PasswordResetRequestForm` — email-only, report listed it by mistake) + an inline validator for the two cheap/faithful rules (min-8, not-all-numeric) via the existing `localErrorKey` path (signal before the backend rejects). Baseline = dcm's 4 default validators. ucm-only, **zero per-app**. Full spec: [work-orders/AUTH-1.md](work-orders/AUTH-1.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). **Codex → Claude fallback**: re-confirmed out of workspace credits three times on 2026-08-06 alone (see `project_codex_quota_status` memory) before this WO started, so implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. `PasswordChangeForm.jsx` had no `localErrorKey` mechanism at all before this WO (only `PasswordSetForm.jsx` did) — added it from scratch, matching the existing pattern exactly (same `<Box sx={{color:'error.main'}}>`, same check ordering). **`ui_reviewer`: no findings** (helperText/error-box conventions match, translation coverage 316/316 keys × 4 locales, hint text checked against dcm's actual `AUTH_PASSWORD_VALIDATORS`, no missing DX-1 harness entry since none existed before either). **`reviewer`: one P2 fixed before commit** — the inline min-8/not-all-numeric rule was hard-coded with no escape hatch; since `AUTH_PASSWORD_VALIDATORS` is a per-app Django setting this shared-core repo doesn't control, an app running a looser backend policy would have valid passwords rejected client-side on its next ucm pin-bump. Fixed by adding optional `minPasswordLength` (default 8) / `allowNumericPassword` (default false) props to both forms, with regression tests; all other consumers keep today's behaviour unchanged (identical defaults). Two P3s from `reviewer` disclosed, not fixed (both pre-existing/out-of-scope): the WO's own rationale text overstated that `PasswordChangeForm` already had the local-error pattern (it didn't; corrected here); neither form clears its local error on the next keystroke after a rejection (pre-existing `PasswordSetForm` behaviour, now also present in `PasswordChangeForm`). Gate: 32/32 new tests across 4 files; full suite 322/322, `tsc` clean. **Published version correction**: the shared org CI runner had a multi-hour backlog on 2026-08-06 (queued jobs "not acquired by Runner"; see `reference_netcup_ci_runner` memory) — the 2.28.0 tag itself was never actually published on npm (confirmed via `npm view … @2.28.0` → 404), because by the time a runner slot freed up, three more sessions had pushed CHART-3/4/5 on top and the workflow published from that later HEAD instead. AUTH-1/AUTH-2 code is confirmed present (verified by downloading and grepping the actual npm tarball) starting at the first version that did publish, **npm 2.28.1**, and in every version since (currently 2.29.1). Consumers adopt on their own pin-bump (Spesix pinned at 2.5.0 = separate later bump, unaffected today). Companion: AUTH-2. |
|
|
54
57
|
| AUTH-2 | Translate backend auth error codes (stop leaking raw codes) | ucm surfaces raw backend/allauth error **codes**, not text: `apiClient.jsx:107` extracts `err.code`, components do `t(err.code)`/`setErrorKey(err.code)`, but the catalogue has no entries → users see literal `email_password_mismatch` / `password_too_short`. Affects **all** rendering auth screens (login/change-pw/MFA/profile). Fix = add de/fr/en i18n entries **keyed by the enumerated codes** → the existing `t(err.code)` resolves, **zero component/app change**. Does NOT reach the delegating invitation `PasswordSetForm` (that's AUTH-1). Full spec: [work-orders/AUTH-2.md](work-orders/AUTH-2.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). Same Codex→Claude fallback and reviewer pairing as AUTH-1 (one combined diff, both WOs landed together). **Keying decided: raw-code (drop-in)**, per the WO's own stated preference — no operator request for the namespaced `i18nKey` alternative. **Codes enumerated by reading the installed allauth/Django source directly** (not guessed): `email_password_mismatch`/`username_password_mismatch`/`phone_password_mismatch` (`allauth/headless/account/inputs.py` `LoginInput.clean`), `too_many_login_attempts`/`invalid_login`/`account_inactive`/`enter_current_password` (`allauth/account/adapter.py` error_messages + `allauth/headless/account/inputs.py` `ChangePasswordInput.clean_current_password` — confirmed this genuinely means "wrong current password", not merely "field left blank"), `incorrect_code` (`allauth/mfa/**`), `password_too_short`/`password_too_common`/`password_entirely_numeric`/`password_too_similar` (Django `contrib/auth/password_validation.py`, matching dcm's `AUTH_PASSWORD_VALIDATORS` baseline). Added de/fr/en/**sw** (file's established 4-locale convention, honoured over the WO text's literal "de/fr/en"). **`ui_reviewer` + `reviewer`: no AUTH-2-specific findings** (both spot-checked the code spellings independently against the same installed packages; no key collisions with existing `Auth.*` keys; `authErrorCodeRendering.test.jsx` proves a mapped code renders human text via a real i18next instance and a genuinely-uncoded error still falls back to the existing generic `Auth.PASSWORD_CHANGE_FAILED`). Gate: shared with AUTH-1 — full suite 322/322, `tsc` clean. **Published version correction**: see AUTH-1's row — the 2.28.0 tag was skipped by a CI runner backlog and never published; verified present in the npm tarball from **2.28.1** onward (currently 2.29.1). Companion: AUTH-1. |
|
|
55
58
|
| AUTH-3 | SignUpPage showed the access-code-specific success message for every signup mode | Operator-observed live bug 2026-08-07: open self-registration (no access code) showed "Falls der Zugangscode für dieses Projekt gültig ist, wurde ein Einladungslink an X gesendet" — nonsensical when there is no code at all. Root cause: `SignUpPage.jsx`'s `handleSubmit` hardcoded `Auth.INVITE_REQUEST_SUCCESS` for every mode (open/access-code/email-domain/QR) regardless of the backend's actual response. | 2026-08-07 | done | 599e73d, b4e1fe7 | No WO file — operator-reported bug, fixed directly, same convention as MSG-6c/MSG-6d/MSG-6j. Tier 2 (shared-core). **Codex → Claude fallback**: one fresh probe made today (new day, prior confirmation was 2026-08-06) — still "workspace out of credits", stamped in `project_codex_quota_status` memory — implemented directly, author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. **Root cause confirmed against the actual backend** (`django-core-micha` `register_request` view + `validate_access_code_or_error`): every signup mode's success response already carries a mode-agnostic, accurate `code: "Auth.INVITE_SENT"`; an invalid access code is never silently accepted, it raises a distinct 400 (`Auth.ACCESS_CODE_INVALID_OR_INACTIVE`) — so the old key's "if valid…" ambiguity never matched backend reality for any mode, not just the ones reported. Fix: `setSuccessKey(res?.code || 'Auth.INVITE_SENT')`, using the backend's own code; no component/app change beyond this. **Both reviewers independently flagged the same P3**: switching away from the old key silently dropped the `{{email}}` confirmation (`Auth.INVITE_SENT` had none). Fixed before commit — extended `Auth.INVITE_SENT`'s text to include `{{email}}` in all 4 locales (safe: it had exactly one consumer, this call site, confirmed by grep). `Auth.INVITE_REQUEST_SUCCESS` is now dead/orphaned (both reviewers flagged as P3, tracked here, not removed — out of this fix's scope). Gate: `tests/SignUpPage.test.jsx` 13/13 (3 new, proving the backend code wins over the old hardcoded key, and the no-code fallback); full suite 342/342, `tsc` clean. |
|
|
59
|
+
| SHELL-2 | AccountPage: from a horizontal tab strip to a grouped sidebar + mobile drawer | Measured live at 375 px on 2026-08-08 (running cockpit, host header neutralised so the numbers describe ucm alone): `AccountPage` does **not** break the layout -- all six sections render at document width 375. What fails is discoverability and the users table. The tab strip scroller is 343 px visible against 589 px of content with `overflow-x: auto`, 246 px of travel, and **`scrollButtons: 0`** -- Support and any consumer-injected section sit off screen with no arrow, no fade, no hint they exist (they do respond to a horizontal swipe, so this is a discoverability failure, not a reachability one -- unlike cockpit's own header tabs, which are `width: 0px` + `overflow: hidden` and genuinely unreachable). The users table renders 1045 px inside a 341 px `MuiTableContainer`; one third is visible and the `Aktionen` column sits ~700 px to the right. Adopts jg-ferien's `EventInfoPage` pattern (`SectionSwitcher.jsx`, driven from `EventInfoPage.jsx:1126-1159`) in **both** halves -- sticky grouped 280 px sidebar at `md`+, bottom drawer below `md` -- plus an in-page trigger bar, which the reference does not supply because jg's trigger lives in that app's own page header and `AccountPage` has none. | 2026-08-08 | blocked | | **Held back 2026-08-08 — reshaped into DS-11** (see `webapp-management/DESIGN_SYSTEM_PROGRAM.md`): do NOT implement this WO as written; the Envelope will be revised (open: keep the SHELL-2 ID or mint a new SHELL-* ID). Known corrections for the revision: publishing is version-gated, not per-push (this row's "publishes immediately" is imprecise), and the grouped-shell part becomes an inheritable ucm component with `AccountPage` as first consumer. Envelope: [work-orders/SHELL-2.md](work-orders/SHELL-2.md). Prototype: [work-orders/assets/SHELL-2-account-navigation.html](work-orders/assets/SHELL-2-account-navigation.html). Tier 2 (shared core) -- independent `reviewer` (Sonnet, full) **and** `ui_reviewer` concurrently, both mandatory before commit. **Operator decisions, settled 2026-08-08 and not to be renegotiated by implementer or reviewer:** both halves of the pattern, so the horizontal tab strip disappears **on desktop too** (asked explicitly, chosen deliberately, in full knowledge that 14 consumers inherit the desktop change on their next pin bump); fixed groups `Mein Konto` / `Verwaltung` / `Hilfe` / `Weitere`, with `extraTabs` gaining an **optional** `group` field, additive and backward compatible; no standalone "Uebersicht" entry above the groups (jg-ferien has one, `AccountPage` has no overview screen for it to point at, so none is invented); users table becomes a card list below `md` and stays a table at `md`+; the mobile trigger bar is the **sole** carrier of the current section name, so the section heading below it is **removed**, not merely undrawn -- desktop keeps its heading because it has no trigger bar. **Two contracts that must survive and are currently untested:** the `?tab=` URL contract incl. fallback to `profile` on an unknown *or* unpermitted value (`AccountPage.jsx:60`, `:180-186`) -- cockpit's `/user-management`, `/profile` and `/change-password` redirects all hang off it -- and the full `extraTabs` shape (`:146-155`), whose `label` may be a **function** and which supports `visible(context)`. `AccountPage` and `UserListComponent` have **zero tests today** (none of the 20 files under `tests/` touches either), so both test files are new, not extensions. **No staging net:** this repo has no `develop`; a push to `main` touching `src/**` or `package.json` publishes to npm immediately via `publish.yml`, so the independent review is the only gate and is not back-fillable. Version target `2.30.0` (minor -- `extraTabs` gains a public field; additive API is a minor under semver). Explicit non-goals: pagination/filtering for the user list, the sections' internal content, the six auth pages (measured clean at 375 px), and **cockpit's header** -- that toolbar is what inflates the document to 498 px on every route including `/account`, and it is a separate WO in the cockpit repo, sequenced after this one at the operator's choice. Carried-over trap from the reference: jg-ferien uses `env(safe-area-inset-bottom)` while its `index.html` omits `viewport-fit=cover`, so those insets resolve to zero there -- copy the padding, do not assume it does anything. |
|
|
60
|
+
| THEME-1 | Shared MUI theme baseline: `createAppTheme` factory + completeness assertion | Every app theme sets a handful of values and leaves the rest at MUI's defaults, so implementations drift toward Material Design 2014 (uppercase buttons, 20px h6, 16px body, elevation shadows). The baseline was distilled from 13 prototype artifacts across 5 repos plus 5 app themes, and settled with the operator over two review rounds against a rendered reference sheet. | 2026-08-09 | planned | | Reference sheet (visual + token spec, live WCAG contrast checks): [work-orders/assets/THEME-1-baseline-reference-sheet.html](work-orders/assets/THEME-1-baseline-reference-sheet.html). Its canonical token table is the value source — the Envelope references it rather than restating the numbers. Envelope: [work-orders/THEME-1.md](work-orders/THEME-1.md) (authored 2026-08-10, Approval Gate #1 pending). Version target 2.30.0; DS-4 follows as 2.31.0. Program context: `webapp-management/DESIGN_SYSTEM_PROGRAM.md` (DS-2). |
|
|
61
|
+
| CHART-6 | Chart-chrome defaults in the three wrappers + the two generic fixes hoisted up | The kit's `LineChart`/`BarChart`/`TimeSeriesChart` are ~18 lines of logic each and set no tick font, grid, margin or marker styling, so MUI X-Charts' own visual language shows through untouched. Hand discipline does not close it: hram, the exemplar, sets `tickLabelStyle.fontSize` at only 4 of 9 call sites. Also resolves `TimeSeriesChart`'s double legend and hoists fitness-monitor's label-collapse workaround (two consumers) plus `yearTickInterval`. | 2026-08-10 | planned | | Envelope: [work-orders/CHART-6.md](work-orders/CHART-6.md) (Approval Gate #1 pending). **Hard precondition: THEME-1 must land and publish as 2.30.0 first** — both touch `src/index.js` and `src/components/charts/`, and this repo publishes on every push. Version target 2.31.0. Legend auto-placement is deliberately out of scope (own strand — the in-plot overlay path is unverified in X-Charts 8.28.2). Program context: `webapp-management/DESIGN_SYSTEM_PROGRAM.md` (DS-4). |
|
package/dev/entries.jsx
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import
|
|
2
|
+
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
3
|
+
import {
|
|
4
|
+
Alert,
|
|
5
|
+
Box,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
Checkbox,
|
|
10
|
+
Chip,
|
|
11
|
+
Divider,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormControlLabel,
|
|
14
|
+
IconButton,
|
|
15
|
+
InputLabel,
|
|
16
|
+
MenuItem,
|
|
17
|
+
Paper,
|
|
18
|
+
Select,
|
|
19
|
+
Stack,
|
|
20
|
+
Table,
|
|
21
|
+
TableBody,
|
|
22
|
+
TableCell,
|
|
23
|
+
TableHead,
|
|
24
|
+
TableRow,
|
|
25
|
+
TextField,
|
|
26
|
+
Tooltip,
|
|
27
|
+
Typography,
|
|
28
|
+
} from '@mui/material';
|
|
3
29
|
|
|
4
30
|
import { AuthContext } from '../src/auth/AuthContext';
|
|
5
31
|
import { BarChart } from '../src/components/charts/BarChart';
|
|
6
32
|
import { ChartFrame } from '../src/components/charts/ChartFrame';
|
|
33
|
+
import { LineChart } from '../src/components/charts/LineChart';
|
|
7
34
|
import { TimeSeriesChart } from '../src/components/charts/TimeSeriesChart';
|
|
8
35
|
import { NotificationBell } from '../src/notifications/NotificationBell';
|
|
9
36
|
import { NotificationsProvider } from '../src/notifications/NotificationsProvider';
|
|
@@ -41,6 +68,144 @@ function BarChartEntry() {
|
|
|
41
68
|
);
|
|
42
69
|
}
|
|
43
70
|
|
|
71
|
+
function StatusChip({ label, tone }) {
|
|
72
|
+
return (
|
|
73
|
+
<Chip
|
|
74
|
+
label={label}
|
|
75
|
+
sx={{ bgcolor: `${tone}.fill`, color: `${tone}.fillText` }}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ThemeBaselineEntry() {
|
|
81
|
+
return (
|
|
82
|
+
<Stack spacing={3}>
|
|
83
|
+
<Box>
|
|
84
|
+
<Typography component="h1" variant="h4" gutterBottom>Shared theme baseline</Typography>
|
|
85
|
+
<Typography color="text.secondary">
|
|
86
|
+
Real MUI surfaces rendered through createAppTheme.
|
|
87
|
+
</Typography>
|
|
88
|
+
</Box>
|
|
89
|
+
|
|
90
|
+
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" alignItems="center">
|
|
91
|
+
<Button variant="contained">Contained</Button>
|
|
92
|
+
<Button variant="outlined">Outlined</Button>
|
|
93
|
+
<Button variant="text">Text</Button>
|
|
94
|
+
<Tooltip title="More actions">
|
|
95
|
+
<IconButton aria-label="More actions"><MoreVertIcon /></IconButton>
|
|
96
|
+
</Tooltip>
|
|
97
|
+
</Stack>
|
|
98
|
+
|
|
99
|
+
<Divider />
|
|
100
|
+
|
|
101
|
+
<Stack direction="column" spacing={2} sx={{ '@container (min-width: 900px)': { flexDirection: 'row' } }}>
|
|
102
|
+
<Paper sx={{ p: 2, flex: 1 }}>
|
|
103
|
+
<Typography variant="h6">Paper</Typography>
|
|
104
|
+
<Typography variant="body2" color="text.secondary">Outlined, with no resting shadow.</Typography>
|
|
105
|
+
</Paper>
|
|
106
|
+
<Card sx={{ flex: 1 }}>
|
|
107
|
+
<CardContent>
|
|
108
|
+
<Typography variant="h6">Card</Typography>
|
|
109
|
+
<Typography variant="body2" color="text.secondary">Eight-pixel surface radius.</Typography>
|
|
110
|
+
</CardContent>
|
|
111
|
+
</Card>
|
|
112
|
+
</Stack>
|
|
113
|
+
|
|
114
|
+
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
|
|
115
|
+
<Chip label="Default chip" variant="outlined" />
|
|
116
|
+
<StatusChip label="Success" tone="success" />
|
|
117
|
+
<StatusChip label="Warning" tone="warning" />
|
|
118
|
+
<StatusChip label="Critical" tone="error" />
|
|
119
|
+
<StatusChip label="Info" tone="info" />
|
|
120
|
+
<StatusChip label="Stale" tone="stale" />
|
|
121
|
+
</Stack>
|
|
122
|
+
|
|
123
|
+
<Stack direction="column" spacing={2} alignItems="flex-start" sx={{ '@container (min-width: 900px)': { flexDirection: 'row' } }}>
|
|
124
|
+
<TextField label="Project name" defaultValue="Baseline harness" fullWidth />
|
|
125
|
+
<FormControl fullWidth>
|
|
126
|
+
<InputLabel id="baseline-status-label">Status</InputLabel>
|
|
127
|
+
<Select labelId="baseline-status-label" label="Status" defaultValue="active">
|
|
128
|
+
<MenuItem value="active">Active</MenuItem>
|
|
129
|
+
<MenuItem value="stale">Stale</MenuItem>
|
|
130
|
+
</Select>
|
|
131
|
+
</FormControl>
|
|
132
|
+
<FormControlLabel control={<Checkbox defaultChecked />} label="Enabled" />
|
|
133
|
+
</Stack>
|
|
134
|
+
|
|
135
|
+
<Stack spacing={1}>
|
|
136
|
+
<Alert severity="success">Success status on its own tint.</Alert>
|
|
137
|
+
<Alert severity="warning">Warning status on its own tint.</Alert>
|
|
138
|
+
<Alert severity="error">Critical status on its own tint.</Alert>
|
|
139
|
+
<Alert severity="info">Info status using MUI's info hue family.</Alert>
|
|
140
|
+
<Alert icon={false} sx={{ bgcolor: 'stale.bg', color: 'stale.text' }}>Stale freshness status.</Alert>
|
|
141
|
+
</Stack>
|
|
142
|
+
|
|
143
|
+
<Paper sx={{ overflow: 'hidden' }}>
|
|
144
|
+
<Table size="small">
|
|
145
|
+
<TableHead><TableRow><TableCell>Application</TableCell><TableCell>Status</TableCell><TableCell align="right">Cases</TableCell></TableRow></TableHead>
|
|
146
|
+
<TableBody>
|
|
147
|
+
<TableRow><TableCell>hram</TableCell><TableCell>Healthy</TableCell><TableCell align="right">128</TableCell></TableRow>
|
|
148
|
+
<TableRow><TableCell>jg-ferien</TableCell><TableCell>Stale</TableCell><TableCell align="right">42</TableCell></TableRow>
|
|
149
|
+
</TableBody>
|
|
150
|
+
</Table>
|
|
151
|
+
</Paper>
|
|
152
|
+
|
|
153
|
+
<ChartFrame title="Series ramp" subtitle="Six categorical baseline colours" minHeight={360}>
|
|
154
|
+
<BarChart
|
|
155
|
+
xAxis={[{ scaleType: 'band', data: ['Baseline'] }]}
|
|
156
|
+
series={[
|
|
157
|
+
{ label: 'Series 1', data: [8] },
|
|
158
|
+
{ label: 'Series 2', data: [12] },
|
|
159
|
+
{ label: 'Series 3', data: [10] },
|
|
160
|
+
{ label: 'Series 4', data: [16] },
|
|
161
|
+
{ label: 'Series 5', data: [13] },
|
|
162
|
+
{ label: 'Series 6', data: [9] },
|
|
163
|
+
]}
|
|
164
|
+
xAxisLabel="Series"
|
|
165
|
+
yAxisLabel="Cases"
|
|
166
|
+
height={320}
|
|
167
|
+
/>
|
|
168
|
+
</ChartFrame>
|
|
169
|
+
|
|
170
|
+
<Stack
|
|
171
|
+
direction="column"
|
|
172
|
+
spacing={2}
|
|
173
|
+
sx={{ '@container (min-width: 900px)': { flexDirection: 'row' } }}
|
|
174
|
+
>
|
|
175
|
+
<ChartFrame title="Chart defaults" subtitle="No caller chrome props" minHeight={360}>
|
|
176
|
+
<LineChart
|
|
177
|
+
xAxis={[{ data: ['Jan', 'Feb', 'Mar', 'Apr'] }]}
|
|
178
|
+
series={[
|
|
179
|
+
{ label: 'Observed', data: [8, 12, 10, 16] },
|
|
180
|
+
{ label: 'Forecast', data: [7, 11, 13, 15] },
|
|
181
|
+
]}
|
|
182
|
+
xAxisLabel="Month"
|
|
183
|
+
yAxisLabel="Cases"
|
|
184
|
+
height={320}
|
|
185
|
+
/>
|
|
186
|
+
</ChartFrame>
|
|
187
|
+
<ChartFrame title="Caller overrides" subtitle="Tuned ticks, marks, grid and legend" minHeight={360}>
|
|
188
|
+
<LineChart
|
|
189
|
+
xAxis={[{
|
|
190
|
+
data: ['Jan', 'Feb', 'Mar', 'Apr'],
|
|
191
|
+
tickLabelStyle: { fontSize: 14, angle: -25 },
|
|
192
|
+
}]}
|
|
193
|
+
series={[
|
|
194
|
+
{ label: 'Observed', data: [8, 12, 10, 16], showMark: true },
|
|
195
|
+
{ label: 'Forecast', data: [7, 11, 13, 15], showMark: true },
|
|
196
|
+
]}
|
|
197
|
+
xAxisLabel="Month"
|
|
198
|
+
yAxisLabel="Cases"
|
|
199
|
+
grid={{ horizontal: true, vertical: true }}
|
|
200
|
+
legendPosition={{ vertical: 'top', horizontal: 'end' }}
|
|
201
|
+
height={320}
|
|
202
|
+
/>
|
|
203
|
+
</ChartFrame>
|
|
204
|
+
</Stack>
|
|
205
|
+
</Stack>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
44
209
|
function TimeSeriesChartEntry() {
|
|
45
210
|
// Real API response from the live jg-ferien Aktivität bug (1-week range,
|
|
46
211
|
// 4h granularity), pasted verbatim by the operator -- reproducing the
|
|
@@ -203,6 +368,7 @@ function PollCardEntry() { return <AuthContext.Provider value={{ user: { id: 1 }
|
|
|
203
368
|
function MessagingScopeConfigEntry() { return <MessagingProvider api={messagingHarnessApi} active={false}><MessagingScopeConfig conversationId={12} /></MessagingProvider>; }
|
|
204
369
|
|
|
205
370
|
export const entries = [
|
|
371
|
+
{ id: 'theme-baseline', label: 'Theme / Shared baseline', Component: ThemeBaselineEntry },
|
|
206
372
|
{ id: 'notification-bell', label: 'Notifications / Bell', Component: NotificationBellEntry },
|
|
207
373
|
{ id: 'bar-chart', label: 'Charts / BarChart', Component: BarChartEntry },
|
|
208
374
|
{ id: 'time-series-chart', label: 'Charts / TimeSeriesChart', Component: TimeSeriesChartEntry },
|
package/dev/main.jsx
CHANGED
|
@@ -8,6 +8,7 @@ import { Box, Button, ButtonGroup, CssBaseline, FormControl, InputLabel, MenuIte
|
|
|
8
8
|
import { chartsTranslations } from '../src/i18n/chartsTranslations';
|
|
9
9
|
import { notificationsTranslations } from '../src/i18n/notificationsTranslations';
|
|
10
10
|
import { messagingTranslations } from '../src/i18n/messagingTranslations';
|
|
11
|
+
import { createAppTheme } from '../src/theme/createAppTheme';
|
|
11
12
|
import { entries } from './entries';
|
|
12
13
|
import { MockTransportProvider } from './mockTransport';
|
|
13
14
|
|
|
@@ -29,7 +30,13 @@ function Harness() {
|
|
|
29
30
|
const [mode, setMode] = useState('light');
|
|
30
31
|
const [width, setWidth] = useState('desktop');
|
|
31
32
|
const entry = entries.find((item) => item.id === entryId) || entries[0];
|
|
32
|
-
const
|
|
33
|
+
const effectiveMode = entry.id === 'theme-baseline' ? 'light' : mode;
|
|
34
|
+
const theme = useMemo(
|
|
35
|
+
() => entry.id === 'theme-baseline'
|
|
36
|
+
? createAppTheme({ palette: { primary: { main: '#3D5A99' } } })
|
|
37
|
+
: createTheme({ palette: { mode } }),
|
|
38
|
+
[entry.id, mode],
|
|
39
|
+
);
|
|
33
40
|
const viewportWidth = width === 'mobile' ? 390 : width === 'tablet' ? 768 : 1180;
|
|
34
41
|
const Entry = entry.Component;
|
|
35
42
|
|
|
@@ -40,10 +47,10 @@ function Harness() {
|
|
|
40
47
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ mb: 3 }} alignItems={{ md: 'center' }}>
|
|
41
48
|
<Typography variant="h5" sx={{ mr: 'auto' }}>ui-core-micha harness</Typography>
|
|
42
49
|
<FormControl size="small" sx={{ minWidth: 220 }}><InputLabel id="entry-label">Entry</InputLabel><Select labelId="entry-label" label="Entry" value={entryId} onChange={(event) => setEntryId(event.target.value)}>{entries.map((item) => <MenuItem key={item.id} value={item.id}>{item.label}</MenuItem>)}</Select></FormControl>
|
|
43
|
-
<ButtonGroup size="small"><Button variant={
|
|
50
|
+
<ButtonGroup size="small"><Button variant={effectiveMode === 'light' ? 'contained' : 'outlined'} onClick={() => setMode('light')}>Light</Button><Button disabled={entry.id === 'theme-baseline'} variant={effectiveMode === 'dark' ? 'contained' : 'outlined'} onClick={() => setMode('dark')}>Dark</Button></ButtonGroup>
|
|
44
51
|
<ButtonGroup size="small">{['mobile', 'tablet', 'desktop'].map((size) => <Button key={size} variant={width === size ? 'contained' : 'outlined'} onClick={() => setWidth(size)}>{size}</Button>)}</ButtonGroup>
|
|
45
52
|
</Stack>
|
|
46
|
-
<Paper variant="outlined" sx={{ width: viewportWidth, maxWidth: '100%', mx: 'auto', p: 3, overflow: 'hidden' }}>
|
|
53
|
+
<Paper variant="outlined" sx={{ width: viewportWidth, maxWidth: '100%', mx: 'auto', p: 3, overflow: 'hidden', containerType: 'inline-size' }}>
|
|
47
54
|
<Entry />
|
|
48
55
|
</Paper>
|
|
49
56
|
</Box>
|
|
@@ -12,21 +12,23 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Box } from '@mui/material';
|
|
15
|
+
import { useTheme } from '@mui/material/styles';
|
|
15
16
|
import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
|
|
16
17
|
import { useNeutralChartPalette } from './palette';
|
|
17
|
-
|
|
18
|
-
const values = (axes === null || axes === void 0 ? void 0 : axes.length) ? axes : [defaults];
|
|
19
|
-
return values.map((axis) => (Object.assign(Object.assign({}, axis), { label: axis.label || label })));
|
|
20
|
-
}
|
|
18
|
+
import { DEFAULT_LEGEND_POSITION, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, } from './chartDefaults';
|
|
21
19
|
/**
|
|
22
20
|
* Responsive MUI X-Charts bar preset. xAxisLabel and yAxisLabel (including units) are required.
|
|
23
21
|
* It supplies labelled axes, tooltips, and an automatic multi-series legend.
|
|
24
22
|
*/
|
|
25
23
|
export function BarChart(_a) {
|
|
26
|
-
var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect"]);
|
|
24
|
+
var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect, grid = { horizontal: true }, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slotProps"]);
|
|
25
|
+
const theme = useTheme();
|
|
27
26
|
const neutralPalette = useNeutralChartPalette();
|
|
28
27
|
if (!xAxisLabel || !yAxisLabel) {
|
|
29
28
|
throw new Error('BarChart requires xAxisLabel and yAxisLabel.');
|
|
30
29
|
}
|
|
31
|
-
|
|
30
|
+
const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'band', data: [] }, theme.typography.caption.fontSize);
|
|
31
|
+
const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear' }, theme.typography.caption.fontSize);
|
|
32
|
+
const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
|
|
33
|
+
return (_jsx(Box, { "data-testid": "bar-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { series: series, xAxis: rotatedTickSpace.xAxis, yAxis: labelledYAxis, colors: palette || neutralPalette.categorical, grid: grid, hideLegend: hideLegend, margin: rotatedTickSpace.margin, slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
|
|
32
34
|
}
|
|
@@ -12,21 +12,38 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Box } from '@mui/material';
|
|
15
|
-
import {
|
|
15
|
+
import { useTheme } from '@mui/material/styles';
|
|
16
|
+
import { LineChart as MuiLineChart, MarkElement as MuiMarkElement, } from '@mui/x-charts/LineChart';
|
|
16
17
|
import { useNeutralChartPalette } from './palette';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import { DEFAULT_LEGEND_POSITION, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, } from './chartDefaults';
|
|
19
|
+
// Fixes MUI's hollow `background.paper`-filled marker (MarkElement.js's own
|
|
20
|
+
// `styled()` default). CHART-6's WO named a `MuiMarkElement` theme-token
|
|
21
|
+
// override in src/theme/tokens.js as the preferred mechanism, with a
|
|
22
|
+
// per-wrapper slots/sx override as the documented fallback. This uses the
|
|
23
|
+
// fallback deliberately, not because the theme route was infeasible: a
|
|
24
|
+
// wrapper-level fix applies to every consumer immediately on this version
|
|
25
|
+
// bump, including the 14 apps that haven't adopted `createAppTheme` yet
|
|
26
|
+
// (THEME-1 is a separate, optional pin bump) -- a theme-token override would
|
|
27
|
+
// only take effect for apps on the new baseline theme. Composes correctly
|
|
28
|
+
// with a caller's own `slots.mark` (spread after this default, so it wins).
|
|
29
|
+
function FilledMarkElement(_a) {
|
|
30
|
+
var { color, style } = _a, props = __rest(_a, ["color", "style"]);
|
|
31
|
+
return _jsx(MuiMarkElement, Object.assign({}, props, { color: color, style: Object.assign({ fill: color }, style) }));
|
|
20
32
|
}
|
|
21
33
|
/**
|
|
22
34
|
* Responsive MUI X-Charts line preset. xAxisLabel and yAxisLabel (including units) are required.
|
|
23
35
|
* It supplies labelled axes, tooltips, and an automatic multi-series legend.
|
|
24
36
|
*/
|
|
25
37
|
export function LineChart(_a) {
|
|
26
|
-
var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect"]);
|
|
38
|
+
var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect, grid = { horizontal: true }, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slots, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slots", "slotProps"]);
|
|
39
|
+
const theme = useTheme();
|
|
27
40
|
const neutralPalette = useNeutralChartPalette();
|
|
28
41
|
if (!xAxisLabel || !yAxisLabel) {
|
|
29
42
|
throw new Error('LineChart requires xAxisLabel and yAxisLabel.');
|
|
30
43
|
}
|
|
31
|
-
|
|
44
|
+
const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'point', data: [] }, theme.typography.caption.fontSize);
|
|
45
|
+
const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear' }, theme.typography.caption.fontSize);
|
|
46
|
+
const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
|
|
47
|
+
const defaultedSeries = series.map((item) => (Object.assign({ showMark: false, labelMarkType: 'square' }, item)));
|
|
48
|
+
return (_jsx(Box, { "data-testid": "line-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { series: defaultedSeries, xAxis: rotatedTickSpace.xAxis, yAxis: labelledYAxis, colors: palette || neutralPalette.categorical, grid: grid, hideLegend: hideLegend, margin: rotatedTickSpace.margin, slots: Object.assign({ mark: FilledMarkElement }, slots), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
|
|
32
49
|
}
|
|
@@ -66,6 +66,15 @@ const RANGE_OPTIONS = [
|
|
|
66
66
|
{ key: '1m', granularity: 'day', labelKey: 'TimeSeriesChart.RANGE_1_MONTH' },
|
|
67
67
|
{ key: '1y', granularity: 'month', labelKey: 'TimeSeriesChart.RANGE_1_YEAR' },
|
|
68
68
|
];
|
|
69
|
+
function SeriesToggleDot({ color, active }) {
|
|
70
|
+
return (_jsx(Box, { component: "span", "data-testid": "series-colour-dot", sx: {
|
|
71
|
+
width: (theme) => theme.spacing(1.5),
|
|
72
|
+
height: (theme) => theme.spacing(1.5),
|
|
73
|
+
borderRadius: '50%',
|
|
74
|
+
bgcolor: color,
|
|
75
|
+
opacity: active ? 1 : 0.35,
|
|
76
|
+
} }));
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* A time-series preset on ChartFrame + BarChart: a range picker (emits the
|
|
71
80
|
* selected range AND its mapped granularity to the host) and series toggle
|
|
@@ -81,7 +90,7 @@ const RANGE_OPTIONS = [
|
|
|
81
90
|
* (requires `secondaryYAxisLabel`). Omit `axis` (or set it to `'primary'`)
|
|
82
91
|
* on every series for today's single-shared-axis behaviour, unchanged.
|
|
83
92
|
*/
|
|
84
|
-
export function TimeSeriesChart({ title, subtitle, xAxisLabel, yAxisLabel, secondaryYAxisLabel, data, loading = false, error = false, onRangeChange, defaultRange = '1w', }) {
|
|
93
|
+
export function TimeSeriesChart({ title, subtitle, xAxisLabel, yAxisLabel, secondaryYAxisLabel, data, loading = false, error = false, onRangeChange, defaultRange = '1w', skipAnimation, }) {
|
|
85
94
|
const { t } = useTranslation();
|
|
86
95
|
const palette = useNeutralChartPalette().categorical;
|
|
87
96
|
const seriesConfig = (data === null || data === void 0 ? void 0 : data.series) || [];
|
|
@@ -153,13 +162,13 @@ export function TimeSeriesChart({ title, subtitle, xAxisLabel, yAxisLabel, secon
|
|
|
153
162
|
Object.assign({ id: 'secondary', label: secondaryYAxisLabel, position: 'right' }, (isIntegerAxis(secondaryVisibleSeries) ? { valueFormatter: integerTickFormatter } : {})),
|
|
154
163
|
]
|
|
155
164
|
: (isIntegerAxis(primaryVisibleSeries) ? [{ valueFormatter: integerTickFormatter }] : undefined);
|
|
156
|
-
const toolbar = (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }, children: [_jsx(RadioGroup, { row: true, value: range, onChange: handleRangeChange, "aria-label": t('TimeSeriesChart.RANGE_LABEL'), children: RANGE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.key, control: _jsx(Radio, { size: "small" }), label: t(option.labelKey) }, option.key))) }), seriesConfig.length > 0 && (_jsx(FormGroup, { row: true, "aria-label": t('TimeSeriesChart.SERIES_LABEL'), children: seriesConfig.map((series, index) => (_jsx(FormControlLabel, { control: (_jsx(Checkbox, { size: "small", checked: visibleKeys.has(series.key), onChange: () => toggleSeries(series.key), sx: {
|
|
165
|
+
const toolbar = (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }, children: [_jsx(RadioGroup, { row: true, value: range, onChange: handleRangeChange, "aria-label": t('TimeSeriesChart.RANGE_LABEL'), children: RANGE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.key, control: _jsx(Radio, { size: "small" }), label: t(option.labelKey) }, option.key))) }), seriesConfig.length > 0 && (_jsx(FormGroup, { row: true, "aria-label": t('TimeSeriesChart.SERIES_LABEL'), children: seriesConfig.map((series, index) => (_jsx(FormControlLabel, { control: (_jsx(Checkbox, { size: "small", checked: visibleKeys.has(series.key), onChange: () => toggleSeries(series.key), icon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: false }), checkedIcon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: true }), sx: {
|
|
157
166
|
color: palette[index % palette.length],
|
|
158
167
|
'&.Mui-checked': { color: palette[index % palette.length] },
|
|
159
168
|
} })), label: series.label }, series.key))) }))] }));
|
|
160
169
|
return (_jsx(ChartFrame, { title: title, subtitle: subtitle, toolbar: toolbar, loading: loading, error: error, isEmpty: isDataEmpty, minHeight: CHART_HEIGHT, children: !isDataEmpty && (_jsx(BarChart, { xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xAxis: [{
|
|
161
170
|
data: (data === null || data === void 0 ? void 0 : data.xLabels) || [],
|
|
162
171
|
tickLabelInterval: makeTickLabelInterval(((data === null || data === void 0 ? void 0 : data.xLabels) || []).length),
|
|
163
|
-
}], yAxis: yAxis, series: visibleSeries.map((series) => (Object.assign({ data: series.data, label: series.label }, (hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {})))), palette: visiblePalette, height: CHART_HEIGHT })) }));
|
|
172
|
+
}], yAxis: yAxis, series: visibleSeries.map((series) => (Object.assign({ data: series.data, label: series.label }, (hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {})))), palette: visiblePalette, height: CHART_HEIGHT, hideLegend: seriesConfig.length > 0, skipAnimation: skipAnimation })) }));
|
|
164
173
|
}
|
|
165
174
|
export default TimeSeriesChart;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const DEFAULT_LEGEND_POSITION = {
|
|
2
|
+
vertical: 'bottom',
|
|
3
|
+
horizontal: 'start',
|
|
4
|
+
};
|
|
5
|
+
export function withAxisDefaults(axes, label, defaults, tickFontSize) {
|
|
6
|
+
const values = (axes === null || axes === void 0 ? void 0 : axes.length) ? axes : [defaults];
|
|
7
|
+
return values.map((axis) => (Object.assign(Object.assign(Object.assign({}, defaults), axis), { tickLabelStyle: Object.assign({ fontSize: tickFontSize }, axis.tickLabelStyle), label: axis.label || label })));
|
|
8
|
+
}
|
|
9
|
+
function cssLengthToPixels(value) {
|
|
10
|
+
if (typeof value === 'number')
|
|
11
|
+
return value;
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return 0;
|
|
14
|
+
const parsed = Number.parseFloat(value);
|
|
15
|
+
if (!Number.isFinite(parsed))
|
|
16
|
+
return 0;
|
|
17
|
+
if (value.endsWith('rem') || value.endsWith('em'))
|
|
18
|
+
return parsed * 16;
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
const MUI_LABELLED_X_AXIS_HEIGHT = 45;
|
|
22
|
+
const MUI_CHART_MARGIN_BOTTOM = 20;
|
|
23
|
+
const AVERAGE_GLYPH_WIDTH_EM = 0.6;
|
|
24
|
+
function formattedTickText(axis, value) {
|
|
25
|
+
if (!axis.valueFormatter)
|
|
26
|
+
return String(value);
|
|
27
|
+
try {
|
|
28
|
+
return String(axis.valueFormatter(value, { location: 'tick' }));
|
|
29
|
+
}
|
|
30
|
+
catch (_a) {
|
|
31
|
+
// Some formatters require MUI's complete scale context. Falling back to
|
|
32
|
+
// the raw value is conservative: it reserves at least as much space.
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function rotatedTickMetrics(axis, defaultLineHeight) {
|
|
37
|
+
var _a, _b, _c;
|
|
38
|
+
const angle = Number((_a = axis.tickLabelStyle) === null || _a === void 0 ? void 0 : _a.angle) || 0;
|
|
39
|
+
if (angle === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const fontSize = cssLengthToPixels((_b = axis.tickLabelStyle) === null || _b === void 0 ? void 0 : _b.fontSize);
|
|
42
|
+
if (fontSize === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const lineHeight = Number((_c = axis.tickLabelStyle) === null || _c === void 0 ? void 0 : _c.lineHeight) || defaultLineHeight;
|
|
45
|
+
const longestLabel = (axis.data || []).reduce((longest, value) => {
|
|
46
|
+
const text = formattedTickText(axis, value);
|
|
47
|
+
return text.length > longest.length ? text : longest;
|
|
48
|
+
}, '');
|
|
49
|
+
const textWidth = Math.max(fontSize, longestLabel.length * fontSize * AVERAGE_GLYPH_WIDTH_EM);
|
|
50
|
+
const radians = (angle * Math.PI) / 180;
|
|
51
|
+
const projectedHeight = Math.abs(Math.cos(radians)) * fontSize * lineHeight
|
|
52
|
+
+ Math.abs(Math.sin(radians)) * textWidth;
|
|
53
|
+
const extraHeight = Math.max(1, Math.ceil(projectedHeight - fontSize));
|
|
54
|
+
return { extraHeight };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Rotated labels project both their line box and text width vertically.
|
|
58
|
+
* X-Charts uses xAxis.height when deciding whether a label fits, while margin
|
|
59
|
+
* only separates the axis from the SVG edge, so both surfaces must grow.
|
|
60
|
+
* Explicit caller height and bottom-margin values always remain untouched.
|
|
61
|
+
*/
|
|
62
|
+
export function spaceForRotatedTicks(xAxis, margin, defaultLineHeight = 1) {
|
|
63
|
+
let largestExtraHeight = 0;
|
|
64
|
+
const spacedXAxis = xAxis.map((axis) => {
|
|
65
|
+
const metrics = rotatedTickMetrics(axis, defaultLineHeight);
|
|
66
|
+
if (!metrics)
|
|
67
|
+
return axis;
|
|
68
|
+
largestExtraHeight = Math.max(largestExtraHeight, metrics.extraHeight);
|
|
69
|
+
if (axis.height != null)
|
|
70
|
+
return axis;
|
|
71
|
+
return Object.assign(Object.assign({}, axis), { height: MUI_LABELLED_X_AXIS_HEIGHT + metrics.extraHeight });
|
|
72
|
+
});
|
|
73
|
+
const callerSetBottom = typeof margin === 'number' || (margin === null || margin === void 0 ? void 0 : margin.bottom) != null;
|
|
74
|
+
const spacedMargin = largestExtraHeight > 0 && !callerSetBottom
|
|
75
|
+
? Object.assign(Object.assign({}, margin), { bottom: MUI_CHART_MARGIN_BOTTOM + largestExtraHeight }) : margin;
|
|
76
|
+
return { xAxis: spacedXAxis, margin: spacedMargin };
|
|
77
|
+
}
|
|
78
|
+
export function withChartSlotDefaults(slotProps, legendPosition) {
|
|
79
|
+
return Object.assign(Object.assign({}, slotProps), { tooltip: Object.assign({ trigger: 'axis' }, slotProps === null || slotProps === void 0 ? void 0 : slotProps.tooltip), legend: Object.assign({ position: legendPosition }, slotProps === null || slotProps === void 0 ? void 0 : slotProps.legend) });
|
|
80
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Short, granularity-aware x-axis tick labels.
|
|
2
|
+
//
|
|
3
|
+
// Not a style choice -- MUI X-Charts has a known, unfixed bug (mui-x#18768,
|
|
4
|
+
// duplicate of #18399): when the measured space for a tick label gets
|
|
5
|
+
// tight, its ellipsize() logic doesn't truncate the text, it collapses it
|
|
6
|
+
// to an empty string (the <tspan> stays in the DOM with no content). It
|
|
7
|
+
// only reproduces with long label text ("08/05, 10:00 AM"-style, ~16
|
|
8
|
+
// characters) combined with this app's actual font metrics (DM Sans) --
|
|
9
|
+
// short labels like "05.08." or "10:00" leave enough margin that the bug
|
|
10
|
+
// never triggers. Same fix already proven in jg-ferien's ActivitySection.
|
|
11
|
+
// `locale` defaults to "de-CH" only as a fallback for callers that don't
|
|
12
|
+
// pass the app's active i18n language (found in review: an earlier version
|
|
13
|
+
// hardcoded "de-CH" always, showing German month names to en/fr users).
|
|
14
|
+
export function formatShortTime(date, locale = 'de-CH') {
|
|
15
|
+
// hour12 forced false regardless of locale -- an English 12-hour label
|
|
16
|
+
// ("10:00 AM") is exactly the longer style that triggered the original
|
|
17
|
+
// collapse bug; staying short is the point, not locale fidelity here.
|
|
18
|
+
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
19
|
+
}
|
|
20
|
+
export function formatShortDate(date, locale = 'de-CH') {
|
|
21
|
+
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit' });
|
|
22
|
+
}
|
|
23
|
+
export function formatShortMonth(date, locale = 'de-CH') {
|
|
24
|
+
return date.toLocaleDateString(locale, { month: 'short', year: '2-digit' });
|
|
25
|
+
}
|
|
26
|
+
// "All" spans years -- day/month labels repeat (e.g. every "01.04." shows up
|
|
27
|
+
// once per year) and read as meaningless noise; a bare year number is the
|
|
28
|
+
// only granularity dense multi-year history can show cleanly.
|
|
29
|
+
export function formatShortYear(date, locale = 'de-CH') {
|
|
30
|
+
return date.toLocaleDateString(locale, { year: 'numeric' });
|
|
31
|
+
}
|