@micha.bigler/ui-core-micha 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORK_ORDERS.md +4 -2
- package/dist/components/charts/BarChart.js +32 -0
- package/dist/components/charts/ChartFrame.js +50 -0
- package/dist/components/charts/LineChart.js +32 -0
- package/dist/components/charts/exportChart.js +67 -0
- package/dist/components/charts/formatters.js +23 -0
- package/dist/components/charts/palette.js +21 -0
- package/dist/i18n/chartsTranslations.js +38 -0
- package/dist/i18n/notificationsTranslations.js +2 -0
- package/dist/index.js +12 -4
- package/dist/notifications/NotificationsProvider.js +13 -0
- package/dist/notifications/PopupSurface.js +84 -0
- package/dist/onboarding/OnboardingWizard.js +3 -12
- package/dist/onboarding/WizardDialogShell.js +29 -0
- package/package.json +3 -1
- package/src/components/charts/BarChart.jsx +44 -0
- package/src/components/charts/ChartFrame.jsx +112 -0
- package/src/components/charts/LineChart.jsx +44 -0
- package/src/components/charts/exportChart.js +71 -0
- package/src/components/charts/formatters.js +39 -0
- package/src/components/charts/palette.js +24 -0
- package/src/i18n/chartsTranslations.ts +38 -0
- package/src/i18n/notificationsTranslations.ts +2 -0
- package/src/index.js +19 -4
- package/src/notifications/NotificationsProvider.jsx +21 -0
- package/src/notifications/PopupSurface.jsx +103 -0
- package/src/onboarding/OnboardingWizard.jsx +9 -23
- package/src/onboarding/WizardDialogShell.jsx +51 -0
- package/tests/BarChart.test.jsx +55 -0
- package/tests/ChartFrame.test.jsx +122 -0
- package/tests/LineChart.test.jsx +33 -0
- package/tests/NotificationsProvider.test.jsx +38 -0
- package/tests/OnboardingWizard.test.jsx +143 -0
- package/tests/PopupSurface.test.jsx +142 -0
- package/tests/chartsPalette.test.js +39 -0
- package/tests/notificationsExports.test.js +5 -0
- package/work-orders/CHART-1.md +285 -0
- package/work-orders/NOTIF-12.md +75 -23
package/WORK_ORDERS.md
CHANGED
|
@@ -11,6 +11,7 @@ rules are defined centrally in `webapps/AGENTS.md` → "Work-Order Register".
|
|
|
11
11
|
| `ONB-*` | Onboarding wizard (steps, conditions, persistence) |
|
|
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
|
+
| `CHART-*` | Shared chart / data-viz kit (`ChartFrame` + MUI X-Charts presets + neutral palette/formatters) |
|
|
14
15
|
|
|
15
16
|
Introduce a new prefix when none fits and add it here. New WOs always get a
|
|
16
17
|
prefixed ID; never reuse a bare flat number across workstreams.
|
|
@@ -21,6 +22,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
21
22
|
|---|---|---|---|---|---|---|
|
|
22
23
|
| ONB-1 | Per-app configurable notifications onboarding step | New `browserPush` prop `{nagUntil, showOnce}` on `OnboardingProvider`; parameterizes the `browser_push` descriptor's condition (default changes from implicit "all-channels" to "any-channel", stopping the over-nag); `showOnce` via a persisted `onboarding_seen` set (frozen-at-mount ref, no mid-session flicker) | 2026-07-17 | done | 1ae7c20 | Default behavior change affects cockpit and all consumers on their next ucm bump. jg-ferien companion WO pins the new version with `browserPush={{nagUntil: 'any-channel'}}` explicit (matches new default, but pinned explicitly per the WO). |
|
|
23
24
|
| PERF-3B1 | Parallel auth bootstrap after CSRF | Starts auth-methods and current-user concurrently once CSRF is available, while preserving error handling and loading semantics. | 2026-07-19 | done | ee89329 | Independent `reviewer` + `sec_reviewer` passes both clean (no findings); one P3 test-coverage gap from the reviewer (missing mirror case: auth-methods rejects, current-user succeeds) closed with an added regression test — 45/45 tests green. Published as 2.10.1 (patch, no interface change) and pinned in jg-ferien alongside its PERF-3A companion WO. |
|
|
25
|
+
| CHART-1 | Shared chart kit: themed `ChartFrame` + MUI X-Charts Bar/Line presets | Typed-agnostic `ChartFrame` (Paper + title + toolbar slot + loading/error/empty + responsive container + SVG/PNG export + a11y) wrapping any chart body via `children`; thin `BarChart`/`LineChart` presets over MUI X-Charts baking the governance chart contract (both-axis labels, legend-when-multiseries, tooltip, theme-token colours, responsive); neutral theme-aware palette + locale-aware formatters. Additive exports; publishes ucm 2.15.0. | 2026-07-30 | done | f1b3614 | Envelope authored by Expertenchat; `work-orders/CHART-1.md`. Operator approved the `@mui/x-charts` peerDependency 2026-07-30 (in-chat) before Codex ran. Codex-built direct (no nesting). Independent `reviewer` (background, author=Codex ≠ reviewer): found R1 (P1, real bug — 3 of 4 WO-required tests failed on actual execution: no RTL `cleanup()` between `render()` calls left stale DOM across the `ChartFrame` state-precedence/empty tests, and the `BarChart` responsive-sizing assertion called `.not.toContain(null)` on a `getAttribute('style')` that MUI's `sx` never sets inline — an invalid matcher call), R2 (P2 — `BarChart`/`LineChart` passed an invalid `tooltip={{trigger:'axis'}}` prop that isn't part of either component's real API per installed `@mui/x-charts@8.29.2` types; silently worked only via the library's own default, and the mocked tests couldn't catch it), R3 (P2 — `ChartFrame`'s chart region set both `aria-label` and `aria-labelledby`; per ARIA accessible-name computation `aria-labelledby` wins, so a caller-supplied `ariaLabel` override was silently discarded by assistive tech), R4 (P3 — the PNG export control was never actually clicked in the export test, only checked for presence). All four fixed by the orchestrator directly (in-scope reviewer-finding fixes, not a re-implementation): added `afterEach(cleanup)` to the three render-based test files, fixed the responsive-sizing assertion to tolerate `null`/empty inline style, moved tooltip config to the real `slotProps={{tooltip:{trigger:'axis'}}}` API (tests updated to match), suppressed `aria-labelledby` whenever a custom `ariaLabel` is supplied (with a new regression test), and added a PNG-export regression test asserting the callback fires. Re-verified: 16/16 scoped vitest tests green, `pnpm`/tsc build clean. hram adoption (pin bump + ~20 Results/Research panel migration) is a gated follow-on WO in the hram repo, gated on 2.15.0 published (publish-from-main CI on this push). SVG specialists (scatter/tornado/heatmap/CI/frontier) get frame+export in adoption but keep raw-SVG bodies — retiring raw SVG is a tracked future CHART-2. |
|
|
24
26
|
| NOTIF-6 | NotificationsProvider + bell on canonical dcm feed/* API | Single-WS-owner `NotificationsProvider`/`useNotifications` (initial REST seed via `feedApi.js` + one `/ws/notifications/` socket, reconnect w/ backoff) + generic MUI `NotificationBell`; additive exports only | 2026-07-27 | done | 6c63fb9 | Codex-built direct (no nesting), independent `reviewer`: single-WS-owner invariant traced clean (no way to break it — `authenticated`/`replaceNotifications`/`refresh` all referentially stable across unrelated re-renders, re-render test is genuine not a no-op). R1 (P2, real bug): the `notification.status` WS handler patched item flags but never adjusted `unreadCount`, so the badge went stale after a cross-tab/cross-device mark — the test had locked in the stale count as expected behavior. Fixed: delta computed from before/after unread state on the WS patch path, mirroring `mark()`'s own logic; test corrected + a new cross-tab regression test added. 57 passing, build clean. No new npm dependency. Full history + backend contract: dcm's `WORK_ORDERS.md`/`docs/design/notifications-platform.md`. Publishes ucm 2.11.0 |
|
|
25
|
-
| NOTIF-13 | Layer-1 transport extraction (pluggable realtime) | Extract the socket out of `NotificationsProvider` into a generic subscribe-by-message-type realtime primitive so a second domain (messaging) can ride the same socket; the catch-all that turns every non-`notification.status` message into a feed entry is replaced by explicit dispatch with unknown types ignored. Additive: `NotificationsProvider` stays the sole mount point, its context value unchanged. Full spec: [work-orders/NOTIF-13.md](work-orders/NOTIF-13.md). | 2026-07-29 |
|
|
26
|
-
| NOTIF-12 | Popup channel via the wizard renderer | Make `popup` a real delivery channel: extract the presentational dialog shell out of `OnboardingWizard`, render popup-eligible notifications through it, and record seen/dismissed on `NotificationRecipient` (design D-F7: renderer reuse only, NOT the onboarding progress store); dcm `PopupDispatcher` stops being a logging stub. Full spec: [work-orders/NOTIF-12.md](work-orders/NOTIF-12.md). | 2026-07-29 |
|
|
27
|
+
| NOTIF-13 | Layer-1 transport extraction (pluggable realtime) | Extract the socket out of `NotificationsProvider` into a generic subscribe-by-message-type realtime primitive so a second domain (messaging) can ride the same socket; the catch-all that turns every non-`notification.status` message into a feed entry is replaced by explicit dispatch with unknown types ignored. Additive: `NotificationsProvider` stays the sole mount point, its context value unchanged. Full spec: [work-orders/NOTIF-13.md](work-orders/NOTIF-13.md). | 2026-07-29 | done | ucm: 7a83ee9; dcm: de77335 | Tier 2 (cockpit consumes `NotificationsProvider` at 2.12.0 in prod). Phase A, the hinge: unblocks NOTIF-15 (jg bell adoption) and is the substrate for Phase B messaging. Canonical register row: django-core-micha `WORK_ORDERS.md`. Operator decision 2026-07-29: additive only, NO standalone `RealtimeProvider` apps must mount. Scope E (envelope contract + helper + doc) also touches dcm; note that the roadmap wording "dcm multi-stream consumer" overstates it — `push_to_users`/`NotificationConsumer` are already stream-agnostic. Codex failed twice on this WO with a genuine CLI/model-version error (`gpt-5.6-terra requires a newer version of Codex`), not a transient issue — orchestrator fell back to direct Claude implementation per the Codex→Claude fallback rule. Independent `reviewer` (sec_reviewer not required per WO): found R1 (P2, real bug — decoupling the REST seed from the socket connect opened a race where a live push arriving before the initial feed loaded would be silently discarded by `refresh()`'s unconditional `replaceNotifications()`; fixed via a `seeded` state gate restoring the exact original seed-then-connect ordering, re-verified 70/70 green), R2 (P3 — the WO's "must be called out in the CHANGELOG" has no home since ucm has no CHANGELOG.md convention at all; operator decision: document via commit message + this register entry only, no new file), R3 (register housekeeping, closed by this update). ucm full suite 70/70 + `pnpm build` clean; dcm notifications suite 92/92, full dcm suite 392/392. Published: ucm 2.13.0 (npm), dcm 2.33.0 (PyPI, verified live before this record) — no app pin bumps in this WO per spec. |
|
|
28
|
+
| NOTIF-12 | Popup channel via the wizard renderer | Make `popup` a real delivery channel: extract the presentational dialog shell out of `OnboardingWizard`, render popup-eligible notifications through it, and record seen/dismissed on `NotificationRecipient` (design D-F7: renderer reuse only, NOT the onboarding progress store); dcm `PopupDispatcher` stops being a logging stub. Full spec: [work-orders/NOTIF-12.md](work-orders/NOTIF-12.md). | 2026-07-29 | done | ucm: c8e222f, d7bdcbe; dcm: django-core-micha 1612429, 82fd89f | Tier 2 (extracts a renderer cockpit+jg run in production; the roadmap label "uncritical" is about urgency, not blast radius). **HARD GATE: starts only after NOTIF-13 landed + published** — consumes its envelope contract and `subscribe()`, and edits the same ucm files (AGENTS.md overlap rule). **SHIPS WITH ZERO PRODUCERS, deliberately** (operator decision 2026-07-29, made with the evidence): no notification type declares `popup` in `eligible_channels` anywhere in dcm/jg/cockpit/hram/spesix/survey_app, so the channel is wired but inactive until an app opts a type in. WO therefore requires (a) CHANGELOG wording that does not overclaim (NOTIF-4 R1 precedent) and (b) a test-local producer type proving the path end-to-end, so it is not untested dead code. Was NOTIF-11 (renumbered 2026-07-29 for the jg cutover/drop split). **Gate satisfied 2026-07-30** (ucm 2.13.0 / dcm 2.33.0); WO re-verified against the landed NOTIF-13 code and corrected in `345631d`: NOTIF-13 shipped the envelope as a **domain**-level discriminator, so a popup payload built the same way as a chip payload would be indistinguishable from it - the WO now mandates a `channel` field inside the envelope (never a second envelope value) plus feed de-duplication by `notification_id`, without which chip+popup double-counts the unread badge. **Codex failed twice with the same genuine CLI/model-version error as NOTIF-13** (`gpt-5.6-terra requires a newer version of Codex`) — orchestrator implemented directly in Claude per the Codex→Claude fallback rule, both scopes. Independent `reviewer` (background agent, `fork_context=false`, full diff of both repos): found R1 (P1, real bug — the WS payload only carried `notification_id`, but `feed/mark/` resolves ids against `NotificationRecipient.pk`; every `markSeen`/`markDismissed` from a session-live-only popup would either no-op or corrupt an unrelated recipient row once a producer opts in — fixed by adding `recipient_id` to both `ChipDispatcher` and `PopupDispatcher` payloads, dcm-side, with regression tests), R2 (P2 — the reused dialog shell always showed the onboarding "Setup"/step-counter header even for a popup, a real mislabeling once any producer exists — fixed via an optional `title` override on `WizardDialogShell`, defaulting to the prior exact text so `OnboardingWizard` stays bit-for-bit unchanged; popup now shows its own notification title), R3 (P2 — coexistence only gated on a *blocking* onboarding step, so a non-blocking step (e.g. `browser_push`) and a popup could stack two simultaneous MUI dialogs — fixed by broadening the gate to any active onboarding step, with a new regression test). All three fixed and re-verified: ucm 88/88 vitest + `pnpm`/tsc build clean, dcm 95/95 pytest (notifications package). Published (once pushed): ucm 2.14.0 (npm, CI-gated on push to `main`), dcm 2.34.0 (PyPI, same). No app pin bumps in this WO per spec. Own orchestrator security/exposure pass in parallel with the reviewer: no findings (content rendering follows the existing `NotificationBell` i18n-interpolation pattern, no new WS consumer, `prefs.py`/`router.py`/`resolve_channels` untouched). |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Box } from '@mui/material';
|
|
15
|
+
import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
|
|
16
|
+
import { useNeutralChartPalette } from './palette';
|
|
17
|
+
function labelledAxis(axes, label, defaults) {
|
|
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
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Responsive MUI X-Charts bar preset. xAxisLabel and yAxisLabel (including units) are required.
|
|
23
|
+
* It supplies labelled axes, tooltips, and an automatic multi-series legend.
|
|
24
|
+
*/
|
|
25
|
+
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"]);
|
|
27
|
+
const neutralPalette = useNeutralChartPalette();
|
|
28
|
+
if (!xAxisLabel || !yAxisLabel) {
|
|
29
|
+
throw new Error('BarChart requires xAxisLabel and yAxisLabel.');
|
|
30
|
+
}
|
|
31
|
+
return (_jsx(Box, { "data-testid": "bar-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { series: series, xAxis: labelledAxis(xAxis, xAxisLabel, { scaleType: 'band', data: [] }), yAxis: labelledAxis(yAxis, yAxisLabel, { scaleType: 'linear' }), colors: palette || neutralPalette.categorical, hideLegend: series.length <= 1, slotProps: { tooltip: { trigger: 'axis' } } })) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useId, useRef, useState } from 'react';
|
|
3
|
+
import { Alert, Box, Button, CircularProgress, Paper, Stack, Typography, } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { exportChartPng, exportChartSvg } from './exportChart';
|
|
6
|
+
/**
|
|
7
|
+
* Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
|
|
8
|
+
*/
|
|
9
|
+
export function ChartFrame({ title, subtitle, toolbar, loading = false, error = false, isEmpty = false, emptyMessage, minHeight, aspect, exportOptions = false, onExportSvg, onExportPng, ariaLabel, children, variant = 'outlined', sx, }) {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const titleId = useId();
|
|
12
|
+
const chartRef = useRef(null);
|
|
13
|
+
const [exportError, setExportError] = useState(false);
|
|
14
|
+
const options = exportOptions === true ? {} : exportOptions || {};
|
|
15
|
+
const exportsEnabled = Boolean(exportOptions);
|
|
16
|
+
const hasCustomAriaLabel = Boolean(ariaLabel);
|
|
17
|
+
const chartLabel = ariaLabel || title;
|
|
18
|
+
const runExport = async (type) => {
|
|
19
|
+
setExportError(false);
|
|
20
|
+
try {
|
|
21
|
+
const filename = options.filename || 'chart';
|
|
22
|
+
const result = type === 'svg'
|
|
23
|
+
? exportChartSvg(chartRef.current, `${filename}.svg`)
|
|
24
|
+
: await exportChartPng(chartRef.current, `${filename}.png`);
|
|
25
|
+
const callback = type === 'svg' ? onExportSvg : onExportPng;
|
|
26
|
+
callback === null || callback === void 0 ? void 0 : callback(result);
|
|
27
|
+
}
|
|
28
|
+
catch (_error) {
|
|
29
|
+
setExportError(true);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
let content = children;
|
|
33
|
+
if (loading) {
|
|
34
|
+
content = _jsx(CircularProgress, { "aria-label": t('ChartFrame.LOADING') });
|
|
35
|
+
}
|
|
36
|
+
else if (error) {
|
|
37
|
+
content = _jsx(Alert, { severity: "error", children: typeof error === 'string' ? error : t('ChartFrame.ERROR_DEFAULT') });
|
|
38
|
+
}
|
|
39
|
+
else if (isEmpty) {
|
|
40
|
+
content = _jsx(Alert, { severity: "info", children: emptyMessage || t('ChartFrame.EMPTY_DEFAULT') });
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(Paper, { variant: variant, sx: Object.assign({ p: 2 }, sx), children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 2 }, children: [_jsxs(Box, { children: [_jsx(Typography, { id: titleId, variant: "h6", children: title }), subtitle && _jsx(Typography, { variant: "body2", color: "text.secondary", children: subtitle })] }), toolbar && _jsx(Box, { children: toolbar })] }), _jsx(Box, { ref: chartRef, role: "img", "aria-label": chartLabel, "aria-labelledby": hasCustomAriaLabel ? undefined : titleId, sx: {
|
|
43
|
+
width: '100%',
|
|
44
|
+
minHeight,
|
|
45
|
+
aspectRatio: aspect,
|
|
46
|
+
display: 'flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
}, children: content }), exportError && _jsx(Alert, { severity: "error", sx: { mt: 2 }, children: t('ChartFrame.EXPORT_ERROR') }), exportsEnabled && (_jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", sx: { mt: 2 }, children: [options.svg !== false && (_jsx(Button, { size: "small", onClick: () => runExport('svg'), children: t('ChartFrame.EXPORT_SVG_LABEL') })), options.png !== false && (_jsx(Button, { size: "small", onClick: () => runExport('png'), children: t('ChartFrame.EXPORT_PNG_LABEL') }))] }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Box } from '@mui/material';
|
|
15
|
+
import { LineChart as MuiLineChart } from '@mui/x-charts/LineChart';
|
|
16
|
+
import { useNeutralChartPalette } from './palette';
|
|
17
|
+
function labelledAxis(axes, label, defaults) {
|
|
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
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Responsive MUI X-Charts line preset. xAxisLabel and yAxisLabel (including units) are required.
|
|
23
|
+
* It supplies labelled axes, tooltips, and an automatic multi-series legend.
|
|
24
|
+
*/
|
|
25
|
+
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"]);
|
|
27
|
+
const neutralPalette = useNeutralChartPalette();
|
|
28
|
+
if (!xAxisLabel || !yAxisLabel) {
|
|
29
|
+
throw new Error('LineChart requires xAxisLabel and yAxisLabel.');
|
|
30
|
+
}
|
|
31
|
+
return (_jsx(Box, { "data-testid": "line-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { series: series, xAxis: labelledAxis(xAxis, xAxisLabel, { scaleType: 'point', data: [] }), yAxis: labelledAxis(yAxis, yAxisLabel, { scaleType: 'linear' }), colors: palette || neutralPalette.categorical, hideLegend: series.length <= 1, slotProps: { tooltip: { trigger: 'axis' } } })) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function findSvg(chartContainer) {
|
|
2
|
+
const svg = chartContainer === null || chartContainer === void 0 ? void 0 : chartContainer.querySelector('svg');
|
|
3
|
+
if (!svg)
|
|
4
|
+
throw new Error('Chart container does not contain an SVG element.');
|
|
5
|
+
return svg;
|
|
6
|
+
}
|
|
7
|
+
function svgBlob(chartContainer) {
|
|
8
|
+
const svg = findSvg(chartContainer).cloneNode(true);
|
|
9
|
+
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
10
|
+
return new Blob([new XMLSerializer().serializeToString(svg)], { type: 'image/svg+xml;charset=utf-8' });
|
|
11
|
+
}
|
|
12
|
+
function download(blob, filename) {
|
|
13
|
+
const url = URL.createObjectURL(blob);
|
|
14
|
+
const anchor = document.createElement('a');
|
|
15
|
+
anchor.href = url;
|
|
16
|
+
anchor.download = filename;
|
|
17
|
+
document.body.appendChild(anchor);
|
|
18
|
+
anchor.click();
|
|
19
|
+
anchor.remove();
|
|
20
|
+
URL.revokeObjectURL(url);
|
|
21
|
+
}
|
|
22
|
+
/** Serializes the first SVG rendered inside a chart container and downloads it. */
|
|
23
|
+
export function exportChartSvg(chartContainer, filename = 'chart.svg') {
|
|
24
|
+
const blob = svgBlob(chartContainer);
|
|
25
|
+
download(blob, filename);
|
|
26
|
+
return blob;
|
|
27
|
+
}
|
|
28
|
+
/** Rasterizes the first SVG rendered inside a chart container at two-times scale and downloads it. */
|
|
29
|
+
export function exportChartPng(chartContainer, filename = 'chart.png') {
|
|
30
|
+
const source = svgBlob(chartContainer);
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const url = URL.createObjectURL(source);
|
|
33
|
+
const image = new Image();
|
|
34
|
+
image.onload = () => {
|
|
35
|
+
try {
|
|
36
|
+
const svg = findSvg(chartContainer);
|
|
37
|
+
const width = Number(svg.getAttribute('width')) || svg.clientWidth || 1;
|
|
38
|
+
const height = Number(svg.getAttribute('height')) || svg.clientHeight || 1;
|
|
39
|
+
const canvas = document.createElement('canvas');
|
|
40
|
+
canvas.width = width * 2;
|
|
41
|
+
canvas.height = height * 2;
|
|
42
|
+
const context = canvas.getContext('2d');
|
|
43
|
+
context.fillStyle = 'white';
|
|
44
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
45
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
46
|
+
canvas.toBlob((blob) => {
|
|
47
|
+
URL.revokeObjectURL(url);
|
|
48
|
+
if (!blob) {
|
|
49
|
+
reject(new Error('Unable to create PNG export.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
download(blob, filename);
|
|
53
|
+
resolve(blob);
|
|
54
|
+
}, 'image/png');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
URL.revokeObjectURL(url);
|
|
58
|
+
reject(error);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
image.onerror = () => {
|
|
62
|
+
URL.revokeObjectURL(url);
|
|
63
|
+
reject(new Error('Unable to load chart SVG for PNG export.'));
|
|
64
|
+
};
|
|
65
|
+
image.src = url;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
export function formatPercentage(value, locale, options = {}) {
|
|
3
|
+
return new Intl.NumberFormat(locale, Object.assign({ style: 'percent', maximumFractionDigits: 1 }, options)).format(value);
|
|
4
|
+
}
|
|
5
|
+
export function formatCompact(value, locale, options = {}) {
|
|
6
|
+
return new Intl.NumberFormat(locale, Object.assign({ notation: 'compact', maximumFractionDigits: 1 }, options)).format(value);
|
|
7
|
+
}
|
|
8
|
+
export function formatRatio(value, locale, options = {}) {
|
|
9
|
+
return new Intl.NumberFormat(locale, Object.assign({ maximumFractionDigits: 2 }, options)).format(value);
|
|
10
|
+
}
|
|
11
|
+
/** Creates locale-aware formatter functions for use in chart axes and tooltips. */
|
|
12
|
+
export function createChartFormatters(locale) {
|
|
13
|
+
return {
|
|
14
|
+
percentage: (value, options) => formatPercentage(value, locale, options),
|
|
15
|
+
compact: (value, options) => formatCompact(value, locale, options),
|
|
16
|
+
ratio: (value, options) => formatRatio(value, locale, options),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Resolves formatter locale from the active react-i18next instance. */
|
|
20
|
+
export function useChartFormatters() {
|
|
21
|
+
const { i18n } = useTranslation();
|
|
22
|
+
return createChartFormatters(i18n.language);
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useTheme } from '@mui/material/styles';
|
|
2
|
+
/**
|
|
3
|
+
* Returns neutral chart colours derived only from the active MUI theme.
|
|
4
|
+
* Consumers may replace either array with their own domain palette.
|
|
5
|
+
*/
|
|
6
|
+
export function getNeutralChartPalette(theme) {
|
|
7
|
+
const palette = theme.palette;
|
|
8
|
+
const isDark = palette.mode === 'dark';
|
|
9
|
+
return {
|
|
10
|
+
categorical: isDark
|
|
11
|
+
? [palette.primary.light, palette.secondary.light, palette.info.light, palette.success.light, palette.warning.light]
|
|
12
|
+
: [palette.primary.main, palette.secondary.main, palette.info.main, palette.success.main, palette.warning.main],
|
|
13
|
+
sequential: isDark
|
|
14
|
+
? [palette.action.disabled, palette.text.secondary, palette.primary.light, palette.primary.main, palette.primary.dark]
|
|
15
|
+
: [palette.action.disabled, palette.divider, palette.text.secondary, palette.primary.light, palette.primary.main],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Resolves the kit's neutral palette from the nearest MUI ThemeProvider. */
|
|
19
|
+
export function useNeutralChartPalette() {
|
|
20
|
+
return getNeutralChartPalette(useTheme());
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const chartsTranslations = {
|
|
2
|
+
'ChartFrame.LOADING': {
|
|
3
|
+
de: 'Diagramm wird geladen.',
|
|
4
|
+
fr: 'Chargement du graphique.',
|
|
5
|
+
en: 'Loading chart.',
|
|
6
|
+
sw: 'Inapakia chati.',
|
|
7
|
+
},
|
|
8
|
+
'ChartFrame.EMPTY_DEFAULT': {
|
|
9
|
+
de: 'Keine Daten verfügbar.',
|
|
10
|
+
fr: 'Aucune donnée disponible.',
|
|
11
|
+
en: 'No data available.',
|
|
12
|
+
sw: 'Hakuna data inayopatikana.',
|
|
13
|
+
},
|
|
14
|
+
'ChartFrame.ERROR_DEFAULT': {
|
|
15
|
+
de: 'Das Diagramm konnte nicht geladen werden.',
|
|
16
|
+
fr: 'Impossible de charger le graphique.',
|
|
17
|
+
en: 'The chart could not be loaded.',
|
|
18
|
+
sw: 'Chati haikuweza kupakiwa.',
|
|
19
|
+
},
|
|
20
|
+
'ChartFrame.EXPORT_SVG_LABEL': {
|
|
21
|
+
de: 'SVG exportieren',
|
|
22
|
+
fr: 'Exporter en SVG',
|
|
23
|
+
en: 'Export SVG',
|
|
24
|
+
sw: 'Hamisha SVG',
|
|
25
|
+
},
|
|
26
|
+
'ChartFrame.EXPORT_PNG_LABEL': {
|
|
27
|
+
de: 'PNG exportieren',
|
|
28
|
+
fr: 'Exporter en PNG',
|
|
29
|
+
en: 'Export PNG',
|
|
30
|
+
sw: 'Hamisha PNG',
|
|
31
|
+
},
|
|
32
|
+
'ChartFrame.EXPORT_ERROR': {
|
|
33
|
+
de: 'Das Diagramm konnte nicht exportiert werden.',
|
|
34
|
+
fr: 'Impossible d’exporter le graphique.',
|
|
35
|
+
en: 'The chart could not be exported.',
|
|
36
|
+
sw: 'Chati haikuweza kuhamishwa.',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -17,4 +17,6 @@ export const notificationsTranslations = {
|
|
|
17
17
|
'NotificationSettings.PUSH_CONFLICT': { de: 'Dieses Gerät ist bereits mit einem anderen Konto für Push-Benachrichtigungen verbunden.', fr: 'Cet appareil est déjà associé à un autre compte pour les notifications push.', en: 'This device is already associated with another account for push notifications.', sw: 'Kifaa hiki tayari kimeunganishwa na akaunti nyingine kwa arifa za push.' },
|
|
18
18
|
'NotificationSettings.PUSH_NOT_SUPPORTED': { de: 'Push-Benachrichtigungen werden von diesem Browser nicht unterstützt.', fr: 'Les notifications push ne sont pas prises en charge par ce navigateur.', en: 'Push notifications are not supported by this browser.', sw: 'Arifa za push hazitumiki na kivinjari hiki.' },
|
|
19
19
|
'NotificationSettings.IOS_HINT': { de: 'Füge diese App zuerst zum Home-Bildschirm hinzu, um Push-Benachrichtigungen zu aktivieren.', fr: 'Ajoutez d’abord cette application à l’écran d’accueil pour activer les notifications push.', en: 'Add this app to your home screen first to enable push notifications.', sw: 'Ongeza programu hii kwenye skrini yako ya mwanzo kwanza ili kuwasha arifa za push.' },
|
|
20
|
+
'PopupSurface.CLOSE': { de: 'Schliessen', fr: 'Fermer', en: 'Close', sw: 'Funga' },
|
|
21
|
+
'PopupSurface.TITLE': { de: 'Benachrichtigung', fr: 'Notification', en: 'Notification', sw: 'Arifa' },
|
|
20
22
|
};
|
package/dist/index.js
CHANGED
|
@@ -27,16 +27,23 @@ export { RegistrationMethodsManager } from './components/RegistrationMethodsMana
|
|
|
27
27
|
export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
|
|
28
28
|
export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
|
|
29
29
|
export { QrSignupManager } from './components/QrSignupManager';
|
|
30
|
-
// --- 6.
|
|
30
|
+
// --- 6. Charts ---
|
|
31
|
+
export { ChartFrame } from './components/charts/ChartFrame';
|
|
32
|
+
export { BarChart } from './components/charts/BarChart';
|
|
33
|
+
export { LineChart } from './components/charts/LineChart';
|
|
34
|
+
export { getNeutralChartPalette, useNeutralChartPalette } from './components/charts/palette';
|
|
35
|
+
export { formatPercentage, formatCompact, formatRatio, createChartFormatters, useChartFormatters, } from './components/charts/formatters';
|
|
36
|
+
// --- 7. Translations ---
|
|
31
37
|
export { authTranslations } from './i18n/authTranslations';
|
|
32
|
-
// ---
|
|
38
|
+
// --- 8. Notifications ---
|
|
33
39
|
export { NotificationSettings } from './notifications/NotificationSettings';
|
|
34
40
|
export * from './notifications/api';
|
|
35
41
|
export { NotificationsProvider, useNotifications } from './notifications/NotificationsProvider';
|
|
36
42
|
export { useRealtime } from './notifications/realtime';
|
|
37
43
|
export { NotificationBell } from './notifications/NotificationBell';
|
|
44
|
+
export { PopupSurface } from './notifications/PopupSurface';
|
|
38
45
|
export { getNotificationFeed, getUnreadCount, markNotifications } from './notifications/feedApi';
|
|
39
|
-
// ---
|
|
46
|
+
// --- 9. Onboarding ---
|
|
40
47
|
export * from './onboarding/api';
|
|
41
48
|
export { selectActiveSteps } from './onboarding/stepSelection';
|
|
42
49
|
export { OnboardingContext, OnboardingProvider, UNIVERSAL_STEP_DESCRIPTORS, useOnboarding, } from './onboarding/OnboardingProvider';
|
|
@@ -45,6 +52,7 @@ export { CookieConsentStep } from './onboarding/steps/CookieConsentStep';
|
|
|
45
52
|
export { CompleteNameStep } from './onboarding/steps/CompleteNameStep';
|
|
46
53
|
export { BrowserPushStep } from './onboarding/steps/BrowserPushStep';
|
|
47
54
|
export { PwaInstallStep } from './onboarding/steps/PwaInstallStep';
|
|
48
|
-
// ---
|
|
55
|
+
// --- 10. Translations ---
|
|
49
56
|
export { notificationsTranslations } from './i18n/notificationsTranslations';
|
|
50
57
|
export { onboardingTranslations } from './i18n/onboardingTranslations';
|
|
58
|
+
export { chartsTranslations } from './i18n/chartsTranslations';
|
|
@@ -91,6 +91,19 @@ export function NotificationsProvider({ children, wsUrlBase }) {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
const pushedNotification = normalizeNotificationPush(data);
|
|
94
|
+
// NOTIF-12 D2: a notification routed to more than one channel (e.g. chip
|
|
95
|
+
// + popup) arrives as multiple WS messages sharing one notification_id.
|
|
96
|
+
// Fold repeats into the existing feed entry (last-write-wins on content)
|
|
97
|
+
// instead of appending a duplicate and double-incrementing the badge.
|
|
98
|
+
const existingIndex = pushedNotification.notification_id == null
|
|
99
|
+
? -1
|
|
100
|
+
: notificationsRef.current.findIndex((notification) => notification.notification_id === pushedNotification.notification_id);
|
|
101
|
+
if (existingIndex !== -1) {
|
|
102
|
+
const nextNotifications = notificationsRef.current.map((notification, index) => (index === existingIndex
|
|
103
|
+
? Object.assign(Object.assign({}, notification), { notification_type: pushedNotification.notification_type, content: pushedNotification.content }) : notification));
|
|
104
|
+
replaceNotifications(nextNotifications);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
94
107
|
replaceNotifications([pushedNotification, ...notificationsRef.current]);
|
|
95
108
|
setUnreadCount((count) => count + 1);
|
|
96
109
|
}, [replaceNotifications]);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import Button from '@mui/material/Button';
|
|
5
|
+
import Stack from '@mui/material/Stack';
|
|
6
|
+
import Typography from '@mui/material/Typography';
|
|
7
|
+
import { useOnboarding } from '../onboarding/OnboardingProvider';
|
|
8
|
+
import { WizardDialogShell } from '../onboarding/WizardDialogShell';
|
|
9
|
+
import { useNotifications } from './NotificationsProvider';
|
|
10
|
+
import { DEFAULT_ENVELOPE, useRealtime } from './realtime';
|
|
11
|
+
function PopupStepComponent({ onDismiss, ctx }) {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const content = (ctx === null || ctx === void 0 ? void 0 : ctx.content) || {};
|
|
14
|
+
return (_jsxs(Stack, { spacing: 2, children: [content.body_key && (_jsx(Typography, { variant: "body1", children: t(content.body_key, content.params || {}) })), _jsx(Button, { onClick: onDismiss, sx: { alignSelf: 'flex-end' }, children: t('PopupSurface.CLOSE') })] }));
|
|
15
|
+
}
|
|
16
|
+
const POPUP_STEP = { blocking: false, Component: PopupStepComponent };
|
|
17
|
+
/**
|
|
18
|
+
* Renders popup-eligible notifications (NOTIF-12) through the onboarding
|
|
19
|
+
* wizard's dialog shell, reusing only the renderer (design D-F7) — status
|
|
20
|
+
* lives on NotificationRecipient via the existing feed/mark path, never in
|
|
21
|
+
* the onboarding progress store.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately session-live only: it watches the raw realtime stream for
|
|
24
|
+
* `channel === "popup"` messages rather than the already-seeded feed list,
|
|
25
|
+
* since the feed carries no per-channel discriminator (see NotificationsProvider).
|
|
26
|
+
*/
|
|
27
|
+
export function PopupSurface() {
|
|
28
|
+
var _a;
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const { markSeen, markDismissed } = useNotifications();
|
|
31
|
+
const { subscribe } = useRealtime();
|
|
32
|
+
const onboarding = useOnboarding();
|
|
33
|
+
const [queue, setQueue] = useState([]);
|
|
34
|
+
const seenIdsRef = useRef(new Set());
|
|
35
|
+
const handleRealtimeMessage = useCallback((data) => {
|
|
36
|
+
if (data.type === 'notification.status')
|
|
37
|
+
return;
|
|
38
|
+
if (data.channel !== 'popup')
|
|
39
|
+
return;
|
|
40
|
+
setQueue((previous) => {
|
|
41
|
+
var _a;
|
|
42
|
+
if (previous.some((item) => item.notification_id === data.notification_id))
|
|
43
|
+
return previous;
|
|
44
|
+
return [...previous, {
|
|
45
|
+
notification_id: data.notification_id,
|
|
46
|
+
// recipient_id is the id feed/mark/ actually resolves against
|
|
47
|
+
// (NotificationRecipient.pk, not Notification.pk). Fall back to
|
|
48
|
+
// notification_id only for an older dcm that predates it — matching
|
|
49
|
+
// NotificationsProvider's own normalizeNotificationPush precedent.
|
|
50
|
+
markId: (_a = data.recipient_id) !== null && _a !== void 0 ? _a : data.notification_id,
|
|
51
|
+
content: data.content || {},
|
|
52
|
+
}];
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
useEffect(() => subscribe(DEFAULT_ENVELOPE, handleRealtimeMessage), [subscribe, handleRealtimeMessage]);
|
|
56
|
+
// Any active onboarding step — blocking or not — must win: OnboardingWizard
|
|
57
|
+
// renders its own dialog for as long as onboarding.activeSteps is non-empty,
|
|
58
|
+
// and two independent MUI Dialogs must never be mounted at once.
|
|
59
|
+
const onboardingActive = Boolean((_a = onboarding === null || onboarding === void 0 ? void 0 : onboarding.activeSteps) === null || _a === void 0 ? void 0 : _a.length);
|
|
60
|
+
const activePopup = queue[0];
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!activePopup || onboardingActive)
|
|
63
|
+
return;
|
|
64
|
+
if (activePopup.markId == null)
|
|
65
|
+
return;
|
|
66
|
+
if (seenIdsRef.current.has(activePopup.notification_id))
|
|
67
|
+
return;
|
|
68
|
+
seenIdsRef.current.add(activePopup.notification_id);
|
|
69
|
+
markSeen([activePopup.markId]);
|
|
70
|
+
}, [activePopup, onboardingActive, markSeen]);
|
|
71
|
+
const dismissActivePopup = useCallback(() => {
|
|
72
|
+
if (!activePopup)
|
|
73
|
+
return;
|
|
74
|
+
if (activePopup.markId != null)
|
|
75
|
+
markDismissed([activePopup.markId]);
|
|
76
|
+
setQueue((previous) => previous.filter((item) => item.notification_id !== activePopup.notification_id));
|
|
77
|
+
}, [activePopup, markDismissed]);
|
|
78
|
+
if (!activePopup || onboardingActive)
|
|
79
|
+
return null;
|
|
80
|
+
return (_jsx(WizardDialogShell, { step: POPUP_STEP, stepIndex: 0, total: 1, onComplete: dismissActivePopup, onDismiss: dismissActivePopup, ctx: { content: activePopup.content }, title: activePopup.content.title_key
|
|
81
|
+
? t(activePopup.content.title_key, activePopup.content.params || {})
|
|
82
|
+
: t('PopupSurface.TITLE') }));
|
|
83
|
+
}
|
|
84
|
+
export default PopupSurface;
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import Box from '@mui/material/Box';
|
|
5
|
-
import Dialog from '@mui/material/Dialog';
|
|
6
|
-
import DialogContent from '@mui/material/DialogContent';
|
|
7
|
-
import DialogTitle from '@mui/material/DialogTitle';
|
|
8
|
-
import LinearProgress from '@mui/material/LinearProgress';
|
|
9
|
-
import Typography from '@mui/material/Typography';
|
|
10
3
|
import { AuthContext } from '../auth/AuthContext';
|
|
11
4
|
import { useOnboarding } from './OnboardingProvider';
|
|
5
|
+
import { WizardDialogShell } from './WizardDialogShell';
|
|
12
6
|
export function OnboardingWizard() {
|
|
13
|
-
const { t } = useTranslation();
|
|
14
7
|
const onboarding = useOnboarding();
|
|
15
8
|
const authContext = useContext(AuthContext);
|
|
16
9
|
const [sessionDismissed, setSessionDismissed] = useState(() => new Set());
|
|
@@ -37,8 +30,6 @@ export function OnboardingWizard() {
|
|
|
37
30
|
return null;
|
|
38
31
|
const { dismissStep, ctx } = onboarding;
|
|
39
32
|
const total = totalRef.current || visibleSteps.length;
|
|
40
|
-
const progress = total > 1 ? Math.round((completed / total) * 100) : 0;
|
|
41
|
-
const StepComponent = currentStep.Component;
|
|
42
33
|
const completeCurrentStep = () => {
|
|
43
34
|
var _a, _b;
|
|
44
35
|
setSessionDismissed((previous) => new Set([...previous, currentStep.id]));
|
|
@@ -52,6 +43,6 @@ export function OnboardingWizard() {
|
|
|
52
43
|
dismissStep(currentStep.id);
|
|
53
44
|
completeCurrentStep();
|
|
54
45
|
};
|
|
55
|
-
return (
|
|
46
|
+
return (_jsx(WizardDialogShell, { step: currentStep, stepIndex: completed, total: total, onComplete: completeCurrentStep, onDismiss: dismissCurrentStep, ctx: ctx }));
|
|
56
47
|
}
|
|
57
48
|
export default OnboardingWizard;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import Box from '@mui/material/Box';
|
|
5
|
+
import Dialog from '@mui/material/Dialog';
|
|
6
|
+
import DialogContent from '@mui/material/DialogContent';
|
|
7
|
+
import DialogTitle from '@mui/material/DialogTitle';
|
|
8
|
+
import LinearProgress from '@mui/material/LinearProgress';
|
|
9
|
+
import Typography from '@mui/material/Typography';
|
|
10
|
+
/**
|
|
11
|
+
* Presentational dialog shell shared by OnboardingWizard and the popup
|
|
12
|
+
* notification surface (NOTIF-12). Owns only the dialog frame, the
|
|
13
|
+
* counter/progress header, and the onComplete/onDismiss/ctx child contract —
|
|
14
|
+
* step selection, dismissal persistence, and any progress store stay with
|
|
15
|
+
* the caller.
|
|
16
|
+
*/
|
|
17
|
+
export function WizardDialogShell({ step, stepIndex, total, onComplete, onDismiss, ctx, title }) {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const progress = total > 1 ? Math.round((stepIndex / total) * 100) : 0;
|
|
20
|
+
const StepComponent = step.Component;
|
|
21
|
+
// `title` lets a non-onboarding caller (e.g. the popup surface) supply its
|
|
22
|
+
// own header instead of the onboarding-specific step counter/"Setup" copy.
|
|
23
|
+
// Omitted by OnboardingWizard, so its header text is unchanged.
|
|
24
|
+
const headerLabel = title !== null && title !== void 0 ? title : (total > 1
|
|
25
|
+
? t('Onboarding.STEP_COUNTER', { current: stepIndex + 1, total })
|
|
26
|
+
: t('Onboarding.SETUP'));
|
|
27
|
+
return (_jsxs(Dialog, { open: true, fullWidth: true, maxWidth: "sm", disableEscapeKeyDown: step.blocking, onClose: step.blocking ? undefined : onDismiss, children: [_jsxs(DialogTitle, { sx: { pb: 0 }, children: [_jsx(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: _jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: headerLabel }) }), total > 1 && _jsx(LinearProgress, { variant: "determinate", value: progress, sx: { mt: 1, borderRadius: 1 } })] }), _jsx(DialogContent, { sx: { pt: 2 }, children: _jsx(StepComponent, { onComplete: onComplete, onDismiss: onDismiss, ctx: ctx }) })] }));
|
|
28
|
+
}
|
|
29
|
+
export default WizardDialogShell;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micha.bigler/ui-core-micha",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@emotion/react": "^11.14.0",
|
|
13
13
|
"@emotion/styled": "^11.14.1",
|
|
14
14
|
"@mui/material": "^7.3.11",
|
|
15
|
+
"@mui/x-charts": "^8.0.0",
|
|
15
16
|
"axios": "^1.16.1",
|
|
16
17
|
"i18next": "^25.10.10 || ^26.0.0",
|
|
17
18
|
"qrcode.react": "^4.2.0",
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@mui/icons-material": "^7.3.11",
|
|
31
|
+
"@mui/x-charts": "^8.0.0",
|
|
30
32
|
"@testing-library/react": "^16.3.2",
|
|
31
33
|
"i18next": "^26.2.0",
|
|
32
34
|
"jsdom": "^29.1.1",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
|
+
import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
|
|
4
|
+
import { useNeutralChartPalette } from './palette';
|
|
5
|
+
|
|
6
|
+
function labelledAxis(axes, label, defaults) {
|
|
7
|
+
const values = axes?.length ? axes : [defaults];
|
|
8
|
+
return values.map((axis) => ({ ...axis, label: axis.label || label }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Responsive MUI X-Charts bar preset. xAxisLabel and yAxisLabel (including units) are required.
|
|
13
|
+
* It supplies labelled axes, tooltips, and an automatic multi-series legend.
|
|
14
|
+
*/
|
|
15
|
+
export function BarChart({
|
|
16
|
+
series = [],
|
|
17
|
+
xAxisLabel,
|
|
18
|
+
yAxisLabel,
|
|
19
|
+
xAxis,
|
|
20
|
+
yAxis,
|
|
21
|
+
palette,
|
|
22
|
+
minHeight,
|
|
23
|
+
aspect,
|
|
24
|
+
...chartProps
|
|
25
|
+
}) {
|
|
26
|
+
const neutralPalette = useNeutralChartPalette();
|
|
27
|
+
if (!xAxisLabel || !yAxisLabel) {
|
|
28
|
+
throw new Error('BarChart requires xAxisLabel and yAxisLabel.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box data-testid="bar-chart-container" sx={{ width: '100%', minHeight, aspectRatio: aspect }}>
|
|
33
|
+
<MuiBarChart
|
|
34
|
+
{...chartProps}
|
|
35
|
+
series={series}
|
|
36
|
+
xAxis={labelledAxis(xAxis, xAxisLabel, { scaleType: 'band', data: [] })}
|
|
37
|
+
yAxis={labelledAxis(yAxis, yAxisLabel, { scaleType: 'linear' })}
|
|
38
|
+
colors={palette || neutralPalette.categorical}
|
|
39
|
+
hideLegend={series.length <= 1}
|
|
40
|
+
slotProps={{ tooltip: { trigger: 'axis' } }}
|
|
41
|
+
/>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|