@eifi1/ui-kit 0.24.1 → 0.25.1

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.
Files changed (148) hide show
  1. package/README.md +6 -6
  2. package/dist/chart.d.ts +2 -2
  3. package/dist/components/account-chips.d.ts +1 -1
  4. package/dist/components/amount-input.d.ts +2 -2
  5. package/dist/components/button-group.d.ts +2 -2
  6. package/dist/components/calculator.d.ts +2 -2
  7. package/dist/components/column-mapper.d.ts +28 -9
  8. package/dist/components/column-mapper.js +63 -77
  9. package/dist/components/column-mapper.js.map +1 -1
  10. package/dist/components/confirm-dialog.d.ts +2 -2
  11. package/dist/components/copy-button.d.ts +2 -2
  12. package/dist/components/danger-confirm.d.ts +2 -2
  13. package/dist/components/data-table-cells.d.ts +1 -1
  14. package/dist/components/data-table-filter-popover.d.ts +1 -1
  15. package/dist/components/data-table-filters.d.ts +1 -1
  16. package/dist/components/data-table-labels.d.ts +1 -1
  17. package/dist/components/data-table-pagination.d.ts +1 -1
  18. package/dist/components/data-table-pagination.js +6 -1
  19. package/dist/components/data-table-pagination.js.map +1 -1
  20. package/dist/components/data-table.d.ts +1 -1
  21. package/dist/components/data-table.js +7 -1
  22. package/dist/components/data-table.js.map +1 -1
  23. package/dist/components/facing-pair.d.ts +2 -2
  24. package/dist/components/file-button.d.ts +2 -2
  25. package/dist/components/file-dropzone.d.ts +2 -2
  26. package/dist/components/form-actions.d.ts +2 -2
  27. package/dist/components/iban-input.d.ts +2 -2
  28. package/dist/components/language-select.d.ts +2 -2
  29. package/dist/components/money-field.d.ts +2 -2
  30. package/dist/components/number-field.d.ts +2 -2
  31. package/dist/components/number-input.d.ts +2 -2
  32. package/dist/components/numpad-sheet.d.ts +2 -2
  33. package/dist/components/phone-input.d.ts +2 -2
  34. package/dist/components/series-chart.d.ts +2 -2
  35. package/dist/components/settings-fields.d.ts +2 -2
  36. package/dist/components/share-card.d.ts +2 -2
  37. package/dist/components/swipeable-row.js +1 -1
  38. package/dist/components/swipeable-row.js.map +1 -1
  39. package/dist/components/table.d.ts +35 -1
  40. package/dist/components/table.js +4 -0
  41. package/dist/components/table.js.map +1 -1
  42. package/dist/components/text-link.d.ts +2 -2
  43. package/dist/components/time-input.d.ts +2 -2
  44. package/dist/components/tooltip.d.ts +103 -14
  45. package/dist/components/tooltip.js +194 -57
  46. package/dist/components/tooltip.js.map +1 -1
  47. package/dist/components/translation-review-editor.d.ts +9 -1
  48. package/dist/components/translation-review-editor.js +11 -1
  49. package/dist/components/translation-review-editor.js.map +1 -1
  50. package/dist/components/translation-review-labels.d.ts +10 -0
  51. package/dist/components/translation-review-labels.js +5 -1
  52. package/dist/components/translation-review-labels.js.map +1 -1
  53. package/dist/components/translation-review.d.ts +145 -8
  54. package/dist/components/translation-review.js +372 -91
  55. package/dist/components/translation-review.js.map +1 -1
  56. package/dist/components/ui.d.ts +2 -2
  57. package/dist/components/ui.js +55 -18
  58. package/dist/components/ui.js.map +1 -1
  59. package/dist/components/use-table-state.d.ts +1 -1
  60. package/dist/{data-table-labels-B7OdnM0S.d.ts → data-table-labels-Cdn1QS2k.d.ts} +21 -1
  61. package/dist/data-table.d.ts +1 -1
  62. package/dist/feedback/feedback-attachment.d.ts +2 -2
  63. package/dist/feedback/feedback-dialog.d.ts +2 -2
  64. package/dist/feedback/feedback-dialog.js +6 -1
  65. package/dist/feedback/feedback-dialog.js.map +1 -1
  66. package/dist/feedback/feedback-inbox.d.ts +2 -2
  67. package/dist/feedback/feedback-inbox.js +2 -0
  68. package/dist/feedback/feedback-inbox.js.map +1 -1
  69. package/dist/feedback/feedback-thread.d.ts +2 -2
  70. package/dist/{feedback-DOwPu-Il.d.ts → feedback-BXKH-ToU.d.ts} +32 -3
  71. package/dist/feedback.d.ts +2 -2
  72. package/dist/hooks/use-body-scroll-lock.js +16 -0
  73. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  74. package/dist/hooks/use-file-drop.d.ts +2 -2
  75. package/dist/hooks/use-search-param-state.d.ts +58 -3
  76. package/dist/hooks/use-search-param-state.js +107 -35
  77. package/dist/hooks/use-search-param-state.js.map +1 -1
  78. package/dist/i18n/defaults.d.ts +2 -2
  79. package/dist/i18n/german.d.ts +2 -2
  80. package/dist/i18n/german.js +8 -1
  81. package/dist/i18n/german.js.map +1 -1
  82. package/dist/i18n/kit-labels.d.ts +2 -2
  83. package/dist/i18n/languages.d.ts +2 -2
  84. package/dist/i18n/locales/de-CH.d.ts +2 -2
  85. package/dist/i18n/locales/en.d.ts +2 -2
  86. package/dist/i18n/locales/en.js +5 -1
  87. package/dist/i18n/locales/en.js.map +1 -1
  88. package/dist/i18n/locales/es.d.ts +2 -2
  89. package/dist/i18n/locales/es.js +7 -1
  90. package/dist/i18n/locales/es.js.map +1 -1
  91. package/dist/i18n/locales/fr.d.ts +2 -2
  92. package/dist/i18n/locales/fr.js +6 -1
  93. package/dist/i18n/locales/fr.js.map +1 -1
  94. package/dist/i18n/locales/hu.d.ts +2 -2
  95. package/dist/i18n/locales/hu.js +9 -1
  96. package/dist/i18n/locales/hu.js.map +1 -1
  97. package/dist/i18n/locales/it.d.ts +2 -2
  98. package/dist/i18n/locales/it.js +6 -1
  99. package/dist/i18n/locales/it.js.map +1 -1
  100. package/dist/i18n/locales/zh.d.ts +2 -2
  101. package/dist/i18n/locales/zh.js +6 -1
  102. package/dist/i18n/locales/zh.js.map +1 -1
  103. package/dist/i18n/review.d.ts +2 -2
  104. package/dist/i18n/review.js +11 -0
  105. package/dist/i18n/review.js.map +1 -1
  106. package/dist/index.d.ts +6 -6
  107. package/dist/index.js +9 -1
  108. package/dist/index.js.map +1 -1
  109. package/dist/lib/strip-fade.d.ts +84 -5
  110. package/dist/lib/strip-fade.js +109 -11
  111. package/dist/lib/strip-fade.js.map +1 -1
  112. package/dist/lib/translation-review.d.ts +45 -1
  113. package/dist/lib/translation-review.js +38 -1
  114. package/dist/lib/translation-review.js.map +1 -1
  115. package/dist/rhf/fields.d.ts +2 -2
  116. package/dist/rhf/form.d.ts +2 -2
  117. package/dist/rhf.d.ts +2 -2
  118. package/dist/shell/app-shell.d.ts +2 -2
  119. package/dist/shell/top-bar-brand.d.ts +2 -2
  120. package/dist/shell.d.ts +2 -2
  121. package/dist/wizard/stepper-nav.d.ts +2 -2
  122. package/dist/wizard.d.ts +2 -2
  123. package/package.json +1 -1
  124. package/src/components/column-mapper.tsx +120 -113
  125. package/src/components/data-table-pagination.tsx +10 -1
  126. package/src/components/data-table.tsx +34 -1
  127. package/src/components/swipeable-row.tsx +1 -1
  128. package/src/components/table.tsx +39 -0
  129. package/src/components/tooltip.tsx +487 -104
  130. package/src/components/translation-review-editor.tsx +21 -1
  131. package/src/components/translation-review-labels.ts +17 -0
  132. package/src/components/translation-review.tsx +657 -109
  133. package/src/components/ui.tsx +104 -4
  134. package/src/feedback/feedback-dialog.tsx +21 -0
  135. package/src/feedback/feedback-inbox.tsx +19 -2
  136. package/src/hooks/use-body-scroll-lock.ts +25 -0
  137. package/src/hooks/use-search-param-state.ts +283 -41
  138. package/src/i18n/german.ts +10 -0
  139. package/src/i18n/locales/en.ts +7 -0
  140. package/src/i18n/locales/es.ts +9 -0
  141. package/src/i18n/locales/fr.ts +9 -0
  142. package/src/i18n/locales/hu.ts +11 -0
  143. package/src/i18n/locales/it.ts +9 -0
  144. package/src/i18n/locales/zh.ts +8 -0
  145. package/src/i18n/review.ts +11 -0
  146. package/src/index.ts +12 -0
  147. package/src/lib/strip-fade.ts +220 -15
  148. package/src/lib/translation-review.ts +85 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @eifi1/ui-kit — shared design-system barrel.\n//\n// Domain-free UI + theme surface shared across apps (lead app: Keksdose). Consumers import\n// components/theme from \"@eifi1/ui-kit\" and the token stylesheet from \"@eifi1/ui-kit/tokens.css\".\n\n// ── lib ────────────────────────────────────────────────────────────────────\nexport * from \"./lib/calc\";\nexport { cn } from \"./lib/cn\";\nexport { logger, setStoreLog } from \"./lib/logger\";\n// Intl number / money / percent / date / relative-time formatting, and `useKitFormat()`\n// for the same bound to the provider's locale (kastlan's formatters.ts, English \"5m ago\").\nexport * from \"./lib/format\";\n// localStorage that never throws (Safari private mode, blocked site data).\nexport { readStored, writeStored } from \"./lib/safe-storage\";\n// Date helpers are also available via the \"@eifi1/ui-kit/dates\" subpath.\n\n// ── hooks ────────────────────────────────────────────────────────────────────\nexport { useMediaQuery } from \"./hooks/use-media-query\";\nexport { useBodyScrollLock } from \"./hooks/use-body-scroll-lock\";\nexport { useAnchoredRect } from \"./hooks/use-anchored-rect\";\nexport type { AnchorRect } from \"./hooks/use-anchored-rect\";\nexport { useAnchoredPanel, anchoredPanelPlacement, useVisualViewport } from \"./hooks/use-anchored-panel\";\nexport type {\n AnchoredPanel,\n AnchoredPanelOptions,\n ViewportBox,\n} from \"./hooks/use-anchored-panel\";\nexport { useEscapeKey, useOutsideClick } from \"./hooks/use-dismiss\";\n// Focus containment for an overlay, and a live region for a change that moves no focus.\n// Both were extracted from the two components that already did them correctly (Modal and\n// CommandPalette) so the rest of the kit stops being the exception.\nexport { useFocusTrap } from \"./hooks/use-focus-trap\";\nexport type { FocusTrapOptions } from \"./hooks/use-focus-trap\";\nexport { useAnnounce } from \"./hooks/use-announce\";\nexport type { UseAnnounceReturn, UseAnnounceOptions, AnnounceRegionProps } from \"./hooks/use-announce\";\nexport { useOverlayHistory } from \"./hooks/use-overlay-history\";\nexport { useCloseTransition, OVERLAY_EXIT_MS } from \"./hooks/use-close-transition\";\nexport { useRowSwipe } from \"./hooks/use-row-swipe\";\nexport type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from \"./hooks/use-row-swipe\";\n// Any element as a file drop target, screened like FileButton (kastlan).\nexport { useFileDrop, dragHasFiles } from \"./hooks/use-file-drop\";\nexport type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from \"./hooks/use-file-drop\";\n// kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.\nexport { useDebounce, useDebouncedCallback } from \"./hooks/use-debounce\";\nexport type { DebouncedCallbackOptions, DebouncedFunction } from \"./hooks/use-debounce\";\n// One search param as state, the active tab in `?tab=`, and a dialog's open state in the\n// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.\nexport { useSearchParamState, useTabParam, useDialogParam } from \"./hooks/use-search-param-state\";\nexport type { SearchParamStateOptions, DialogParam } from \"./hooks/use-search-param-state\";\n// Copy that reports whether it worked — keksdose's copy buttons said \"Copied\" when it had not.\nexport { useCopyToClipboard, copyToClipboard } from \"./hooks/use-copy-to-clipboard\";\nexport type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from \"./hooks/use-copy-to-clipboard\";\n// Keyboard shortcuts (\"Mod+K\", \"Ctrl+Shift+F\") that leave typing in fields alone.\nexport { useHotkey, parseHotkey, matchesHotkey } from \"./hooks/use-hotkey\";\nexport type { Hotkey, ParsedHotkey, UseHotkeyOptions } from \"./hooks/use-hotkey\";\n// A protected file as an object URL, fetched with the app's own auth (kastlan's AuthedImage).\nexport { useAuthedSrc } from \"./hooks/use-authed-src\";\nexport type { AuthedFetcher, AuthedSrcStatus, UseAuthedSrcOptions, UseAuthedSrcResult } from \"./hooks/use-authed-src\";\n\n// ── theme / palettes ─────────────────────────────────────────────────────────\nexport * from \"./theme/chart-palette\";\n// Colour maths (sRGB ↔ OKLCH, WCAG contrast, CVD simulation) and the palette deriver\n// that builds a whole, contrast-solved palette from one brand colour plus any colours\n// pinned by hand. `auditPalette` / `auditChartRamp` measure a palette rather than\n// trusting a comment about it.\nexport * from \"./theme/color\";\nexport * from \"./theme/palette-derive\";\nexport * from \"./theme/palette-presets\";\nexport * from \"./theme/theme-store\";\nexport * from \"./theme/palette-store\";\n\n// ── components ───────────────────────────────────────────────────────────────\nexport * from \"./components/ui\";\n// The label-ABOVE field group (label, hint, error, required), inside wizards or out.\nexport * from \"./components/field\";\nexport * from \"./components/search-field\";\nexport * from \"./components/dropdown\";\nexport * from \"./components/popover\";\nexport { SwipeableRow } from \"./components/swipeable-row\";\nexport type { SwipeAction, SwipeableRowProps } from \"./components/swipeable-row\";\nexport * from \"./components/calculator\";\nexport * from \"./components/numpad-sheet\";\nexport * from \"./components/number-input\";\nexport * from \"./components/currency-select\";\nexport * from \"./components/amount-input\";\nexport * from \"./components/money-field\";\nexport * from \"./components/combobox\";\nexport * from \"./components/picker-sheet\";\nexport * from \"./components/entity-combobox\";\nexport * from \"./components/multi-entity-combobox\";\nexport * from \"./components/multi-select\";\nexport * from \"./components/tooltip\";\nexport * from \"./components/user-avatar\";\nexport * from \"./components/settings-fields\";\n// Sync state for a database-backed field: the engine (`useFieldSync`) and the\n// affordance (`FieldSyncIndicator`), kept separate because the state is useful\n// without the dot — a form can gate its submit on any field being `pending`.\nexport * from \"./components/field-sync\";\nexport * from \"./components/month-picker\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/switch\";\nexport * from \"./components/slider\";\nexport * from \"./components/time-input\";\nexport * from \"./components/number-field\";\nexport * from \"./components/sparkline\";\nexport * from \"./components/stat-tile\";\nexport * from \"./components/signature-pad\";\nexport * from \"./components/password-strength\";\nexport * from \"./components/page-contents\";\nexport * from \"./components/disclosure\";\nexport * from \"./components/dialog-frame\";\n// Named, not `*`: danger-confirm also holds the password field and pending hook that\n// ReauthDialog shares with it, which are kit-internal.\nexport {\n DangerConfirm,\n DEFAULT_DANGER_CONFIRM_LABELS,\n typedMatches,\n // 0.22.0: the typed-confirmation field on its own (keksdose K7) and the current-password\n // field kastlan wants for sign-in and re-authentication (kastlan 1).\n TypedConfirmField,\n CurrentPasswordInput,\n} from \"./components/danger-confirm\";\nexport type {\n DangerConfirmLabels,\n DangerConfirmProps,\n TypedMatch,\n DangerConsequence,\n TypedConfirmFieldProps,\n TypedConfirmFieldLabels,\n CurrentPasswordInputProps,\n CurrentPasswordInputLabels,\n // 0.23.0: onConfirm's second argument — the guards' values (keksdose G4b).\n DangerConfirmValues,\n} from \"./components/danger-confirm\";\n// 0.18: re-authentication before a sensitive action (Kurvenschmiede 4).\nexport { ReauthDialog, DEFAULT_REAUTH_DIALOG_LABELS } from \"./components/reauth-dialog\";\nexport type { ReauthDialogProps, ReauthDialogLabels } from \"./components/reauth-dialog\";\n// `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.\nexport * from \"./components/confirm-dialog\";\n// A non-modal corner panel and its round trigger (keksdose's assistant launcher).\nexport * from \"./components/floating-panel\";\n// The count / clear / actions bar a selection brings up (keksdose's three copies).\nexport * from \"./components/bulk-action-bar\";\nexport * from \"./components/swatch-picker\";\nexport * from \"./components/icon-picker\";\nexport * from \"./components/choice-card\";\nexport * from \"./components/autocomplete\";\nexport * from \"./components/measured-grid\";\nexport { useWindowedRows } from \"./hooks/use-windowed-rows\";\nexport type { WindowedRows } from \"./hooks/use-windowed-rows\";\n// Named, not `export *`: file-button.tsx also holds the screening helpers the\n// dropzone shares, which are internal.\nexport { FileButton, useFilePicker, matchesAccept, DEFAULT_FILE_PICKER_LABELS } from \"./components/file-button\";\nexport type {\n FileButtonProps,\n UseFilePickerOptions,\n UseFilePickerReturn,\n FilePickerLabels,\n FileRejection,\n FileRejectionReason,\n FileScreenOptions,\n} from \"./components/file-button\";\nexport * from \"./components/treemap\";\nexport * from \"./components/series-chart\";\nexport * from \"./components/chart-zoom\";\nexport * from \"./components/toggle-legend\";\nexport * from \"./components/facing-pair\";\nexport * from \"./components/series-chart-labels\";\nexport * from \"./components/pie-chart\";\nexport * from \"./components/pie-chart-labels\";\nexport * from \"./components/account-settings\";\n// 0.12.0: the fourth account section, and the QR code TwoFactorSetting draws its\n// otpauth URI with (encoder in lib/qr-encode.ts — no dependency, see there).\nexport * from \"./components/passkeys-setting\";\nexport * from \"./components/qr-code\";\nexport { encodeQr } from \"./lib/qr-encode\";\nexport type { QrEncodeOptions, QrErrorCorrection, QrMatrix } from \"./lib/qr-encode\";\nexport * from \"./components/alert-banner\";\nexport * from \"./components/toggle-group\";\n// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.\nexport * from \"./components/nav-pills\";\n// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is\n// given — and the list field built from it. Distinct from Button on purpose: a row of\n// buttons reads as \"choose an action\", a row of chips as \"here are the things\".\nexport * from \"./components/chip\";\nexport * from \"./components/wizard-stepper\";\nexport * from \"./components/hover-menu\";\nexport * from \"./components/modal\";\nexport * from \"./components/full-bleed-dialog\";\nexport * from \"./components/grouped-picker\";\nexport * from \"./components/file-dropzone\";\nexport * from \"./components/mini-calendar\";\nexport * from \"./components/calendar-heatmap\";\nexport * from \"./components/date-picker\";\nexport * from \"./components/tree-view\";\nexport * from \"./components/chart\";\n// 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.\nexport * from \"./components/description-list\";\n// The Save / Cancel row forms and dialogs end in, and the editable line-item repeater\n// (journal lines, lease components, invoice positions). 0.12.0.\nexport * from \"./components/form-actions\";\n// 0.18: lock every opted-in commit under one provider, the reason in its tooltip\n// (Kurvenschmiede 2). `useCommitReason` stays internal.\nexport { WriteLockProvider, useWriteLock, DEFAULT_WRITE_LOCK_LABELS } from \"./components/write-lock\";\nexport type { WriteLock, WriteLockLabels, WriteLockProviderProps } from \"./components/write-lock\";\n// 0.18: the parts both apps' admin rosters repeat (Kurvenschmiede 6).\nexport { RoleChip, AccountStateChip, DateMark, dateColumn, DEFAULT_ACCOUNT_STATE_LABELS, ACCOUNT_STATE_TONES } from \"./components/account-chips\";\nexport type { RoleChipProps, RoleDefinition, RoleVocabulary, AccountState, AccountStateLabels, AccountStateChipProps, DateMarkProps, DateColumnOptions } from \"./components/account-chips\";\n// 0.18: the cold-start notice keksdose built (#199), for every app: a framework-free\n// watchdog over the app's requests and the corner notice that explains the wait.\nexport { createServerWake, serverWake, watchReadsAnd, attachServerWake, wrapFetch } from \"./lib/server-wake\";\nexport type { ServerWakeStage, ServerWakeFilter, ServerWakeOptions, ServerWakeRequest, ServerWakeWatcher, ServerWakeAxiosConfig, AxiosLikeInstance } from \"./lib/server-wake\";\nexport { ServerWakeNotice, useServerWakeStage, DEFAULT_SERVER_WAKE_LABELS } from \"./components/server-wake\";\nexport type { ServerWakeLabels, ServerWakeNoticeProps } from \"./components/server-wake\";\n// 0.18: sharing — grantees, roles, pending grants, candidates (Kurvenschmiede 1).\nexport { ShareCard, ShareDialog, SharePanel, DEFAULT_SHARE_CARD_LABELS } from \"./components/share-card\";\nexport type { ShareCardProps, ShareDialogProps, SharePanelProps, ShareCardLabels, ShareRole, ShareGrantee, SharePendingGrant, ShareCandidate, ShareAddRequest } from \"./components/share-card\";\nexport * from \"./components/line-items\";\nexport * from \"./components/progress-bar\";\n// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.\nexport { Skeleton } from \"./components/skeleton\";\nexport type { SkeletonProps, SkeletonShape } from \"./components/skeleton\";\n// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.\nexport * from \"./components/loading-state\";\nexport * from \"./components/signed-amount\";\nexport * from \"./components/error-boundary\";\nexport * from \"./components/copy-button\";\nexport { AuthedImage, DEFAULT_AUTHED_IMAGE_LABELS } from \"./components/authed-image\";\nexport type { AuthedImageProps, AuthedImageLabels } from \"./components/authed-image\";\nexport * from \"./components/image-grid\";\nexport * from \"./components/lightbox\";\nexport * from \"./components/button-group\";\nexport * from \"./components/table\";\nexport * from \"./components/separator\";\n// Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.\nexport { ScrollArea } from \"./components/scroll-area\";\nexport type { ScrollAreaProps } from \"./components/scroll-area\";\n// 0.10.0 rows, menu rows, text primitives and page chrome the apps drew by hand.\nexport * from \"./components/list\";\nexport * from \"./components/menu-item\";\nexport * from \"./components/text\";\nexport * from \"./components/status-dot\";\nexport * from \"./components/page-header\";\nexport * from \"./components/breadcrumbs\";\n// The one inline link (kastlan's EntityLink / CellLink / legal and source links), through\n// `<UiKitProvider linkComponent>`. Named: the file also holds the kit's internal link picker.\nexport { TextLink } from \"./components/text-link\";\nexport type {\n TextLinkProps,\n TextLinkRenderProps,\n TextLinkTone,\n TextLinkUnderline,\n TextLinkCurrent,\n} from \"./components/text-link\";\n// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's\n// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,\n// tones and z-index both apps had wired by hand.\nexport { toast, Toaster, DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION } from \"./components/toast\";\nexport type {\n ToastId,\n ToastAction,\n ToastOptions,\n ToastPromiseOptions,\n ToastUndoOptions,\n ToastRedoOptions,\n ToastLabels,\n ToastPosition,\n ToastSwipeDirection,\n ToasterOffset,\n ToasterProps,\n} from \"./components/toast\";\n\n// ── data-table suite ─────────────────────────────────────────────────────────\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// data-table re-exports SortState internally; export the rest explicitly to\n// avoid a duplicate SortState star-export (it comes from data-table-sort).\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n} from \"./components/data-table\";\n// 0.12.0: the yes/no cell, and a server table's state held by its owner, URL-synced.\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n\n// ── shell (composable app chrome) ────────────────────────────────────────────\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n\n// ── feedback ─────────────────────────────────────────────────────────────────\n// The form somebody files a report with, and the parts an inbox is built from:\n// the status vocabulary both apps share value for value, the policy about what a\n// row may move to from where it stands, and the look of a status control, a\n// category badge and an opened report. The app still wires its own API, columns\n// and strings — see the note at the top of `feedback-inbox.tsx` for what is\n// deliberately left to it.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n\n// ── multi-step wizard ────────────────────────────────────────────────────────\n// The engine (step machine, validators, collected data), the chrome around it\n// (indicator + nav bar + cancel confirm) and the review step it ends on.\n// `components/wizard-stepper` next door is a different, much smaller thing: a bare\n// step indicator for a two-step import flow, with no engine.\n//\n// Two modules were removed here rather than kept as stock. `wizard-field` held a\n// `WizardField`/`WizardSelectField` pair that had never been rendered in any app —\n// a field group nothing was built from, which this comment used to advertise. And\n// `use-rhf-wizard-step` was the only thing in the package that touched\n// react-hook-form, for a hook neither consumer called: keeping it meant an optional\n// peer, a dev dependency and a type import in every consumer's typecheck, bought\n// for nobody. A design system may carry stock; untested stock nothing has ever\n// rendered is not stock, it is a liability with an export.\nexport * from \"./wizard/types\";\nexport * from \"./wizard/use-wizard\";\nexport * from \"./wizard/wizard-context\";\nexport * from \"./wizard/validation\";\nexport * from \"./wizard/wizard-step\";\nexport * from \"./wizard/wizard-summary\";\nexport * from \"./wizard/stepper-nav\";\n\n// ── guided tours ─────────────────────────────────────────────────────────────\nexport * from \"./tour/tour\";\n\n// ── command palette / global search ──────────────────────────────────────────\nexport * from \"./search/command-palette\";\nexport * from \"./search/search-index\";\nexport * from \"./search/global-search\";\n\n// ── i18n: one label tree, one optional provider ──────────────────────────────\n// `UiKitLabels` names every string the kit renders; `<UiKitProvider labels locale>`\n// hands a translation to every component below it. Precedence: a component's own\n// prop > the provider > the English default. See src/i18n/kit-labels.tsx.\nexport * from \"./i18n/kit-labels\";\nexport * from \"./i18n/defaults\";\n// The kit's words as `key → text` rows for an app's translation review (keksdose).\nexport { kitLabelStrings } from \"./i18n/review\";\n// The seven languages, how an app resolves one, and the kit catalogue loader for it.\nexport * from \"./i18n/languages\";\n// CLDR plural categories for an app's i18next catalogue, filled from `_other`.\nexport { withAllPlurals } from \"./i18n/plurals\";\n// Translation review (0.19): one contract for kastlan's and keksdose's review pages —\n// the pure rows/status/placeholder logic and the presentational page parts.\nexport {\n REVIEW_STATUSES,\n fromApiReview,\n toApiWrite,\n flattenStrings,\n keyNamespace,\n placeholderTokens,\n placeholderMismatch,\n reviewStatus,\n keyInArea,\n keyInAreas,\n translationRows,\n summariseRows,\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n filterTranslationRows,\n reviewWrite,\n mergeReviews,\n dropReviews,\n translationCorrections,\n} from \"./lib/translation-review\";\nexport type {\n TranslationVerdict,\n TranslationReview,\n TranslationReviewWrite,\n TranslationReviewKey,\n ReviewStatus,\n TranslationRow,\n ApiTranslationReview,\n ApiTranslationReviewWrite,\n TranslationRowsInput,\n TranslationSummary,\n TranslationReviewFilter,\n TranslationCorrection,\n} from \"./lib/translation-review\";\nexport {\n TranslationReviewPanel,\n TranslationReviewEditor,\n ReviewStatusChip,\n TranslationProgress,\n TranslationLocaleTabs,\n TranslationExportButton,\n REVIEW_STATUS_TONES,\n DEFAULT_TRANSLATION_REVIEW_LABELS,\n} from \"./components/translation-review\";\nexport type {\n TranslationReviewPanelProps,\n TranslationReviewEditorProps,\n ReviewStatusChipProps,\n TranslationProgressProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationExportButtonProps,\n TranslationReviewLabels,\n} from \"./components/translation-review\";\n// The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.\nexport { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from \"./components/legal\";\nexport type { LegalLabels, LegalLink, LegalLinksProps, LegalSectionProps, LegalLayoutProps } from \"./components/legal\";\n\n// ── 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits) ──\nexport * from \"./components/checkbox-group\";\nexport * from \"./components/one-time-code-input\";\nexport * from \"./components/language-select\";\nexport * from \"./components/tile-radio\";\nexport * from \"./components/inline-edit-field\";\nexport * from \"./components/sign-chip\";\nexport * from \"./components/country-select\";\nexport { COUNTRY_CODES, countryName } from \"./lib/countries\";\nexport * from \"./components/iban-input\";\nexport {\n IBAN_LENGTHS,\n compactIban,\n formatIban,\n ibanCheckDigits,\n ibanProblem,\n isValidIban,\n isQrIban,\n} from \"./lib/iban\";\nexport type { IbanKind, IbanProblem } from \"./lib/iban\";\nexport { formatIsin, isinCheckDigit, isValidIsin } from \"./lib/isin\";\nexport * from \"./components/phone-input\";\nexport {\n PHONE_DIAL_CODES,\n PHONE_COUNTRIES,\n parsePhone,\n formatNationalPhone,\n formatPhone,\n isE164,\n} from \"./lib/phone\";\nexport type { PhoneCountryCode, PhoneCountry, ParsedPhone } from \"./lib/phone\";\n\n// ── 0.23.0: the apps' 0.22 adoption round (kastlan, keksdose, Kurvenschmiede) ──\n// The label strip over custom content (keksdose G8).\nexport * from \"./components/field-strip\";\n// Paste or drop a table, give each column a role (Kurvenschmiede's columns input,\n// keksdose's import map step). The lexer itself stays in `@eifi1/ui-kit/table-text`.\nexport * from \"./components/column-mapper\";\nexport {\n assignColumnRole,\n guessMapping,\n missingRoles,\n readMappedTable,\n readTextFile,\n roleOfColumn,\n} from \"./lib/column-mapping\";\nexport type { ColumnMapperResult, ColumnMapping, ColumnRole } from \"./lib/column-mapping\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @eifi1/ui-kit — shared design-system barrel.\n//\n// Domain-free UI + theme surface shared across apps (lead app: Keksdose). Consumers import\n// components/theme from \"@eifi1/ui-kit\" and the token stylesheet from \"@eifi1/ui-kit/tokens.css\".\n\n// ── lib ────────────────────────────────────────────────────────────────────\nexport * from \"./lib/calc\";\nexport { cn } from \"./lib/cn\";\nexport { logger, setStoreLog } from \"./lib/logger\";\n// Intl number / money / percent / date / relative-time formatting, and `useKitFormat()`\n// for the same bound to the provider's locale (kastlan's formatters.ts, English \"5m ago\").\nexport * from \"./lib/format\";\n// localStorage that never throws (Safari private mode, blocked site data).\nexport { readStored, writeStored } from \"./lib/safe-storage\";\n// Date helpers are also available via the \"@eifi1/ui-kit/dates\" subpath.\n\n// ── hooks ────────────────────────────────────────────────────────────────────\nexport { useMediaQuery } from \"./hooks/use-media-query\";\nexport { useBodyScrollLock } from \"./hooks/use-body-scroll-lock\";\nexport { useAnchoredRect } from \"./hooks/use-anchored-rect\";\nexport type { AnchorRect } from \"./hooks/use-anchored-rect\";\nexport { useAnchoredPanel, anchoredPanelPlacement, useVisualViewport } from \"./hooks/use-anchored-panel\";\nexport type {\n AnchoredPanel,\n AnchoredPanelOptions,\n ViewportBox,\n} from \"./hooks/use-anchored-panel\";\nexport { useEscapeKey, useOutsideClick } from \"./hooks/use-dismiss\";\n// Focus containment for an overlay, and a live region for a change that moves no focus.\n// Both were extracted from the two components that already did them correctly (Modal and\n// CommandPalette) so the rest of the kit stops being the exception.\nexport { useFocusTrap } from \"./hooks/use-focus-trap\";\nexport type { FocusTrapOptions } from \"./hooks/use-focus-trap\";\nexport { useAnnounce } from \"./hooks/use-announce\";\nexport type { UseAnnounceReturn, UseAnnounceOptions, AnnounceRegionProps } from \"./hooks/use-announce\";\nexport { useOverlayHistory } from \"./hooks/use-overlay-history\";\nexport { useCloseTransition, OVERLAY_EXIT_MS } from \"./hooks/use-close-transition\";\nexport { useRowSwipe } from \"./hooks/use-row-swipe\";\nexport type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from \"./hooks/use-row-swipe\";\n// Any element as a file drop target, screened like FileButton (kastlan).\nexport { useFileDrop, dragHasFiles } from \"./hooks/use-file-drop\";\nexport type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from \"./hooks/use-file-drop\";\n// kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.\nexport { useDebounce, useDebouncedCallback } from \"./hooks/use-debounce\";\nexport type { DebouncedCallbackOptions, DebouncedFunction } from \"./hooks/use-debounce\";\n// One search param as state, the active tab in `?tab=`, and a dialog's open state in the\n// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.\nexport { useSearchParamState, useTabParam, useDialogParam } from \"./hooks/use-search-param-state\";\nexport type { SearchParamStateOptions, DialogParam } from \"./hooks/use-search-param-state\";\n// 0.25: several keys, one navigation (keksdose live #378 — four setters in a row clobbered).\nexport { useSearchParamsState } from \"./hooks/use-search-param-state\";\nexport type { SearchParamField, SearchParamFields, SearchParamsUpdate } from \"./hooks/use-search-param-state\";\n// Copy that reports whether it worked — keksdose's copy buttons said \"Copied\" when it had not.\nexport { useCopyToClipboard, copyToClipboard } from \"./hooks/use-copy-to-clipboard\";\nexport type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from \"./hooks/use-copy-to-clipboard\";\n// Keyboard shortcuts (\"Mod+K\", \"Ctrl+Shift+F\") that leave typing in fields alone.\nexport { useHotkey, parseHotkey, matchesHotkey } from \"./hooks/use-hotkey\";\nexport type { Hotkey, ParsedHotkey, UseHotkeyOptions } from \"./hooks/use-hotkey\";\n// A protected file as an object URL, fetched with the app's own auth (kastlan's AuthedImage).\nexport { useAuthedSrc } from \"./hooks/use-authed-src\";\nexport type { AuthedFetcher, AuthedSrcStatus, UseAuthedSrcOptions, UseAuthedSrcResult } from \"./hooks/use-authed-src\";\n\n// ── theme / palettes ─────────────────────────────────────────────────────────\nexport * from \"./theme/chart-palette\";\n// Colour maths (sRGB ↔ OKLCH, WCAG contrast, CVD simulation) and the palette deriver\n// that builds a whole, contrast-solved palette from one brand colour plus any colours\n// pinned by hand. `auditPalette` / `auditChartRamp` measure a palette rather than\n// trusting a comment about it.\nexport * from \"./theme/color\";\nexport * from \"./theme/palette-derive\";\nexport * from \"./theme/palette-presets\";\nexport * from \"./theme/theme-store\";\nexport * from \"./theme/palette-store\";\n\n// ── components ───────────────────────────────────────────────────────────────\nexport * from \"./components/ui\";\n// The label-ABOVE field group (label, hint, error, required), inside wizards or out.\nexport * from \"./components/field\";\nexport * from \"./components/search-field\";\nexport * from \"./components/dropdown\";\nexport * from \"./components/popover\";\nexport { SwipeableRow } from \"./components/swipeable-row\";\nexport type { SwipeAction, SwipeableRowProps } from \"./components/swipeable-row\";\nexport * from \"./components/calculator\";\nexport * from \"./components/numpad-sheet\";\nexport * from \"./components/number-input\";\nexport * from \"./components/currency-select\";\nexport * from \"./components/amount-input\";\nexport * from \"./components/money-field\";\nexport * from \"./components/combobox\";\nexport * from \"./components/picker-sheet\";\nexport * from \"./components/entity-combobox\";\nexport * from \"./components/multi-entity-combobox\";\nexport * from \"./components/multi-select\";\nexport * from \"./components/tooltip\";\nexport * from \"./components/user-avatar\";\nexport * from \"./components/settings-fields\";\n// Sync state for a database-backed field: the engine (`useFieldSync`) and the\n// affordance (`FieldSyncIndicator`), kept separate because the state is useful\n// without the dot — a form can gate its submit on any field being `pending`.\nexport * from \"./components/field-sync\";\nexport * from \"./components/month-picker\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/switch\";\nexport * from \"./components/slider\";\nexport * from \"./components/time-input\";\nexport * from \"./components/number-field\";\nexport * from \"./components/sparkline\";\nexport * from \"./components/stat-tile\";\nexport * from \"./components/signature-pad\";\nexport * from \"./components/password-strength\";\nexport * from \"./components/page-contents\";\nexport * from \"./components/disclosure\";\nexport * from \"./components/dialog-frame\";\n// Named, not `*`: danger-confirm also holds the password field and pending hook that\n// ReauthDialog shares with it, which are kit-internal.\nexport {\n DangerConfirm,\n DEFAULT_DANGER_CONFIRM_LABELS,\n typedMatches,\n // 0.22.0: the typed-confirmation field on its own (keksdose K7) and the current-password\n // field kastlan wants for sign-in and re-authentication (kastlan 1).\n TypedConfirmField,\n CurrentPasswordInput,\n} from \"./components/danger-confirm\";\nexport type {\n DangerConfirmLabels,\n DangerConfirmProps,\n TypedMatch,\n DangerConsequence,\n TypedConfirmFieldProps,\n TypedConfirmFieldLabels,\n CurrentPasswordInputProps,\n CurrentPasswordInputLabels,\n // 0.23.0: onConfirm's second argument — the guards' values (keksdose G4b).\n DangerConfirmValues,\n} from \"./components/danger-confirm\";\n// 0.18: re-authentication before a sensitive action (Kurvenschmiede 4).\nexport { ReauthDialog, DEFAULT_REAUTH_DIALOG_LABELS } from \"./components/reauth-dialog\";\nexport type { ReauthDialogProps, ReauthDialogLabels } from \"./components/reauth-dialog\";\n// `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.\nexport * from \"./components/confirm-dialog\";\n// A non-modal corner panel and its round trigger (keksdose's assistant launcher).\nexport * from \"./components/floating-panel\";\n// The count / clear / actions bar a selection brings up (keksdose's three copies).\nexport * from \"./components/bulk-action-bar\";\nexport * from \"./components/swatch-picker\";\nexport * from \"./components/icon-picker\";\nexport * from \"./components/choice-card\";\nexport * from \"./components/autocomplete\";\nexport * from \"./components/measured-grid\";\nexport { useWindowedRows } from \"./hooks/use-windowed-rows\";\nexport type { WindowedRows } from \"./hooks/use-windowed-rows\";\n// Named, not `export *`: file-button.tsx also holds the screening helpers the\n// dropzone shares, which are internal.\nexport { FileButton, useFilePicker, matchesAccept, DEFAULT_FILE_PICKER_LABELS } from \"./components/file-button\";\nexport type {\n FileButtonProps,\n UseFilePickerOptions,\n UseFilePickerReturn,\n FilePickerLabels,\n FileRejection,\n FileRejectionReason,\n FileScreenOptions,\n} from \"./components/file-button\";\nexport * from \"./components/treemap\";\nexport * from \"./components/series-chart\";\nexport * from \"./components/chart-zoom\";\nexport * from \"./components/toggle-legend\";\nexport * from \"./components/facing-pair\";\nexport * from \"./components/series-chart-labels\";\nexport * from \"./components/pie-chart\";\nexport * from \"./components/pie-chart-labels\";\nexport * from \"./components/account-settings\";\n// 0.12.0: the fourth account section, and the QR code TwoFactorSetting draws its\n// otpauth URI with (encoder in lib/qr-encode.ts — no dependency, see there).\nexport * from \"./components/passkeys-setting\";\nexport * from \"./components/qr-code\";\nexport { encodeQr } from \"./lib/qr-encode\";\nexport type { QrEncodeOptions, QrErrorCorrection, QrMatrix } from \"./lib/qr-encode\";\nexport * from \"./components/alert-banner\";\nexport * from \"./components/toggle-group\";\n// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.\nexport * from \"./components/nav-pills\";\n// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is\n// given — and the list field built from it. Distinct from Button on purpose: a row of\n// buttons reads as \"choose an action\", a row of chips as \"here are the things\".\nexport * from \"./components/chip\";\nexport * from \"./components/wizard-stepper\";\nexport * from \"./components/hover-menu\";\nexport * from \"./components/modal\";\nexport * from \"./components/full-bleed-dialog\";\nexport * from \"./components/grouped-picker\";\nexport * from \"./components/file-dropzone\";\nexport * from \"./components/mini-calendar\";\nexport * from \"./components/calendar-heatmap\";\nexport * from \"./components/date-picker\";\nexport * from \"./components/tree-view\";\nexport * from \"./components/chart\";\n// 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.\nexport * from \"./components/description-list\";\n// The Save / Cancel row forms and dialogs end in, and the editable line-item repeater\n// (journal lines, lease components, invoice positions). 0.12.0.\nexport * from \"./components/form-actions\";\n// 0.18: lock every opted-in commit under one provider, the reason in its tooltip\n// (Kurvenschmiede 2). `useCommitReason` stays internal.\nexport { WriteLockProvider, useWriteLock, DEFAULT_WRITE_LOCK_LABELS } from \"./components/write-lock\";\nexport type { WriteLock, WriteLockLabels, WriteLockProviderProps } from \"./components/write-lock\";\n// 0.18: the parts both apps' admin rosters repeat (Kurvenschmiede 6).\nexport { RoleChip, AccountStateChip, DateMark, dateColumn, DEFAULT_ACCOUNT_STATE_LABELS, ACCOUNT_STATE_TONES } from \"./components/account-chips\";\nexport type { RoleChipProps, RoleDefinition, RoleVocabulary, AccountState, AccountStateLabels, AccountStateChipProps, DateMarkProps, DateColumnOptions } from \"./components/account-chips\";\n// 0.18: the cold-start notice keksdose built (#199), for every app: a framework-free\n// watchdog over the app's requests and the corner notice that explains the wait.\nexport { createServerWake, serverWake, watchReadsAnd, attachServerWake, wrapFetch } from \"./lib/server-wake\";\nexport type { ServerWakeStage, ServerWakeFilter, ServerWakeOptions, ServerWakeRequest, ServerWakeWatcher, ServerWakeAxiosConfig, AxiosLikeInstance } from \"./lib/server-wake\";\nexport { ServerWakeNotice, useServerWakeStage, DEFAULT_SERVER_WAKE_LABELS } from \"./components/server-wake\";\nexport type { ServerWakeLabels, ServerWakeNoticeProps } from \"./components/server-wake\";\n// 0.18: sharing — grantees, roles, pending grants, candidates (Kurvenschmiede 1).\nexport { ShareCard, ShareDialog, SharePanel, DEFAULT_SHARE_CARD_LABELS } from \"./components/share-card\";\nexport type { ShareCardProps, ShareDialogProps, SharePanelProps, ShareCardLabels, ShareRole, ShareGrantee, SharePendingGrant, ShareCandidate, ShareAddRequest } from \"./components/share-card\";\nexport * from \"./components/line-items\";\nexport * from \"./components/progress-bar\";\n// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.\nexport { Skeleton } from \"./components/skeleton\";\nexport type { SkeletonProps, SkeletonShape } from \"./components/skeleton\";\n// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.\nexport * from \"./components/loading-state\";\nexport * from \"./components/signed-amount\";\nexport * from \"./components/error-boundary\";\nexport * from \"./components/copy-button\";\nexport { AuthedImage, DEFAULT_AUTHED_IMAGE_LABELS } from \"./components/authed-image\";\nexport type { AuthedImageProps, AuthedImageLabels } from \"./components/authed-image\";\nexport * from \"./components/image-grid\";\nexport * from \"./components/lightbox\";\nexport * from \"./components/button-group\";\nexport * from \"./components/table\";\nexport * from \"./components/separator\";\n// Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.\nexport { ScrollArea } from \"./components/scroll-area\";\nexport type { ScrollAreaProps } from \"./components/scroll-area\";\n// 0.10.0 rows, menu rows, text primitives and page chrome the apps drew by hand.\nexport * from \"./components/list\";\nexport * from \"./components/menu-item\";\nexport * from \"./components/text\";\nexport * from \"./components/status-dot\";\nexport * from \"./components/page-header\";\nexport * from \"./components/breadcrumbs\";\n// The one inline link (kastlan's EntityLink / CellLink / legal and source links), through\n// `<UiKitProvider linkComponent>`. Named: the file also holds the kit's internal link picker.\nexport { TextLink } from \"./components/text-link\";\nexport type {\n TextLinkProps,\n TextLinkRenderProps,\n TextLinkTone,\n TextLinkUnderline,\n TextLinkCurrent,\n} from \"./components/text-link\";\n// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's\n// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,\n// tones and z-index both apps had wired by hand.\nexport { toast, Toaster, DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION } from \"./components/toast\";\nexport type {\n ToastId,\n ToastAction,\n ToastOptions,\n ToastPromiseOptions,\n ToastUndoOptions,\n ToastRedoOptions,\n ToastLabels,\n ToastPosition,\n ToastSwipeDirection,\n ToasterOffset,\n ToasterProps,\n} from \"./components/toast\";\n\n// ── data-table suite ─────────────────────────────────────────────────────────\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// data-table re-exports SortState internally; export the rest explicitly to\n// avoid a duplicate SortState star-export (it comes from data-table-sort).\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n} from \"./components/data-table\";\n// 0.12.0: the yes/no cell, and a server table's state held by its owner, URL-synced.\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n\n// ── shell (composable app chrome) ────────────────────────────────────────────\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n\n// ── feedback ─────────────────────────────────────────────────────────────────\n// The form somebody files a report with, and the parts an inbox is built from:\n// the status vocabulary both apps share value for value, the policy about what a\n// row may move to from where it stands, and the look of a status control, a\n// category badge and an opened report. The app still wires its own API, columns\n// and strings — see the note at the top of `feedback-inbox.tsx` for what is\n// deliberately left to it.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n\n// ── multi-step wizard ────────────────────────────────────────────────────────\n// The engine (step machine, validators, collected data), the chrome around it\n// (indicator + nav bar + cancel confirm) and the review step it ends on.\n// `components/wizard-stepper` next door is a different, much smaller thing: a bare\n// step indicator for a two-step import flow, with no engine.\n//\n// Two modules were removed here rather than kept as stock. `wizard-field` held a\n// `WizardField`/`WizardSelectField` pair that had never been rendered in any app —\n// a field group nothing was built from, which this comment used to advertise. And\n// `use-rhf-wizard-step` was the only thing in the package that touched\n// react-hook-form, for a hook neither consumer called: keeping it meant an optional\n// peer, a dev dependency and a type import in every consumer's typecheck, bought\n// for nobody. A design system may carry stock; untested stock nothing has ever\n// rendered is not stock, it is a liability with an export.\nexport * from \"./wizard/types\";\nexport * from \"./wizard/use-wizard\";\nexport * from \"./wizard/wizard-context\";\nexport * from \"./wizard/validation\";\nexport * from \"./wizard/wizard-step\";\nexport * from \"./wizard/wizard-summary\";\nexport * from \"./wizard/stepper-nav\";\n\n// ── guided tours ─────────────────────────────────────────────────────────────\nexport * from \"./tour/tour\";\n\n// ── command palette / global search ──────────────────────────────────────────\nexport * from \"./search/command-palette\";\nexport * from \"./search/search-index\";\nexport * from \"./search/global-search\";\n\n// ── i18n: one label tree, one optional provider ──────────────────────────────\n// `UiKitLabels` names every string the kit renders; `<UiKitProvider labels locale>`\n// hands a translation to every component below it. Precedence: a component's own\n// prop > the provider > the English default. See src/i18n/kit-labels.tsx.\nexport * from \"./i18n/kit-labels\";\nexport * from \"./i18n/defaults\";\n// The kit's words as `key → text` rows for an app's translation review (keksdose).\nexport { kitLabelStrings } from \"./i18n/review\";\n// The seven languages, how an app resolves one, and the kit catalogue loader for it.\nexport * from \"./i18n/languages\";\n// CLDR plural categories for an app's i18next catalogue, filled from `_other`.\nexport { withAllPlurals } from \"./i18n/plurals\";\n// Translation review (0.19): one contract for kastlan's and keksdose's review pages —\n// the pure rows/status/placeholder logic and the presentational page parts.\nexport {\n REVIEW_STATUSES,\n fromApiReview,\n toApiWrite,\n flattenStrings,\n keyNamespace,\n placeholderTokens,\n placeholderMismatch,\n reviewStatus,\n keyInArea,\n keyInAreas,\n translationRows,\n summariseRows,\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n filterTranslationRows,\n reviewWrite,\n mergeReviews,\n dropReviews,\n translationCorrections,\n // 0.25: grouping and the approvals' Undo (keksdose live #377).\n groupTranslationRows,\n unreviewedRows,\n reviewUndo,\n} from \"./lib/translation-review\";\nexport type {\n TranslationVerdict,\n TranslationReview,\n TranslationReviewWrite,\n TranslationReviewKey,\n ReviewStatus,\n TranslationRow,\n ApiTranslationReview,\n ApiTranslationReviewWrite,\n TranslationRowsInput,\n TranslationSummary,\n TranslationReviewFilter,\n TranslationCorrection,\n TranslationRowGroup,\n TranslationReviewUndo,\n} from \"./lib/translation-review\";\nexport {\n TranslationReviewPanel,\n TranslationReviewEditor,\n ReviewStatusChip,\n TranslationProgress,\n TranslationLocaleTabs,\n TranslationExportButton,\n REVIEW_STATUS_TONES,\n DEFAULT_TRANSLATION_REVIEW_LABELS,\n} from \"./components/translation-review\";\nexport type {\n TranslationReviewPanelProps,\n TranslationReviewEditorProps,\n ReviewStatusChipProps,\n TranslationProgressProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationExportButtonProps,\n TranslationReviewLabels,\n TranslationReviewSaveInfo,\n TranslationReviewOrigin,\n TranslationReviewGroupBy,\n} from \"./components/translation-review\";\n// The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.\nexport { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from \"./components/legal\";\nexport type { LegalLabels, LegalLink, LegalLinksProps, LegalSectionProps, LegalLayoutProps } from \"./components/legal\";\n\n// ── 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits) ──\nexport * from \"./components/checkbox-group\";\nexport * from \"./components/one-time-code-input\";\nexport * from \"./components/language-select\";\nexport * from \"./components/tile-radio\";\nexport * from \"./components/inline-edit-field\";\nexport * from \"./components/sign-chip\";\nexport * from \"./components/country-select\";\nexport { COUNTRY_CODES, countryName } from \"./lib/countries\";\nexport * from \"./components/iban-input\";\nexport {\n IBAN_LENGTHS,\n compactIban,\n formatIban,\n ibanCheckDigits,\n ibanProblem,\n isValidIban,\n isQrIban,\n} from \"./lib/iban\";\nexport type { IbanKind, IbanProblem } from \"./lib/iban\";\nexport { formatIsin, isinCheckDigit, isValidIsin } from \"./lib/isin\";\nexport * from \"./components/phone-input\";\nexport {\n PHONE_DIAL_CODES,\n PHONE_COUNTRIES,\n parsePhone,\n formatNationalPhone,\n formatPhone,\n isE164,\n} from \"./lib/phone\";\nexport type { PhoneCountryCode, PhoneCountry, ParsedPhone } from \"./lib/phone\";\n\n// ── 0.23.0: the apps' 0.22 adoption round (kastlan, keksdose, Kurvenschmiede) ──\n// The label strip over custom content (keksdose G8).\nexport * from \"./components/field-strip\";\n// Paste or drop a table, give each column a role (Kurvenschmiede's columns input,\n// keksdose's import map step). The lexer itself stays in `@eifi1/ui-kit/table-text`.\nexport * from \"./components/column-mapper\";\nexport {\n assignColumnRole,\n guessMapping,\n missingRoles,\n readMappedTable,\n readTextFile,\n roleOfColumn,\n} from \"./lib/column-mapping\";\nexport type { ColumnMapperResult, ColumnMapping, ColumnRole } from \"./lib/column-mapping\";\n"],"mappings":"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","names":[]}
@@ -1,9 +1,53 @@
1
- import { RefObject } from 'react';
1
+ import { CSSProperties, FocusEvent, RefObject } from 'react';
2
2
 
3
3
  declare const TAB_FADE_PX = 24;
4
4
  /**
5
- * Shared by Tabs' strip, AppShell's scrolling phone sub-nav and (0.24) ColumnRoleTable's
6
- * preview; internal, not part of the package's surface.
5
+ * What {@link useStripFade} can be asked to do beyond the strip it was written for.
6
+ */
7
+ interface StripFadeOptions {
8
+ /**
9
+ * Keep the strip's own SCROLLBARS out of the fade (0.25, for the kit Table's and
10
+ * DataTable's `edgeFade`). A `mask-image` masks everything the element paints, and
11
+ * a classic scrollbar is painted by the element: on a desktop with always-visible
12
+ * scrollbars, a DataTable's vertical scrollbar sits at the inline end, inside the
13
+ * very band that fades whenever columns are hidden that way — it all but vanished,
14
+ * and the horizontal one lost its ends. With this set, the mask is opaque over
15
+ * each scrollbar (measured: `offsetWidth − clientWidth − borders`, its side read
16
+ * off `clientLeft`, which is where an RTL scrollbar on the left shows up) and the
17
+ * fade runs inside it, from the content's own edge. Where scrollbars take no room
18
+ * — phones, overlay scrollbars, `scrollbar-width: none` — nothing changes: the
19
+ * mask is the plain one. Off by default, so Tabs and AppShell paint exactly as
20
+ * they did.
21
+ */
22
+ spareScrollbars?: boolean;
23
+ }
24
+ /** The room the classic scrollbars of a strip take, and the borders beside them. */
25
+ interface StripScrollbars {
26
+ /** Vertical scrollbar on the left (RTL in Chromium and Firefox), in px. */
27
+ left: number;
28
+ /** Vertical scrollbar on the right, in px. */
29
+ right: number;
30
+ /** Horizontal scrollbar, in px. */
31
+ bottom: number;
32
+ /** The border widths outside each of them, in px — the fade still covers a border. */
33
+ borderLeft: number;
34
+ borderRight: number;
35
+ borderBottom: number;
36
+ }
37
+ /**
38
+ * The `mask-image` for a strip whose `left` and/or `right` (physical) edge has
39
+ * content behind it, or `undefined` when neither has. Pure, for {@link useStripFade}
40
+ * and its tests; `scrollbars` (see {@link StripFadeOptions.spareScrollbars}) keeps
41
+ * those bands opaque and starts the fade at the content's edge instead of the box's.
42
+ */
43
+ declare function stripFadeMask(edges: {
44
+ left: boolean;
45
+ right: boolean;
46
+ }, scrollbars?: StripScrollbars): string | undefined;
47
+ /**
48
+ * Shared by Tabs' strip, AppShell's scrolling phone sub-nav, (0.24) ColumnRoleTable's
49
+ * preview and (0.25) Table's and DataTable's `edgeFade`; internal, not part of the
50
+ * package's surface.
7
51
  *
8
52
  * Which ends of a sideways-scrolling strip have tabs cut off behind them, as the
9
53
  * `mask-image` that fades those ends out — or `undefined` while everything fits.
@@ -17,7 +61,7 @@ declare const TAB_FADE_PX = 24;
17
61
  * element, and `scrollLeft` is a magnitude here because an RTL strip counts it
18
62
  * negative from the start edge.
19
63
  */
20
- declare function useStripFade(ref: RefObject<HTMLElement | null>): {
64
+ declare function useStripFade(ref: RefObject<HTMLElement | null>, options?: StripFadeOptions): {
21
65
  mask: string | undefined;
22
66
  overflow: "start" | "end" | "both" | undefined;
23
67
  };
@@ -25,7 +69,42 @@ declare function useStripFade(ref: RefObject<HTMLElement | null>): {
25
69
  * Scroll `item` into view inside `strip` — the strip only (`scrollBy` on it, never
26
70
  * `scrollIntoView`, which would scroll the page too) — far enough to clear the fade.
27
71
  * A no-op when the strip fits or the item is already clear of both edges.
72
+ *
73
+ * The edges are the strip's CONTENT edges (0.25: `clientLeft` + `clientWidth`), not
74
+ * its box's: a vertical scrollbar or a side border takes room the item can never be
75
+ * scrolled into, and {@link StripFadeOptions.spareScrollbars} starts the fade inside
76
+ * the scrollbar. A Tabs strip has neither, so its edges are where they were.
28
77
  */
29
78
  declare function scrollIntoStrip(strip: HTMLElement, item: HTMLElement): void;
79
+ /**
80
+ * Focus landed on `focused` inside a faded table's scroller (0.25, Table and
81
+ * DataTable `edgeFade`): bring it clear of the fade. The browser's own focus scroll
82
+ * stops at the scroller's edge — under the fade, where a focused control is a few
83
+ * faint pixels. Its whole CELL is brought clear when the cell fits between the two
84
+ * fades (a select that fills its header, as ColumnRoleTable's do, then keeps the
85
+ * cell's padding round it too); a cell wider than that — a long note with a link at
86
+ * its end — would be brought in by its start and leave the link out, so then the
87
+ * focused element itself is what is scrolled clear.
88
+ */
89
+ declare function scrollFocusIntoStrip(strip: HTMLElement, focused: HTMLElement): void;
90
+ /** What {@link useEdgeFade} puts on the scroller; every field `undefined` while off. */
91
+ interface EdgeFadeProps {
92
+ "data-overflow": "start" | "end" | "both" | undefined;
93
+ style: CSSProperties | undefined;
94
+ onFocus: ((event: FocusEvent<HTMLElement>) => void) | undefined;
95
+ onBlur: ((event: FocusEvent<HTMLElement>) => void) | undefined;
96
+ }
97
+ /**
98
+ * Table's and DataTable's `edgeFade` on their scroller (0.25; internal, like the rest
99
+ * of this file): the measured fade with the scroller's own scrollbars spared, its
100
+ * `data-overflow`, focus inside brought clear of it ({@link scrollFocusIntoStrip}),
101
+ * and — on a scroller that is itself a tab stop, as Table's is while it overflows —
102
+ * the fade lifted while the scroller shows the keyboard's focus outline: an inset
103
+ * outline lies exactly in the faded band, and its sides would fade out with the
104
+ * columns. Inert while `on` is false: no measuring, no listeners, no attributes.
105
+ *
106
+ * `style` is the mask alone; a caller with a style of its own spreads it over that.
107
+ */
108
+ declare function useEdgeFade(scroller: RefObject<HTMLElement | null>, on: boolean): EdgeFadeProps;
30
109
 
31
- export { TAB_FADE_PX, scrollIntoStrip, useStripFade };
110
+ export { type EdgeFadeProps, type StripFadeOptions, type StripScrollbars, TAB_FADE_PX, scrollFocusIntoStrip, scrollIntoStrip, stripFadeMask, useEdgeFade, useStripFade };
@@ -1,21 +1,76 @@
1
1
  "use client";
2
- import { useLayoutEffect, useState } from "react";
2
+ import { useCallback, useLayoutEffect, useState } from "react";
3
3
  const TAB_FADE_PX = 24;
4
- function useStripFade(ref) {
5
- const [edges, setEdges] = useState({ left: false, right: false, rtl: false });
4
+ const NO_SCROLLBARS = {
5
+ left: 0,
6
+ right: 0,
7
+ bottom: 0,
8
+ borderLeft: 0,
9
+ borderRight: 0,
10
+ borderBottom: 0
11
+ };
12
+ const MIN_SCROLLBAR_PX = 2;
13
+ function measureScrollbars(el, style) {
14
+ const px = (value) => parseFloat(value) || 0;
15
+ const borderLeft = px(style.borderLeftWidth);
16
+ const borderRight = px(style.borderRightWidth);
17
+ const borderTop = px(style.borderTopWidth);
18
+ const borderBottom = px(style.borderBottomWidth);
19
+ const vertical = el.offsetWidth - el.clientWidth - borderLeft - borderRight;
20
+ const horizontal = el.offsetHeight - el.clientHeight - borderTop - borderBottom;
21
+ const onLeft = el.clientLeft - borderLeft >= MIN_SCROLLBAR_PX;
22
+ const v = vertical >= MIN_SCROLLBAR_PX ? Math.round(vertical) : 0;
23
+ return {
24
+ left: onLeft ? v : 0,
25
+ right: onLeft ? 0 : v,
26
+ bottom: horizontal >= MIN_SCROLLBAR_PX ? Math.round(horizontal) : 0,
27
+ borderLeft,
28
+ borderRight,
29
+ borderBottom
30
+ };
31
+ }
32
+ const sameScrollbars = (a, b) => a.left === b.left && a.right === b.right && a.bottom === b.bottom && a.borderLeft === b.borderLeft && a.borderRight === b.borderRight && a.borderBottom === b.borderBottom;
33
+ function stripFadeMask(edges, scrollbars = NO_SCROLLBARS) {
34
+ if (!edges.left && !edges.right) return void 0;
35
+ const F = TAB_FADE_PX;
36
+ const sb = scrollbars;
37
+ let l = edges.left ? `transparent, #000 ${F}px` : "#000";
38
+ let r = edges.right ? `#000 calc(100% - ${F}px), transparent` : "#000";
39
+ const fromRight = (px) => px === 0 ? "100%" : `calc(100% - ${px}px)`;
40
+ if (edges.left && sb.left > 0) {
41
+ const inner = sb.borderLeft + sb.left;
42
+ const border2 = sb.borderLeft > 0 ? `transparent ${sb.borderLeft}px, ` : "";
43
+ l = `${border2}#000 ${sb.borderLeft}px, #000 ${inner}px, transparent ${inner}px, #000 ${inner + F}px`;
44
+ }
45
+ if (edges.right && sb.right > 0) {
46
+ const inner = sb.borderRight + sb.right;
47
+ const border2 = sb.borderRight > 0 ? `, #000 ${fromRight(sb.borderRight)}, transparent ${fromRight(sb.borderRight)}` : "";
48
+ r = `#000 ${fromRight(inner + F)}, transparent ${fromRight(inner)}, #000 ${fromRight(inner)}${border2}`;
49
+ }
50
+ const across = `linear-gradient(to right, ${l}, ${r})`;
51
+ if (sb.bottom <= 0) return across;
52
+ const top = sb.borderBottom + sb.bottom;
53
+ const border = sb.borderBottom > 0 ? `transparent ${sb.borderBottom}px, ` : "";
54
+ return `${across}, linear-gradient(to top, ${border}#000 ${sb.borderBottom}px, #000 ${top}px, transparent ${top}px)`;
55
+ }
56
+ function useStripFade(ref, options) {
57
+ const spare = options?.spareScrollbars === true;
58
+ const [edges, setEdges] = useState({ left: false, right: false, rtl: false, scrollbars: NO_SCROLLBARS });
6
59
  useLayoutEffect(() => {
7
60
  const el = ref.current;
8
61
  if (!el) return;
9
62
  const measure = () => {
10
63
  const hidden = el.scrollWidth - el.clientWidth;
11
64
  const scrolled = Math.abs(el.scrollLeft);
12
- const rtl2 = getComputedStyle(el).direction === "rtl";
65
+ const style = getComputedStyle(el);
66
+ const rtl2 = style.direction === "rtl";
13
67
  const atStart = scrolled > 1;
14
68
  const atEnd = hidden - scrolled > 1;
15
69
  const left = rtl2 ? atEnd : atStart;
16
70
  const right = rtl2 ? atStart : atEnd;
71
+ const scrollbars = spare && (left || right) ? measureScrollbars(el, style) : NO_SCROLLBARS;
17
72
  setEdges(
18
- (prev) => prev.left === left && prev.right === right && prev.rtl === rtl2 ? prev : { left, right, rtl: rtl2 }
73
+ (prev) => prev.left === left && prev.right === right && prev.rtl === rtl2 && sameScrollbars(prev.scrollbars, scrollbars) ? prev : { left, right, rtl: rtl2, scrollbars }
19
74
  );
20
75
  };
21
76
  measure();
@@ -28,27 +83,70 @@ function useStripFade(ref) {
28
83
  observer?.disconnect();
29
84
  };
30
85
  });
31
- if (!edges.left && !edges.right) return { mask: void 0, overflow: void 0 };
32
- const l = edges.left ? `transparent, #000 ${TAB_FADE_PX}px` : "#000";
33
- const r = edges.right ? `#000 calc(100% - ${TAB_FADE_PX}px), transparent` : "#000";
86
+ const mask = stripFadeMask(edges, edges.scrollbars);
87
+ if (mask === void 0) return { mask: void 0, overflow: void 0 };
34
88
  const rtl = edges.rtl;
35
89
  const start = rtl ? edges.right : edges.left;
36
90
  const end = rtl ? edges.left : edges.right;
37
91
  return {
38
- mask: `linear-gradient(to right, ${l}, ${r})`,
92
+ mask,
39
93
  overflow: start && end ? "both" : start ? "start" : "end"
40
94
  };
41
95
  }
42
96
  function scrollIntoStrip(strip, item) {
43
97
  if (strip.scrollWidth <= strip.clientWidth) return;
44
- const s = strip.getBoundingClientRect();
98
+ const box = strip.getBoundingClientRect();
99
+ const left = box.left + strip.clientLeft;
100
+ const right = left + strip.clientWidth;
45
101
  const t = item.getBoundingClientRect();
46
- const delta = t.left < s.left + TAB_FADE_PX ? t.left - s.left - TAB_FADE_PX : t.right > s.right - TAB_FADE_PX ? t.right - s.right + TAB_FADE_PX : 0;
102
+ const delta = t.left < left + TAB_FADE_PX ? t.left - left - TAB_FADE_PX : t.right > right - TAB_FADE_PX ? t.right - right + TAB_FADE_PX : 0;
47
103
  if (delta !== 0) strip.scrollBy({ left: delta });
48
104
  }
105
+ function scrollFocusIntoStrip(strip, focused) {
106
+ if (focused === strip || !strip.contains(focused)) return;
107
+ const cell = focused.closest("td, th");
108
+ const rect = cell && strip.contains(cell) ? cell.getBoundingClientRect() : null;
109
+ const fits = rect !== null && rect.right - rect.left <= strip.clientWidth - 2 * TAB_FADE_PX;
110
+ scrollIntoStrip(strip, fits && cell ? cell : focused);
111
+ }
112
+ const NO_ELEMENT = { current: null };
113
+ const SPARE_SCROLLBARS = { spareScrollbars: true };
114
+ function focusVisible(el) {
115
+ try {
116
+ return el.matches(":focus-visible");
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ function useEdgeFade(scroller, on) {
122
+ const fade = useStripFade(on ? scroller : NO_ELEMENT, SPARE_SCROLLBARS);
123
+ const [selfFocused, setSelfFocused] = useState(false);
124
+ const onFocus = useCallback((event) => {
125
+ const box = event.currentTarget;
126
+ if (event.target === box) {
127
+ if (focusVisible(box)) setSelfFocused(true);
128
+ return;
129
+ }
130
+ if (event.target instanceof HTMLElement) scrollFocusIntoStrip(box, event.target);
131
+ }, []);
132
+ const onBlur = useCallback((event) => {
133
+ if (event.target === event.currentTarget) setSelfFocused(false);
134
+ }, []);
135
+ if (!on) return { "data-overflow": void 0, style: void 0, onFocus: void 0, onBlur: void 0 };
136
+ const mask = selfFocused ? void 0 : fade.mask;
137
+ return {
138
+ "data-overflow": fade.overflow,
139
+ style: mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0,
140
+ onFocus,
141
+ onBlur
142
+ };
143
+ }
49
144
  export {
50
145
  TAB_FADE_PX,
146
+ scrollFocusIntoStrip,
51
147
  scrollIntoStrip,
148
+ stripFadeMask,
149
+ useEdgeFade,
52
150
  useStripFade
53
151
  };
54
152
  //# sourceMappingURL=strip-fade.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/strip-fade.ts"],"sourcesContent":["import { useLayoutEffect, useState, type RefObject } from \"react\";\n\n// How far the fade at a scrolled strip's cut edge reaches, and how much room a tab\n// brought into view keeps from that edge so the fade never lies on top of it.\nexport const TAB_FADE_PX = 24;\n\n/**\n * Shared by Tabs' strip, AppShell's scrolling phone sub-nav and (0.24) ColumnRoleTable's\n * preview; internal, not part of the package's surface.\n *\n * Which ends of a sideways-scrolling strip have tabs cut off behind them, as the\n * `mask-image` that fades those ends out — or `undefined` while everything fits.\n *\n * A strip that overflowed used to stop dead at its edge: the last tab sliced through\n * mid-word, and nothing on a phone (whose scrollbars are overlays that only show\n * while you drag) said there was more to reach. A fade is the cue — the letters\n * thin out rather than end — and it is measured, not assumed, so a strip that fits\n * is painted exactly as before. Physical `left`/`right` in the gradient, because\n * `mask-image` has no logical directions; the reading direction is read from the\n * element, and `scrollLeft` is a magnitude here because an RTL strip counts it\n * negative from the start edge.\n */\nexport function useStripFade(\n ref: RefObject<HTMLElement | null>,\n): { mask: string | undefined; overflow: \"start\" | \"end\" | \"both\" | undefined } {\n const [edges, setEdges] = useState({ left: false, right: false, rtl: false });\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const measure = () => {\n const hidden = el.scrollWidth - el.clientWidth;\n const scrolled = Math.abs(el.scrollLeft);\n const rtl = getComputedStyle(el).direction === \"rtl\";\n const atStart = scrolled > 1;\n const atEnd = hidden - scrolled > 1;\n const left = rtl ? atEnd : atStart;\n const right = rtl ? atStart : atEnd;\n setEdges((prev) =>\n prev.left === left && prev.right === right && prev.rtl === rtl ? prev : { left, right, rtl },\n );\n };\n measure();\n el.addEventListener(\"scroll\", measure, { passive: true });\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(measure);\n observer?.observe(el);\n // The tabs, too: a label or a badge that grows changes the overflow without\n // resizing the strip's own box.\n for (const child of Array.from(el.children)) observer?.observe(child);\n return () => {\n el.removeEventListener(\"scroll\", measure);\n observer?.disconnect();\n };\n });\n if (!edges.left && !edges.right) return { mask: undefined, overflow: undefined };\n const l = edges.left ? `transparent, #000 ${TAB_FADE_PX}px` : \"#000\";\n const r = edges.right ? `#000 calc(100% - ${TAB_FADE_PX}px), transparent` : \"#000\";\n const rtl = edges.rtl;\n const start = rtl ? edges.right : edges.left;\n const end = rtl ? edges.left : edges.right;\n return {\n mask: `linear-gradient(to right, ${l}, ${r})`,\n overflow: start && end ? \"both\" : start ? \"start\" : \"end\",\n };\n}\n\n/**\n * Scroll `item` into view inside `strip` — the strip only (`scrollBy` on it, never\n * `scrollIntoView`, which would scroll the page too) — far enough to clear the fade.\n * A no-op when the strip fits or the item is already clear of both edges.\n */\nexport function scrollIntoStrip(strip: HTMLElement, item: HTMLElement): void {\n if (strip.scrollWidth <= strip.clientWidth) return;\n const s = strip.getBoundingClientRect();\n const t = item.getBoundingClientRect();\n const delta =\n t.left < s.left + TAB_FADE_PX\n ? t.left - s.left - TAB_FADE_PX\n : t.right > s.right - TAB_FADE_PX\n ? t.right - s.right + TAB_FADE_PX\n : 0;\n if (delta !== 0) strip.scrollBy({ left: delta });\n}\n"],"mappings":";AAAA,SAAS,iBAAiB,gBAAgC;AAInD,MAAM,cAAc;AAkBpB,SAAS,aACd,KAC8E;AAC9E,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE,MAAM,OAAO,OAAO,OAAO,KAAK,MAAM,CAAC;AAC5E,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,UAAU,MAAM;AACpB,YAAM,SAAS,GAAG,cAAc,GAAG;AACnC,YAAM,WAAW,KAAK,IAAI,GAAG,UAAU;AACvC,YAAMA,OAAM,iBAAiB,EAAE,EAAE,cAAc;AAC/C,YAAM,UAAU,WAAW;AAC3B,YAAM,QAAQ,SAAS,WAAW;AAClC,YAAM,OAAOA,OAAM,QAAQ;AAC3B,YAAM,QAAQA,OAAM,UAAU;AAC9B;AAAA,QAAS,CAAC,SACR,KAAK,SAAS,QAAQ,KAAK,UAAU,SAAS,KAAK,QAAQA,OAAM,OAAO,EAAE,MAAM,OAAO,KAAAA,KAAI;AAAA,MAC7F;AAAA,IACF;AACA,YAAQ;AACR,OAAG,iBAAiB,UAAU,SAAS,EAAE,SAAS,KAAK,CAAC;AACxD,UAAM,WAAW,OAAO,mBAAmB,cAAc,OAAO,IAAI,eAAe,OAAO;AAC1F,cAAU,QAAQ,EAAE;AAGpB,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,WAAU,QAAQ,KAAK;AACpE,WAAO,MAAM;AACX,SAAG,oBAAoB,UAAU,OAAO;AACxC,gBAAU,WAAW;AAAA,IACvB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,MAAO,QAAO,EAAE,MAAM,QAAW,UAAU,OAAU;AAC/E,QAAM,IAAI,MAAM,OAAO,qBAAqB,WAAW,OAAO;AAC9D,QAAM,IAAI,MAAM,QAAQ,oBAAoB,WAAW,qBAAqB;AAC5E,QAAM,MAAM,MAAM;AAClB,QAAM,QAAQ,MAAM,MAAM,QAAQ,MAAM;AACxC,QAAM,MAAM,MAAM,MAAM,OAAO,MAAM;AACrC,SAAO;AAAA,IACL,MAAM,6BAA6B,CAAC,KAAK,CAAC;AAAA,IAC1C,UAAU,SAAS,MAAM,SAAS,QAAQ,UAAU;AAAA,EACtD;AACF;AAOO,SAAS,gBAAgB,OAAoB,MAAyB;AAC3E,MAAI,MAAM,eAAe,MAAM,YAAa;AAC5C,QAAM,IAAI,MAAM,sBAAsB;AACtC,QAAM,IAAI,KAAK,sBAAsB;AACrC,QAAM,QACJ,EAAE,OAAO,EAAE,OAAO,cACd,EAAE,OAAO,EAAE,OAAO,cAClB,EAAE,QAAQ,EAAE,QAAQ,cAClB,EAAE,QAAQ,EAAE,QAAQ,cACpB;AACR,MAAI,UAAU,EAAG,OAAM,SAAS,EAAE,MAAM,MAAM,CAAC;AACjD;","names":["rtl"]}
1
+ {"version":3,"sources":["../../src/lib/strip-fade.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useState } from \"react\";\nimport type { CSSProperties, FocusEvent, RefObject } from \"react\";\n\n// How far the fade at a scrolled strip's cut edge reaches, and how much room a tab\n// brought into view keeps from that edge so the fade never lies on top of it.\nexport const TAB_FADE_PX = 24;\n\n/**\n * What {@link useStripFade} can be asked to do beyond the strip it was written for.\n */\nexport interface StripFadeOptions {\n /**\n * Keep the strip's own SCROLLBARS out of the fade (0.25, for the kit Table's and\n * DataTable's `edgeFade`). A `mask-image` masks everything the element paints, and\n * a classic scrollbar is painted by the element: on a desktop with always-visible\n * scrollbars, a DataTable's vertical scrollbar sits at the inline end, inside the\n * very band that fades whenever columns are hidden that way — it all but vanished,\n * and the horizontal one lost its ends. With this set, the mask is opaque over\n * each scrollbar (measured: `offsetWidth − clientWidth − borders`, its side read\n * off `clientLeft`, which is where an RTL scrollbar on the left shows up) and the\n * fade runs inside it, from the content's own edge. Where scrollbars take no room\n * — phones, overlay scrollbars, `scrollbar-width: none` — nothing changes: the\n * mask is the plain one. Off by default, so Tabs and AppShell paint exactly as\n * they did.\n */\n spareScrollbars?: boolean;\n}\n\n/** The room the classic scrollbars of a strip take, and the borders beside them. */\nexport interface StripScrollbars {\n /** Vertical scrollbar on the left (RTL in Chromium and Firefox), in px. */\n left: number;\n /** Vertical scrollbar on the right, in px. */\n right: number;\n /** Horizontal scrollbar, in px. */\n bottom: number;\n /** The border widths outside each of them, in px — the fade still covers a border. */\n borderLeft: number;\n borderRight: number;\n borderBottom: number;\n}\n\nconst NO_SCROLLBARS: StripScrollbars = {\n left: 0,\n right: 0,\n bottom: 0,\n borderLeft: 0,\n borderRight: 0,\n borderBottom: 0,\n};\n\n// A scrollbar is never this thin; anything under it is sub-pixel rounding between\n// `offsetWidth` and `clientWidth`, not a scrollbar.\nconst MIN_SCROLLBAR_PX = 2;\n\nfunction measureScrollbars(el: HTMLElement, style: CSSStyleDeclaration): StripScrollbars {\n const px = (value: string) => parseFloat(value) || 0;\n const borderLeft = px(style.borderLeftWidth);\n const borderRight = px(style.borderRightWidth);\n const borderTop = px(style.borderTopWidth);\n const borderBottom = px(style.borderBottomWidth);\n const vertical = el.offsetWidth - el.clientWidth - borderLeft - borderRight;\n const horizontal = el.offsetHeight - el.clientHeight - borderTop - borderBottom;\n const onLeft = el.clientLeft - borderLeft >= MIN_SCROLLBAR_PX;\n const v = vertical >= MIN_SCROLLBAR_PX ? Math.round(vertical) : 0;\n return {\n left: onLeft ? v : 0,\n right: onLeft ? 0 : v,\n bottom: horizontal >= MIN_SCROLLBAR_PX ? Math.round(horizontal) : 0,\n borderLeft,\n borderRight,\n borderBottom,\n };\n}\n\nconst sameScrollbars = (a: StripScrollbars, b: StripScrollbars) =>\n a.left === b.left &&\n a.right === b.right &&\n a.bottom === b.bottom &&\n a.borderLeft === b.borderLeft &&\n a.borderRight === b.borderRight &&\n a.borderBottom === b.borderBottom;\n\n/**\n * The `mask-image` for a strip whose `left` and/or `right` (physical) edge has\n * content behind it, or `undefined` when neither has. Pure, for {@link useStripFade}\n * and its tests; `scrollbars` (see {@link StripFadeOptions.spareScrollbars}) keeps\n * those bands opaque and starts the fade at the content's edge instead of the box's.\n */\nexport function stripFadeMask(\n edges: { left: boolean; right: boolean },\n scrollbars: StripScrollbars = NO_SCROLLBARS,\n): string | undefined {\n if (!edges.left && !edges.right) return undefined;\n const F = TAB_FADE_PX;\n const sb = scrollbars;\n // The plain mask, as Tabs and AppShell have always drawn it.\n let l = edges.left ? `transparent, #000 ${F}px` : \"#000\";\n let r = edges.right ? `#000 calc(100% - ${F}px), transparent` : \"#000\";\n const fromRight = (px: number) => (px === 0 ? \"100%\" : `calc(100% - ${px}px)`);\n // A vertical scrollbar on a FADED side: the border outside it fades with the rest\n // of that edge, the scrollbar itself stays whole, and the fade starts inside it.\n if (edges.left && sb.left > 0) {\n const inner = sb.borderLeft + sb.left;\n const border = sb.borderLeft > 0 ? `transparent ${sb.borderLeft}px, ` : \"\";\n l = `${border}#000 ${sb.borderLeft}px, #000 ${inner}px, transparent ${inner}px, #000 ${inner + F}px`;\n }\n if (edges.right && sb.right > 0) {\n const inner = sb.borderRight + sb.right;\n const border =\n sb.borderRight > 0 ? `, #000 ${fromRight(sb.borderRight)}, transparent ${fromRight(sb.borderRight)}` : \"\";\n r = `#000 ${fromRight(inner + F)}, transparent ${fromRight(inner)}, #000 ${fromRight(inner)}${border}`;\n }\n const across = `linear-gradient(to right, ${l}, ${r})`;\n if (sb.bottom <= 0) return across;\n // The horizontal scrollbar, as a second layer: layers ADD, so this band is opaque\n // over its whole width whatever the first layer says there.\n const top = sb.borderBottom + sb.bottom;\n const border = sb.borderBottom > 0 ? `transparent ${sb.borderBottom}px, ` : \"\";\n return `${across}, linear-gradient(to top, ${border}#000 ${sb.borderBottom}px, #000 ${top}px, transparent ${top}px)`;\n}\n\n/**\n * Shared by Tabs' strip, AppShell's scrolling phone sub-nav, (0.24) ColumnRoleTable's\n * preview and (0.25) Table's and DataTable's `edgeFade`; internal, not part of the\n * package's surface.\n *\n * Which ends of a sideways-scrolling strip have tabs cut off behind them, as the\n * `mask-image` that fades those ends out — or `undefined` while everything fits.\n *\n * A strip that overflowed used to stop dead at its edge: the last tab sliced through\n * mid-word, and nothing on a phone (whose scrollbars are overlays that only show\n * while you drag) said there was more to reach. A fade is the cue — the letters\n * thin out rather than end — and it is measured, not assumed, so a strip that fits\n * is painted exactly as before. Physical `left`/`right` in the gradient, because\n * `mask-image` has no logical directions; the reading direction is read from the\n * element, and `scrollLeft` is a magnitude here because an RTL strip counts it\n * negative from the start edge.\n */\nexport function useStripFade(\n ref: RefObject<HTMLElement | null>,\n options?: StripFadeOptions,\n): { mask: string | undefined; overflow: \"start\" | \"end\" | \"both\" | undefined } {\n const spare = options?.spareScrollbars === true;\n const [edges, setEdges] = useState({ left: false, right: false, rtl: false, scrollbars: NO_SCROLLBARS });\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const measure = () => {\n const hidden = el.scrollWidth - el.clientWidth;\n const scrolled = Math.abs(el.scrollLeft);\n const style = getComputedStyle(el);\n const rtl = style.direction === \"rtl\";\n const atStart = scrolled > 1;\n const atEnd = hidden - scrolled > 1;\n const left = rtl ? atEnd : atStart;\n const right = rtl ? atStart : atEnd;\n const scrollbars = spare && (left || right) ? measureScrollbars(el, style) : NO_SCROLLBARS;\n setEdges((prev) =>\n prev.left === left && prev.right === right && prev.rtl === rtl && sameScrollbars(prev.scrollbars, scrollbars)\n ? prev\n : { left, right, rtl, scrollbars },\n );\n };\n measure();\n el.addEventListener(\"scroll\", measure, { passive: true });\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(measure);\n observer?.observe(el);\n // The tabs, too: a label or a badge that grows changes the overflow without\n // resizing the strip's own box.\n for (const child of Array.from(el.children)) observer?.observe(child);\n return () => {\n el.removeEventListener(\"scroll\", measure);\n observer?.disconnect();\n };\n });\n const mask = stripFadeMask(edges, edges.scrollbars);\n if (mask === undefined) return { mask: undefined, overflow: undefined };\n const rtl = edges.rtl;\n const start = rtl ? edges.right : edges.left;\n const end = rtl ? edges.left : edges.right;\n return {\n mask,\n overflow: start && end ? \"both\" : start ? \"start\" : \"end\",\n };\n}\n\n/**\n * Scroll `item` into view inside `strip` — the strip only (`scrollBy` on it, never\n * `scrollIntoView`, which would scroll the page too) — far enough to clear the fade.\n * A no-op when the strip fits or the item is already clear of both edges.\n *\n * The edges are the strip's CONTENT edges (0.25: `clientLeft` + `clientWidth`), not\n * its box's: a vertical scrollbar or a side border takes room the item can never be\n * scrolled into, and {@link StripFadeOptions.spareScrollbars} starts the fade inside\n * the scrollbar. A Tabs strip has neither, so its edges are where they were.\n */\nexport function scrollIntoStrip(strip: HTMLElement, item: HTMLElement): void {\n if (strip.scrollWidth <= strip.clientWidth) return;\n const box = strip.getBoundingClientRect();\n const left = box.left + strip.clientLeft;\n const right = left + strip.clientWidth;\n const t = item.getBoundingClientRect();\n const delta =\n t.left < left + TAB_FADE_PX\n ? t.left - left - TAB_FADE_PX\n : t.right > right - TAB_FADE_PX\n ? t.right - right + TAB_FADE_PX\n : 0;\n if (delta !== 0) strip.scrollBy({ left: delta });\n}\n\n/**\n * Focus landed on `focused` inside a faded table's scroller (0.25, Table and\n * DataTable `edgeFade`): bring it clear of the fade. The browser's own focus scroll\n * stops at the scroller's edge — under the fade, where a focused control is a few\n * faint pixels. Its whole CELL is brought clear when the cell fits between the two\n * fades (a select that fills its header, as ColumnRoleTable's do, then keeps the\n * cell's padding round it too); a cell wider than that — a long note with a link at\n * its end — would be brought in by its start and leave the link out, so then the\n * focused element itself is what is scrolled clear.\n */\nexport function scrollFocusIntoStrip(strip: HTMLElement, focused: HTMLElement): void {\n if (focused === strip || !strip.contains(focused)) return;\n const cell = focused.closest<HTMLElement>(\"td, th\");\n const rect = cell && strip.contains(cell) ? cell.getBoundingClientRect() : null;\n const fits = rect !== null && rect.right - rect.left <= strip.clientWidth - 2 * TAB_FADE_PX;\n scrollIntoStrip(strip, fits && cell ? cell : focused);\n}\n\n/** `edgeFade` off: {@link useStripFade} gets no element and measures nothing. */\nconst NO_ELEMENT: RefObject<HTMLElement | null> = { current: null };\nconst SPARE_SCROLLBARS: StripFadeOptions = { spareScrollbars: true };\n\n/** `:focus-visible`, or `false` where the engine cannot say. */\nfunction focusVisible(el: Element): boolean {\n try {\n return el.matches(\":focus-visible\");\n } catch {\n return false;\n }\n}\n\n/** What {@link useEdgeFade} puts on the scroller; every field `undefined` while off. */\nexport interface EdgeFadeProps {\n \"data-overflow\": \"start\" | \"end\" | \"both\" | undefined;\n style: CSSProperties | undefined;\n onFocus: ((event: FocusEvent<HTMLElement>) => void) | undefined;\n onBlur: ((event: FocusEvent<HTMLElement>) => void) | undefined;\n}\n\n/**\n * Table's and DataTable's `edgeFade` on their scroller (0.25; internal, like the rest\n * of this file): the measured fade with the scroller's own scrollbars spared, its\n * `data-overflow`, focus inside brought clear of it ({@link scrollFocusIntoStrip}),\n * and — on a scroller that is itself a tab stop, as Table's is while it overflows —\n * the fade lifted while the scroller shows the keyboard's focus outline: an inset\n * outline lies exactly in the faded band, and its sides would fade out with the\n * columns. Inert while `on` is false: no measuring, no listeners, no attributes.\n *\n * `style` is the mask alone; a caller with a style of its own spreads it over that.\n */\nexport function useEdgeFade(scroller: RefObject<HTMLElement | null>, on: boolean): EdgeFadeProps {\n const fade = useStripFade(on ? scroller : NO_ELEMENT, SPARE_SCROLLBARS);\n const [selfFocused, setSelfFocused] = useState(false);\n const onFocus = useCallback((event: FocusEvent<HTMLElement>) => {\n const box = event.currentTarget;\n if (event.target === box) {\n // A click in the table's text focuses the scroller too (it is the nearest tab\n // stop); only the keyboard's focus draws an outline worth lifting the fade for.\n if (focusVisible(box)) setSelfFocused(true);\n return;\n }\n if (event.target instanceof HTMLElement) scrollFocusIntoStrip(box, event.target);\n }, []);\n const onBlur = useCallback((event: FocusEvent<HTMLElement>) => {\n if (event.target === event.currentTarget) setSelfFocused(false);\n }, []);\n if (!on) return { \"data-overflow\": undefined, style: undefined, onFocus: undefined, onBlur: undefined };\n const mask = selfFocused ? undefined : fade.mask;\n return {\n \"data-overflow\": fade.overflow,\n style: mask ? { maskImage: mask, WebkitMaskImage: mask } : undefined,\n onFocus,\n onBlur,\n };\n}\n"],"mappings":"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","names":["border","rtl"]}
@@ -199,9 +199,53 @@ interface TranslationReviewFilter {
199
199
  }
200
200
  declare const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter;
201
201
  declare function filterTranslationRows(rows: readonly TranslationRow[], filter: Partial<TranslationReviewFilter>): TranslationRow[];
202
+ /** One group of rows — an area, a source — and how they stand. */
203
+ interface TranslationRowGroup {
204
+ key: string;
205
+ rows: TranslationRow[];
206
+ summary: TranslationSummary;
207
+ }
208
+ /**
209
+ * Rows in groups, each group where its first row is — the reference's order, so the areas
210
+ * read the way the app's authors wrote the bundle — and each group's rows in their own
211
+ * order. keksdose live #377: a reviewer on a phone works area by area ("the budget
212
+ * strings, then the legal ones"), and a flat list of 3859 rows is not a way to see
213
+ * where an area stands.
214
+ *
215
+ * `groupOf` names a row's group: `(row) => row.namespace` for the areas,
216
+ * `(row) => row.source` for kastlan's screen texts and documents.
217
+ */
218
+ declare function groupTranslationRows(rows: readonly TranslationRow[], groupOf: (row: TranslationRow) => string): TranslationRowGroup[];
219
+ /**
220
+ * The rows a group's "Approve the unreviewed" takes: the ones nobody has given a verdict
221
+ * and that have a text. Not `changed` — a string that moved since it was judged asks to
222
+ * be READ again, which a bulk action is the opposite of — and not `missing`, which has
223
+ * nothing to approve.
224
+ */
225
+ declare function unreviewedRows(rows: readonly TranslationRow[]): TranslationRow[];
202
226
  /** What gets stored for a verdict: the wording it was given on, so a later change of the
203
227
  * string (or of its reference) shows up as `changed`. */
204
228
  declare function reviewWrite(row: TranslationRow, verdict: TranslationVerdict, note?: string | null, suggestion?: string | null): TranslationReviewWrite;
229
+ /** What takes a set of verdicts back: see {@link reviewUndo}. */
230
+ interface TranslationReviewUndo {
231
+ /** The verdicts the rows had before, to store again — each on the wording it was
232
+ * given on, so a row that was `changed` reads `changed` again. */
233
+ writes: TranslationReviewWrite[];
234
+ /** The rows that had no verdict before: back to unreviewed. */
235
+ clears: TranslationReviewKey[];
236
+ }
237
+ /**
238
+ * How to take back what was just said about `rows` — the rows AS THEY WERE before the
239
+ * write, with the verdicts they carried then. The panel's Undo toast (0.25, keksdose live
240
+ * #377) runs it: a swipe on a phone approves in one movement, and a movement can be a
241
+ * mistake.
242
+ *
243
+ * A row that had a verdict gets it stored again (its note, its suggestion, the wording it
244
+ * was judged on); one that had none is cleared. The server stamps the restored verdict
245
+ * with the person undoing and the time, as it stamps every write — the contract has no
246
+ * way to write a verdict in somebody else's name, and should not.
247
+ */
248
+ declare function reviewUndo(rows: readonly TranslationRow[]): TranslationReviewUndo;
205
249
  /** The app's cached verdict list with `written` in place of what was said before — a
206
250
  * save answers with the rows it wrote, so a page patches its copy instead of
207
251
  * downloading every verdict again after each click. Any spelling with `locale` and
@@ -229,4 +273,4 @@ interface TranslationCorrection {
229
273
  * string included), as the export file lists them. */
230
274
  declare function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[];
231
275
 
232
- export { type ApiTranslationReview, type ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, REVIEW_STATUSES, type ReviewStatus, type TranslationCorrection, type TranslationReview, type TranslationReviewFilter, type TranslationReviewKey, type TranslationReviewWrite, type TranslationRow, type TranslationRowsInput, type TranslationSummary, type TranslationVerdict, dropReviews, filterTranslationRows, flattenStrings, fromApiReview, keyInArea, keyInAreas, keyNamespace, mergeReviews, placeholderMismatch, placeholderTokens, reviewStatus, reviewWrite, summariseRows, toApiWrite, translationCorrections, translationRows };
276
+ export { type ApiTranslationReview, type ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, REVIEW_STATUSES, type ReviewStatus, type TranslationCorrection, type TranslationReview, type TranslationReviewFilter, type TranslationReviewKey, type TranslationReviewUndo, type TranslationReviewWrite, type TranslationRow, type TranslationRowGroup, type TranslationRowsInput, type TranslationSummary, type TranslationVerdict, dropReviews, filterTranslationRows, flattenStrings, fromApiReview, groupTranslationRows, keyInArea, keyInAreas, keyNamespace, mergeReviews, placeholderMismatch, placeholderTokens, reviewStatus, reviewUndo, reviewWrite, summariseRows, toApiWrite, translationCorrections, translationRows, unreviewedRows };
@@ -138,6 +138,19 @@ function filterTranslationRows(rows, filter) {
138
138
  (row) => (f.status === "all" || row.status === f.status) && (f.source === "all" || row.source === f.source) && (f.namespace === "all" || row.namespace === f.namespace) && (!f.placeholdersOnly || row.placeholderMismatch) && (!q || row.key.toLowerCase().includes(q) || row.text.toLowerCase().includes(q) || row.reference.toLowerCase().includes(q))
139
139
  );
140
140
  }
141
+ function groupTranslationRows(rows, groupOf) {
142
+ const byKey = /* @__PURE__ */ new Map();
143
+ for (const row of rows) {
144
+ const key = groupOf(row);
145
+ const list = byKey.get(key);
146
+ if (list) list.push(row);
147
+ else byKey.set(key, [row]);
148
+ }
149
+ return [...byKey].map(([key, list]) => ({ key, rows: list, summary: summariseRows(list) }));
150
+ }
151
+ function unreviewedRows(rows) {
152
+ return rows.filter((row) => row.status === "unreviewed" && row.text !== "");
153
+ }
141
154
  function reviewWrite(row, verdict, note = null, suggestion = null) {
142
155
  return {
143
156
  locale: row.locale,
@@ -149,6 +162,27 @@ function reviewWrite(row, verdict, note = null, suggestion = null) {
149
162
  suggestion
150
163
  };
151
164
  }
165
+ function reviewUndo(rows) {
166
+ const writes = [];
167
+ const clears = [];
168
+ for (const row of rows) {
169
+ const before = row.review;
170
+ if (!before) {
171
+ clears.push({ locale: row.locale, key: row.key });
172
+ continue;
173
+ }
174
+ writes.push({
175
+ locale: row.locale,
176
+ key: row.key,
177
+ text: before.text,
178
+ referenceText: before.referenceText,
179
+ verdict: before.verdict,
180
+ note: before.note,
181
+ suggestion: before.suggestion
182
+ });
183
+ }
184
+ return { writes, clears };
185
+ }
152
186
  function mergeReviews(prev, written) {
153
187
  const id = (r) => `${r.locale}|${r.key}`;
154
188
  const replaced = new Set(written.map(id));
@@ -178,6 +212,7 @@ export {
178
212
  filterTranslationRows,
179
213
  flattenStrings,
180
214
  fromApiReview,
215
+ groupTranslationRows,
181
216
  keyInArea,
182
217
  keyInAreas,
183
218
  keyNamespace,
@@ -185,10 +220,12 @@ export {
185
220
  placeholderMismatch,
186
221
  placeholderTokens,
187
222
  reviewStatus,
223
+ reviewUndo,
188
224
  reviewWrite,
189
225
  summariseRows,
190
226
  toApiWrite,
191
227
  translationCorrections,
192
- translationRows
228
+ translationRows,
229
+ unreviewedRows
193
230
  };
194
231
  //# sourceMappingURL=translation-review.js.map