@eifi1/ui-kit 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +3 -3
  2. package/dist/components/data-table.js +3 -2
  3. package/dist/components/data-table.js.map +1 -1
  4. package/dist/components/swipeable-row.d.ts +5 -1
  5. package/dist/components/swipeable-row.js.map +1 -1
  6. package/dist/components/translation-review-labels.d.ts +4 -0
  7. package/dist/components/translation-review-labels.js +2 -1
  8. package/dist/components/translation-review-labels.js.map +1 -1
  9. package/dist/components/translation-review.d.ts +27 -3
  10. package/dist/components/translation-review.js +50 -21
  11. package/dist/components/translation-review.js.map +1 -1
  12. package/dist/i18n/german.js +1 -0
  13. package/dist/i18n/german.js.map +1 -1
  14. package/dist/i18n/locales/en.js +1 -0
  15. package/dist/i18n/locales/en.js.map +1 -1
  16. package/dist/i18n/locales/es.js +1 -0
  17. package/dist/i18n/locales/es.js.map +1 -1
  18. package/dist/i18n/locales/fr.js +1 -0
  19. package/dist/i18n/locales/fr.js.map +1 -1
  20. package/dist/i18n/locales/hu.js +1 -0
  21. package/dist/i18n/locales/hu.js.map +1 -1
  22. package/dist/i18n/locales/it.js +1 -0
  23. package/dist/i18n/locales/it.js.map +1 -1
  24. package/dist/i18n/locales/zh.js +1 -0
  25. package/dist/i18n/locales/zh.js.map +1 -1
  26. package/dist/i18n/review.js +1 -0
  27. package/dist/i18n/review.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +7 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/translation-review.d.ts +76 -1
  32. package/dist/lib/translation-review.js +30 -0
  33. package/dist/lib/translation-review.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/data-table.tsx +9 -2
  36. package/src/components/swipeable-row.tsx +5 -1
  37. package/src/components/translation-review-labels.ts +5 -0
  38. package/src/components/translation-review.tsx +102 -35
  39. package/src/i18n/german.ts +1 -0
  40. package/src/i18n/locales/en.ts +1 -0
  41. package/src/i18n/locales/es.ts +1 -0
  42. package/src/i18n/locales/fr.ts +1 -0
  43. package/src/i18n/locales/hu.ts +1 -0
  44. package/src/i18n/locales/it.ts +1 -0
  45. package/src/i18n/locales/zh.ts +1 -0
  46. package/src/i18n/review.ts +1 -0
  47. package/src/index.ts +6 -0
  48. package/src/lib/translation-review.ts +109 -0
  49. package/tokens.css +4 -0
package/dist/index.d.ts CHANGED
@@ -149,7 +149,7 @@ export { DEFAULT_UI_KIT_LABELS } from './i18n/defaults.js';
149
149
  export { kitLabelStrings } from './i18n/review.js';
150
150
  export { KIT_LANGUAGES, KitLanguage, KitLanguageCode, formatLocaleOf, languageOptions, loadUiKitLabels, peekUiKitLabels, resolveLanguage, useUiKitLabels } from './i18n/languages.js';
151
151
  export { withAllPlurals } from './i18n/plurals.js';
152
- export { ApiTranslationReview, ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, REVIEW_STATUSES, ReviewStatus, TranslationCorrection, TranslationReview, TranslationReviewFilter, TranslationReviewKey, TranslationReviewUndo, TranslationReviewWrite, TranslationRow, TranslationRowGroup, TranslationRowsInput, TranslationSummary, TranslationVerdict, dropReviews, filterTranslationRows, flattenStrings, fromApiReview, groupTranslationRows, keyInArea, keyInAreas, keyNamespace, mergeReviews, placeholderMismatch, placeholderTokens, reviewStatus, reviewUndo, reviewWrite, summariseRows, toApiWrite, translationCorrections, translationRows, unreviewedRows } from './lib/translation-review.js';
152
+ export { ApiTranslationReview, ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, DEFAULT_TRANSLATION_REVIEW_SWIPE, REVIEW_STATUSES, ReviewStatus, TRANSLATION_REVIEW_SWIPE_ACTIONS, TranslationCorrection, TranslationReview, TranslationReviewFilter, TranslationReviewKey, TranslationReviewSwipeAction, TranslationReviewSwipeBinding, TranslationReviewUndo, TranslationReviewWrite, TranslationRow, TranslationRowGroup, TranslationRowsInput, TranslationSummary, TranslationVerdict, dropReviews, filterTranslationRows, flattenStrings, fromApiReview, groupTranslationRows, keyInArea, keyInAreas, keyNamespace, mergeReviews, placeholderMismatch, placeholderTokens, reviewStatus, reviewUndo, reviewWrite, summariseRows, toApiWrite, translationCorrections, translationReviewSwipePlan, translationRows, unreviewedRows } from './lib/translation-review.js';
153
153
  export { TranslationReviewGroupBy, TranslationReviewOrigin, TranslationReviewPanel, TranslationReviewPanelProps, TranslationReviewSaveInfo } from './components/translation-review.js';
154
154
  export { DEFAULT_LEGAL_LABELS, LegalLabels, LegalLayout, LegalLayoutProps, LegalLink, LegalLinks, LegalLinksProps, LegalSection, LegalSectionProps } from './components/legal.js';
155
155
  export { CheckboxGroup, CheckboxGroupLegendVisibility, CheckboxGroupOption, CheckboxGroupProps } from './components/checkbox-group.js';
package/dist/index.js CHANGED
@@ -192,7 +192,10 @@ import {
192
192
  translationCorrections,
193
193
  groupTranslationRows,
194
194
  unreviewedRows,
195
- reviewUndo
195
+ reviewUndo,
196
+ TRANSLATION_REVIEW_SWIPE_ACTIONS,
197
+ DEFAULT_TRANSLATION_REVIEW_SWIPE,
198
+ translationReviewSwipePlan
196
199
  } from "./lib/translation-review.js";
197
200
  import {
198
201
  TranslationReviewPanel,
@@ -261,6 +264,7 @@ export {
261
264
  DEFAULT_TOAST_LABELS,
262
265
  DEFAULT_TRANSLATION_REVIEW_FILTER,
263
266
  DEFAULT_TRANSLATION_REVIEW_LABELS,
267
+ DEFAULT_TRANSLATION_REVIEW_SWIPE,
264
268
  DEFAULT_WRITE_LOCK_LABELS,
265
269
  DangerConfirm,
266
270
  DataTable,
@@ -287,6 +291,7 @@ export {
287
291
  Skeleton,
288
292
  SwipeableRow,
289
293
  TOAST_ACTION_DURATION,
294
+ TRANSLATION_REVIEW_SWIPE_ACTIONS,
290
295
  TextLink,
291
296
  Toaster,
292
297
  TranslationExportButton,
@@ -351,6 +356,7 @@ export {
351
356
  toApiWrite,
352
357
  toast,
353
358
  translationCorrections,
359
+ translationReviewSwipePlan,
354
360
  translationRows,
355
361
  typedMatches,
356
362
  unreviewedRows,
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// 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
+ {"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 // 0.26: swipe bindings for an app's Settings → Interaction (keksdose #377 rework).\n TRANSLATION_REVIEW_SWIPE_ACTIONS,\n DEFAULT_TRANSLATION_REVIEW_SWIPE,\n translationReviewSwipePlan,\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 TranslationReviewSwipeAction,\n TranslationReviewSwipeBinding,\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":[]}
@@ -253,6 +253,81 @@ declare function reviewUndo(rows: readonly TranslationRow[]): TranslationReviewU
253
253
  declare function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[];
254
254
  /** The cached list without the verdicts on `cleared`. */
255
255
  declare function dropReviews<T extends TranslationReviewKey>(prev: readonly T[], cleared: readonly TranslationReviewKey[]): T[];
256
+ /**
257
+ * What a phone card's swipe can be bound to in `TranslationReviewPanel`'s `swipe`
258
+ * (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add the swipe options to be shown
259
+ * also to the settings /settings#interaction area where the other swipe options are
260
+ * defined"*). keksdose lets the user bind every list's swipes there — per side, a primary
261
+ * action at the first threshold and a secondary one at a longer drag — and lists each
262
+ * surface's ids in its `SWIPE_ACTIONS`; this is the translation list's, in the order a
263
+ * settings page offers them.
264
+ *
265
+ * - `approve` — the string is right as it stands. With the panel's Undo toast.
266
+ * - `edit` — open the editor with the cursor in the wording: "Needs a change" (or
267
+ * "Suggest translation" on a missing string). Writes nothing; the editor does.
268
+ * - `clear` — take the verdict back: unreviewed again. With the Undo toast, which stores
269
+ * the verdict again.
270
+ *
271
+ * THERE IS NO SEPARATE "needs a change". The editor has no verdict to preselect: Approve
272
+ * and "Needs a change" stand side by side in it, and "Needs a change" takes a note or a
273
+ * different wording — a verdict without either tells whoever fixes the string nothing, so
274
+ * a swipe never sends one blind (0.25). The one honest thing a "needs a change" swipe can
275
+ * do is therefore open the editor with the cursor in the wording — which is `edit`, and
276
+ * `edit` already says "Needs a change" on the card. A second id doing the same would arm
277
+ * a second threshold that does what the first already did: the duplicate keksdose's
278
+ * `resolveSwipePlan` drops for exactly that reason.
279
+ */
280
+ declare const TRANSLATION_REVIEW_SWIPE_ACTIONS: readonly ["approve", "edit", "clear"];
281
+ /** One of {@link TRANSLATION_REVIEW_SWIPE_ACTIONS}. */
282
+ type TranslationReviewSwipeAction = (typeof TRANSLATION_REVIEW_SWIPE_ACTIONS)[number];
283
+ /**
284
+ * Which swipe does what on a phone card — `TranslationReviewPanel`'s `swipe`. Each side is
285
+ * an ordered ladder, exactly as `SwipeableRow` and DataTable's `mobileSwipeActions` model
286
+ * one: index 0 commits at the first threshold, index 1 at the longer drag (and so on — the
287
+ * thresholds spread evenly over the card's width). A side left out, or empty, does not
288
+ * swipe.
289
+ *
290
+ * LOGICAL SIDES: `end` is a drag toward the reading end of the line — right in a
291
+ * left-to-right page, left in a right-to-left one — and `start` the other way, so one
292
+ * binding holds in both directions. An app whose settings store physical sides (keksdose's
293
+ * `right`/`left`) hands `right` as `end` for a left-to-right page.
294
+ */
295
+ interface TranslationReviewSwipeBinding {
296
+ /** A drag toward the reading START, nearest threshold first. */
297
+ start?: readonly TranslationReviewSwipeAction[];
298
+ /** A drag toward the reading END, nearest threshold first. */
299
+ end?: readonly TranslationReviewSwipeAction[];
300
+ }
301
+ /** What `swipe={true}` means — 0.25's fixed mapping: approve toward the end, open the
302
+ * editor ("Needs a change") toward the start. A settings page's "reset" goes back here. */
303
+ declare const DEFAULT_TRANSLATION_REVIEW_SWIPE: TranslationReviewSwipeBinding;
304
+ /** The ladders one row actually offers, from a {@link TranslationReviewSwipeBinding}. */
305
+ interface TranslationReviewSwipePlan {
306
+ start: TranslationReviewSwipeAction[];
307
+ end: TranslationReviewSwipeAction[];
308
+ }
309
+ /**
310
+ * The binding resolved against one row — keksdose's `resolveSwipePlan`, for this list.
311
+ * An id the row cannot offer DROPS OUT and the ladder closes up behind it, so the second
312
+ * stage becomes the first rather than the side going dead behind an unreachable first
313
+ * stage (or arming a threshold that does nothing):
314
+ *
315
+ * - `approve`: not on a missing string (nothing to approve) or an approved one (it would
316
+ * say it again) — the rows that have no approve button either;
317
+ * - `clear`: not without `canClear` (the app passed no `onClear`), and not on a row with
318
+ * no verdict to take back (unreviewed; missing with no suggestion);
319
+ * - `edit`: on every row;
320
+ * - an id named twice on one side: the second time (a longer drag to the same effect);
321
+ * - an id this version does not know (a binding stored by another version, an app's own
322
+ * "none" slot): dropped like one that does not apply.
323
+ *
324
+ * Only what the ROW decides. Whether the panel may write at all — `readOnly`, no
325
+ * `onSave`, a write lock, the row's own write still out — is the panel's, and then no
326
+ * side swipes.
327
+ */
328
+ declare function translationReviewSwipePlan(binding: TranslationReviewSwipeBinding, row: TranslationRow, { canClear }?: {
329
+ canClear?: boolean;
330
+ }): TranslationReviewSwipePlan;
256
331
  /**
257
332
  * One entry of the "Export corrections" file — enough for a developer to apply the change
258
333
  * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a
@@ -273,4 +348,4 @@ interface TranslationCorrection {
273
348
  * string included), as the export file lists them. */
274
349
  declare function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[];
275
350
 
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 };
351
+ export { type ApiTranslationReview, type ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, DEFAULT_TRANSLATION_REVIEW_SWIPE, REVIEW_STATUSES, type ReviewStatus, TRANSLATION_REVIEW_SWIPE_ACTIONS, type TranslationCorrection, type TranslationReview, type TranslationReviewFilter, type TranslationReviewKey, type TranslationReviewSwipeAction, type TranslationReviewSwipeBinding, type TranslationReviewSwipePlan, 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, translationReviewSwipePlan, translationRows, unreviewedRows };
@@ -192,6 +192,33 @@ function dropReviews(prev, cleared) {
192
192
  const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));
193
193
  return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));
194
194
  }
195
+ const TRANSLATION_REVIEW_SWIPE_ACTIONS = ["approve", "edit", "clear"];
196
+ const DEFAULT_TRANSLATION_REVIEW_SWIPE = Object.freeze({
197
+ start: Object.freeze(["edit"]),
198
+ end: Object.freeze(["approve"])
199
+ });
200
+ function translationReviewSwipePlan(binding, row, { canClear = false } = {}) {
201
+ const offers = (id) => {
202
+ switch (id) {
203
+ case "approve":
204
+ return row.text !== "" && row.status !== "approved";
205
+ case "edit":
206
+ return true;
207
+ case "clear":
208
+ return canClear && row.review !== null;
209
+ default:
210
+ return false;
211
+ }
212
+ };
213
+ const side = (ids) => {
214
+ const out = [];
215
+ for (const id of ids ?? []) {
216
+ if (!out.includes(id) && offers(id)) out.push(id);
217
+ }
218
+ return out;
219
+ };
220
+ return { start: side(binding.start), end: side(binding.end) };
221
+ }
195
222
  function translationCorrections(rows) {
196
223
  return rows.filter((row) => row.review?.verdict === "NEEDS_CHANGE").map((row) => ({
197
224
  locale: row.locale,
@@ -207,7 +234,9 @@ function translationCorrections(rows) {
207
234
  }
208
235
  export {
209
236
  DEFAULT_TRANSLATION_REVIEW_FILTER,
237
+ DEFAULT_TRANSLATION_REVIEW_SWIPE,
210
238
  REVIEW_STATUSES,
239
+ TRANSLATION_REVIEW_SWIPE_ACTIONS,
211
240
  dropReviews,
212
241
  filterTranslationRows,
213
242
  flattenStrings,
@@ -225,6 +254,7 @@ export {
225
254
  summariseRows,
226
255
  toApiWrite,
227
256
  translationCorrections,
257
+ translationReviewSwipePlan,
228
258
  translationRows,
229
259
  unreviewedRows
230
260
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/translation-review.ts"],"sourcesContent":["/**\n * The pure half of an app's translation review (H1 in docs/i18n-harmonization.md): from\n * the strings the app ships and the verdicts its server stores to one row per (locale,\n * key) with a status — keksdose's `features/translations/translation-catalogue.ts` and\n * kastlan's copy of it, which had grown apart by a fifth status and a second kind of key.\n *\n * WHY THE STRINGS ARE THE CATALOGUE. Nothing on the server lists which strings exist: a\n * verdict only says what somebody thought of one. So the rows are built from what the app\n * ships — its locale bundles, kastlan's document texts, the kit's own words\n * (`kitLabelStrings`) — and the review can never offer a string users do not see. A\n * verdict is stored WITH the wording it was given on, and a string that changed since is\n * noticed by comparing the two: `changed`, read it again, whatever the verdict was.\n *\n * WHAT STAYS IN THE APP. Which locale is read against which (keksdose: everything against\n * English, English against de-CH; kastlan: against German, German against English), where\n * the strings come from, who may review what (the locales and areas its server answers\n * with), and every request. The kit takes those as input and never derives them.\n *\n * camelCase, like the rest of the kit. The contract's wire shape is snake_case\n * (`reference_text`, `reviewer_name`, `reviewed_at`); {@link fromApiReview} and\n * {@link toApiWrite} are the two lines each app would otherwise write — both apps speak\n * the same contract, so the mapping is written once, here.\n */\n\n// ── Shapes ──────────────────────────────────────────────────────────────────────\n\nexport type TranslationVerdict = \"APPROVED\" | \"NEEDS_CHANGE\";\n\n/** One stored verdict: what a reviewer said about one string in one locale. */\nexport interface TranslationReview {\n locale: string;\n key: string;\n /** The wording that was judged — compared with today's to notice a change. Empty for\n * a string the locale lacked (a suggested translation for a `missing` row). */\n text: string;\n /** The reference wording at the time. `null` from a server that did not store it. */\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n /** The reviewer's wording — or, for a missing string, the translation to add. */\n suggestion: string | null;\n /** `null` once the reviewer's account has been erased. */\n reviewerName: string | null;\n /** ISO timestamp. */\n reviewedAt: string;\n}\n\n/** What a save sends: the verdict plus the wording it is given on. */\nexport interface TranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** What a reset sends: back to \"unreviewed\". */\nexport interface TranslationReviewKey {\n locale: string;\n key: string;\n}\n\n/**\n * Where a string stands, in the order a reviewer works through them: what is not there\n * at all, what nobody has read, what changed after it was read, what was sent back, and\n * what is done. `missing` never occurs for an app whose locales have every key\n * (keksdose); kastlan's French lacks 172 screen texts.\n */\nexport type ReviewStatus = \"missing\" | \"unreviewed\" | \"changed\" | \"needs_change\" | \"approved\";\n\nexport const REVIEW_STATUSES: readonly ReviewStatus[] = [\n \"missing\",\n \"unreviewed\",\n \"changed\",\n \"needs_change\",\n \"approved\",\n];\n\nexport interface TranslationRow {\n /** `${locale}|${key}` — unique across every locale on a page. A bar, not a colon:\n * kastlan's keys contain colons (`common:actions.save`). */\n id: string;\n key: string;\n /** The key's group: `budget` for `budget.rta`, `common` for `common:actions.save`,\n * `kit` for the kit's own words. See {@link keyNamespace}. */\n namespace: string;\n /** Where the string comes from, in the app's own words — kastlan's `screen` and\n * `documents`, keksdose's one source. `\"\"` when the app does not say. */\n source: string;\n locale: string;\n /** The wording users see today; empty when the locale lacks the string. */\n text: string;\n /** The same key in the reference locale. */\n reference: string;\n status: ReviewStatus;\n review: TranslationReview | null;\n /** The interpolations and tags differ from the reference's — `{{count}}` dropped, a\n * `<1>` link lost, a `{date}` renamed. A machine can check that much, so it does,\n * before a person reads. Never set on a missing string: it has nothing to lose. */\n placeholderMismatch: boolean;\n}\n\n// ── The wire contract ───────────────────────────────────────────────────────────\n\n/** A verdict as `GET /translations/reviews` returns it (H1's shared contract). Typed\n * structurally, so an app's generated OpenAPI type fits without a cast. */\nexport interface ApiTranslationReview {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n reviewer_name: string | null;\n reviewed_at: string;\n}\n\n/** One item of `PUT /translations/reviews`. */\nexport interface ApiTranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** A verdict from the server, in the kit's spelling. */\nexport function fromApiReview(review: ApiTranslationReview): TranslationReview {\n return {\n locale: review.locale,\n key: review.key,\n text: review.text,\n referenceText: review.reference_text,\n verdict: review.verdict,\n note: review.note,\n suggestion: review.suggestion,\n reviewerName: review.reviewer_name,\n reviewedAt: review.reviewed_at,\n };\n}\n\n/** A save, in the server's spelling. `TranslationReviewKey` needs no mapping — it is\n * `{ locale, key }` on both sides. */\nexport function toApiWrite(write: TranslationReviewWrite): ApiTranslationReviewWrite {\n return {\n locale: write.locale,\n key: write.key,\n text: write.text,\n reference_text: write.referenceText,\n verdict: write.verdict,\n note: write.note,\n suggestion: write.suggestion,\n };\n}\n\n// ── Strings ─────────────────────────────────────────────────────────────────────\n\n/**\n * A nested bundle as `key → string`, in the bundle's own order. Non-string leaves are\n * skipped rather than stringified into something nobody wrote.\n *\n * `prefix` is put in front of every key AS IS, so it names the separator too:\n * `flattenStrings(bundle)` is keksdose's `budget.rta`, `flattenStrings(tree, \"common:\")`\n * kastlan's `common:actions.save`, and `flattenStrings(kitLabelStrings(labels), \"kit.\")`\n * the kit's own words as `kit.combobox.resultCount(3)` — already flat, so only the prefix\n * is added.\n */\nexport function flattenStrings(tree: Readonly<Record<string, unknown>>, prefix = \"\"): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(tree)) {\n const path = `${prefix}${key}`;\n if (typeof value === \"string\") out[path] = value;\n else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n Object.assign(out, flattenStrings(value as Record<string, unknown>, `${path}.`));\n }\n }\n return out;\n}\n\n/**\n * The key's group, for the namespace filter: the part before a colon when there is one\n * (kastlan's `common:actions.save` → `common`, `doc_text.invoice:due` →\n * `doc_text.invoice`), else the first dotted segment (keksdose's `budget.rta` →\n * `budget`, the kit's `kit.combobox.resultCount(3)` → `kit`). Only the part before a\n * sample's `(` is looked at, so a colon inside one cannot move the split.\n */\nexport function keyNamespace(key: string): string {\n const head = key.split(\"(\", 1)[0];\n const colon = head.indexOf(\":\");\n return colon >= 0 ? head.slice(0, colon) : head.split(\".\", 1)[0];\n}\n\n// i18next `{{x}}` (formatting options ignored: `{{x, number}}` is `{{x}}`); a Python\n// format field `{x}` / `{0}` / `{amount:.2f}` (kastlan's document texts), but not a\n// LaTeX argument (`\\textbf{x}` — a letter or backslash right before the brace); a\n// numbered or named tag (`<1>`, `<strong>`), not prose in angle brackets (\"<deleted\n// user>\"); and a nested `$t(key)`. `{{x}}` is tried first at its position and consumed\n// whole, so it is never counted a second time as `{x}`.\nconst PLACEHOLDER_RE =\n /\\{\\{\\s*([^}\\s,]+)[^}]*\\}\\}|(?<![\\\\A-Za-z])\\{([a-z_][a-z0-9_]*|\\d+)(?:[!:][^{}]*)?\\}|<\\/?(\\d+|[a-z][a-z0-9]*)\\s*\\/?>|\\$t\\(([^)]+)\\)/g;\n\n/** The placeholders in a string, as a sorted multiset: `{{count}}`, `{date}`, `<1>`,\n * `$t(common.save)`. */\nexport function placeholderTokens(text: string): string[] {\n const found: string[] = [];\n for (const m of text.matchAll(PLACEHOLDER_RE)) {\n if (m[1]) found.push(`{{${m[1]}}}`);\n else if (m[2]) found.push(`{${m[2]}}`);\n else if (m[3]) found.push(`<${m[3]}>`);\n else if (m[4]) found.push(`$t(${m[4]})`);\n }\n return found.sort();\n}\n\n/** Whether `text` has other placeholders than `reference` — dropped, renamed or added.\n * Moving one is fine: a translation reorders a sentence. */\nexport function placeholderMismatch(text: string, reference: string): boolean {\n const a = placeholderTokens(text);\n const b = placeholderTokens(reference);\n return a.length !== b.length || a.some((p, i) => p !== b[i]);\n}\n\n// ── Status ──────────────────────────────────────────────────────────────────────\n\n/**\n * A verdict counts only for the wording it was given on. If the string or its reference\n * changed since, it is `changed` — read it again — whatever the verdict was. A string the\n * locale lacks is `missing` until somebody suggests its translation (stored as\n * NEEDS_CHANGE on the empty text, so it reads `needs_change`, and `changed` once a\n * developer adds the string).\n */\nexport function reviewStatus(\n review: TranslationReview | null | undefined,\n text: string,\n reference: string,\n): ReviewStatus {\n if (!review) return text === \"\" && reference !== \"\" ? \"missing\" : \"unreviewed\";\n if (review.text !== text) return \"changed\";\n if (review.referenceText != null && review.referenceText !== reference) return \"changed\";\n return review.verdict === \"APPROVED\" ? \"approved\" : \"needs_change\";\n}\n\n/** Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`\n * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not. */\nexport function keyInArea(key: string, area: string): boolean {\n return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);\n}\n\n/**\n * Whether `key` lies in one of `areas` — the scope a reviewer may be limited to\n * (keksdose: `[\"legal\"]`, a lawyer who judges the Imprint, the Privacy Policy and the\n * Terms and sees nothing else). `null`/`undefined` is every area. The server applies the\n * same rule and refuses verdicts outside; this decides only what is listed.\n */\nexport function keyInAreas(\n key: string,\n areas: readonly string[] | null | undefined,\n inArea: (key: string, area: string) => boolean = keyInArea,\n): boolean {\n return !areas || areas.some((area) => inArea(key, area));\n}\n\nexport interface TranslationRowsInput {\n locale: string;\n /** The locale's strings, `key → text` ({@link flattenStrings}). */\n strings: Readonly<Record<string, string>>;\n /** The reference locale's strings — whichever locale THIS app reads `locale` against. */\n reference: Readonly<Record<string, string>>;\n /** The verdicts: this locale's by key, or the whole list as the server sent it (other\n * locales' verdicts are skipped). */\n reviews?: ReadonlyMap<string, TranslationReview> | readonly TranslationReview[];\n /** Default {@link keyNamespace}. */\n namespaceOf?: (key: string) => string;\n /** The row's source, an app-defined word (kastlan: `doc_text.` keys are `documents`,\n * the rest `screen`). Default: none (`\"\"`). */\n sourceOf?: (key: string) => string;\n /** Keep only keys inside these areas ({@link keyInAreas}); `null` keeps every key. */\n areas?: readonly string[] | null;\n /** Default {@link keyInArea}. */\n inArea?: (key: string, area: string) => boolean;\n}\n\n/**\n * Every key of one locale, with its status: the reference's keys first, in its order, so\n * rows read the way the app's authors wrote them; a key only this locale has is appended\n * rather than dropped (a parity test should forbid it, but the page should not lie if one\n * slips through).\n */\nexport function translationRows({\n locale,\n strings,\n reference,\n reviews,\n namespaceOf: groupOf = keyNamespace,\n sourceOf,\n areas,\n inArea,\n}: TranslationRowsInput): TranslationRow[] {\n const byKey = new Map<string, TranslationReview>();\n if (reviews instanceof Map) {\n for (const [key, review] of reviews) byKey.set(key, review);\n } else if (reviews) {\n for (const review of reviews as readonly TranslationReview[]) {\n if (review.locale === locale) byKey.set(review.key, review);\n }\n }\n const keys = Object.keys(reference);\n for (const key of Object.keys(strings)) if (!(key in reference)) keys.push(key);\n return keys\n .filter((key) => keyInAreas(key, areas, inArea))\n .map((key) => {\n const text = strings[key] ?? \"\";\n const ref = reference[key] ?? \"\";\n const review = byKey.get(key) ?? null;\n return {\n id: `${locale}|${key}`,\n key,\n namespace: groupOf(key),\n source: sourceOf?.(key) ?? \"\",\n locale,\n text,\n reference: ref,\n status: reviewStatus(review, text, ref),\n review,\n placeholderMismatch: text !== \"\" && placeholderMismatch(text, ref),\n };\n });\n}\n\n/** How many rows stand where, and how many there are. */\nexport type TranslationSummary = Record<ReviewStatus, number> & { total: number };\n\nexport function summariseRows(rows: readonly TranslationRow[]): TranslationSummary {\n const summary: TranslationSummary = {\n missing: 0,\n unreviewed: 0,\n changed: 0,\n needs_change: 0,\n approved: 0,\n total: rows.length,\n };\n for (const row of rows) summary[row.status] += 1;\n return summary;\n}\n\n// ── Filtering ───────────────────────────────────────────────────────────────────\n\n/** The panel's filters. `\"all\"` switches one off. */\nexport interface TranslationReviewFilter {\n status: ReviewStatus | \"all\";\n source: string;\n namespace: string;\n /** Searched in the key, the text and the reference, case-insensitively. */\n query: string;\n placeholdersOnly: boolean;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter = {\n status: \"all\",\n source: \"all\",\n namespace: \"all\",\n query: \"\",\n placeholdersOnly: false,\n};\n\nexport function filterTranslationRows(\n rows: readonly TranslationRow[],\n filter: Partial<TranslationReviewFilter>,\n): TranslationRow[] {\n const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };\n const q = f.query.trim().toLowerCase();\n return rows.filter(\n (row) =>\n (f.status === \"all\" || row.status === f.status) &&\n (f.source === \"all\" || row.source === f.source) &&\n (f.namespace === \"all\" || row.namespace === f.namespace) &&\n (!f.placeholdersOnly || row.placeholderMismatch) &&\n (!q ||\n row.key.toLowerCase().includes(q) ||\n row.text.toLowerCase().includes(q) ||\n row.reference.toLowerCase().includes(q)),\n );\n}\n\n// ── Grouping ────────────────────────────────────────────────────────────────────\n\n/** One group of rows — an area, a source — and how they stand. */\nexport interface TranslationRowGroup {\n key: string;\n rows: TranslationRow[];\n summary: TranslationSummary;\n}\n\n/**\n * Rows in groups, each group where its first row is — the reference's order, so the areas\n * read the way the app's authors wrote the bundle — and each group's rows in their own\n * order. keksdose live #377: a reviewer on a phone works area by area (\"the budget\n * strings, then the legal ones\"), and a flat list of 3859 rows is not a way to see\n * where an area stands.\n *\n * `groupOf` names a row's group: `(row) => row.namespace` for the areas,\n * `(row) => row.source` for kastlan's screen texts and documents.\n */\nexport function groupTranslationRows(\n rows: readonly TranslationRow[],\n groupOf: (row: TranslationRow) => string,\n): TranslationRowGroup[] {\n const byKey = new Map<string, TranslationRow[]>();\n for (const row of rows) {\n const key = groupOf(row);\n const list = byKey.get(key);\n if (list) list.push(row);\n else byKey.set(key, [row]);\n }\n return [...byKey].map(([key, list]) => ({ key, rows: list, summary: summariseRows(list) }));\n}\n\n/**\n * The rows a group's \"Approve the unreviewed\" takes: the ones nobody has given a verdict\n * and that have a text. Not `changed` — a string that moved since it was judged asks to\n * be READ again, which a bulk action is the opposite of — and not `missing`, which has\n * nothing to approve.\n */\nexport function unreviewedRows(rows: readonly TranslationRow[]): TranslationRow[] {\n return rows.filter((row) => row.status === \"unreviewed\" && row.text !== \"\");\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────────\n\n/** What gets stored for a verdict: the wording it was given on, so a later change of the\n * string (or of its reference) shows up as `changed`. */\nexport function reviewWrite(\n row: TranslationRow,\n verdict: TranslationVerdict,\n note: string | null = null,\n suggestion: string | null = null,\n): TranslationReviewWrite {\n return {\n locale: row.locale,\n key: row.key,\n text: row.text,\n referenceText: row.reference,\n verdict,\n note,\n suggestion,\n };\n}\n\n/** What takes a set of verdicts back: see {@link reviewUndo}. */\nexport interface TranslationReviewUndo {\n /** The verdicts the rows had before, to store again — each on the wording it was\n * given on, so a row that was `changed` reads `changed` again. */\n writes: TranslationReviewWrite[];\n /** The rows that had no verdict before: back to unreviewed. */\n clears: TranslationReviewKey[];\n}\n\n/**\n * How to take back what was just said about `rows` — the rows AS THEY WERE before the\n * write, with the verdicts they carried then. The panel's Undo toast (0.25, keksdose live\n * #377) runs it: a swipe on a phone approves in one movement, and a movement can be a\n * mistake.\n *\n * A row that had a verdict gets it stored again (its note, its suggestion, the wording it\n * was judged on); one that had none is cleared. The server stamps the restored verdict\n * with the person undoing and the time, as it stamps every write — the contract has no\n * way to write a verdict in somebody else's name, and should not.\n */\nexport function reviewUndo(rows: readonly TranslationRow[]): TranslationReviewUndo {\n const writes: TranslationReviewWrite[] = [];\n const clears: TranslationReviewKey[] = [];\n for (const row of rows) {\n const before = row.review;\n if (!before) {\n clears.push({ locale: row.locale, key: row.key });\n continue;\n }\n writes.push({\n locale: row.locale,\n key: row.key,\n text: before.text,\n referenceText: before.referenceText,\n verdict: before.verdict,\n note: before.note,\n suggestion: before.suggestion,\n });\n }\n return { writes, clears };\n}\n\n/** The app's cached verdict list with `written` in place of what was said before — a\n * save answers with the rows it wrote, so a page patches its copy instead of\n * downloading every verdict again after each click. Any spelling with `locale` and\n * `key` (the API's or the kit's). */\nexport function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[] {\n const id = (r: TranslationReviewKey) => `${r.locale}|${r.key}`;\n const replaced = new Set(written.map(id));\n return [...prev.filter((r) => !replaced.has(id(r))), ...written];\n}\n\n/** The cached list without the verdicts on `cleared`. */\nexport function dropReviews<T extends TranslationReviewKey>(\n prev: readonly T[],\n cleared: readonly TranslationReviewKey[],\n): T[] {\n const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));\n return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));\n}\n\n// ── Export ──────────────────────────────────────────────────────────────────────\n\n/**\n * One entry of the \"Export corrections\" file — enough for a developer to apply the change\n * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a\n * FILE, the one keksdose and kastlan already write, beside an API spelled that way.\n */\nexport interface TranslationCorrection {\n locale: string;\n key: string;\n status: ReviewStatus;\n current: string;\n reference: string;\n suggestion: string | null;\n note: string | null;\n reviewer: string | null;\n reviewed_at: string | null;\n}\n\n/** The rows sent back (verdict NEEDS_CHANGE — a suggested translation for a missing\n * string included), as the export file lists them. */\nexport function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[] {\n return rows\n .filter((row) => row.review?.verdict === \"NEEDS_CHANGE\")\n .map((row) => ({\n locale: row.locale,\n key: row.key,\n status: row.status,\n current: row.text,\n reference: row.reference,\n suggestion: row.review?.suggestion ?? null,\n note: row.review?.note ?? null,\n reviewer: row.review?.reviewerName ?? null,\n reviewed_at: row.review?.reviewedAt ?? null,\n }));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/translation-review.ts"],"sourcesContent":["/**\n * The pure half of an app's translation review (H1 in docs/i18n-harmonization.md): from\n * the strings the app ships and the verdicts its server stores to one row per (locale,\n * key) with a status — keksdose's `features/translations/translation-catalogue.ts` and\n * kastlan's copy of it, which had grown apart by a fifth status and a second kind of key.\n *\n * WHY THE STRINGS ARE THE CATALOGUE. Nothing on the server lists which strings exist: a\n * verdict only says what somebody thought of one. So the rows are built from what the app\n * ships — its locale bundles, kastlan's document texts, the kit's own words\n * (`kitLabelStrings`) — and the review can never offer a string users do not see. A\n * verdict is stored WITH the wording it was given on, and a string that changed since is\n * noticed by comparing the two: `changed`, read it again, whatever the verdict was.\n *\n * WHAT STAYS IN THE APP. Which locale is read against which (keksdose: everything against\n * English, English against de-CH; kastlan: against German, German against English), where\n * the strings come from, who may review what (the locales and areas its server answers\n * with), and every request. The kit takes those as input and never derives them.\n *\n * camelCase, like the rest of the kit. The contract's wire shape is snake_case\n * (`reference_text`, `reviewer_name`, `reviewed_at`); {@link fromApiReview} and\n * {@link toApiWrite} are the two lines each app would otherwise write — both apps speak\n * the same contract, so the mapping is written once, here.\n */\n\n// ── Shapes ──────────────────────────────────────────────────────────────────────\n\nexport type TranslationVerdict = \"APPROVED\" | \"NEEDS_CHANGE\";\n\n/** One stored verdict: what a reviewer said about one string in one locale. */\nexport interface TranslationReview {\n locale: string;\n key: string;\n /** The wording that was judged — compared with today's to notice a change. Empty for\n * a string the locale lacked (a suggested translation for a `missing` row). */\n text: string;\n /** The reference wording at the time. `null` from a server that did not store it. */\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n /** The reviewer's wording — or, for a missing string, the translation to add. */\n suggestion: string | null;\n /** `null` once the reviewer's account has been erased. */\n reviewerName: string | null;\n /** ISO timestamp. */\n reviewedAt: string;\n}\n\n/** What a save sends: the verdict plus the wording it is given on. */\nexport interface TranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** What a reset sends: back to \"unreviewed\". */\nexport interface TranslationReviewKey {\n locale: string;\n key: string;\n}\n\n/**\n * Where a string stands, in the order a reviewer works through them: what is not there\n * at all, what nobody has read, what changed after it was read, what was sent back, and\n * what is done. `missing` never occurs for an app whose locales have every key\n * (keksdose); kastlan's French lacks 172 screen texts.\n */\nexport type ReviewStatus = \"missing\" | \"unreviewed\" | \"changed\" | \"needs_change\" | \"approved\";\n\nexport const REVIEW_STATUSES: readonly ReviewStatus[] = [\n \"missing\",\n \"unreviewed\",\n \"changed\",\n \"needs_change\",\n \"approved\",\n];\n\nexport interface TranslationRow {\n /** `${locale}|${key}` — unique across every locale on a page. A bar, not a colon:\n * kastlan's keys contain colons (`common:actions.save`). */\n id: string;\n key: string;\n /** The key's group: `budget` for `budget.rta`, `common` for `common:actions.save`,\n * `kit` for the kit's own words. See {@link keyNamespace}. */\n namespace: string;\n /** Where the string comes from, in the app's own words — kastlan's `screen` and\n * `documents`, keksdose's one source. `\"\"` when the app does not say. */\n source: string;\n locale: string;\n /** The wording users see today; empty when the locale lacks the string. */\n text: string;\n /** The same key in the reference locale. */\n reference: string;\n status: ReviewStatus;\n review: TranslationReview | null;\n /** The interpolations and tags differ from the reference's — `{{count}}` dropped, a\n * `<1>` link lost, a `{date}` renamed. A machine can check that much, so it does,\n * before a person reads. Never set on a missing string: it has nothing to lose. */\n placeholderMismatch: boolean;\n}\n\n// ── The wire contract ───────────────────────────────────────────────────────────\n\n/** A verdict as `GET /translations/reviews` returns it (H1's shared contract). Typed\n * structurally, so an app's generated OpenAPI type fits without a cast. */\nexport interface ApiTranslationReview {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n reviewer_name: string | null;\n reviewed_at: string;\n}\n\n/** One item of `PUT /translations/reviews`. */\nexport interface ApiTranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** A verdict from the server, in the kit's spelling. */\nexport function fromApiReview(review: ApiTranslationReview): TranslationReview {\n return {\n locale: review.locale,\n key: review.key,\n text: review.text,\n referenceText: review.reference_text,\n verdict: review.verdict,\n note: review.note,\n suggestion: review.suggestion,\n reviewerName: review.reviewer_name,\n reviewedAt: review.reviewed_at,\n };\n}\n\n/** A save, in the server's spelling. `TranslationReviewKey` needs no mapping — it is\n * `{ locale, key }` on both sides. */\nexport function toApiWrite(write: TranslationReviewWrite): ApiTranslationReviewWrite {\n return {\n locale: write.locale,\n key: write.key,\n text: write.text,\n reference_text: write.referenceText,\n verdict: write.verdict,\n note: write.note,\n suggestion: write.suggestion,\n };\n}\n\n// ── Strings ─────────────────────────────────────────────────────────────────────\n\n/**\n * A nested bundle as `key → string`, in the bundle's own order. Non-string leaves are\n * skipped rather than stringified into something nobody wrote.\n *\n * `prefix` is put in front of every key AS IS, so it names the separator too:\n * `flattenStrings(bundle)` is keksdose's `budget.rta`, `flattenStrings(tree, \"common:\")`\n * kastlan's `common:actions.save`, and `flattenStrings(kitLabelStrings(labels), \"kit.\")`\n * the kit's own words as `kit.combobox.resultCount(3)` — already flat, so only the prefix\n * is added.\n */\nexport function flattenStrings(tree: Readonly<Record<string, unknown>>, prefix = \"\"): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(tree)) {\n const path = `${prefix}${key}`;\n if (typeof value === \"string\") out[path] = value;\n else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n Object.assign(out, flattenStrings(value as Record<string, unknown>, `${path}.`));\n }\n }\n return out;\n}\n\n/**\n * The key's group, for the namespace filter: the part before a colon when there is one\n * (kastlan's `common:actions.save` → `common`, `doc_text.invoice:due` →\n * `doc_text.invoice`), else the first dotted segment (keksdose's `budget.rta` →\n * `budget`, the kit's `kit.combobox.resultCount(3)` → `kit`). Only the part before a\n * sample's `(` is looked at, so a colon inside one cannot move the split.\n */\nexport function keyNamespace(key: string): string {\n const head = key.split(\"(\", 1)[0];\n const colon = head.indexOf(\":\");\n return colon >= 0 ? head.slice(0, colon) : head.split(\".\", 1)[0];\n}\n\n// i18next `{{x}}` (formatting options ignored: `{{x, number}}` is `{{x}}`); a Python\n// format field `{x}` / `{0}` / `{amount:.2f}` (kastlan's document texts), but not a\n// LaTeX argument (`\\textbf{x}` — a letter or backslash right before the brace); a\n// numbered or named tag (`<1>`, `<strong>`), not prose in angle brackets (\"<deleted\n// user>\"); and a nested `$t(key)`. `{{x}}` is tried first at its position and consumed\n// whole, so it is never counted a second time as `{x}`.\nconst PLACEHOLDER_RE =\n /\\{\\{\\s*([^}\\s,]+)[^}]*\\}\\}|(?<![\\\\A-Za-z])\\{([a-z_][a-z0-9_]*|\\d+)(?:[!:][^{}]*)?\\}|<\\/?(\\d+|[a-z][a-z0-9]*)\\s*\\/?>|\\$t\\(([^)]+)\\)/g;\n\n/** The placeholders in a string, as a sorted multiset: `{{count}}`, `{date}`, `<1>`,\n * `$t(common.save)`. */\nexport function placeholderTokens(text: string): string[] {\n const found: string[] = [];\n for (const m of text.matchAll(PLACEHOLDER_RE)) {\n if (m[1]) found.push(`{{${m[1]}}}`);\n else if (m[2]) found.push(`{${m[2]}}`);\n else if (m[3]) found.push(`<${m[3]}>`);\n else if (m[4]) found.push(`$t(${m[4]})`);\n }\n return found.sort();\n}\n\n/** Whether `text` has other placeholders than `reference` — dropped, renamed or added.\n * Moving one is fine: a translation reorders a sentence. */\nexport function placeholderMismatch(text: string, reference: string): boolean {\n const a = placeholderTokens(text);\n const b = placeholderTokens(reference);\n return a.length !== b.length || a.some((p, i) => p !== b[i]);\n}\n\n// ── Status ──────────────────────────────────────────────────────────────────────\n\n/**\n * A verdict counts only for the wording it was given on. If the string or its reference\n * changed since, it is `changed` — read it again — whatever the verdict was. A string the\n * locale lacks is `missing` until somebody suggests its translation (stored as\n * NEEDS_CHANGE on the empty text, so it reads `needs_change`, and `changed` once a\n * developer adds the string).\n */\nexport function reviewStatus(\n review: TranslationReview | null | undefined,\n text: string,\n reference: string,\n): ReviewStatus {\n if (!review) return text === \"\" && reference !== \"\" ? \"missing\" : \"unreviewed\";\n if (review.text !== text) return \"changed\";\n if (review.referenceText != null && review.referenceText !== reference) return \"changed\";\n return review.verdict === \"APPROVED\" ? \"approved\" : \"needs_change\";\n}\n\n/** Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`\n * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not. */\nexport function keyInArea(key: string, area: string): boolean {\n return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);\n}\n\n/**\n * Whether `key` lies in one of `areas` — the scope a reviewer may be limited to\n * (keksdose: `[\"legal\"]`, a lawyer who judges the Imprint, the Privacy Policy and the\n * Terms and sees nothing else). `null`/`undefined` is every area. The server applies the\n * same rule and refuses verdicts outside; this decides only what is listed.\n */\nexport function keyInAreas(\n key: string,\n areas: readonly string[] | null | undefined,\n inArea: (key: string, area: string) => boolean = keyInArea,\n): boolean {\n return !areas || areas.some((area) => inArea(key, area));\n}\n\nexport interface TranslationRowsInput {\n locale: string;\n /** The locale's strings, `key → text` ({@link flattenStrings}). */\n strings: Readonly<Record<string, string>>;\n /** The reference locale's strings — whichever locale THIS app reads `locale` against. */\n reference: Readonly<Record<string, string>>;\n /** The verdicts: this locale's by key, or the whole list as the server sent it (other\n * locales' verdicts are skipped). */\n reviews?: ReadonlyMap<string, TranslationReview> | readonly TranslationReview[];\n /** Default {@link keyNamespace}. */\n namespaceOf?: (key: string) => string;\n /** The row's source, an app-defined word (kastlan: `doc_text.` keys are `documents`,\n * the rest `screen`). Default: none (`\"\"`). */\n sourceOf?: (key: string) => string;\n /** Keep only keys inside these areas ({@link keyInAreas}); `null` keeps every key. */\n areas?: readonly string[] | null;\n /** Default {@link keyInArea}. */\n inArea?: (key: string, area: string) => boolean;\n}\n\n/**\n * Every key of one locale, with its status: the reference's keys first, in its order, so\n * rows read the way the app's authors wrote them; a key only this locale has is appended\n * rather than dropped (a parity test should forbid it, but the page should not lie if one\n * slips through).\n */\nexport function translationRows({\n locale,\n strings,\n reference,\n reviews,\n namespaceOf: groupOf = keyNamespace,\n sourceOf,\n areas,\n inArea,\n}: TranslationRowsInput): TranslationRow[] {\n const byKey = new Map<string, TranslationReview>();\n if (reviews instanceof Map) {\n for (const [key, review] of reviews) byKey.set(key, review);\n } else if (reviews) {\n for (const review of reviews as readonly TranslationReview[]) {\n if (review.locale === locale) byKey.set(review.key, review);\n }\n }\n const keys = Object.keys(reference);\n for (const key of Object.keys(strings)) if (!(key in reference)) keys.push(key);\n return keys\n .filter((key) => keyInAreas(key, areas, inArea))\n .map((key) => {\n const text = strings[key] ?? \"\";\n const ref = reference[key] ?? \"\";\n const review = byKey.get(key) ?? null;\n return {\n id: `${locale}|${key}`,\n key,\n namespace: groupOf(key),\n source: sourceOf?.(key) ?? \"\",\n locale,\n text,\n reference: ref,\n status: reviewStatus(review, text, ref),\n review,\n placeholderMismatch: text !== \"\" && placeholderMismatch(text, ref),\n };\n });\n}\n\n/** How many rows stand where, and how many there are. */\nexport type TranslationSummary = Record<ReviewStatus, number> & { total: number };\n\nexport function summariseRows(rows: readonly TranslationRow[]): TranslationSummary {\n const summary: TranslationSummary = {\n missing: 0,\n unreviewed: 0,\n changed: 0,\n needs_change: 0,\n approved: 0,\n total: rows.length,\n };\n for (const row of rows) summary[row.status] += 1;\n return summary;\n}\n\n// ── Filtering ───────────────────────────────────────────────────────────────────\n\n/** The panel's filters. `\"all\"` switches one off. */\nexport interface TranslationReviewFilter {\n status: ReviewStatus | \"all\";\n source: string;\n namespace: string;\n /** Searched in the key, the text and the reference, case-insensitively. */\n query: string;\n placeholdersOnly: boolean;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter = {\n status: \"all\",\n source: \"all\",\n namespace: \"all\",\n query: \"\",\n placeholdersOnly: false,\n};\n\nexport function filterTranslationRows(\n rows: readonly TranslationRow[],\n filter: Partial<TranslationReviewFilter>,\n): TranslationRow[] {\n const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };\n const q = f.query.trim().toLowerCase();\n return rows.filter(\n (row) =>\n (f.status === \"all\" || row.status === f.status) &&\n (f.source === \"all\" || row.source === f.source) &&\n (f.namespace === \"all\" || row.namespace === f.namespace) &&\n (!f.placeholdersOnly || row.placeholderMismatch) &&\n (!q ||\n row.key.toLowerCase().includes(q) ||\n row.text.toLowerCase().includes(q) ||\n row.reference.toLowerCase().includes(q)),\n );\n}\n\n// ── Grouping ────────────────────────────────────────────────────────────────────\n\n/** One group of rows — an area, a source — and how they stand. */\nexport interface TranslationRowGroup {\n key: string;\n rows: TranslationRow[];\n summary: TranslationSummary;\n}\n\n/**\n * Rows in groups, each group where its first row is — the reference's order, so the areas\n * read the way the app's authors wrote the bundle — and each group's rows in their own\n * order. keksdose live #377: a reviewer on a phone works area by area (\"the budget\n * strings, then the legal ones\"), and a flat list of 3859 rows is not a way to see\n * where an area stands.\n *\n * `groupOf` names a row's group: `(row) => row.namespace` for the areas,\n * `(row) => row.source` for kastlan's screen texts and documents.\n */\nexport function groupTranslationRows(\n rows: readonly TranslationRow[],\n groupOf: (row: TranslationRow) => string,\n): TranslationRowGroup[] {\n const byKey = new Map<string, TranslationRow[]>();\n for (const row of rows) {\n const key = groupOf(row);\n const list = byKey.get(key);\n if (list) list.push(row);\n else byKey.set(key, [row]);\n }\n return [...byKey].map(([key, list]) => ({ key, rows: list, summary: summariseRows(list) }));\n}\n\n/**\n * The rows a group's \"Approve the unreviewed\" takes: the ones nobody has given a verdict\n * and that have a text. Not `changed` — a string that moved since it was judged asks to\n * be READ again, which a bulk action is the opposite of — and not `missing`, which has\n * nothing to approve.\n */\nexport function unreviewedRows(rows: readonly TranslationRow[]): TranslationRow[] {\n return rows.filter((row) => row.status === \"unreviewed\" && row.text !== \"\");\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────────\n\n/** What gets stored for a verdict: the wording it was given on, so a later change of the\n * string (or of its reference) shows up as `changed`. */\nexport function reviewWrite(\n row: TranslationRow,\n verdict: TranslationVerdict,\n note: string | null = null,\n suggestion: string | null = null,\n): TranslationReviewWrite {\n return {\n locale: row.locale,\n key: row.key,\n text: row.text,\n referenceText: row.reference,\n verdict,\n note,\n suggestion,\n };\n}\n\n/** What takes a set of verdicts back: see {@link reviewUndo}. */\nexport interface TranslationReviewUndo {\n /** The verdicts the rows had before, to store again — each on the wording it was\n * given on, so a row that was `changed` reads `changed` again. */\n writes: TranslationReviewWrite[];\n /** The rows that had no verdict before: back to unreviewed. */\n clears: TranslationReviewKey[];\n}\n\n/**\n * How to take back what was just said about `rows` — the rows AS THEY WERE before the\n * write, with the verdicts they carried then. The panel's Undo toast (0.25, keksdose live\n * #377) runs it: a swipe on a phone approves in one movement, and a movement can be a\n * mistake.\n *\n * A row that had a verdict gets it stored again (its note, its suggestion, the wording it\n * was judged on); one that had none is cleared. The server stamps the restored verdict\n * with the person undoing and the time, as it stamps every write — the contract has no\n * way to write a verdict in somebody else's name, and should not.\n */\nexport function reviewUndo(rows: readonly TranslationRow[]): TranslationReviewUndo {\n const writes: TranslationReviewWrite[] = [];\n const clears: TranslationReviewKey[] = [];\n for (const row of rows) {\n const before = row.review;\n if (!before) {\n clears.push({ locale: row.locale, key: row.key });\n continue;\n }\n writes.push({\n locale: row.locale,\n key: row.key,\n text: before.text,\n referenceText: before.referenceText,\n verdict: before.verdict,\n note: before.note,\n suggestion: before.suggestion,\n });\n }\n return { writes, clears };\n}\n\n/** The app's cached verdict list with `written` in place of what was said before — a\n * save answers with the rows it wrote, so a page patches its copy instead of\n * downloading every verdict again after each click. Any spelling with `locale` and\n * `key` (the API's or the kit's). */\nexport function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[] {\n const id = (r: TranslationReviewKey) => `${r.locale}|${r.key}`;\n const replaced = new Set(written.map(id));\n return [...prev.filter((r) => !replaced.has(id(r))), ...written];\n}\n\n/** The cached list without the verdicts on `cleared`. */\nexport function dropReviews<T extends TranslationReviewKey>(\n prev: readonly T[],\n cleared: readonly TranslationReviewKey[],\n): T[] {\n const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));\n return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));\n}\n\n// ── Swipe bindings ──────────────────────────────────────────────────────────────\n\n/**\n * What a phone card's swipe can be bound to in `TranslationReviewPanel`'s `swipe`\n * (keksdose — Marcel's live #377 rework, 2026-10-03: *\"Add the swipe options to be shown\n * also to the settings /settings#interaction area where the other swipe options are\n * defined\"*). keksdose lets the user bind every list's swipes there — per side, a primary\n * action at the first threshold and a secondary one at a longer drag — and lists each\n * surface's ids in its `SWIPE_ACTIONS`; this is the translation list's, in the order a\n * settings page offers them.\n *\n * - `approve` — the string is right as it stands. With the panel's Undo toast.\n * - `edit` — open the editor with the cursor in the wording: \"Needs a change\" (or\n * \"Suggest translation\" on a missing string). Writes nothing; the editor does.\n * - `clear` — take the verdict back: unreviewed again. With the Undo toast, which stores\n * the verdict again.\n *\n * THERE IS NO SEPARATE \"needs a change\". The editor has no verdict to preselect: Approve\n * and \"Needs a change\" stand side by side in it, and \"Needs a change\" takes a note or a\n * different wording — a verdict without either tells whoever fixes the string nothing, so\n * a swipe never sends one blind (0.25). The one honest thing a \"needs a change\" swipe can\n * do is therefore open the editor with the cursor in the wording — which is `edit`, and\n * `edit` already says \"Needs a change\" on the card. A second id doing the same would arm\n * a second threshold that does what the first already did: the duplicate keksdose's\n * `resolveSwipePlan` drops for exactly that reason.\n */\nexport const TRANSLATION_REVIEW_SWIPE_ACTIONS = [\"approve\", \"edit\", \"clear\"] as const;\n\n/** One of {@link TRANSLATION_REVIEW_SWIPE_ACTIONS}. */\nexport type TranslationReviewSwipeAction = (typeof TRANSLATION_REVIEW_SWIPE_ACTIONS)[number];\n\n/**\n * Which swipe does what on a phone card — `TranslationReviewPanel`'s `swipe`. Each side is\n * an ordered ladder, exactly as `SwipeableRow` and DataTable's `mobileSwipeActions` model\n * one: index 0 commits at the first threshold, index 1 at the longer drag (and so on — the\n * thresholds spread evenly over the card's width). A side left out, or empty, does not\n * swipe.\n *\n * LOGICAL SIDES: `end` is a drag toward the reading end of the line — right in a\n * left-to-right page, left in a right-to-left one — and `start` the other way, so one\n * binding holds in both directions. An app whose settings store physical sides (keksdose's\n * `right`/`left`) hands `right` as `end` for a left-to-right page.\n */\nexport interface TranslationReviewSwipeBinding {\n /** A drag toward the reading START, nearest threshold first. */\n start?: readonly TranslationReviewSwipeAction[];\n /** A drag toward the reading END, nearest threshold first. */\n end?: readonly TranslationReviewSwipeAction[];\n}\n\n/** What `swipe={true}` means — 0.25's fixed mapping: approve toward the end, open the\n * editor (\"Needs a change\") toward the start. A settings page's \"reset\" goes back here. */\nexport const DEFAULT_TRANSLATION_REVIEW_SWIPE: TranslationReviewSwipeBinding = Object.freeze({\n start: Object.freeze([\"edit\"] as const),\n end: Object.freeze([\"approve\"] as const),\n});\n\n/** The ladders one row actually offers, from a {@link TranslationReviewSwipeBinding}. */\nexport interface TranslationReviewSwipePlan {\n start: TranslationReviewSwipeAction[];\n end: TranslationReviewSwipeAction[];\n}\n\n/**\n * The binding resolved against one row — keksdose's `resolveSwipePlan`, for this list.\n * An id the row cannot offer DROPS OUT and the ladder closes up behind it, so the second\n * stage becomes the first rather than the side going dead behind an unreachable first\n * stage (or arming a threshold that does nothing):\n *\n * - `approve`: not on a missing string (nothing to approve) or an approved one (it would\n * say it again) — the rows that have no approve button either;\n * - `clear`: not without `canClear` (the app passed no `onClear`), and not on a row with\n * no verdict to take back (unreviewed; missing with no suggestion);\n * - `edit`: on every row;\n * - an id named twice on one side: the second time (a longer drag to the same effect);\n * - an id this version does not know (a binding stored by another version, an app's own\n * \"none\" slot): dropped like one that does not apply.\n *\n * Only what the ROW decides. Whether the panel may write at all — `readOnly`, no\n * `onSave`, a write lock, the row's own write still out — is the panel's, and then no\n * side swipes.\n */\nexport function translationReviewSwipePlan(\n binding: TranslationReviewSwipeBinding,\n row: TranslationRow,\n { canClear = false }: { canClear?: boolean } = {},\n): TranslationReviewSwipePlan {\n const offers = (id: TranslationReviewSwipeAction): boolean => {\n switch (id) {\n case \"approve\":\n return row.text !== \"\" && row.status !== \"approved\";\n case \"edit\":\n return true;\n case \"clear\":\n return canClear && row.review !== null;\n default:\n return false;\n }\n };\n const side = (ids: readonly TranslationReviewSwipeAction[] | undefined): TranslationReviewSwipeAction[] => {\n const out: TranslationReviewSwipeAction[] = [];\n for (const id of ids ?? []) {\n if (!out.includes(id) && offers(id)) out.push(id);\n }\n return out;\n };\n return { start: side(binding.start), end: side(binding.end) };\n}\n\n// ── Export ──────────────────────────────────────────────────────────────────────\n\n/**\n * One entry of the \"Export corrections\" file — enough for a developer to apply the change\n * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a\n * FILE, the one keksdose and kastlan already write, beside an API spelled that way.\n */\nexport interface TranslationCorrection {\n locale: string;\n key: string;\n status: ReviewStatus;\n current: string;\n reference: string;\n suggestion: string | null;\n note: string | null;\n reviewer: string | null;\n reviewed_at: string | null;\n}\n\n/** The rows sent back (verdict NEEDS_CHANGE — a suggested translation for a missing\n * string included), as the export file lists them. */\nexport function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[] {\n return rows\n .filter((row) => row.review?.verdict === \"NEEDS_CHANGE\")\n .map((row) => ({\n locale: row.locale,\n key: row.key,\n status: row.status,\n current: row.text,\n reference: row.reference,\n suggestion: row.review?.suggestion ?? null,\n note: row.review?.note ?? null,\n reviewer: row.review?.reviewerName ?? null,\n reviewed_at: row.review?.reviewedAt ?? null,\n }));\n}\n"],"mappings":";AAwEO,MAAM,kBAA2C;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAsDO,SAAS,cAAc,QAAiD;AAC7E,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,eAAe,OAAO;AAAA,IACtB,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,IACnB,cAAc,OAAO;AAAA,IACrB,YAAY,OAAO;AAAA,EACrB;AACF;AAIO,SAAS,WAAW,OAA0D;AACnF,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,gBAAgB,MAAM;AAAA,IACtB,SAAS,MAAM;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM;AAAA,EACpB;AACF;AAcO,SAAS,eAAe,MAAyC,SAAS,IAA4B;AAC3G,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,UAAM,OAAO,GAAG,MAAM,GAAG,GAAG;AAC5B,QAAI,OAAO,UAAU,SAAU,KAAI,IAAI,IAAI;AAAA,aAClC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACpE,aAAO,OAAO,KAAK,eAAe,OAAkC,GAAG,IAAI,GAAG,CAAC;AAAA,IACjF;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,aAAa,KAAqB;AAChD,QAAM,OAAO,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;AAChC,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,SAAO,SAAS,IAAI,KAAK,MAAM,GAAG,KAAK,IAAI,KAAK,MAAM,KAAK,CAAC,EAAE,CAAC;AACjE;AAQA,MAAM,iBACJ;AAIK,SAAS,kBAAkB,MAAwB;AACxD,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,KAAK,SAAS,cAAc,GAAG;AAC7C,QAAI,EAAE,CAAC,EAAG,OAAM,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI;AAAA,aACzB,EAAE,CAAC,EAAG,OAAM,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG;AAAA,aAC5B,EAAE,CAAC,EAAG,OAAM,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG;AAAA,aAC5B,EAAE,CAAC,EAAG,OAAM,KAAK,MAAM,EAAE,CAAC,CAAC,GAAG;AAAA,EACzC;AACA,SAAO,MAAM,KAAK;AACpB;AAIO,SAAS,oBAAoB,MAAc,WAA4B;AAC5E,QAAM,IAAI,kBAAkB,IAAI;AAChC,QAAM,IAAI,kBAAkB,SAAS;AACrC,SAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,EAAE,CAAC,CAAC;AAC7D;AAWO,SAAS,aACd,QACA,MACA,WACc;AACd,MAAI,CAAC,OAAQ,QAAO,SAAS,MAAM,cAAc,KAAK,YAAY;AAClE,MAAI,OAAO,SAAS,KAAM,QAAO;AACjC,MAAI,OAAO,iBAAiB,QAAQ,OAAO,kBAAkB,UAAW,QAAO;AAC/E,SAAO,OAAO,YAAY,aAAa,aAAa;AACtD;AAIO,SAAS,UAAU,KAAa,MAAuB;AAC5D,SAAO,QAAQ,QAAQ,IAAI,WAAW,GAAG,IAAI,GAAG,KAAK,IAAI,WAAW,GAAG,IAAI,GAAG;AAChF;AAQO,SAAS,WACd,KACA,OACA,SAAiD,WACxC;AACT,SAAO,CAAC,SAAS,MAAM,KAAK,CAAC,SAAS,OAAO,KAAK,IAAI,CAAC;AACzD;AA4BO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa,UAAU;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF,GAA2C;AACzC,QAAM,QAAQ,oBAAI,IAA+B;AACjD,MAAI,mBAAmB,KAAK;AAC1B,eAAW,CAAC,KAAK,MAAM,KAAK,QAAS,OAAM,IAAI,KAAK,MAAM;AAAA,EAC5D,WAAW,SAAS;AAClB,eAAW,UAAU,SAAyC;AAC5D,UAAI,OAAO,WAAW,OAAQ,OAAM,IAAI,OAAO,KAAK,MAAM;AAAA,IAC5D;AAAA,EACF;AACA,QAAM,OAAO,OAAO,KAAK,SAAS;AAClC,aAAW,OAAO,OAAO,KAAK,OAAO,EAAG,KAAI,EAAE,OAAO,WAAY,MAAK,KAAK,GAAG;AAC9E,SAAO,KACJ,OAAO,CAAC,QAAQ,WAAW,KAAK,OAAO,MAAM,CAAC,EAC9C,IAAI,CAAC,QAAQ;AACZ,UAAM,OAAO,QAAQ,GAAG,KAAK;AAC7B,UAAM,MAAM,UAAU,GAAG,KAAK;AAC9B,UAAM,SAAS,MAAM,IAAI,GAAG,KAAK;AACjC,WAAO;AAAA,MACL,IAAI,GAAG,MAAM,IAAI,GAAG;AAAA,MACpB;AAAA,MACA,WAAW,QAAQ,GAAG;AAAA,MACtB,QAAQ,WAAW,GAAG,KAAK;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,QAAQ,aAAa,QAAQ,MAAM,GAAG;AAAA,MACtC;AAAA,MACA,qBAAqB,SAAS,MAAM,oBAAoB,MAAM,GAAG;AAAA,IACnE;AAAA,EACF,CAAC;AACL;AAKO,SAAS,cAAc,MAAqD;AACjF,QAAM,UAA8B;AAAA,IAClC,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,cAAc;AAAA,IACd,UAAU;AAAA,IACV,OAAO,KAAK;AAAA,EACd;AACA,aAAW,OAAO,KAAM,SAAQ,IAAI,MAAM,KAAK;AAC/C,SAAO;AACT;AAcO,MAAM,oCAA6D;AAAA,EACxE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,OAAO;AAAA,EACP,kBAAkB;AACpB;AAEO,SAAS,sBACd,MACA,QACkB;AAClB,QAAM,IAAI,EAAE,GAAG,mCAAmC,GAAG,OAAO;AAC5D,QAAM,IAAI,EAAE,MAAM,KAAK,EAAE,YAAY;AACrC,SAAO,KAAK;AAAA,IACV,CAAC,SACE,EAAE,WAAW,SAAS,IAAI,WAAW,EAAE,YACvC,EAAE,WAAW,SAAS,IAAI,WAAW,EAAE,YACvC,EAAE,cAAc,SAAS,IAAI,cAAc,EAAE,eAC7C,CAAC,EAAE,oBAAoB,IAAI,yBAC3B,CAAC,KACA,IAAI,IAAI,YAAY,EAAE,SAAS,CAAC,KAChC,IAAI,KAAK,YAAY,EAAE,SAAS,CAAC,KACjC,IAAI,UAAU,YAAY,EAAE,SAAS,CAAC;AAAA,EAC5C;AACF;AAqBO,SAAS,qBACd,MACA,SACuB;AACvB,QAAM,QAAQ,oBAAI,IAA8B;AAChD,aAAW,OAAO,MAAM;AACtB,UAAM,MAAM,QAAQ,GAAG;AACvB,UAAM,OAAO,MAAM,IAAI,GAAG;AAC1B,QAAI,KAAM,MAAK,KAAK,GAAG;AAAA,QAClB,OAAM,IAAI,KAAK,CAAC,GAAG,CAAC;AAAA,EAC3B;AACA,SAAO,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,OAAO,EAAE,KAAK,MAAM,MAAM,SAAS,cAAc,IAAI,EAAE,EAAE;AAC5F;AAQO,SAAS,eAAe,MAAmD;AAChF,SAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,gBAAgB,IAAI,SAAS,EAAE;AAC5E;AAMO,SAAS,YACd,KACA,SACA,OAAsB,MACtB,aAA4B,MACJ;AACxB,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,KAAK,IAAI;AAAA,IACT,MAAM,IAAI;AAAA,IACV,eAAe,IAAI;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAsBO,SAAS,WAAW,MAAwD;AACjF,QAAM,SAAmC,CAAC;AAC1C,QAAM,SAAiC,CAAC;AACxC,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,IAAI;AACnB,QAAI,CAAC,QAAQ;AACX,aAAO,KAAK,EAAE,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC;AAChD;AAAA,IACF;AACA,WAAO,KAAK;AAAA,MACV,QAAQ,IAAI;AAAA,MACZ,KAAK,IAAI;AAAA,MACT,MAAM,OAAO;AAAA,MACb,eAAe,OAAO;AAAA,MACtB,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO;AAAA,MACb,YAAY,OAAO;AAAA,IACrB,CAAC;AAAA,EACH;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAMO,SAAS,aAA6C,MAAoB,SAA4B;AAC3G,QAAM,KAAK,CAAC,MAA4B,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG;AAC5D,QAAM,WAAW,IAAI,IAAI,QAAQ,IAAI,EAAE,CAAC;AACxC,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO;AACjE;AAGO,SAAS,YACd,MACA,SACK;AACL,QAAM,OAAO,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;AAC/D,SAAO,KAAK,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;AAC7D;AA4BO,MAAM,mCAAmC,CAAC,WAAW,QAAQ,OAAO;AA0BpE,MAAM,mCAAkE,OAAO,OAAO;AAAA,EAC3F,OAAO,OAAO,OAAO,CAAC,MAAM,CAAU;AAAA,EACtC,KAAK,OAAO,OAAO,CAAC,SAAS,CAAU;AACzC,CAAC;AA2BM,SAAS,2BACd,SACA,KACA,EAAE,WAAW,MAAM,IAA4B,CAAC,GACpB;AAC5B,QAAM,SAAS,CAAC,OAA8C;AAC5D,YAAQ,IAAI;AAAA,MACV,KAAK;AACH,eAAO,IAAI,SAAS,MAAM,IAAI,WAAW;AAAA,MAC3C,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO,YAAY,IAAI,WAAW;AAAA,MACpC;AACE,eAAO;AAAA,IACX;AAAA,EACF;AACA,QAAM,OAAO,CAAC,QAA6F;AACzG,UAAM,MAAsC,CAAC;AAC7C,eAAW,MAAM,OAAO,CAAC,GAAG;AAC1B,UAAI,CAAC,IAAI,SAAS,EAAE,KAAK,OAAO,EAAE,EAAG,KAAI,KAAK,EAAE;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACA,SAAO,EAAE,OAAO,KAAK,QAAQ,KAAK,GAAG,KAAK,KAAK,QAAQ,GAAG,EAAE;AAC9D;AAuBO,SAAS,uBAAuB,MAA0D;AAC/F,SAAO,KACJ,OAAO,CAAC,QAAQ,IAAI,QAAQ,YAAY,cAAc,EACtD,IAAI,CAAC,SAAS;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,KAAK,IAAI;AAAA,IACT,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI;AAAA,IACb,WAAW,IAAI;AAAA,IACf,YAAY,IAAI,QAAQ,cAAc;AAAA,IACtC,MAAM,IAAI,QAAQ,QAAQ;AAAA,IAC1B,UAAU,IAAI,QAAQ,gBAAgB;AAAA,IACtC,aAAa,IAAI,QAAQ,cAAc;AAAA,EACzC,EAAE;AACN;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -1116,8 +1116,15 @@ export function DataTable<T>({
1116
1116
  label: a.label,
1117
1117
  icon: a.icon,
1118
1118
  onCommit: () => void runRowAction(a.action, row),
1119
- className: a.tone === "danger" ? "bg-[var(--danger)]" : "bg-[var(--brand)]",
1120
- armedClassName: a.tone === "danger" ? "bg-[var(--danger-hover)]" : "bg-[var(--brand-hover)]",
1119
+ // Each fill's own text colour — the fills are pastels in dark mode (0.26).
1120
+ className:
1121
+ a.tone === "danger"
1122
+ ? "bg-[var(--danger)] text-[var(--danger-contrast)]"
1123
+ : "bg-[var(--brand)] text-[var(--brand-contrast)]",
1124
+ armedClassName:
1125
+ a.tone === "danger"
1126
+ ? "bg-[var(--danger-hover)] text-[var(--danger-contrast)]"
1127
+ : "bg-[var(--brand-hover)] text-[var(--brand-contrast)]",
1121
1128
  });
1122
1129
  return {
1123
1130
  start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
@@ -18,7 +18,11 @@ export interface SwipeAction {
18
18
  /** Rendered inside the panel. Icons stay the consumer's choice — this package
19
19
  * ships no opinion about which icon set an app uses. */
20
20
  icon?: ReactNode;
21
- /** Background utility class while the action is previewed but not yet armed. */
21
+ /** Background utility class while the action is previewed but not yet armed. The
22
+ * panel's text is white unless this sets a text colour too — and a fill that turns
23
+ * light in dark mode needs one: pass the fill's contrast token
24
+ * (`bg-[var(--danger)] text-[var(--danger-contrast)]`; also `--brand-contrast`,
25
+ * `--success-contrast`), as the kit's own swipes do since 0.26. */
22
26
  className: string;
23
27
  /** Background utility class once the drag has passed this action's threshold. */
24
28
  armedClassName: string;
@@ -94,6 +94,10 @@ export interface TranslationReviewLabels {
94
94
  /** 0.25: asked first when a group's bulk approve is larger than a page — more than the
95
95
  * reviewer can have had on screen. `group` is the group's name. */
96
96
  confirmGroup: (count: number, group: string) => string;
97
+ /** The Undo toast after a swipe bound to `clear` took a verdict back (keksdose, live
98
+ * #377 rework: the swipes bound in its settings) — one string, or a batch of them.
99
+ * The swipe itself is named by {@link reset}. */
100
+ clearedToast: (count: number) => string;
97
101
  }
98
102
 
99
103
  export const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {
@@ -150,6 +154,7 @@ export const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {
150
154
  count === 1
151
155
  ? `Approve the unreviewed string in ${group}?`
152
156
  : `Approve all ${count} unreviewed strings in ${group}, including those not on screen?`,
157
+ clearedToast: (count) => (count === 1 ? "String marked unreviewed" : `${count} strings marked unreviewed`),
153
158
  };
154
159
 
155
160
  /** A status's word, from the namespace's five. */