@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,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { KeyRound } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useKitLocale } from "../i18n/kit-labels.js";
|
|
7
|
+
import { Button, Card, EmptyState, Input } from "./ui.js";
|
|
8
|
+
import { Skeleton } from "./skeleton.js";
|
|
9
|
+
import { useAccountSettingsLabels } from "./account-settings-labels.js";
|
|
10
|
+
function PasskeysSetting({
|
|
11
|
+
passkeys,
|
|
12
|
+
loading,
|
|
13
|
+
onAdd,
|
|
14
|
+
adding = false,
|
|
15
|
+
onRename,
|
|
16
|
+
onDelete,
|
|
17
|
+
busyId,
|
|
18
|
+
nameField = true,
|
|
19
|
+
unavailable,
|
|
20
|
+
formatDate,
|
|
21
|
+
locale: localeProp,
|
|
22
|
+
labels: labelsProp,
|
|
23
|
+
className
|
|
24
|
+
}) {
|
|
25
|
+
const labels = useAccountSettingsLabels("passkeys", labelsProp);
|
|
26
|
+
const locale = useKitLocale(localeProp);
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const date = (value) => {
|
|
29
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
30
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
31
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
32
|
+
return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
|
|
33
|
+
};
|
|
34
|
+
const add = () => {
|
|
35
|
+
if (adding) return;
|
|
36
|
+
const typed = name;
|
|
37
|
+
Promise.resolve(onAdd(typed.trim())).then(() => setName((current) => current === typed ? "" : current)).catch(() => {
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const isLoading = loading || passkeys === void 0;
|
|
41
|
+
const items = passkeys ?? [];
|
|
42
|
+
const body = (() => {
|
|
43
|
+
if (unavailable != null && unavailable !== false) {
|
|
44
|
+
return /* @__PURE__ */ jsx(EmptyState, { variant: "inline", size: "sm", icon: /* @__PURE__ */ jsx(KeyRound, {}), title: unavailable });
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
isLoading ? /* @__PURE__ */ jsx(Skeleton, { lines: 2, label: labels.loading }) : items.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { variant: "inline", size: "sm", icon: /* @__PURE__ */ jsx(KeyRound, {}), title: labels.empty }) : /* @__PURE__ */ jsx("ul", { "aria-label": labels.list, className: "divide-y divide-[var(--border)]", children: items.map((item) => /* @__PURE__ */ jsx(
|
|
48
|
+
PasskeyRow,
|
|
49
|
+
{
|
|
50
|
+
item,
|
|
51
|
+
created: date(item.createdAt),
|
|
52
|
+
lastUsed: date(item.lastUsedAt),
|
|
53
|
+
busy: busyId !== void 0 && busyId !== null && busyId === item.id,
|
|
54
|
+
onRename,
|
|
55
|
+
onDelete,
|
|
56
|
+
labels
|
|
57
|
+
},
|
|
58
|
+
item.id
|
|
59
|
+
)) }),
|
|
60
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2 pt-1", children: [
|
|
61
|
+
nameField && /* @__PURE__ */ jsx(
|
|
62
|
+
Input,
|
|
63
|
+
{
|
|
64
|
+
label: labels.nameLabel,
|
|
65
|
+
placeholder: labels.namePlaceholder,
|
|
66
|
+
value: name,
|
|
67
|
+
maxLength: 120,
|
|
68
|
+
onChange: (e) => setName(e.target.value),
|
|
69
|
+
onKeyDown: (e) => {
|
|
70
|
+
if (e.key === "Enter") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
add();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
/* @__PURE__ */ jsxs(Button, { onClick: add, disabled: adding, "aria-busy": adding || void 0, children: [
|
|
78
|
+
/* @__PURE__ */ jsx(KeyRound, { className: "size-4", "aria-hidden": true }),
|
|
79
|
+
adding ? labels.adding : labels.add
|
|
80
|
+
] })
|
|
81
|
+
] })
|
|
82
|
+
] });
|
|
83
|
+
})();
|
|
84
|
+
return /* @__PURE__ */ jsxs(Card, { className: cn("p-4 space-y-3", className), children: [
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
86
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: labels.title }),
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-muted)]", children: labels.description })
|
|
88
|
+
] }),
|
|
89
|
+
body
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
92
|
+
function PasskeyRow({
|
|
93
|
+
item,
|
|
94
|
+
created,
|
|
95
|
+
lastUsed,
|
|
96
|
+
busy,
|
|
97
|
+
onRename,
|
|
98
|
+
onDelete,
|
|
99
|
+
labels
|
|
100
|
+
}) {
|
|
101
|
+
const [mode, setMode] = useState("view");
|
|
102
|
+
const [draft, setDraft] = useState(item.name);
|
|
103
|
+
const [returnTo, setReturnTo] = useState(null);
|
|
104
|
+
const cancelDeleteRef = useRef(null);
|
|
105
|
+
const renameRef = useRef(null);
|
|
106
|
+
const renameButtonRef = useRef(null);
|
|
107
|
+
const deleteButtonRef = useRef(null);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (mode === "delete") cancelDeleteRef.current?.focus();
|
|
110
|
+
else if (mode === "rename") renameRef.current?.select();
|
|
111
|
+
else if (returnTo) (returnTo === "rename" ? renameButtonRef : deleteButtonRef).current?.focus();
|
|
112
|
+
}, [mode, returnTo]);
|
|
113
|
+
const open = (next) => {
|
|
114
|
+
setReturnTo(next);
|
|
115
|
+
setDraft(item.name);
|
|
116
|
+
setMode(next);
|
|
117
|
+
};
|
|
118
|
+
const leave = () => setMode("view");
|
|
119
|
+
const escapeLeaves = (e) => {
|
|
120
|
+
if (e.key === "Escape") {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
leave();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const saveRename = () => {
|
|
127
|
+
const next = draft.trim();
|
|
128
|
+
if (!onRename || !next || busy) return;
|
|
129
|
+
if (next === item.name) {
|
|
130
|
+
leave();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
Promise.resolve(onRename(item.id, next)).then(leave).catch(() => {
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const confirmDelete = () => {
|
|
137
|
+
if (!onDelete || busy) return;
|
|
138
|
+
Promise.resolve(onDelete(item.id)).then(() => setMode("view")).catch(() => {
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed].filter(Boolean).join(" \xB7 ");
|
|
142
|
+
if (mode === "rename") {
|
|
143
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex flex-wrap items-center gap-2 py-2", children: [
|
|
144
|
+
/* @__PURE__ */ jsx(
|
|
145
|
+
Input,
|
|
146
|
+
{
|
|
147
|
+
ref: renameRef,
|
|
148
|
+
"aria-label": labels.renameField(item.name),
|
|
149
|
+
className: "min-w-0 flex-1",
|
|
150
|
+
value: draft,
|
|
151
|
+
maxLength: 120,
|
|
152
|
+
onChange: (e) => setDraft(e.target.value),
|
|
153
|
+
onKeyDown: (e) => {
|
|
154
|
+
if (e.key === "Enter") {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
saveRename();
|
|
157
|
+
} else if (e.key === "Escape") {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
leave();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: saveRename, disabled: busy || !draft.trim(), children: labels.save }),
|
|
166
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: leave, children: labels.cancel })
|
|
167
|
+
] });
|
|
168
|
+
}
|
|
169
|
+
if (mode === "delete") {
|
|
170
|
+
return /* @__PURE__ */ jsxs("li", { className: "space-y-2 py-2", children: [
|
|
171
|
+
/* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm", children: labels.deleteConfirm(item.name) }),
|
|
172
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
173
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "danger", onClick: confirmDelete, onKeyDown: escapeLeaves, disabled: busy, children: labels.confirmDelete }),
|
|
174
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelDeleteRef, size: "sm", variant: "ghost", onClick: leave, onKeyDown: escapeLeaves, children: labels.cancel })
|
|
175
|
+
] })
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 py-2", children: [
|
|
179
|
+
/* @__PURE__ */ jsx(KeyRound, { className: "size-4 shrink-0 text-[var(--text-muted)]", "aria-hidden": true }),
|
|
180
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
181
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: item.name }),
|
|
182
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-muted)]", children: meta })
|
|
183
|
+
] }),
|
|
184
|
+
onRename && /* @__PURE__ */ jsx(
|
|
185
|
+
Button,
|
|
186
|
+
{
|
|
187
|
+
ref: renameButtonRef,
|
|
188
|
+
size: "sm",
|
|
189
|
+
variant: "ghost",
|
|
190
|
+
disabled: busy,
|
|
191
|
+
onClick: () => open("rename"),
|
|
192
|
+
"aria-label": labels.renameItem(item.name),
|
|
193
|
+
children: labels.rename
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
onDelete && /* @__PURE__ */ jsx(
|
|
197
|
+
Button,
|
|
198
|
+
{
|
|
199
|
+
ref: deleteButtonRef,
|
|
200
|
+
size: "sm",
|
|
201
|
+
variant: "ghost",
|
|
202
|
+
tone: "danger",
|
|
203
|
+
disabled: busy,
|
|
204
|
+
onClick: () => open("delete"),
|
|
205
|
+
"aria-label": labels.deleteItem(item.name),
|
|
206
|
+
children: labels.delete
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
] });
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
PasskeysSetting
|
|
213
|
+
};
|
|
214
|
+
//# sourceMappingURL=passkeys-setting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/passkeys-setting.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { KeyRound } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { Button, Card, EmptyState, Input } from \"./ui\";\nimport { Skeleton } from \"./skeleton\";\nimport { useAccountSettingsLabels } from \"./account-settings-labels\";\nimport type { PasskeysSettingLabels } from \"./account-settings-labels\";\n\n/** One registered passkey, as the app's API lists it. */\nexport interface PasskeyItem {\n id: string | number;\n name: string;\n /** A `Date`, an ISO string or epoch ms. */\n createdAt?: Date | string | number | null;\n /** Null (or left out) reads as \"Never used\". */\n lastUsedAt?: Date | string | number | null;\n}\n\nexport interface PasskeysSettingProps {\n /** `undefined` while the first load is in flight (or pass `loading`). */\n passkeys: readonly PasskeyItem[] | undefined;\n loading?: boolean;\n /**\n * Run the WebAuthn ceremony and register the passkey — app-side, since the options\n * and the attestation are the app's server's business. Called with the trimmed name\n * field, which may be EMPTY: pick your own default (\"Passkey\", the device name).\n * Return the promise and the field clears when it resolves; a rejection (including\n * the user cancelling the OS prompt) keeps what was typed.\n */\n onAdd: (name: string) => void | Promise<unknown>;\n /** The ceremony is running. The add button says so and does not start a second one. */\n adding?: boolean;\n /** Offer \"Rename\" on each row. Return a promise to leave edit mode only on success. */\n onRename?: (id: PasskeyItem[\"id\"], name: string) => void | Promise<unknown>;\n /** Offer \"Delete\" on each row — always behind an inline confirmation. */\n onDelete?: (id: PasskeyItem[\"id\"]) => void | Promise<unknown>;\n /** A row whose rename or delete is in flight; its buttons are disabled. */\n busyId?: PasskeyItem[\"id\"] | null;\n /** Show the name field above the add button. Default true. */\n nameField?: boolean;\n /**\n * Shown INSTEAD of the list and the add button — passkeys are not supported by this\n * browser, or the page is not a secure context. Its wording is the app's: only the\n * app knows which of those it detected.\n */\n unavailable?: ReactNode;\n /** A date for the \"Added …\" / \"Last used …\" lines. Default: the locale's medium date. */\n formatDate?: (date: Date) => string;\n /** Overrides the provider's locale for the default date format. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { passkeys } }}>` > English. */\n labels?: Partial<PasskeysSettingLabels>;\n className?: string;\n}\n\n/**\n * The passkeys section of an account page: what is registered, when it was last used,\n * rename and delete, and \"Add passkey\" — the fourth card beside `ProfileSetting`,\n * `PasswordSetting` and `TwoFactorSetting`, and built the same way (a self-contained\n * `<Card>` owning its transient form state; the app owns the data and the API).\n *\n * Lifted from kastlan's passkeys-card. **The WebAuthn ceremony stays app-side**:\n * `navigator.credentials.create` needs options minted by the app's server and hands\n * back an attestation only that server can verify, so the kit calls `onAdd` and waits.\n *\n * **Delete confirms inline**, in the row, rather than through `useConfirm`: this card\n * must work in an app without a `ConfirmProvider`, and the question belongs next to\n * the passkey it names. Focus moves to Cancel, as in every destructive confirm in the\n * kit, so the Enter that pressed Delete cannot also answer it.\n */\nexport function PasskeysSetting({\n passkeys,\n loading,\n onAdd,\n adding = false,\n onRename,\n onDelete,\n busyId,\n nameField = true,\n unavailable,\n formatDate,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: PasskeysSettingProps) {\n const labels = useAccountSettingsLabels(\"passkeys\", labelsProp);\n const locale = useKitLocale(localeProp);\n const [name, setName] = useState(\"\");\n\n const date = (value: PasskeyItem[\"createdAt\"]): string | null => {\n if (value === null || value === undefined || value === \"\") return null;\n const d = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(d.getTime())) return null;\n return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(d);\n };\n\n const add = () => {\n if (adding) return;\n const typed = name;\n Promise.resolve(onAdd(typed.trim()))\n // Clear only what was sent: the field stays editable while the OS prompt is up.\n .then(() => setName((current) => (current === typed ? \"\" : current)))\n .catch(() => {});\n };\n\n const isLoading = loading || passkeys === undefined;\n const items = passkeys ?? [];\n\n const body = (() => {\n if (unavailable != null && unavailable !== false) {\n return <EmptyState variant=\"inline\" size=\"sm\" icon={<KeyRound />} title={unavailable} />;\n }\n return (\n <>\n {isLoading ? (\n <Skeleton lines={2} label={labels.loading} />\n ) : items.length === 0 ? (\n <EmptyState variant=\"inline\" size=\"sm\" icon={<KeyRound />} title={labels.empty} />\n ) : (\n <ul aria-label={labels.list} className=\"divide-y divide-[var(--border)]\">\n {items.map((item) => (\n <PasskeyRow\n key={item.id}\n item={item}\n created={date(item.createdAt)}\n lastUsed={date(item.lastUsedAt)}\n busy={busyId !== undefined && busyId !== null && busyId === item.id}\n onRename={onRename}\n onDelete={onDelete}\n labels={labels}\n />\n ))}\n </ul>\n )}\n <div className=\"space-y-2 pt-1\">\n {nameField && (\n <Input\n label={labels.nameLabel}\n placeholder={labels.namePlaceholder}\n value={name}\n maxLength={120}\n onChange={(e) => setName(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n add();\n }\n }}\n />\n )}\n <Button onClick={add} disabled={adding} aria-busy={adding || undefined}>\n <KeyRound className=\"size-4\" aria-hidden />\n {adding ? labels.adding : labels.add}\n </Button>\n </div>\n </>\n );\n })();\n\n return (\n <Card className={cn(\"p-4 space-y-3\", className)}>\n <div>\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {labels.description}\n </div>\n </div>\n {body}\n </Card>\n );\n}\n\ntype RowMode = \"view\" | \"rename\" | \"delete\";\n\nfunction PasskeyRow({\n item,\n created,\n lastUsed,\n busy,\n onRename,\n onDelete,\n labels,\n}: {\n item: PasskeyItem;\n created: string | null;\n lastUsed: string | null;\n busy: boolean;\n onRename?: PasskeysSettingProps[\"onRename\"];\n onDelete?: PasskeysSettingProps[\"onDelete\"];\n labels: PasskeysSettingLabels;\n}) {\n const [mode, setMode] = useState<RowMode>(\"view\");\n const [draft, setDraft] = useState(item.name);\n // Which button opened the sub-mode, so leaving it puts focus back there — the\n // confirm or the field unmounts under the focus, which otherwise falls to <body>.\n const [returnTo, setReturnTo] = useState<\"rename\" | \"delete\" | null>(null);\n const cancelDeleteRef = useRef<HTMLButtonElement>(null);\n const renameRef = useRef<HTMLInputElement>(null);\n const renameButtonRef = useRef<HTMLButtonElement>(null);\n const deleteButtonRef = useRef<HTMLButtonElement>(null);\n\n useEffect(() => {\n if (mode === \"delete\") cancelDeleteRef.current?.focus();\n else if (mode === \"rename\") renameRef.current?.select();\n else if (returnTo) (returnTo === \"rename\" ? renameButtonRef : deleteButtonRef).current?.focus();\n }, [mode, returnTo]);\n\n const open = (next: \"rename\" | \"delete\") => {\n setReturnTo(next);\n setDraft(item.name);\n setMode(next);\n };\n const leave = () => setMode(\"view\");\n const escapeLeaves = (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n leave();\n }\n };\n\n const saveRename = () => {\n const next = draft.trim();\n if (!onRename || !next || busy) return;\n if (next === item.name) {\n leave();\n return;\n }\n Promise.resolve(onRename(item.id, next))\n .then(leave)\n .catch(() => {});\n };\n\n const confirmDelete = () => {\n if (!onDelete || busy) return;\n // The row unmounts once the list refreshes; on a failure it stays, confirm open.\n Promise.resolve(onDelete(item.id))\n .then(() => setMode(\"view\"))\n .catch(() => {});\n };\n\n const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed]\n .filter(Boolean)\n .join(\" · \");\n\n if (mode === \"rename\") {\n return (\n <li className=\"flex flex-wrap items-center gap-2 py-2\">\n <Input\n ref={renameRef}\n aria-label={labels.renameField(item.name)}\n className=\"min-w-0 flex-1\"\n value={draft}\n maxLength={120}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n saveRename();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n leave();\n }\n }}\n />\n <Button size=\"sm\" onClick={saveRename} disabled={busy || !draft.trim()}>\n {labels.save}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={leave}>\n {labels.cancel}\n </Button>\n </li>\n );\n }\n\n if (mode === \"delete\") {\n return (\n <li className=\"space-y-2 py-2\">\n <p role=\"alert\" className=\"text-sm\">\n {labels.deleteConfirm(item.name)}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"danger\" onClick={confirmDelete} onKeyDown={escapeLeaves} disabled={busy}>\n {labels.confirmDelete}\n </Button>\n <Button ref={cancelDeleteRef} size=\"sm\" variant=\"ghost\" onClick={leave} onKeyDown={escapeLeaves}>\n {labels.cancel}\n </Button>\n </div>\n </li>\n );\n }\n\n return (\n <li className=\"flex items-center gap-3 py-2\">\n <KeyRound className=\"size-4 shrink-0 text-[var(--text-muted)]\" aria-hidden />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm font-medium\">{item.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">{meta}</div>\n </div>\n {onRename && (\n <Button\n ref={renameButtonRef}\n size=\"sm\"\n variant=\"ghost\"\n disabled={busy}\n onClick={() => open(\"rename\")}\n aria-label={labels.renameItem(item.name)}\n >\n {labels.rename}\n </Button>\n )}\n {onDelete && (\n <Button\n ref={deleteButtonRef}\n size=\"sm\"\n variant=\"ghost\"\n tone=\"danger\"\n disabled={busy}\n onClick={() => open(\"delete\")}\n aria-label={labels.deleteItem(item.name)}\n >\n {labels.delete}\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import { useRef } from "react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { CLIPS_ATTRIBUTE } from "../lib/clipping.js";
|
|
7
8
|
import { DropdownSearchHeader } from "./dropdown.js";
|
|
8
9
|
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
|
|
9
10
|
import { useOverlayHistory } from "../hooks/use-overlay-history.js";
|
|
@@ -80,7 +81,7 @@ function PickerSheet({
|
|
|
80
81
|
placeholder: searchPlaceholder
|
|
81
82
|
}
|
|
82
83
|
),
|
|
83
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children })
|
|
84
|
+
/* @__PURE__ */ jsx("div", { ...{ [CLIPS_ATTRIBUTE]: "" }, className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children })
|
|
84
85
|
]
|
|
85
86
|
}
|
|
86
87
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\nimport { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * Two of the div's own attributes are omitted because this component already owns\n * the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's\n * tooltip string, and `onClose` is \"the sheet was dismissed\", not the DOM's\n * `<dialog>` close event. Everything else a caller can pass reaches the sheet.\n */\nexport interface PickerSheetProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"onClose\"> {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n /** Default: `pickerSheet.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n children: ReactNode;\n}\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel,\n children,\n \"aria-label\": ariaLabel,\n className,\n style,\n ...rest\n}: PickerSheetProps) {\n // Every picker in the kit opens one of these on a phone, and most of them pass\n // their own `closeLabel` through as `undefined` — so this lookup is what makes the\n // one way out of a full-screen sheet speak the app's language without each of\n // them having to (see the prop's note in `EntityCombobox`).\n const labels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // A fallback ref so a caller that does not need the handle still gets the opening\n // focus, rather than the behaviour depending on whether a prop was passed. What that\n // focus is FOR is on the trap below.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n\n const sheetRef = useRef<HTMLDivElement>(null);\n /**\n * The sheet said `aria-modal=\"true\"` and then left focus on the field behind it, so\n * a screen reader was told to hide the page the user's cursor was still standing in\n * (the audit's *\"Only Modal manages focus\"*). Through the shared hook rather than\n * the bare `.focus()` that used to live here: the hook re-reads the tabbable list on\n * every Tab, which this panel needs more than most — its list is filtered as you\n * type, so the rows a trap captured at open time are gone by the second keystroke.\n *\n * **`initialFocus` is the search box**, not the container, and this is the one place\n * in the package that overrides `Modal`'s rule. The sheet exists in order to be typed\n * into (live #212: *\"make the text input filter active on select click\"*), and the\n * callers cannot do it themselves — `Combobox`'s field runs\n * `setOpen(true); sheetInputRef.current?.focus()` in a single `onFocus`, so the focus\n * call happens while this sheet is still unmounted and the ref is null. Only the\n * sheet knows when its own input exists. Popping the keyboard is the POINT here,\n * where in a form dialog it is the cost.\n *\n * **`restoreFocus` is off**, which is the uncomfortable half. Every caller in this\n * kit opens the sheet from the field's own `onFocus` handler, so handing focus back\n * to that field on close re-opens the sheet the user has just dismissed — a row tap\n * would select a value and immediately put the list back over it. The field that\n * opened it is the only thing that knows whether re-focusing it means \"open\"; a sheet\n * that cannot tell must not guess. The cost is that focus lands on `<body>` after a\n * dismissal, which on the phone this shape is for costs nothing, and on a pointer\n * device is what happened before this change too.\n */\n useFocusTrap(sheetRef, {\n active: open,\n restoreFocus: false,\n initialFocus: () => searchRef.current,\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // The sheet is almost always the INNERMOST overlay — it opens from a field inside a\n // `Modal` or a `FullBleedDialog`, both of which take Escape on their own panel. A\n // React portal's events still travel up the React tree, so without stopping it here\n // one press would dismiss this list and the dialog underneath with it: Keksdose\n // live #309 (*\"Mouse Back does not only close the select but also the whole edit or\n // create dialog which is cumbersome\"*), which the Back gesture already learned, on\n // the key a desktop user reaches for instead.\n e.stopPropagation();\n onClose();\n };\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n // Spread FIRST, and every attribute below it deliberately wins: `style`,\n // `onMouseDown` and `onKeyDown` here are not styling choices, they are live\n // #328, dev#477 and live #309 respectively, and a caller who passed one by\n // accident would re-open a bug three apps have already paid for. `style` is\n // MERGED rather than won outright (see below), which is the one case where a\n // caller has something to add.\n {...rest}\n ref={sheetRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Without it the container cannot take focus, and `useFocusTrap` has nothing to\n // fall back to when the list is empty and the search box is the only thing in\n // here — see the hook's \"nothing tabbable\" branch.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n // The caller's own name wins over the heading: a sheet titled \"Account\" in a\n // form with two of them needs to say WHICH, and `title` is what the user reads\n // rather than what a reader announces.\n aria-label={ariaLabel ?? (typeof title === \"string\" ? title : undefined)}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n // A caller's `style` is kept and the correction laid OVER it: the three values\n // below are the whole of live #328, so they are not negotiable, but a z-index\n // or a transition from outside has nothing to do with them. Without a visual\n // viewport to correct for this is character-for-character what the caller passed.\n style={vv ? { ...style, top: vv.top, height: vv.height, bottom: \"auto\" } : style}\n className={cn(\"fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none\", className)}\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3 py-2\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={onClose}\n className=\"rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n <div className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">{children}</div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-start text-base\",\n \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n);\n"],"mappings":";AA0NM,SACE,KADF;AA1NN,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,6BAA6B,oBAAoB;AA+CnD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAKnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,EAAE,OAAO,WAAW,CAAC;AAC7F,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAIjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAE9B,QAAM,WAAW,OAAuB,IAAI;AA2B5C,eAAa,UAAU;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,cAAc,MAAM,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QAOE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QAIX,UAAU;AAAA,QACV,WAAW;AAAA,QAIX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiB9D,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAwBtC,OAAO,KAAK,EAAE,GAAG,OAAO,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QAC3E,WAAW,GAAG,wEAAwE,SAAS;AAAA,QAE/F;AAAA,+BAAC,SAAI,WAAU,qEACb;AAAA,gCAAC,UAAK,WAAU,4EACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBACnB,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAGA,oBAAC,SAAI,WAAU,qDAAqD,UAAS;AAAA;AAAA;AAAA,IAC/E;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { CLIPS_ATTRIBUTE } from \"../lib/clipping\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\nimport { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * Two of the div's own attributes are omitted because this component already owns\n * the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's\n * tooltip string, and `onClose` is \"the sheet was dismissed\", not the DOM's\n * `<dialog>` close event. Everything else a caller can pass reaches the sheet.\n */\nexport interface PickerSheetProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"onClose\"> {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n /** Default: `pickerSheet.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n children: ReactNode;\n}\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel,\n children,\n \"aria-label\": ariaLabel,\n className,\n style,\n ...rest\n}: PickerSheetProps) {\n // Every picker in the kit opens one of these on a phone, and most of them pass\n // their own `closeLabel` through as `undefined` — so this lookup is what makes the\n // one way out of a full-screen sheet speak the app's language without each of\n // them having to (see the prop's note in `EntityCombobox`).\n const labels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // A fallback ref so a caller that does not need the handle still gets the opening\n // focus, rather than the behaviour depending on whether a prop was passed. What that\n // focus is FOR is on the trap below.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n\n const sheetRef = useRef<HTMLDivElement>(null);\n /**\n * The sheet said `aria-modal=\"true\"` and then left focus on the field behind it, so\n * a screen reader was told to hide the page the user's cursor was still standing in\n * (the audit's *\"Only Modal manages focus\"*). Through the shared hook rather than\n * the bare `.focus()` that used to live here: the hook re-reads the tabbable list on\n * every Tab, which this panel needs more than most — its list is filtered as you\n * type, so the rows a trap captured at open time are gone by the second keystroke.\n *\n * **`initialFocus` is the search box**, not the container, and this is the one place\n * in the package that overrides `Modal`'s rule. The sheet exists in order to be typed\n * into (live #212: *\"make the text input filter active on select click\"*), and the\n * callers cannot do it themselves — `Combobox`'s field runs\n * `setOpen(true); sheetInputRef.current?.focus()` in a single `onFocus`, so the focus\n * call happens while this sheet is still unmounted and the ref is null. Only the\n * sheet knows when its own input exists. Popping the keyboard is the POINT here,\n * where in a form dialog it is the cost.\n *\n * **`restoreFocus` is off**, which is the uncomfortable half. Every caller in this\n * kit opens the sheet from the field's own `onFocus` handler, so handing focus back\n * to that field on close re-opens the sheet the user has just dismissed — a row tap\n * would select a value and immediately put the list back over it. The field that\n * opened it is the only thing that knows whether re-focusing it means \"open\"; a sheet\n * that cannot tell must not guess. The cost is that focus lands on `<body>` after a\n * dismissal, which on the phone this shape is for costs nothing, and on a pointer\n * device is what happened before this change too.\n */\n useFocusTrap(sheetRef, {\n active: open,\n restoreFocus: false,\n initialFocus: () => searchRef.current,\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // The sheet is almost always the INNERMOST overlay — it opens from a field inside a\n // `Modal` or a `FullBleedDialog`, both of which take Escape on their own panel. A\n // React portal's events still travel up the React tree, so without stopping it here\n // one press would dismiss this list and the dialog underneath with it: Keksdose\n // live #309 (*\"Mouse Back does not only close the select but also the whole edit or\n // create dialog which is cumbersome\"*), which the Back gesture already learned, on\n // the key a desktop user reaches for instead.\n e.stopPropagation();\n onClose();\n };\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n // Spread FIRST, and every attribute below it deliberately wins: `style`,\n // `onMouseDown` and `onKeyDown` here are not styling choices, they are live\n // #328, dev#477 and live #309 respectively, and a caller who passed one by\n // accident would re-open a bug three apps have already paid for. `style` is\n // MERGED rather than won outright (see below), which is the one case where a\n // caller has something to add.\n {...rest}\n ref={sheetRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Without it the container cannot take focus, and `useFocusTrap` has nothing to\n // fall back to when the list is empty and the search box is the only thing in\n // here — see the hook's \"nothing tabbable\" branch.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n // The caller's own name wins over the heading: a sheet titled \"Account\" in a\n // form with two of them needs to say WHICH, and `title` is what the user reads\n // rather than what a reader announces.\n aria-label={ariaLabel ?? (typeof title === \"string\" ? title : undefined)}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n // A caller's `style` is kept and the correction laid OVER it: the three values\n // below are the whole of live #328, so they are not negotiable, but a z-index\n // or a transition from outside has nothing to do with them. Without a visual\n // viewport to correct for this is character-for-character what the caller passed.\n style={vv ? { ...style, top: vv.top, height: vv.height, bottom: \"auto\" } : style}\n className={cn(\"fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none\", className)}\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3 py-2\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={onClose}\n className=\"rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n {/* Marked as the scroller it is (`CLIPS_ATTRIBUTE`), so a Tooltip in a row —\n `Combobox`'s `optionAdornment` — portals out under jsdom as it does in the\n browser rather than joining the option's name (keksdose E8). */}\n <div {...{ [CLIPS_ATTRIBUTE]: \"\" }} className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-start text-base\",\n \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n);\n"],"mappings":";AA2NM,SACE,KADF;AA3NN,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,6BAA6B,oBAAoB;AA+CnD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAKnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,EAAE,OAAO,WAAW,CAAC;AAC7F,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAIjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAE9B,QAAM,WAAW,OAAuB,IAAI;AA2B5C,eAAa,UAAU;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,cAAc,MAAM,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QAOE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QAIX,UAAU;AAAA,QACV,WAAW;AAAA,QAIX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiB9D,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAwBtC,OAAO,KAAK,EAAE,GAAG,OAAO,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QAC3E,WAAW,GAAG,wEAAwE,SAAS;AAAA,QAE/F;AAAA,+BAAC,SAAI,WAAU,qEACb;AAAA,gCAAC,UAAK,WAAU,4EACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBACnB,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAMA,oBAAC,SAAK,GAAG,EAAE,CAAC,eAAe,GAAG,GAAG,GAAG,WAAU,qDAC3C,UACH;AAAA;AAAA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface PieChartLabels {
|
|
2
|
+
/** Accessible name of the group of keyboard stops laid over the slices — one tab
|
|
3
|
+
* stop, arrow keys between them. */
|
|
4
|
+
slices: string;
|
|
5
|
+
/**
|
|
6
|
+
* One slice's accessible name. `value` arrives through `formatValue`, `percent` as
|
|
7
|
+
* the slice's share of what is drawn, already formatted in the locale ("42 %" in
|
|
8
|
+
* German, "42%" in English) — the function only arranges them.
|
|
9
|
+
*/
|
|
10
|
+
slice: (label: string, value: string, percent: string) => string;
|
|
11
|
+
/** The caption over the donut's centre figure. */
|
|
12
|
+
total: string;
|
|
13
|
+
/** What a chart with nothing to draw says, when the caller passes no `empty`. */
|
|
14
|
+
empty: string;
|
|
15
|
+
/** Accessible name of the chart's legend (a group of switches, one per slice). */
|
|
16
|
+
legend: string;
|
|
17
|
+
}
|
|
18
|
+
declare const DEFAULT_PIE_CHART_LABELS: PieChartLabels;
|
|
19
|
+
|
|
20
|
+
export { DEFAULT_PIE_CHART_LABELS, type PieChartLabels };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_PIE_CHART_LABELS = {
|
|
3
|
+
slices: "Chart slices",
|
|
4
|
+
slice: (label, value, percent) => `${label}: ${value} (${percent})`,
|
|
5
|
+
total: "Total",
|
|
6
|
+
empty: "No data",
|
|
7
|
+
legend: "Categories"
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_PIE_CHART_LABELS
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=pie-chart-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/pie-chart-labels.ts"],"sourcesContent":["// All user-facing strings for `PieChart`, as the `pieChart` namespace of\n// `<UiKitProvider labels>`, and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, for the reason `series-chart-labels`\n// is one: the label registry (src/i18n) imports the defaults, and must not pull the\n// chart library in behind them.\n\nexport interface PieChartLabels {\n /** Accessible name of the group of keyboard stops laid over the slices — one tab\n * stop, arrow keys between them. */\n slices: string;\n /**\n * One slice's accessible name. `value` arrives through `formatValue`, `percent` as\n * the slice's share of what is drawn, already formatted in the locale (\"42 %\" in\n * German, \"42%\" in English) — the function only arranges them.\n */\n slice: (label: string, value: string, percent: string) => string;\n /** The caption over the donut's centre figure. */\n total: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of the chart's legend (a group of switches, one per slice). */\n legend: string;\n}\n\nexport const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {\n slices: \"Chart slices\",\n slice: (label, value, percent) => `${label}: ${value} (${percent})`,\n total: \"Total\",\n empty: \"No data\",\n legend: \"Categories\",\n};\n"],"mappings":";AAyBO,MAAM,2BAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,OAAO,CAAC,OAAO,OAAO,YAAY,GAAG,KAAK,KAAK,KAAK,KAAK,OAAO;AAAA,EAChE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { PieChartLabels } from './pie-chart-labels.js';
|
|
4
|
+
|
|
5
|
+
/** One slice. */
|
|
6
|
+
interface PieChartSlice {
|
|
7
|
+
/** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the
|
|
8
|
+
* legend's hidden set. Labels can collide; keys must not. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** What the slice is called — in the legend, the tooltip and its accessible name. */
|
|
11
|
+
label: string;
|
|
12
|
+
/**
|
|
13
|
+
* The slice's size. A value that is not a positive finite number — `null` for a
|
|
14
|
+
* figure that is missing, a zero, a negative — is NOT drawn and not in the legend:
|
|
15
|
+
* an angle cannot be negative, and a zero-degree slice is one a pointer and a
|
|
16
|
+
* keyboard would both stop on to find nothing.
|
|
17
|
+
*/
|
|
18
|
+
value: number | null | undefined;
|
|
19
|
+
/** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in
|
|
20
|
+
* `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */
|
|
21
|
+
color?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`
|
|
25
|
+
* anchor can find it. `children` is the chart's own.
|
|
26
|
+
*/
|
|
27
|
+
interface PieChartProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
28
|
+
data: readonly PieChartSlice[];
|
|
29
|
+
/** `"donut"` (default) leaves a hole with the total in it; `"pie"` is the full disc. */
|
|
30
|
+
variant?: "donut" | "pie";
|
|
31
|
+
/** Height of the plot in px (the legend is under it). Default 300. */
|
|
32
|
+
height?: number;
|
|
33
|
+
/** How a value is written in the tooltip, the slice's name, a `"value"` slice label
|
|
34
|
+
* and the donut's total. Default: `Intl.NumberFormat` in `locale`. */
|
|
35
|
+
formatValue?: (value: number) => string;
|
|
36
|
+
/**
|
|
37
|
+
* What is written beside each slice: its share (`"percent"`, the default), its
|
|
38
|
+
* value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a
|
|
39
|
+
* sliver sits on its neighbour's. The figure is in the tooltip and the slice's name
|
|
40
|
+
* either way.
|
|
41
|
+
*/
|
|
42
|
+
sliceLabels?: "percent" | "value" | "none";
|
|
43
|
+
/** The smallest share (0–1) that gets a slice label. Default 0.04. */
|
|
44
|
+
minLabelShare?: number;
|
|
45
|
+
/** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default
|
|
46
|
+
* `true`; ignored for `variant="pie"`, which has no centre to draw in. */
|
|
47
|
+
center?: boolean;
|
|
48
|
+
/** The caption in the donut's centre. Default: `labels.total`. */
|
|
49
|
+
centerLabel?: ReactNode;
|
|
50
|
+
/** The figure in the donut's centre. Default: the drawn total through `formatValue`. */
|
|
51
|
+
centerValue?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* The legend under the chart. `"toggle"` (default): each entry is a switch that
|
|
54
|
+
* takes its slice off the chart, and the percentages are then of what is left —
|
|
55
|
+
* the shares always read "of this picture". `"static"`: a key with nothing to press.
|
|
56
|
+
*/
|
|
57
|
+
legend?: "toggle" | "static" | "none";
|
|
58
|
+
/** The slices switched off, for a caller that owns the set (a URL, a saved view).
|
|
59
|
+
* Left out, the chart keeps its own. */
|
|
60
|
+
hiddenSlices?: ReadonlySet<string>;
|
|
61
|
+
/** Called with the new hidden set whenever the legend switches a slice. */
|
|
62
|
+
onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;
|
|
63
|
+
/** A slice was activated — by click, or Enter/Space on a focused slice. Makes each
|
|
64
|
+
* slice a `role="button"`. */
|
|
65
|
+
onSliceClick?: (key: string, slice: PieChartSlice) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and
|
|
68
|
+
* `"value"` slice labels — so the host's demo-mode rule blurs it. A percentage alone
|
|
69
|
+
* reveals no amount, so `"percent"` labels stay readable, and the slices themselves
|
|
70
|
+
* are not tagged: a blurred ring is no picture at all.
|
|
71
|
+
*/
|
|
72
|
+
redact?: boolean;
|
|
73
|
+
/** What to show with nothing to draw. Default: `labels.empty`. */
|
|
74
|
+
empty?: ReactNode;
|
|
75
|
+
/** BCP 47 tag for the numbers and percentages. Default: the provider's. */
|
|
76
|
+
locale?: string;
|
|
77
|
+
labels?: Partial<PieChartLabels>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Part-to-whole chart: a donut (or `variant="pie"`), one slice per
|
|
81
|
+
* {@link PieChartSlice}, coloured from the chart ramp by index.
|
|
82
|
+
*
|
|
83
|
+
* **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving
|
|
84
|
+
* tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —
|
|
85
|
+
* clockwise from twelve o'clock, the order of the legend — with ←/→ following the
|
|
86
|
+
* reading direction in RTL; Home/End go to the first and last. Each slice is named
|
|
87
|
+
* "Label: value (xx%)" (`labels.slice`), because a colour and an angle are nothing a
|
|
88
|
+
* screen reader can say, and focusing one reads it out in the donut's centre. With
|
|
89
|
+
* `onSliceClick`, Enter and Space activate it as a click does.
|
|
90
|
+
*
|
|
91
|
+
* The legend switches slices off (`legend="toggle"`); the percentages are always of
|
|
92
|
+
* what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend
|
|
93
|
+
* and the centre follow the page.
|
|
94
|
+
*
|
|
95
|
+
* With nothing to draw it says so (`empty`) rather than drawing an empty box.
|
|
96
|
+
*
|
|
97
|
+
* Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.
|
|
98
|
+
*/
|
|
99
|
+
declare function PieChart({ data, variant, height, formatValue: formatValueProp, sliceLabels, minLabelShare, center, centerLabel, centerValue, legend, hiddenSlices: hiddenProp, onHiddenSlicesChange, onSliceClick, redact, empty, locale: localeProp, labels: labelsProp, className, style, ...rest }: PieChartProps): react.JSX.Element;
|
|
100
|
+
|
|
101
|
+
export { PieChart, type PieChartProps, type PieChartSlice };
|