@micha.bigler/ui-core-micha 2.40.0 → 2.41.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.
- package/DESIGN.md +1 -0
- package/WORK_ORDERS.md +3 -0
- package/dev/entries.jsx +158 -0
- package/dist/auth/apiClient.js +34 -4
- package/dist/components/charts/BarChart.js +2 -2
- package/dist/components/charts/ChartFrame.js +1 -1
- package/dist/components/charts/LineChart.js +2 -2
- package/dist/components/charts/ScatterChart.js +178 -0
- package/dist/components/charts/chartDefaults.js +30 -1
- package/dist/components/charts/palette.js +6 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/auth/apiClient.jsx +33 -4
- package/src/components/charts/BarChart.jsx +2 -1
- package/src/components/charts/ChartFrame.jsx +3 -1
- package/src/components/charts/LineChart.jsx +2 -1
- package/src/components/charts/ScatterChart.jsx +277 -0
- package/src/components/charts/chartDefaults.js +32 -1
- package/src/components/charts/palette.js +6 -0
- package/src/index.js +6 -0
- package/tests/ScatterChart.test.jsx +234 -0
- package/tests/apiClientPublicPaths.test.js +178 -0
- package/tests/chartDefaults.test.js +75 -3
- package/tests/chartsPalette.test.js +20 -0
- package/work-orders/AUTH-5.md +154 -0
- package/work-orders/THEME-10.md +303 -0
- package/work-orders/THEME-11.md +208 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# AUTH-5 — `addPublicPath` muss dynamische Routen ausdrücken können
|
|
2
|
+
|
|
3
|
+
**Target repo:** `ui-core-micha` (branch `main`)
|
|
4
|
+
**Tier:** 3 — shared-core. Die Änderung betrifft die Login-Weiterleitung jeder konsumierenden App.
|
|
5
|
+
|
|
6
|
+
**Kein Prototyp.** Diese Work Order hat keine Oberfläche — sie erweitert eine Registrierungs-API.
|
|
7
|
+
|
|
8
|
+
**Auslöser:** `jg-ferien` PUB-7 Teil 2. Diese WO ist dessen Vorbedingung; nach der Veröffentlichung
|
|
9
|
+
zieht jg seinen Pin von **`2.37.1`** hoch.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## A. Envelope
|
|
14
|
+
|
|
15
|
+
### Goal
|
|
16
|
+
|
|
17
|
+
`apiClient` leitet bei 401/403 per hartem `window.location.assign("/login")` um — **ausser** der
|
|
18
|
+
aktuelle Pfad steht auf einer Allowlist (`redirectToLoginOnce`, `src/auth/apiClient.jsx`). Der
|
|
19
|
+
Mechanismus ist richtig gebaut: auf einer öffentlichen Seite unterbleibt die Weiterleitung und der
|
|
20
|
+
401 wird an die Komponente durchgereicht.
|
|
21
|
+
|
|
22
|
+
**Was er nicht kann, ist dynamische Routen.** `matchesPublicPath` vergleicht mit `startsWith`, ausser
|
|
23
|
+
bei `/`, wo Gleichheit verlangt wird. Eine App mit öffentlichen Seiten unter einem **variablen
|
|
24
|
+
Segment** — `/<slug>`, `/<slug>/team` — kann ihre öffentliche Fläche damit nicht beschreiben. Ein
|
|
25
|
+
konkreter Slug lässt sich eintragen; der nächste, den jemand anlegt, fällt wieder heraus.
|
|
26
|
+
|
|
27
|
+
**Warum das mehr ist als eine Unbequemlichkeit.** Fehlt der Pfad in der Liste, landet ein anonymer
|
|
28
|
+
Besucher beim ersten 401 auf `/login` — auf einer Seite, die öffentlich sein soll. Von aussen ist das
|
|
29
|
+
nicht von „diese Seite gibt es nicht" zu unterscheiden, und der Fehler zeigt sich erst, wenn ein
|
|
30
|
+
zweites Department existiert. Er sieht dann aus wie ein Zufall, nicht wie eine Lücke.
|
|
31
|
+
|
|
32
|
+
**Erwartetes Ergebnis.** Eine konsumierende App kann ihre öffentliche Fläche vollständig beschreiben,
|
|
33
|
+
auch wenn Teile davon aus variablen Segmenten bestehen.
|
|
34
|
+
|
|
35
|
+
### Scope
|
|
36
|
+
|
|
37
|
+
- **`addPublicPath` nimmt zusätzlich ein `RegExp` entgegen** (String bleibt unverändert gültig).
|
|
38
|
+
- **`matchesPublicPath` unterscheidet nach Typ**: String wie bisher (`/` exakt, sonst `startsWith`),
|
|
39
|
+
`RegExp` per `test()` gegen **ausschliesslich `window.location.pathname`** — nie gegen Query oder
|
|
40
|
+
Fragment, damit ein Muster nicht versehentlich über den Pfad hinaus greift.
|
|
41
|
+
- **`removePublicPath` funktioniert für beide Formen.** Bei `RegExp` über Identität des übergebenen
|
|
42
|
+
Objekts; das gehört dokumentiert, weil zwei gleich aussehende Literale nicht dasselbe Objekt sind.
|
|
43
|
+
- **`BUILTIN_PUBLIC_PATHS` bleibt eingefroren und rein string-basiert.** Kein Muster in der
|
|
44
|
+
Bibliotheks-Liste — sie darf sich nicht durch eine Konsumenten-Erweiterung verschieben.
|
|
45
|
+
- **Die Dokumentation am Aufruf sagt, was der Konsument nun selbst trägt**: ein zu weites Muster
|
|
46
|
+
unterdrückt die Weiterleitung dort, wo sie gebraucht wird, und ein abgelaufener Login endet dann in
|
|
47
|
+
einer stillen Fehlerseite statt am Login. Muster gehören verankert (`^…$`).
|
|
48
|
+
|
|
49
|
+
### Non-goals / do not touch
|
|
50
|
+
|
|
51
|
+
- **Keine Änderung am Weiterleitungsverhalten selbst** — weder Ziel, noch `?next=`, noch der
|
|
52
|
+
einmalige Sperrmechanismus (`redirectingToLogin`).
|
|
53
|
+
- **Keine Änderung an der eingefrorenen Builtin-Liste** und keine an `isPublicSitePath`.
|
|
54
|
+
- **Keine Änderung am Per-Request-Opt-out** für Bootstrap-Proben.
|
|
55
|
+
- **Keine neue Prüfung gegen Query oder Fragment.**
|
|
56
|
+
|
|
57
|
+
### Risks
|
|
58
|
+
|
|
59
|
+
- **Die Verantwortung wandert zum Konsumenten.** Ein unverankertes Muster wie `/team` trifft auch
|
|
60
|
+
`/admin/team`. Das ist der Preis der Flexibilität und muss in der Dokumentation stehen, nicht nur
|
|
61
|
+
im Kopf des Autors.
|
|
62
|
+
- **Geteilter Code**: jede konsumierende App bekommt die Änderung. Deshalb rein additiv — bestehende
|
|
63
|
+
String-Registrierungen müssen sich zeichengenau gleich verhalten.
|
|
64
|
+
- **Ein Muster, das zu eng greift, ist unsichtbar**, bis jemand die betroffene Seite anonym öffnet.
|
|
65
|
+
Deshalb gehört die Gegenrichtung mitgetestet.
|
|
66
|
+
|
|
67
|
+
### Required tests to WRITE
|
|
68
|
+
|
|
69
|
+
- **Regression, nicht verhandelbar:** String-Registrierungen verhalten sich exakt wie bisher — `/`
|
|
70
|
+
nur bei Gleichheit, andere Einträge per Präfix.
|
|
71
|
+
- Ein registriertes `RegExp` trifft die passenden Pfade; ein nicht passender Pfad **löst weiterhin
|
|
72
|
+
die Weiterleitung aus**.
|
|
73
|
+
- Das Muster wird gegen `pathname` geprüft, nicht gegen Query oder Fragment.
|
|
74
|
+
- `removePublicPath` entfernt einen `RegExp`-Eintrag; ein Builtin-Pfad lässt sich weiterhin **nicht**
|
|
75
|
+
entfernen.
|
|
76
|
+
- Ein `RegExp` in `CONSUMER_PUBLIC_PATHS` bricht die bestehende Auswertung der String-Einträge nicht.
|
|
77
|
+
- **Affected set** (laufen, nicht schreiben): die vorhandenen `apiClient`-Tests.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## B. Implementation map
|
|
82
|
+
|
|
83
|
+
**Selbst umgesetzt (nicht per Codex)** — laufende Operator-Anweisung für diese Session.
|
|
84
|
+
|
|
85
|
+
### Design-Entscheidungen
|
|
86
|
+
|
|
87
|
+
- **`addPublicPath`/`removePublicPath`** akzeptieren jetzt zusätzlich `instanceof RegExp` neben
|
|
88
|
+
`string`; `CONSUMER_PUBLIC_PATHS` bleibt ein `Set`, das für `RegExp`-Objekte automatisch
|
|
89
|
+
Identitäts-Löschung liefert (`Set.delete` nutzt SameValueZero) — kein Sonderfall nötig.
|
|
90
|
+
- **`matchesPublicPath`** prüft `entry instanceof RegExp` zuerst und ruft `entry.test(pathname)` —
|
|
91
|
+
ausschliesslich gegen `window.location.pathname`, nie gegen `search`/`hash`, da die Funktion
|
|
92
|
+
ohnehin nur `pathname` als Parameter erhält (kein Risiko einer versehentlichen Konkatenation).
|
|
93
|
+
- **`BUILTIN_PUBLIC_PATHS` unverändert** — bleibt string-only und eingefroren, keine Berührung.
|
|
94
|
+
- Dokumentation an `addPublicPath`/`removePublicPath` erweitert: Anker-Pflicht (`^…$`) für
|
|
95
|
+
`RegExp`-Muster und Identitäts-Hinweis für `removePublicPath`.
|
|
96
|
+
|
|
97
|
+
### Kontextpaket
|
|
98
|
+
|
|
99
|
+
- `src/auth/apiClient.jsx` — einzige geänderte Quelldatei.
|
|
100
|
+
|
|
101
|
+
### Tests
|
|
102
|
+
|
|
103
|
+
`tests/apiClientPublicPaths.test.js` (neu, 11 Fälle) — fährt durch den echten
|
|
104
|
+
Response-Interceptor (nicht durch eine Nachbildung von `matchesPublicPath`), damit ein Fehler in
|
|
105
|
+
der Verdrahtung selbst auffällt, nicht nur ein Fehler im Matcher. Jeder Test importiert das Modul
|
|
106
|
+
frisch (`vi.resetModules()` + dynamisches `import()`), weil `redirectingToLogin` ein
|
|
107
|
+
Modul-Level-Riegel ist, der sich in jsdom nie durch echte Navigation zurücksetzt.
|
|
108
|
+
`window.location` wird durch ein rein synthetisches Objekt ersetzt (jsdoms echtes
|
|
109
|
+
`location.assign` ist nicht konfigurierbar, und `pushState` bindet nicht an ein ersetztes
|
|
110
|
+
`location`-Objekt) — deckt exakt das ab, was der Interceptor liest (`pathname`) und aufruft
|
|
111
|
+
(`assign`). Affected set: `authApiRegistration.test.js`, `AuthContext.test.jsx`,
|
|
112
|
+
`feedApi.test.js`, `messagingApi.test.js`, `notificationsApi.test.js` — alle grün.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## C. Orchestrator only
|
|
117
|
+
|
|
118
|
+
> **STOPP — wer diesen Auftrag als eigene Spezifikation liest, ist mit diesem Abschnitt nicht
|
|
119
|
+
> gemeint.** Er beschreibt dem Orchestrator, wie er dich aufruft, wie das Ergebnis geprüft und wie es
|
|
120
|
+
> verbucht wird. **Du BIST dieser Aufruf — starte kein `codex exec`, spawne keine Reviewer, pflege
|
|
121
|
+
> das Register nicht, committe nicht.** Deine Aufgabe endet beim Diff.
|
|
122
|
+
|
|
123
|
+
### Execution directive
|
|
124
|
+
|
|
125
|
+
`.claude/codex-status.md` im Workspace-Root vor dem Aufruf prüfen. Ohne Zeile für den
|
|
126
|
+
Ausführungstag Codex verwenden — direkt per Bash, nie über die `debugger`/`*_coder`-Wrapper, mit
|
|
127
|
+
BEIDEN Flags `--skip-git-repo-check` und `--dangerously-bypass-approvals-and-sandbox`. Der Rückfall
|
|
128
|
+
auf direkte Umsetzung dreht die Autorschaft und macht den unabhängigen `reviewer` zwingend.
|
|
129
|
+
|
|
130
|
+
### Review routing
|
|
131
|
+
|
|
132
|
+
Tier 3 (shared-core): **`reviewer` und `sec_reviewer`**, gleichzeitig in einem Hintergrund-Batch vor
|
|
133
|
+
dem Commit. `ui_reviewer` entfällt — es gibt keine Oberfläche. `sec_reviewer` prüft die Kernfrage:
|
|
134
|
+
kann ein registriertes Muster die Weiterleitung dort unterdrücken, wo sie gebraucht wird, und ist die
|
|
135
|
+
Auswertung auf `pathname` beschränkt.
|
|
136
|
+
|
|
137
|
+
### Verification
|
|
138
|
+
|
|
139
|
+
Kein Prototyp, kein Zwei-Breiten-Abgleich. Der Nachweis sind die Tests, insbesondere die
|
|
140
|
+
Regressionsfälle für die String-Registrierung.
|
|
141
|
+
|
|
142
|
+
Nach grünem Review: Version anheben und veröffentlichen. **Die WO ist erst abgeschlossen, wenn die
|
|
143
|
+
Version veröffentlicht ist** — jg-ferien PUB-7 hängt daran und kann ohne sie nicht landen.
|
|
144
|
+
|
|
145
|
+
### Register + commit
|
|
146
|
+
|
|
147
|
+
Zeile `AUTH-5` erreicht `done` nur mit beiden Reviewern samt Verdikt in der `Notiz` und der
|
|
148
|
+
veröffentlichten Version. Auf grün nach `main` committen und pushen.
|
|
149
|
+
|
|
150
|
+
### Mini-handover
|
|
151
|
+
|
|
152
|
+
Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), Branch `main`.
|
|
153
|
+
WO: `work-orders/AUTH-5.md`. Konsument: `jg-ferien` PUB-7 Teil 2 (Pin heute `2.37.1`).
|
|
154
|
+
`git pull`, WO lesen, dann `orchestrate-codex` folgen.
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# THEME-10 — A scatter preset, drawn from three real consumers
|
|
2
|
+
|
|
3
|
+
## Part A — Envelope
|
|
4
|
+
|
|
5
|
+
**Goal.** Add a `ScatterChart` preset alongside `BarChart` / `LineChart` / `TimeSeriesChart`,
|
|
6
|
+
with the same axis handling, tick formatting, legend and frame behaviour, so a consuming app
|
|
7
|
+
can plot a point cloud without building its own chart from raw SVG.
|
|
8
|
+
|
|
9
|
+
**Why.** hram draws **three** scatters entirely by hand — its own axis lines, gridlines, ticks,
|
|
10
|
+
tick labels and rotated axis titles, on hard-coded canvases. Only `ChartFrame` comes from this
|
|
11
|
+
package. That is not negligence: **this package has no scatter preset**, so there was nothing
|
|
12
|
+
to reach for. Every fix this kit has made since (axis sizing, tick formatting, label placement,
|
|
13
|
+
the role palette) passes all three by, and will keep doing so until a preset exists.
|
|
14
|
+
|
|
15
|
+
| Consumer | Canvas | Composition |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `AllocationPerformancePanel` | `WIDTH 720`, `HEIGHT 420`, `MARGIN.left 92` | cloud + computed envelope curve + three individually marked points; continuous per-point colouring by one of three equity measures |
|
|
18
|
+
| `AccessGapScatterPanel` | `CHART_H 714`, responsive width, `M.left 58` | bubble cloud (radius by population, z-ordered) + labelled y=x reference diagonal; **categorical** per-point colouring (division or settlement) with a discrete legend; both axes pinned to 0–1, ticks fixed at 0/25/50/75/100 % |
|
|
19
|
+
| `OptimizationResultsPanel` | `WIDTH 720`, `HEIGHT 420`, `MARGIN.left 72` | cloud at `r=6` + dashed reference line; the status-quo point drawn hollow (`fill: none`) against filled candidates |
|
|
20
|
+
|
|
21
|
+
The operator chose this route deliberately over letting each app reach for MUI X directly
|
|
22
|
+
(hram `FIX-15`, fork option b), so that scatters are built the same way as every other chart in
|
|
23
|
+
the estate rather than becoming a second, parallel charting style.
|
|
24
|
+
|
|
25
|
+
**On the single-consumer risk this WO originally carried.** The first draft was written when
|
|
26
|
+
only the allocation panel was known, and warned that an abstraction drawn from one example is
|
|
27
|
+
how the wrong shape gets frozen. **The operator then named the access panel, and a sweep found
|
|
28
|
+
a third.** That risk is largely spent: three independent consumers, written by different hands
|
|
29
|
+
at different times, are enough to tell a real axis of variation from a guess. What replaces it
|
|
30
|
+
is a narrower discipline:
|
|
31
|
+
|
|
32
|
+
- **Every option in the API must be traceable to one of the three panels above.** They are the
|
|
33
|
+
requirements document. An option no listed consumer needs does not go in — adding one later
|
|
34
|
+
is cheap, removing one from a published package is not.
|
|
35
|
+
- **Two of the variation axes were discovered, not designed**, and that is the point: neither
|
|
36
|
+
categorical-colour-plus-discrete-legend nor data-driven mark radius appears in the allocation
|
|
37
|
+
panel at all. Had this been built from that panel alone, both would have been missed and the
|
|
38
|
+
access panel would have had to bypass the preset.
|
|
39
|
+
- The surface is no longer "provisional pending a second consumer" — but the docs should still
|
|
40
|
+
state which consumers it was drawn from, so a fourth with different needs knows what it is
|
|
41
|
+
extending.
|
|
42
|
+
|
|
43
|
+
**Scope.**
|
|
44
|
+
|
|
45
|
+
1. **`ScatterChart` preset** with the same contract shape as the existing presets: `series`,
|
|
46
|
+
`xAxis`, `yAxis`, `xAxisLabel`, `yAxisLabel`, `palette`, `grid`, `minHeight`, `aspect`,
|
|
47
|
+
`hideLegend`, `legendPosition`, `slotProps`, and remaining props forwarded to MUI.
|
|
48
|
+
2. **Reuse the THEME-9 machinery rather than reimplementing it** — `withAxisDefaults`,
|
|
49
|
+
`sizeYAxisForContent`, `spaceForRotatedTicks`, `defaultNumericTickFormatter`,
|
|
50
|
+
`withGridDefaults`, `withChartSlotDefaults`. A preset that sizes its own axes differently
|
|
51
|
+
from `BarChart` would re-open exactly what THEME-9 just closed.
|
|
52
|
+
3. **A neutral cloud by default.** Unlike a bar chart, where each series takes a categorical
|
|
53
|
+
colour, an *undifferentiated* scatter cloud is one neutral mass — the role register's
|
|
54
|
+
neutral, not a series hue. **The distinction matters, because one consumer colours
|
|
55
|
+
categorically and is right to:** the access panel encodes division or settlement class, a
|
|
56
|
+
real data dimension, and already draws those from the frozen family register. The allocation
|
|
57
|
+
panel's defect is different in kind — it paints a cloud that encodes *nothing* in
|
|
58
|
+
`dataViz.cost.main`, a KPI identity colour. Neutral is the default for "no dimension
|
|
59
|
+
encoded", not a prohibition on colour.
|
|
60
|
+
4. **Three colouring modes, all three in use today:** neutral (default), **categorical** with a
|
|
61
|
+
discrete legend (access — division, settlement), and **continuous** with a scale legend
|
|
62
|
+
(allocation — three equity measures). Categorical draws from the family register's
|
|
63
|
+
positional slots; the consumer must not have to hand-index a palette array to get them.
|
|
64
|
+
5. **Mark size: small fixed default, optionally data-driven.** The default is sized for hundreds
|
|
65
|
+
of overlapping points (hram plots ~300; it must not fuse into a solid area). The access panel
|
|
66
|
+
additionally needs a **radius bound to a value** (`max(3, 10·√(pop/maxPop))`) with **explicit
|
|
67
|
+
z-ordering by that value**, so large bubbles do not hide small ones. Bubble sizing without a
|
|
68
|
+
defined draw order is a silently wrong chart.
|
|
69
|
+
6. **Support the compositions the consumers actually need** — a point cloud **plus** overlaid
|
|
70
|
+
reference geometry **plus** individually marked points:
|
|
71
|
+
- reference geometry appears in all three: a computed curve (allocation's envelope), a
|
|
72
|
+
straight identity line carrying a rotated inline label (access's y=x "no gap"), and a
|
|
73
|
+
dashed reference line (optimization);
|
|
74
|
+
- individually marked points appear in two, including a **hollow** state — optimization
|
|
75
|
+
draws the status quo as `fill: none` with a coloured stroke against filled candidates.
|
|
76
|
+
|
|
77
|
+
Whether that is one component with layers or a documented composition of MUI's
|
|
78
|
+
`ChartContainer` + plots is an implementation decision — but a consumer must be able to do it
|
|
79
|
+
*through* this package. If it cannot, the app bypasses the preset and we are back where we
|
|
80
|
+
started.
|
|
81
|
+
7. **Axis furniture from theme tokens, not hard-coded greys.** This is the concrete regression
|
|
82
|
+
the preset exists to stop: the access panel hard-codes `#F0F0F0` (grid), `#D0D0D0` (axis),
|
|
83
|
+
`#999` (ticks), `#666` (tick labels), `#555` (axis titles), `#B0B0B0` (reference line) —
|
|
84
|
+
six greys that no theme change can reach. The other two are better but still partial.
|
|
85
|
+
8. **Support a pinned tick set on a fixed domain.** The access panel's axes are both 0–1 with
|
|
86
|
+
ticks fixed at 0/25/50/75/100 % — a deliberate choice (the y=x diagonal is only readable on
|
|
87
|
+
equal, fixed axes), not something auto-ticking should override.
|
|
88
|
+
9. Documentation in `DESIGN.md` alongside the other chart guidance, including the axis-title
|
|
89
|
+
rule THEME-9 introduced, and naming the three consumers the surface was drawn from.
|
|
90
|
+
|
|
91
|
+
**Non-goals / do-not-touch.** No changes to the existing presets beyond what sharing helpers
|
|
92
|
+
requires. No new colour tokens — everything comes from the existing role register and palette.
|
|
93
|
+
No zoom, brush, selection-rectangle or animation features: none of them has a consumer, and
|
|
94
|
+
each would be a guess. Do not change `ChartFrame`.
|
|
95
|
+
|
|
96
|
+
**Explicitly NOT in scope — the row-based dot plots.** The same sweep found two more
|
|
97
|
+
hand-drawn hram panels that also plot circles, and they are **a different chart, not a wide
|
|
98
|
+
scatter**: `CalibrationPanel` is a forest plot (`MARGIN.left = 150`, one `ROW_HEIGHT` band per
|
|
99
|
+
parameter, point estimate `r=5` plus a dashed interval) and `SensitivityAnalysisPanel` is a
|
|
100
|
+
tornado (`MARGIN.left = 220`, `<rect>` bars per row). `OptimizationResultsPanel` carries a third
|
|
101
|
+
of the same family in its divergence sub-view (`DIVERGENCE_MARGIN.left = 150`). Their y-axis is
|
|
102
|
+
categorical, their left margin is 150–220 px of row labels, and their layout problem is row
|
|
103
|
+
density — none of which a scatter preset should try to absorb. **Stretching this preset to cover
|
|
104
|
+
them is exactly the over-generalisation this WO is guarding against.** They are a separate
|
|
105
|
+
preset and a separate WO; note them, do not serve them.
|
|
106
|
+
|
|
107
|
+
**Tier 3** — shared-core surface, new public API. `reviewer` mandatory; `ui_reviewer` too,
|
|
108
|
+
since the deliverable is visual.
|
|
109
|
+
|
|
110
|
+
**Tests to write.** The mark count — a scatter that silently drops points is the failure mode
|
|
111
|
+
nobody sees, and it is trivial to assert. That the default cloud colour is the neutral role, not
|
|
112
|
+
a categorical series colour. That bubble mode draws large marks **before** small ones (the
|
|
113
|
+
z-order is the whole correctness of a bubble chart, and it is one assertion on draw order). Axis
|
|
114
|
+
behaviour is already covered by THEME-9's tests if the helpers are reused — if a test has to be
|
|
115
|
+
duplicated here, the helpers were not reused.
|
|
116
|
+
|
|
117
|
+
**Risks.**
|
|
118
|
+
- **Serving three consumers is not the same as serving all scatters.** The single-consumer risk
|
|
119
|
+
is spent, but the opposite one is now live: with three shapes in hand it is tempting to
|
|
120
|
+
generalise to a fourth that does not exist. Every option traces to a listed panel, or it does
|
|
121
|
+
not go in. If the API starts growing during implementation, stop and report.
|
|
122
|
+
- **The row-plot family is the specific over-reach to watch** (see non-goals). Two more panels
|
|
123
|
+
draw circles and will look like near-misses. They are not.
|
|
124
|
+
- **Performance at a few hundred marks** is fine; at tens of thousands it is not. State the
|
|
125
|
+
tested magnitude in the docs rather than implying it scales indefinitely.
|
|
126
|
+
- The overlay composition (scope 6) is where this can quietly fail its purpose. Verify against
|
|
127
|
+
the real panel shapes — cloud + envelope + three special points, and bubble cloud + labelled
|
|
128
|
+
diagonal — not against a synthetic example.
|
|
129
|
+
|
|
130
|
+
**This WO is commissioned before the requirement set is closed — deliberately.** The operator
|
|
131
|
+
dispatched it on 2026-08-14 knowing that the access and optimization panels will each get their
|
|
132
|
+
own prototype, and that **those prototypes are expected to carry additional requirements**. The
|
|
133
|
+
three shapes listed above are what is known now, not the final set. Two consequences the
|
|
134
|
+
implementer must honour:
|
|
135
|
+
|
|
136
|
+
- **Keep the surface additively extensible.** A later requirement must be satisfiable by adding
|
|
137
|
+
a prop, not by changing the meaning of an existing one or reshaping the contract. Prefer an
|
|
138
|
+
explicit option over an inferred behaviour wherever the two cost the same.
|
|
139
|
+
- **Do not read this as licence to pre-build for the unknown.** The rule stands: every option
|
|
140
|
+
ships because a listed panel needs it. "A prototype might later want X" is not a consumer.
|
|
141
|
+
The insurance against the unknown is that the API can grow, not that it already did.
|
|
142
|
+
|
|
143
|
+
**One forthcoming shape is already named — do NOT build it, but do not foreclose it.** The
|
|
144
|
+
optimization panel will want its MILP solution and its simulated solution shown as a **paired
|
|
145
|
+
point** (two marks joined by a connector, with the difference between them displayed). The
|
|
146
|
+
plumbing is largely there: that panel already puts both on shared scales
|
|
147
|
+
(`buildScale([...cloud.map(p => p.cost), ...milp.map(p => p.x)])`) and already joins the MILP
|
|
148
|
+
points with a `polyline`. The part that is **not** additive is the data unit: every contract in
|
|
149
|
+
this WO is *one mark per datum*, whereas a pair is two marks plus a link that must travel
|
|
150
|
+
together — shared tooltip, shared colour, one hit target, and a delta label anchored between
|
|
151
|
+
them. Retrofitting that means reshaping `series`, not adding a prop.
|
|
152
|
+
|
|
153
|
+
So: **design the point contract so a "pair" variant can be added later without changing what a
|
|
154
|
+
single point means** — most simply, by not assuming a 1:1 datum→mark mapping anywhere in the
|
|
155
|
+
internals. Ship nothing for it now. It has no prototype yet, and two of its questions are
|
|
156
|
+
app-level rather than preset-level: which key joins a MILP point to its simulated counterpart
|
|
157
|
+
(the scatter's points come from `planningPoints`, the deltas from a separate `getDivergence`
|
|
158
|
+
call keyed by regime), and whether the difference is rendered as a per-pair numeric label —
|
|
159
|
+
which would be genuine mark labelling, absent from this preset — or carried by the connector
|
|
160
|
+
length plus the tooltip, which costs nothing.
|
|
161
|
+
|
|
162
|
+
**Delivery is not done at publish.** hram `FIX-15` (allocation) is the first consumer and is
|
|
163
|
+
blocked on this package; the pin bump and that panel's rebuild are what prove the preset works.
|
|
164
|
+
The access and optimization panels are known consumers with **no WO yet** — they are what this
|
|
165
|
+
surface was designed against, so they are not optional evidence, but converting them is separate
|
|
166
|
+
work to be scheduled, not folded in here. THEME-8 and THEME-9 are both sitting published with
|
|
167
|
+
consumer pins still open, so bundle the pin bump rather than adding a third pending hop.
|
|
168
|
+
|
|
169
|
+
## Part B — Implementation map — ADDRESSED TO THE IMPLEMENTER
|
|
170
|
+
|
|
171
|
+
Codex was not dispatched: `.claude/codex-status.md` already carried a same-day (2026-08-14)
|
|
172
|
+
`unavailable` line (THEME-8/9/FIX-11/FIX-13, same day), so per the known-unavailable shortcut
|
|
173
|
+
the Orchestrator implemented directly, which flips authorship — `reviewer` + `ui_reviewer`
|
|
174
|
+
became mandatory (already required at Tier 3).
|
|
175
|
+
|
|
176
|
+
### What landed
|
|
177
|
+
|
|
178
|
+
`src/components/charts/ScatterChart.jsx` (new), exported from `src/index.js` alongside
|
|
179
|
+
`BarChart`/`LineChart`/`TimeSeriesChart`:
|
|
180
|
+
|
|
181
|
+
- **Contract shape (scope 1).** `series`/`xAxis`/`yAxis`/`xAxisLabel`/`yAxisLabel`/`palette`/
|
|
182
|
+
`grid`/`minHeight`/`aspect`/`hideLegend`/`legendPosition`/`slotProps`, remaining props
|
|
183
|
+
forwarded to MUI's own `ScatterChart` — same shape as `BarChart`/`LineChart`.
|
|
184
|
+
- **THEME-9 reuse (scope 2).** `withAxisDefaults` + `sizeYAxisForContent` +
|
|
185
|
+
`defaultNumericTickFormatter` for the Y axis (both axes on a scatter are numeric by
|
|
186
|
+
construction, so no band/point default or `spaceForRotatedTicks` reuse was needed — X gets
|
|
187
|
+
MUI's own default sizing, which THEME-9 already established is adequate for an unrotated
|
|
188
|
+
numeric axis).
|
|
189
|
+
- **Neutral default + colouring modes (scope 3/4).** `palette.js` gained a `neutral` key
|
|
190
|
+
(`theme.palette.text.secondary`) — a single series with no explicit `color` renders in that
|
|
191
|
+
tone; multiple series render categorically (MUI's own multi-series legend, unchanged
|
|
192
|
+
mechanism already used by `BarChart`); continuous colouring is the caller's own resolved
|
|
193
|
+
colour applied via `getPointStyle` (this package does not ship a colour-scale/legend
|
|
194
|
+
component — out of scope, see "Not built" below).
|
|
195
|
+
- **Bubble sizing + explicit z-order (scope 5).** `scaleBubbleRadius(value, maxValue)` —
|
|
196
|
+
`max(3, 10·√(value/maxValue))`, exported. `sizeAccessor` on `ScatterChart` turns on bubble
|
|
197
|
+
mode: every series is sorted largest-first before being handed to MUI (SVG paints later
|
|
198
|
+
elements on top, so small bubbles are never hidden), and the max is **one chart-wide value
|
|
199
|
+
across every series** (matches the access panel's own `Math.max(1, ...points.map(p =>
|
|
200
|
+
p.population))` over ALL points regardless of division/settlement — a per-series max was an
|
|
201
|
+
implementation bug caught during the rendered verification below, fixed before commit).
|
|
202
|
+
- **Individually marked points (scope 6, part).** `getPointStyle(point, {seriesId, dataIndex})
|
|
203
|
+
=> {hollow?, color?, shape?, radius?, strokeWidth?}` — a status-quo point drawn hollow
|
|
204
|
+
(`fill:none`), a per-point colour override, `'circle'|'square'|'diamond'`. Every shape option
|
|
205
|
+
traces to the allocation panel specifically (`AllocationPerformancePanel.jsx:500-554`, its
|
|
206
|
+
`ShapeGlyph` legend): `isSelected` → diamond, `isTargeted` → **square**, plain candidate →
|
|
207
|
+
filled circle, status quo → **hollow** circle — the SAME panel that also motivated the
|
|
208
|
+
hollow state (allocation's own status-quo point is hollow too, not only optimization's — the
|
|
209
|
+
Envelope's Part A table names optimization's hollow explicitly but allocation's own point
|
|
210
|
+
rendering carries the identical `fill: isStatusQuo ? 'none' : color` pattern). `ui_reviewer`
|
|
211
|
+
finding U1 flagged `'square'` as apparently untraceable; it traces to `isTargeted`, cited here
|
|
212
|
+
because the original Part A table didn't spell out allocation's shape variety. Implemented via
|
|
213
|
+
a custom MUI marker slot (`slots.marker`) — confirmed against `@mui/x-charts/ScatterChart/
|
|
214
|
+
Scatter.js` that MUI's own marker size is fixed PER SERIES (`series.markerSize`), never
|
|
215
|
+
derived from a point's own `z`, so per-point radius/colour/shape genuinely requires replacing
|
|
216
|
+
the marker, not a prop MUI already exposes.
|
|
217
|
+
- **Reference geometry (scope 6, part).** `ScatterReferenceCurve`/`ScatterReferenceLine`,
|
|
218
|
+
exported, rendered as `ScatterChart` `children` — read the chart's own live `useXScale`/
|
|
219
|
+
`useYScale` (confirmed exported from `@mui/x-charts/hooks`), never a second hand-rolled pixel
|
|
220
|
+
mapping. Covers all three named shapes: allocation's envelope (curve), access's labelled y=x
|
|
221
|
+
diagonal (line + inline rotated label), optimization's dashed threshold (line, dashed).
|
|
222
|
+
- **Theme-token furniture (scope 7).** Inherited for free — grid/axis/tick colours already come
|
|
223
|
+
from the theme via `withGridDefaults`/MUI's own axis styling (the same mechanism `BarChart`/
|
|
224
|
+
`LineChart` already use), never a hard-coded grey.
|
|
225
|
+
- **Pinned tick set on a fixed domain (scope 8).** No new mechanism needed — MUI's own
|
|
226
|
+
`tickInterval: any[]` (confirmed in `@mui/x-charts/hooks/useTicks.d.ts`) already accepts an
|
|
227
|
+
explicit tick array via plain `xAxis`/`yAxis` passthrough (`tickInterval: [0,0.25,0.5,0.75,1]`
|
|
228
|
+
reproduces the access panel's fixed 0/25/50/75/100% ticks exactly).
|
|
229
|
+
- **DESIGN.md (scope 9).** New principle 8b: neutral-by-default, the three colouring modes, the
|
|
230
|
+
bubble-z-order rule, and the explicit "not this preset" pointer to the row-based dot-plot
|
|
231
|
+
family (forest plot, tornado — a different WO).
|
|
232
|
+
|
|
233
|
+
### Forward-compatibility requirement (added to the Envelope after initial implementation)
|
|
234
|
+
|
|
235
|
+
The WO was updated mid-session to add: "commissioned before the requirement set is closed,"
|
|
236
|
+
additive-only extension going forward, and an explicit instruction to **not assume a 1:1
|
|
237
|
+
datum→mark mapping anywhere in the internals**, so a later "paired point" shape (optimization's
|
|
238
|
+
MILP-vs-simulated pair) can be added without reshaping the contract. Reviewed against the
|
|
239
|
+
landed code: nothing here assumes a fixed mark count per datum — a point object may carry
|
|
240
|
+
arbitrary extra fields beyond `{x,y,z,id}` (already exercised by `getPointStyle`'s status-
|
|
241
|
+
quo/bubble usage), `getPointStyle`/`sizeAccessor` key off the datum itself, and reference
|
|
242
|
+
geometry composes via `children` rather than being baked into the per-point contract. A pair
|
|
243
|
+
variant can layer on later (extra series entries + a connector drawn through `children`)
|
|
244
|
+
without touching `series`'s shape or either callback's signature. Documented in the component's
|
|
245
|
+
own docstring so this reasoning survives independent of this WO file.
|
|
246
|
+
|
|
247
|
+
### Not built (named, not silently dropped)
|
|
248
|
+
|
|
249
|
+
- **A continuous colour-scale/legend component.** The WO's scope items describe "continuous
|
|
250
|
+
with a scale legend" as a supported MODE, not a component this package must ship — the
|
|
251
|
+
allocation panel already owns a working gradient-legend implementation
|
|
252
|
+
(`AllocationPerformancePanel.jsx:594-646`) built from its own `getColorScale` utility;
|
|
253
|
+
`getPointStyle` is the seam that lets it apply that resolved colour per point through this
|
|
254
|
+
preset. Building a second, generic version was not named as a required deliverable in scope
|
|
255
|
+
1-9 and risked exactly the over-generalisation the WO warns against (no listed consumer needs
|
|
256
|
+
a DIFFERENT continuous-legend implementation from the one it already has).
|
|
257
|
+
- **The paired-point shape.** Explicitly deferred per the Envelope update above — no prototype,
|
|
258
|
+
no WO, ship nothing.
|
|
259
|
+
- ~~A `dev/entries.jsx` fixture for optimization's exact shape was not added as a third harness
|
|
260
|
+
entry~~ — **reversed per `ui_reviewer` finding U2.** It was added (`OptimizationShapeScatterEntry`)
|
|
261
|
+
and rendered live: this is exactly the combination (no `yAxis` min/max, no explicit `palette`)
|
|
262
|
+
that `reviewer` finding R1 caught a real bug on (`sizeYAxisForContent` never received number
|
|
263
|
+
candidates from scatter's `{x,y}` point objects) — the other two fixtures each avoided the gap
|
|
264
|
+
incidentally (access sets explicit `yAxis` min/max; allocation's Y magnitude happened to fit
|
|
265
|
+
MUI's flat default), so U2's point that live rendering catches what the unit suite doesn't was
|
|
266
|
+
correct and specific, not general caution.
|
|
267
|
+
|
|
268
|
+
### Target repo
|
|
269
|
+
|
|
270
|
+
`C:\Users\biglmi\Documents\webapps\ui-core-micha`
|
|
271
|
+
|
|
272
|
+
## Part C — Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
273
|
+
|
|
274
|
+
> **If you are the implementer reading this work order as your own specification: STOP at this
|
|
275
|
+
> line.**
|
|
276
|
+
|
|
277
|
+
- **Execution.** Codex known-unavailable today (see Part B) — implemented directly in Claude.
|
|
278
|
+
- **Review routing.** `reviewer` + `ui_reviewer`, concurrent, one background batch.
|
|
279
|
+
- **Verification.** Package suite (`ScatterChart.test.jsx` — 15 tests: mark count, neutral
|
|
280
|
+
default, categorical/explicit palette override, bubble z-order, chart-wide bubble max,
|
|
281
|
+
hollow/shape marker rendering, reference curve/line geometry against the live scale;
|
|
282
|
+
`chartsPalette.test.js` gained the `neutral` key pin; full affected set incl.
|
|
283
|
+
`BarChart.test.jsx`/`LineChart.test.jsx`/`chartDefaults.test.js` — 51 tests, green).
|
|
284
|
+
**Rendered check against both real consumer shapes, done live** in `ui-core-micha`'s own dev
|
|
285
|
+
harness (`dev/entries.jsx`, two new entries: "ScatterChart (allocation shape)" and "(access
|
|
286
|
+
shape)"), on the browser tab already carrying real viewport dimensions (cross-origin
|
|
287
|
+
navigation from the previously-displayed hram tab, working around the earlier THEME-9 session's
|
|
288
|
+
0×0-viewport limitation on freshly-opened tabs).
|
|
289
|
+
- **Allocation shape:** 6/6 points rendered (mark count matches supplied), hollow status-quo
|
|
290
|
+
(`fill:"none"`), continuous per-point colour gradient (hsl hue 188→164→132→104.8 tracking
|
|
291
|
+
the fixture's equity values), diamond shape for the selected point, envelope curve drawn
|
|
292
|
+
through the correct live-scale pixel coordinates (`M85,295 L377.9,196 L729.3,86 L905,64`,
|
|
293
|
+
matching the four supplied envelope points).
|
|
294
|
+
- **Access shape:** 7/7 points rendered across 3 categorical series (colours `#0F62FE`/
|
|
295
|
+
`#24A148`/`#FF832B`, correct per-series), fixed 0/25/50/75/100% ticks on both axes, labelled
|
|
296
|
+
dashed y=x reference line present. **First render caught a real bug**: bubble radii were
|
|
297
|
+
computed per-series instead of chart-wide, so a small series' own largest point rendered at
|
|
298
|
+
full radius regardless of its actual size relative to other series — fixed (`chartMax`,
|
|
299
|
+
single value across all series' data) and re-verified live: radii now correctly proportional
|
|
300
|
+
across all three series against one shared max (7.75/6.32/3.16 · 5.48/4.08 · 10/3).
|
|
301
|
+
- **Register & commit.** Advance the THEME-10 row with the reviewer verdicts and the rendered
|
|
302
|
+
mark counts above. hram `FIX-15` (the first actual consumer) does not exist yet — no pin bump
|
|
303
|
+
to bundle in this WO; that happens when `FIX-15` is authored.
|