@eifi1/ui-kit 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -12
- package/dist/admin/admin-area-labels.d.ts +9 -0
- package/dist/admin/admin-area-labels.js +5 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.d.ts +16 -1
- package/dist/admin/admin-overview.js +12 -1
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/mail-test-card.d.ts +3 -2
- package/dist/admin/mail-test-card.js +3 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/components/appearance-labels.d.ts +10 -0
- package/dist/components/appearance-labels.js +6 -1
- package/dist/components/appearance-labels.js.map +1 -1
- package/dist/components/data-table-column-settings.js +2 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-head.js +2 -2
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +18 -2
- package/dist/components/data-table-row-actions.js +86 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-shared.js +9 -2
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table.d.ts +43 -2
- package/dist/components/data-table.js +9 -4
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-format-setting.d.ts +29 -0
- package/dist/components/date-format-setting.js +36 -0
- package/dist/components/date-format-setting.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +8 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tabs.d.ts +3 -2
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -0
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-labels.d.ts +5 -0
- package/dist/components/translation-review-labels.js +2 -0
- package/dist/components/translation-review-labels.js.map +1 -1
- package/dist/components/translation-review.d.ts +18 -8
- package/dist/components/translation-review.js +64 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/write-lock-labels.js +2 -1
- package/dist/components/write-lock-labels.js.map +1 -1
- package/dist/components/write-lock.d.ts +33 -3
- package/dist/components/write-lock.js +9 -0
- package/dist/components/write-lock.js.map +1 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +4 -2
- package/dist/feedback/feedback-row-detail.js +17 -26
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +18 -0
- package/dist/feedback/feedback-swipe.js +10 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/i18n/german.js +41 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +12 -1
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +40 -6
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +41 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +41 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +41 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +41 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +41 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +3 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/date-format.d.ts +81 -0
- package/dist/lib/date-format.js +56 -0
- package/dist/lib/date-format.js.map +1 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/lib/format.js +5 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/notifications/device-zone-report.d.ts +27 -0
- package/dist/notifications/device-zone-report.js +24 -0
- package/dist/notifications/device-zone-report.js.map +1 -0
- package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
- package/dist/notifications/feedback-notification-types-labels.js +4 -1
- package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
- package/dist/notifications/feedback-notification-types.d.ts +11 -5
- package/dist/notifications/feedback-notification-types.js +2 -1
- package/dist/notifications/feedback-notification-types.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +33 -0
- package/dist/notifications/notification-bell.js +74 -0
- package/dist/notifications/notification-bell.js.map +1 -0
- package/dist/notifications/notification-inbox.d.ts +77 -0
- package/dist/notifications/notification-inbox.js +206 -0
- package/dist/notifications/notification-inbox.js.map +1 -0
- package/dist/notifications/notification-types-setting.d.ts +6 -4
- package/dist/notifications/notification-types-setting.js +103 -45
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +19 -7
- package/dist/notifications/notifications-labels.d.ts +41 -7
- package/dist/notifications/notifications-labels.js +18 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +20 -3
- package/dist/notifications/push-devices-setting.js +93 -33
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.d.ts +108 -0
- package/dist/notifications/push-worker.js +62 -0
- package/dist/notifications/push-worker.js.map +1 -0
- package/dist/notifications/quiet-hours-setting.d.ts +29 -17
- package/dist/notifications/quiet-hours-setting.js +9 -41
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/time-zone-select.d.ts +5 -2
- package/dist/notifications/time-zone-select.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +4 -17
- package/dist/notifications/use-push-device.js +7 -15
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/local-first.d.ts +33 -0
- package/dist/offline/local-first.js +39 -0
- package/dist/offline/local-first.js.map +1 -0
- package/dist/offline/query-persistence.d.ts +26 -1
- package/dist/offline/query-persistence.js +27 -23
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline/service-worker-updates.d.ts +46 -0
- package/dist/offline/service-worker-updates.js +39 -0
- package/dist/offline/service-worker-updates.js.map +1 -0
- package/dist/offline.d.ts +5 -2
- package/dist/offline.js +11 -2
- package/dist/offline.js.map +1 -1
- package/dist/push-worker.d.ts +2 -0
- package/dist/push-worker.js +14 -0
- package/dist/push-worker.js.map +1 -0
- package/dist/shell/account-menu-text-size.d.ts +33 -0
- package/dist/shell/account-menu-text-size.js +51 -0
- package/dist/shell/account-menu-text-size.js.map +1 -0
- package/dist/shell/top-bar-actions.d.ts +6 -2
- package/dist/shell/top-bar-actions.js +1 -0
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/sync/db.d.ts +76 -0
- package/dist/sync/db.js +24 -0
- package/dist/sync/db.js.map +1 -0
- package/dist/sync/engine.d.ts +124 -0
- package/dist/sync/engine.js +474 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/hooks.d.ts +30 -0
- package/dist/sync/hooks.js +67 -0
- package/dist/sync/hooks.js.map +1 -0
- package/dist/sync/testing.d.ts +28 -0
- package/dist/sync/testing.js +41 -0
- package/dist/sync/testing.js.map +1 -0
- package/dist/sync/wire.d.ts +50 -0
- package/dist/sync/wire.js +2 -0
- package/dist/sync/wire.js.map +1 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +22 -0
- package/dist/sync.js.map +1 -0
- package/dist/tour/tour-labels.js +1 -0
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +30 -0
- package/dist/tour/tour.js +50 -6
- package/dist/tour/tour.js.map +1 -1
- package/package.json +19 -1
- package/src/admin/admin-area-labels.ts +13 -0
- package/src/admin/admin-overview.tsx +37 -2
- package/src/admin/mail-test-card.tsx +7 -3
- package/src/components/appearance-labels.ts +16 -0
- package/src/components/data-table-column-settings.tsx +2 -2
- package/src/components/data-table-head.tsx +2 -2
- package/src/components/data-table-row-actions.tsx +118 -2
- package/src/components/data-table-shared.tsx +21 -2
- package/src/components/data-table.tsx +51 -6
- package/src/components/date-format-setting.tsx +64 -0
- package/src/components/swipeable-row.tsx +8 -0
- package/src/components/tabs.tsx +4 -3
- package/src/components/translation-review-editor.tsx +2 -0
- package/src/components/translation-review-labels.ts +7 -0
- package/src/components/translation-review.tsx +102 -14
- package/src/components/write-lock-labels.ts +1 -0
- package/src/components/write-lock.tsx +36 -3
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-row-detail.tsx +18 -23
- package/src/feedback/feedback-swipe.tsx +28 -1
- package/src/i18n/german.ts +44 -4
- package/src/i18n/kit-labels.tsx +18 -1
- package/src/i18n/locales/en.ts +1 -0
- package/src/i18n/locales/es.ts +44 -4
- package/src/i18n/locales/fr.ts +44 -4
- package/src/i18n/locales/hu.ts +44 -4
- package/src/i18n/locales/it.ts +44 -4
- package/src/i18n/locales/zh.ts +44 -2
- package/src/i18n/review.ts +3 -1
- package/src/index.ts +42 -0
- package/src/lib/date-format.ts +151 -0
- package/src/lib/format.ts +10 -4
- package/src/notifications/device-zone-report.ts +54 -0
- package/src/notifications/feedback-notification-types-labels.ts +10 -1
- package/src/notifications/feedback-notification-types.ts +12 -5
- package/src/notifications/notification-bell.tsx +116 -0
- package/src/notifications/notification-inbox.tsx +304 -0
- package/src/notifications/notification-types-setting.tsx +151 -55
- package/src/notifications/notification-types.ts +20 -8
- package/src/notifications/notifications-labels.ts +69 -11
- package/src/notifications/push-devices-setting.tsx +123 -34
- package/src/notifications/push-worker.ts +160 -0
- package/src/notifications/quiet-hours-setting.tsx +41 -58
- package/src/notifications/time-zone-select.tsx +5 -2
- package/src/notifications/use-push-device.ts +20 -33
- package/src/offline/local-first.ts +76 -0
- package/src/offline/query-persistence.ts +57 -24
- package/src/offline/service-worker-updates.ts +81 -0
- package/src/offline.ts +7 -1
- package/src/push-worker.ts +25 -0
- package/src/shell/account-menu-text-size.tsx +77 -0
- package/src/shell/top-bar-actions.tsx +6 -1
- package/src/shell.ts +3 -0
- package/src/sync/db.ts +103 -0
- package/src/sync/engine.ts +699 -0
- package/src/sync/hooks.ts +93 -0
- package/src/sync/testing.ts +73 -0
- package/src/sync/wire.ts +61 -0
- package/src/sync.ts +29 -0
- package/src/tour/tour-labels.ts +1 -0
- package/src/tour/tour.tsx +100 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/translation-review.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, ListChecks, PencilLine, RotateCcw } from \"lucide-react\";\n\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n DEFAULT_TRANSLATION_REVIEW_SWIPE,\n REVIEW_STATUSES,\n filterTranslationRows,\n groupTranslationRows,\n keyInAreas,\n reviewUndo,\n reviewWrite,\n summariseRows,\n translationReviewSwipePlan,\n unreviewedRows,\n} from \"../lib/translation-review\";\nimport type {\n ReviewStatus,\n TranslationReviewFilter,\n TranslationReviewKey,\n TranslationReviewSwipeAction,\n TranslationReviewSwipeBinding,\n TranslationReviewWrite,\n TranslationRow,\n TranslationRowGroup,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { BulkActionBar } from \"./bulk-action-bar\";\nimport { Checkbox } from \"./checkbox\";\nimport { Chip } from \"./chip\";\nimport { DataTable } from \"./data-table\";\nimport type { DataTableColumn, DataTableProps, MobileSwipeActions } from \"./data-table\";\nimport { Disclosure } from \"./disclosure\";\nimport { SearchField } from \"./search-field\";\nimport { toast } from \"./toast\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { useSharedTableView } from \"./use-table-state\";\nimport { TranslationReviewEditor } from \"./translation-review-editor\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { ReviewStatusChip, TranslationProgress } from \"./translation-review-parts\";\nimport { Button } from \"./button\";\nimport { IconButton } from \"./icon-button\";\nimport { Select } from \"./select\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"./write-lock\";\n\nexport {\n REVIEW_STATUS_TONES,\n ReviewStatusChip,\n TranslationExportButton,\n TranslationLocaleTabs,\n TranslationProgress,\n} from \"./translation-review-parts\";\nexport type {\n ReviewStatusChipProps,\n TranslationExportButtonProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationProgressProps,\n} from \"./translation-review-parts\";\nexport { TranslationReviewEditor } from \"./translation-review-editor\";\nexport type { TranslationReviewEditorProps } from \"./translation-review-editor\";\nexport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nexport type { TranslationReviewLabels } from \"./translation-review-labels\";\n\n/**\n * An app's translation review page, as kit parts (H1, docs/i18n-harmonization.md):\n * keksdose's /translations, which kastlan was about to build a second time. The app keeps\n * what is its own — where the strings come from, which locale is read against which, who\n * may review what, every request — and hands the kit rows (`translationRows`) and two\n * callbacks. Everything a reviewer touches is here.\n *\n * Presentational — no request, no route — and one LOCALE at a time. Its table is the\n * kit's DataTable, so like every DataTable it renders under the app's Router. The page\n * around it puts\n * {@link TranslationLocaleTabs} above it and {@link TranslationExportButton} in its header,\n * because both span every locale while this panel is the one open.\n *\n * Callbacks may return promises, and the panel waits for them: an editor stays open with\n * its text and the error when a save is refused (a 403 for a key outside the reviewer's\n * areas, a network failure), a bulk action keeps its selection. Chunking a long bulk\n * write to the server's batch limit is the app's; the panel sends one call per action.\n */\n\ntype MaybePromise = void | Promise<unknown>;\n\n/** What asked for a write — see {@link TranslationReviewSaveInfo}. */\nexport type TranslationReviewOrigin = \"editor\" | \"row\" | \"swipe\" | \"group\" | \"selection\" | \"undo\";\n\n/**\n * The second argument of `onSave` and `onClear` (0.25): where the write came from, and\n * whether the panel answers it with a toast of its own.\n *\n * WHY THE APP IS TOLD. keksdose and kastlan toast \"Saved\" from their mutation on every\n * save. With {@link TranslationReviewPanelProps.undo} on, an approval is answered by the\n * kit's Undo toast instead — and a reviewer swiping through a list then read two toasts\n * per card. The app skips its own where `toasted` is true:\n *\n * onSave={(writes, { toasted }) => save.mutateAsync({ items: writes.map(toApiWrite), quiet: toasted })}\n *\n * An app that ignores the argument keeps working; it only says \"Saved\" twice.\n */\nexport interface TranslationReviewSaveInfo {\n /** The open editor, a row's approve button, a swipe on a phone card, a group's\n * \"Approve unreviewed\", the selection's bulk bar, or an Undo pressed on the toast. */\n origin: TranslationReviewOrigin;\n /** The panel shows its own toast for this write (an approval's Undo toast, or a\n * swiped `clear`'s). */\n toasted: boolean;\n}\n\n/** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */\nexport type TranslationReviewGroupBy = \"namespace\" | \"source\" | ((row: TranslationRow) => string);\n\nexport interface TranslationReviewPanelProps {\n /** One locale's rows, from `translationRows`. */\n rows: readonly TranslationRow[];\n /** The reviewed language's own name — the text column's header (\"Français\"). */\n localeLabel: string;\n /** The reference language's own name — the reference column's header. */\n referenceLabel: string;\n /** Store verdicts: one for an editor or a row's approve button, many for a bulk\n * approve. Left out (or `readOnly`): nothing can be changed. The second argument\n * says where the write came from ({@link TranslationReviewSaveInfo}). */\n onSave?: (writes: TranslationReviewWrite[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** Clear verdicts — back to unreviewed. Left out: no reset, and an approval of a row\n * that had no verdict cannot be undone. */\n onClear?: (keys: TranslationReviewKey[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** A locale the viewer may read but not review: the rows and their verdicts, no\n * actions, and a line saying so. */\n readOnly?: boolean;\n /**\n * The areas the reviewer is limited to (keksdose's lawyer: `[\"legal\"]`), as the server\n * answers them; `null` is every area. Rows outside are not shown or counted — build the\n * rows with the same `areas` and they never exist — and a notice names the areas.\n */\n areas?: readonly string[] | null;\n /** An area's name for the notice (\"the legal pages (Imprint, …)\"). Default: the key. */\n areaLabels?: Readonly<Record<string, string>>;\n /** Default `keyInArea`: the key is the area or sits under it (`legal.…`, `legal:…`,\n * and the kit's `kit.legal.…` since 0.28). */\n inArea?: (key: string, area: string) => boolean;\n /** A source's name for the source filter, which appears once the rows come from more\n * than one (kastlan: `{ screen: \"Screen\", documents: \"PDFs and emails\" }`). Default:\n * the source itself. */\n sourceLabels?: Readonly<Record<string, string>>;\n /**\n * Controlled filters — for an app that keeps them in the URL (keksdose's `?status=`,\n * `?ns=`, `?q=`). Whatever is left out is the panel's own. Pair with `onFilterChange`.\n *\n * A field passed here is the app's, and the control shows what the app says: one the app\n * passes but does not write back from `onFilterChange` is a control that does nothing.\n * A development build warns about that once per field (0.25.1, Kurvenschmiede); a field\n * left out entirely is fine — the panel keeps it.\n */\n filter?: Partial<TranslationReviewFilter>;\n /** Every filter, on every change — write them in ONE update: `useSearchParamsState`'s\n * setter takes this object as it is (`filter={filter} onFilterChange={setFilter}`).\n * Single `useSearchParamState` setters compose since 0.25; before, four in a row\n * clobbered each other (keksdose live #378). */\n onFilterChange?: (filter: TranslationReviewFilter) => void;\n /** The progress bar over the filters. Default true. */\n progress?: boolean;\n /** Rows per page. Default 50. */\n pageSize?: number;\n /** Persists the table's sort and page size (DataTable's `storageKey`), under the\n * prefix below — under {@link groupBy}, the one sort and page size every group\n * shares, in the same place (0.35). */\n storageKey?: string;\n /**\n * The `localStorage` prefix in front of `storageKey` (DataTable's `storageKeyPrefix`).\n * Left out, it is the provider's `tableStorageKeyPrefix`, then the kit's\n * `\"hbui-table:\"` — so an app that sets its prefix on `UiKitProvider` needs nothing\n * here. Before 0.34 the panel took no prefix and its view always landed under the\n * kit's, which an app listing what it stores had to list as its own (KS U4).\n */\n storageKeyPrefix?: string;\n /** How a verdict's date reads in the editor. */\n formatDate?: (iso: string) => string;\n /** Turns a callback's rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /**\n * Swipe the phone cards (0.25, keksdose live #377 — Marcel reviewing on a phone): toward\n * the reading END to approve, toward the START for \"Needs a change\", which OPENS the\n * editor with the cursor in the wording rather than sending the string back blind — a\n * verdict without a note or a better wording tells whoever fixes it nothing. In a\n * right-to-left page the two sides mirror with the reading direction.\n *\n * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same\n * gesture, the same feedback, and the same keyboard path — every swipe is also a real\n * button, read out and reachable by Tab, so the gesture is a shortcut and never the only\n * way. A row has no swipe while the editor under it is open, while its own write is out,\n * under a {@link WriteLockProvider} lock, in a `readOnly` locale, and toward \"Approve\"\n * where there is nothing to approve (a missing string, an approved one). A reviewer\n * limited to `areas` never sees a row outside them, so never swipes one.\n *\n * Off by default, so no existing page gains a gesture on the bump. Phones only; the\n * desktop table keeps its approve button.\n *\n * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *\"Add\n * the swipe options to be shown also to the settings /settings#interaction area where\n * the other swipe options are defined\"*): keksdose lets the user bind every list's\n * swipes in Settings → Interaction, per side a primary action at the first threshold and\n * a secondary one at a longer drag, and a fixed mapping is one its settings cannot\n * reach. So each logical side takes an ordered ladder of\n * {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the\n * longer drag, as {@link SwipeableRow} stages them:\n *\n * swipe={{ end: [\"approve\", \"clear\"], start: [\"edit\"] }}\n *\n * `approve` (with Undo), `edit` (the editor, cursor in the wording — \"Needs a change\"),\n * `clear` (the verdict taken back, with Undo, which stores it again; only with\n * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it\n * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,\n * `clear` on a row with no verdict. Every rule above still holds for every binding —\n * no side swipes in a `readOnly` locale, under a write lock, with the editor open or\n * the row's write out.\n *\n * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: [\"approve\"], start:\n * [\"edit\"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding\n * turns the Undo toast on by default, as `true` does.\n */\n swipe?: boolean | TranslationReviewSwipeBinding;\n /**\n * Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a\n * swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores\n * again what the rows said before ({@link reviewUndo}) — or clears them, for rows that\n * had no verdict, which needs `onClear` (without it the toast reports and offers\n * nothing). A batch has ONE Undo.\n *\n * Default: on with `swipe` or `groupBy` — the two ways to approve in one movement or many\n * rows at once bring their way back — and off otherwise, so an existing page does not\n * gain a toast on the bump. Needs the app's `<Toaster>`. Tell the app's own \"Saved\"\n * toast apart with the `toasted` flag ({@link TranslationReviewSaveInfo}).\n */\n undo?: boolean;\n /**\n * Group the strings under a header per area (0.25, keksdose live #377: *\"grouping to the\n * translation review entries for faster reviewing\"*): `\"namespace\"` — the row's\n * `namespace`, which `translationRows` derives from the key (`budget`, `kit.dataTable`\n * with keksdose's `namespaceOf`) — `\"source\"`, or a function of the row.\n *\n * Each group is a section with a heading ({@link groupHeadingAs}): its name, \"n\n * unreviewed / total\", and \"Approve unreviewed (n)\", which approves the group's rows\n * nobody has given a verdict ({@link unreviewedRows}) under the same rules as one\n * approval — write lock, `readOnly`, one write at a time — with one Undo for the batch.\n * A batch larger than `pageSize` asks first, inside the opened group: more than a page\n * is more than the reviewer can have had on screen.\n *\n * WHAT IT MEANS FOR THE FILTERS, THE SORT AND THE PAGES. The filters narrow the rows\n * first, then the rows are grouped: a header counts what the filters show in it (so\n * \"Approve unreviewed\" never takes a row the reviewer filtered away), and an area with\n * nothing left is not shown. Groups stand in the order their first string has in the\n * reference; a column sort orders the rows INSIDE every group alike. Each group pages\n * on its own, `pageSize` rows at a time — a header then always stands over its own\n * rows, and paging one area does not move another — rather than one pager cutting\n * through the headers. The page SIZE is one for all of them, as the sort is: a\n * group's pager changes it everywhere.\n *\n * `storageKey` persists that one view — the sort and the page size — in the place the\n * ungrouped table keeps its own (same key, same prefix, same blob), read on mount and\n * written on a change, so a reviewer switching the grouping on or off keeps both\n * (0.35, Kurvenschmiede; before, it persisted the ungrouped table only). What only\n * the ungrouped table has — column widths, hidden columns — stays in the blob for it.\n *\n * Groups fold (each on its own, remembered while the panel lives), and a folded group's\n * rows are not rendered at all — only its header. Which groups start open is\n * {@link defaultGroupsOpen}: by default, on a phone they start folded when there are\n * several — the headers are then the area list, and the reviewer opens the one to work\n * through — and on a wide screen they start open while that is cheap (see there).\n */\n groupBy?: TranslationReviewGroupBy;\n /**\n * Which groups start open under {@link groupBy} (0.25.1). A group the reviewer opens or\n * folds keeps that; this is what every other group follows.\n *\n * - `\"auto\"` (default): on a phone, open when there is one group and folded when there\n * are several, as in 0.25.0. On a wide screen, open while opening every group renders\n * at most 100 rows (each group shows up to `pageSize` of them; the budget is one page\n * where `pageSize` is larger) — and folded above that, the headers then being the area\n * list, as on a phone.\n * - `true`: every group starts open, on a phone too.\n * - `false`: every group starts folded, on a wide screen too.\n *\n * WHY A BUDGET OF ROWS (keksdose, measured on 0.25.0): a wide screen opened EVERY group.\n * keksdose's /translations has 122 areas and ~4700 strings — 122 tables and 2053 rows at\n * once, ~46 000 elements, 5.2 s before the panel appeared and 2.5 s for a filter click\n * to paint, against 50 rows and 0.34 s ungrouped; its page test went from 4 s to 146 s.\n * kastlan (~31 areas, ~1300 rows open) and Kurvenschmiede (28, ~700) had the same page.\n * What costs is the rows on screen, so the rule counts them rather than the groups:\n * 100 rows is two of the panel's default pages — any one or two areas still open as\n * before, however long, and so do a dozen small ones, or a search whose hits fall in\n * many areas — while a whole catalogue starts as its list of areas: keksdose's shape\n * in jsdom, 122 headers and ~2100 elements instead of ~44 000 (the ungrouped panel has\n * ~1200). The rule follows the filters: narrow them and the groups open.\n *\n * A boolean rather than a controlled open state: the panel already keeps each group's\n * own state, and an app that only wants \"all open\" or \"all folded\" should not have to\n * keep a record of 122 flags to say it.\n */\n defaultGroupsOpen?: boolean | \"auto\";\n /** A group's name. Default: `sourceLabels` for `groupBy=\"source\"`, else the key. */\n groupLabel?: (group: string) => string;\n /** The level of the group headings. Default `h2`: the panel is a page's body, under\n * the page's `h1`. */\n groupHeadingAs?: \"h2\" | \"h3\" | \"h4\";\n className?: string;\n labels?: LabelOverride<TranslationReviewLabels>;\n}\n\n\n/**\n * How many rows the groups may render at once on a wide screen before they start folded\n * ({@link TranslationReviewPanelProps.defaultGroupsOpen}): two of the default 50-row pages.\n * A row costs about twenty elements in either layout, so this is the cost of two pages of\n * the ungrouped table — not of 2053 rows in 122 tables.\n */\nconst GROUPS_OPEN_ROW_BUDGET = 100;\n\n/**\n * Would opening every group stay within the budget? Each group renders up to `pageSize`\n * rows (it pages on its own); the budget is never less than one page, so a single group —\n * which renders at most one — always starts open, as on a phone.\n */\nfunction groupsFitOpen(groups: readonly TranslationRowGroup[], pageSize: number): boolean {\n const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);\n let rendered = 0;\n for (const group of groups) {\n rendered += Math.min(group.rows.length, pageSize);\n if (rendered > budget) return false;\n }\n return true;\n}\n\n/**\n * The kit's dev switch, as NumberInput's money guard and `lib/logger.ts` read it: a\n * development build — the consumer's bundler makes `import.meta.env.DEV` false in a\n * production one, and the branch is dead code there — and never under vitest, where every\n * consumer suite would print it; a test opts in with `vi.stubEnv(\"VITEST\", \"\")`. Read per\n * call, and optional-chained: `import.meta.env` is a Vite injection, absent under Node.\n */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\ntype FilterField = keyof TranslationReviewFilter;\n\n/** How long after the last filter change the app's `filter` is looked at again: well past\n * a router's transition, so a URL that is still being written is not mistaken for one\n * that never will be. */\nconst FILTER_ECHO_MS = 1500;\n\n/**\n * The development warning for a controlled filter that does not follow (Kurvenschmiede,\n * 0.25.1). A field the app passes in `filter` is the app's: the panel shows the app's\n * value, whatever the reviewer picked. An app that passes a field but does not write the\n * reported value back — its `onFilterChange` hands on only some fields, `filter` is built\n * as `{ ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...fromTheUrl }`, a URL parser rejects what\n * its serialiser wrote, or there is no `onFilterChange` at all — has a control that\n * silently does nothing: the box unticks itself, the select jumps back.\n *\n * So, after the panel reports a change of a field the app controls, it looks at the\n * app's `filter` again once the change has had time to land ({@link FILTER_ECHO_MS}, read\n * from the last COMMITTED props, so a router transition still rendering does not count),\n * and warns — once per field — when that field still holds the value it had before.\n *\n * Quiet by construction for an app that passes no `filter`, and for a field the app LEAVES\n * OUT: that field is the panel's own and works (keksdose keeps the placeholder switch so,\n * on purpose). Quiet too when the app moved the field somewhere else itself. Development\n * builds only (see {@link devWarnings}): in production and under vitest nothing is\n * recorded and no timer is set.\n */\nfunction useFilterEchoCheck(filterProp: Partial<TranslationReviewFilter> | undefined) {\n const committed = useRef(filterProp);\n useEffect(() => {\n committed.current = filterProp;\n });\n // Per field: what the app's filter said when the reviewer first changed it, and what\n // the panel reported last.\n const pending = useRef(new Map<FilterField, { from: unknown; to: unknown }>());\n const warned = useRef(new Set<FilterField>());\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(timer.current), []);\n\n return (next: TranslationReviewFilter) => {\n if (filterProp === undefined || !devWarnings()) return;\n for (const field of Object.keys(next) as FilterField[]) {\n const from = filterProp[field];\n if (from === undefined || warned.current.has(field)) continue;\n const seen = pending.current.get(field);\n if (seen) seen.to = next[field];\n else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });\n }\n if (pending.current.size === 0) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n const now = committed.current;\n for (const [field, { from, to }] of pending.current) {\n if (to === from || now?.[field] !== from || warned.current.has(field)) continue;\n warned.current.add(field);\n console.warn(\n `[ui-kit] TranslationReviewPanel: the \"${field}\" filter was changed to ${JSON.stringify(to)}, but the ` +\n `\\`filter\\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in ` +\n `\\`filter\\` is the app's: write back every field \\`onFilterChange\\` reports, in one update ` +\n `(useSearchParamsState's setter takes the object as it is), or leave the field out of \\`filter\\` and ` +\n `the panel keeps it. (Development builds only; once per field.)`,\n );\n }\n pending.current.clear();\n }, FILTER_ECHO_MS);\n };\n}\n\n/** The row's text — or, for a missing string, the line that says so. */\nfunction TextCell({ row, labels, clamp }: { row: TranslationRow; labels: TranslationReviewLabels; clamp?: boolean }) {\n return row.text === \"\" ? (\n <span className=\"italic text-muted\">{labels.missingText}</span>\n ) : (\n <span lang={row.locale} className={cn(\"whitespace-pre-line break-words\", clamp && \"line-clamp-3 large:line-clamp-none\")}>\n {row.text}\n </span>\n );\n}\n\n/**\n * The page body for one locale: progress, filters (status, source, namespace, search,\n * placeholder problems), the strings with an editor that unfolds under each, and the\n * bulk actions over a selection.\n *\n * The status filter is the work queue in order — what is not there, what nobody has\n * read, what changed after it was read, what was sent back, what is done — with each\n * count on it. \"Missing\" is offered only where something is.\n *\n * Bulk approve takes the selected rows that are not approved yet and have a text:\n * approving an absent string says nothing, and re-approving one says it again. On a\n * phone the table has no checkboxes, so \"Select all shown\" selects what the filters\n * show — filter to `Unreviewed` in one area, read, select, approve. With `groupBy`, a\n * group's own \"Approve unreviewed\" is the shorter way, and with `swipe` a card is approved\n * by a swipe.\n *\n * The panel's writes go out ONE AT A TIME, in the order they were asked for: two writes on\n * one locale's verdicts can answer out of order, and an Undo must not overtake the\n * approval it takes back. A swipe on the next card while one is out is queued rather than\n * refused — a reviewer swiping down a list is faster than the server.\n */\nexport function TranslationReviewPanel({\n rows: rowsProp,\n localeLabel,\n referenceLabel,\n onSave,\n onClear,\n readOnly = false,\n areas,\n areaLabels,\n inArea,\n sourceLabels,\n filter: filterProp,\n onFilterChange,\n progress = true,\n pageSize = 50,\n storageKey,\n storageKeyPrefix,\n formatDate,\n formatError,\n swipe = false,\n undo: undoProp,\n groupBy,\n groupLabel,\n groupHeadingAs = \"h2\",\n defaultGroupsOpen = \"auto\",\n className,\n labels: labelsProp,\n}: TranslationReviewPanelProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const locale = useKitLocale();\n // A verdict is an admin route, which a lapsed plan never locks\n // (docs/billing-harmonization.md §12.13, §12.36): the buttons take\n // COMMIT_EXCEPT_BILLING, and the swipes read the lock as they see it.\n const lock = useWriteLock(COMMIT_EXCEPT_BILLING);\n // The phone layout DataTable switches its layout on, so \"a phone\" means the same here\n // as in the table, at every text size: `usePhoneLayout()` itself (§10.4; 0.32.1,\n // keksdose's 0.32 report — `!useBreakpoint(\"md\")` could disagree with it at 767.5 px).\n const phone = usePhoneLayout();\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const editable = !readOnly && onSave !== undefined;\n const binding: TranslationReviewSwipeBinding | null =\n swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;\n const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);\n\n const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);\n // The app's fields over the panel's own; a field the app leaves `undefined` is the\n // panel's, not a blank.\n const filter: TranslationReviewFilter = { ...ownFilter };\n for (const [name, value] of Object.entries(filterProp ?? {})) {\n if (value !== undefined) Object.assign(filter, { [name]: value });\n }\n const echoFilter = useFilterEchoCheck(filterProp);\n const [openId, setOpenId] = useState<string | null>(null);\n // The row whose editor a swipe opened, to be written in: its wording field takes focus.\n const [focusId, setFocusId] = useState<string | null>(null);\n const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());\n // The writes asked for and not yet answered, in order: the first is the one out. The\n // editor keeps its own, for its own buttons.\n const [queued, setQueued] = useState<readonly string[]>([]);\n const claimed = useRef(new Set<string>());\n const tail = useRef<Promise<unknown>>(Promise.resolve());\n const undoCount = useRef(0);\n const [failure, setFailure] = useState<ReactNode>(null);\n const busy = queued.length > 0;\n const isQueued = (key: string) => queued.includes(key);\n\n const rows = useMemo(\n () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),\n [rowsProp, areas, inArea],\n );\n const summary = useMemo(() => summariseRows(rows), [rows]);\n const sources = useMemo(() => [...new Set(rows.map((r) => r.source))], [rows]);\n const source = sources.includes(filter.source) ? filter.source : \"all\";\n const namespaces = useMemo(\n () => [...new Set(rows.filter((r) => source === \"all\" || r.source === source).map((r) => r.namespace))],\n [rows, source],\n );\n const namespace = namespaces.includes(filter.namespace) ? filter.namespace : \"all\";\n const mismatches = useMemo(() => rows.filter((r) => r.placeholderMismatch).length, [rows]);\n // `filter` is rebuilt every render; its fields are what the list depends on.\n const { status, query, placeholdersOnly } = filter;\n const visible = useMemo(\n () => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),\n [rows, status, source, namespace, query, placeholdersOnly],\n );\n\n // ── Groups ──\n const groups = useMemo(() => {\n if (groupBy === undefined) return null;\n const of =\n groupBy === \"namespace\"\n ? (r: TranslationRow) => r.namespace\n : groupBy === \"source\"\n ? (r: TranslationRow) => r.source\n : groupBy;\n return groupTranslationRows(visible, of);\n }, [visible, groupBy]);\n const groupName = (key: string) =>\n groupLabel?.(key) ?? (groupBy === \"source\" ? sourceLabels?.[key] : undefined) ?? (key || \"—\");\n // Only what the reviewer chose; a group nobody has touched follows the default, which\n // follows the filters (see `defaultGroupsOpen`).\n const [groupOpen, setGroupOpen] = useState<Readonly<Record<string, boolean>>>({});\n const groupOpenByDefault =\n defaultGroupsOpen !== \"auto\"\n ? defaultGroupsOpen\n : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));\n const isGroupOpen = (key: string) => groupOpen[key] ?? groupOpenByDefault;\n const setGroupOpenFor = (key: string, open: boolean) => setGroupOpen((prev) => ({ ...prev, [key]: open }));\n /** A group the reviewer works in — an editor opened in it — stays open when the default\n * turns (a search cleared, the rows grown past the budget): folding it would drop the\n * editor and whatever was typed in it. */\n const keepGroupOpen = (key: string | undefined) => {\n if (key !== undefined) setGroupOpen((prev) => (key in prev ? prev : { ...prev, [key]: true }));\n };\n // The group whose large batch is waiting for a yes — and, until its confirm button\n // has mounted and taken focus, the group that asked.\n const [confirming, setConfirming] = useState<string | null>(null);\n const confirmFocus = useRef<string | null>(null);\n // One view for every group's table — one sort, so a header click orders all of them\n // alike, and one page size — kept under `storageKey` in the single table's own blob, so\n // switching the grouping keeps both (0.35, Kurvenschmiede).\n const groupView = useSharedTableView({\n active: groupBy !== undefined,\n storageKey,\n storageKeyPrefix,\n defaultPageSize: pageSize,\n });\n\n const setFilter = (patch: Partial<TranslationReviewFilter>) => {\n const next = { ...filter, ...patch };\n setOwnFilter(next);\n onFilterChange?.(next);\n echoFilter(next);\n // A selection the new filter hides would be acted on unseen.\n setSelected(new Set());\n };\n\n /** Queue a write under `key` (one per key at a time); resolves whether it worked. */\n const run = (key: string, action: () => MaybePromise): Promise<boolean> => {\n if (claimed.current.has(key)) return Promise.resolve(false);\n claimed.current.add(key);\n setQueued((q) => [...q, key]);\n const step = tail.current.then(async () => {\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n claimed.current.delete(key);\n setQueued((q) => q.filter((k) => k !== key));\n }\n });\n tail.current = step;\n return step;\n };\n\n /** \"String approved — Undo\", once `targets` (as they were BEFORE the write) are\n * approved — or, after a swiped `clear`, \"String marked unreviewed — Undo\". Either way\n * Undo puts back what the rows said before ({@link reviewUndo}), through the same queue\n * as every write. */\n const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {\n const back = reviewUndo(targets);\n const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);\n const save = onSave;\n const clear = onClear;\n // Taking back the approval of a row that had no verdict is a clear: without\n // `onClear` there is no way to say it, so the toast reports and offers nothing.\n if (!save || (back.clears.length > 0 && !clear)) {\n toast.success(message, { description });\n return;\n }\n const key = `undo:${++undoCount.current}`;\n toast.undo(message, {\n description,\n onUndo: () =>\n void run(key, async () => {\n if (back.writes.length > 0) await save(back.writes, { origin: \"undo\", toasted: false });\n if (back.clears.length > 0) await clear?.(back.clears, { origin: \"undo\", toasted: false });\n }),\n });\n };\n\n const approve = async (\n key: string,\n targets: readonly TranslationRow[],\n origin: TranslationReviewOrigin,\n description?: ReactNode,\n ): Promise<boolean> => {\n if (!onSave || targets.length === 0) return false;\n const save = onSave;\n const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, \"APPROVED\")), { origin, toasted: undoOn }));\n if (ok && undoOn) sayDone(targets, description);\n return ok;\n };\n\n /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —\n * the Undo toast stores the verdict again. Under the row's own key, so it queues behind\n * (and never overlaps) an approval of the same row. */\n const clearRow = async (row: TranslationRow) => {\n if (!onClear || row.review === null) return;\n const clear = onClear;\n const keys = [{ locale: row.locale, key: row.key }];\n const ok = await run(`row:${row.id}`, () => clear(keys, { origin: \"swipe\", toasted: undoOn }));\n if (ok && undoOn) sayDone([row], row.key, true);\n };\n\n /** The editor's own save: it waits, closes and shows its errors itself, so it does\n * not go through the queue — but an approval there is answered like any other. */\n const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {\n const toasted = undoOn && write.verdict === \"APPROVED\";\n await onSave?.([write], { origin: \"editor\", toasted });\n if (toasted) sayDone([row], row.key);\n };\n\n /**\n * Focus back into the panel once a group's batch has settled, if it fell out: the\n * button that was pressed goes with the rows it approved (and under the `Unreviewed`\n * filter the whole group can go). Two frames, so the app's new rows have been drawn.\n */\n const keepFocusAt = (groupKey: string) => {\n const check = () => {\n const root = rootRef.current;\n const active = document.activeElement;\n if (!root || (active && active !== document.body && active.isConnected)) return;\n const sections = [...root.querySelectorAll<HTMLElement>(\"[data-review-group]\")];\n const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];\n section?.querySelector<HTMLElement>(\"[data-disclosure-trigger]\")?.focus();\n };\n requestAnimationFrame(() => requestAnimationFrame(check));\n };\n\n const approveGroup = async (group: TranslationRowGroup, confirmed: boolean) => {\n const targets = unreviewedRows(group.rows);\n if (targets.length === 0) return;\n if (!confirmed && targets.length > pageSize) {\n // Asked inside the opened group, over the rows it is about.\n setGroupOpenFor(group.key, true);\n setConfirming(group.key);\n confirmFocus.current = group.key;\n return;\n }\n const ok = await approve(`group:${group.key}`, targets, \"group\", groupName(group.key));\n if (ok) setConfirming((key) => (key === group.key ? null : key));\n keepFocusAt(group.key);\n };\n\n const visibleIds = new Set(visible.map((r) => r.id));\n const selectedRows = visible.filter((r) => selected.has(r.id));\n // What \"Select all shown\" takes: grouped, the rows of the open groups — a folded\n // group's rows are not shown.\n const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;\n const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));\n const toApprove = selectedRows.filter((r) => r.text !== \"\" && r.status !== \"approved\");\n const toReset = selectedRows.filter((r) => r.review !== null);\n\n const approveSelected = async () => {\n if (await approve(\"bulk-approve\", toApprove, \"selection\")) setSelected(new Set());\n };\n const resetSelected = async () => {\n if (!onClear || toReset.length === 0) return;\n const clear = onClear;\n const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));\n if (await run(\"bulk-reset\", () => clear(keys, { origin: \"selection\", toasted: false }))) {\n setSelected(new Set());\n }\n };\n\n // The status options: \"missing\" only where something is (or it is the filter).\n const statuses = REVIEW_STATUSES.filter((s) => s !== \"missing\" || summary.missing > 0 || filter.status === s);\n const scopeNames = (areas ?? []).map((area) => areaLabels?.[area] ?? area);\n const scopeText =\n scopeNames.length > 0 ? new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" }).format(scopeNames) : \"\";\n\n const columns: DataTableColumn<TranslationRow>[] = [\n {\n key: \"key\",\n header: labels.key,\n cell: (r) => <code className=\"break-all text-xs text-secondary\">{r.key}</code>,\n sortBy: (r) => r.key,\n mobileHidden: true,\n },\n {\n key: \"reference\",\n header: referenceLabel,\n cell: (r) => (\n <span className=\"line-clamp-3 whitespace-pre-line break-words text-secondary\">{r.reference}</span>\n ),\n mobileHidden: true,\n },\n {\n key: \"text\",\n header: localeLabel,\n cell: (r) => <TextCell row={r} labels={labels} clamp />,\n mobilePrimary: true,\n },\n {\n key: \"status\",\n header: labels.statusColumn,\n cell: (r) => (\n <span className=\"inline-flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </span>\n ),\n sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),\n },\n ];\n if (editable) {\n columns.push({\n key: \"approve\",\n header: <span className=\"sr-only\">{labels.approve}</span>,\n cell: (r) =>\n r.status === \"approved\" || r.text === \"\" ? null : (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.approve}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // the row's one action, in a hugging column of a dense table beside the\n // string under review, which \"Approve\" as text at Large would squeeze. A\n // single action stays inline in `RowActions` too, so the menu would not help;\n // the swipe and the editor say it in words.\n labelVisible={false}\n pending={isQueued(`row:${r.id}`)}\n disabled={busy}\n onClick={(event) => {\n // The row's own click opens the editor; this one only approves.\n event.stopPropagation();\n void approve(`row:${r.id}`, [r], \"row\", r.key);\n }}\n >\n <Check />\n </IconButton>\n ),\n });\n }\n\n /** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */\n const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {\n const missing = r.text === \"\";\n switch (id) {\n case \"approve\":\n return {\n label: labels.approve,\n icon: <Check className=\"size-4\" aria-hidden />,\n onCommit: () => void approve(`row:${r.id}`, [r], \"swipe\", r.key),\n // SwipeableRow paints the tone: its soft wash idle, its fill and that fill's\n // foreground armed (0.33).\n tone: \"success\",\n };\n case \"edit\":\n return {\n label: missing ? labels.suggest : labels.flag,\n icon: <PencilLine className=\"size-4\" aria-hidden />,\n // Opens the editor rather than sending the string back: \"needs a change\"\n // with neither a note nor a better wording tells nobody anything.\n onCommit: () => {\n keepGroupOpen(groupKey);\n setOpenId(r.id);\n setFocusId(r.id);\n },\n tone: missing ? \"brand\" : \"danger\",\n };\n case \"clear\":\n return {\n label: labels.reset,\n icon: <RotateCcw className=\"size-4\" aria-hidden />,\n onCommit: () => void clearRow(r),\n // A step back, not a verdict: neither the approval's green nor the send-back's\n // red. `neutral` is the pair this swipe improvised and 0.33 made a token: the\n // muted ink as a fill under the surface colour.\n tone: \"neutral\",\n };\n }\n };\n\n /**\n * A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved\n * against the row, each side's ladder closed up over what the row cannot offer. None\n * where the row must not be acted on — the same conditions under which its buttons are\n * locked or absent.\n */\n const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {\n if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;\n const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });\n if (plan.start.length === 0 && plan.end.length === 0) return null;\n return {\n start: plan.start.map((id) => swipeAction(r, id, groupKey)),\n end: plan.end.map((id) => swipeAction(r, id, groupKey)),\n };\n };\n\n const toggleIn = (prev: ReadonlySet<string>, many: readonly TranslationRow[], checked: boolean) => {\n const next = new Set([...prev].filter((id) => visibleIds.has(id)));\n for (const r of many) {\n if (checked) next.add(r.id);\n else next.delete(r.id);\n }\n return next;\n };\n /** The checkbox column of a table over `list` — the whole list, or one group. */\n const selectionFor = (list: readonly TranslationRow[]): DataTableProps<TranslationRow>[\"selection\"] => {\n if (!editable) return undefined;\n const picked = list.filter((r) => selected.has(r.id)).length;\n return {\n isSelected: (r) => selected.has(r.id),\n onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),\n onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),\n allSelected: list.length > 0 && picked === list.length,\n someSelected: picked > 0 && picked < list.length,\n onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked)),\n };\n };\n\n const table = (list: TranslationRow[], group?: { key: string; name: string }) => (\n <DataTable<TranslationRow>\n rows={list}\n rowKey={(r) => r.id}\n columns={columns}\n empty={labels.empty}\n defaultPageSize={pageSize}\n {...(group\n ? {\n // Inside the group's own card: no frame in a frame, no column rail per group,\n // no scroller per group, and the group's name as the table's.\n frame: false,\n chrome: \"minimal\" as const,\n maxBodyHeight: \"none\",\n sorts: groupView.sorts,\n onSortsChange: groupView.setSorts,\n pageSize: groupView.pageSize,\n onPageSizeChange: groupView.setPageSize,\n labels: { table: group.name },\n }\n : { storageKey, storageKeyPrefix })}\n isExpanded={(r) => r.id === openId}\n onRowClick={(r) => {\n if (openId !== r.id) keepGroupOpen(group?.key);\n setFocusId(null);\n setOpenId((open) => (open === r.id ? null : r.id));\n }}\n expandedRow={(r) => (\n <TranslationReviewEditor\n key={r.id}\n row={r}\n referenceLabel={referenceLabel}\n localeLabel={localeLabel}\n readOnly={!editable}\n focusWording={focusId === r.id}\n onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}\n onClear={onClear ? (key) => onClear([key], { origin: \"editor\", toasted: false }) : undefined}\n onClose={() => {\n setOpenId(null);\n setFocusId(null);\n }}\n formatDate={formatDate}\n formatError={formatError}\n labels={labelsProp}\n />\n )}\n mobileCard={(r) => (\n <div className=\"min-w-0 space-y-1\">\n <code className=\"block break-all text-xs text-muted\">{r.key}</code>\n <div className=\"text-sm text-primary\">\n <TextCell row={r} labels={labels} clamp />\n </div>\n <div className=\"flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </div>\n </div>\n )}\n mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}\n selection={selectionFor(list)}\n />\n );\n\n const groupSection = (g: TranslationRowGroup, index: number) => {\n const name = groupName(g.key);\n const titleId = `${baseId}-group-${index}`;\n const questionId = `${baseId}-confirm-${index}`;\n const targets = editable ? unreviewedRows(g.rows) : [];\n const key = `group:${g.key}`;\n const open = isGroupOpen(g.key);\n // A folded group renders its header and nothing else. Its body is handed over only\n // while it is open, or when the REVIEWER folded it — so the fold animates over the\n // rows it hides. A group folded by the default gets none: when the default turns (a\n // search cleared over 122 areas), the groups that close would otherwise render their\n // grown tables for the length of the fold.\n const body = open || groupOpen[g.key] === false;\n return (\n <Disclosure\n key={g.key}\n data-review-group={g.key}\n headingAs={groupHeadingAs}\n title={<span id={titleId}>{name}</span>}\n hint={labels.groupCount(g.summary.unreviewed, g.summary.total)}\n open={open}\n onOpenChange={(next) => {\n setGroupOpenFor(g.key, next);\n if (!next) setConfirming((c) => (c === g.key ? null : c));\n }}\n trailing={\n targets.length > 0 ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n // The visible words say what; the group's name, read after them, says where.\n aria-describedby={titleId}\n pending={isQueued(key) && confirming !== g.key}\n disabled={busy}\n onClick={() => void approveGroup(g, false)}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveGroup(targets.length)}\n </Button>\n ) : undefined\n }\n bodyClassName=\"space-y-0 px-0 pb-0 border-t border-subtle\"\n >\n {body && confirming === g.key && targets.length > 0 && (\n <div className=\"border-b border-subtle p-3\">\n <AlertBanner tone=\"warning\" size=\"sm\">\n <div className=\"min-w-0 space-y-2\">\n <p id={questionId}>{labels.confirmGroup(targets.length, name)}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n // Focus lands here when the question appears: the expected answer to a\n // question the reviewer just asked for, and Undo stays behind it.\n ref={(el) => {\n if (el && confirmFocus.current === g.key) {\n confirmFocus.current = null;\n el.focus();\n }\n }}\n variant=\"primary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n aria-describedby={questionId}\n pending={isQueued(key)}\n disabled={busy}\n onClick={() => void approveGroup(g, true)}\n >\n {labels.approveGroup(targets.length)}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={isQueued(key)}\n onClick={() => {\n setConfirming(null);\n keepFocusAt(g.key);\n }}\n >\n {labels.cancel}\n </Button>\n </div>\n </div>\n </AlertBanner>\n </div>\n )}\n {body && table(g.rows, { key: g.key, name })}\n </Disclosure>\n );\n };\n return (\n <div ref={rootRef} className={cn(\"min-w-0 space-y-4\", className)}>\n {scopeText && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.scope(scopeText)}\n </AlertBanner>\n )}\n {readOnly && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.readOnly}\n </AlertBanner>\n )}\n\n {progress && <TranslationProgress summary={summary} labels={labelsProp} />}\n\n <ToggleGroup<ReviewStatus | \"all\">\n aria-label={labels.statusFilter}\n overflow=\"wrap\"\n size=\"sm\"\n value={filter.status}\n onChange={(next) => setFilter({ status: next })}\n options={[\n { value: \"all\", label: labels.filterCount(labels.all, summary.total) },\n ...statuses.map((s) => ({ value: s, label: labels.filterCount(reviewStatusLabel(labels, s), summary[s]) })),\n ]}\n />\n\n <div className=\"flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end\">\n {sources.length > 1 && (\n <div className=\"md:w-48\">\n <Select label={labels.source} value={source} onChange={(e) => setFilter({ source: e.target.value })}>\n <option value=\"all\">{labels.allSources}</option>\n {sources.map((s) => (\n <option key={s} value={s}>\n {sourceLabels?.[s] ?? s}\n </option>\n ))}\n </Select>\n </div>\n )}\n <div className=\"md:w-56\">\n <Select label={labels.namespace} value={namespace} onChange={(e) => setFilter({ namespace: e.target.value })}>\n <option value=\"all\">{labels.allNamespaces}</option>\n {namespaces.map((ns) => (\n <option key={ns} value={ns}>\n {ns}\n </option>\n ))}\n </Select>\n </div>\n <SearchField\n className=\"min-w-0 md:flex-1\"\n aria-label={labels.search}\n value={filter.query}\n onChange={(next) => setFilter({ query: next })}\n />\n <Checkbox\n label={labels.placeholdersOnly(mismatches)}\n checked={filter.placeholdersOnly}\n onCheckedChange={(next) => setFilter({ placeholdersOnly: next })}\n />\n </div>\n\n {editable && shownRows.length > 0 && (\n // The phone's table has no checkboxes; this is its way into the bulk actions.\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"md:hidden\"\n disabled={allShownSelected}\n onClick={() => setSelected((prev) => toggleIn(prev, shownRows, true))}\n >\n <ListChecks className=\"size-4\" aria-hidden />\n {labels.selectShown}\n </Button>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n\n {editable && (\n <BulkActionBar count={selectedRows.length} onClear={() => setSelected(new Set())}>\n <Button\n variant=\"primary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={isQueued(\"bulk-approve\")}\n disabled={busy || toApprove.length === 0}\n onClick={() => void approveSelected()}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveSelected}\n </Button>\n {onClear && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={isQueued(\"bulk-reset\")}\n disabled={busy || toReset.length === 0}\n onClick={() => void resetSelected()}\n >\n <RotateCcw className=\"size-4\" aria-hidden />\n {labels.resetSelected}\n </Button>\n )}\n </BulkActionBar>\n )}\n\n {groups && groups.length > 0 ? (\n <div className=\"space-y-3\">{groups.map(groupSection)}</div>\n ) : (\n table(visible)\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["ReviewStatusChip","TranslationProgress","TranslationReviewEditor","DEFAULT_TRANSLATION_REVIEW_LABELS"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/translation-review.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, ListChecks, PencilLine, RotateCcw } from \"lucide-react\";\n\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n DEFAULT_TRANSLATION_REVIEW_SWIPE,\n REVIEW_STATUSES,\n filterTranslationRows,\n groupTranslationRows,\n keyInAreas,\n reviewUndo,\n reviewWrite,\n summariseRows,\n translationReviewSwipePlan,\n unreviewedRows,\n} from \"../lib/translation-review\";\nimport type {\n ReviewStatus,\n TranslationReviewFilter,\n TranslationReviewKey,\n TranslationReviewSwipeAction,\n TranslationReviewSwipeBinding,\n TranslationReviewWrite,\n TranslationRow,\n TranslationRowGroup,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { BulkActionBar } from \"./bulk-action-bar\";\nimport { Checkbox } from \"./checkbox\";\nimport { Chip } from \"./chip\";\nimport { DataTable } from \"./data-table\";\nimport type { DataTableColumn, DataTableProps, MobileSwipeActions } from \"./data-table\";\nimport { Disclosure } from \"./disclosure\";\nimport { SearchField } from \"./search-field\";\nimport { toast } from \"./toast\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { useSharedTableView } from \"./use-table-state\";\nimport { TranslationReviewEditor } from \"./translation-review-editor\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { ReviewStatusChip, TranslationProgress } from \"./translation-review-parts\";\nimport { Button } from \"./button\";\nimport { IconButton } from \"./icon-button\";\nimport { DEFAULT_ROW_ACTIONS_LABELS, rowActionsColumn, type RowAction } from \"./row-actions\";\nimport { Select } from \"./select\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"./write-lock\";\n\nexport {\n REVIEW_STATUS_TONES,\n ReviewStatusChip,\n TranslationExportButton,\n TranslationLocaleTabs,\n TranslationProgress,\n} from \"./translation-review-parts\";\nexport type {\n ReviewStatusChipProps,\n TranslationExportButtonProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationProgressProps,\n} from \"./translation-review-parts\";\nexport { TranslationReviewEditor } from \"./translation-review-editor\";\nexport type { TranslationReviewEditorProps } from \"./translation-review-editor\";\nexport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nexport type { TranslationReviewLabels } from \"./translation-review-labels\";\n\n/**\n * An app's translation review page, as kit parts (H1, docs/i18n-harmonization.md):\n * keksdose's /translations, which kastlan was about to build a second time. The app keeps\n * what is its own — where the strings come from, which locale is read against which, who\n * may review what, every request — and hands the kit rows (`translationRows`) and two\n * callbacks. Everything a reviewer touches is here.\n *\n * Presentational — no request, no route — and one LOCALE at a time. Its table is the\n * kit's DataTable, so like every DataTable it renders under the app's Router. The page\n * around it puts\n * {@link TranslationLocaleTabs} above it and {@link TranslationExportButton} in its header,\n * because both span every locale while this panel is the one open.\n *\n * Callbacks may return promises, and the panel waits for them: an editor stays open with\n * its text and the error when a save is refused (a 403 for a key outside the reviewer's\n * areas, a network failure), a bulk action keeps its selection. Chunking a long bulk\n * write to the server's batch limit is the app's; the panel sends one call per action.\n */\n\ntype MaybePromise = void | Promise<unknown>;\n\n/** What asked for a write — see {@link TranslationReviewSaveInfo}. */\nexport type TranslationReviewOrigin = \"editor\" | \"row\" | \"swipe\" | \"group\" | \"selection\" | \"undo\";\n\n/**\n * The second argument of `onSave` and `onClear` (0.25): where the write came from, and\n * whether the panel answers it with a toast of its own.\n *\n * WHY THE APP IS TOLD. keksdose and kastlan toast \"Saved\" from their mutation on every\n * save. With {@link TranslationReviewPanelProps.undo} on, an approval is answered by the\n * kit's Undo toast instead — and a reviewer swiping through a list then read two toasts\n * per card. The app skips its own where `toasted` is true:\n *\n * onSave={(writes, { toasted }) => save.mutateAsync({ items: writes.map(toApiWrite), quiet: toasted })}\n *\n * An app that ignores the argument keeps working; it only says \"Saved\" twice.\n */\nexport interface TranslationReviewSaveInfo {\n /** The open editor, a row's approve button, a swipe on a phone card, a group's\n * \"Approve unreviewed\", the selection's bulk bar, or an Undo pressed on the toast. */\n origin: TranslationReviewOrigin;\n /** The panel shows its own toast for this write (an approval's Undo toast, or a\n * swiped `clear`'s). */\n toasted: boolean;\n}\n\n/** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */\nexport type TranslationReviewGroupBy = \"namespace\" | \"source\" | ((row: TranslationRow) => string);\n\nexport interface TranslationReviewPanelProps {\n /** One locale's rows, from `translationRows`. */\n rows: readonly TranslationRow[];\n /** The reviewed language's own name — the text column's header (\"Français\"). */\n localeLabel: string;\n /** The reference language's own name — the reference column's header. */\n referenceLabel: string;\n /** Store verdicts: one for an editor or a row's approve button, many for a bulk\n * approve. Left out (or `readOnly`): nothing can be changed. The second argument\n * says where the write came from ({@link TranslationReviewSaveInfo}). */\n onSave?: (writes: TranslationReviewWrite[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** Clear verdicts — back to unreviewed. Left out: no reset, and an approval of a row\n * that had no verdict cannot be undone. */\n onClear?: (keys: TranslationReviewKey[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** A locale the viewer may read but not review: the rows and their verdicts, no\n * actions, and a line saying so. */\n readOnly?: boolean;\n /**\n * The areas the reviewer is limited to (keksdose's lawyer: `[\"legal\"]`), as the server\n * answers them; `null` is every area. Rows outside are not shown or counted — build the\n * rows with the same `areas` and they never exist — and a notice names the areas.\n */\n areas?: readonly string[] | null;\n /** An area's name for the notice (\"the legal pages (Imprint, …)\"). Default: the key. */\n areaLabels?: Readonly<Record<string, string>>;\n /** Default `keyInArea`: the key is the area or sits under it (`legal.…`, `legal:…`,\n * and the kit's `kit.legal.…` since 0.28). */\n inArea?: (key: string, area: string) => boolean;\n /** A source's name for the source filter, which appears once the rows come from more\n * than one (kastlan: `{ screen: \"Screen\", documents: \"PDFs and emails\" }`). Default:\n * the source itself. */\n sourceLabels?: Readonly<Record<string, string>>;\n /**\n * Controlled filters — for an app that keeps them in the URL (keksdose's `?status=`,\n * `?ns=`, `?q=`). Whatever is left out is the panel's own. Pair with `onFilterChange`.\n *\n * A field passed here is the app's, and the control shows what the app says: one the app\n * passes but does not write back from `onFilterChange` is a control that does nothing.\n * A development build warns about that once per field (0.25.1, Kurvenschmiede); a field\n * left out entirely is fine — the panel keeps it.\n */\n filter?: Partial<TranslationReviewFilter>;\n /** Every filter, on every change — write them in ONE update: `useSearchParamsState`'s\n * setter takes this object as it is (`filter={filter} onFilterChange={setFilter}`).\n * Single `useSearchParamState` setters compose since 0.25; before, four in a row\n * clobbered each other (keksdose live #378). */\n onFilterChange?: (filter: TranslationReviewFilter) => void;\n /** The progress bar over the filters. Default true. */\n progress?: boolean;\n /** Rows per page. Default 50. */\n pageSize?: number;\n /** Persists the table's sort and page size (DataTable's `storageKey`), under the\n * prefix below — under {@link groupBy}, the one sort and page size every group\n * shares, in the same place (0.35). */\n storageKey?: string;\n /**\n * The `localStorage` prefix in front of `storageKey` (DataTable's `storageKeyPrefix`).\n * Left out, it is the provider's `tableStorageKeyPrefix`, then the kit's\n * `\"hbui-table:\"` — so an app that sets its prefix on `UiKitProvider` needs nothing\n * here. Before 0.34 the panel took no prefix and its view always landed under the\n * kit's, which an app listing what it stores had to list as its own (KS U4).\n */\n storageKeyPrefix?: string;\n /** How a verdict's date reads in the editor. */\n formatDate?: (iso: string) => string;\n /** Turns a callback's rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /**\n * Swipe the phone cards (0.25, keksdose live #377 — Marcel reviewing on a phone): toward\n * the reading END to approve, toward the START for \"Needs a change\", which OPENS the\n * editor with the cursor in the wording rather than sending the string back blind — a\n * verdict without a note or a better wording tells whoever fixes it nothing. In a\n * right-to-left page the two sides mirror with the reading direction.\n *\n * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same\n * gesture, the same feedback, and the same keyboard path — every swipe is also a real\n * button, read out and reachable by Tab, so the gesture is a shortcut and never the only\n * way. A row has no swipe while its own write is out (a swipe's, a button's, or the open\n * editor's own), under a {@link WriteLockProvider} lock, in a `readOnly` locale, and\n * toward \"Approve\" where there is nothing to approve (a missing string, an approved one).\n * A reviewer limited to `areas` never sees a row outside them, so never swipes one.\n *\n * An OPEN row swipes too, on its top line (keksdose live #416, Marcel: *\"The swipe shall\n * still be possible since it is faster than scrolling down to the buttons\"*), as\n * DataTable's open card does since 0.40: the editor below is outside the gesture, and a\n * swipe neither toggles it shut nor resets it. A swipe acts on the STORED string, as the\n * row's buttons do — Approve approves what the locale says, not the editor's draft — and\n * the editor stays open with whatever was typed in it; \"Needs a change\" puts the cursor\n * back at the end of that draft.\n *\n * Off by default, so no existing page gains a gesture on the bump. The gesture is the\n * phone's; on a wide screen the binding is the row's actions column instead (0.46,\n * keksdose dev#598): Approve where the row can take it, bound or not, then the bound\n * `edit` and `clear`, each once — under a write lock they stay and say why.\n *\n * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *\"Add\n * the swipe options to be shown also to the settings /settings#interaction area where\n * the other swipe options are defined\"*): keksdose lets the user bind every list's\n * swipes in Settings → Interaction, per side a primary action at the first threshold and\n * a secondary one at a longer drag, and a fixed mapping is one its settings cannot\n * reach. So each logical side takes an ordered ladder of\n * {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the\n * longer drag, as {@link SwipeableRow} stages them:\n *\n * swipe={{ end: [\"approve\", \"clear\"], start: [\"edit\"] }}\n *\n * `approve` (with Undo), `edit` (the editor, cursor in the wording — \"Needs a change\"),\n * `clear` (the verdict taken back, with Undo, which stores it again; only with\n * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it\n * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,\n * `clear` on a row with no verdict. Every rule above still holds for every binding —\n * no side swipes in a `readOnly` locale, under a write lock, or while the row's write\n * (or its open editor's) is out.\n *\n * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: [\"approve\"], start:\n * [\"edit\"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding\n * turns the Undo toast on by default, as `true` does.\n */\n swipe?: boolean | TranslationReviewSwipeBinding;\n /**\n * Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a\n * swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores\n * again what the rows said before ({@link reviewUndo}) — or clears them, for rows that\n * had no verdict, which needs `onClear` (without it the toast reports and offers\n * nothing). A batch has ONE Undo.\n *\n * Default: on with `swipe` or `groupBy` — the two ways to approve in one movement or many\n * rows at once bring their way back — and off otherwise, so an existing page does not\n * gain a toast on the bump. Needs the app's `<Toaster>`. Tell the app's own \"Saved\"\n * toast apart with the `toasted` flag ({@link TranslationReviewSaveInfo}).\n */\n undo?: boolean;\n /**\n * Group the strings under a header per area (0.25, keksdose live #377: *\"grouping to the\n * translation review entries for faster reviewing\"*): `\"namespace\"` — the row's\n * `namespace`, which `translationRows` derives from the key (`budget`, `kit.dataTable`\n * with keksdose's `namespaceOf`) — `\"source\"`, or a function of the row.\n *\n * Each group is a section with a heading ({@link groupHeadingAs}): its name, \"n\n * unreviewed / total\", and \"Approve unreviewed (n)\", which approves the group's rows\n * nobody has given a verdict ({@link unreviewedRows}) under the same rules as one\n * approval — write lock, `readOnly`, one write at a time — with one Undo for the batch.\n * A batch larger than `pageSize` asks first, inside the opened group: more than a page\n * is more than the reviewer can have had on screen.\n *\n * WHAT IT MEANS FOR THE FILTERS, THE SORT AND THE PAGES. The filters narrow the rows\n * first, then the rows are grouped: a header counts what the filters show in it (so\n * \"Approve unreviewed\" never takes a row the reviewer filtered away), and an area with\n * nothing left is not shown. Groups stand in the order their first string has in the\n * reference; a column sort orders the rows INSIDE every group alike. Each group pages\n * on its own, `pageSize` rows at a time — a header then always stands over its own\n * rows, and paging one area does not move another — rather than one pager cutting\n * through the headers. The page SIZE is one for all of them, as the sort is: a\n * group's pager changes it everywhere.\n *\n * `storageKey` persists that one view — the sort and the page size — in the place the\n * ungrouped table keeps its own (same key, same prefix, same blob), read on mount and\n * written on a change, so a reviewer switching the grouping on or off keeps both\n * (0.35, Kurvenschmiede; before, it persisted the ungrouped table only). What only\n * the ungrouped table has — column widths, hidden columns — stays in the blob for it.\n *\n * Groups fold (each on its own, remembered while the panel lives), and a folded group's\n * rows are not rendered at all — only its header. Which groups start open is\n * {@link defaultGroupsOpen}: by default, on a phone they start folded when there are\n * several — the headers are then the area list, and the reviewer opens the one to work\n * through — and on a wide screen they start open while that is cheap (see there).\n */\n groupBy?: TranslationReviewGroupBy;\n /**\n * Which groups start open under {@link groupBy} (0.25.1). A group the reviewer opens or\n * folds keeps that; this is what every other group follows.\n *\n * - `\"auto\"` (default): on a phone, open when there is one group and folded when there\n * are several, as in 0.25.0. On a wide screen, open while opening every group renders\n * at most 100 rows (each group shows up to `pageSize` of them; the budget is one page\n * where `pageSize` is larger) — and folded above that, the headers then being the area\n * list, as on a phone.\n * - `true`: every group starts open, on a phone too.\n * - `false`: every group starts folded, on a wide screen too.\n *\n * WHY A BUDGET OF ROWS (keksdose, measured on 0.25.0): a wide screen opened EVERY group.\n * keksdose's /translations has 122 areas and ~4700 strings — 122 tables and 2053 rows at\n * once, ~46 000 elements, 5.2 s before the panel appeared and 2.5 s for a filter click\n * to paint, against 50 rows and 0.34 s ungrouped; its page test went from 4 s to 146 s.\n * kastlan (~31 areas, ~1300 rows open) and Kurvenschmiede (28, ~700) had the same page.\n * What costs is the rows on screen, so the rule counts them rather than the groups:\n * 100 rows is two of the panel's default pages — any one or two areas still open as\n * before, however long, and so do a dozen small ones, or a search whose hits fall in\n * many areas — while a whole catalogue starts as its list of areas: keksdose's shape\n * in jsdom, 122 headers and ~2100 elements instead of ~44 000 (the ungrouped panel has\n * ~1200). The rule follows the filters: narrow them and the groups open.\n *\n * A boolean rather than a controlled open state: the panel already keeps each group's\n * own state, and an app that only wants \"all open\" or \"all folded\" should not have to\n * keep a record of 122 flags to say it.\n */\n defaultGroupsOpen?: boolean | \"auto\";\n /** A group's name. Default: `sourceLabels` for `groupBy=\"source\"`, else the key. */\n groupLabel?: (group: string) => string;\n /** The level of the group headings. Default `h2`: the panel is a page's body, under\n * the page's `h1`. */\n groupHeadingAs?: \"h2\" | \"h3\" | \"h4\";\n className?: string;\n labels?: LabelOverride<TranslationReviewLabels>;\n}\n\n\n/**\n * How many rows the groups may render at once on a wide screen before they start folded\n * ({@link TranslationReviewPanelProps.defaultGroupsOpen}): two of the default 50-row pages.\n * A row costs about twenty elements in either layout, so this is the cost of two pages of\n * the ungrouped table — not of 2053 rows in 122 tables.\n */\nconst GROUPS_OPEN_ROW_BUDGET = 100;\n\n/**\n * Would opening every group stay within the budget? Each group renders up to `pageSize`\n * rows (it pages on its own); the budget is never less than one page, so a single group —\n * which renders at most one — always starts open, as on a phone.\n */\nfunction groupsFitOpen(groups: readonly TranslationRowGroup[], pageSize: number): boolean {\n const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);\n let rendered = 0;\n for (const group of groups) {\n rendered += Math.min(group.rows.length, pageSize);\n if (rendered > budget) return false;\n }\n return true;\n}\n\n/**\n * The kit's dev switch, as NumberInput's money guard and `lib/logger.ts` read it: a\n * development build — the consumer's bundler makes `import.meta.env.DEV` false in a\n * production one, and the branch is dead code there — and never under vitest, where every\n * consumer suite would print it; a test opts in with `vi.stubEnv(\"VITEST\", \"\")`. Read per\n * call, and optional-chained: `import.meta.env` is a Vite injection, absent under Node.\n */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\ntype FilterField = keyof TranslationReviewFilter;\n\n/** How long after the last filter change the app's `filter` is looked at again: well past\n * a router's transition, so a URL that is still being written is not mistaken for one\n * that never will be. */\nconst FILTER_ECHO_MS = 1500;\n\n/**\n * The development warning for a controlled filter that does not follow (Kurvenschmiede,\n * 0.25.1). A field the app passes in `filter` is the app's: the panel shows the app's\n * value, whatever the reviewer picked. An app that passes a field but does not write the\n * reported value back — its `onFilterChange` hands on only some fields, `filter` is built\n * as `{ ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...fromTheUrl }`, a URL parser rejects what\n * its serialiser wrote, or there is no `onFilterChange` at all — has a control that\n * silently does nothing: the box unticks itself, the select jumps back.\n *\n * So, after the panel reports a change of a field the app controls, it looks at the\n * app's `filter` again once the change has had time to land ({@link FILTER_ECHO_MS}, read\n * from the last COMMITTED props, so a router transition still rendering does not count),\n * and warns — once per field — when that field still holds the value it had before.\n *\n * Quiet by construction for an app that passes no `filter`, and for a field the app LEAVES\n * OUT: that field is the panel's own and works (keksdose keeps the placeholder switch so,\n * on purpose). Quiet too when the app moved the field somewhere else itself. Development\n * builds only (see {@link devWarnings}): in production and under vitest nothing is\n * recorded and no timer is set.\n */\nfunction useFilterEchoCheck(filterProp: Partial<TranslationReviewFilter> | undefined) {\n const committed = useRef(filterProp);\n useEffect(() => {\n committed.current = filterProp;\n });\n // Per field: what the app's filter said when the reviewer first changed it, and what\n // the panel reported last.\n const pending = useRef(new Map<FilterField, { from: unknown; to: unknown }>());\n const warned = useRef(new Set<FilterField>());\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(timer.current), []);\n\n return (next: TranslationReviewFilter) => {\n if (filterProp === undefined || !devWarnings()) return;\n for (const field of Object.keys(next) as FilterField[]) {\n const from = filterProp[field];\n if (from === undefined || warned.current.has(field)) continue;\n const seen = pending.current.get(field);\n if (seen) seen.to = next[field];\n else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });\n }\n if (pending.current.size === 0) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n const now = committed.current;\n for (const [field, { from, to }] of pending.current) {\n if (to === from || now?.[field] !== from || warned.current.has(field)) continue;\n warned.current.add(field);\n console.warn(\n `[ui-kit] TranslationReviewPanel: the \"${field}\" filter was changed to ${JSON.stringify(to)}, but the ` +\n `\\`filter\\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in ` +\n `\\`filter\\` is the app's: write back every field \\`onFilterChange\\` reports, in one update ` +\n `(useSearchParamsState's setter takes the object as it is), or leave the field out of \\`filter\\` and ` +\n `the panel keeps it. (Development builds only; once per field.)`,\n );\n }\n pending.current.clear();\n }, FILTER_ECHO_MS);\n };\n}\n\n/** The row's text — or, for a missing string, the line that says so. */\nfunction TextCell({ row, labels, clamp }: { row: TranslationRow; labels: TranslationReviewLabels; clamp?: boolean }) {\n return row.text === \"\" ? (\n <span className=\"italic text-muted\">{labels.missingText}</span>\n ) : (\n <span lang={row.locale} className={cn(\"whitespace-pre-line break-words\", clamp && \"line-clamp-3 large:line-clamp-none\")}>\n {row.text}\n </span>\n );\n}\n\n/**\n * The page body for one locale: progress, filters (status, source, namespace, search,\n * placeholder problems), the strings with an editor that unfolds under each, and the\n * bulk actions over a selection.\n *\n * The status filter is the work queue in order — what is not there, what nobody has\n * read, what changed after it was read, what was sent back, what is done — with each\n * count on it. \"Missing\" is offered only where something is.\n *\n * Bulk approve takes the selected rows that are not approved yet and have a text:\n * approving an absent string says nothing, and re-approving one says it again. On a\n * phone the table has no checkboxes, so \"Select all shown\" selects what the filters\n * show — filter to `Unreviewed` in one area, read, select, approve. With `groupBy`, a\n * group's own \"Approve unreviewed\" is the shorter way, and with `swipe` a card is approved\n * by a swipe.\n *\n * The panel's writes go out ONE AT A TIME, in the order they were asked for: two writes on\n * one locale's verdicts can answer out of order, and an Undo must not overtake the\n * approval it takes back. A swipe on the next card while one is out is queued rather than\n * refused — a reviewer swiping down a list is faster than the server.\n */\nexport function TranslationReviewPanel({\n rows: rowsProp,\n localeLabel,\n referenceLabel,\n onSave,\n onClear,\n readOnly = false,\n areas,\n areaLabels,\n inArea,\n sourceLabels,\n filter: filterProp,\n onFilterChange,\n progress = true,\n pageSize = 50,\n storageKey,\n storageKeyPrefix,\n formatDate,\n formatError,\n swipe = false,\n undo: undoProp,\n groupBy,\n groupLabel,\n groupHeadingAs = \"h2\",\n defaultGroupsOpen = \"auto\",\n className,\n labels: labelsProp,\n}: TranslationReviewPanelProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const locale = useKitLocale();\n // A verdict is an admin route, which a lapsed plan never locks\n // (docs/billing-harmonization.md §12.13, §12.36): the buttons take\n // COMMIT_EXCEPT_BILLING, and the swipes read the lock as they see it.\n const lock = useWriteLock(COMMIT_EXCEPT_BILLING);\n // The desktop actions column's plain-text name (dev#598), the provider's \"Actions\".\n const rowActionLabels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS);\n // The phone layout DataTable switches its layout on, so \"a phone\" means the same here\n // as in the table, at every text size: `usePhoneLayout()` itself (§10.4; 0.32.1,\n // keksdose's 0.32 report — `!useBreakpoint(\"md\")` could disagree with it at 767.5 px).\n const phone = usePhoneLayout();\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const editable = !readOnly && onSave !== undefined;\n const binding: TranslationReviewSwipeBinding | null =\n swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;\n const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);\n\n const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);\n // The app's fields over the panel's own; a field the app leaves `undefined` is the\n // panel's, not a blank.\n const filter: TranslationReviewFilter = { ...ownFilter };\n for (const [name, value] of Object.entries(filterProp ?? {})) {\n if (value !== undefined) Object.assign(filter, { [name]: value });\n }\n const echoFilter = useFilterEchoCheck(filterProp);\n const [openId, setOpenId] = useState<string | null>(null);\n // The row whose editor a swipe opened, to be written in: its wording field takes focus.\n const [focusId, setFocusId] = useState<string | null>(null);\n const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());\n // The writes asked for and not yet answered, in order: the first is the one out. The\n // editor keeps its own, for its own buttons.\n const [queued, setQueued] = useState<readonly string[]>([]);\n const claimed = useRef(new Set<string>());\n const tail = useRef<Promise<unknown>>(Promise.resolve());\n const undoCount = useRef(0);\n const [failure, setFailure] = useState<ReactNode>(null);\n const busy = queued.length > 0;\n const isQueued = (key: string) => queued.includes(key);\n // The rows whose open editor has a write of its own out: their swipes stand down until\n // it is answered, as for a write in the queue (0.46, keksdose live #416).\n const [editorWrites, setEditorWrites] = useState<ReadonlySet<string>>(() => new Set());\n const whileEditorWrites = async (rowId: string, write: () => MaybePromise) => {\n setEditorWrites((prev) => new Set(prev).add(rowId));\n try {\n await write();\n } finally {\n setEditorWrites((prev) => {\n const next = new Set(prev);\n next.delete(rowId);\n return next;\n });\n }\n };\n\n const rows = useMemo(\n () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),\n [rowsProp, areas, inArea],\n );\n const summary = useMemo(() => summariseRows(rows), [rows]);\n const sources = useMemo(() => [...new Set(rows.map((r) => r.source))], [rows]);\n const source = sources.includes(filter.source) ? filter.source : \"all\";\n const namespaces = useMemo(\n () => [...new Set(rows.filter((r) => source === \"all\" || r.source === source).map((r) => r.namespace))],\n [rows, source],\n );\n const namespace = namespaces.includes(filter.namespace) ? filter.namespace : \"all\";\n const mismatches = useMemo(() => rows.filter((r) => r.placeholderMismatch).length, [rows]);\n // `filter` is rebuilt every render; its fields are what the list depends on.\n const { status, query, placeholdersOnly } = filter;\n const visible = useMemo(\n () => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),\n [rows, status, source, namespace, query, placeholdersOnly],\n );\n\n // ── Groups ──\n const groups = useMemo(() => {\n if (groupBy === undefined) return null;\n const of =\n groupBy === \"namespace\"\n ? (r: TranslationRow) => r.namespace\n : groupBy === \"source\"\n ? (r: TranslationRow) => r.source\n : groupBy;\n return groupTranslationRows(visible, of);\n }, [visible, groupBy]);\n const groupName = (key: string) =>\n groupLabel?.(key) ?? (groupBy === \"source\" ? sourceLabels?.[key] : undefined) ?? (key || \"—\");\n // Only what the reviewer chose; a group nobody has touched follows the default, which\n // follows the filters (see `defaultGroupsOpen`).\n const [groupOpen, setGroupOpen] = useState<Readonly<Record<string, boolean>>>({});\n const groupOpenByDefault =\n defaultGroupsOpen !== \"auto\"\n ? defaultGroupsOpen\n : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));\n const isGroupOpen = (key: string) => groupOpen[key] ?? groupOpenByDefault;\n const setGroupOpenFor = (key: string, open: boolean) => setGroupOpen((prev) => ({ ...prev, [key]: open }));\n /** A group the reviewer works in — an editor opened in it — stays open when the default\n * turns (a search cleared, the rows grown past the budget): folding it would drop the\n * editor and whatever was typed in it. */\n const keepGroupOpen = (key: string | undefined) => {\n if (key !== undefined) setGroupOpen((prev) => (key in prev ? prev : { ...prev, [key]: true }));\n };\n // The group whose large batch is waiting for a yes — and, until its confirm button\n // has mounted and taken focus, the group that asked.\n const [confirming, setConfirming] = useState<string | null>(null);\n const confirmFocus = useRef<string | null>(null);\n // One view for every group's table — one sort, so a header click orders all of them\n // alike, and one page size — kept under `storageKey` in the single table's own blob, so\n // switching the grouping keeps both (0.35, Kurvenschmiede).\n const groupView = useSharedTableView({\n active: groupBy !== undefined,\n storageKey,\n storageKeyPrefix,\n defaultPageSize: pageSize,\n });\n\n const setFilter = (patch: Partial<TranslationReviewFilter>) => {\n const next = { ...filter, ...patch };\n setOwnFilter(next);\n onFilterChange?.(next);\n echoFilter(next);\n // A selection the new filter hides would be acted on unseen.\n setSelected(new Set());\n };\n\n /** Queue a write under `key` (one per key at a time); resolves whether it worked. */\n const run = (key: string, action: () => MaybePromise): Promise<boolean> => {\n if (claimed.current.has(key)) return Promise.resolve(false);\n claimed.current.add(key);\n setQueued((q) => [...q, key]);\n const step = tail.current.then(async () => {\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n claimed.current.delete(key);\n setQueued((q) => q.filter((k) => k !== key));\n }\n });\n tail.current = step;\n return step;\n };\n\n /** \"String approved — Undo\", once `targets` (as they were BEFORE the write) are\n * approved — or, after a swiped `clear`, \"String marked unreviewed — Undo\". Either way\n * Undo puts back what the rows said before ({@link reviewUndo}), through the same queue\n * as every write. */\n const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {\n const back = reviewUndo(targets);\n const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);\n const save = onSave;\n const clear = onClear;\n // Taking back the approval of a row that had no verdict is a clear: without\n // `onClear` there is no way to say it, so the toast reports and offers nothing.\n if (!save || (back.clears.length > 0 && !clear)) {\n toast.success(message, { description });\n return;\n }\n const key = `undo:${++undoCount.current}`;\n toast.undo(message, {\n description,\n onUndo: () =>\n void run(key, async () => {\n if (back.writes.length > 0) await save(back.writes, { origin: \"undo\", toasted: false });\n if (back.clears.length > 0) await clear?.(back.clears, { origin: \"undo\", toasted: false });\n }),\n });\n };\n\n const approve = async (\n key: string,\n targets: readonly TranslationRow[],\n origin: TranslationReviewOrigin,\n description?: ReactNode,\n ): Promise<boolean> => {\n if (!onSave || targets.length === 0) return false;\n const save = onSave;\n const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, \"APPROVED\")), { origin, toasted: undoOn }));\n if (ok && undoOn) sayDone(targets, description);\n return ok;\n };\n\n /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —\n * the Undo toast stores the verdict again. Under the row's own key, so it queues behind\n * (and never overlaps) an approval of the same row. */\n const clearRow = async (row: TranslationRow, origin: TranslationReviewOrigin = \"swipe\") => {\n if (!onClear || row.review === null) return;\n const clear = onClear;\n const keys = [{ locale: row.locale, key: row.key }];\n const ok = await run(`row:${row.id}`, () => clear(keys, { origin, toasted: undoOn }));\n if (ok && undoOn) sayDone([row], row.key, true);\n };\n\n /** The editor's own save: it waits, closes and shows its errors itself, so it does\n * not go through the queue — but an approval there is answered like any other. */\n const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {\n const toasted = undoOn && write.verdict === \"APPROVED\";\n await whileEditorWrites(row.id, () => onSave?.([write], { origin: \"editor\", toasted }));\n if (toasted) sayDone([row], row.key);\n };\n\n /**\n * Focus back into the panel once a group's batch has settled, if it fell out: the\n * button that was pressed goes with the rows it approved (and under the `Unreviewed`\n * filter the whole group can go). Two frames, so the app's new rows have been drawn.\n */\n const keepFocusAt = (groupKey: string) => {\n const check = () => {\n const root = rootRef.current;\n const active = document.activeElement;\n if (!root || (active && active !== document.body && active.isConnected)) return;\n const sections = [...root.querySelectorAll<HTMLElement>(\"[data-review-group]\")];\n const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];\n section?.querySelector<HTMLElement>(\"[data-disclosure-trigger]\")?.focus();\n };\n requestAnimationFrame(() => requestAnimationFrame(check));\n };\n\n const approveGroup = async (group: TranslationRowGroup, confirmed: boolean) => {\n const targets = unreviewedRows(group.rows);\n if (targets.length === 0) return;\n if (!confirmed && targets.length > pageSize) {\n // Asked inside the opened group, over the rows it is about.\n setGroupOpenFor(group.key, true);\n setConfirming(group.key);\n confirmFocus.current = group.key;\n return;\n }\n const ok = await approve(`group:${group.key}`, targets, \"group\", groupName(group.key));\n if (ok) setConfirming((key) => (key === group.key ? null : key));\n keepFocusAt(group.key);\n };\n\n const visibleIds = new Set(visible.map((r) => r.id));\n const selectedRows = visible.filter((r) => selected.has(r.id));\n // What \"Select all shown\" takes: grouped, the rows of the open groups — a folded\n // group's rows are not shown.\n const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;\n const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));\n const toApprove = selectedRows.filter((r) => r.text !== \"\" && r.status !== \"approved\");\n const toReset = selectedRows.filter((r) => r.review !== null);\n\n const approveSelected = async () => {\n if (await approve(\"bulk-approve\", toApprove, \"selection\")) setSelected(new Set());\n };\n const resetSelected = async () => {\n if (!onClear || toReset.length === 0) return;\n const clear = onClear;\n const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));\n if (await run(\"bulk-reset\", () => clear(keys, { origin: \"selection\", toasted: false }))) {\n setSelected(new Set());\n }\n };\n\n // The status options: \"missing\" only where something is (or it is the filter).\n const statuses = REVIEW_STATUSES.filter((s) => s !== \"missing\" || summary.missing > 0 || filter.status === s);\n const scopeNames = (areas ?? []).map((area) => areaLabels?.[area] ?? area);\n const scopeText =\n scopeNames.length > 0 ? new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" }).format(scopeNames) : \"\";\n\n const columns: DataTableColumn<TranslationRow>[] = [\n {\n key: \"key\",\n header: labels.key,\n cell: (r) => <code className=\"break-all text-xs text-secondary\">{r.key}</code>,\n sortBy: (r) => r.key,\n mobileHidden: true,\n },\n {\n key: \"reference\",\n header: referenceLabel,\n cell: (r) => (\n <span className=\"line-clamp-3 whitespace-pre-line break-words text-secondary\">{r.reference}</span>\n ),\n mobileHidden: true,\n },\n {\n key: \"text\",\n header: localeLabel,\n cell: (r) => <TextCell row={r} labels={labels} clamp />,\n mobilePrimary: true,\n },\n {\n key: \"status\",\n header: labels.statusColumn,\n cell: (r) => (\n <span className=\"inline-flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </span>\n ),\n sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),\n },\n ];\n if (editable && binding) {\n // keksdose dev#598 item 2: with a binding, the desktop row offers what the card's\n // swipes offer — `clear` was reachable only by a swipe. Approve first on every row\n // that can take it, bound or not (the desktop always had it), then the binding's\n // actions, end side first, each once. The swipe's words, glyphs and handlers; unlike\n // the swipe, a locked row keeps its buttons and says why (`commit`), and a row whose\n // write is out shows it in flight. Hidden on the phone, where the swipes are.\n columns.push(\n rowActionsColumn<TranslationRow>({\n key: \"actions\",\n headerText: rowActionLabels.actions,\n name: (r) => r.key,\n tooltipSide: \"start\",\n actions: (r) =>\n translationReviewSwipePlan(\n { end: [\"approve\", ...(binding.end ?? []), ...(binding.start ?? [])] },\n r,\n { canClear: onClear !== undefined },\n ).end.map((id): RowAction => {\n const swipe = swipeAction(r, id);\n // `edit` only opens the editor, whose own Save is the commit.\n const writes = id !== \"edit\";\n return {\n key: id,\n // `edit` opens the editor: its own name, apart from the editor's submit\n // (\"Needs a change\"), which saves (keksdose B2/S3).\n label: id === \"edit\" ? (r.text === \"\" ? labels.suggestRow : labels.flagRow) : swipe.label,\n icon: <>{swipe.icon}</>,\n tone: \"muted\",\n commit: writes ? COMMIT_EXCEPT_BILLING : undefined,\n pending: writes && isQueued(`row:${r.id}`),\n disabled: busy,\n onSelect:\n id === \"approve\"\n ? () => void approve(`row:${r.id}`, [r], \"row\", r.key)\n : id === \"clear\"\n ? () => void clearRow(r, \"row\")\n : // The row's group stays open while its editor is (`keepGroupOpen`).\n () => swipeAction(r, id, groups?.find((g) => g.rows.includes(r))?.key).onCommit(),\n };\n }),\n column: { mobileHidden: true },\n }),\n );\n } else if (editable) {\n columns.push({\n key: \"approve\",\n header: <span className=\"sr-only\">{labels.approve}</span>,\n cell: (r) =>\n r.status === \"approved\" || r.text === \"\" ? null : (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit={COMMIT_EXCEPT_BILLING}\n label={labels.approve}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // the row's one action, in a hugging column of a dense table beside the\n // string under review, which \"Approve\" as text at Large would squeeze. A\n // single action stays inline in `RowActions` too, so the menu would not help;\n // the swipe and the editor say it in words.\n labelVisible={false}\n pending={isQueued(`row:${r.id}`)}\n disabled={busy}\n onClick={(event) => {\n // The row's own click opens the editor; this one only approves.\n event.stopPropagation();\n void approve(`row:${r.id}`, [r], \"row\", r.key);\n }}\n >\n <Check />\n </IconButton>\n ),\n });\n }\n\n /** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */\n const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {\n const missing = r.text === \"\";\n switch (id) {\n case \"approve\":\n return {\n id,\n label: labels.approve,\n icon: <Check className=\"size-4\" aria-hidden />,\n onCommit: () => void approve(`row:${r.id}`, [r], \"swipe\", r.key),\n // SwipeableRow paints the tone: its soft wash idle, its fill and that fill's\n // foreground armed (0.33).\n tone: \"success\",\n };\n case \"edit\":\n return {\n id,\n label: missing ? labels.suggest : labels.flag,\n icon: <PencilLine className=\"size-4\" aria-hidden />,\n // Opens the editor rather than sending the string back: \"needs a change\"\n // with neither a note nor a better wording tells nobody anything.\n onCommit: () => {\n keepGroupOpen(groupKey);\n setOpenId(r.id);\n setFocusId(r.id);\n // On a row already open (#416) the editor is mounted and keeps its draft:\n // the cursor goes to the end of its wording now.\n if (openId === r.id) {\n const field = rootRef.current?.querySelector<HTMLTextAreaElement>(\"[data-review-wording]\");\n field?.focus();\n field?.setSelectionRange(field.value.length, field.value.length);\n }\n },\n tone: missing ? \"brand\" : \"danger\",\n };\n case \"clear\":\n return {\n id,\n label: labels.reset,\n icon: <RotateCcw className=\"size-4\" aria-hidden />,\n onCommit: () => void clearRow(r),\n // A step back, not a verdict: neither the approval's green nor the send-back's\n // red. `neutral` is the pair this swipe improvised and 0.33 made a token: the\n // muted ink as a fill under the surface colour.\n tone: \"neutral\",\n };\n }\n };\n\n /**\n * A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved\n * against the row, each side's ladder closed up over what the row cannot offer. None\n * where the row must not be acted on — the same conditions under which its buttons are\n * locked or absent.\n */\n const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {\n if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`) || editorWrites.has(r.id)) return null;\n const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });\n if (plan.start.length === 0 && plan.end.length === 0) return null;\n return {\n start: plan.start.map((id) => swipeAction(r, id, groupKey)),\n end: plan.end.map((id) => swipeAction(r, id, groupKey)),\n };\n };\n\n const toggleIn = (prev: ReadonlySet<string>, many: readonly TranslationRow[], checked: boolean) => {\n const next = new Set([...prev].filter((id) => visibleIds.has(id)));\n for (const r of many) {\n if (checked) next.add(r.id);\n else next.delete(r.id);\n }\n return next;\n };\n /** The checkbox column of a table over `list` — the whole list, or one group. */\n const selectionFor = (list: readonly TranslationRow[]): DataTableProps<TranslationRow>[\"selection\"] => {\n if (!editable) return undefined;\n const picked = list.filter((r) => selected.has(r.id)).length;\n return {\n isSelected: (r) => selected.has(r.id),\n onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),\n onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),\n allSelected: list.length > 0 && picked === list.length,\n someSelected: picked > 0 && picked < list.length,\n onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked)),\n };\n };\n\n const table = (list: TranslationRow[], group?: { key: string; name: string }) => (\n <DataTable<TranslationRow>\n rows={list}\n rowKey={(r) => r.id}\n columns={columns}\n empty={labels.empty}\n defaultPageSize={pageSize}\n {...(group\n ? {\n // Inside the group's own card: no frame in a frame, no column rail per group,\n // no scroller per group, and the group's name as the table's.\n frame: false,\n chrome: \"minimal\" as const,\n maxBodyHeight: \"none\",\n sorts: groupView.sorts,\n onSortsChange: groupView.setSorts,\n pageSize: groupView.pageSize,\n onPageSizeChange: groupView.setPageSize,\n labels: { table: group.name },\n }\n : { storageKey, storageKeyPrefix })}\n isExpanded={(r) => r.id === openId}\n onRowClick={(r) => {\n if (openId !== r.id) keepGroupOpen(group?.key);\n setFocusId(null);\n setOpenId((open) => (open === r.id ? null : r.id));\n }}\n expandedRow={(r) => (\n <TranslationReviewEditor\n key={r.id}\n row={r}\n referenceLabel={referenceLabel}\n localeLabel={localeLabel}\n readOnly={!editable}\n focusWording={focusId === r.id}\n onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}\n onClear={\n onClear\n ? (key) => whileEditorWrites(r.id, () => onClear([key], { origin: \"editor\", toasted: false }))\n : undefined\n }\n onClose={() => {\n setOpenId(null);\n setFocusId(null);\n }}\n formatDate={formatDate}\n formatError={formatError}\n labels={labelsProp}\n />\n )}\n mobileCard={(r) => (\n <div className=\"min-w-0 space-y-1\">\n <code className=\"block break-all text-xs text-muted\">{r.key}</code>\n <div className=\"text-sm text-primary\">\n <TextCell row={r} labels={labels} clamp />\n </div>\n <div className=\"flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </div>\n </div>\n )}\n mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}\n // The desktop's buttons for the binding are the panel's own \"actions\" column.\n desktopSwipeActions={false}\n selection={selectionFor(list)}\n />\n );\n\n const groupSection = (g: TranslationRowGroup, index: number) => {\n const name = groupName(g.key);\n const titleId = `${baseId}-group-${index}`;\n const questionId = `${baseId}-confirm-${index}`;\n const targets = editable ? unreviewedRows(g.rows) : [];\n const key = `group:${g.key}`;\n const open = isGroupOpen(g.key);\n // A folded group renders its header and nothing else. Its body is handed over only\n // while it is open, or when the REVIEWER folded it — so the fold animates over the\n // rows it hides. A group folded by the default gets none: when the default turns (a\n // search cleared over 122 areas), the groups that close would otherwise render their\n // grown tables for the length of the fold.\n const body = open || groupOpen[g.key] === false;\n return (\n <Disclosure\n key={g.key}\n data-review-group={g.key}\n headingAs={groupHeadingAs}\n title={<span id={titleId}>{name}</span>}\n hint={labels.groupCount(g.summary.unreviewed, g.summary.total)}\n open={open}\n onOpenChange={(next) => {\n setGroupOpenFor(g.key, next);\n if (!next) setConfirming((c) => (c === g.key ? null : c));\n }}\n trailing={\n targets.length > 0 ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n // The visible words say what; the group's name, read after them, says where.\n aria-describedby={titleId}\n pending={isQueued(key) && confirming !== g.key}\n disabled={busy}\n onClick={() => void approveGroup(g, false)}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveGroup(targets.length)}\n </Button>\n ) : undefined\n }\n bodyClassName=\"space-y-0 px-0 pb-0 border-t border-subtle\"\n >\n {body && confirming === g.key && targets.length > 0 && (\n <div className=\"border-b border-subtle p-3\">\n <AlertBanner tone=\"warning\" size=\"sm\">\n <div className=\"min-w-0 space-y-2\">\n <p id={questionId}>{labels.confirmGroup(targets.length, name)}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n // Focus lands here when the question appears: the expected answer to a\n // question the reviewer just asked for, and Undo stays behind it.\n ref={(el) => {\n if (el && confirmFocus.current === g.key) {\n confirmFocus.current = null;\n el.focus();\n }\n }}\n variant=\"primary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n aria-describedby={questionId}\n pending={isQueued(key)}\n disabled={busy}\n onClick={() => void approveGroup(g, true)}\n >\n {labels.approveGroup(targets.length)}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={isQueued(key)}\n onClick={() => {\n setConfirming(null);\n keepFocusAt(g.key);\n }}\n >\n {labels.cancel}\n </Button>\n </div>\n </div>\n </AlertBanner>\n </div>\n )}\n {body && table(g.rows, { key: g.key, name })}\n </Disclosure>\n );\n };\n return (\n <div ref={rootRef} className={cn(\"min-w-0 space-y-4\", className)}>\n {scopeText && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.scope(scopeText)}\n </AlertBanner>\n )}\n {readOnly && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.readOnly}\n </AlertBanner>\n )}\n\n {progress && <TranslationProgress summary={summary} labels={labelsProp} />}\n\n <ToggleGroup<ReviewStatus | \"all\">\n aria-label={labels.statusFilter}\n overflow=\"wrap\"\n size=\"sm\"\n value={filter.status}\n onChange={(next) => setFilter({ status: next })}\n options={[\n { value: \"all\", label: labels.filterCount(labels.all, summary.total) },\n ...statuses.map((s) => ({ value: s, label: labels.filterCount(reviewStatusLabel(labels, s), summary[s]) })),\n ]}\n />\n\n <div className=\"flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end\">\n {sources.length > 1 && (\n <div className=\"md:w-48\">\n <Select label={labels.source} value={source} onChange={(e) => setFilter({ source: e.target.value })}>\n <option value=\"all\">{labels.allSources}</option>\n {sources.map((s) => (\n <option key={s} value={s}>\n {sourceLabels?.[s] ?? s}\n </option>\n ))}\n </Select>\n </div>\n )}\n <div className=\"md:w-56\">\n <Select label={labels.namespace} value={namespace} onChange={(e) => setFilter({ namespace: e.target.value })}>\n <option value=\"all\">{labels.allNamespaces}</option>\n {namespaces.map((ns) => (\n <option key={ns} value={ns}>\n {ns}\n </option>\n ))}\n </Select>\n </div>\n <SearchField\n className=\"min-w-0 md:flex-1\"\n aria-label={labels.search}\n value={filter.query}\n onChange={(next) => setFilter({ query: next })}\n />\n <Checkbox\n label={labels.placeholdersOnly(mismatches)}\n checked={filter.placeholdersOnly}\n onCheckedChange={(next) => setFilter({ placeholdersOnly: next })}\n />\n </div>\n\n {editable && shownRows.length > 0 && (\n // The phone's table has no checkboxes; this is its way into the bulk actions.\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"md:hidden\"\n disabled={allShownSelected}\n onClick={() => setSelected((prev) => toggleIn(prev, shownRows, true))}\n >\n <ListChecks className=\"size-4\" aria-hidden />\n {labels.selectShown}\n </Button>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n\n {editable && (\n <BulkActionBar count={selectedRows.length} onClear={() => setSelected(new Set())}>\n <Button\n variant=\"primary\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={isQueued(\"bulk-approve\")}\n disabled={busy || toApprove.length === 0}\n onClick={() => void approveSelected()}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveSelected}\n </Button>\n {onClear && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={isQueued(\"bulk-reset\")}\n disabled={busy || toReset.length === 0}\n onClick={() => void resetSelected()}\n >\n <RotateCcw className=\"size-4\" aria-hidden />\n {labels.resetSelected}\n </Button>\n )}\n </BulkActionBar>\n )}\n\n {groups && groups.length > 0 ? (\n <div className=\"space-y-3\">{groups.map(groupSection)}</div>\n ) : (\n table(visible)\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["ReviewStatusChip","TranslationProgress","TranslationReviewEditor","DEFAULT_TRANSLATION_REVIEW_LABELS","swipe"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/write-lock-labels.ts"],"sourcesContent":["// The English words of ./write-lock, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./write-lock re-exports them.\n\nimport type { WriteLockLabels } from \"./write-lock\";\n\nexport const DEFAULT_WRITE_LOCK_LABELS: WriteLockLabels = {\n reason: \"You can view this but not change it.\",\n};\n"],"mappings":";AAMO,MAAM,4BAA6C;AAAA,EACxD,QAAQ;
|
|
1
|
+
{"version":3,"sources":["../../src/components/write-lock-labels.ts"],"sourcesContent":["// The English words of ./write-lock, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./write-lock re-exports them.\n\nimport type { WriteLockLabels } from \"./write-lock\";\n\nexport const DEFAULT_WRITE_LOCK_LABELS: WriteLockLabels = {\n reason: \"You can view this but not change it.\",\n offline: \"Needs a connection.\",\n};\n"],"mappings":";AAMO,MAAM,4BAA6C;AAAA,EACxD,QAAQ;AAAA,EACR,SAAS;AACX;","names":[]}
|
|
@@ -5,17 +5,19 @@ import { DEFAULT_WRITE_LOCK_LABELS } from "./write-lock-labels.js";
|
|
|
5
5
|
export interface WriteLockLabels {
|
|
6
6
|
/** The tooltip of a locked commit when the provider gives no `reason`. */
|
|
7
7
|
reason: string;
|
|
8
|
+
/** 0.46 (kastlan K4): the reason of {@link useOfflineWriteLock} — "Needs a connection." */
|
|
9
|
+
offline: string;
|
|
8
10
|
}
|
|
9
11
|
export { DEFAULT_WRITE_LOCK_LABELS };
|
|
10
12
|
/**
|
|
11
13
|
* What kind of lock a source is (docs/billing-harmonization.md §12.36, ui-kit 0.33).
|
|
12
|
-
* `"demo"` (the read-only demo, `demo.writeLocked`)
|
|
13
|
-
* `billing.lockReason`) are the kit's; an app names its own — Kurvenschmiede's
|
|
14
|
+
* `"demo"` (the read-only demo, `demo.writeLocked`), `"billing"` (a lapsed plan,
|
|
15
|
+
* `billing.lockReason`) and, since 0.46, `"offline"` ({@link OFFLINE_LOCK}) are the kit's; an app names its own — Kurvenschmiede's
|
|
14
16
|
* `"access"` (a curve shared to view) and `"customer"` (an account that owns nothing).
|
|
15
17
|
* A control exempts itself from a kind with {@link CommitScope}; a source without a
|
|
16
18
|
* kind is never exempted.
|
|
17
19
|
*/
|
|
18
|
-
export type WriteLockKind = "demo" | "billing" | (string & {});
|
|
20
|
+
export type WriteLockKind = "demo" | "billing" | "offline" | (string & {});
|
|
19
21
|
/** One locked source, as the provider holds it: its kind and its one sentence. */
|
|
20
22
|
export interface WriteLockHold {
|
|
21
23
|
kind: WriteLockKind | undefined;
|
|
@@ -44,6 +46,19 @@ export type CommitScope = boolean | {
|
|
|
44
46
|
export declare const COMMIT_EXCEPT_BILLING: {
|
|
45
47
|
readonly except: readonly ["billing"];
|
|
46
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* 0.46 (kastlan K4, docs/local-first-harmonization.md D4): the kind of the lock "this
|
|
51
|
+
* device has no connection". An action that only the server can do (numbers, money,
|
|
52
|
+
* mail, people, settings) is locked with "Needs a connection." while offline; a control
|
|
53
|
+
* that writes to the device's copy and syncs later takes {@link COMMIT_EXCEPT_OFFLINE}.
|
|
54
|
+
* Feed it with {@link useOfflineWriteLock}.
|
|
55
|
+
*/
|
|
56
|
+
export declare const OFFLINE_LOCK: "offline";
|
|
57
|
+
/** Every lock but the offline one: the scope of a control that works on the device's copy
|
|
58
|
+
* (it is still locked for a demo or a lapsed plan). */
|
|
59
|
+
export declare const COMMIT_EXCEPT_OFFLINE: {
|
|
60
|
+
readonly except: readonly ["offline"];
|
|
61
|
+
};
|
|
47
62
|
/**
|
|
48
63
|
* What {@link useWriteLock} returns: whether commits are locked here as a control with
|
|
49
64
|
* the scope asked for sees it, and why.
|
|
@@ -152,3 +167,18 @@ export declare function useWriteLock(scope?: CommitScope): WriteLock;
|
|
|
152
167
|
* commit controls.
|
|
153
168
|
*/
|
|
154
169
|
export declare function useCommitReason(commit: CommitScope | undefined, own: ReactNode): ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* 0.46 (kastlan K4): the offline lock as one source of `combineWriteLocks` —
|
|
172
|
+
* `{ locked: offline, kind: "offline", reason: writeLock.offline }` ("Needs a
|
|
173
|
+
* connection.", seven languages). `offline` is the app's own reading of the connection
|
|
174
|
+
* (`!navigator.onLine`, or its better guess); the kit does not watch the network here.
|
|
175
|
+
* List it after the demo's and billing's sources: the server refuses those first.
|
|
176
|
+
*
|
|
177
|
+
* <WriteLockProvider {...combineWriteLocks([demo, billing, useOfflineWriteLock(!online)])}>
|
|
178
|
+
* <Button commit={COMMIT_EXCEPT_OFFLINE}>Save handover</Button>
|
|
179
|
+
*/
|
|
180
|
+
export declare function useOfflineWriteLock(offline: boolean, labels?: LabelOverride<WriteLockLabels>): {
|
|
181
|
+
locked: boolean;
|
|
182
|
+
kind: typeof OFFLINE_LOCK;
|
|
183
|
+
reason: string;
|
|
184
|
+
};
|
|
@@ -5,6 +5,8 @@ import { useKitLabels } from "../i18n/kit-labels.js";
|
|
|
5
5
|
import { hasContent } from "../lib/presence.js";
|
|
6
6
|
import { DEFAULT_WRITE_LOCK_LABELS } from "./write-lock-labels.js";
|
|
7
7
|
const COMMIT_EXCEPT_BILLING = { except: ["billing"] };
|
|
8
|
+
const OFFLINE_LOCK = "offline";
|
|
9
|
+
const COMMIT_EXCEPT_OFFLINE = { except: [OFFLINE_LOCK] };
|
|
8
10
|
const NO_HOLDS = [];
|
|
9
11
|
const UNLOCKED = { locked: false, reason: void 0, kind: void 0, holds: NO_HOLDS };
|
|
10
12
|
const WriteLockContext = createContext(UNLOCKED);
|
|
@@ -42,11 +44,18 @@ function useCommitReason(commit, own) {
|
|
|
42
44
|
const lock = writeLockFor(useContext(WriteLockContext), commit);
|
|
43
45
|
return lock.locked ? lock.reason : own;
|
|
44
46
|
}
|
|
47
|
+
function useOfflineWriteLock(offline, labels) {
|
|
48
|
+
const words = useKitLabels("writeLock", DEFAULT_WRITE_LOCK_LABELS, labels);
|
|
49
|
+
return { locked: offline, kind: OFFLINE_LOCK, reason: words.offline };
|
|
50
|
+
}
|
|
45
51
|
export {
|
|
46
52
|
COMMIT_EXCEPT_BILLING,
|
|
53
|
+
COMMIT_EXCEPT_OFFLINE,
|
|
47
54
|
DEFAULT_WRITE_LOCK_LABELS,
|
|
55
|
+
OFFLINE_LOCK,
|
|
48
56
|
WriteLockProvider,
|
|
49
57
|
useCommitReason,
|
|
58
|
+
useOfflineWriteLock,
|
|
50
59
|
useWriteLock,
|
|
51
60
|
writeLockFor
|
|
52
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/write-lock.tsx"],"sourcesContent":["import { createContext, useContext, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { hasContent } from \"../lib/presence\";\nimport { DEFAULT_WRITE_LOCK_LABELS } from \"./write-lock-labels\";\n\n/** The words {@link WriteLockProvider} renders on its own behalf. */\nexport interface WriteLockLabels {\n /** The tooltip of a locked commit when the provider gives no `reason`. */\n reason: string;\n}\n\n// Defined in ./write-lock-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_WRITE_LOCK_LABELS };\n\n/**\n * What kind of lock a source is (docs/billing-harmonization.md §12.36, ui-kit 0.33).\n * `\"demo\"` (the read-only demo, `demo.writeLocked`) and `\"billing\"` (a lapsed plan,\n * `billing.lockReason`) are the kit's; an app names its own — Kurvenschmiede's\n * `\"access\"` (a curve shared to view) and `\"customer\"` (an account that owns nothing).\n * A control exempts itself from a kind with {@link CommitScope}; a source without a\n * kind is never exempted.\n */\nexport type WriteLockKind = \"demo\" | \"billing\" | (string & {});\n\n/** One locked source, as the provider holds it: its kind and its one sentence. */\nexport interface WriteLockHold {\n kind: WriteLockKind | undefined;\n reason: ReactNode;\n}\n\n/**\n * A control's opt-in to the lock — the type of every `commit` prop in the kit.\n *\n * - `true`: every lock refuses the control (a save, an add, an invite).\n * - `{ except: [kinds] }`: every lock but those kinds. A billing lock leaves the\n * reader able to remove access, revoke sessions, delete the account and change their\n * own settings (§3.3, §12.13), so the kit's parts for those take\n * {@link COMMIT_EXCEPT_BILLING} built in, and an app's own controls of that sort do\n * too.\n * - `false` or left out: no lock reaches it.\n *\n * Under a demo lock and a billing lock together, a control exempt from billing is\n * still refused, with the demo's reason.\n */\nexport type CommitScope = boolean | { readonly except: readonly WriteLockKind[] };\n\n/** Every lock but billing's: the scope of an action a lapsed plan still allows — removing\n * access, the account's own settings, an operator's admin route (§12.13). One constant,\n * so the call sites read alike. */\nexport const COMMIT_EXCEPT_BILLING = { except: [\"billing\"] } as const;\n\n/**\n * What {@link useWriteLock} returns: whether commits are locked here as a control with\n * the scope asked for sees it, and why.\n */\nexport interface WriteLock {\n locked: boolean;\n /** The one sentence a locked commit shows in its tooltip: the first hold in scope's.\n * Always set while `locked`; `undefined` when not. */\n reason: ReactNode | undefined;\n /** The first hold in scope's kind; `undefined` while unlocked, or for a source that\n * names none. */\n kind: WriteLockKind | undefined;\n /**\n * EVERY locked source the provider holds, in the order the server refuses them\n * (§12.3: the demo's 403 before billing's 402) — unfiltered by the scope, for code\n * that must know what else locks (a toast that names the plan, a swipe plan). Empty\n * while the provider is unlocked.\n */\n holds: readonly WriteLockHold[];\n}\n\nconst NO_HOLDS: readonly WriteLockHold[] = [];\n\nconst UNLOCKED: WriteLock = { locked: false, reason: undefined, kind: undefined, holds: NO_HOLDS };\n\nconst WriteLockContext = createContext<WriteLock>(UNLOCKED);\n\nexport interface WriteLockProviderProps {\n /** Commits below are locked. `false` unlocks a subtree under an outer lock. */\n locked: boolean;\n /**\n * The one sentence that says why — \"Shared with you to read\", \"Read-only demo —\n * saving is disabled\", \"A customer account cannot create curves\". The app's policy,\n * already translated. Left out: `labels.reason`.\n */\n reason?: ReactNode;\n /** What kind of lock this is — `\"demo\"`, `\"billing\"` or the app's own (see\n * {@link WriteLockKind}), for a provider of one source. Left out, no control can be\n * exempt from it. */\n kind?: WriteLockKind;\n /**\n * Every locked source, in refusal order — what `combineWriteLocks` answers, so\n * `<WriteLockProvider {...combineWriteLocks([...])}>` carries them. Given while\n * `locked`, it wins over `kind` and `reason`; a hold without a reason takes\n * `labels.reason`.\n */\n holds?: readonly WriteLockHold[];\n labels?: LabelOverride<WriteLockLabels>;\n children?: ReactNode;\n}\n\n// `null`/`\"\"`/`false` count as \"no reason given\" (`hasContent`): a lock whose tooltip is\n// empty would be a disabled button that cannot say why, the thing this replaces.\n\n/**\n * The read-only lock — **everything renders; only the commit is locked.**\n *\n * keksdose (features/budgets/write-lock.tsx, the read-only demo budget) and\n * Kurvenschmiede (features/sharing/write-lock.tsx, a curve shared to view, a customer\n * account that owns nothing) each built the same `SaveGuard`: the page, its tabs,\n * forms and dialogs render exactly as for an owner, because a viewer looking at half\n * a page is not looking at what they were sent; what changes is that the control\n * that SAVES is off and says why on hover or focus. Both wrapped each Save in a\n * Tooltip and forced a native `disabled` on it — out of the tab order, so the reason\n * never reached a keyboard — and both had to remember it at thirty call sites.\n *\n * Here the page mounts one provider and each commit control opts in with `commit`\n * ({@link Button}, {@link IconButton}, {@link FormActions}): under a lock it takes the\n * kit's `disabledReason` path with this reason — `aria-disabled` and focusable,\n * every activation swallowed, the reason in the kit Tooltip and its description.\n *\n * Who is locked is the app's: the provider takes a boolean, never a role. Fields stay\n * editable — nothing in them reaches the server until the commit is pressed, and a\n * form you cannot fill in is not a demo of the form (keksdose accounts-page). A\n * control that IS its own commit (a toggle that saves on change) reads\n * {@link useWriteLock} and disables itself. The lock is a courtesy, not a guard: the\n * server still refuses the write.\n *\n * **A lock says its source** (0.33, docs/billing-harmonization.md §12.36). A lapsed\n * plan locks creating and changing, but not removing access or the account's own\n * settings (§3.3, §12.13), and the kit's parts for those hard-coded `commit` — so\n * kastlan's app-wide lock locked a member's own sign-out everywhere. Each source now\n * has a kind, the provider holds every locked one in order (`holds`, from\n * `combineWriteLocks`), and a control's `commit` may name the kinds it is exempt from\n * ({@link CommitScope}). The kit's parts follow §3.3 and §12.13 by themselves.\n *\n * Nested providers: the nearest wins, so an inner `locked={false}` reopens a section\n * the caller may write (a viewer's own notes on a shared curve) — every source at\n * once — and an inner lock with its own reason (Kurvenschmiede's owner lock beside the\n * viewer lock) overrides the outer one's sentence and its holds.\n */\nexport function WriteLockProvider({ locked, reason, kind, holds, labels, children }: WriteLockProviderProps) {\n // The `writeLock` namespace: prop > `<UiKitProvider labels>` > English.\n const fallback = useKitLabels(\"writeLock\", DEFAULT_WRITE_LOCK_LABELS, labels).reason;\n // `holds` wins while it has any; otherwise this provider is one source of its own.\n const several = locked && holds !== undefined && holds.length > 0 ? holds : undefined;\n const firstKind = several ? several[0].kind : kind;\n const firstGiven = several ? several[0].reason : reason;\n const firstReason = hasContent(firstGiven) ? firstGiven : fallback;\n // Memoised on the first hold's kind and sentence — the strings an app passes — so a\n // one-source lock keeps one context value across renders; a list of several is\n // memoised on its own identity.\n const many = several && several.length > 1 ? several : undefined;\n const value = useMemo<WriteLock>(() => {\n if (!locked) return UNLOCKED;\n const all = many\n ? many.map((hold) => ({ kind: hold.kind, reason: hasContent(hold.reason) ? hold.reason : fallback }))\n : [{ kind: firstKind, reason: firstReason }];\n return { locked: true, reason: firstReason, kind: firstKind, holds: all };\n }, [locked, many, firstKind, firstReason, fallback]);\n return <WriteLockContext.Provider value={value}>{children}</WriteLockContext.Provider>;\n}\n\n/**\n * Whether a hold refuses a control with this scope: `true` refuses with every hold,\n * `{ except }` with every hold whose kind it does not name. A hold with no kind is\n * never exempted — an app that forgot to name its source must not reopen by accident.\n */\nfunction inScope(hold: WriteLockHold, scope: CommitScope): boolean {\n if (scope === true) return true;\n if (scope === false) return false;\n return hold.kind === undefined || !scope.except.includes(hold.kind);\n}\n\n/**\n * The lock as a control with `scope` sees it — {@link useWriteLock}'s answer, pure, for\n * a scope known only at call time: `useConfirm` reads the CALLER's lock when it is\n * called, and applies each `confirm({ commit })`'s own scope to it when it runs.\n *\n * `undefined` and `false` see no lock (a control without `commit`); `true` sees the\n * lock as it is; `{ except }` sees the first hold of another kind, with its reason\n * and kind. `holds` is passed on unfiltered.\n */\nexport function writeLockFor(lock: WriteLock, scope: CommitScope | undefined): WriteLock {\n if (!lock.locked || scope === true) return lock;\n // A lock built by hand (a test's, an app's own context) may carry no holds: it is\n // then the one source it describes.\n const holds = lock.holds.length > 0 ? lock.holds : [{ kind: lock.kind, reason: lock.reason }];\n const first = scope === undefined ? undefined : holds.find((hold) => inScope(hold, scope));\n if (!first) return { ...UNLOCKED, holds: lock.holds };\n // The first in scope is the provider's own first: the lock as it is, same object.\n if (first === holds[0]) return lock;\n return { locked: true, reason: first.reason, kind: first.kind, holds: lock.holds };\n}\n\n/**\n * The nearest {@link WriteLockProvider}'s lock, as a control with `scope` sees it —\n * unlocked with no provider. Left out, the scope is `true`: every lock.\n *\n * For code that must KNOW rather than a control that opts in: a toast that says why,\n * a swipe plan that leaves an action out, a hand-rolled toggle that disables itself.\n * `useWriteLock(COMMIT_EXCEPT_BILLING)` answers \"would a lapsed plan's lock refuse me?\"\n * as a removal sees it — what keksdose's `useWriteLockKind() !== \"billing\"` asked.\n */\nexport function useWriteLock(scope: CommitScope = true): WriteLock {\n return writeLockFor(useContext(WriteLockContext), scope);\n}\n\n/**\n * The `disabledReason` a control with `commit` should use: the lock's reason while\n * the lock refuses this scope — it wins over the control's own, since it is the answer\n * to the question the hover is asking — else the control's own. Internal to the kit's\n * commit controls.\n */\nexport function useCommitReason(commit: CommitScope | undefined, own: ReactNode): ReactNode {\n const lock = writeLockFor(useContext(WriteLockContext), commit);\n return lock.locked ? lock.reason : own;\n}\n"],"mappings":";AAkKS;AAlKT,SAAS,eAAe,YAAY,eAAe;AAEnD,SAAS,oBAAwC;AACjD,SAAS,kBAAkB;AAC3B,SAAS,iCAAiC;AA8CnC,MAAM,wBAAwB,EAAE,QAAQ,CAAC,SAAS,EAAE;AAuB3D,MAAM,WAAqC,CAAC;AAE5C,MAAM,WAAsB,EAAE,QAAQ,OAAO,QAAQ,QAAW,MAAM,QAAW,OAAO,SAAS;AAEjG,MAAM,mBAAmB,cAAyB,QAAQ;AAkEnD,SAAS,kBAAkB,EAAE,QAAQ,QAAQ,MAAM,OAAO,QAAQ,SAAS,GAA2B;AAE3G,QAAM,WAAW,aAAa,aAAa,2BAA2B,MAAM,EAAE;AAE9E,QAAM,UAAU,UAAU,UAAU,UAAa,MAAM,SAAS,IAAI,QAAQ;AAC5E,QAAM,YAAY,UAAU,QAAQ,CAAC,EAAE,OAAO;AAC9C,QAAM,aAAa,UAAU,QAAQ,CAAC,EAAE,SAAS;AACjD,QAAM,cAAc,WAAW,UAAU,IAAI,aAAa;AAI1D,QAAM,OAAO,WAAW,QAAQ,SAAS,IAAI,UAAU;AACvD,QAAM,QAAQ,QAAmB,MAAM;AACrC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,MAAM,OACR,KAAK,IAAI,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,QAAQ,WAAW,KAAK,MAAM,IAAI,KAAK,SAAS,SAAS,EAAE,IAClG,CAAC,EAAE,MAAM,WAAW,QAAQ,YAAY,CAAC;AAC7C,WAAO,EAAE,QAAQ,MAAM,QAAQ,aAAa,MAAM,WAAW,OAAO,IAAI;AAAA,EAC1E,GAAG,CAAC,QAAQ,MAAM,WAAW,aAAa,QAAQ,CAAC;AACnD,SAAO,oBAAC,iBAAiB,UAAjB,EAA0B,OAAe,UAAS;AAC5D;AAOA,SAAS,QAAQ,MAAqB,OAA6B;AACjE,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO,KAAK,SAAS,UAAa,CAAC,MAAM,OAAO,SAAS,KAAK,IAAI;AACpE;AAWO,SAAS,aAAa,MAAiB,OAA2C;AACvF,MAAI,CAAC,KAAK,UAAU,UAAU,KAAM,QAAO;AAG3C,QAAM,QAAQ,KAAK,MAAM,SAAS,IAAI,KAAK,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,OAAO,CAAC;AAC5F,QAAM,QAAQ,UAAU,SAAY,SAAY,MAAM,KAAK,CAAC,SAAS,QAAQ,MAAM,KAAK,CAAC;AACzF,MAAI,CAAC,MAAO,QAAO,EAAE,GAAG,UAAU,OAAO,KAAK,MAAM;AAEpD,MAAI,UAAU,MAAM,CAAC,EAAG,QAAO;AAC/B,SAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,MAAM,MAAM,OAAO,KAAK,MAAM;AACnF;AAWO,SAAS,aAAa,QAAqB,MAAiB;AACjE,SAAO,aAAa,WAAW,gBAAgB,GAAG,KAAK;AACzD;AAQO,SAAS,gBAAgB,QAAiC,KAA2B;AAC1F,QAAM,OAAO,aAAa,WAAW,gBAAgB,GAAG,MAAM;AAC9D,SAAO,KAAK,SAAS,KAAK,SAAS;AACrC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/write-lock.tsx"],"sourcesContent":["import { createContext, useContext, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { hasContent } from \"../lib/presence\";\nimport { DEFAULT_WRITE_LOCK_LABELS } from \"./write-lock-labels\";\n\n/** The words {@link WriteLockProvider} renders on its own behalf. */\nexport interface WriteLockLabels {\n /** The tooltip of a locked commit when the provider gives no `reason`. */\n reason: string;\n /** 0.46 (kastlan K4): the reason of {@link useOfflineWriteLock} — \"Needs a connection.\" */\n offline: string;\n}\n\n// Defined in ./write-lock-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_WRITE_LOCK_LABELS };\n\n/**\n * What kind of lock a source is (docs/billing-harmonization.md §12.36, ui-kit 0.33).\n * `\"demo\"` (the read-only demo, `demo.writeLocked`), `\"billing\"` (a lapsed plan,\n * `billing.lockReason`) and, since 0.46, `\"offline\"` ({@link OFFLINE_LOCK}) are the kit's; an app names its own — Kurvenschmiede's\n * `\"access\"` (a curve shared to view) and `\"customer\"` (an account that owns nothing).\n * A control exempts itself from a kind with {@link CommitScope}; a source without a\n * kind is never exempted.\n */\nexport type WriteLockKind = \"demo\" | \"billing\" | \"offline\" | (string & {});\n\n/** One locked source, as the provider holds it: its kind and its one sentence. */\nexport interface WriteLockHold {\n kind: WriteLockKind | undefined;\n reason: ReactNode;\n}\n\n/**\n * A control's opt-in to the lock — the type of every `commit` prop in the kit.\n *\n * - `true`: every lock refuses the control (a save, an add, an invite).\n * - `{ except: [kinds] }`: every lock but those kinds. A billing lock leaves the\n * reader able to remove access, revoke sessions, delete the account and change their\n * own settings (§3.3, §12.13), so the kit's parts for those take\n * {@link COMMIT_EXCEPT_BILLING} built in, and an app's own controls of that sort do\n * too.\n * - `false` or left out: no lock reaches it.\n *\n * Under a demo lock and a billing lock together, a control exempt from billing is\n * still refused, with the demo's reason.\n */\nexport type CommitScope = boolean | { readonly except: readonly WriteLockKind[] };\n\n/** Every lock but billing's: the scope of an action a lapsed plan still allows — removing\n * access, the account's own settings, an operator's admin route (§12.13). One constant,\n * so the call sites read alike. */\nexport const COMMIT_EXCEPT_BILLING = { except: [\"billing\"] } as const;\n\n/**\n * 0.46 (kastlan K4, docs/local-first-harmonization.md D4): the kind of the lock \"this\n * device has no connection\". An action that only the server can do (numbers, money,\n * mail, people, settings) is locked with \"Needs a connection.\" while offline; a control\n * that writes to the device's copy and syncs later takes {@link COMMIT_EXCEPT_OFFLINE}.\n * Feed it with {@link useOfflineWriteLock}.\n */\nexport const OFFLINE_LOCK = \"offline\" as const;\n\n/** Every lock but the offline one: the scope of a control that works on the device's copy\n * (it is still locked for a demo or a lapsed plan). */\nexport const COMMIT_EXCEPT_OFFLINE = { except: [OFFLINE_LOCK] } as const;\n\n/**\n * What {@link useWriteLock} returns: whether commits are locked here as a control with\n * the scope asked for sees it, and why.\n */\nexport interface WriteLock {\n locked: boolean;\n /** The one sentence a locked commit shows in its tooltip: the first hold in scope's.\n * Always set while `locked`; `undefined` when not. */\n reason: ReactNode | undefined;\n /** The first hold in scope's kind; `undefined` while unlocked, or for a source that\n * names none. */\n kind: WriteLockKind | undefined;\n /**\n * EVERY locked source the provider holds, in the order the server refuses them\n * (§12.3: the demo's 403 before billing's 402) — unfiltered by the scope, for code\n * that must know what else locks (a toast that names the plan, a swipe plan). Empty\n * while the provider is unlocked.\n */\n holds: readonly WriteLockHold[];\n}\n\nconst NO_HOLDS: readonly WriteLockHold[] = [];\n\nconst UNLOCKED: WriteLock = { locked: false, reason: undefined, kind: undefined, holds: NO_HOLDS };\n\nconst WriteLockContext = createContext<WriteLock>(UNLOCKED);\n\nexport interface WriteLockProviderProps {\n /** Commits below are locked. `false` unlocks a subtree under an outer lock. */\n locked: boolean;\n /**\n * The one sentence that says why — \"Shared with you to read\", \"Read-only demo —\n * saving is disabled\", \"A customer account cannot create curves\". The app's policy,\n * already translated. Left out: `labels.reason`.\n */\n reason?: ReactNode;\n /** What kind of lock this is — `\"demo\"`, `\"billing\"` or the app's own (see\n * {@link WriteLockKind}), for a provider of one source. Left out, no control can be\n * exempt from it. */\n kind?: WriteLockKind;\n /**\n * Every locked source, in refusal order — what `combineWriteLocks` answers, so\n * `<WriteLockProvider {...combineWriteLocks([...])}>` carries them. Given while\n * `locked`, it wins over `kind` and `reason`; a hold without a reason takes\n * `labels.reason`.\n */\n holds?: readonly WriteLockHold[];\n labels?: LabelOverride<WriteLockLabels>;\n children?: ReactNode;\n}\n\n// `null`/`\"\"`/`false` count as \"no reason given\" (`hasContent`): a lock whose tooltip is\n// empty would be a disabled button that cannot say why, the thing this replaces.\n\n/**\n * The read-only lock — **everything renders; only the commit is locked.**\n *\n * keksdose (features/budgets/write-lock.tsx, the read-only demo budget) and\n * Kurvenschmiede (features/sharing/write-lock.tsx, a curve shared to view, a customer\n * account that owns nothing) each built the same `SaveGuard`: the page, its tabs,\n * forms and dialogs render exactly as for an owner, because a viewer looking at half\n * a page is not looking at what they were sent; what changes is that the control\n * that SAVES is off and says why on hover or focus. Both wrapped each Save in a\n * Tooltip and forced a native `disabled` on it — out of the tab order, so the reason\n * never reached a keyboard — and both had to remember it at thirty call sites.\n *\n * Here the page mounts one provider and each commit control opts in with `commit`\n * ({@link Button}, {@link IconButton}, {@link FormActions}): under a lock it takes the\n * kit's `disabledReason` path with this reason — `aria-disabled` and focusable,\n * every activation swallowed, the reason in the kit Tooltip and its description.\n *\n * Who is locked is the app's: the provider takes a boolean, never a role. Fields stay\n * editable — nothing in them reaches the server until the commit is pressed, and a\n * form you cannot fill in is not a demo of the form (keksdose accounts-page). A\n * control that IS its own commit (a toggle that saves on change) reads\n * {@link useWriteLock} and disables itself. The lock is a courtesy, not a guard: the\n * server still refuses the write.\n *\n * **A lock says its source** (0.33, docs/billing-harmonization.md §12.36). A lapsed\n * plan locks creating and changing, but not removing access or the account's own\n * settings (§3.3, §12.13), and the kit's parts for those hard-coded `commit` — so\n * kastlan's app-wide lock locked a member's own sign-out everywhere. Each source now\n * has a kind, the provider holds every locked one in order (`holds`, from\n * `combineWriteLocks`), and a control's `commit` may name the kinds it is exempt from\n * ({@link CommitScope}). The kit's parts follow §3.3 and §12.13 by themselves.\n *\n * Nested providers: the nearest wins, so an inner `locked={false}` reopens a section\n * the caller may write (a viewer's own notes on a shared curve) — every source at\n * once — and an inner lock with its own reason (Kurvenschmiede's owner lock beside the\n * viewer lock) overrides the outer one's sentence and its holds.\n */\nexport function WriteLockProvider({ locked, reason, kind, holds, labels, children }: WriteLockProviderProps) {\n // The `writeLock` namespace: prop > `<UiKitProvider labels>` > English.\n const fallback = useKitLabels(\"writeLock\", DEFAULT_WRITE_LOCK_LABELS, labels).reason;\n // `holds` wins while it has any; otherwise this provider is one source of its own.\n const several = locked && holds !== undefined && holds.length > 0 ? holds : undefined;\n const firstKind = several ? several[0].kind : kind;\n const firstGiven = several ? several[0].reason : reason;\n const firstReason = hasContent(firstGiven) ? firstGiven : fallback;\n // Memoised on the first hold's kind and sentence — the strings an app passes — so a\n // one-source lock keeps one context value across renders; a list of several is\n // memoised on its own identity.\n const many = several && several.length > 1 ? several : undefined;\n const value = useMemo<WriteLock>(() => {\n if (!locked) return UNLOCKED;\n const all = many\n ? many.map((hold) => ({ kind: hold.kind, reason: hasContent(hold.reason) ? hold.reason : fallback }))\n : [{ kind: firstKind, reason: firstReason }];\n return { locked: true, reason: firstReason, kind: firstKind, holds: all };\n }, [locked, many, firstKind, firstReason, fallback]);\n return <WriteLockContext.Provider value={value}>{children}</WriteLockContext.Provider>;\n}\n\n/**\n * Whether a hold refuses a control with this scope: `true` refuses with every hold,\n * `{ except }` with every hold whose kind it does not name. A hold with no kind is\n * never exempted — an app that forgot to name its source must not reopen by accident.\n */\nfunction inScope(hold: WriteLockHold, scope: CommitScope): boolean {\n if (scope === true) return true;\n if (scope === false) return false;\n return hold.kind === undefined || !scope.except.includes(hold.kind);\n}\n\n/**\n * The lock as a control with `scope` sees it — {@link useWriteLock}'s answer, pure, for\n * a scope known only at call time: `useConfirm` reads the CALLER's lock when it is\n * called, and applies each `confirm({ commit })`'s own scope to it when it runs.\n *\n * `undefined` and `false` see no lock (a control without `commit`); `true` sees the\n * lock as it is; `{ except }` sees the first hold of another kind, with its reason\n * and kind. `holds` is passed on unfiltered.\n */\nexport function writeLockFor(lock: WriteLock, scope: CommitScope | undefined): WriteLock {\n if (!lock.locked || scope === true) return lock;\n // A lock built by hand (a test's, an app's own context) may carry no holds: it is\n // then the one source it describes.\n const holds = lock.holds.length > 0 ? lock.holds : [{ kind: lock.kind, reason: lock.reason }];\n const first = scope === undefined ? undefined : holds.find((hold) => inScope(hold, scope));\n if (!first) return { ...UNLOCKED, holds: lock.holds };\n // The first in scope is the provider's own first: the lock as it is, same object.\n if (first === holds[0]) return lock;\n return { locked: true, reason: first.reason, kind: first.kind, holds: lock.holds };\n}\n\n/**\n * The nearest {@link WriteLockProvider}'s lock, as a control with `scope` sees it —\n * unlocked with no provider. Left out, the scope is `true`: every lock.\n *\n * For code that must KNOW rather than a control that opts in: a toast that says why,\n * a swipe plan that leaves an action out, a hand-rolled toggle that disables itself.\n * `useWriteLock(COMMIT_EXCEPT_BILLING)` answers \"would a lapsed plan's lock refuse me?\"\n * as a removal sees it — what keksdose's `useWriteLockKind() !== \"billing\"` asked.\n */\nexport function useWriteLock(scope: CommitScope = true): WriteLock {\n return writeLockFor(useContext(WriteLockContext), scope);\n}\n\n/**\n * The `disabledReason` a control with `commit` should use: the lock's reason while\n * the lock refuses this scope — it wins over the control's own, since it is the answer\n * to the question the hover is asking — else the control's own. Internal to the kit's\n * commit controls.\n */\nexport function useCommitReason(commit: CommitScope | undefined, own: ReactNode): ReactNode {\n const lock = writeLockFor(useContext(WriteLockContext), commit);\n return lock.locked ? lock.reason : own;\n}\n\n/**\n * 0.46 (kastlan K4): the offline lock as one source of `combineWriteLocks` —\n * `{ locked: offline, kind: \"offline\", reason: writeLock.offline }` (\"Needs a\n * connection.\", seven languages). `offline` is the app's own reading of the connection\n * (`!navigator.onLine`, or its better guess); the kit does not watch the network here.\n * List it after the demo's and billing's sources: the server refuses those first.\n *\n * <WriteLockProvider {...combineWriteLocks([demo, billing, useOfflineWriteLock(!online)])}>\n * <Button commit={COMMIT_EXCEPT_OFFLINE}>Save handover</Button>\n */\nexport function useOfflineWriteLock(\n offline: boolean,\n labels?: LabelOverride<WriteLockLabels>,\n): { locked: boolean; kind: typeof OFFLINE_LOCK; reason: string } {\n const words = useKitLabels(\"writeLock\", DEFAULT_WRITE_LOCK_LABELS, labels);\n return { locked: offline, kind: OFFLINE_LOCK, reason: words.offline };\n}\n"],"mappings":";AAiLS;AAjLT,SAAS,eAAe,YAAY,eAAe;AAEnD,SAAS,oBAAwC;AACjD,SAAS,kBAAkB;AAC3B,SAAS,iCAAiC;AAgDnC,MAAM,wBAAwB,EAAE,QAAQ,CAAC,SAAS,EAAE;AASpD,MAAM,eAAe;AAIrB,MAAM,wBAAwB,EAAE,QAAQ,CAAC,YAAY,EAAE;AAuB9D,MAAM,WAAqC,CAAC;AAE5C,MAAM,WAAsB,EAAE,QAAQ,OAAO,QAAQ,QAAW,MAAM,QAAW,OAAO,SAAS;AAEjG,MAAM,mBAAmB,cAAyB,QAAQ;AAkEnD,SAAS,kBAAkB,EAAE,QAAQ,QAAQ,MAAM,OAAO,QAAQ,SAAS,GAA2B;AAE3G,QAAM,WAAW,aAAa,aAAa,2BAA2B,MAAM,EAAE;AAE9E,QAAM,UAAU,UAAU,UAAU,UAAa,MAAM,SAAS,IAAI,QAAQ;AAC5E,QAAM,YAAY,UAAU,QAAQ,CAAC,EAAE,OAAO;AAC9C,QAAM,aAAa,UAAU,QAAQ,CAAC,EAAE,SAAS;AACjD,QAAM,cAAc,WAAW,UAAU,IAAI,aAAa;AAI1D,QAAM,OAAO,WAAW,QAAQ,SAAS,IAAI,UAAU;AACvD,QAAM,QAAQ,QAAmB,MAAM;AACrC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,MAAM,OACR,KAAK,IAAI,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,QAAQ,WAAW,KAAK,MAAM,IAAI,KAAK,SAAS,SAAS,EAAE,IAClG,CAAC,EAAE,MAAM,WAAW,QAAQ,YAAY,CAAC;AAC7C,WAAO,EAAE,QAAQ,MAAM,QAAQ,aAAa,MAAM,WAAW,OAAO,IAAI;AAAA,EAC1E,GAAG,CAAC,QAAQ,MAAM,WAAW,aAAa,QAAQ,CAAC;AACnD,SAAO,oBAAC,iBAAiB,UAAjB,EAA0B,OAAe,UAAS;AAC5D;AAOA,SAAS,QAAQ,MAAqB,OAA6B;AACjE,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO,KAAK,SAAS,UAAa,CAAC,MAAM,OAAO,SAAS,KAAK,IAAI;AACpE;AAWO,SAAS,aAAa,MAAiB,OAA2C;AACvF,MAAI,CAAC,KAAK,UAAU,UAAU,KAAM,QAAO;AAG3C,QAAM,QAAQ,KAAK,MAAM,SAAS,IAAI,KAAK,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,OAAO,CAAC;AAC5F,QAAM,QAAQ,UAAU,SAAY,SAAY,MAAM,KAAK,CAAC,SAAS,QAAQ,MAAM,KAAK,CAAC;AACzF,MAAI,CAAC,MAAO,QAAO,EAAE,GAAG,UAAU,OAAO,KAAK,MAAM;AAEpD,MAAI,UAAU,MAAM,CAAC,EAAG,QAAO;AAC/B,SAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,MAAM,MAAM,OAAO,KAAK,MAAM;AACnF;AAWO,SAAS,aAAa,QAAqB,MAAiB;AACjE,SAAO,aAAa,WAAW,gBAAgB,GAAG,KAAK;AACzD;AAQO,SAAS,gBAAgB,QAAiC,KAA2B;AAC1F,QAAM,OAAO,aAAa,WAAW,gBAAgB,GAAG,MAAM;AAC9D,SAAO,KAAK,SAAS,KAAK,SAAS;AACrC;AAYO,SAAS,oBACd,SACA,QACgE;AAChE,QAAM,QAAQ,aAAa,aAAa,2BAA2B,MAAM;AACzE,SAAO,EAAE,QAAQ,SAAS,MAAM,cAAc,QAAQ,MAAM,QAAQ;AACtE;","names":[]}
|
package/dist/data-table.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export * from "./components/data-table-filters.js";
|
|
|
4
4
|
export * from "./components/data-table-pagination.js";
|
|
5
5
|
export { FilterPopover } from "./components/data-table-filter-popover.js";
|
|
6
6
|
export { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from "./components/data-table.js";
|
|
7
|
-
export type { DataTableSortLabels, DataTableColumn, DataTableCellProps, DataTableHeadProps, DataTableProps, ServerPagination, FilterState, MobileSwipeActions, DataTableDensity, DataTableChrome, DataTableRowAction, } from "./components/data-table.js";
|
|
7
|
+
export type { DataTableSortLabels, DataTableColumn, DataTableCellProps, DataTableHeadProps, DataTableProps, ServerPagination, FilterState, MobileSwipeActions, DataTableDensity, DataTableChrome, DataTableRowAction, DataTableDesktopSwipeActions, } from "./components/data-table.js";
|
|
8
8
|
export * from "./components/data-table-cells.js";
|
|
9
9
|
export { useTableUrlState, readTableUrlState } from "./components/use-table-state.js";
|
|
10
10
|
export type { TableUrlState, TableUrlSync, TableUrlStateProps, UseTableUrlStateOptions, UseTableUrlStateReturn, } from "./components/use-table-state.js";
|
package/dist/data-table.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`\n// branch, so a Router is needed even with URL sync switched off.\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// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n // The barrel had these two and this entry did not (refactor audit 2026-10, MOD-5):\n // the types of a column's `cellProps` and `headProps`.\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n} from \"./components/data-table\";\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"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,WAAW,sCAAsC;
|
|
1
|
+
{"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`\n// branch, so a Router is needed even with URL sync switched off.\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// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n // The barrel had these two and this entry did not (refactor audit 2026-10, MOD-5):\n // the types of a column's `cellProps` and `headProps`.\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n DataTableDesktopSwipeActions,\n} from \"./components/data-table\";\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"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,WAAW,sCAAsC;AAiB1D,cAAc;AACd,SAAS,kBAAkB,yBAAyB;","names":[]}
|
package/dist/eslint/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const roleUtilities = {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
const VERSION = true ? "0.
|
|
50
|
+
const VERSION = true ? "0.46.0" : "0.0.0-source";
|
|
51
51
|
const recommended = {
|
|
52
52
|
name: "@eifi1/ui-kit/recommended",
|
|
53
53
|
// Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
|