@eifi1/ui-kit 0.11.0 → 0.13.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 +59 -34
- 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 +40 -4
- package/dist/components/amount-input.js +50 -12
- 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/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.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/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- 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.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- 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-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +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 +125 -27
- 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 +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- 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/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- 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 +37 -4
- package/dist/components/list.js +35 -14
- 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 +42 -20
- 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 +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- 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 +5 -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/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- 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 +122 -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 +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.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 +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- 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-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -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 +164 -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 +161 -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 +161 -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 +161 -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 +161 -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 +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -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 +283 -0
- package/dist/rhf/fields.js +631 -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/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- 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 +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- 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/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -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 +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- 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 +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- 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/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- 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 +82 -19
- 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 +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -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 +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -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 +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE
|
|
2
|
+
// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's
|
|
3
|
+
// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by
|
|
4
|
+
// hand inside the kit's `ChartContainer` — with a legend that could not switch
|
|
5
|
+
// anything, slices a keyboard could not reach, and colours from a local hex list.
|
|
6
|
+
//
|
|
7
|
+
// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and
|
|
8
|
+
// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the
|
|
9
|
+
// kit's own legends (`ToggleLegend`, `StaticLegend`).
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
|
|
12
|
+
import { Pie, PieChart as RechartsPieChart, Sector } from "recharts";
|
|
13
|
+
import type { PieLabelRenderProps, PieSectorShapeProps } from "recharts";
|
|
14
|
+
import { cn } from "../lib/cn";
|
|
15
|
+
import { horizontalStep } from "../lib/direction";
|
|
16
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
17
|
+
import { paletteFor } from "../theme/chart-palette";
|
|
18
|
+
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "./chart";
|
|
19
|
+
import { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from "./pie-chart-labels";
|
|
20
|
+
import { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from "./toggle-legend";
|
|
21
|
+
|
|
22
|
+
/** One slice. */
|
|
23
|
+
export interface PieChartSlice {
|
|
24
|
+
/** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the
|
|
25
|
+
* legend's hidden set. Labels can collide; keys must not. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** What the slice is called — in the legend, the tooltip and its accessible name. */
|
|
28
|
+
label: string;
|
|
29
|
+
/**
|
|
30
|
+
* The slice's size. A value that is not a positive finite number — `null` for a
|
|
31
|
+
* figure that is missing, a zero, a negative — is NOT drawn and not in the legend:
|
|
32
|
+
* an angle cannot be negative, and a zero-degree slice is one a pointer and a
|
|
33
|
+
* keyboard would both stop on to find nothing.
|
|
34
|
+
*/
|
|
35
|
+
value: number | null | undefined;
|
|
36
|
+
/** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in
|
|
37
|
+
* `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */
|
|
38
|
+
color?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`
|
|
43
|
+
* anchor can find it. `children` is the chart's own.
|
|
44
|
+
*/
|
|
45
|
+
export interface PieChartProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
46
|
+
data: readonly PieChartSlice[];
|
|
47
|
+
/** `"donut"` (default) leaves a hole with the total in it; `"pie"` is the full disc. */
|
|
48
|
+
variant?: "donut" | "pie";
|
|
49
|
+
/** Height of the plot in px (the legend is under it). Default 300. */
|
|
50
|
+
height?: number;
|
|
51
|
+
/** How a value is written in the tooltip, the slice's name, a `"value"` slice label
|
|
52
|
+
* and the donut's total. Default: `Intl.NumberFormat` in `locale`. */
|
|
53
|
+
formatValue?: (value: number) => string;
|
|
54
|
+
/**
|
|
55
|
+
* What is written beside each slice: its share (`"percent"`, the default), its
|
|
56
|
+
* value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a
|
|
57
|
+
* sliver sits on its neighbour's. The figure is in the tooltip and the slice's name
|
|
58
|
+
* either way.
|
|
59
|
+
*/
|
|
60
|
+
sliceLabels?: "percent" | "value" | "none";
|
|
61
|
+
/** The smallest share (0–1) that gets a slice label. Default 0.04. */
|
|
62
|
+
minLabelShare?: number;
|
|
63
|
+
/** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default
|
|
64
|
+
* `true`; ignored for `variant="pie"`, which has no centre to draw in. */
|
|
65
|
+
center?: boolean;
|
|
66
|
+
/** The caption in the donut's centre. Default: `labels.total`. */
|
|
67
|
+
centerLabel?: ReactNode;
|
|
68
|
+
/** The figure in the donut's centre. Default: the drawn total through `formatValue`. */
|
|
69
|
+
centerValue?: ReactNode;
|
|
70
|
+
/**
|
|
71
|
+
* The legend under the chart. `"toggle"` (default): each entry is a switch that
|
|
72
|
+
* takes its slice off the chart, and the percentages are then of what is left —
|
|
73
|
+
* the shares always read "of this picture". `"static"`: a key with nothing to press.
|
|
74
|
+
*/
|
|
75
|
+
legend?: "toggle" | "static" | "none";
|
|
76
|
+
/** The slices switched off, for a caller that owns the set (a URL, a saved view).
|
|
77
|
+
* Left out, the chart keeps its own. */
|
|
78
|
+
hiddenSlices?: ReadonlySet<string>;
|
|
79
|
+
/** Called with the new hidden set whenever the legend switches a slice. */
|
|
80
|
+
onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;
|
|
81
|
+
/** A slice was activated — by click, or Enter/Space on a focused slice. Makes each
|
|
82
|
+
* slice a `role="button"`. */
|
|
83
|
+
onSliceClick?: (key: string, slice: PieChartSlice) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and
|
|
86
|
+
* `"value"` slice labels — so the host's demo-mode rule blurs it. A percentage alone
|
|
87
|
+
* reveals no amount, so `"percent"` labels stay readable, and the slices themselves
|
|
88
|
+
* are not tagged: a blurred ring is no picture at all.
|
|
89
|
+
*/
|
|
90
|
+
redact?: boolean;
|
|
91
|
+
/** What to show with nothing to draw. Default: `labels.empty`. */
|
|
92
|
+
empty?: ReactNode;
|
|
93
|
+
/** BCP 47 tag for the numbers and percentages. Default: the provider's. */
|
|
94
|
+
locale?: string;
|
|
95
|
+
labels?: Partial<PieChartLabels>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A slice as drawn: only the ones with a positive value, in `data` order. */
|
|
99
|
+
interface DrawnSlice {
|
|
100
|
+
slice: PieChartSlice;
|
|
101
|
+
value: number;
|
|
102
|
+
color: string;
|
|
103
|
+
/** Of what is drawn, 0–1. */
|
|
104
|
+
share: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the
|
|
108
|
+
// swatch, so no series config is needed — and none is wanted, because a config key is a
|
|
109
|
+
// CSS identifier and a slice key is the caller's data (see `ChartStyle`).
|
|
110
|
+
const PIE_CONFIG: ChartConfig = {};
|
|
111
|
+
|
|
112
|
+
const RADIAN = Math.PI / 180;
|
|
113
|
+
|
|
114
|
+
/** The shared tooltip with the bubble tagged `data-private` — see the treemap's
|
|
115
|
+
* `RedactedTooltipContent` for why it has to BE the element recharts clones. */
|
|
116
|
+
function RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {
|
|
117
|
+
return (
|
|
118
|
+
<div data-private>
|
|
119
|
+
<ChartTooltipContent {...props} />
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Part-to-whole chart: a donut (or `variant="pie"`), one slice per
|
|
126
|
+
* {@link PieChartSlice}, coloured from the chart ramp by index.
|
|
127
|
+
*
|
|
128
|
+
* **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving
|
|
129
|
+
* tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —
|
|
130
|
+
* clockwise from twelve o'clock, the order of the legend — with ←/→ following the
|
|
131
|
+
* reading direction in RTL; Home/End go to the first and last. Each slice is named
|
|
132
|
+
* "Label: value (xx%)" (`labels.slice`), because a colour and an angle are nothing a
|
|
133
|
+
* screen reader can say, and focusing one reads it out in the donut's centre. With
|
|
134
|
+
* `onSliceClick`, Enter and Space activate it as a click does.
|
|
135
|
+
*
|
|
136
|
+
* The legend switches slices off (`legend="toggle"`); the percentages are always of
|
|
137
|
+
* what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend
|
|
138
|
+
* and the centre follow the page.
|
|
139
|
+
*
|
|
140
|
+
* With nothing to draw it says so (`empty`) rather than drawing an empty box.
|
|
141
|
+
*
|
|
142
|
+
* Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.
|
|
143
|
+
*/
|
|
144
|
+
export function PieChart({
|
|
145
|
+
data,
|
|
146
|
+
variant = "donut",
|
|
147
|
+
height = 300,
|
|
148
|
+
formatValue: formatValueProp,
|
|
149
|
+
sliceLabels = "percent",
|
|
150
|
+
minLabelShare = 0.04,
|
|
151
|
+
center = true,
|
|
152
|
+
centerLabel,
|
|
153
|
+
centerValue,
|
|
154
|
+
legend = "toggle",
|
|
155
|
+
hiddenSlices: hiddenProp,
|
|
156
|
+
onHiddenSlicesChange,
|
|
157
|
+
onSliceClick,
|
|
158
|
+
redact = false,
|
|
159
|
+
empty,
|
|
160
|
+
locale: localeProp,
|
|
161
|
+
labels: labelsProp,
|
|
162
|
+
className,
|
|
163
|
+
style,
|
|
164
|
+
...rest
|
|
165
|
+
}: PieChartProps) {
|
|
166
|
+
const labels = useKitLabels("pieChart", DEFAULT_PIE_CHART_LABELS, labelsProp);
|
|
167
|
+
const locale = useKitLocale(localeProp);
|
|
168
|
+
const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
|
|
169
|
+
const percentFormats = useMemo(
|
|
170
|
+
() => ({
|
|
171
|
+
whole: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }),
|
|
172
|
+
fine: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }),
|
|
173
|
+
}),
|
|
174
|
+
[locale],
|
|
175
|
+
);
|
|
176
|
+
// Memoised, like everything the sectors are drawn from: they must not be rebuilt on
|
|
177
|
+
// this component's own re-renders (see `pie` below).
|
|
178
|
+
const formatValue = useCallback(
|
|
179
|
+
(v: number) => (formatValueProp ? formatValueProp(v) : numberFormat.format(v)),
|
|
180
|
+
[formatValueProp, numberFormat],
|
|
181
|
+
);
|
|
182
|
+
// One decimal under 10 %, where rounding to whole percents starts to lie — a slice
|
|
183
|
+
// of 0.4 % labelled "0%" says it is not there.
|
|
184
|
+
const formatShare = useCallback(
|
|
185
|
+
(share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),
|
|
186
|
+
[percentFormats],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());
|
|
190
|
+
const hidden = hiddenProp ?? ownHidden;
|
|
191
|
+
const toggle = (key: string) => {
|
|
192
|
+
const next = toggleHidden(hidden, key);
|
|
193
|
+
if (hiddenProp === undefined) setOwnHidden(next);
|
|
194
|
+
onHiddenSlicesChange?.(next);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice
|
|
198
|
+
// keeps its colour when a neighbour is switched off or has no value.
|
|
199
|
+
const { drawable, drawn, total, pieData } = useMemo(() => {
|
|
200
|
+
const drawable = data
|
|
201
|
+
.map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))
|
|
202
|
+
.filter((d): d is { slice: PieChartSlice; value: number; color: string } =>
|
|
203
|
+
typeof d.value === "number" && Number.isFinite(d.value) && d.value > 0,
|
|
204
|
+
);
|
|
205
|
+
const shown = drawable.filter((d) => !hidden.has(d.slice.key));
|
|
206
|
+
const total = shown.reduce((acc, d) => acc + d.value, 0);
|
|
207
|
+
const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));
|
|
208
|
+
const pieData = drawn.map((d) => ({
|
|
209
|
+
key: d.slice.key,
|
|
210
|
+
label: d.slice.label,
|
|
211
|
+
value: d.value,
|
|
212
|
+
fill: d.color,
|
|
213
|
+
}));
|
|
214
|
+
return { drawable, drawn, total, pieData };
|
|
215
|
+
}, [data, hidden]);
|
|
216
|
+
|
|
217
|
+
// The roving tab stop, and the slice that has focus (which the centre reads out).
|
|
218
|
+
const [stop, setStop] = useState(0);
|
|
219
|
+
const [focused, setFocused] = useState<number | undefined>(undefined);
|
|
220
|
+
const current = Math.min(stop, Math.max(0, drawn.length - 1));
|
|
221
|
+
const refs = useRef<(SVGGElement | null)[]>([]);
|
|
222
|
+
const focusedSlice = focused !== undefined ? drawn[focused] : undefined;
|
|
223
|
+
|
|
224
|
+
// Read through a ref: the sectors are drawn once per `data`, and a caller's inline
|
|
225
|
+
// `onSliceClick` is a new function every render.
|
|
226
|
+
const clickRef = useRef(onSliceClick);
|
|
227
|
+
useLayoutEffect(() => {
|
|
228
|
+
clickRef.current = onSliceClick;
|
|
229
|
+
});
|
|
230
|
+
const clickable = onSliceClick !== undefined;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its
|
|
234
|
+
* own that carries the pointer's hover and click, so those are untouched.
|
|
235
|
+
*
|
|
236
|
+
* Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in
|
|
237
|
+
* `SeriesChart`: an active pie sector is re-parented into an extra layer, which
|
|
238
|
+
* remounts this element and drops the focus it just took. The donut's centre is the
|
|
239
|
+
* keyboard's read-out instead.
|
|
240
|
+
*/
|
|
241
|
+
const shape = useCallback(
|
|
242
|
+
(props: PieSectorShapeProps) => {
|
|
243
|
+
const {
|
|
244
|
+
index,
|
|
245
|
+
isActive: _isActive,
|
|
246
|
+
tabIndex: _tabIndex,
|
|
247
|
+
animationElapsedTime: _elapsed,
|
|
248
|
+
isAnimating: _animating,
|
|
249
|
+
isEntrance: _entrance,
|
|
250
|
+
// recharts hands the shape its own React `key` among the props. Spread into
|
|
251
|
+
// JSX, a `key` inside a props object is a warning on every render (React 19),
|
|
252
|
+
// so it is lifted out here and passed on its own.
|
|
253
|
+
key,
|
|
254
|
+
...sector
|
|
255
|
+
} = props as PieSectorShapeProps & {
|
|
256
|
+
animationElapsedTime?: number;
|
|
257
|
+
isAnimating?: boolean;
|
|
258
|
+
isEntrance?: boolean;
|
|
259
|
+
key?: string | number | null;
|
|
260
|
+
};
|
|
261
|
+
const d = drawn[index];
|
|
262
|
+
if (!d) return <Sector key={key ?? undefined} {...sector} />;
|
|
263
|
+
const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {
|
|
264
|
+
const step = horizontalStep(event.key, event.currentTarget);
|
|
265
|
+
let target: number | undefined;
|
|
266
|
+
if (step) target = index + step;
|
|
267
|
+
else if (event.key === "ArrowDown") target = index + 1;
|
|
268
|
+
else if (event.key === "ArrowUp") target = index - 1;
|
|
269
|
+
else if (event.key === "Home") target = 0;
|
|
270
|
+
else if (event.key === "End") target = drawn.length - 1;
|
|
271
|
+
if (target !== undefined) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
// Round the circle: past the last slice is the first, as the eye goes.
|
|
274
|
+
refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();
|
|
275
|
+
} else if (clickable && (event.key === "Enter" || event.key === " ")) {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
clickRef.current?.(d.slice.key, d.slice);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
return (
|
|
281
|
+
<g
|
|
282
|
+
ref={(node) => {
|
|
283
|
+
refs.current[index] = node;
|
|
284
|
+
}}
|
|
285
|
+
role={clickable ? "button" : "img"}
|
|
286
|
+
// The first slice until the effect below moves the stop.
|
|
287
|
+
tabIndex={index === 0 ? 0 : -1}
|
|
288
|
+
aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}
|
|
289
|
+
data-slice={d.slice.key}
|
|
290
|
+
onKeyDown={onKeyDown}
|
|
291
|
+
onFocus={() => {
|
|
292
|
+
setStop(index);
|
|
293
|
+
setFocused(index);
|
|
294
|
+
}}
|
|
295
|
+
onBlur={() => setFocused(undefined)}
|
|
296
|
+
// CSS beats a presentation attribute, so this recolours the sector's own
|
|
297
|
+
// stroke for as long as it has keyboard focus.
|
|
298
|
+
className="outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]"
|
|
299
|
+
>
|
|
300
|
+
<Sector key={key ?? undefined} {...sector} />
|
|
301
|
+
</g>
|
|
302
|
+
);
|
|
303
|
+
},
|
|
304
|
+
[drawn, labels, formatValue, formatShare, clickable],
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const label = useCallback(
|
|
308
|
+
(props: PieLabelRenderProps) => {
|
|
309
|
+
const d = drawn[props.index ?? -1];
|
|
310
|
+
const cx = Number(props.cx);
|
|
311
|
+
const cy = Number(props.cy);
|
|
312
|
+
const radius = Number(props.outerRadius);
|
|
313
|
+
const mid = Number(props.midAngle);
|
|
314
|
+
if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;
|
|
315
|
+
const r = radius + 10;
|
|
316
|
+
const x = cx + r * Math.cos(-mid * RADIAN);
|
|
317
|
+
const y = cy + r * Math.sin(-mid * RADIAN);
|
|
318
|
+
const isValue = sliceLabels === "value";
|
|
319
|
+
return (
|
|
320
|
+
<text
|
|
321
|
+
x={x}
|
|
322
|
+
y={y}
|
|
323
|
+
textAnchor={x >= cx ? "start" : "end"}
|
|
324
|
+
dominantBaseline="central"
|
|
325
|
+
fontSize={11}
|
|
326
|
+
fill="var(--text-secondary)"
|
|
327
|
+
data-private={redact && isValue ? "" : undefined}
|
|
328
|
+
style={{ pointerEvents: "none" }}
|
|
329
|
+
>
|
|
330
|
+
{isValue ? formatValue(d.value) : formatShare(d.share)}
|
|
331
|
+
</text>
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
[drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare],
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const withLabels = sliceLabels !== "none";
|
|
338
|
+
// Room outside the ring for the labels, or the ring fills the box.
|
|
339
|
+
const outer = withLabels ? 72 : 88;
|
|
340
|
+
const inner = variant === "donut" ? Math.round(outer * 0.6) : 0;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts
|
|
344
|
+
* keys its sectors by an animation id that is renewed whenever the Pie's props object
|
|
345
|
+
* is — i.e. on every render of the Pie — so re-rendering it remounts every sector,
|
|
346
|
+
* the one with focus included, which dropped the focus to <body> on the very state
|
|
347
|
+
* update that focusing a slice makes. An unchanged element is one React does not
|
|
348
|
+
* re-render, so the sectors stay put, and the roving tab stop is moved on them in
|
|
349
|
+
* place (the effect below).
|
|
350
|
+
*/
|
|
351
|
+
const pie = useMemo(
|
|
352
|
+
() => (
|
|
353
|
+
<Pie
|
|
354
|
+
data={pieData}
|
|
355
|
+
dataKey="value"
|
|
356
|
+
nameKey="label"
|
|
357
|
+
cx="50%"
|
|
358
|
+
cy="50%"
|
|
359
|
+
outerRadius={`${outer}%`}
|
|
360
|
+
innerRadius={`${inner}%`}
|
|
361
|
+
startAngle={90}
|
|
362
|
+
endAngle={-270}
|
|
363
|
+
// The card showing through between the slices, as the treemap's gutter.
|
|
364
|
+
stroke="var(--bg-surface)"
|
|
365
|
+
strokeWidth={2}
|
|
366
|
+
rootTabIndex={-1}
|
|
367
|
+
isAnimationActive={false}
|
|
368
|
+
shape={shape}
|
|
369
|
+
label={withLabels ? label : false}
|
|
370
|
+
labelLine={false}
|
|
371
|
+
onClick={
|
|
372
|
+
clickable
|
|
373
|
+
? (_entry: unknown, index: number) => {
|
|
374
|
+
const d = drawn[index];
|
|
375
|
+
if (d) clickRef.current?.(d.slice.key, d.slice);
|
|
376
|
+
}
|
|
377
|
+
: undefined
|
|
378
|
+
}
|
|
379
|
+
/>
|
|
380
|
+
),
|
|
381
|
+
[pieData, drawn, shape, label, withLabels, outer, inner, clickable],
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// The roving tab stop, moved on the sectors in place (they are not re-rendered).
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
refs.current.forEach((el, i) => el?.setAttribute("tabindex", i === current ? "0" : "-1"));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const legendEntries: LegendEntry[] = drawable.map((d) => ({
|
|
390
|
+
key: d.slice.key,
|
|
391
|
+
label: d.slice.label,
|
|
392
|
+
color: d.color,
|
|
393
|
+
}));
|
|
394
|
+
|
|
395
|
+
const emptyBox = (
|
|
396
|
+
<div
|
|
397
|
+
className="flex items-center justify-center text-sm text-[var(--text-muted)]"
|
|
398
|
+
style={{ height }}
|
|
399
|
+
>
|
|
400
|
+
{empty ?? labels.empty}
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// Nothing drawable at all: no chart, and no legend of switches for nothing.
|
|
405
|
+
if (drawable.length === 0) {
|
|
406
|
+
return (
|
|
407
|
+
<div {...rest} className={cn("w-full", className)} style={style}>
|
|
408
|
+
{emptyBox}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const Content = redact ? RedactedTooltipContent : ChartTooltipContent;
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<div {...rest} className={cn("w-full", className)} style={style}>
|
|
417
|
+
{drawn.length === 0 ? (
|
|
418
|
+
// Every slice switched off: the legend below is how they come back.
|
|
419
|
+
emptyBox
|
|
420
|
+
) : (
|
|
421
|
+
<div role="group" aria-label={labels.slices} className="relative" style={{ height }}>
|
|
422
|
+
<ChartContainer
|
|
423
|
+
config={PIE_CONFIG}
|
|
424
|
+
className={cn("h-full", clickable && "[&_.recharts-pie-sector]:cursor-pointer")}
|
|
425
|
+
>
|
|
426
|
+
<RechartsPieChart accessibilityLayer={false}>
|
|
427
|
+
<ChartTooltip
|
|
428
|
+
content={
|
|
429
|
+
<Content
|
|
430
|
+
hideLabel
|
|
431
|
+
valueFormatter={(v) =>
|
|
432
|
+
`${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`
|
|
433
|
+
}
|
|
434
|
+
/>
|
|
435
|
+
}
|
|
436
|
+
/>
|
|
437
|
+
{pie}
|
|
438
|
+
</RechartsPieChart>
|
|
439
|
+
</ChartContainer>
|
|
440
|
+
{variant === "donut" && center && (
|
|
441
|
+
<div
|
|
442
|
+
data-private={redact ? "" : undefined}
|
|
443
|
+
// While a slice has keyboard focus the centre reads it out — the
|
|
444
|
+
// keyboard's tooltip. Hidden from assistive technology then: the slice's
|
|
445
|
+
// own name has just said the same.
|
|
446
|
+
aria-hidden={focusedSlice ? true : undefined}
|
|
447
|
+
className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center"
|
|
448
|
+
>
|
|
449
|
+
<span className="max-w-[40%] truncate text-xs text-[var(--text-muted)]">
|
|
450
|
+
{focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}
|
|
451
|
+
</span>
|
|
452
|
+
<span className="max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]">
|
|
453
|
+
{focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}
|
|
454
|
+
</span>
|
|
455
|
+
{focusedSlice && (
|
|
456
|
+
<span className="text-xs tabular-nums text-[var(--text-muted)]">
|
|
457
|
+
{formatShare(focusedSlice.share)}
|
|
458
|
+
</span>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
{legend === "toggle" && (
|
|
465
|
+
<ToggleLegend
|
|
466
|
+
entries={legendEntries}
|
|
467
|
+
hidden={hidden}
|
|
468
|
+
onToggle={toggle}
|
|
469
|
+
showSingle
|
|
470
|
+
className="justify-center"
|
|
471
|
+
labels={{ legend: labels.legend }}
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
{legend === "static" && (
|
|
475
|
+
<StaticLegend entries={legendEntries} className="justify-center" aria-label={labels.legend} />
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
@@ -169,6 +169,9 @@ export function Popover({
|
|
|
169
169
|
{trigger({ open, toggle, ref: triggerRef })}
|
|
170
170
|
{open && pos &&
|
|
171
171
|
createPortal(
|
|
172
|
+
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the
|
|
173
|
+
dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for
|
|
174
|
+
whatever inside it holds focus. */
|
|
172
175
|
<div
|
|
173
176
|
// `...rest` first: the role, the id the trigger's `aria-controls` points at,
|
|
174
177
|
// the `tabIndex` the trap needs and the measured position are what make this
|