@eifi1/ui-kit 0.11.0 → 0.12.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/README.md +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Pie, PieChart as RechartsPieChart, Sector } from "recharts";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { horizontalStep } from "../lib/direction.js";
|
|
7
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
8
|
+
import { paletteFor } from "../theme/chart-palette.js";
|
|
9
|
+
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
|
|
10
|
+
import { DEFAULT_PIE_CHART_LABELS } from "./pie-chart-labels.js";
|
|
11
|
+
import { StaticLegend, ToggleLegend, toggleHidden } from "./toggle-legend.js";
|
|
12
|
+
const PIE_CONFIG = {};
|
|
13
|
+
const RADIAN = Math.PI / 180;
|
|
14
|
+
function RedactedTooltipContent(props) {
|
|
15
|
+
return /* @__PURE__ */ jsx("div", { "data-private": true, children: /* @__PURE__ */ jsx(ChartTooltipContent, { ...props }) });
|
|
16
|
+
}
|
|
17
|
+
function PieChart({
|
|
18
|
+
data,
|
|
19
|
+
variant = "donut",
|
|
20
|
+
height = 300,
|
|
21
|
+
formatValue: formatValueProp,
|
|
22
|
+
sliceLabels = "percent",
|
|
23
|
+
minLabelShare = 0.04,
|
|
24
|
+
center = true,
|
|
25
|
+
centerLabel,
|
|
26
|
+
centerValue,
|
|
27
|
+
legend = "toggle",
|
|
28
|
+
hiddenSlices: hiddenProp,
|
|
29
|
+
onHiddenSlicesChange,
|
|
30
|
+
onSliceClick,
|
|
31
|
+
redact = false,
|
|
32
|
+
empty,
|
|
33
|
+
locale: localeProp,
|
|
34
|
+
labels: labelsProp,
|
|
35
|
+
className,
|
|
36
|
+
style,
|
|
37
|
+
...rest
|
|
38
|
+
}) {
|
|
39
|
+
const labels = useKitLabels("pieChart", DEFAULT_PIE_CHART_LABELS, labelsProp);
|
|
40
|
+
const locale = useKitLocale(localeProp);
|
|
41
|
+
const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
|
|
42
|
+
const percentFormats = useMemo(
|
|
43
|
+
() => ({
|
|
44
|
+
whole: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }),
|
|
45
|
+
fine: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 })
|
|
46
|
+
}),
|
|
47
|
+
[locale]
|
|
48
|
+
);
|
|
49
|
+
const formatValue = useCallback(
|
|
50
|
+
(v) => formatValueProp ? formatValueProp(v) : numberFormat.format(v),
|
|
51
|
+
[formatValueProp, numberFormat]
|
|
52
|
+
);
|
|
53
|
+
const formatShare = useCallback(
|
|
54
|
+
(share) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),
|
|
55
|
+
[percentFormats]
|
|
56
|
+
);
|
|
57
|
+
const [ownHidden, setOwnHidden] = useState(() => /* @__PURE__ */ new Set());
|
|
58
|
+
const hidden = hiddenProp ?? ownHidden;
|
|
59
|
+
const toggle = (key) => {
|
|
60
|
+
const next = toggleHidden(hidden, key);
|
|
61
|
+
if (hiddenProp === void 0) setOwnHidden(next);
|
|
62
|
+
onHiddenSlicesChange?.(next);
|
|
63
|
+
};
|
|
64
|
+
const { drawable, drawn, total, pieData } = useMemo(() => {
|
|
65
|
+
const drawable2 = data.map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) })).filter(
|
|
66
|
+
(d) => typeof d.value === "number" && Number.isFinite(d.value) && d.value > 0
|
|
67
|
+
);
|
|
68
|
+
const shown = drawable2.filter((d) => !hidden.has(d.slice.key));
|
|
69
|
+
const total2 = shown.reduce((acc, d) => acc + d.value, 0);
|
|
70
|
+
const drawn2 = shown.map((d) => ({ ...d, share: total2 > 0 ? d.value / total2 : 0 }));
|
|
71
|
+
const pieData2 = drawn2.map((d) => ({
|
|
72
|
+
key: d.slice.key,
|
|
73
|
+
label: d.slice.label,
|
|
74
|
+
value: d.value,
|
|
75
|
+
fill: d.color
|
|
76
|
+
}));
|
|
77
|
+
return { drawable: drawable2, drawn: drawn2, total: total2, pieData: pieData2 };
|
|
78
|
+
}, [data, hidden]);
|
|
79
|
+
const [stop, setStop] = useState(0);
|
|
80
|
+
const [focused, setFocused] = useState(void 0);
|
|
81
|
+
const current = Math.min(stop, Math.max(0, drawn.length - 1));
|
|
82
|
+
const refs = useRef([]);
|
|
83
|
+
const focusedSlice = focused !== void 0 ? drawn[focused] : void 0;
|
|
84
|
+
const clickRef = useRef(onSliceClick);
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
clickRef.current = onSliceClick;
|
|
87
|
+
});
|
|
88
|
+
const clickable = onSliceClick !== void 0;
|
|
89
|
+
const shape = useCallback(
|
|
90
|
+
(props) => {
|
|
91
|
+
const {
|
|
92
|
+
index,
|
|
93
|
+
isActive: _isActive,
|
|
94
|
+
tabIndex: _tabIndex,
|
|
95
|
+
animationElapsedTime: _elapsed,
|
|
96
|
+
isAnimating: _animating,
|
|
97
|
+
isEntrance: _entrance,
|
|
98
|
+
// recharts hands the shape its own React `key` among the props. Spread into
|
|
99
|
+
// JSX, a `key` inside a props object is a warning on every render (React 19),
|
|
100
|
+
// so it is lifted out here and passed on its own.
|
|
101
|
+
key,
|
|
102
|
+
...sector
|
|
103
|
+
} = props;
|
|
104
|
+
const d = drawn[index];
|
|
105
|
+
if (!d) return /* @__PURE__ */ jsx(Sector, { ...sector }, key ?? void 0);
|
|
106
|
+
const onKeyDown = (event) => {
|
|
107
|
+
const step = horizontalStep(event.key, event.currentTarget);
|
|
108
|
+
let target;
|
|
109
|
+
if (step) target = index + step;
|
|
110
|
+
else if (event.key === "ArrowDown") target = index + 1;
|
|
111
|
+
else if (event.key === "ArrowUp") target = index - 1;
|
|
112
|
+
else if (event.key === "Home") target = 0;
|
|
113
|
+
else if (event.key === "End") target = drawn.length - 1;
|
|
114
|
+
if (target !== void 0) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
refs.current[(target % drawn.length + drawn.length) % drawn.length]?.focus();
|
|
117
|
+
} else if (clickable && (event.key === "Enter" || event.key === " ")) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
clickRef.current?.(d.slice.key, d.slice);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return /* @__PURE__ */ jsx(
|
|
123
|
+
"g",
|
|
124
|
+
{
|
|
125
|
+
ref: (node) => {
|
|
126
|
+
refs.current[index] = node;
|
|
127
|
+
},
|
|
128
|
+
role: clickable ? "button" : "img",
|
|
129
|
+
tabIndex: index === 0 ? 0 : -1,
|
|
130
|
+
"aria-label": labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share)),
|
|
131
|
+
"data-slice": d.slice.key,
|
|
132
|
+
onKeyDown,
|
|
133
|
+
onFocus: () => {
|
|
134
|
+
setStop(index);
|
|
135
|
+
setFocused(index);
|
|
136
|
+
},
|
|
137
|
+
onBlur: () => setFocused(void 0),
|
|
138
|
+
className: "outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]",
|
|
139
|
+
children: /* @__PURE__ */ jsx(Sector, { ...sector }, key ?? void 0)
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
[drawn, labels, formatValue, formatShare, clickable]
|
|
144
|
+
);
|
|
145
|
+
const label = useCallback(
|
|
146
|
+
(props) => {
|
|
147
|
+
const d = drawn[props.index ?? -1];
|
|
148
|
+
const cx = Number(props.cx);
|
|
149
|
+
const cy = Number(props.cy);
|
|
150
|
+
const radius = Number(props.outerRadius);
|
|
151
|
+
const mid = Number(props.midAngle);
|
|
152
|
+
if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;
|
|
153
|
+
const r = radius + 10;
|
|
154
|
+
const x = cx + r * Math.cos(-mid * RADIAN);
|
|
155
|
+
const y = cy + r * Math.sin(-mid * RADIAN);
|
|
156
|
+
const isValue = sliceLabels === "value";
|
|
157
|
+
return /* @__PURE__ */ jsx(
|
|
158
|
+
"text",
|
|
159
|
+
{
|
|
160
|
+
x,
|
|
161
|
+
y,
|
|
162
|
+
textAnchor: x >= cx ? "start" : "end",
|
|
163
|
+
dominantBaseline: "central",
|
|
164
|
+
fontSize: 11,
|
|
165
|
+
fill: "var(--text-secondary)",
|
|
166
|
+
"data-private": redact && isValue ? "" : void 0,
|
|
167
|
+
style: { pointerEvents: "none" },
|
|
168
|
+
children: isValue ? formatValue(d.value) : formatShare(d.share)
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
[drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare]
|
|
173
|
+
);
|
|
174
|
+
const withLabels = sliceLabels !== "none";
|
|
175
|
+
const outer = withLabels ? 72 : 88;
|
|
176
|
+
const inner = variant === "donut" ? Math.round(outer * 0.6) : 0;
|
|
177
|
+
const pie = useMemo(
|
|
178
|
+
() => /* @__PURE__ */ jsx(
|
|
179
|
+
Pie,
|
|
180
|
+
{
|
|
181
|
+
data: pieData,
|
|
182
|
+
dataKey: "value",
|
|
183
|
+
nameKey: "label",
|
|
184
|
+
cx: "50%",
|
|
185
|
+
cy: "50%",
|
|
186
|
+
outerRadius: `${outer}%`,
|
|
187
|
+
innerRadius: `${inner}%`,
|
|
188
|
+
startAngle: 90,
|
|
189
|
+
endAngle: -270,
|
|
190
|
+
stroke: "var(--bg-surface)",
|
|
191
|
+
strokeWidth: 2,
|
|
192
|
+
rootTabIndex: -1,
|
|
193
|
+
isAnimationActive: false,
|
|
194
|
+
shape,
|
|
195
|
+
label: withLabels ? label : false,
|
|
196
|
+
labelLine: false,
|
|
197
|
+
onClick: clickable ? (_entry, index) => {
|
|
198
|
+
const d = drawn[index];
|
|
199
|
+
if (d) clickRef.current?.(d.slice.key, d.slice);
|
|
200
|
+
} : void 0
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
[pieData, drawn, shape, label, withLabels, outer, inner, clickable]
|
|
204
|
+
);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
refs.current.forEach((el, i) => el?.setAttribute("tabindex", i === current ? "0" : "-1"));
|
|
207
|
+
});
|
|
208
|
+
const legendEntries = drawable.map((d) => ({
|
|
209
|
+
key: d.slice.key,
|
|
210
|
+
label: d.slice.label,
|
|
211
|
+
color: d.color
|
|
212
|
+
}));
|
|
213
|
+
const emptyBox = /* @__PURE__ */ jsx(
|
|
214
|
+
"div",
|
|
215
|
+
{
|
|
216
|
+
className: "flex items-center justify-center text-sm text-[var(--text-muted)]",
|
|
217
|
+
style: { height },
|
|
218
|
+
children: empty ?? labels.empty
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
if (drawable.length === 0) {
|
|
222
|
+
return /* @__PURE__ */ jsx("div", { ...rest, className: cn("w-full", className), style, children: emptyBox });
|
|
223
|
+
}
|
|
224
|
+
const Content = redact ? RedactedTooltipContent : ChartTooltipContent;
|
|
225
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("w-full", className), style, children: [
|
|
226
|
+
drawn.length === 0 ? (
|
|
227
|
+
// Every slice switched off: the legend below is how they come back.
|
|
228
|
+
emptyBox
|
|
229
|
+
) : /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": labels.slices, className: "relative", style: { height }, children: [
|
|
230
|
+
/* @__PURE__ */ jsx(
|
|
231
|
+
ChartContainer,
|
|
232
|
+
{
|
|
233
|
+
config: PIE_CONFIG,
|
|
234
|
+
className: cn("h-full", clickable && "[&_.recharts-pie-sector]:cursor-pointer"),
|
|
235
|
+
children: /* @__PURE__ */ jsxs(RechartsPieChart, { accessibilityLayer: false, children: [
|
|
236
|
+
/* @__PURE__ */ jsx(
|
|
237
|
+
ChartTooltip,
|
|
238
|
+
{
|
|
239
|
+
content: /* @__PURE__ */ jsx(
|
|
240
|
+
Content,
|
|
241
|
+
{
|
|
242
|
+
hideLabel: true,
|
|
243
|
+
valueFormatter: (v) => `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
pie
|
|
249
|
+
] })
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
variant === "donut" && center && /* @__PURE__ */ jsxs(
|
|
253
|
+
"div",
|
|
254
|
+
{
|
|
255
|
+
"data-private": redact ? "" : void 0,
|
|
256
|
+
"aria-hidden": focusedSlice ? true : void 0,
|
|
257
|
+
className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",
|
|
258
|
+
children: [
|
|
259
|
+
/* @__PURE__ */ jsx("span", { className: "max-w-[40%] truncate text-xs text-[var(--text-muted)]", children: focusedSlice ? focusedSlice.slice.label : centerLabel ?? labels.total }),
|
|
260
|
+
/* @__PURE__ */ jsx("span", { className: "max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]", children: focusedSlice ? formatValue(focusedSlice.value) : centerValue ?? formatValue(total) }),
|
|
261
|
+
focusedSlice && /* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-[var(--text-muted)]", children: formatShare(focusedSlice.share) })
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
] }),
|
|
266
|
+
legend === "toggle" && /* @__PURE__ */ jsx(
|
|
267
|
+
ToggleLegend,
|
|
268
|
+
{
|
|
269
|
+
entries: legendEntries,
|
|
270
|
+
hidden,
|
|
271
|
+
onToggle: toggle,
|
|
272
|
+
showSingle: true,
|
|
273
|
+
className: "justify-center",
|
|
274
|
+
labels: { legend: labels.legend }
|
|
275
|
+
}
|
|
276
|
+
),
|
|
277
|
+
legend === "static" && /* @__PURE__ */ jsx(StaticLegend, { entries: legendEntries, className: "justify-center", "aria-label": labels.legend })
|
|
278
|
+
] });
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
PieChart
|
|
282
|
+
};
|
|
283
|
+
//# sourceMappingURL=pie-chart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /** Height of the plot in px (the legend is under it). Default 300. */\n height?: number;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: Partial<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say, and focusing one reads it out in the donut's centre. With\n * `onSliceClick`, Enter and Space activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = 300,\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const percentFormats = useMemo(\n () => ({\n whole: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }),\n fine: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }),\n }),\n [locale],\n );\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : numberFormat.format(v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, where rounding to whole percents starts to lie — a slice\n // of 0.4 % labelled \"0%\" says it is not there.\n const formatShare = useCallback(\n (share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),\n [percentFormats],\n );\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter((d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n const step = horizontalStep(event.key, event.currentTarget);\n let target: number | undefined;\n if (step) target = index + step;\n else if (event.key === \"ArrowDown\") target = index + 1;\n else if (event.key === \"ArrowUp\") target = index - 1;\n else if (event.key === \"Home\") target = 0;\n else if (event.key === \"End\") target = drawn.length - 1;\n if (target !== undefined) {\n event.preventDefault();\n // Round the circle: past the last slice is the first, as the eye goes.\n refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div\n className=\"flex items-center justify-center text-sm text-[var(--text-muted)]\"\n style={{ height }}\n >\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className=\"relative\" style={{ height }}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) =>\n `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`\n }\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n <span className=\"max-w-[40%] truncate text-xs text-[var(--text-muted)]\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-[var(--text-muted)]\">\n {formatShare(focusedSlice.share)}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["drawable","total","drawn","pieData"]}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
/** The words a progress bar adds of its own. */
|
|
5
|
+
interface ProgressBarLabels {
|
|
6
|
+
/** Shown after the value when `max={null}`: a quota with no ceiling. */
|
|
7
|
+
unlimited: string;
|
|
8
|
+
/** The `overage` line: how far past `max` the value is. `amount` is formatted. */
|
|
9
|
+
overLimit: (amount: string) => string;
|
|
10
|
+
}
|
|
11
|
+
declare const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels;
|
|
4
12
|
/**
|
|
5
13
|
* `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the
|
|
6
14
|
* names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what
|
|
@@ -31,13 +39,27 @@ interface ProgressBarSegment {
|
|
|
31
39
|
label?: ReactNode;
|
|
32
40
|
/** A stable React key, when `label` is not a string. */
|
|
33
41
|
key?: string;
|
|
42
|
+
/**
|
|
43
|
+
* List it in the `legend` but leave it out of the bar — its value, its share of the
|
|
44
|
+
* `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names
|
|
45
|
+
* EVERY bucket under its bar, the empty ones included, while the bar draws only the
|
|
46
|
+
* ones with a share: pass the empty buckets as `legendOnly` entries.
|
|
47
|
+
*/
|
|
48
|
+
legendOnly?: boolean;
|
|
34
49
|
}
|
|
35
50
|
interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
36
51
|
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
37
52
|
* and nobody knows how much is left (an import that reports no count). */
|
|
38
53
|
value?: number;
|
|
39
54
|
min?: number;
|
|
40
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's
|
|
57
|
+
* billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no
|
|
58
|
+
* bar is drawn; the label row shows the value and "Unlimited" (`progressBar.unlimited`)
|
|
59
|
+
* as plain text. `formatValue` is then called with `max` = `Infinity`; without it,
|
|
60
|
+
* the value is a plain number in the kit's locale.
|
|
61
|
+
*/
|
|
62
|
+
max?: number | null;
|
|
41
63
|
/**
|
|
42
64
|
* `progress` (default): a task moving toward completion — `role="progressbar"`.
|
|
43
65
|
* `meter`: a share of a known whole that is not going anywhere (budget used, disk
|
|
@@ -74,6 +96,25 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
74
96
|
/** With `segments`: a list under the bar naming each part with its swatch and value
|
|
75
97
|
* — the table view of the bar, in text tokens rather than the segment colour. */
|
|
76
98
|
legend?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* What a legend row shows at its end, in place of the part's formatted value — which
|
|
101
|
+
* it is handed, with the segment and its index. keksdose's cut card shows the share
|
|
102
|
+
* AND a private outflow amount per bucket (cut-card:150), which one figure could not:
|
|
103
|
+
* `legendValue={(seg, share) => <>{share} <Amount … /></>}`.
|
|
104
|
+
*/
|
|
105
|
+
legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;
|
|
106
|
+
/** A muted line under the bar — the plan's reset date, "3 of 5 seats in use". It
|
|
107
|
+
* describes the bar (`aria-describedby`). */
|
|
108
|
+
hint?: ReactNode;
|
|
109
|
+
/**
|
|
110
|
+
* When `value` is past `max`, say by how much, in a danger-coloured line under the bar
|
|
111
|
+
* (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the
|
|
112
|
+
* excess as a plain number; a function renders the line itself from the excess (a
|
|
113
|
+
* currency, "2 seats"). The bar stays full at `max` either way — colour it with
|
|
114
|
+
* `tone="danger"` if it should read as over. Ignored for `segments` and `max={null}`.
|
|
115
|
+
*/
|
|
116
|
+
overage?: boolean | ((over: number) => ReactNode);
|
|
117
|
+
labels?: Partial<ProgressBarLabels>;
|
|
77
118
|
}
|
|
78
119
|
/**
|
|
79
120
|
* A horizontal bar that fills toward `max`.
|
|
@@ -92,6 +133,6 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
92
133
|
* tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an
|
|
93
134
|
* unlayered rule beats every layered utility, `motion-reduce:animate-none` included.
|
|
94
135
|
*/
|
|
95
|
-
declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, segments, legend, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
|
|
136
|
+
declare function ProgressBar({ value, min, max: maxProp, variant, label, showValue, formatValue, tone, size, locale, segments, legend, legendValue, hint, overage, labels, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest }: ProgressBarProps): react.JSX.Element;
|
|
96
137
|
|
|
97
|
-
export { ProgressBar, type ProgressBarProps, type ProgressBarSegment, type ProgressBarSize, type ProgressBarTone };
|
|
138
|
+
export { DEFAULT_PROGRESS_BAR_LABELS, ProgressBar, type ProgressBarLabels, type ProgressBarProps, type ProgressBarSegment, type ProgressBarSize, type ProgressBarTone };
|
|
@@ -3,6 +3,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
6
|
+
const DEFAULT_PROGRESS_BAR_LABELS = {
|
|
7
|
+
unlimited: "Unlimited",
|
|
8
|
+
overLimit: (amount) => `${amount} over the limit`
|
|
9
|
+
};
|
|
6
10
|
const FILL = {
|
|
7
11
|
brand: "bg-[var(--brand)]",
|
|
8
12
|
neutral: "bg-[var(--text-muted)]",
|
|
@@ -36,7 +40,7 @@ function segmentFills(segments) {
|
|
|
36
40
|
function ProgressBar({
|
|
37
41
|
value,
|
|
38
42
|
min = 0,
|
|
39
|
-
max = 100,
|
|
43
|
+
max: maxProp = 100,
|
|
40
44
|
variant = "progress",
|
|
41
45
|
label,
|
|
42
46
|
showValue = false,
|
|
@@ -46,26 +50,49 @@ function ProgressBar({
|
|
|
46
50
|
locale,
|
|
47
51
|
segments,
|
|
48
52
|
legend = false,
|
|
53
|
+
legendValue,
|
|
54
|
+
hint,
|
|
55
|
+
overage,
|
|
56
|
+
labels,
|
|
49
57
|
className,
|
|
50
58
|
"aria-label": ariaLabel,
|
|
51
59
|
"aria-labelledby": ariaLabelledBy,
|
|
60
|
+
"aria-describedby": ariaDescribedBy,
|
|
52
61
|
...rest
|
|
53
62
|
}) {
|
|
54
63
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
64
|
+
const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
|
|
55
65
|
const kitLocale = useKitLocale(locale);
|
|
56
66
|
const labelId = useId();
|
|
67
|
+
const hintId = useId();
|
|
68
|
+
const hintNode = hint != null ? /* @__PURE__ */ jsx("p", { id: hintId, "data-part": "hint", className: "mt-1 text-xs text-[var(--text-muted)]", children: hint }) : null;
|
|
69
|
+
if (maxProp === null) {
|
|
70
|
+
const shown = value === void 0 || !Number.isFinite(value) ? void 0 : formatValue ? formatValue(value, Infinity, min) : new Intl.NumberFormat(kitLocale).format(value);
|
|
71
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("min-w-0", className), "data-state": "unlimited", children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2 text-sm", children: [
|
|
73
|
+
label != null && /* @__PURE__ */ jsx("span", { id: labelId, className: "min-w-0 text-[var(--text-secondary)]", children: label }),
|
|
74
|
+
/* @__PURE__ */ jsxs("span", { "data-part": "unlimited", className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: [
|
|
75
|
+
shown !== void 0 && `${shown} \xB7 `,
|
|
76
|
+
text.unlimited
|
|
77
|
+
] })
|
|
78
|
+
] }),
|
|
79
|
+
hintNode
|
|
80
|
+
] });
|
|
81
|
+
}
|
|
82
|
+
const max = maxProp;
|
|
57
83
|
const stacked = segments !== void 0;
|
|
58
84
|
const meter = stacked || variant === "meter";
|
|
59
85
|
const span = max - min;
|
|
60
86
|
const partValue = (v) => Number.isFinite(v) ? Math.max(0, v - min) : 0;
|
|
61
87
|
const fills = stacked ? segmentFills(segments) : [];
|
|
62
|
-
const
|
|
88
|
+
const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];
|
|
89
|
+
const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : void 0;
|
|
63
90
|
const format = (v) => formatValue ? formatValue(v, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
|
|
64
91
|
span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span
|
|
65
92
|
);
|
|
66
|
-
const partTexts = stacked ?
|
|
67
|
-
const
|
|
68
|
-
return typeof seg.label === "string" || typeof seg.label === "number" ? common.fieldValue(String(seg.label),
|
|
93
|
+
const partTexts = stacked ? barParts.map((seg) => {
|
|
94
|
+
const text2 = format(min + partValue(seg.value));
|
|
95
|
+
return typeof seg.label === "string" || typeof seg.label === "number" ? common.fieldValue(String(seg.label), text2) : text2;
|
|
69
96
|
}) : [];
|
|
70
97
|
const current = partTotal ?? (value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value);
|
|
71
98
|
const indeterminate = current === void 0;
|
|
@@ -73,6 +100,9 @@ function ProgressBar({
|
|
|
73
100
|
const fraction = clamped === void 0 || span <= 0 ? 0 : (clamped - min) / span;
|
|
74
101
|
const valueText = clamped === void 0 ? void 0 : format(clamped);
|
|
75
102
|
const ariaValueText = stacked && partTexts.length > 0 ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts) : valueText;
|
|
103
|
+
const over = overage && !stacked && value !== void 0 && Number.isFinite(value) && value > max ? value - max : 0;
|
|
104
|
+
const overageNode = over > 0 ? /* @__PURE__ */ jsx("p", { id: `${hintId}-over`, "data-part": "overage", className: "mt-1 text-xs text-[var(--danger)]", children: typeof overage === "function" ? overage(over) : text.overLimit(new Intl.NumberFormat(kitLocale).format(over)) }) : null;
|
|
105
|
+
const describedBy = [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(" ") || void 0;
|
|
76
106
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
|
|
77
107
|
const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
|
|
78
108
|
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-state": indeterminate ? "indeterminate" : "determinate", children: [
|
|
@@ -89,6 +119,7 @@ function ProgressBar({
|
|
|
89
119
|
role: meter ? "meter" : "progressbar",
|
|
90
120
|
"aria-label": name,
|
|
91
121
|
"aria-labelledby": labelledBy,
|
|
122
|
+
"aria-describedby": describedBy,
|
|
92
123
|
"aria-valuemin": indeterminate ? void 0 : min,
|
|
93
124
|
"aria-valuemax": indeterminate ? void 0 : max,
|
|
94
125
|
"aria-valuenow": clamped,
|
|
@@ -98,21 +129,23 @@ function ProgressBar({
|
|
|
98
129
|
children: stacked ? (
|
|
99
130
|
// The parts in a flex row with a 2px gap, so two neighbouring steps of one
|
|
100
131
|
// hue are parted by the track rather than by their difference in lightness.
|
|
101
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-full w-full gap-0.5", children: segments.map(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-full w-full gap-0.5", children: segments.map(
|
|
133
|
+
(seg, i) => seg.legendOnly ? null : /* @__PURE__ */ jsx(
|
|
134
|
+
"div",
|
|
135
|
+
{
|
|
136
|
+
"data-part": "segment",
|
|
137
|
+
className: cn(
|
|
138
|
+
"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
|
|
139
|
+
fills[i]
|
|
140
|
+
),
|
|
141
|
+
style: {
|
|
142
|
+
width: `${span <= 0 ? 0 : partValue(seg.value) / span * 100}%`,
|
|
143
|
+
backgroundColor: seg.className ? void 0 : seg.color
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
seg.key ?? i
|
|
147
|
+
)
|
|
148
|
+
) })
|
|
116
149
|
) : indeterminate ? /* @__PURE__ */ jsx(
|
|
117
150
|
"div",
|
|
118
151
|
{
|
|
@@ -152,11 +185,14 @@ function ProgressBar({
|
|
|
152
185
|
}
|
|
153
186
|
),
|
|
154
187
|
seg.label != null && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[var(--text-secondary)]", children: seg.label }),
|
|
155
|
-
/* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]", children: format(min + partValue(seg.value)) })
|
|
156
|
-
] }, seg.key ?? i)) })
|
|
188
|
+
/* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]", children: legendValue ? legendValue(seg, format(min + partValue(seg.value)), i) : format(min + partValue(seg.value)) })
|
|
189
|
+
] }, seg.key ?? i)) }),
|
|
190
|
+
overageNode,
|
|
191
|
+
hintNode
|
|
157
192
|
] });
|
|
158
193
|
}
|
|
159
194
|
export {
|
|
195
|
+
DEFAULT_PROGRESS_BAR_LABELS,
|
|
160
196
|
ProgressBar
|
|
161
197
|
};
|
|
162
198
|
//# sourceMappingURL=progress-bar.js.map
|