@eifi1/ui-kit 0.48.2 → 0.50.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 +34 -16
- package/dist/admin/admin-action-confirm.js +1 -1
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.js +5 -1
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/user-roster.d.ts +24 -12
- package/dist/admin/user-roster.js +21 -6
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js +2 -2
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/not-found-page.js +1 -1
- package/dist/auth/not-found-page.js.map +1 -1
- package/dist/auth/reset-password-form.js +2 -2
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +5 -3
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/plan-card.js +2 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +1 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/breadcrumbs.d.ts +19 -1
- package/dist/components/breadcrumbs.js +11 -4
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-look.d.ts +37 -7
- package/dist/components/button-look.js +33 -6
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +4 -1
- package/dist/components/button.js +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +2 -1
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-edge.d.ts +10 -0
- package/dist/components/chart-edge.js +57 -0
- package/dist/components/chart-edge.js.map +1 -0
- package/dist/components/chart.js +2 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +8 -1
- package/dist/components/chip.js +4 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.js +1 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox-suggestions.js +1 -0
- package/dist/components/combobox-suggestions.js.map +1 -1
- package/dist/components/confirm-context.d.ts +5 -0
- package/dist/components/confirm-context.js +7 -0
- package/dist/components/confirm-context.js.map +1 -0
- package/dist/components/confirm-dialog.js +3 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/currency-select.d.ts +28 -2
- package/dist/components/currency-select.js +56 -10
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/danger-confirm.js +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-fit.js +21 -3
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +58 -3
- package/dist/components/data-table-mobile.js +147 -16
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -1
- package/dist/components/data-table-row-actions.js +7 -0
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table.d.ts +34 -1
- package/dist/components/data-table.js +37 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dropdown.js +1 -0
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/floating-panel.d.ts +36 -13
- package/dist/components/floating-panel.js +27 -7
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +1 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/icon-button.d.ts +11 -0
- package/dist/components/icon-button.js +7 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/modal.js +1 -0
- package/dist/components/modal.js.map +1 -1
- package/dist/components/nav-pills.d.ts +8 -1
- package/dist/components/nav-pills.js +61 -38
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -1
- package/dist/components/number-input.js +6 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +2 -0
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header-context.d.ts +8 -0
- package/dist/components/page-header-context.js +7 -0
- package/dist/components/page-header-context.js.map +1 -0
- package/dist/components/page-header.d.ts +14 -2
- package/dist/components/page-header.js +10 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js +1 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +5 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.js +1 -1
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions-labels.js +3 -1
- package/dist/components/row-actions-labels.js.map +1 -1
- package/dist/components/row-actions.d.ts +43 -9
- package/dist/components/row-actions.js +78 -15
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +37 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/series-chart.js +8 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +1 -1
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/sparkline.js +2 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -7
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +5 -1
- package/dist/components/status-dot.js +4 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +2 -2
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +26 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.js +37 -6
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-banner.js +1 -1
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/demo/demo-ended.js +1 -1
- package/dist/demo/demo-ended.js.map +1 -1
- package/dist/demo/demo-start.js +1 -1
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox.js +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +9 -1
- package/dist/hooks/use-dismiss.js +25 -5
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-document-title.d.ts +14 -0
- package/dist/hooks/use-document-title.js +22 -0
- package/dist/hooks/use-document-title.js.map +1 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +20 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +34 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +22 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +20 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +20 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +20 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +20 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +3 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -0
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.js +2 -2
- package/dist/landing/landing-actions.js.map +1 -1
- package/dist/landing/public-header.js +2 -2
- package/dist/landing/public-header.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +27 -0
- package/dist/lib/clipboard-minus.js +63 -0
- package/dist/lib/clipboard-minus.js.map +1 -0
- package/dist/lib/data-attributes.d.ts +11 -0
- package/dist/lib/data-attributes.js +10 -0
- package/dist/lib/data-attributes.js.map +1 -0
- package/dist/lib/document-title.d.ts +17 -0
- package/dist/lib/document-title.js +29 -0
- package/dist/lib/document-title.js.map +1 -0
- package/dist/lib/float-clearance.js +8 -2
- package/dist/lib/float-clearance.js.map +1 -1
- package/dist/lib/format.d.ts +36 -10
- package/dist/lib/format.js +57 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +32 -0
- package/dist/lib/minus.js +14 -0
- package/dist/lib/minus.js.map +1 -0
- package/dist/lib/status-tone.d.ts +30 -0
- package/dist/lib/status-tone.js +17 -0
- package/dist/lib/status-tone.js.map +1 -0
- package/dist/settings/settings-layout.js +3 -2
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +1 -1
- package/dist/shell/account-menu-language.js +9 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/app-shell-labels.js +2 -1
- package/dist/shell/app-shell-labels.js.map +1 -1
- package/dist/shell/app-shell.d.ts +58 -16
- package/dist/shell/app-shell.js +151 -43
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/language-code.d.ts +3 -0
- package/dist/shell/language-code.js +8 -0
- package/dist/shell/language-code.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +14 -6
- package/dist/shell/topbar-controls.js +14 -17
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +1 -1
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/testing/react.d.ts +25 -0
- package/dist/testing/react.js +19 -0
- package/dist/testing/react.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +82 -15
- package/dist/theme/chart-palette.js +51 -24
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +9 -0
- package/dist/theme/palette-presets.js +77 -62
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +42 -0
- package/dist/tour/tour-dock.js +80 -0
- package/dist/tour/tour-dock.js.map +1 -0
- package/dist/tour/tour-gates.d.ts +71 -0
- package/dist/tour/tour-gates.js +114 -0
- package/dist/tour/tour-gates.js.map +1 -0
- package/dist/tour/tour-guide-button.d.ts +27 -0
- package/dist/tour/tour-guide-button.js +45 -0
- package/dist/tour/tour-guide-button.js.map +1 -0
- package/dist/tour/tour-guide-labels.d.ts +8 -0
- package/dist/tour/tour-guide-labels.js +9 -0
- package/dist/tour/tour-guide-labels.js.map +1 -0
- package/dist/tour/tour-labels.js +9 -1
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +133 -9
- package/dist/tour/tour.js +594 -70
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -2
- package/dist/tour/use-tour-url-sync.js +7 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +18 -5
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +11 -0
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +11 -5
- package/dist/wizard/use-wizard.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +41 -14
- package/src/auth/accept-invitation.tsx +3 -3
- package/src/auth/not-found-page.tsx +1 -1
- package/src/auth/reset-password-form.tsx +3 -3
- package/src/auth/verify-email.tsx +13 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/amount-input.tsx +5 -4
- package/src/components/autocomplete.tsx +2 -0
- package/src/components/breadcrumbs.tsx +32 -4
- package/src/components/button-look.ts +86 -15
- package/src/components/button.tsx +6 -3
- package/src/components/calendar-heatmap.tsx +2 -1
- package/src/components/chart-edge.ts +73 -0
- package/src/components/chart.tsx +3 -1
- package/src/components/chip.tsx +10 -1
- package/src/components/combobox-core.tsx +2 -0
- package/src/components/combobox-suggestions.tsx +2 -0
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/currency-select.tsx +95 -6
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +41 -6
- package/src/components/data-table-mobile.tsx +259 -17
- package/src/components/data-table-row-actions.tsx +9 -1
- package/src/components/data-table-row.tsx +3 -0
- package/src/components/data-table.tsx +91 -19
- package/src/components/dropdown.tsx +2 -0
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/floating-panel.tsx +67 -16
- package/src/components/form-actions.tsx +2 -2
- package/src/components/full-bleed-dialog.tsx +3 -0
- package/src/components/icon-button.tsx +21 -8
- package/src/components/modal.tsx +3 -0
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/number-input.tsx +8 -3
- package/src/components/numpad-sheet.tsx +4 -0
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/picker-sheet.tsx +4 -0
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/popover.tsx +2 -0
- package/src/components/progress-bar.tsx +6 -3
- package/src/components/reauth-dialog.tsx +1 -1
- package/src/components/row-actions-labels.ts +2 -0
- package/src/components/row-actions.tsx +154 -33
- package/src/components/section-nav.tsx +65 -0
- package/src/components/series-chart.tsx +12 -3
- package/src/components/share-card.tsx +1 -1
- package/src/components/signature-pad.tsx +1 -1
- package/src/components/sparkline.tsx +2 -1
- package/src/components/stat-tile.tsx +19 -7
- package/src/components/status-dot.tsx +7 -1
- package/src/components/tooltip.tsx +3 -1
- package/src/components/translation-review-editor.tsx +2 -2
- package/src/components/translation-review.tsx +2 -2
- package/src/components/treemap.tsx +34 -8
- package/src/components/use-table-state.ts +63 -8
- package/src/data-table.ts +3 -3
- package/src/demo/demo-banner.tsx +1 -1
- package/src/demo/demo-ended.tsx +1 -1
- package/src/demo/demo-start.tsx +1 -1
- package/src/feedback/feedback-inbox.tsx +1 -1
- package/src/hooks/use-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +17 -0
- package/src/i18n/kit-labels.tsx +79 -1
- package/src/i18n/locales/es.ts +17 -0
- package/src/i18n/locales/fr.ts +17 -0
- package/src/i18n/locales/hu.ts +17 -0
- package/src/i18n/locales/it.ts +17 -0
- package/src/i18n/locales/zh.ts +17 -0
- package/src/i18n/review.ts +3 -0
- package/src/i18n/shared-labels.ts +1 -0
- package/src/index.ts +13 -0
- package/src/landing/landing-actions.tsx +2 -2
- package/src/landing/public-header.tsx +2 -2
- package/src/lib/clipboard-minus.ts +94 -0
- package/src/lib/data-attributes.ts +14 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +117 -30
- package/src/lib/minus.ts +50 -0
- package/src/lib/status-tone.ts +43 -0
- package/src/settings/settings-layout.tsx +3 -2
- package/src/shell/account-menu-language.tsx +14 -2
- package/src/shell/app-shell-labels.ts +1 -0
- package/src/shell/app-shell.tsx +291 -68
- package/src/shell/language-code.ts +5 -0
- package/src/shell/topbar-controls.tsx +35 -15
- package/src/testing/css-variables.generated.ts +6 -6
- package/src/testing/react.ts +59 -0
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour-dock.ts +132 -0
- package/src/tour/tour-gates.ts +195 -0
- package/src/tour/tour-guide-button.tsx +64 -0
- package/src/tour/tour-guide-labels.ts +15 -0
- package/src/tour/tour-labels.ts +8 -0
- package/src/tour/tour.tsx +949 -86
- package/src/tour/use-tour-url-sync.ts +14 -4
- package/src/tour.ts +3 -0
- package/src/wizard/stepper-nav.tsx +27 -7
- package/src/wizard/types.ts +12 -0
- package/src/wizard/use-wizard.ts +16 -9
- package/tokens.css +28 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/share-card.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Trash2, UserPlus, UsersRound } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Autocomplete } from \"./autocomplete\";\nimport type { ComboOption } from \"./combobox-core\";\nimport { Chip } from \"./chip\";\nimport { RoleChip } from \"./account-chips\";\nimport type { RoleDefinition } from \"./account-chips\";\nimport { useConfirm } from \"./confirm-dialog\";\nimport { CopyButton } from \"./copy-button\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport type { DialogFrameProps } from \"./dialog-frame\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { Tooltip } from \"./tooltip\";\nimport { Button } from \"./button\";\nimport { Card } from \"./card\";\nimport { IconButton } from \"./icon-button\";\nimport { Spinner } from \"./spinner\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"./write-lock\";\nimport type { WriteLock } from \"./write-lock\";\nimport type { CardProps } from \"./card\";\nimport type { DisabledReasonDisplay } from \"./field-parts\";\nimport { DEFAULT_SHARE_CARD_LABELS } from \"./share-card-labels\";\n\n/**\n * Who holds one record, and the owner's every way of changing that — Kurvenschmiede's\n * `features/sharing/share-dialog.tsx` and keksdose's `features/budgets/budget-share-card.tsx`,\n * which are the same component twice (Kurvenschmiede's own doc comment says so).\n *\n * Presentational: the kit draws the rows, asks before a removal (the kit's\n * {@link useConfirm}), keeps the field and the busy state, and checks only that an\n * address is SHAPED like one. Everything that is policy stays with the app — who may\n * share, whether an address has an account (that is what makes a grant pending), what\n * a role permits, every request — through the callbacks, which may return a promise.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ShareCardLabels {\n /** ShareDialog's heading. */\n dialogTitle: string;\n /** ShareDialog's close button. */\n close: string;\n /** The add field's label. */\n email: string;\n /** The add field's label when `addWithoutEmail` lets it stay empty. */\n emailOptional: string;\n emailPlaceholder: string;\n /** Under the field when what was typed is not an address. */\n invalidEmail: string;\n /** Accessible name of the role choice beside the field. */\n role: string;\n /** Accessible name of one grantee's role choice. */\n roleOf: (name: string) => string;\n /** The add button. */\n add: string;\n /** Heading over the list. */\n whoHasAccess: string;\n /** The list is empty. */\n nobodyYet: string;\n /** Chip on a grant whose address has no account yet. */\n pending: string;\n /** A pending grant with no address — a link anybody holding it can redeem. */\n openInvite: string;\n copyLink: string;\n /** A candidate that is a team (its sublabel, and the line under a team row). */\n team: string;\n /** Under the field while a team is the one named. */\n teamHint: (name: string) => string;\n /** The remove button on a grantee. */\n remove: string;\n removeConfirm: (name: string) => string;\n /** The remove button on a pending grant. */\n revokePending: string;\n revokePendingConfirm: (name: string) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** Shown while `loading`. */\n loading: string;\n}\n\n// Defined in ./share-card-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SHARE_CARD_LABELS };\n\n// ── Data shapes ───────────────────────────────────────────────────────────────\n\n/**\n * One role of the app's vocabulary: Kurvenschmiede's viewer/editor, keksdose's guest.\n *\n * The admin roster's {@link RoleDefinition} (label, tone, icon) plus the key it is\n * filed under — an ARRAY here rather than the keyed `RoleVocabulary` record,\n * because the order is the order of the role choice's segments.\n */\nexport type ShareRole = RoleDefinition & {\n key: string;\n /** What the role allows — under the role choice once chosen, and the chip's tooltip. */\n description?: string;\n};\n\n/** Somebody (or a team) who holds the record now. */\nexport interface ShareGrantee {\n id: string | number;\n name: string;\n /** The second line. A team row says {@link ShareCardLabels.team} instead. */\n email?: string | null;\n /** A {@link ShareRole.key}. */\n role: string;\n /** `team`: the grant reaches everybody in it (Kurvenschmiede). Default `person`. */\n kind?: \"person\" | \"team\";\n /** Shown, but neither its role nor its access can be changed here (an owner row). */\n locked?: boolean;\n /** The app's own line under the row — keksdose's per-guest key delivery. */\n extra?: ReactNode;\n}\n\n/** A grant waiting for its address to have an account — or, with no address, an\n * open invite link (keksdose). */\nexport interface SharePendingGrant {\n id: string | number;\n email?: string | null;\n /** A {@link ShareRole.key}; left out when the app has only one role. */\n role?: string;\n /** The invite link, offered with a copy button. */\n link?: string;\n}\n\n/** A completion offered while typing: a past grantee, or one of the sharer's teams. */\nexport interface ShareCandidate {\n id: string | number;\n name: string;\n /** A person's address: taking the row fills the field with it, and the grant is\n * then an ordinary address grant. */\n email?: string | null;\n /** `team` (or a person with no address): taking the row names THIS candidate, and\n * `onAdd` receives it instead of an address. */\n kind?: \"person\" | \"team\";\n icon?: ReactNode;\n}\n\n/** What the add button asks for. Exactly one of `email` / `candidate` is set, except\n * under `addWithoutEmail`, where both may be null (an open invite). */\nexport interface ShareAddRequest {\n email: string | null;\n candidate: ShareCandidate | null;\n /** A {@link ShareRole.key}. */\n role: string;\n}\n\ntype MaybePromise = void | Promise<unknown>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SharePanelProps {\n grantees: ShareGrantee[];\n /** The role vocabulary, in display order, NARROWEST FIRST — viewer, then editor, as\n * Kurvenschmiede lists them. One role: no choice is offered and each row shows its\n * chip. The order is what tells a lowering from a raise under a write lock: a lapsed\n * plan lets a grantee's role be lowered, which removes access, and never raised\n * (docs/billing-harmonization.md decision 26 as amended, §12.36). */\n roles: ShareRole[];\n pending?: SharePendingGrant[];\n /** Completions for the field. Left out: the field is a plain address field. */\n candidates?: ShareCandidate[];\n /** The role a new grant starts with. Default: the first of `roles`. */\n defaultRole?: string;\n /** Resolve to clear the field; reject to keep it and show the error. Left out (or\n * `readOnly`): no add form. */\n onAdd?: (request: ShareAddRequest) => MaybePromise;\n /** Left out: roles are shown as chips. */\n onRoleChange?: (grantee: ShareGrantee, role: string) => MaybePromise;\n /** Called after the kit's confirm. Left out: no remove button. */\n onRemove?: (grantee: ShareGrantee) => MaybePromise;\n /** Called after the kit's confirm. Left out: no withdraw button. */\n onRevokePending?: (grant: SharePendingGrant) => MaybePromise;\n /** keksdose: an empty field is a request too — an invite link with no address. */\n addWithoutEmail?: boolean;\n /** Under the add field: whatever the app needs said there (Kurvenschmiede's \"your\n * pending grant does not let anybody register\"). A function receives the taken\n * candidate, so it can say something else while a team is named. */\n caption?: ReactNode | ((candidate: ShareCandidate | null) => ReactNode);\n /** Above everything — what sharing means here (keksdose's guest explanation). */\n intro?: ReactNode;\n /** The list is being fetched: a spinner instead of the rows. */\n loading?: boolean;\n /** The app's own error, shown in the error box (wins over a caught rejection). */\n error?: ReactNode;\n /** Turns a callback's rejection into words. Default {@link ShareCardLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /** Look, do not touch: no form, chips for roles, no remove buttons. Copying an\n * invite link still works. */\n readOnly?: boolean;\n /** Focus the field on mount — a dialog that exists to add somebody. */\n autoFocus?: boolean;\n labels?: LabelOverride<ShareCardLabels>;\n}\n\n// Shape only. Whether the address exists, is allowed, or is the sharer's own is the\n// app's question — the field must never become a way to probe for accounts.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** The kit's {@link RoleChip}, so a role looks the same here as in the admin roster,\n * with the role's description in a tooltip. */\nfunction ShareRoleChip({ role }: { role: ShareRole | undefined }) {\n if (!role) return null;\n const chip = <RoleChip value={role.key} roles={{ [role.key]: role }} />;\n return role.description ? <Tooltip label={role.description}>{chip}</Tooltip> : chip;\n}\n\n/**\n * A row's controls: the role toggle and the remove icon, or a pending grant's copy and\n * revoke. Held to the row and wrapping inside it (0.32.1, the 360 px Extra-large sweep;\n * docs/text-size-harmonization.md §4 \"nothing overflows\"): under a write lock on a touch\n * screen or at Large the remove icon gives its reason as a line under it, up to 20rem\n * wide, and a `shrink-0` box ran 31 px past a 390 px phone at Normal and 269 px past a\n * 360 px one at Extra large. Where the controls fit in one row, nothing changes.\n */\nconst ROW_CONTROLS = \"ms-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-1\";\n\n/**\n * A row's role choice saves on change, so it is a commit that cannot take `commit`:\n * under a write lock it is disabled, and the lock's reason is put in the kit Tooltip\n * around it — the same sentence the locked buttons beside it give.\n */\nfunction LockedRoleChoice({ reason, children }: { reason: ReactNode; children: ReactNode }) {\n return reason === undefined ? children : <Tooltip label={reason}>{children}</Tooltip>;\n}\n\n/** What a grantee's role choice may do under the write lock: `group` locks the whole\n * choice with that reason; `raise` leaves it live for a lowering and refuses a raise\n * with that reason. Neither: the choice is free. */\ninterface RoleChoiceLock {\n group?: ReactNode;\n raise?: ReactNode;\n}\n\n/**\n * A grantee's role choice under the write lock (decision 26 as amended,\n * docs/billing-harmonization.md §12.36). `roles` is read narrowest first, so an option\n * before the grantee's role LOWERS it: that removes access, which a lapsed plan allows\n * (§12.13), so it sees the lock as `COMMIT_EXCEPT_BILLING` does (`removal`). An option\n * after it RAISES it — a role grant, which every lock refuses (`every`), billing's with\n * its own reason. Kurvenschmiede's server refuses exactly that: a widening while lapsed,\n * not a narrowing (`shares_router.py:229-234`).\n *\n * A segmented choice is locked as a whole, so per grantee: under a demo's lock nothing\n * moves; under billing's, a grantee at the narrowest role has only raises (locked), one\n * at the widest only lowerings (live), and one in between keeps its lowerings while its\n * raises are dimmed and refused. A role the vocabulary does not list cannot be told\n * either way and stays locked.\n */\nfunction roleChoiceLock(index: number, count: number, every: WriteLock, removal: WriteLock): RoleChoiceLock {\n if (removal.locked) return { group: removal.reason };\n if (!every.locked) return {};\n if (index <= 0) return { group: every.reason };\n return index < count - 1 ? { raise: every.reason } : {};\n}\n\n/** The look of a raise refused while the lowerings beside it stay live — the dimmed\n * segment of a locked group (ToggleGroup's `dimmed`), on that segment alone. */\nconst RAISE_REFUSED = \"cursor-not-allowed opacity-50 hover:bg-transparent dark:hover:bg-transparent\";\n\n/**\n * The body both forms share: the add form, then the list. Exported for an app that\n * frames it itself (a settings section, a sheet).\n */\nexport function SharePanel({\n grantees,\n roles,\n pending = [],\n candidates,\n defaultRole,\n onAdd,\n onRoleChange,\n onRemove,\n onRevokePending,\n addWithoutEmail = false,\n caption,\n intro,\n loading = false,\n error,\n formatError,\n readOnly = false,\n autoFocus,\n labels: labelsProp,\n}: SharePanelProps) {\n // The `shareCard` namespace: prop > `<UiKitProvider labels>` > English.\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, labelsProp);\n const locale = useKitLocale();\n const confirm = useConfirm();\n // Every commit below opts into the surrounding WriteLockProvider with `commit`; the\n // role choices save on change, cannot take it, and read the lock themselves. Adding\n // and raising a role see every lock (`lock`); removing access, withdrawing an\n // invitation and lowering a role see it as a lapsed plan's lock allows them\n // (`removal`, COMMIT_EXCEPT_BILLING; docs/billing-harmonization.md §12.13, §12.36).\n const lock = useWriteLock();\n const removal = useWriteLock(COMMIT_EXCEPT_BILLING);\n const [email, setEmail] = useState(\"\");\n // A candidate taken from the completion. Typing again lets it go: the field then\n // names an address again, and a chosen team beside free text would be two answers\n // to \"who\" (Kurvenschmiede's rule).\n const [target, setTarget] = useState<ShareCandidate | null>(null);\n const [newRole, setNewRole] = useState(defaultRole ?? roles[0]?.key ?? \"\");\n const [invalid, setInvalid] = useState(false);\n // One action at a time, for the whole panel: two in flight against one record's\n // grants can answer out of order. The key says which control draws the spinner.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n\n const roleOf = (key: string | undefined) => roles.find((r) => r.key === key);\n const choosable = roles.length > 1;\n const canAdd = !readOnly && onAdd !== undefined;\n // Under a write lock the form's Share button gives the lock's sentence as a line (on\n // touch and at Large). A row's remove or revoke icon then keeps it in its tooltip, as\n // the role toggle beside it does, rather than repeat the same line under every row\n // (0.32.1, the 360 px Extra-large sweep). Without the form the rows' lines are the\n // only place it is said, so they stay — and ROW_CONTROLS wraps them.\n const rowReason: DisabledReasonDisplay = canAdd ? \"tooltip\" : \"auto\";\n\n const run = async (key: string, action: () => MaybePromise): Promise<boolean> => {\n setBusy(key);\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n setBusy(null);\n }\n };\n\n const address = email.trim();\n const empty = !target && address === \"\";\n\n const submit = async () => {\n // Enter in the field is a commit too — the button's lock does not see it.\n if (!onAdd || busy || lock.locked) return;\n if (empty && !addWithoutEmail) return;\n if (!target && address !== \"\" && !EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n const request: ShareAddRequest = {\n email: target ? null : address || null,\n candidate: target,\n role: newRole,\n };\n if (await run(\"add\", () => onAdd(request))) {\n setEmail(\"\");\n setTarget(null);\n }\n };\n\n const options: ComboOption<string>[] = (candidates ?? []).map((c) => ({\n value: String(c.id),\n label: c.name,\n sublabel: c.kind === \"team\" ? labels.team : (c.email ?? undefined),\n icon: c.icon ?? (c.kind === \"team\" ? <UsersRound className=\"size-4\" /> : undefined),\n }));\n\n // The completion is a convenience, not a search: an address that matches nobody is\n // the ordinary case (somebody new), so no \"No results\" box opens over the field —\n // and over its error. The same narrowing Autocomplete applies, asked first — folded as\n // every kit list folds (DUP-5).\n const needle = foldForSearch((target ? \"\" : email).trim(), locale);\n const offers =\n needle !== \"\" &&\n options.some((o) => [o.label, o.sublabel ?? \"\"].some((s) => foldForSearch(s, locale).includes(needle)));\n\n const captionNode = typeof caption === \"function\" ? caption(target) : caption;\n const shownError = error ?? failure;\n const nobody = grantees.length === 0 && pending.length === 0;\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-xs text-muted\">{intro}</div>}\n\n {canAdd && (\n <div className=\"space-y-2\">\n <div\n className={cn(\n \"grid grid-cols-1 gap-2\",\n choosable && \"sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start\",\n )}\n >\n <Autocomplete<string>\n label={addWithoutEmail ? labels.emailOptional : labels.email}\n placeholder={labels.emailPlaceholder}\n value={target ? target.name : email}\n options={options}\n open={offers ? undefined : false}\n fillOnSelect={false}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n disabled={busy !== null}\n error={invalid ? labels.invalidEmail : undefined}\n onChange={(text) => {\n setTarget(null);\n setInvalid(false);\n setFailure(null);\n setEmail(text);\n }}\n onSelect={(option) => {\n const chosen = (candidates ?? []).find((c) => String(c.id) === option.value);\n if (!chosen) return;\n setInvalid(false);\n if (chosen.kind !== \"team\" && chosen.email) {\n setTarget(null);\n setEmail(chosen.email);\n } else {\n setTarget(chosen);\n setEmail(\"\");\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") void submit();\n }}\n />\n {choosable && (\n <ToggleGroup<string>\n aria-label={labels.role}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={newRole}\n onChange={setNewRole}\n disabled={busy !== null}\n caption={\n roles.some((r) => r.description) ? (key: string) => roleOf(key)?.description : undefined\n }\n // Level with the floating field beside it, as the two apps do by hand.\n optionClassName=\"sm:min-h-10\"\n />\n )}\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n {target?.kind === \"team\" && <Caption>{labels.teamHint(target.name)}</Caption>}\n {captionNode && <Caption as=\"div\">{captionNode}</Caption>}\n </div>\n <Button\n variant=\"brand\"\n commit\n pending={busy === \"add\"}\n disabled={busy !== null || (empty && !addWithoutEmail)}\n onClick={() => void submit()}\n className=\"ms-auto\"\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.add}\n </Button>\n </div>\n </div>\n )}\n\n {shownError && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {shownError}\n </AlertBanner>\n )}\n\n {loading ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : (\n <div className=\"space-y-2\">\n <SectionLabel as=\"h4\">{labels.whoHasAccess}</SectionLabel>\n {nobody && <Caption>{labels.nobodyYet}</Caption>}\n <ul className=\"divide-y divide-subtle\">\n {grantees.map((g) => {\n const editable = !readOnly && !g.locked;\n const changeable = editable && onRoleChange !== undefined && choosable;\n const key = `grantee:${g.id}`;\n const rank = roles.findIndex((r) => r.key === g.role);\n const roleLock = roleChoiceLock(rank, roles.length, lock, removal);\n return (\n <li key={key} className=\"space-y-1.5 py-2 first:pt-0 last:pb-0\">\n {/* Wraps rather than squeezes: on a phone the controls drop under\n the name, full row, instead of truncating it to a letter. */}\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium text-primary\">\n {g.kind === \"team\" && <UsersRound className=\"size-4 shrink-0\" aria-hidden />}\n <span data-private className=\"truncate-until-large\">\n {g.name}\n </span>\n {!changeable && <ShareRoleChip role={roleOf(g.role)} />}\n </div>\n {(g.kind === \"team\" || g.email) && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {g.kind === \"team\" ? labels.team : g.email}\n </div>\n )}\n </div>\n {(changeable || (editable && onRemove)) && (\n <div className={ROW_CONTROLS}>\n {changeable && (\n <LockedRoleChoice reason={roleLock.group ?? roleLock.raise}>\n <ToggleGroup<string>\n size=\"sm\"\n aria-label={labels.roleOf(g.name)}\n options={roles.map((r, i) => ({\n value: r.key,\n label: r.label,\n className: roleLock.raise !== undefined && i > rank ? RAISE_REFUSED : undefined,\n }))}\n value={g.role}\n disabled={busy !== null || roleLock.group !== undefined}\n onChange={(next) => {\n if (next === g.role) return;\n // A raise under a lock that allows only lowerings: refused,\n // and the tooltip round the choice says why.\n if (roleLock.raise !== undefined && roles.findIndex((r) => r.key === next) > rank) return;\n void run(`role:${g.id}`, () => onRoleChange(g, next));\n }}\n />\n </LockedRoleChoice>\n )}\n {editable && onRemove && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n // Removing access: a lapsed plan allows it (§12.13).\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.remove}\n // The icon alone at every text size (§10.8): the row's one\n // control beside its role toggle, where \"Remove access\" as\n // text at Large would not fit a 240 px row. It carries a\n // spinner and the write lock, which a \"⋯\" menu entry\n // cannot, and its confirmation names the action in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.removeConfirm(g.name),\n confirmLabel: labels.remove,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRemove(g));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n {g.extra}\n </li>\n );\n })}\n {pending.map((p) => {\n const key = `pending:${p.id}`;\n const name = p.email || labels.openInvite;\n return (\n <li key={key} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div\n data-private={p.email ? true : undefined}\n className={cn(\n \"truncate-until-large text-sm\",\n p.email ? \"text-primary\" : \"text-muted\",\n )}\n >\n {name}\n </div>\n <div className=\"mt-0.5 flex flex-wrap items-center gap-1\">\n <Chip size=\"sm\" shape=\"square\" tone=\"warning\" caps>\n {labels.pending}\n </Chip>\n <ShareRoleChip role={roleOf(p.role)} />\n </div>\n </div>\n {(p.link || (!readOnly && onRevokePending)) && (\n <div className={ROW_CONTROLS}>\n {p.link && <CopyButton size=\"xs\" tone=\"muted\" text={p.link} label={labels.copyLink} />}\n {!readOnly && onRevokePending && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n // Withdrawing an invitation removes access too (§12.13).\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.revokePending}\n // As the grantee's remove above: one action beside the\n // copy icon (which is an icon at every size), and the\n // confirmation says it in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.revokePendingConfirm(name),\n confirmLabel: labels.revokePending,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRevokePending(p));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n}\n\n// ── The two frames ────────────────────────────────────────────────────────────\n\nexport interface ShareCardProps extends SharePanelProps {\n /** A heading over the card. */\n title?: ReactNode;\n /** The Card's variant. Default `outline` — a block inside a section, as keksdose's\n * card under a budget row is. */\n variant?: CardProps[\"variant\"];\n className?: string;\n}\n\n/** {@link SharePanel} in a card — keksdose's inline form, under the row it shares. */\nexport function ShareCard({ title, variant = \"outline\", className, ...panel }: ShareCardProps) {\n return (\n <Card variant={variant} padding=\"sm\" className={cn(\"space-y-3\", className)}>\n {title && <h3 className=\"text-sm font-semibold text-primary\">{title}</h3>}\n <SharePanel {...panel} />\n </Card>\n );\n}\n\nexport interface ShareDialogProps\n extends SharePanelProps,\n Pick<DialogFrameProps, \"open\" | \"onClose\" | \"size\" | \"className\"> {\n /** The heading. Default {@link ShareCardLabels.dialogTitle}. */\n title?: ReactNode;\n /** Under the heading — what is being shared (Kurvenschmiede passes its name). */\n description?: ReactNode;\n}\n\n/**\n * {@link SharePanel} in the kit's {@link DialogFrame} with a Close button —\n * Kurvenschmiede's form. Sharing commits as it goes, so there is nothing to save or\n * cancel: Close is the only action.\n */\nexport function ShareDialog({ open, onClose, size, className, title, description, ...panel }: ShareDialogProps) {\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, panel.labels);\n return (\n <DialogFrame\n open={open}\n onClose={onClose}\n size={size}\n className={className}\n title={title ?? labels.dialogTitle}\n description={description}\n actions={(close) => (\n <Button variant=\"secondary\" onClick={close}>\n {labels.close}\n </Button>\n )}\n >\n <SharePanel {...panel} />\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/share-card.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Trash2, UserPlus, UsersRound } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Autocomplete } from \"./autocomplete\";\nimport type { ComboOption } from \"./combobox-core\";\nimport { Chip } from \"./chip\";\nimport { RoleChip } from \"./account-chips\";\nimport type { RoleDefinition } from \"./account-chips\";\nimport { useConfirm } from \"./confirm-dialog\";\nimport { CopyButton } from \"./copy-button\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport type { DialogFrameProps } from \"./dialog-frame\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { Tooltip } from \"./tooltip\";\nimport { Button } from \"./button\";\nimport { Card } from \"./card\";\nimport { IconButton } from \"./icon-button\";\nimport { Spinner } from \"./spinner\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"./write-lock\";\nimport type { WriteLock } from \"./write-lock\";\nimport type { CardProps } from \"./card\";\nimport type { DisabledReasonDisplay } from \"./field-parts\";\nimport { DEFAULT_SHARE_CARD_LABELS } from \"./share-card-labels\";\n\n/**\n * Who holds one record, and the owner's every way of changing that — Kurvenschmiede's\n * `features/sharing/share-dialog.tsx` and keksdose's `features/budgets/budget-share-card.tsx`,\n * which are the same component twice (Kurvenschmiede's own doc comment says so).\n *\n * Presentational: the kit draws the rows, asks before a removal (the kit's\n * {@link useConfirm}), keeps the field and the busy state, and checks only that an\n * address is SHAPED like one. Everything that is policy stays with the app — who may\n * share, whether an address has an account (that is what makes a grant pending), what\n * a role permits, every request — through the callbacks, which may return a promise.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ShareCardLabels {\n /** ShareDialog's heading. */\n dialogTitle: string;\n /** ShareDialog's close button. */\n close: string;\n /** The add field's label. */\n email: string;\n /** The add field's label when `addWithoutEmail` lets it stay empty. */\n emailOptional: string;\n emailPlaceholder: string;\n /** Under the field when what was typed is not an address. */\n invalidEmail: string;\n /** Accessible name of the role choice beside the field. */\n role: string;\n /** Accessible name of one grantee's role choice. */\n roleOf: (name: string) => string;\n /** The add button. */\n add: string;\n /** Heading over the list. */\n whoHasAccess: string;\n /** The list is empty. */\n nobodyYet: string;\n /** Chip on a grant whose address has no account yet. */\n pending: string;\n /** A pending grant with no address — a link anybody holding it can redeem. */\n openInvite: string;\n copyLink: string;\n /** A candidate that is a team (its sublabel, and the line under a team row). */\n team: string;\n /** Under the field while a team is the one named. */\n teamHint: (name: string) => string;\n /** The remove button on a grantee. */\n remove: string;\n removeConfirm: (name: string) => string;\n /** The remove button on a pending grant. */\n revokePending: string;\n revokePendingConfirm: (name: string) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** Shown while `loading`. */\n loading: string;\n}\n\n// Defined in ./share-card-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SHARE_CARD_LABELS };\n\n// ── Data shapes ───────────────────────────────────────────────────────────────\n\n/**\n * One role of the app's vocabulary: Kurvenschmiede's viewer/editor, keksdose's guest.\n *\n * The admin roster's {@link RoleDefinition} (label, tone, icon) plus the key it is\n * filed under — an ARRAY here rather than the keyed `RoleVocabulary` record,\n * because the order is the order of the role choice's segments.\n */\nexport type ShareRole = RoleDefinition & {\n key: string;\n /** What the role allows — under the role choice once chosen, and the chip's tooltip. */\n description?: string;\n};\n\n/** Somebody (or a team) who holds the record now. */\nexport interface ShareGrantee {\n id: string | number;\n name: string;\n /** The second line. A team row says {@link ShareCardLabels.team} instead. */\n email?: string | null;\n /** A {@link ShareRole.key}. */\n role: string;\n /** `team`: the grant reaches everybody in it (Kurvenschmiede). Default `person`. */\n kind?: \"person\" | \"team\";\n /** Shown, but neither its role nor its access can be changed here (an owner row). */\n locked?: boolean;\n /** The app's own line under the row — keksdose's per-guest key delivery. */\n extra?: ReactNode;\n}\n\n/** A grant waiting for its address to have an account — or, with no address, an\n * open invite link (keksdose). */\nexport interface SharePendingGrant {\n id: string | number;\n email?: string | null;\n /** A {@link ShareRole.key}; left out when the app has only one role. */\n role?: string;\n /** The invite link, offered with a copy button. */\n link?: string;\n}\n\n/** A completion offered while typing: a past grantee, or one of the sharer's teams. */\nexport interface ShareCandidate {\n id: string | number;\n name: string;\n /** A person's address: taking the row fills the field with it, and the grant is\n * then an ordinary address grant. */\n email?: string | null;\n /** `team` (or a person with no address): taking the row names THIS candidate, and\n * `onAdd` receives it instead of an address. */\n kind?: \"person\" | \"team\";\n icon?: ReactNode;\n}\n\n/** What the add button asks for. Exactly one of `email` / `candidate` is set, except\n * under `addWithoutEmail`, where both may be null (an open invite). */\nexport interface ShareAddRequest {\n email: string | null;\n candidate: ShareCandidate | null;\n /** A {@link ShareRole.key}. */\n role: string;\n}\n\ntype MaybePromise = void | Promise<unknown>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SharePanelProps {\n grantees: ShareGrantee[];\n /** The role vocabulary, in display order, NARROWEST FIRST — viewer, then editor, as\n * Kurvenschmiede lists them. One role: no choice is offered and each row shows its\n * chip. The order is what tells a lowering from a raise under a write lock: a lapsed\n * plan lets a grantee's role be lowered, which removes access, and never raised\n * (docs/billing-harmonization.md decision 26 as amended, §12.36). */\n roles: ShareRole[];\n pending?: SharePendingGrant[];\n /** Completions for the field. Left out: the field is a plain address field. */\n candidates?: ShareCandidate[];\n /** The role a new grant starts with. Default: the first of `roles`. */\n defaultRole?: string;\n /** Resolve to clear the field; reject to keep it and show the error. Left out (or\n * `readOnly`): no add form. */\n onAdd?: (request: ShareAddRequest) => MaybePromise;\n /** Left out: roles are shown as chips. */\n onRoleChange?: (grantee: ShareGrantee, role: string) => MaybePromise;\n /** Called after the kit's confirm. Left out: no remove button. */\n onRemove?: (grantee: ShareGrantee) => MaybePromise;\n /** Called after the kit's confirm. Left out: no withdraw button. */\n onRevokePending?: (grant: SharePendingGrant) => MaybePromise;\n /** keksdose: an empty field is a request too — an invite link with no address. */\n addWithoutEmail?: boolean;\n /** Under the add field: whatever the app needs said there (Kurvenschmiede's \"your\n * pending grant does not let anybody register\"). A function receives the taken\n * candidate, so it can say something else while a team is named. */\n caption?: ReactNode | ((candidate: ShareCandidate | null) => ReactNode);\n /** Above everything — what sharing means here (keksdose's guest explanation). */\n intro?: ReactNode;\n /** The list is being fetched: a spinner instead of the rows. */\n loading?: boolean;\n /** The app's own error, shown in the error box (wins over a caught rejection). */\n error?: ReactNode;\n /** Turns a callback's rejection into words. Default {@link ShareCardLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /** Look, do not touch: no form, chips for roles, no remove buttons. Copying an\n * invite link still works. */\n readOnly?: boolean;\n /** Focus the field on mount — a dialog that exists to add somebody. */\n autoFocus?: boolean;\n labels?: LabelOverride<ShareCardLabels>;\n}\n\n// Shape only. Whether the address exists, is allowed, or is the sharer's own is the\n// app's question — the field must never become a way to probe for accounts.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** The kit's {@link RoleChip}, so a role looks the same here as in the admin roster,\n * with the role's description in a tooltip. */\nfunction ShareRoleChip({ role }: { role: ShareRole | undefined }) {\n if (!role) return null;\n const chip = <RoleChip value={role.key} roles={{ [role.key]: role }} />;\n return role.description ? <Tooltip label={role.description}>{chip}</Tooltip> : chip;\n}\n\n/**\n * A row's controls: the role toggle and the remove icon, or a pending grant's copy and\n * revoke. Held to the row and wrapping inside it (0.32.1, the 360 px Extra-large sweep;\n * docs/text-size-harmonization.md §4 \"nothing overflows\"): under a write lock on a touch\n * screen or at Large the remove icon gives its reason as a line under it, up to 20rem\n * wide, and a `shrink-0` box ran 31 px past a 390 px phone at Normal and 269 px past a\n * 360 px one at Extra large. Where the controls fit in one row, nothing changes.\n */\nconst ROW_CONTROLS = \"ms-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-1\";\n\n/**\n * A row's role choice saves on change, so it is a commit that cannot take `commit`:\n * under a write lock it is disabled, and the lock's reason is put in the kit Tooltip\n * around it — the same sentence the locked buttons beside it give.\n */\nfunction LockedRoleChoice({ reason, children }: { reason: ReactNode; children: ReactNode }) {\n return reason === undefined ? children : <Tooltip label={reason}>{children}</Tooltip>;\n}\n\n/** What a grantee's role choice may do under the write lock: `group` locks the whole\n * choice with that reason; `raise` leaves it live for a lowering and refuses a raise\n * with that reason. Neither: the choice is free. */\ninterface RoleChoiceLock {\n group?: ReactNode;\n raise?: ReactNode;\n}\n\n/**\n * A grantee's role choice under the write lock (decision 26 as amended,\n * docs/billing-harmonization.md §12.36). `roles` is read narrowest first, so an option\n * before the grantee's role LOWERS it: that removes access, which a lapsed plan allows\n * (§12.13), so it sees the lock as `COMMIT_EXCEPT_BILLING` does (`removal`). An option\n * after it RAISES it — a role grant, which every lock refuses (`every`), billing's with\n * its own reason. Kurvenschmiede's server refuses exactly that: a widening while lapsed,\n * not a narrowing (`shares_router.py:229-234`).\n *\n * A segmented choice is locked as a whole, so per grantee: under a demo's lock nothing\n * moves; under billing's, a grantee at the narrowest role has only raises (locked), one\n * at the widest only lowerings (live), and one in between keeps its lowerings while its\n * raises are dimmed and refused. A role the vocabulary does not list cannot be told\n * either way and stays locked.\n */\nfunction roleChoiceLock(index: number, count: number, every: WriteLock, removal: WriteLock): RoleChoiceLock {\n if (removal.locked) return { group: removal.reason };\n if (!every.locked) return {};\n if (index <= 0) return { group: every.reason };\n return index < count - 1 ? { raise: every.reason } : {};\n}\n\n/** The look of a raise refused while the lowerings beside it stay live — the dimmed\n * segment of a locked group (ToggleGroup's `dimmed`), on that segment alone. */\nconst RAISE_REFUSED = \"cursor-not-allowed opacity-50 hover:bg-transparent dark:hover:bg-transparent\";\n\n/**\n * The body both forms share: the add form, then the list. Exported for an app that\n * frames it itself (a settings section, a sheet).\n */\nexport function SharePanel({\n grantees,\n roles,\n pending = [],\n candidates,\n defaultRole,\n onAdd,\n onRoleChange,\n onRemove,\n onRevokePending,\n addWithoutEmail = false,\n caption,\n intro,\n loading = false,\n error,\n formatError,\n readOnly = false,\n autoFocus,\n labels: labelsProp,\n}: SharePanelProps) {\n // The `shareCard` namespace: prop > `<UiKitProvider labels>` > English.\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, labelsProp);\n const locale = useKitLocale();\n const confirm = useConfirm();\n // Every commit below opts into the surrounding WriteLockProvider with `commit`; the\n // role choices save on change, cannot take it, and read the lock themselves. Adding\n // and raising a role see every lock (`lock`); removing access, withdrawing an\n // invitation and lowering a role see it as a lapsed plan's lock allows them\n // (`removal`, COMMIT_EXCEPT_BILLING; docs/billing-harmonization.md §12.13, §12.36).\n const lock = useWriteLock();\n const removal = useWriteLock(COMMIT_EXCEPT_BILLING);\n const [email, setEmail] = useState(\"\");\n // A candidate taken from the completion. Typing again lets it go: the field then\n // names an address again, and a chosen team beside free text would be two answers\n // to \"who\" (Kurvenschmiede's rule).\n const [target, setTarget] = useState<ShareCandidate | null>(null);\n const [newRole, setNewRole] = useState(defaultRole ?? roles[0]?.key ?? \"\");\n const [invalid, setInvalid] = useState(false);\n // One action at a time, for the whole panel: two in flight against one record's\n // grants can answer out of order. The key says which control draws the spinner.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n\n const roleOf = (key: string | undefined) => roles.find((r) => r.key === key);\n const choosable = roles.length > 1;\n const canAdd = !readOnly && onAdd !== undefined;\n // Under a write lock the form's Share button gives the lock's sentence as a line (on\n // touch and at Large). A row's remove or revoke icon then keeps it in its tooltip, as\n // the role toggle beside it does, rather than repeat the same line under every row\n // (0.32.1, the 360 px Extra-large sweep). Without the form the rows' lines are the\n // only place it is said, so they stay — and ROW_CONTROLS wraps them.\n const rowReason: DisabledReasonDisplay = canAdd ? \"tooltip\" : \"auto\";\n\n const run = async (key: string, action: () => MaybePromise): Promise<boolean> => {\n setBusy(key);\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n setBusy(null);\n }\n };\n\n const address = email.trim();\n const empty = !target && address === \"\";\n\n const submit = async () => {\n // Enter in the field is a commit too — the button's lock does not see it.\n if (!onAdd || busy || lock.locked) return;\n if (empty && !addWithoutEmail) return;\n if (!target && address !== \"\" && !EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n const request: ShareAddRequest = {\n email: target ? null : address || null,\n candidate: target,\n role: newRole,\n };\n if (await run(\"add\", () => onAdd(request))) {\n setEmail(\"\");\n setTarget(null);\n }\n };\n\n const options: ComboOption<string>[] = (candidates ?? []).map((c) => ({\n value: String(c.id),\n label: c.name,\n sublabel: c.kind === \"team\" ? labels.team : (c.email ?? undefined),\n icon: c.icon ?? (c.kind === \"team\" ? <UsersRound className=\"size-4\" /> : undefined),\n }));\n\n // The completion is a convenience, not a search: an address that matches nobody is\n // the ordinary case (somebody new), so no \"No results\" box opens over the field —\n // and over its error. The same narrowing Autocomplete applies, asked first — folded as\n // every kit list folds (DUP-5).\n const needle = foldForSearch((target ? \"\" : email).trim(), locale);\n const offers =\n needle !== \"\" &&\n options.some((o) => [o.label, o.sublabel ?? \"\"].some((s) => foldForSearch(s, locale).includes(needle)));\n\n const captionNode = typeof caption === \"function\" ? caption(target) : caption;\n const shownError = error ?? failure;\n const nobody = grantees.length === 0 && pending.length === 0;\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-xs text-muted\">{intro}</div>}\n\n {canAdd && (\n <div className=\"space-y-2\">\n <div\n className={cn(\n \"grid grid-cols-1 gap-2\",\n choosable && \"sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start\",\n )}\n >\n <Autocomplete<string>\n label={addWithoutEmail ? labels.emailOptional : labels.email}\n placeholder={labels.emailPlaceholder}\n value={target ? target.name : email}\n options={options}\n open={offers ? undefined : false}\n fillOnSelect={false}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n disabled={busy !== null}\n error={invalid ? labels.invalidEmail : undefined}\n onChange={(text) => {\n setTarget(null);\n setInvalid(false);\n setFailure(null);\n setEmail(text);\n }}\n onSelect={(option) => {\n const chosen = (candidates ?? []).find((c) => String(c.id) === option.value);\n if (!chosen) return;\n setInvalid(false);\n if (chosen.kind !== \"team\" && chosen.email) {\n setTarget(null);\n setEmail(chosen.email);\n } else {\n setTarget(chosen);\n setEmail(\"\");\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") void submit();\n }}\n />\n {choosable && (\n <ToggleGroup<string>\n aria-label={labels.role}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={newRole}\n onChange={setNewRole}\n disabled={busy !== null}\n caption={\n roles.some((r) => r.description) ? (key: string) => roleOf(key)?.description : undefined\n }\n // Level with the floating field beside it, as the two apps do by hand.\n optionClassName=\"sm:min-h-10\"\n />\n )}\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n {target?.kind === \"team\" && <Caption>{labels.teamHint(target.name)}</Caption>}\n {captionNode && <Caption as=\"div\">{captionNode}</Caption>}\n </div>\n <Button\n variant=\"accent\"\n commit\n pending={busy === \"add\"}\n disabled={busy !== null || (empty && !addWithoutEmail)}\n onClick={() => void submit()}\n className=\"ms-auto\"\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.add}\n </Button>\n </div>\n </div>\n )}\n\n {shownError && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {shownError}\n </AlertBanner>\n )}\n\n {loading ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : (\n <div className=\"space-y-2\">\n <SectionLabel as=\"h4\">{labels.whoHasAccess}</SectionLabel>\n {nobody && <Caption>{labels.nobodyYet}</Caption>}\n <ul className=\"divide-y divide-subtle\">\n {grantees.map((g) => {\n const editable = !readOnly && !g.locked;\n const changeable = editable && onRoleChange !== undefined && choosable;\n const key = `grantee:${g.id}`;\n const rank = roles.findIndex((r) => r.key === g.role);\n const roleLock = roleChoiceLock(rank, roles.length, lock, removal);\n return (\n <li key={key} className=\"space-y-1.5 py-2 first:pt-0 last:pb-0\">\n {/* Wraps rather than squeezes: on a phone the controls drop under\n the name, full row, instead of truncating it to a letter. */}\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium text-primary\">\n {g.kind === \"team\" && <UsersRound className=\"size-4 shrink-0\" aria-hidden />}\n <span data-private className=\"truncate-until-large\">\n {g.name}\n </span>\n {!changeable && <ShareRoleChip role={roleOf(g.role)} />}\n </div>\n {(g.kind === \"team\" || g.email) && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {g.kind === \"team\" ? labels.team : g.email}\n </div>\n )}\n </div>\n {(changeable || (editable && onRemove)) && (\n <div className={ROW_CONTROLS}>\n {changeable && (\n <LockedRoleChoice reason={roleLock.group ?? roleLock.raise}>\n <ToggleGroup<string>\n size=\"sm\"\n aria-label={labels.roleOf(g.name)}\n options={roles.map((r, i) => ({\n value: r.key,\n label: r.label,\n className: roleLock.raise !== undefined && i > rank ? RAISE_REFUSED : undefined,\n }))}\n value={g.role}\n disabled={busy !== null || roleLock.group !== undefined}\n onChange={(next) => {\n if (next === g.role) return;\n // A raise under a lock that allows only lowerings: refused,\n // and the tooltip round the choice says why.\n if (roleLock.raise !== undefined && roles.findIndex((r) => r.key === next) > rank) return;\n void run(`role:${g.id}`, () => onRoleChange(g, next));\n }}\n />\n </LockedRoleChoice>\n )}\n {editable && onRemove && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n // Removing access: a lapsed plan allows it (§12.13).\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.remove}\n // The icon alone at every text size (§10.8): the row's one\n // control beside its role toggle, where \"Remove access\" as\n // text at Large would not fit a 240 px row. It carries a\n // spinner and the write lock, which a \"⋯\" menu entry\n // cannot, and its confirmation names the action in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.removeConfirm(g.name),\n confirmLabel: labels.remove,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRemove(g));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n {g.extra}\n </li>\n );\n })}\n {pending.map((p) => {\n const key = `pending:${p.id}`;\n const name = p.email || labels.openInvite;\n return (\n <li key={key} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div\n data-private={p.email ? true : undefined}\n className={cn(\n \"truncate-until-large text-sm\",\n p.email ? \"text-primary\" : \"text-muted\",\n )}\n >\n {name}\n </div>\n <div className=\"mt-0.5 flex flex-wrap items-center gap-1\">\n <Chip size=\"sm\" shape=\"square\" tone=\"warning\" caps>\n {labels.pending}\n </Chip>\n <ShareRoleChip role={roleOf(p.role)} />\n </div>\n </div>\n {(p.link || (!readOnly && onRevokePending)) && (\n <div className={ROW_CONTROLS}>\n {p.link && <CopyButton size=\"xs\" tone=\"muted\" text={p.link} label={labels.copyLink} />}\n {!readOnly && onRevokePending && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n // Withdrawing an invitation removes access too (§12.13).\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.revokePending}\n // As the grantee's remove above: one action beside the\n // copy icon (which is an icon at every size), and the\n // confirmation says it in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.revokePendingConfirm(name),\n confirmLabel: labels.revokePending,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRevokePending(p));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n}\n\n// ── The two frames ────────────────────────────────────────────────────────────\n\nexport interface ShareCardProps extends SharePanelProps {\n /** A heading over the card. */\n title?: ReactNode;\n /** The Card's variant. Default `outline` — a block inside a section, as keksdose's\n * card under a budget row is. */\n variant?: CardProps[\"variant\"];\n className?: string;\n}\n\n/** {@link SharePanel} in a card — keksdose's inline form, under the row it shares. */\nexport function ShareCard({ title, variant = \"outline\", className, ...panel }: ShareCardProps) {\n return (\n <Card variant={variant} padding=\"sm\" className={cn(\"space-y-3\", className)}>\n {title && <h3 className=\"text-sm font-semibold text-primary\">{title}</h3>}\n <SharePanel {...panel} />\n </Card>\n );\n}\n\nexport interface ShareDialogProps\n extends SharePanelProps,\n Pick<DialogFrameProps, \"open\" | \"onClose\" | \"size\" | \"className\"> {\n /** The heading. Default {@link ShareCardLabels.dialogTitle}. */\n title?: ReactNode;\n /** Under the heading — what is being shared (Kurvenschmiede passes its name). */\n description?: ReactNode;\n}\n\n/**\n * {@link SharePanel} in the kit's {@link DialogFrame} with a Close button —\n * Kurvenschmiede's form. Sharing commits as it goes, so there is nothing to save or\n * cancel: Close is the only action.\n */\nexport function ShareDialog({ open, onClose, size, className, title, description, ...panel }: ShareDialogProps) {\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, panel.labels);\n return (\n <DialogFrame\n open={open}\n onClose={onClose}\n size={size}\n className={className}\n title={title ?? labels.dialogTitle}\n description={description}\n actions={(close) => (\n <Button variant=\"secondary\" onClick={close}>\n {labels.close}\n </Button>\n )}\n >\n <SharePanel {...panel} />\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -385,7 +385,7 @@ function SignaturePad({
|
|
|
385
385
|
]
|
|
386
386
|
}
|
|
387
387
|
),
|
|
388
|
-
onSave && /* @__PURE__ */ jsx(Button, { type: "button", variant: "
|
|
388
|
+
onSave && /* @__PURE__ */ jsx(Button, { type: "button", variant: "accent", onClick: save, disabled: disabled || isEmpty, className: "ms-auto", children: labels.save })
|
|
389
389
|
] }),
|
|
390
390
|
hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error }),
|
|
391
391
|
/* @__PURE__ */ jsx("div", { ...regionProps })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/signature-pad.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentPropsWithRef, PointerEvent as ReactPointerEvent, ReactNode, Ref } from \"react\";\nimport { Eraser, Keyboard, PenLine, Undo2 } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { Button } from \"./button\";\nimport { FIELD_INVALID } from \"./field-base\";\nimport { Input } from \"./input\";\nimport { FIELD_ERROR_CLASS } from \"./field-parts\";\nimport { DEFAULT_SIGNATURE_PAD_LABELS } from \"./signature-pad-labels\";\n\n/**\n * A drawn signature, captured on a canvas and handed back as a PNG data URL.\n *\n * Kastlan wrote the first one (`features/handover/components/signature-pad.tsx`) for\n * the handover protocol, where tenant and inspector each sign on the same phone. This\n * keeps its contract — draw, get a PNG — and fixes what a copy per app would keep\n * getting wrong:\n *\n * 1. **Strokes are data, not pixels.** Each stroke is kept as a list of points, and\n * the canvas is a VIEW of them. That is what lets a resize (a phone rotating\n * mid-signature) keep the drawing instead of wiping it — setting `canvas.width`\n * clears the bitmap, so a pixels-only pad loses everything — and what makes undo\n * one `pop()`. The pixel buffer is sized to `devicePixelRatio`, so a stroke is\n * sharp on a 3× screen rather than upscaled from a 1× bitmap.\n * 2. **Smooth, and pressure-aware.** Segments are quadratic curves through the\n * midpoints between samples, so a fast stroke is a curve, not a polyline; a pen\n * that reports pressure varies the width, a mouse or finger (whose `pressure` is a\n * constant 0.5, or junk) draws at the base width.\n * 3. **Two inks.** On screen the ink is the computed `color` of the canvas, which is\n * `--text-primary` — so it reads in the dark theme, where a hard-coded near-black\n * stroke vanished into the surface. The exported PNG is re-rendered from the same\n * strokes in `exportInk` (near-black by default) on a transparent background. A\n * signature is a DOCUMENT artefact: it is printed on the protocol, shown on a white\n * card, embedded in a PDF — none of which follow the signer's theme. Exporting\n * the on-screen ink would have produced a pale-grey-on-transparent PNG for every\n * dark-mode signer, invisible on white paper.\n *\n * **Accessibility.** A canvas has no content a screen reader or a keyboard can reach,\n * and no amount of ARIA changes that — drawing is a pointer act. What the pad does:\n * the canvas is `role=\"img\"`, named by the visible label and described by the\n * instructions and by its state (\"Nothing drawn yet\" / \"Signature drawn\"), so a\n * screen-reader user knows what the box is and whether it holds anything; undo and\n * clear are real buttons, and their effect is announced. What it cannot do is let\n * someone sign without a pointer — hence `allowTypedName`, which offers a typed name\n * instead. It is OPT-IN, because whether a typed name counts as a signature is a\n * product and legal decision, not a UI one (Kastlan's handover accepts one; a flow\n * that needs a drawn mark would not). Where a drawn mark is not strictly required,\n * turn it on.\n */\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the pad renders or speaks. */\nexport interface SignaturePadLabels {\n /** Visible label above the pad, and the canvas's accessible name. The `label`\n * prop overrides it per instance. */\n label: string;\n /** Under the label, and part of the canvas's description. */\n instructions: string;\n /** Appended to the instructions when `allowTypedName` is on. */\n typedFallbackHint: string;\n /** The canvas's state for a screen reader. */\n empty: string;\n signed: string;\n undo: string;\n clear: string;\n /** Only rendered when `onSave` is given. */\n save: string;\n /** The mode switch (`allowTypedName`). */\n useTyped: string;\n useDrawn: string;\n /** Label of the typed-name field. */\n typedName: string;\n /** Live-region messages after the matching button. */\n cleared: string;\n undone: string;\n /** {@link SignatureView}: what it says when there is no signature to show. */\n viewEmpty: string;\n /** {@link SignatureView}: the saved PNG's alternative text. */\n viewDrawn: string;\n /** {@link SignatureView}: the accessible name of a typed-name signature. */\n viewTyped: (name: string) => string;\n}\n\n// Defined in ./signature-pad-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SIGNATURE_PAD_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** How the value handed back was made. */\nexport interface SignatureDetail {\n method: \"drawn\" | \"typed\";\n /** The name as typed, when `method` is `\"typed\"` — the PNG shows it, but a host\n * that stores a typed name as TEXT (Kastlan does) wants the string. */\n typedName?: string;\n}\n\n/** The imperative handle: `ref.current.toDataURL()` at submit time, for a form that\n * does not want to mirror the value into state on every stroke. */\nexport interface SignaturePadHandle {\n /** Remove every stroke and the typed name. Fires `onChange(null)`. */\n clear: () => void;\n /** The PNG, rendered in `exportInk` — or `null` while nothing is signed. */\n toDataURL: () => string | null;\n isEmpty: () => boolean;\n}\n\nexport interface SignaturePadProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Visible label; defaults to `labels.label`. */\n label?: ReactNode;\n /** Replaces `labels.instructions` for this instance (e.g. a legal note). */\n description?: ReactNode;\n /**\n * After every finished stroke, undo, clear and typed-name edit: the PNG, or `null`\n * once the pad is empty. Not called mid-stroke — encoding a PNG per pointer move\n * would stall the drawing on a phone.\n */\n onChange?: (dataUrl: string | null, detail: SignatureDetail) => void;\n /** Renders a Save button, enabled once something is signed. */\n onSave?: (dataUrl: string, detail: SignatureDetail) => void;\n disabled?: boolean;\n /** Paints the pad as wrong and sets `aria-invalid`. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** Message under the pad; implies `invalid` and is attached with\n * `aria-describedby`, like the kit's other fields. */\n error?: ReactNode;\n /** Offer a typed name as an alternative to drawing. See the accessibility note. */\n allowTypedName?: boolean;\n /** Ink of the exported PNG. Default near-black, legible on white. */\n exportInk?: string;\n /** Fill behind the exported PNG. Default transparent. */\n exportBackground?: string;\n /** Base stroke width in CSS pixels at Normal text size. Default 2.5. It grows with the\n * text size (× 1.25 / × 1.5, docs/text-size-harmonization.md §10.11), as the pad's\n * rem height does, so a signature keeps its weight on a larger pad. */\n lineWidth?: number;\n /** Classes for the canvas — its height, chiefly (default `h-40`). */\n canvasClassName?: string;\n /** User-facing strings; see {@link SignaturePadLabels}. */\n labels?: LabelOverride<SignaturePadLabels>;\n /** The {@link SignaturePadHandle}, not an element. A React 19 ref prop since 0.34\n * (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<SignaturePadHandle>;\n}\n\n/* ── Drawing ─────────────────────────────────────────────────────────────── */\n\ninterface Point {\n x: number;\n y: number;\n /** 0–1. A constant 0.5 for anything that is not a pen. */\n p: number;\n}\n\ntype Stroke = Point[];\n\nconst DEFAULT_EXPORT_INK = \"#111111\";\n\n// A handwriting face where the platform has one, `cursive` everywhere else. Only\n// ever used to render a typed name onto the canvas.\nconst SCRIPT_FONT = `\"Segoe Script\", \"Bradley Hand\", \"Brush Script MT\", cursive`;\n\n/** Width multiplier from pressure: 0.5 (the no-pen constant) is exactly 1. */\nconst pressureWidth = (p: number) => 0.4 + 1.2 * p;\n\nconst mid = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, p: (a.p + b.p) / 2 });\n\n/**\n * Segment `i` (1 ≤ i < n) of a stroke: a quadratic from the midpoint before sample\n * `i - 1` to the midpoint after it, with the sample as the control point. The live\n * drawing and the full repaint both go through here, so a stroke looks the same\n * before and after a resize.\n */\nfunction drawSegment(ctx: CanvasRenderingContext2D, pts: Stroke, i: number, scale: number, width: number) {\n const a = i === 1 ? pts[0] : mid(pts[i - 2], pts[i - 1]);\n const c = pts[i - 1];\n const b = mid(pts[i - 1], pts[i]);\n ctx.lineWidth = width * scale * pressureWidth((a.p + b.p) / 2);\n ctx.beginPath();\n ctx.moveTo(a.x * scale, a.y * scale);\n ctx.quadraticCurveTo(c.x * scale, c.y * scale, b.x * scale, b.y * scale);\n ctx.stroke();\n}\n\n/** The last half-segment, from the final midpoint to the final sample. Drawn once the\n * stroke is finished — while drawing, the pen is still there and it would be redrawn\n * on every move. A single tap is a dot. */\nfunction drawTail(ctx: CanvasRenderingContext2D, pts: Stroke, scale: number, width: number) {\n const last = pts[pts.length - 1];\n if (pts.length === 1) {\n ctx.beginPath();\n ctx.arc(last.x * scale, last.y * scale, (width * scale * pressureWidth(last.p)) / 2, 0, Math.PI * 2);\n ctx.fill();\n return;\n }\n const from = pts.length === 2 ? pts[0] : mid(pts[pts.length - 2], last);\n ctx.lineWidth = width * scale * pressureWidth(last.p);\n ctx.beginPath();\n ctx.moveTo(from.x * scale, from.y * scale);\n ctx.lineTo(last.x * scale, last.y * scale);\n ctx.stroke();\n}\n\nfunction prepare(ctx: CanvasRenderingContext2D, ink: string, dpr: number) {\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.strokeStyle = ink;\n ctx.fillStyle = ink;\n}\n\n/** A typed name, as large as fits, on the pad's baseline. */\nfunction drawName(ctx: CanvasRenderingContext2D, name: string, w: number, h: number) {\n let size = Math.min(48, h * 0.45);\n ctx.font = `italic ${size}px ${SCRIPT_FONT}`;\n const room = w * 0.85;\n const measured = ctx.measureText(name).width;\n if (measured > room) {\n size = Math.max(12, (size * room) / measured);\n ctx.font = `italic ${size}px ${SCRIPT_FONT}`;\n }\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"alphabetic\";\n ctx.fillText(name, w / 2, h * 0.68);\n}\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport function SignaturePad({\n ref,\n label,\n description,\n onChange,\n onSave,\n disabled,\n invalid,\n error,\n allowTypedName,\n exportInk = DEFAULT_EXPORT_INK,\n exportBackground,\n lineWidth = 2.5,\n canvasClassName,\n labels: labelsProp,\n className,\n ...rest\n}: SignaturePadProps) {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n const labels = useKitLabels(\"signaturePad\", DEFAULT_SIGNATURE_PAD_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const labelId = useId();\n const descId = useId();\n const statusId = useId();\n const errorId = useId();\n\n const canvasRef = useRef<HTMLCanvasElement>(null);\n /**\n * What is on the pad lives in a ref, not in state: a pointer move must not\n * re-render, and `onChange` has to see the pad AFTER a change, from inside the\n * handler that made it — before React has rendered anything. The state below\n * mirrors the parts the markup needs (empty or not, which mode, the field's text).\n */\n const scene = useRef<{ strokes: Stroke[]; mode: \"drawn\" | \"typed\"; name: string }>({\n strokes: [],\n mode: \"drawn\",\n name: \"\",\n });\n const live = useRef<{ id: number; stroke: Stroke } | null>(null);\n /**\n * The canvas width, in CSS px, the stored points are relative to. Fixed by the\n * first stroke; a later resize scales the drawing UNIFORMLY by `width / refWidth`,\n * so a rotated phone shows the same signature larger or smaller, never stretched.\n */\n const refWidth = useRef(0);\n const [strokeCount, setStrokeCount] = useState(0);\n const [mode, setMode] = useState<\"drawn\" | \"typed\">(\"drawn\");\n const [typedName, setTypedName] = useState(\"\");\n\n const typed = mode === \"typed\";\n const isEmpty = typed ? typedName.trim() === \"\" : strokeCount === 0;\n\n // Everything the paint and export paths read that can change between renders, so\n // the observers below subscribe once instead of on every keystroke.\n // The stroke follows the text size like everything in rem around it (§10.11).\n const { scale: textScale } = useTextSize();\n const strokeWidth = lineWidth * textScale;\n const opts = useRef({ lineWidth: strokeWidth, exportInk, exportBackground });\n useEffect(() => {\n opts.current = { lineWidth: strokeWidth, exportInk, exportBackground };\n }, [strokeWidth, exportInk, exportBackground]);\n\n const sceneEmpty = () => {\n const s = scene.current;\n return s.mode === \"typed\" ? s.name.trim() === \"\" : s.strokes.length === 0;\n };\n\n /** The whole scene, painted into `ctx` at `w`×`h` CSS px. */\n const paintInto = useCallback((ctx: CanvasRenderingContext2D, ink: string, w: number, h: number, dpr: number) => {\n const { lineWidth: width } = opts.current;\n const s = scene.current;\n prepare(ctx, ink, dpr);\n if (s.mode === \"typed\") {\n if (s.name.trim()) drawName(ctx, s.name.trim(), w, h);\n return;\n }\n const scale = refWidth.current > 0 ? w / refWidth.current : 1;\n for (const stroke of s.strokes) {\n for (let i = 1; i < stroke.length; i += 1) drawSegment(ctx, stroke, i, scale, width);\n drawTail(ctx, stroke, scale, width);\n }\n const pending = live.current?.stroke;\n if (pending) for (let i = 1; i < pending.length; i += 1) drawSegment(ctx, pending, i, scale, width);\n }, []);\n\n /** The ink on screen: the canvas's own computed `color`, i.e. `--text-primary`. */\n const screenInk = (canvas: HTMLCanvasElement) =>\n // Read as `color` rather than as the custom property: `--text-primary` is whatever\n // the palette wrote (a `color-mix()` in some presets), which a 2D context may not\n // parse; `color` has already been resolved by the browser.\n getComputedStyle(canvas).color || opts.current.exportInk;\n\n /** Repaint the visible canvas from the scene, in the theme's ink. */\n const repaint = useCallback(() => {\n const canvas = canvasRef.current;\n const ctx = canvas?.getContext(\"2d\");\n if (!canvas || !ctx) return;\n const dpr = window.devicePixelRatio || 1;\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n paintInto(ctx, screenInk(canvas), canvas.width / dpr, canvas.height / dpr, dpr);\n }, [paintInto]);\n\n /** Size the bitmap to the element × devicePixelRatio, then repaint. */\n const resize = useCallback(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n const w = Math.round(rect.width * dpr);\n const h = Math.round(rect.height * dpr);\n // Assigning `width` clears the bitmap even when the value is unchanged, so only\n // when it moved — and the repaint puts the strokes back either way.\n if (canvas.width !== w) canvas.width = w;\n if (canvas.height !== h) canvas.height = h;\n repaint();\n }, [repaint]);\n\n useLayoutEffect(() => {\n resize();\n }, [resize]);\n\n // A changed base width — or text size — redraws what is already there.\n useEffect(() => {\n repaint();\n }, [strokeWidth, repaint]);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const cleanups: Array<() => void> = [];\n if (typeof ResizeObserver !== \"undefined\") {\n const ro = new ResizeObserver(() => resize());\n ro.observe(canvas);\n cleanups.push(() => ro.disconnect());\n }\n // The ink follows the theme, and the theme is a class (`.dark`) or inline\n // variables (the palette store) on <html> — neither of which tells a canvas.\n if (typeof MutationObserver !== \"undefined\") {\n const mo = new MutationObserver(() => repaint());\n mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] });\n cleanups.push(() => mo.disconnect());\n }\n const mq = typeof window.matchMedia === \"function\" ? window.matchMedia(\"(prefers-color-scheme: dark)\") : null;\n if (mq?.addEventListener) {\n mq.addEventListener(\"change\", repaint);\n cleanups.push(() => mq.removeEventListener(\"change\", repaint));\n }\n return () => cleanups.forEach((fn) => fn());\n }, [resize, repaint]);\n\n /** The PNG, re-rendered off-screen in the export ink — `null` while empty. */\n const exportPng = useCallback((): string | null => {\n const canvas = canvasRef.current;\n if (!canvas || sceneEmpty()) return null;\n const { exportInk: ink, exportBackground: bg } = opts.current;\n const dpr = window.devicePixelRatio || 1;\n const out = document.createElement(\"canvas\");\n out.width = canvas.width;\n out.height = canvas.height;\n const ctx = out.getContext(\"2d\");\n if (!ctx) return null;\n if (bg) {\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, out.width, out.height);\n }\n paintInto(ctx, ink, out.width / dpr, out.height / dpr, dpr);\n return out.toDataURL(\"image/png\");\n }, [paintInto]);\n\n const detail = (): SignatureDetail => {\n const s = scene.current;\n return s.mode === \"typed\" ? { method: \"typed\", typedName: s.name.trim() } : { method: \"drawn\" };\n };\n\n /** After every change to the scene: sync the markup's mirror, repaint, tell the host. */\n const onChangeRef = useRef(onChange);\n useEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n const commit = () => {\n const s = scene.current;\n setStrokeCount(s.strokes.length);\n setMode(s.mode);\n setTypedName(s.name);\n repaint();\n onChangeRef.current?.(exportPng(), detail());\n };\n\n const clearScene = () => {\n scene.current = { ...scene.current, strokes: [], name: \"\" };\n live.current = null;\n refWidth.current = 0;\n commit();\n };\n\n useImperativeHandle(ref, () => ({ clear: clearScene, toDataURL: exportPng, isEmpty: sceneEmpty }));\n\n /* ── Pointer ── */\n\n const toPoint = (e: { clientX: number; clientY: number; pressure: number; pointerType: string }, rect: DOMRect): Point => {\n const scale = refWidth.current > 0 && rect.width > 0 ? rect.width / refWidth.current : 1;\n return {\n x: (e.clientX - rect.left) / scale,\n y: (e.clientY - rect.top) / scale,\n // Only a pen's pressure means anything: a mouse reports 0.5 while pressed and\n // touch hardware reports 0, 1 or noise.\n p: e.pointerType === \"pen\" && e.pressure > 0 ? e.pressure : 0.5,\n };\n };\n\n const onPointerDown = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n // One stroke at a time: a palm resting beside the pen is a second pointer.\n if (disabled || typed || live.current) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n e.preventDefault();\n const canvas = e.currentTarget;\n canvas.setPointerCapture?.(e.pointerId);\n const rect = canvas.getBoundingClientRect();\n if (refWidth.current === 0) refWidth.current = rect.width || 1;\n live.current = { id: e.pointerId, stroke: [toPoint(e, rect)] };\n };\n\n const onPointerMove = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n const pending = live.current;\n if (!pending || pending.id !== e.pointerId) return;\n e.preventDefault();\n const canvas = e.currentTarget;\n const rect = canvas.getBoundingClientRect();\n // Coalesced events: the samples the browser merged into this one move. A fast\n // flick on a 120 Hz pen is otherwise a handful of long straight chords.\n const samples = e.nativeEvent.getCoalescedEvents?.() ?? [];\n const events = samples.length > 0 ? samples : [e];\n const ctx = canvas.getContext(\"2d\");\n if (ctx) prepare(ctx, screenInk(canvas), window.devicePixelRatio || 1);\n const scale = refWidth.current > 0 && rect.width > 0 ? rect.width / refWidth.current : 1;\n for (const ev of events) {\n pending.stroke.push(toPoint(ev, rect));\n // Only the newest segment — a full repaint per move is O(ink) on every event.\n if (ctx) drawSegment(ctx, pending.stroke, pending.stroke.length - 1, scale, opts.current.lineWidth);\n }\n };\n\n const endStroke = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n const pending = live.current;\n if (!pending || pending.id !== e.pointerId) return;\n e.currentTarget.releasePointerCapture?.(e.pointerId);\n live.current = null;\n scene.current = { ...scene.current, strokes: [...scene.current.strokes, pending.stroke] };\n commit();\n };\n\n /* ── Buttons ── */\n\n const undo = () => {\n if (disabled || scene.current.strokes.length === 0) return;\n const strokes = scene.current.strokes.slice(0, -1);\n if (strokes.length === 0) refWidth.current = 0;\n scene.current = { ...scene.current, strokes };\n commit();\n announce(labels.undone);\n };\n\n const clear = () => {\n if (disabled || sceneEmpty()) return;\n clearScene();\n announce(labels.cleared);\n };\n\n const switchMode = () => {\n if (disabled) return;\n scene.current = { ...scene.current, mode: scene.current.mode === \"drawn\" ? \"typed\" : \"drawn\" };\n commit();\n };\n\n const onTypedChange = (name: string) => {\n scene.current = { ...scene.current, name };\n commit();\n };\n\n const save = () => {\n if (disabled) return;\n const png = exportPng();\n if (png) onSave?.(png, detail());\n };\n\n const hasError = hasContent(error);\n const isInvalid = Boolean(invalid) || hasError;\n const describedBy = [descId, statusId, hasError ? errorId : null].filter(Boolean).join(\" \");\n\n // `aria-disabled` rather than `disabled` on undo/clear: they disable THEMSELVES\n // (clear empties the pad, the last undo does too), and a natively disabled button\n // drops keyboard focus to <body> the moment it is activated.\n const softDisabled = \"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\";\n\n return (\n <div {...rest} className={cn(\"relative space-y-1.5\", className)} data-empty={isEmpty || undefined}>\n <div>\n <span id={labelId} className=\"block text-sm font-medium text-primary\">\n {label ?? labels.label}\n </span>\n <p id={descId} className=\"text-xs text-muted\">\n {description ?? labels.instructions}\n {allowTypedName && !typed ? ` ${labels.typedFallbackHint}` : null}\n </p>\n </div>\n\n <div className=\"relative\">\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-labelledby={labelId}\n aria-describedby={describedBy}\n // No `aria-invalid` / `aria-disabled`: `img` supports neither. The error\n // reaches a screen reader through `aria-describedby`; the attributes below\n // are for styling and tests.\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endStroke}\n onPointerCancel={endStroke}\n className={cn(\n // `touch-none` stops the page scrolling under a finger that is signing.\n \"block h-40 w-full touch-none select-none rounded-md border border-subtle bg-surface text-primary\",\n typed || disabled ? \"cursor-default\" : \"cursor-crosshair\",\n disabled && \"opacity-60\",\n isInvalid && FIELD_INVALID,\n canvasClassName,\n )}\n />\n {/* The signing line — where people expect to sign. Decoration only. */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-6 bottom-8 border-b border-dashed border-strong\"\n />\n <span id={statusId} className=\"sr-only\">\n {isEmpty ? labels.empty : labels.signed}\n </span>\n </div>\n\n {typed && (\n <Input\n label={labels.typedName}\n value={typedName}\n autoComplete=\"name\"\n disabled={disabled}\n onChange={(e) => onTypedChange(e.target.value)}\n />\n )}\n\n <div className=\"flex flex-wrap items-center gap-2\">\n {!typed && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={undo}\n aria-disabled={disabled || strokeCount === 0 || undefined}\n className={softDisabled}\n >\n <Undo2 aria-hidden=\"true\" className=\"size-4\" />\n {labels.undo}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={clear}\n aria-disabled={disabled || isEmpty || undefined}\n className={softDisabled}\n >\n <Eraser aria-hidden=\"true\" className=\"size-4\" />\n {labels.clear}\n </Button>\n {allowTypedName && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={switchMode}\n disabled={disabled}\n >\n {typed ? <PenLine aria-hidden=\"true\" className=\"size-4\" /> : <Keyboard aria-hidden=\"true\" className=\"size-4\" />}\n {typed ? labels.useDrawn : labels.useTyped}\n </Button>\n )}\n {onSave && (\n <Button type=\"button\" variant=\"brand\" onClick={save} disabled={disabled || isEmpty} className=\"ms-auto\">\n {labels.save}\n </Button>\n )}\n </div>\n\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n {/* Rendered from the start and never unmounted — see useAnnounce. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── Read-only ───────────────────────────────────────────────────────────── */\n\nexport interface SignatureViewProps extends ComponentPropsWithRef<\"figure\"> {\n /** The saved PNG — what `SignaturePad` handed to `onChange`/`onSave`, as stored. */\n value?: string | null;\n /**\n * A signature given as a typed name (`detail.typedName`), for a host that stores the\n * text rather than its PNG. Rendered in the same script face the pad draws it in.\n * Ignored while `value` is set.\n */\n typedName?: string | null;\n /** Visible caption above the signature; defaults to `labels.label`. `null` hides it\n * (the image keeps its alternative text) — for a signature block whose own heading\n * already says whose signature it is. */\n label?: ReactNode;\n /**\n * The exported PNG is dark ink on a transparent ground (`exportInk`), which is\n * invisible on the dark theme's surface. By default the image is colour-inverted\n * under `.dark`, so the ink reads light; `false` keeps the pixels as stored — for a\n * PNG exported with its own `exportBackground`, which inverting would turn black.\n */\n adaptInk?: boolean;\n /** Classes for the frame — its height, chiefly (default `h-40`, the pad's). */\n frameClassName?: string;\n /** User-facing strings; see {@link SignaturePadLabels} (`view*`). */\n labels?: LabelOverride<SignaturePadLabels>;\n}\n\n/**\n * A saved signature, shown — the read side of {@link SignaturePad}: the same frame and\n * signing line, none of the drawing chrome (no canvas, buttons, instructions or live\n * region). A separate component rather than a `readOnly` pad because there is nothing\n * of the pad to reuse: the pad's scene is STROKES, and a stored signature is a PNG\n * (or a name) that cannot be turned back into them.\n *\n * Kastlan's handover protocol, reopened after signing, is the case: two signatures\n * that must be visible, and must not look editable.\n */\nexport function SignatureView({\n value,\n typedName,\n label,\n adaptInk = true,\n frameClassName,\n labels: labelsProp,\n className,\n ...rest\n}: SignatureViewProps) {\n const labels = useKitLabels(\"signaturePad\", DEFAULT_SIGNATURE_PAD_LABELS, labelsProp);\n const name = typedName?.trim() ?? \"\";\n const kind = value ? \"drawn\" : name ? \"typed\" : \"empty\";\n const caption = label === undefined ? labels.label : label;\n const captionId = useId();\n const captioned = caption !== null && caption !== false;\n return (\n <figure\n // Named explicitly: the figcaption-to-figure name is not computed everywhere.\n aria-labelledby={captioned ? captionId : undefined}\n {...rest}\n className={cn(\"m-0 space-y-1.5\", className)}\n data-empty={kind === \"empty\" || undefined}\n >\n {captioned && (\n <figcaption id={captionId} className=\"block text-sm font-medium text-primary\">{caption}</figcaption>\n )}\n <div\n className={cn(\n \"relative flex h-40 items-center justify-center overflow-hidden rounded-md border border-subtle bg-surface\",\n frameClassName,\n )}\n >\n {kind === \"drawn\" && (\n <img\n src={value as string}\n alt={labels.viewDrawn}\n className={cn(\"relative max-h-full max-w-full object-contain\", adaptInk && \"dark:invert\")}\n />\n )}\n {kind === \"typed\" && (\n // One name for the whole thing: the visible text alone would be read as a bare\n // name, with nothing saying it IS the signature.\n <span\n role=\"img\"\n aria-label={labels.viewTyped(name)}\n // A name, so never cut at Large: it wraps inside the 10rem frame from Large\n // up (`truncate-until-large`, text size §10.17; round 0.34 T1).\n className=\"relative max-w-[85%] truncate-until-large px-2 text-4xl italic text-primary\"\n style={{ fontFamily: SCRIPT_FONT }}\n >\n {name}\n </span>\n )}\n {kind === \"empty\" && (\n <span className=\"relative text-xs text-muted\">{labels.viewEmpty}</span>\n )}\n {/* The signing line, as on the pad. Decoration only. */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-6 bottom-8 border-b border-dashed border-strong\"\n />\n </div>\n </figure>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/signature-pad.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentPropsWithRef, PointerEvent as ReactPointerEvent, ReactNode, Ref } from \"react\";\nimport { Eraser, Keyboard, PenLine, Undo2 } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { Button } from \"./button\";\nimport { FIELD_INVALID } from \"./field-base\";\nimport { Input } from \"./input\";\nimport { FIELD_ERROR_CLASS } from \"./field-parts\";\nimport { DEFAULT_SIGNATURE_PAD_LABELS } from \"./signature-pad-labels\";\n\n/**\n * A drawn signature, captured on a canvas and handed back as a PNG data URL.\n *\n * Kastlan wrote the first one (`features/handover/components/signature-pad.tsx`) for\n * the handover protocol, where tenant and inspector each sign on the same phone. This\n * keeps its contract — draw, get a PNG — and fixes what a copy per app would keep\n * getting wrong:\n *\n * 1. **Strokes are data, not pixels.** Each stroke is kept as a list of points, and\n * the canvas is a VIEW of them. That is what lets a resize (a phone rotating\n * mid-signature) keep the drawing instead of wiping it — setting `canvas.width`\n * clears the bitmap, so a pixels-only pad loses everything — and what makes undo\n * one `pop()`. The pixel buffer is sized to `devicePixelRatio`, so a stroke is\n * sharp on a 3× screen rather than upscaled from a 1× bitmap.\n * 2. **Smooth, and pressure-aware.** Segments are quadratic curves through the\n * midpoints between samples, so a fast stroke is a curve, not a polyline; a pen\n * that reports pressure varies the width, a mouse or finger (whose `pressure` is a\n * constant 0.5, or junk) draws at the base width.\n * 3. **Two inks.** On screen the ink is the computed `color` of the canvas, which is\n * `--text-primary` — so it reads in the dark theme, where a hard-coded near-black\n * stroke vanished into the surface. The exported PNG is re-rendered from the same\n * strokes in `exportInk` (near-black by default) on a transparent background. A\n * signature is a DOCUMENT artefact: it is printed on the protocol, shown on a white\n * card, embedded in a PDF — none of which follow the signer's theme. Exporting\n * the on-screen ink would have produced a pale-grey-on-transparent PNG for every\n * dark-mode signer, invisible on white paper.\n *\n * **Accessibility.** A canvas has no content a screen reader or a keyboard can reach,\n * and no amount of ARIA changes that — drawing is a pointer act. What the pad does:\n * the canvas is `role=\"img\"`, named by the visible label and described by the\n * instructions and by its state (\"Nothing drawn yet\" / \"Signature drawn\"), so a\n * screen-reader user knows what the box is and whether it holds anything; undo and\n * clear are real buttons, and their effect is announced. What it cannot do is let\n * someone sign without a pointer — hence `allowTypedName`, which offers a typed name\n * instead. It is OPT-IN, because whether a typed name counts as a signature is a\n * product and legal decision, not a UI one (Kastlan's handover accepts one; a flow\n * that needs a drawn mark would not). Where a drawn mark is not strictly required,\n * turn it on.\n */\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the pad renders or speaks. */\nexport interface SignaturePadLabels {\n /** Visible label above the pad, and the canvas's accessible name. The `label`\n * prop overrides it per instance. */\n label: string;\n /** Under the label, and part of the canvas's description. */\n instructions: string;\n /** Appended to the instructions when `allowTypedName` is on. */\n typedFallbackHint: string;\n /** The canvas's state for a screen reader. */\n empty: string;\n signed: string;\n undo: string;\n clear: string;\n /** Only rendered when `onSave` is given. */\n save: string;\n /** The mode switch (`allowTypedName`). */\n useTyped: string;\n useDrawn: string;\n /** Label of the typed-name field. */\n typedName: string;\n /** Live-region messages after the matching button. */\n cleared: string;\n undone: string;\n /** {@link SignatureView}: what it says when there is no signature to show. */\n viewEmpty: string;\n /** {@link SignatureView}: the saved PNG's alternative text. */\n viewDrawn: string;\n /** {@link SignatureView}: the accessible name of a typed-name signature. */\n viewTyped: (name: string) => string;\n}\n\n// Defined in ./signature-pad-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SIGNATURE_PAD_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** How the value handed back was made. */\nexport interface SignatureDetail {\n method: \"drawn\" | \"typed\";\n /** The name as typed, when `method` is `\"typed\"` — the PNG shows it, but a host\n * that stores a typed name as TEXT (Kastlan does) wants the string. */\n typedName?: string;\n}\n\n/** The imperative handle: `ref.current.toDataURL()` at submit time, for a form that\n * does not want to mirror the value into state on every stroke. */\nexport interface SignaturePadHandle {\n /** Remove every stroke and the typed name. Fires `onChange(null)`. */\n clear: () => void;\n /** The PNG, rendered in `exportInk` — or `null` while nothing is signed. */\n toDataURL: () => string | null;\n isEmpty: () => boolean;\n}\n\nexport interface SignaturePadProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Visible label; defaults to `labels.label`. */\n label?: ReactNode;\n /** Replaces `labels.instructions` for this instance (e.g. a legal note). */\n description?: ReactNode;\n /**\n * After every finished stroke, undo, clear and typed-name edit: the PNG, or `null`\n * once the pad is empty. Not called mid-stroke — encoding a PNG per pointer move\n * would stall the drawing on a phone.\n */\n onChange?: (dataUrl: string | null, detail: SignatureDetail) => void;\n /** Renders a Save button, enabled once something is signed. */\n onSave?: (dataUrl: string, detail: SignatureDetail) => void;\n disabled?: boolean;\n /** Paints the pad as wrong and sets `aria-invalid`. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** Message under the pad; implies `invalid` and is attached with\n * `aria-describedby`, like the kit's other fields. */\n error?: ReactNode;\n /** Offer a typed name as an alternative to drawing. See the accessibility note. */\n allowTypedName?: boolean;\n /** Ink of the exported PNG. Default near-black, legible on white. */\n exportInk?: string;\n /** Fill behind the exported PNG. Default transparent. */\n exportBackground?: string;\n /** Base stroke width in CSS pixels at Normal text size. Default 2.5. It grows with the\n * text size (× 1.25 / × 1.5, docs/text-size-harmonization.md §10.11), as the pad's\n * rem height does, so a signature keeps its weight on a larger pad. */\n lineWidth?: number;\n /** Classes for the canvas — its height, chiefly (default `h-40`). */\n canvasClassName?: string;\n /** User-facing strings; see {@link SignaturePadLabels}. */\n labels?: LabelOverride<SignaturePadLabels>;\n /** The {@link SignaturePadHandle}, not an element. A React 19 ref prop since 0.34\n * (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<SignaturePadHandle>;\n}\n\n/* ── Drawing ─────────────────────────────────────────────────────────────── */\n\ninterface Point {\n x: number;\n y: number;\n /** 0–1. A constant 0.5 for anything that is not a pen. */\n p: number;\n}\n\ntype Stroke = Point[];\n\nconst DEFAULT_EXPORT_INK = \"#111111\";\n\n// A handwriting face where the platform has one, `cursive` everywhere else. Only\n// ever used to render a typed name onto the canvas.\nconst SCRIPT_FONT = `\"Segoe Script\", \"Bradley Hand\", \"Brush Script MT\", cursive`;\n\n/** Width multiplier from pressure: 0.5 (the no-pen constant) is exactly 1. */\nconst pressureWidth = (p: number) => 0.4 + 1.2 * p;\n\nconst mid = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, p: (a.p + b.p) / 2 });\n\n/**\n * Segment `i` (1 ≤ i < n) of a stroke: a quadratic from the midpoint before sample\n * `i - 1` to the midpoint after it, with the sample as the control point. The live\n * drawing and the full repaint both go through here, so a stroke looks the same\n * before and after a resize.\n */\nfunction drawSegment(ctx: CanvasRenderingContext2D, pts: Stroke, i: number, scale: number, width: number) {\n const a = i === 1 ? pts[0] : mid(pts[i - 2], pts[i - 1]);\n const c = pts[i - 1];\n const b = mid(pts[i - 1], pts[i]);\n ctx.lineWidth = width * scale * pressureWidth((a.p + b.p) / 2);\n ctx.beginPath();\n ctx.moveTo(a.x * scale, a.y * scale);\n ctx.quadraticCurveTo(c.x * scale, c.y * scale, b.x * scale, b.y * scale);\n ctx.stroke();\n}\n\n/** The last half-segment, from the final midpoint to the final sample. Drawn once the\n * stroke is finished — while drawing, the pen is still there and it would be redrawn\n * on every move. A single tap is a dot. */\nfunction drawTail(ctx: CanvasRenderingContext2D, pts: Stroke, scale: number, width: number) {\n const last = pts[pts.length - 1];\n if (pts.length === 1) {\n ctx.beginPath();\n ctx.arc(last.x * scale, last.y * scale, (width * scale * pressureWidth(last.p)) / 2, 0, Math.PI * 2);\n ctx.fill();\n return;\n }\n const from = pts.length === 2 ? pts[0] : mid(pts[pts.length - 2], last);\n ctx.lineWidth = width * scale * pressureWidth(last.p);\n ctx.beginPath();\n ctx.moveTo(from.x * scale, from.y * scale);\n ctx.lineTo(last.x * scale, last.y * scale);\n ctx.stroke();\n}\n\nfunction prepare(ctx: CanvasRenderingContext2D, ink: string, dpr: number) {\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.strokeStyle = ink;\n ctx.fillStyle = ink;\n}\n\n/** A typed name, as large as fits, on the pad's baseline. */\nfunction drawName(ctx: CanvasRenderingContext2D, name: string, w: number, h: number) {\n let size = Math.min(48, h * 0.45);\n ctx.font = `italic ${size}px ${SCRIPT_FONT}`;\n const room = w * 0.85;\n const measured = ctx.measureText(name).width;\n if (measured > room) {\n size = Math.max(12, (size * room) / measured);\n ctx.font = `italic ${size}px ${SCRIPT_FONT}`;\n }\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"alphabetic\";\n ctx.fillText(name, w / 2, h * 0.68);\n}\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport function SignaturePad({\n ref,\n label,\n description,\n onChange,\n onSave,\n disabled,\n invalid,\n error,\n allowTypedName,\n exportInk = DEFAULT_EXPORT_INK,\n exportBackground,\n lineWidth = 2.5,\n canvasClassName,\n labels: labelsProp,\n className,\n ...rest\n}: SignaturePadProps) {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n const labels = useKitLabels(\"signaturePad\", DEFAULT_SIGNATURE_PAD_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const labelId = useId();\n const descId = useId();\n const statusId = useId();\n const errorId = useId();\n\n const canvasRef = useRef<HTMLCanvasElement>(null);\n /**\n * What is on the pad lives in a ref, not in state: a pointer move must not\n * re-render, and `onChange` has to see the pad AFTER a change, from inside the\n * handler that made it — before React has rendered anything. The state below\n * mirrors the parts the markup needs (empty or not, which mode, the field's text).\n */\n const scene = useRef<{ strokes: Stroke[]; mode: \"drawn\" | \"typed\"; name: string }>({\n strokes: [],\n mode: \"drawn\",\n name: \"\",\n });\n const live = useRef<{ id: number; stroke: Stroke } | null>(null);\n /**\n * The canvas width, in CSS px, the stored points are relative to. Fixed by the\n * first stroke; a later resize scales the drawing UNIFORMLY by `width / refWidth`,\n * so a rotated phone shows the same signature larger or smaller, never stretched.\n */\n const refWidth = useRef(0);\n const [strokeCount, setStrokeCount] = useState(0);\n const [mode, setMode] = useState<\"drawn\" | \"typed\">(\"drawn\");\n const [typedName, setTypedName] = useState(\"\");\n\n const typed = mode === \"typed\";\n const isEmpty = typed ? typedName.trim() === \"\" : strokeCount === 0;\n\n // Everything the paint and export paths read that can change between renders, so\n // the observers below subscribe once instead of on every keystroke.\n // The stroke follows the text size like everything in rem around it (§10.11).\n const { scale: textScale } = useTextSize();\n const strokeWidth = lineWidth * textScale;\n const opts = useRef({ lineWidth: strokeWidth, exportInk, exportBackground });\n useEffect(() => {\n opts.current = { lineWidth: strokeWidth, exportInk, exportBackground };\n }, [strokeWidth, exportInk, exportBackground]);\n\n const sceneEmpty = () => {\n const s = scene.current;\n return s.mode === \"typed\" ? s.name.trim() === \"\" : s.strokes.length === 0;\n };\n\n /** The whole scene, painted into `ctx` at `w`×`h` CSS px. */\n const paintInto = useCallback((ctx: CanvasRenderingContext2D, ink: string, w: number, h: number, dpr: number) => {\n const { lineWidth: width } = opts.current;\n const s = scene.current;\n prepare(ctx, ink, dpr);\n if (s.mode === \"typed\") {\n if (s.name.trim()) drawName(ctx, s.name.trim(), w, h);\n return;\n }\n const scale = refWidth.current > 0 ? w / refWidth.current : 1;\n for (const stroke of s.strokes) {\n for (let i = 1; i < stroke.length; i += 1) drawSegment(ctx, stroke, i, scale, width);\n drawTail(ctx, stroke, scale, width);\n }\n const pending = live.current?.stroke;\n if (pending) for (let i = 1; i < pending.length; i += 1) drawSegment(ctx, pending, i, scale, width);\n }, []);\n\n /** The ink on screen: the canvas's own computed `color`, i.e. `--text-primary`. */\n const screenInk = (canvas: HTMLCanvasElement) =>\n // Read as `color` rather than as the custom property: `--text-primary` is whatever\n // the palette wrote (a `color-mix()` in some presets), which a 2D context may not\n // parse; `color` has already been resolved by the browser.\n getComputedStyle(canvas).color || opts.current.exportInk;\n\n /** Repaint the visible canvas from the scene, in the theme's ink. */\n const repaint = useCallback(() => {\n const canvas = canvasRef.current;\n const ctx = canvas?.getContext(\"2d\");\n if (!canvas || !ctx) return;\n const dpr = window.devicePixelRatio || 1;\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n paintInto(ctx, screenInk(canvas), canvas.width / dpr, canvas.height / dpr, dpr);\n }, [paintInto]);\n\n /** Size the bitmap to the element × devicePixelRatio, then repaint. */\n const resize = useCallback(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n const w = Math.round(rect.width * dpr);\n const h = Math.round(rect.height * dpr);\n // Assigning `width` clears the bitmap even when the value is unchanged, so only\n // when it moved — and the repaint puts the strokes back either way.\n if (canvas.width !== w) canvas.width = w;\n if (canvas.height !== h) canvas.height = h;\n repaint();\n }, [repaint]);\n\n useLayoutEffect(() => {\n resize();\n }, [resize]);\n\n // A changed base width — or text size — redraws what is already there.\n useEffect(() => {\n repaint();\n }, [strokeWidth, repaint]);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const cleanups: Array<() => void> = [];\n if (typeof ResizeObserver !== \"undefined\") {\n const ro = new ResizeObserver(() => resize());\n ro.observe(canvas);\n cleanups.push(() => ro.disconnect());\n }\n // The ink follows the theme, and the theme is a class (`.dark`) or inline\n // variables (the palette store) on <html> — neither of which tells a canvas.\n if (typeof MutationObserver !== \"undefined\") {\n const mo = new MutationObserver(() => repaint());\n mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] });\n cleanups.push(() => mo.disconnect());\n }\n const mq = typeof window.matchMedia === \"function\" ? window.matchMedia(\"(prefers-color-scheme: dark)\") : null;\n if (mq?.addEventListener) {\n mq.addEventListener(\"change\", repaint);\n cleanups.push(() => mq.removeEventListener(\"change\", repaint));\n }\n return () => cleanups.forEach((fn) => fn());\n }, [resize, repaint]);\n\n /** The PNG, re-rendered off-screen in the export ink — `null` while empty. */\n const exportPng = useCallback((): string | null => {\n const canvas = canvasRef.current;\n if (!canvas || sceneEmpty()) return null;\n const { exportInk: ink, exportBackground: bg } = opts.current;\n const dpr = window.devicePixelRatio || 1;\n const out = document.createElement(\"canvas\");\n out.width = canvas.width;\n out.height = canvas.height;\n const ctx = out.getContext(\"2d\");\n if (!ctx) return null;\n if (bg) {\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, out.width, out.height);\n }\n paintInto(ctx, ink, out.width / dpr, out.height / dpr, dpr);\n return out.toDataURL(\"image/png\");\n }, [paintInto]);\n\n const detail = (): SignatureDetail => {\n const s = scene.current;\n return s.mode === \"typed\" ? { method: \"typed\", typedName: s.name.trim() } : { method: \"drawn\" };\n };\n\n /** After every change to the scene: sync the markup's mirror, repaint, tell the host. */\n const onChangeRef = useRef(onChange);\n useEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n const commit = () => {\n const s = scene.current;\n setStrokeCount(s.strokes.length);\n setMode(s.mode);\n setTypedName(s.name);\n repaint();\n onChangeRef.current?.(exportPng(), detail());\n };\n\n const clearScene = () => {\n scene.current = { ...scene.current, strokes: [], name: \"\" };\n live.current = null;\n refWidth.current = 0;\n commit();\n };\n\n useImperativeHandle(ref, () => ({ clear: clearScene, toDataURL: exportPng, isEmpty: sceneEmpty }));\n\n /* ── Pointer ── */\n\n const toPoint = (e: { clientX: number; clientY: number; pressure: number; pointerType: string }, rect: DOMRect): Point => {\n const scale = refWidth.current > 0 && rect.width > 0 ? rect.width / refWidth.current : 1;\n return {\n x: (e.clientX - rect.left) / scale,\n y: (e.clientY - rect.top) / scale,\n // Only a pen's pressure means anything: a mouse reports 0.5 while pressed and\n // touch hardware reports 0, 1 or noise.\n p: e.pointerType === \"pen\" && e.pressure > 0 ? e.pressure : 0.5,\n };\n };\n\n const onPointerDown = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n // One stroke at a time: a palm resting beside the pen is a second pointer.\n if (disabled || typed || live.current) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n e.preventDefault();\n const canvas = e.currentTarget;\n canvas.setPointerCapture?.(e.pointerId);\n const rect = canvas.getBoundingClientRect();\n if (refWidth.current === 0) refWidth.current = rect.width || 1;\n live.current = { id: e.pointerId, stroke: [toPoint(e, rect)] };\n };\n\n const onPointerMove = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n const pending = live.current;\n if (!pending || pending.id !== e.pointerId) return;\n e.preventDefault();\n const canvas = e.currentTarget;\n const rect = canvas.getBoundingClientRect();\n // Coalesced events: the samples the browser merged into this one move. A fast\n // flick on a 120 Hz pen is otherwise a handful of long straight chords.\n const samples = e.nativeEvent.getCoalescedEvents?.() ?? [];\n const events = samples.length > 0 ? samples : [e];\n const ctx = canvas.getContext(\"2d\");\n if (ctx) prepare(ctx, screenInk(canvas), window.devicePixelRatio || 1);\n const scale = refWidth.current > 0 && rect.width > 0 ? rect.width / refWidth.current : 1;\n for (const ev of events) {\n pending.stroke.push(toPoint(ev, rect));\n // Only the newest segment — a full repaint per move is O(ink) on every event.\n if (ctx) drawSegment(ctx, pending.stroke, pending.stroke.length - 1, scale, opts.current.lineWidth);\n }\n };\n\n const endStroke = (e: ReactPointerEvent<HTMLCanvasElement>) => {\n const pending = live.current;\n if (!pending || pending.id !== e.pointerId) return;\n e.currentTarget.releasePointerCapture?.(e.pointerId);\n live.current = null;\n scene.current = { ...scene.current, strokes: [...scene.current.strokes, pending.stroke] };\n commit();\n };\n\n /* ── Buttons ── */\n\n const undo = () => {\n if (disabled || scene.current.strokes.length === 0) return;\n const strokes = scene.current.strokes.slice(0, -1);\n if (strokes.length === 0) refWidth.current = 0;\n scene.current = { ...scene.current, strokes };\n commit();\n announce(labels.undone);\n };\n\n const clear = () => {\n if (disabled || sceneEmpty()) return;\n clearScene();\n announce(labels.cleared);\n };\n\n const switchMode = () => {\n if (disabled) return;\n scene.current = { ...scene.current, mode: scene.current.mode === \"drawn\" ? \"typed\" : \"drawn\" };\n commit();\n };\n\n const onTypedChange = (name: string) => {\n scene.current = { ...scene.current, name };\n commit();\n };\n\n const save = () => {\n if (disabled) return;\n const png = exportPng();\n if (png) onSave?.(png, detail());\n };\n\n const hasError = hasContent(error);\n const isInvalid = Boolean(invalid) || hasError;\n const describedBy = [descId, statusId, hasError ? errorId : null].filter(Boolean).join(\" \");\n\n // `aria-disabled` rather than `disabled` on undo/clear: they disable THEMSELVES\n // (clear empties the pad, the last undo does too), and a natively disabled button\n // drops keyboard focus to <body> the moment it is activated.\n const softDisabled = \"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\";\n\n return (\n <div {...rest} className={cn(\"relative space-y-1.5\", className)} data-empty={isEmpty || undefined}>\n <div>\n <span id={labelId} className=\"block text-sm font-medium text-primary\">\n {label ?? labels.label}\n </span>\n <p id={descId} className=\"text-xs text-muted\">\n {description ?? labels.instructions}\n {allowTypedName && !typed ? ` ${labels.typedFallbackHint}` : null}\n </p>\n </div>\n\n <div className=\"relative\">\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-labelledby={labelId}\n aria-describedby={describedBy}\n // No `aria-invalid` / `aria-disabled`: `img` supports neither. The error\n // reaches a screen reader through `aria-describedby`; the attributes below\n // are for styling and tests.\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endStroke}\n onPointerCancel={endStroke}\n className={cn(\n // `touch-none` stops the page scrolling under a finger that is signing.\n \"block h-40 w-full touch-none select-none rounded-md border border-subtle bg-surface text-primary\",\n typed || disabled ? \"cursor-default\" : \"cursor-crosshair\",\n disabled && \"opacity-60\",\n isInvalid && FIELD_INVALID,\n canvasClassName,\n )}\n />\n {/* The signing line — where people expect to sign. Decoration only. */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-6 bottom-8 border-b border-dashed border-strong\"\n />\n <span id={statusId} className=\"sr-only\">\n {isEmpty ? labels.empty : labels.signed}\n </span>\n </div>\n\n {typed && (\n <Input\n label={labels.typedName}\n value={typedName}\n autoComplete=\"name\"\n disabled={disabled}\n onChange={(e) => onTypedChange(e.target.value)}\n />\n )}\n\n <div className=\"flex flex-wrap items-center gap-2\">\n {!typed && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={undo}\n aria-disabled={disabled || strokeCount === 0 || undefined}\n className={softDisabled}\n >\n <Undo2 aria-hidden=\"true\" className=\"size-4\" />\n {labels.undo}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={clear}\n aria-disabled={disabled || isEmpty || undefined}\n className={softDisabled}\n >\n <Eraser aria-hidden=\"true\" className=\"size-4\" />\n {labels.clear}\n </Button>\n {allowTypedName && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={switchMode}\n disabled={disabled}\n >\n {typed ? <PenLine aria-hidden=\"true\" className=\"size-4\" /> : <Keyboard aria-hidden=\"true\" className=\"size-4\" />}\n {typed ? labels.useDrawn : labels.useTyped}\n </Button>\n )}\n {onSave && (\n <Button type=\"button\" variant=\"accent\" onClick={save} disabled={disabled || isEmpty} className=\"ms-auto\">\n {labels.save}\n </Button>\n )}\n </div>\n\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n {/* Rendered from the start and never unmounted — see useAnnounce. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── Read-only ───────────────────────────────────────────────────────────── */\n\nexport interface SignatureViewProps extends ComponentPropsWithRef<\"figure\"> {\n /** The saved PNG — what `SignaturePad` handed to `onChange`/`onSave`, as stored. */\n value?: string | null;\n /**\n * A signature given as a typed name (`detail.typedName`), for a host that stores the\n * text rather than its PNG. Rendered in the same script face the pad draws it in.\n * Ignored while `value` is set.\n */\n typedName?: string | null;\n /** Visible caption above the signature; defaults to `labels.label`. `null` hides it\n * (the image keeps its alternative text) — for a signature block whose own heading\n * already says whose signature it is. */\n label?: ReactNode;\n /**\n * The exported PNG is dark ink on a transparent ground (`exportInk`), which is\n * invisible on the dark theme's surface. By default the image is colour-inverted\n * under `.dark`, so the ink reads light; `false` keeps the pixels as stored — for a\n * PNG exported with its own `exportBackground`, which inverting would turn black.\n */\n adaptInk?: boolean;\n /** Classes for the frame — its height, chiefly (default `h-40`, the pad's). */\n frameClassName?: string;\n /** User-facing strings; see {@link SignaturePadLabels} (`view*`). */\n labels?: LabelOverride<SignaturePadLabels>;\n}\n\n/**\n * A saved signature, shown — the read side of {@link SignaturePad}: the same frame and\n * signing line, none of the drawing chrome (no canvas, buttons, instructions or live\n * region). A separate component rather than a `readOnly` pad because there is nothing\n * of the pad to reuse: the pad's scene is STROKES, and a stored signature is a PNG\n * (or a name) that cannot be turned back into them.\n *\n * Kastlan's handover protocol, reopened after signing, is the case: two signatures\n * that must be visible, and must not look editable.\n */\nexport function SignatureView({\n value,\n typedName,\n label,\n adaptInk = true,\n frameClassName,\n labels: labelsProp,\n className,\n ...rest\n}: SignatureViewProps) {\n const labels = useKitLabels(\"signaturePad\", DEFAULT_SIGNATURE_PAD_LABELS, labelsProp);\n const name = typedName?.trim() ?? \"\";\n const kind = value ? \"drawn\" : name ? \"typed\" : \"empty\";\n const caption = label === undefined ? labels.label : label;\n const captionId = useId();\n const captioned = caption !== null && caption !== false;\n return (\n <figure\n // Named explicitly: the figcaption-to-figure name is not computed everywhere.\n aria-labelledby={captioned ? captionId : undefined}\n {...rest}\n className={cn(\"m-0 space-y-1.5\", className)}\n data-empty={kind === \"empty\" || undefined}\n >\n {captioned && (\n <figcaption id={captionId} className=\"block text-sm font-medium text-primary\">{caption}</figcaption>\n )}\n <div\n className={cn(\n \"relative flex h-40 items-center justify-center overflow-hidden rounded-md border border-subtle bg-surface\",\n frameClassName,\n )}\n >\n {kind === \"drawn\" && (\n <img\n src={value as string}\n alt={labels.viewDrawn}\n className={cn(\"relative max-h-full max-w-full object-contain\", adaptInk && \"dark:invert\")}\n />\n )}\n {kind === \"typed\" && (\n // One name for the whole thing: the visible text alone would be read as a bare\n // name, with nothing saying it IS the signature.\n <span\n role=\"img\"\n aria-label={labels.viewTyped(name)}\n // A name, so never cut at Large: it wraps inside the 10rem frame from Large\n // up (`truncate-until-large`, text size §10.17; round 0.34 T1).\n className=\"relative max-w-[85%] truncate-until-large px-2 text-4xl italic text-primary\"\n style={{ fontFamily: SCRIPT_FONT }}\n >\n {name}\n </span>\n )}\n {kind === \"empty\" && (\n <span className=\"relative text-xs text-muted\">{labels.viewEmpty}</span>\n )}\n {/* The signing line, as on the pad. Decoration only. */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-6 bottom-8 border-b border-dashed border-strong\"\n />\n </div>\n </figure>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import { cn } from "../lib/cn.js";
|
|
|
4
4
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
5
5
|
import { useTextSize } from "../theme/text-size.js";
|
|
6
6
|
import { DEFAULT_SPARKLINE_LABELS } from "./sparkline-labels.js";
|
|
7
|
+
import { formatWithMinus } from "../lib/minus.js";
|
|
7
8
|
const TONE_CLASS = {
|
|
8
9
|
chart: "text-[var(--chart-1)]",
|
|
9
10
|
brand: "text-brand",
|
|
@@ -27,7 +28,7 @@ function defaultFormat(locale) {
|
|
|
27
28
|
f = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
|
|
28
29
|
formatters.set(key, f);
|
|
29
30
|
}
|
|
30
|
-
return (v) => f
|
|
31
|
+
return (v) => formatWithMinus(f, v);
|
|
31
32
|
}
|
|
32
33
|
const isValue = (v) => typeof v === "number" && Number.isFinite(v);
|
|
33
34
|
function sparklineSummary(data, labels, format) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/sparkline.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { DEFAULT_SPARKLINE_LABELS } from \"./sparkline-labels\";\n\n/**\n * The words a sparkline speaks. It has no axes, no tooltip and no legend, so its\n * accessible name is the ONLY way a screen reader learns what the shape says —\n * \"Sparkline, image\" is not a summary. Each message takes the values already\n * formatted, so the digits follow the locale and the sentence stays translatable.\n */\nexport interface SparklineLabels {\n /** Last value above the first: \"Rising from 12 to 40\". */\n rising: (first: string, last: string) => string;\n /** Last value below the first. */\n falling: (first: string, last: string) => string;\n /** First and last are equal — the shape may still have moved in between. */\n flat: (value: string) => string;\n /** One value is a dot, not a trend. */\n single: (value: string) => string;\n /** No value at all (empty, or every point a gap). */\n noData: string;\n /** How the series' own name is joined to the summary when `label` is given. */\n named: (name: string, summary: string) => string;\n}\n\n// Defined in ./sparkline-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SPARKLINE_LABELS };\n\n/**\n * What colours the marks. Token names, not colours: every one resolves to a variable\n * in tokens.css, so the sparkline re-skins with the palette like everything else.\n *\n * - `chart` — the first series colour (`--chart-1`), the default.\n * - `income` / `expense` / `net` — the money trio.\n * - `signed` — a money series read by sign: bars each take income or expense by their\n * own value, a line or area takes the colour of its LAST value (where it ended up).\n */\nexport type SparklineTone =\n | \"chart\"\n | \"brand\"\n | \"muted\"\n | \"income\"\n | \"expense\"\n | \"net\"\n | \"signed\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\";\n\n// `color`, not `stroke`/`fill`: every mark paints with `currentColor`, so one class\n// per element is enough and a signed bar chart needs no second lookup table.\nconst TONE_CLASS: Record<Exclude<SparklineTone, \"signed\">, string> = {\n chart: \"text-[var(--chart-1)]\",\n brand: \"text-brand\",\n muted: \"text-muted\",\n income: \"text-money-income\",\n expense: \"text-money-expense\",\n net: \"text-money-net\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-danger\",\n info: \"text-info\",\n};\n\nconst signedClass = (v: number) => (v < 0 ? TONE_CLASS.expense : TONE_CLASS.income);\n\n/** `children` is the marks and `width`/`height` are numbers here (or `fluid`), so\n * those three are the sparkline's own; everything else lands on the `<svg>`. */\nexport interface SparklineProps\n extends Omit<ComponentPropsWithRef<\"svg\">, \"children\" | \"width\" | \"height\"> {\n /** The series, oldest first. `null`/`undefined`/`NaN` is a GAP — the line breaks\n * there rather than drawing a zero or bridging across the missing point. */\n data: ReadonlyArray<number | null | undefined>;\n /** Width in px at Normal. Ignored under `fluid`. The drawn box is this × the text\n * size's scale (0.32, docs/text-size-harmonization.md §3.2): a sparkline stands beside\n * a figure, and a 72 × 24 trend under a 150 % number reads as a speck. The strokes\n * stay px (`non-scaling-stroke`), as every chart's do. */\n width?: number;\n /** Height in px at Normal, × the scale likewise. */\n height?: number;\n /** Fill the container's width instead of a fixed one (the height stays fixed). */\n fluid?: boolean;\n variant?: \"line\" | \"area\" | \"bar\";\n tone?: SparklineTone;\n /** Lower / upper end of the scale. Unset, the scale is the data's own tight range\n * (bars always include zero, since a bar's length IS its value). */\n min?: number;\n max?: number;\n /** A dashed hairline at this value, e.g. `0` for a series that crosses zero, or a\n * target. It is pulled into the scale unless `min`/`max` exclude it. */\n referenceValue?: number;\n /** Dot on the last point (line/area) or dim every bar but the last. Default true —\n * the last value is the one the number beside a sparkline almost always is. */\n highlightLast?: boolean;\n strokeWidth?: number;\n /** What the series is (\"Price\"), prefixed to the spoken summary. */\n label?: string;\n /** How a value is spoken in the summary. Defaults to `Intl.NumberFormat` in the\n * kit's locale, at most two decimals. */\n formatValue?: (value: number) => string;\n locale?: string;\n labels?: LabelOverride<SparklineLabels>;\n}\n\nconst DEFAULT_WIDTH = 72;\nconst DEFAULT_HEIGHT = 24;\n\n// One formatter per locale, not per render: a sparkline is a table-cell component and\n// a hundred rows would otherwise build a hundred identical `Intl.NumberFormat`s.\nconst formatters = new Map<string, Intl.NumberFormat>();\nfunction defaultFormat(locale: string | undefined): (v: number) => string {\n const key = locale ?? \"\";\n let f = formatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });\n formatters.set(key, f);\n }\n return (v) => f.format(v);\n}\n\nconst isValue = (v: number | null | undefined): v is number => typeof v === \"number\" && Number.isFinite(v);\n\n/** The spoken summary, without the series name. Exported for callers that want the\n * same sentence elsewhere (a table's row description, a tooltip). */\nexport function sparklineSummary(\n data: ReadonlyArray<number | null | undefined>,\n labels: SparklineLabels,\n format: (value: number) => string,\n): string {\n const values = data.filter(isValue);\n if (values.length === 0) return labels.noData;\n const first = values[0];\n const last = values[values.length - 1];\n if (values.length === 1) return labels.single(format(last));\n if (last > first) return labels.rising(format(first), format(last));\n if (last < first) return labels.falling(format(first), format(last));\n return labels.flat(format(last));\n}\n\n/**\n * A tiny trend line: shape only — no axes, no tooltip, no legend, no chart library.\n *\n * It is plain SVG on purpose. The two copies it replaces were recharts `LineChart`s,\n * which is a ResizeObserver, a React tree of a dozen components and a 433 KB import\n * per table cell; a sparkline is one `<path>`, cheap enough for every row of a table\n * and small enough to live on the main barrel rather than behind `/chart`.\n *\n * The scale is TIGHT by default (the data's own min to max). A price that moved from\n * 9.75 to 12.40 drawn against an implicit zero baseline is a nearly flat line at the\n * top of the box — the one thing a shape-only chart must not do (keksdose dev#449).\n * Bars are the exception: a bar's length is its value, so their scale includes zero.\n *\n * `role=\"img\"` with a generated summary (\"Rising from 12 to 40\") as its name — the\n * numbers beside it are the values, the sparkline says which way they have moved.\n *\n * Under `fluid` the SVG is stretched non-uniformly to the container's width, so every\n * stroke is `non-scaling-stroke` and the last-point dot is a zero-length ROUND-CAPPED\n * stroke rather than a `<circle>`: a stroke keeps its width under that transform,\n * where a circle would be squashed into an ellipse.\n */\nexport function Sparkline({\n data,\n width = DEFAULT_WIDTH,\n height = DEFAULT_HEIGHT,\n fluid = false,\n variant = \"line\",\n tone = \"chart\",\n min,\n max,\n referenceValue,\n highlightLast = true,\n strokeWidth = 1.5,\n label,\n formatValue,\n locale,\n labels,\n className,\n style,\n ...rest\n}: SparklineProps) {\n const text = useKitLabels(\"sparkline\", DEFAULT_SPARKLINE_LABELS, labels);\n const { scale } = useTextSize();\n const kitLocale = useKitLocale(locale);\n const format = formatValue ?? defaultFormat(kitLocale);\n\n const summary = sparklineSummary(data, text, format);\n const name = label ? text.named(label, summary) : summary;\n\n const W = width;\n const H = height;\n const points = data.map((v) => (isValue(v) ? v : null));\n const values = points.filter(isValue);\n const n = points.length;\n\n // ── Scale ──\n const bounds = [...values];\n if (referenceValue !== undefined && isValue(referenceValue)) bounds.push(referenceValue);\n if (variant === \"bar\") bounds.push(0);\n let lo = min ?? (bounds.length ? Math.min(...bounds) : 0);\n let hi = max ?? (bounds.length ? Math.max(...bounds) : 1);\n if (hi === lo) {\n // A constant series: centre it rather than divide by zero.\n lo -= 1;\n hi += 1;\n }\n const dot = strokeWidth + 1.5;\n const pad = highlightLast && variant !== \"bar\" ? dot : strokeWidth / 2;\n const clamp = (v: number) => Math.min(hi, Math.max(lo, v));\n const y = (v: number) => pad + ((hi - clamp(v)) / (hi - lo)) * (H - 2 * pad);\n const x = (i: number) => (n <= 1 ? W / 2 : pad + (i * (W - 2 * pad)) / (n - 1));\n const r = (v: number) => Math.round(v * 100) / 100;\n\n // Last DEFINED point — the series may well end in a gap. (No `findLastIndex`: the\n // package targets ES2022.)\n let lastIndex = -1;\n for (let i = n - 1; i >= 0 && lastIndex < 0; i -= 1) if (points[i] !== null) lastIndex = i;\n const lastValue = lastIndex >= 0 ? (points[lastIndex] ?? 0) : 0;\n const lineClass = tone === \"signed\" ? signedClass(lastValue) : TONE_CLASS[tone];\n\n // Contiguous runs between gaps. A run of one point is drawn as a dot, so a value\n // isolated between two gaps is still visible.\n const runs: Array<Array<[number, number]>> = [];\n let run: Array<[number, number]> = [];\n points.forEach((v, i) => {\n if (v === null) {\n if (run.length) runs.push(run);\n run = [];\n } else {\n run.push([i, v]);\n }\n });\n if (run.length) runs.push(run);\n\n const linePath = (seg: Array<[number, number]>) =>\n seg.map(([i, v], k) => `${k === 0 ? \"M\" : \"L\"}${r(x(i))} ${r(y(v))}`).join(\" \");\n const dotPath = (i: number, v: number) => `M${r(x(i))} ${r(y(v))} h0`;\n // The area closes to zero when zero is on the scale, else to the bottom edge.\n const areaBase = y(lo <= 0 && hi >= 0 ? 0 : lo);\n\n const STROKE = {\n fill: \"none\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n vectorEffect: \"non-scaling-stroke\",\n } as const;\n\n return (\n <svg\n role=\"img\"\n aria-label={name}\n {...rest}\n // The drawing stays in its own units (the viewBox); only the box it is shown in\n // grows, uniformly, so nothing is distorted and the strokes keep their px width.\n width={fluid ? \"100%\" : W * scale}\n height={H * scale}\n viewBox={`0 0 ${W} ${H}`}\n preserveAspectRatio=\"none\"\n className={cn(fluid ? \"block w-full\" : \"inline-block shrink-0 align-middle\", \"overflow-visible\", className)}\n style={style}\n >\n {referenceValue !== undefined && isValue(referenceValue) && referenceValue >= lo && referenceValue <= hi && (\n <path\n d={`M0 ${r(y(referenceValue))} H${W}`}\n {...STROKE}\n strokeWidth={1}\n strokeDasharray=\"2 2\"\n className=\"text-[var(--border-strong)]\"\n />\n )}\n\n {variant === \"bar\"\n ? (() => {\n const slot = W / Math.max(n, 1);\n const bw = Math.max(1, slot * 0.7);\n const base = y(0);\n return points.map((v, i) => {\n if (v === null) return null;\n const top = Math.min(y(v), base);\n return (\n <rect\n key={i}\n x={r(i * slot + (slot - bw) / 2)}\n y={r(top)}\n width={r(bw)}\n height={r(Math.abs(y(v) - base))}\n fill=\"currentColor\"\n opacity={highlightLast && i !== lastIndex ? 0.55 : 1}\n className={tone === \"signed\" ? signedClass(v) : TONE_CLASS[tone]}\n />\n );\n });\n })()\n : runs.map((seg, k) =>\n seg.length === 1 ? (\n <path\n key={k}\n d={dotPath(seg[0][0], seg[0][1])}\n {...STROKE}\n strokeWidth={strokeWidth * 2}\n className={lineClass}\n />\n ) : (\n <g key={k} className={lineClass}>\n {variant === \"area\" && (\n <path\n d={`${linePath(seg)} L${r(x(seg[seg.length - 1][0]))} ${r(areaBase)} L${r(x(seg[0][0]))} ${r(areaBase)} Z`}\n fill=\"currentColor\"\n fillOpacity={0.15}\n stroke=\"none\"\n />\n )}\n <path d={linePath(seg)} {...STROKE} strokeWidth={strokeWidth} />\n </g>\n ),\n )}\n\n {highlightLast && variant !== \"bar\" && lastIndex >= 0 && (\n <path\n d={dotPath(lastIndex, lastValue)}\n {...STROKE}\n strokeWidth={dot * 2 - 1}\n className={lineClass}\n data-sparkline-last=\"\"\n />\n )}\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/sparkline.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { DEFAULT_SPARKLINE_LABELS } from \"./sparkline-labels\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/**\n * The words a sparkline speaks. It has no axes, no tooltip and no legend, so its\n * accessible name is the ONLY way a screen reader learns what the shape says —\n * \"Sparkline, image\" is not a summary. Each message takes the values already\n * formatted, so the digits follow the locale and the sentence stays translatable.\n */\nexport interface SparklineLabels {\n /** Last value above the first: \"Rising from 12 to 40\". */\n rising: (first: string, last: string) => string;\n /** Last value below the first. */\n falling: (first: string, last: string) => string;\n /** First and last are equal — the shape may still have moved in between. */\n flat: (value: string) => string;\n /** One value is a dot, not a trend. */\n single: (value: string) => string;\n /** No value at all (empty, or every point a gap). */\n noData: string;\n /** How the series' own name is joined to the summary when `label` is given. */\n named: (name: string, summary: string) => string;\n}\n\n// Defined in ./sparkline-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SPARKLINE_LABELS };\n\n/**\n * What colours the marks. Token names, not colours: every one resolves to a variable\n * in tokens.css, so the sparkline re-skins with the palette like everything else.\n *\n * - `chart` — the first series colour (`--chart-1`), the default.\n * - `income` / `expense` / `net` — the money trio.\n * - `signed` — a money series read by sign: bars each take income or expense by their\n * own value, a line or area takes the colour of its LAST value (where it ended up).\n */\nexport type SparklineTone =\n | \"chart\"\n | \"brand\"\n | \"muted\"\n | \"income\"\n | \"expense\"\n | \"net\"\n | \"signed\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\";\n\n// `color`, not `stroke`/`fill`: every mark paints with `currentColor`, so one class\n// per element is enough and a signed bar chart needs no second lookup table.\nconst TONE_CLASS: Record<Exclude<SparklineTone, \"signed\">, string> = {\n chart: \"text-[var(--chart-1)]\",\n brand: \"text-brand\",\n muted: \"text-muted\",\n income: \"text-money-income\",\n expense: \"text-money-expense\",\n net: \"text-money-net\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-danger\",\n info: \"text-info\",\n};\n\nconst signedClass = (v: number) => (v < 0 ? TONE_CLASS.expense : TONE_CLASS.income);\n\n/** `children` is the marks and `width`/`height` are numbers here (or `fluid`), so\n * those three are the sparkline's own; everything else lands on the `<svg>`. */\nexport interface SparklineProps\n extends Omit<ComponentPropsWithRef<\"svg\">, \"children\" | \"width\" | \"height\"> {\n /** The series, oldest first. `null`/`undefined`/`NaN` is a GAP — the line breaks\n * there rather than drawing a zero or bridging across the missing point. */\n data: ReadonlyArray<number | null | undefined>;\n /** Width in px at Normal. Ignored under `fluid`. The drawn box is this × the text\n * size's scale (0.32, docs/text-size-harmonization.md §3.2): a sparkline stands beside\n * a figure, and a 72 × 24 trend under a 150 % number reads as a speck. The strokes\n * stay px (`non-scaling-stroke`), as every chart's do. */\n width?: number;\n /** Height in px at Normal, × the scale likewise. */\n height?: number;\n /** Fill the container's width instead of a fixed one (the height stays fixed). */\n fluid?: boolean;\n variant?: \"line\" | \"area\" | \"bar\";\n tone?: SparklineTone;\n /** Lower / upper end of the scale. Unset, the scale is the data's own tight range\n * (bars always include zero, since a bar's length IS its value). */\n min?: number;\n max?: number;\n /** A dashed hairline at this value, e.g. `0` for a series that crosses zero, or a\n * target. It is pulled into the scale unless `min`/`max` exclude it. */\n referenceValue?: number;\n /** Dot on the last point (line/area) or dim every bar but the last. Default true —\n * the last value is the one the number beside a sparkline almost always is. */\n highlightLast?: boolean;\n strokeWidth?: number;\n /** What the series is (\"Price\"), prefixed to the spoken summary. */\n label?: string;\n /** How a value is spoken in the summary. Defaults to `Intl.NumberFormat` in the\n * kit's locale, at most two decimals. */\n formatValue?: (value: number) => string;\n locale?: string;\n labels?: LabelOverride<SparklineLabels>;\n}\n\nconst DEFAULT_WIDTH = 72;\nconst DEFAULT_HEIGHT = 24;\n\n// One formatter per locale, not per render: a sparkline is a table-cell component and\n// a hundred rows would otherwise build a hundred identical `Intl.NumberFormat`s.\nconst formatters = new Map<string, Intl.NumberFormat>();\nfunction defaultFormat(locale: string | undefined): (v: number) => string {\n const key = locale ?? \"\";\n let f = formatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });\n formatters.set(key, f);\n }\n return (v) => formatWithMinus(f, v);\n}\n\nconst isValue = (v: number | null | undefined): v is number => typeof v === \"number\" && Number.isFinite(v);\n\n/** The spoken summary, without the series name. Exported for callers that want the\n * same sentence elsewhere (a table's row description, a tooltip). */\nexport function sparklineSummary(\n data: ReadonlyArray<number | null | undefined>,\n labels: SparklineLabels,\n format: (value: number) => string,\n): string {\n const values = data.filter(isValue);\n if (values.length === 0) return labels.noData;\n const first = values[0];\n const last = values[values.length - 1];\n if (values.length === 1) return labels.single(format(last));\n if (last > first) return labels.rising(format(first), format(last));\n if (last < first) return labels.falling(format(first), format(last));\n return labels.flat(format(last));\n}\n\n/**\n * A tiny trend line: shape only — no axes, no tooltip, no legend, no chart library.\n *\n * It is plain SVG on purpose. The two copies it replaces were recharts `LineChart`s,\n * which is a ResizeObserver, a React tree of a dozen components and a 433 KB import\n * per table cell; a sparkline is one `<path>`, cheap enough for every row of a table\n * and small enough to live on the main barrel rather than behind `/chart`.\n *\n * The scale is TIGHT by default (the data's own min to max). A price that moved from\n * 9.75 to 12.40 drawn against an implicit zero baseline is a nearly flat line at the\n * top of the box — the one thing a shape-only chart must not do (keksdose dev#449).\n * Bars are the exception: a bar's length is its value, so their scale includes zero.\n *\n * `role=\"img\"` with a generated summary (\"Rising from 12 to 40\") as its name — the\n * numbers beside it are the values, the sparkline says which way they have moved.\n *\n * Under `fluid` the SVG is stretched non-uniformly to the container's width, so every\n * stroke is `non-scaling-stroke` and the last-point dot is a zero-length ROUND-CAPPED\n * stroke rather than a `<circle>`: a stroke keeps its width under that transform,\n * where a circle would be squashed into an ellipse.\n */\nexport function Sparkline({\n data,\n width = DEFAULT_WIDTH,\n height = DEFAULT_HEIGHT,\n fluid = false,\n variant = \"line\",\n tone = \"chart\",\n min,\n max,\n referenceValue,\n highlightLast = true,\n strokeWidth = 1.5,\n label,\n formatValue,\n locale,\n labels,\n className,\n style,\n ...rest\n}: SparklineProps) {\n const text = useKitLabels(\"sparkline\", DEFAULT_SPARKLINE_LABELS, labels);\n const { scale } = useTextSize();\n const kitLocale = useKitLocale(locale);\n const format = formatValue ?? defaultFormat(kitLocale);\n\n const summary = sparklineSummary(data, text, format);\n const name = label ? text.named(label, summary) : summary;\n\n const W = width;\n const H = height;\n const points = data.map((v) => (isValue(v) ? v : null));\n const values = points.filter(isValue);\n const n = points.length;\n\n // ── Scale ──\n const bounds = [...values];\n if (referenceValue !== undefined && isValue(referenceValue)) bounds.push(referenceValue);\n if (variant === \"bar\") bounds.push(0);\n let lo = min ?? (bounds.length ? Math.min(...bounds) : 0);\n let hi = max ?? (bounds.length ? Math.max(...bounds) : 1);\n if (hi === lo) {\n // A constant series: centre it rather than divide by zero.\n lo -= 1;\n hi += 1;\n }\n const dot = strokeWidth + 1.5;\n const pad = highlightLast && variant !== \"bar\" ? dot : strokeWidth / 2;\n const clamp = (v: number) => Math.min(hi, Math.max(lo, v));\n const y = (v: number) => pad + ((hi - clamp(v)) / (hi - lo)) * (H - 2 * pad);\n const x = (i: number) => (n <= 1 ? W / 2 : pad + (i * (W - 2 * pad)) / (n - 1));\n const r = (v: number) => Math.round(v * 100) / 100;\n\n // Last DEFINED point — the series may well end in a gap. (No `findLastIndex`: the\n // package targets ES2022.)\n let lastIndex = -1;\n for (let i = n - 1; i >= 0 && lastIndex < 0; i -= 1) if (points[i] !== null) lastIndex = i;\n const lastValue = lastIndex >= 0 ? (points[lastIndex] ?? 0) : 0;\n const lineClass = tone === \"signed\" ? signedClass(lastValue) : TONE_CLASS[tone];\n\n // Contiguous runs between gaps. A run of one point is drawn as a dot, so a value\n // isolated between two gaps is still visible.\n const runs: Array<Array<[number, number]>> = [];\n let run: Array<[number, number]> = [];\n points.forEach((v, i) => {\n if (v === null) {\n if (run.length) runs.push(run);\n run = [];\n } else {\n run.push([i, v]);\n }\n });\n if (run.length) runs.push(run);\n\n const linePath = (seg: Array<[number, number]>) =>\n seg.map(([i, v], k) => `${k === 0 ? \"M\" : \"L\"}${r(x(i))} ${r(y(v))}`).join(\" \");\n const dotPath = (i: number, v: number) => `M${r(x(i))} ${r(y(v))} h0`;\n // The area closes to zero when zero is on the scale, else to the bottom edge.\n const areaBase = y(lo <= 0 && hi >= 0 ? 0 : lo);\n\n const STROKE = {\n fill: \"none\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n vectorEffect: \"non-scaling-stroke\",\n } as const;\n\n return (\n <svg\n role=\"img\"\n aria-label={name}\n {...rest}\n // The drawing stays in its own units (the viewBox); only the box it is shown in\n // grows, uniformly, so nothing is distorted and the strokes keep their px width.\n width={fluid ? \"100%\" : W * scale}\n height={H * scale}\n viewBox={`0 0 ${W} ${H}`}\n preserveAspectRatio=\"none\"\n className={cn(fluid ? \"block w-full\" : \"inline-block shrink-0 align-middle\", \"overflow-visible\", className)}\n style={style}\n >\n {referenceValue !== undefined && isValue(referenceValue) && referenceValue >= lo && referenceValue <= hi && (\n <path\n d={`M0 ${r(y(referenceValue))} H${W}`}\n {...STROKE}\n strokeWidth={1}\n strokeDasharray=\"2 2\"\n className=\"text-[var(--border-strong)]\"\n />\n )}\n\n {variant === \"bar\"\n ? (() => {\n const slot = W / Math.max(n, 1);\n const bw = Math.max(1, slot * 0.7);\n const base = y(0);\n return points.map((v, i) => {\n if (v === null) return null;\n const top = Math.min(y(v), base);\n return (\n <rect\n key={i}\n x={r(i * slot + (slot - bw) / 2)}\n y={r(top)}\n width={r(bw)}\n height={r(Math.abs(y(v) - base))}\n fill=\"currentColor\"\n opacity={highlightLast && i !== lastIndex ? 0.55 : 1}\n className={tone === \"signed\" ? signedClass(v) : TONE_CLASS[tone]}\n />\n );\n });\n })()\n : runs.map((seg, k) =>\n seg.length === 1 ? (\n <path\n key={k}\n d={dotPath(seg[0][0], seg[0][1])}\n {...STROKE}\n strokeWidth={strokeWidth * 2}\n className={lineClass}\n />\n ) : (\n <g key={k} className={lineClass}>\n {variant === \"area\" && (\n <path\n d={`${linePath(seg)} L${r(x(seg[seg.length - 1][0]))} ${r(areaBase)} L${r(x(seg[0][0]))} ${r(areaBase)} Z`}\n fill=\"currentColor\"\n fillOpacity={0.15}\n stroke=\"none\"\n />\n )}\n <path d={linePath(seg)} {...STROKE} strokeWidth={strokeWidth} />\n </g>\n ),\n )}\n\n {highlightLast && variant !== \"bar\" && lastIndex >= 0 && (\n <path\n d={dotPath(lastIndex, lastValue)}\n {...STROKE}\n strokeWidth={dot * 2 - 1}\n className={lineClass}\n data-sparkline-last=\"\"\n />\n )}\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -13,6 +13,7 @@ import { Sparkline } from "./sparkline.js";
|
|
|
13
13
|
import { Tooltip } from "./tooltip.js";
|
|
14
14
|
import { SKELETON_CLASS } from "./skeleton.js";
|
|
15
15
|
import { DEFAULT_STAT_TILE_LABELS } from "./stat-tile-labels.js";
|
|
16
|
+
import { formatWithMinus } from "../lib/minus.js";
|
|
16
17
|
const TONE_CLASS = {
|
|
17
18
|
neutral: "text-primary",
|
|
18
19
|
brand: "text-brand",
|
|
@@ -92,11 +93,17 @@ function StatTile({
|
|
|
92
93
|
const valueId = `${id}-value`;
|
|
93
94
|
const deltaId = `${id}-delta`;
|
|
94
95
|
const priv = sensitive ? "" : void 0;
|
|
95
|
-
const fmt = format ?? ((n) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const fmt = format ?? ((n) => (
|
|
97
|
+
// U+2212 for a negative headline (0.49, KIT-1).
|
|
98
|
+
formatWithMinus(
|
|
99
|
+
numberFormatter(kitLocale, {
|
|
100
|
+
...currency ? { style: "currency", currency } : null,
|
|
101
|
+
...compact ? { notation: "compact" } : null,
|
|
102
|
+
...numberFormat
|
|
103
|
+
}),
|
|
104
|
+
n
|
|
105
|
+
)
|
|
106
|
+
));
|
|
100
107
|
const show = (v) => typeof v === "number" ? fmt(v) : v;
|
|
101
108
|
const d = typeof delta === "number" ? { value: delta } : delta;
|
|
102
109
|
const hasValue = value !== null && value !== void 0 && value !== "";
|
|
@@ -246,10 +253,10 @@ function FigureRow({
|
|
|
246
253
|
{
|
|
247
254
|
className: cn(
|
|
248
255
|
"flex text-xs tabular-nums text-muted",
|
|
249
|
-
stacked ? "flex-col items-start" : "items-baseline gap-x-2"
|
|
256
|
+
stacked ? "flex-col items-start" : "flex-wrap items-baseline gap-x-2"
|
|
250
257
|
),
|
|
251
258
|
children: [
|
|
252
|
-
/* @__PURE__ */ jsx("dt", { className: "
|
|
259
|
+
/* @__PURE__ */ jsx("dt", { className: "max-w-full uppercase tracking-wide [overflow-wrap:break-word]", children: figure.label }),
|
|
253
260
|
/* @__PURE__ */ jsx(
|
|
254
261
|
"dd",
|
|
255
262
|
{
|