@eifi1/ui-kit 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +60 -6
  2. package/dist/chart.d.ts +4 -1
  3. package/dist/components/amount-input.d.ts +4 -1
  4. package/dist/components/button-group.d.ts +4 -1
  5. package/dist/components/calculator.d.ts +4 -1
  6. package/dist/components/copy-button.d.ts +4 -1
  7. package/dist/components/facing-pair.d.ts +4 -1
  8. package/dist/components/file-button.d.ts +4 -1
  9. package/dist/components/file-dropzone.d.ts +4 -1
  10. package/dist/components/floating-panel.d.ts +4 -1
  11. package/dist/components/floating-panel.js +15 -1
  12. package/dist/components/floating-panel.js.map +1 -1
  13. package/dist/components/form-actions.d.ts +4 -1
  14. package/dist/components/legal.d.ts +84 -0
  15. package/dist/components/legal.js +55 -0
  16. package/dist/components/legal.js.map +1 -0
  17. package/dist/components/money-field.d.ts +4 -1
  18. package/dist/components/number-field.d.ts +4 -1
  19. package/dist/components/number-input.d.ts +4 -1
  20. package/dist/components/numpad-sheet.d.ts +4 -1
  21. package/dist/components/series-chart.d.ts +4 -1
  22. package/dist/components/server-wake.d.ts +4 -2
  23. package/dist/components/server-wake.js +23 -2
  24. package/dist/components/server-wake.js.map +1 -1
  25. package/dist/components/settings-fields.d.ts +4 -1
  26. package/dist/components/share-card.d.ts +4 -1
  27. package/dist/components/text-link.d.ts +4 -1
  28. package/dist/components/time-input.d.ts +4 -1
  29. package/dist/components/translation-review-editor.d.ts +56 -0
  30. package/dist/components/translation-review-editor.js +159 -0
  31. package/dist/components/translation-review-editor.js.map +1 -0
  32. package/dist/components/translation-review-labels.d.ts +92 -0
  33. package/dist/components/translation-review-labels.js +67 -0
  34. package/dist/components/translation-review-labels.js.map +1 -0
  35. package/dist/components/translation-review-parts.d.ts +97 -0
  36. package/dist/components/translation-review-parts.js +163 -0
  37. package/dist/components/translation-review-parts.js.map +1 -0
  38. package/dist/components/translation-review.d.ts +94 -0
  39. package/dist/components/translation-review.js +344 -0
  40. package/dist/components/translation-review.js.map +1 -0
  41. package/dist/components/ui.d.ts +4 -1
  42. package/dist/hooks/use-file-drop.d.ts +4 -1
  43. package/dist/i18n/defaults.d.ts +4 -1
  44. package/dist/i18n/defaults.js +5 -1
  45. package/dist/i18n/defaults.js.map +1 -1
  46. package/dist/i18n/german.d.ts +4 -1
  47. package/dist/i18n/german.js +51 -0
  48. package/dist/i18n/german.js.map +1 -1
  49. package/dist/i18n/kit-labels.d.ts +4 -1
  50. package/dist/i18n/kit-labels.js.map +1 -1
  51. package/dist/i18n/languages.d.ts +243 -0
  52. package/dist/i18n/languages.js +70 -0
  53. package/dist/i18n/languages.js.map +1 -0
  54. package/dist/i18n/locales/de-CH.d.ts +4 -1
  55. package/dist/i18n/locales/en.d.ts +4 -1
  56. package/dist/i18n/locales/en.js +8 -0
  57. package/dist/i18n/locales/en.js.map +1 -1
  58. package/dist/i18n/locales/es.d.ts +4 -1
  59. package/dist/i18n/locales/es.js +83 -33
  60. package/dist/i18n/locales/es.js.map +1 -1
  61. package/dist/i18n/locales/fr.d.ts +4 -1
  62. package/dist/i18n/locales/fr.js +50 -0
  63. package/dist/i18n/locales/fr.js.map +1 -1
  64. package/dist/i18n/locales/hu.d.ts +4 -1
  65. package/dist/i18n/locales/hu.js +51 -0
  66. package/dist/i18n/locales/hu.js.map +1 -1
  67. package/dist/i18n/locales/it.d.ts +4 -1
  68. package/dist/i18n/locales/it.js +90 -40
  69. package/dist/i18n/locales/it.js.map +1 -1
  70. package/dist/i18n/locales/zh.d.ts +4 -1
  71. package/dist/i18n/locales/zh.js +51 -0
  72. package/dist/i18n/locales/zh.js.map +1 -1
  73. package/dist/i18n/review.d.ts +102 -0
  74. package/dist/i18n/review.js +158 -0
  75. package/dist/i18n/review.js.map +1 -0
  76. package/dist/index.d.ts +9 -1
  77. package/dist/index.js +64 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/{kit-labels-CPbsjlXr.d.ts → kit-labels-D1J2_jyD.d.ts} +7 -0
  80. package/dist/lib/floating-stack.d.ts +31 -0
  81. package/dist/lib/floating-stack.js +42 -0
  82. package/dist/lib/floating-stack.js.map +1 -0
  83. package/dist/lib/server-wake.d.ts +24 -8
  84. package/dist/lib/server-wake.js +6 -5
  85. package/dist/lib/server-wake.js.map +1 -1
  86. package/dist/lib/translation-review.d.ts +232 -0
  87. package/dist/lib/translation-review.js +194 -0
  88. package/dist/lib/translation-review.js.map +1 -0
  89. package/dist/rhf/fields.d.ts +4 -1
  90. package/dist/rhf/form.d.ts +4 -1
  91. package/dist/rhf.d.ts +4 -1
  92. package/dist/shell/app-shell.d.ts +4 -1
  93. package/dist/shell/top-bar-brand.d.ts +4 -1
  94. package/dist/shell.d.ts +4 -1
  95. package/dist/wizard/stepper-nav.d.ts +4 -1
  96. package/dist/wizard.d.ts +4 -1
  97. package/package.json +1 -1
  98. package/src/components/floating-panel.tsx +19 -1
  99. package/src/components/legal.tsx +159 -0
  100. package/src/components/server-wake.tsx +37 -5
  101. package/src/components/translation-review-editor.tsx +261 -0
  102. package/src/components/translation-review-labels.ts +152 -0
  103. package/src/components/translation-review-parts.tsx +269 -0
  104. package/src/components/translation-review.tsx +502 -0
  105. package/src/i18n/defaults.ts +4 -0
  106. package/src/i18n/german.ts +52 -0
  107. package/src/i18n/kit-labels.tsx +7 -0
  108. package/src/i18n/languages.ts +285 -0
  109. package/src/i18n/locales/en.ts +8 -0
  110. package/src/i18n/locales/es.ts +84 -33
  111. package/src/i18n/locales/fr.ts +51 -0
  112. package/src/i18n/locales/hu.ts +52 -0
  113. package/src/i18n/locales/it.ts +91 -40
  114. package/src/i18n/locales/zh.ts +52 -0
  115. package/src/i18n/review.ts +193 -0
  116. package/src/index.ts +63 -0
  117. package/src/lib/floating-stack.ts +66 -0
  118. package/src/lib/server-wake.ts +34 -13
  119. package/src/lib/translation-review.ts +465 -0
package/README.md CHANGED
@@ -382,14 +382,14 @@ columns, strings and permissions (see the note at the top of `src/feedback/feedb
382
382
 
383
383
  <!-- BEGIN GENERATED: exports — node scripts/gen-export-inventory.mjs -->
384
384
 
385
- **1101 names from 167 modules** — 508 values and 593 types. _Italic_ is a type-only export.
385
+ **1164 names from 175 modules** — 544 values and 620 types. _Italic_ is a type-only export.
386
386
 
387
387
  Generated from `dist/index.d.ts` by `node scripts/gen-export-inventory.mjs`; the count
388
388
  is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the markers.
389
389
 
390
390
  | Entry point | Names |
391
391
  |---|---|
392
- | `@eifi1/ui-kit` | 1101 |
392
+ | `@eifi1/ui-kit` | 1164 |
393
393
  | `@eifi1/ui-kit/chart` | 104 |
394
394
  | `@eifi1/ui-kit/shell` | 24 |
395
395
  | `@eifi1/ui-kit/data-table` | 49 |
@@ -415,6 +415,7 @@ re-slicing of it, never a second API.
415
415
  | `lib/safe-storage` | `readStored`, `writeStored` |
416
416
  | `lib/qr-encode` | `encodeQr`, _`QrEncodeOptions`_, _`QrErrorCorrection`_, _`QrMatrix`_ |
417
417
  | `lib/server-wake` | `attachServerWake`, `createServerWake`, `serverWake`, `watchReadsAnd`, `wrapFetch`, _`AxiosLikeInstance`_, _`ServerWakeAxiosConfig`_, _`ServerWakeFilter`_, _`ServerWakeOptions`_, _`ServerWakeRequest`_, _`ServerWakeStage`_, _`ServerWakeWatcher`_ |
418
+ | `lib/translation-review` | `DEFAULT_TRANSLATION_REVIEW_FILTER`, `dropReviews`, `filterTranslationRows`, `flattenStrings`, `fromApiReview`, `keyInArea`, `keyInAreas`, `keyNamespace`, `mergeReviews`, `placeholderMismatch`, `placeholderTokens`, `REVIEW_STATUSES`, `reviewStatus`, `reviewWrite`, `summariseRows`, `toApiWrite`, `translationCorrections`, `translationRows`, _`ApiTranslationReview`_, _`ApiTranslationReviewWrite`_, _`ReviewStatus`_, _`TranslationCorrection`_, _`TranslationReview`_, _`TranslationReviewFilter`_, _`TranslationReviewKey`_, _`TranslationReviewWrite`_, _`TranslationRow`_, _`TranslationRowsInput`_, _`TranslationSummary`_, _`TranslationVerdict`_ |
418
419
  | `lib/clipping` | `CLIPS_ATTRIBUTE` |
419
420
 
420
421
  ### hooks
@@ -456,6 +457,8 @@ re-slicing of it, never a second API.
456
457
  |---|---|
457
458
  | `i18n/kit-labels` | `DEFAULT_APP_SHELL_LABELS`, `DEFAULT_CALCULATOR_LABELS`, `DEFAULT_COMBOBOX_LABELS`, `DEFAULT_COMMON_LABELS`, `DEFAULT_CURRENCY_LABELS`, `DEFAULT_DATE_PICKER_LABELS`, `DEFAULT_FILE_LABELS`, `DEFAULT_MULTI_SELECT_LABELS`, `DEFAULT_PICKER_SHEET_LABELS`, `DEFAULT_SWIPEABLE_ROW_LABELS`, `DEFAULT_TOP_BAR_LABELS`, `formatFileSize`, `missingKitLabels`, `UiKitProvider`, `useKitChartTooltipPlacement`, `useKitFileLabels`, `useKitLabelOverrides`, `useKitLabels`, `useKitLink`, `useKitLocale`, `useKitWeekStart`, _`AppShellLabels`_, _`CalculatorLabels`_, _`ChartTooltipPlacement`_, _`ComboboxLabels`_, _`CommonLabels`_, _`CurrencyLabels`_, _`DatePickerLabels`_, _`FileLabels`_, _`KitLinkComponent`_, _`KitLinkProps`_, _`LabelOverride`_, _`MultiSelectLabels`_, _`PickerSheetLabels`_, _`SwipeableRowLabels`_, _`TopBarLabels`_, _`UiKitLabelOverrides`_, _`UiKitLabels`_, _`UiKitProviderProps`_ |
458
459
  | `i18n/defaults` | `DEFAULT_UI_KIT_LABELS` |
460
+ | `i18n/review` | `kitLabelStrings` |
461
+ | `i18n/languages` | `formatLocaleOf`, `KIT_LANGUAGES`, `languageOptions`, `loadUiKitLabels`, `resolveLanguage`, _`KitLanguage`_, _`KitLanguageCode`_ |
459
462
 
460
463
  ### components
461
464
 
@@ -568,10 +571,15 @@ re-slicing of it, never a second API.
568
571
  | `components/data-table-pagination` | `PAGE_SIZE_OPTIONS`, `Pagination` |
569
572
  | `components/data-table-filter-popover` | `FilterPopover` |
570
573
  | `components/data-table-cells` | `booleanColumn`, `BooleanMark`, _`BooleanColumnOptions`_, _`BooleanMarkProps`_ |
574
+ | `components/translation-review` | `TranslationReviewPanel`, _`TranslationReviewPanelProps`_ |
575
+ | `components/legal` | `DEFAULT_LEGAL_LABELS`, `LegalLayout`, `LegalLinks`, `LegalSection`, _`LegalLabels`_, _`LegalLayoutProps`_, _`LegalLink`_, _`LegalLinksProps`_, _`LegalSectionProps`_ |
571
576
  | `components/account-settings-labels` | `DEFAULT_ACCOUNT_SETTINGS_LABELS`, _`AccountSettingsLabels`_, _`PasskeysSettingLabels`_, _`PasswordSettingLabels`_, _`ProfileSettingLabels`_, _`TwoFactorSettingLabels`_ |
572
577
  | `components/combobox-core` | _`ComboClearValue`_, _`ComboOption`_ |
578
+ | `components/translation-review-labels` | `DEFAULT_TRANSLATION_REVIEW_LABELS`, _`TranslationReviewLabels`_ |
579
+ | `components/translation-review-parts` | `REVIEW_STATUS_TONES`, `ReviewStatusChip`, `TranslationExportButton`, `TranslationLocaleTabs`, `TranslationProgress`, _`ReviewStatusChipProps`_, _`TranslationExportButtonProps`_, _`TranslationLocaleTab`_, _`TranslationLocaleTabsProps`_, _`TranslationProgressProps`_ |
573
580
  | `components/series-chart-budget` | _`SeriesChartAxisBudget`_ |
574
581
  | `components/series-chart-ticks` | _`TimeTickUnit`_ |
582
+ | `components/translation-review-editor` | `TranslationReviewEditor`, _`TranslationReviewEditorProps`_ |
575
583
 
576
584
  ### shell
577
585
 
@@ -707,6 +715,17 @@ translation built at runtime, assert in the app's own test:
707
715
  expect(missingKitLabels(de, DEFAULT_UI_KIT_LABELS)).toEqual([]);
708
716
  ```
709
717
 
718
+ **Reviewing the kit's words in an app.** A missing key is a machine's question; whether
719
+ a sentence reads right is a person's. `kitLabelStrings(labels)` turns a label tree into
720
+ `key → text` rows for an app's translation review (keksdose's /translations): string
721
+ labels under their dot path, function labels called with fixed samples — text arguments
722
+ as `{{name}}` placeholders, counts as 1 and 3 — under a key that names the sample
723
+ (`combobox.resultCount(3)`, `shareCard.removeConfirm(name)`). The keys never depend on
724
+ the locale, so `kitLabelStrings(UI_KIT_LABELS_DE_CH)` and the reference
725
+ `kitLabelStrings(DEFAULT_UI_KIT_LABELS)` join one to one. Pass the tree the app gives
726
+ `UiKitProvider`, overrides included. The kit's own test keeps every catalogue on the
727
+ reference's keys and placeholders.
728
+
710
729
  The provider's `locale` reaches every `Intl` formatter in the kit that is not handed a
711
730
  `locale` prop of its own: calendar month and weekday names, day numbers, the data table's
712
731
  counts, file sizes.
@@ -738,10 +757,13 @@ bundles only the language it imports: `@eifi1/ui-kit/i18n/<code>` exports
738
757
  and changes only how counts and file sizes are written. Codes: `de-CH`, `fr`, `it`, `es`,
739
758
  `hu`, `zh`, and `en` (factory only — the constant is `DEFAULT_UI_KIT_LABELS`).
740
759
 
741
- There is ONE German: `de-CH`, Swiss Standard German, formal ("Sie"), with "ss" for every
742
- "ß" — `uiKitLabelsDeCh("de-DE")` keeps that text and writes German digits. An app that
743
- addresses its users with "du" overrides the few sentences that address the reader in its
744
- own provider; the kit does not ship a second register.
760
+ Every catalogue addresses the reader formally: German "Sie", French "vous", Italian "Lei",
761
+ Spanish "usted", Hungarian "Ön" (Italian and Spanish since 0.19.0). An app that says "du",
762
+ "tu" or "tú" overrides the few sentences that address the reader in its own provider; the
763
+ kit does not ship a second register.
764
+
765
+ There is ONE German: `de-CH`, Swiss Standard German, with "ss" for every "ß" —
766
+ `uiKitLabelsDeCh("de-DE")` keeps that text and writes German digits.
745
767
 
746
768
  ```tsx
747
769
  import { UI_KIT_LABELS_DE_CH } from "@eifi1/ui-kit/i18n/de-CH";
@@ -749,6 +771,38 @@ import { UI_KIT_LABELS_DE_CH } from "@eifi1/ui-kit/i18n/de-CH";
749
771
  <UiKitProvider labels={UI_KIT_LABELS_DE_CH} locale="de-CH">…</UiKitProvider>
750
772
  ```
751
773
 
774
+ ### Language registry
775
+
776
+ The kit's seven languages, and the three questions every app asked about them on its own
777
+ (docs/i18n-harmonization.md, H2): `KIT_LANGUAGES` (code, native and English name, the
778
+ `Intl` tag it formats in, the flag-icons country), `resolveLanguage(candidates, offered,
779
+ fallback?)`, `formatLocaleOf(code)` and `loadUiKitLabels(code)`. An app offers a subset
780
+ and keeps its own i18n library for its own words:
781
+
782
+ ```tsx
783
+ import { formatLocaleOf, languageOptions, loadUiKitLabels, resolveLanguage, type KitLanguageCode } from "@eifi1/ui-kit";
784
+
785
+ const OFFERED: KitLanguageCode[] = ["de-CH", "en", "fr", "it"];
786
+ // Precedence (H5): chosen on this device → the account's → the browser's list → de-CH.
787
+ const code = resolveLanguage([localStorage.getItem("myapp-lang"), user?.locale, ...navigator.languages], OFFERED);
788
+ await i18n.init({ lng: code, supportedLngs: OFFERED, fallbackLng: "de-CH" }); // the app's words
789
+ const labels = await loadUiKitLabels(code); // the kit's, fetched on demand, grouped like the language
790
+ document.documentElement.lang = formatLocaleOf(code); // "en-GB", not "en"
791
+
792
+ <UiKitProvider labels={labels} locale={formatLocaleOf(code)}>
793
+ <LanguageMenu options={languageOptions(OFFERED)} current={code} onChange={switchLanguage} />
794
+ </UiKitProvider>
795
+ ```
796
+
797
+ Every German (`de`, `de-AT`, a stored `de-informal`) resolves to `de-CH`; every Chinese
798
+ (`zh-TW`, `zh-Hant`) to `zh`, which is **Simplified** — there is no Traditional catalogue.
799
+ Other regional tags take their base language (`fr-CA` → `fr`), and a candidate the app
800
+ does not offer passes to the next. Each language formats like its home — `de-CH`, `en-GB`,
801
+ `es-ES`, `fr-FR`, `it-IT`, `hu-HU`, `zh-CN` — whatever the reader's region. The loader is
802
+ asynchronous only: a synchronous one would have to bundle all seven catalogues into every
803
+ app. `languageOptions` feeds `LanguageMenu` (which draws `fi fi-<country>` — the app loads
804
+ the flag CSS) and `LanguageSetting` (a native select: names only).
805
+
752
806
  ### The worked example
753
807
 
754
808
  `showcase/src/i18n/` runs the whole showcase in seven locales — English, German, French,
package/dist/chart.d.ts CHANGED
@@ -13,7 +13,7 @@ export { TimeTickUnit } from './components/series-chart-ticks.js';
13
13
  import 'recharts';
14
14
  import 'react';
15
15
  import './lib/direction.js';
16
- import './kit-labels-CPbsjlXr.js';
16
+ import './kit-labels-D1J2_jyD.js';
17
17
  import './components/data-table-labels.js';
18
18
  import './components/mini-calendar.js';
19
19
  import './components/calendar-heatmap.js';
@@ -63,6 +63,7 @@ import './hooks/use-authed-src.js';
63
63
  import './components/image-grid.js';
64
64
  import './components/lightbox.js';
65
65
  import './components/write-lock.js';
66
+ import './components/legal.js';
66
67
  import './components/account-chips.js';
67
68
  import './lib/format.js';
68
69
  import './data-table-3mi2R3so.js';
@@ -71,3 +72,5 @@ import './components/swipeable-row.js';
71
72
  import './components/reauth-dialog.js';
72
73
  import './components/server-wake.js';
73
74
  import './lib/server-wake.js';
75
+ import './components/translation-review-labels.js';
76
+ import './lib/translation-review.js';
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalculatorButtonLabels } from './calculator.js';
4
4
  import { NumberPadSheetLabels } from './numpad-sheet.js';
5
- import '../kit-labels-CPbsjlXr.js';
5
+ import '../kit-labels-D1J2_jyD.js';
6
6
  import './data-table-labels.js';
7
7
  import './mini-calendar.js';
8
8
  import './calendar-heatmap.js';
@@ -54,6 +54,7 @@ import '../hooks/use-authed-src.js';
54
54
  import './image-grid.js';
55
55
  import './lightbox.js';
56
56
  import './write-lock.js';
57
+ import './legal.js';
57
58
  import './account-chips.js';
58
59
  import '../lib/format.js';
59
60
  import '../data-table-3mi2R3so.js';
@@ -62,6 +63,8 @@ import './swipeable-row.js';
62
63
  import './reauth-dialog.js';
63
64
  import './server-wake.js';
64
65
  import '../lib/server-wake.js';
66
+ import './translation-review-labels.js';
67
+ import '../lib/translation-review.js';
65
68
 
66
69
  interface AmountInputProps {
67
70
  value: string;
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, ReactElement } from 'react';
3
- import { f as ButtonVariant, d as ButtonSize } from '../kit-labels-CPbsjlXr.js';
3
+ import { f as ButtonVariant, d as ButtonSize } from '../kit-labels-D1J2_jyD.js';
4
4
  import './data-table-labels.js';
5
5
  import './mini-calendar.js';
6
6
  import './calendar-heatmap.js';
@@ -52,6 +52,7 @@ import '../hooks/use-authed-src.js';
52
52
  import './image-grid.js';
53
53
  import './lightbox.js';
54
54
  import './write-lock.js';
55
+ import './legal.js';
55
56
  import './account-chips.js';
56
57
  import '../lib/format.js';
57
58
  import '../data-table-3mi2R3so.js';
@@ -60,6 +61,8 @@ import './swipeable-row.js';
60
61
  import './reauth-dialog.js';
61
62
  import './server-wake.js';
62
63
  import '../lib/server-wake.js';
64
+ import './translation-review-labels.js';
65
+ import '../lib/translation-review.js';
63
66
 
64
67
  interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<"div">, "role"> {
65
68
  /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { C as CalculatorLabels } from '../kit-labels-CPbsjlXr.js';
2
+ import { C as CalculatorLabels } from '../kit-labels-D1J2_jyD.js';
3
3
  import './data-table-labels.js';
4
4
  import './mini-calendar.js';
5
5
  import './calendar-heatmap.js';
@@ -51,6 +51,7 @@ import '../hooks/use-authed-src.js';
51
51
  import './image-grid.js';
52
52
  import './lightbox.js';
53
53
  import './write-lock.js';
54
+ import './legal.js';
54
55
  import './account-chips.js';
55
56
  import '../lib/format.js';
56
57
  import '../data-table-3mi2R3so.js';
@@ -59,6 +60,8 @@ import './swipeable-row.js';
59
60
  import './reauth-dialog.js';
60
61
  import './server-wake.js';
61
62
  import '../lib/server-wake.js';
63
+ import './translation-review-labels.js';
64
+ import '../lib/translation-review.js';
62
65
 
63
66
  /** Screen-reader names for the calculator panel's own controls — any part of the
64
67
  * kit's `calculator` namespace. Once three optional keys of its own; widened, not
@@ -1,5 +1,5 @@
1
1
  import 'react';
2
- export { D as CopyButton, E as CopyButtonLabels, F as CopyButtonProps, L as DEFAULT_COPY_BUTTON_LABELS } from '../kit-labels-CPbsjlXr.js';
2
+ export { D as CopyButton, E as CopyButtonLabels, F as CopyButtonProps, L as DEFAULT_COPY_BUTTON_LABELS } from '../kit-labels-D1J2_jyD.js';
3
3
  import './tooltip.js';
4
4
  import './data-table-labels.js';
5
5
  import './mini-calendar.js';
@@ -51,6 +51,7 @@ import '../hooks/use-authed-src.js';
51
51
  import './image-grid.js';
52
52
  import './lightbox.js';
53
53
  import './write-lock.js';
54
+ import './legal.js';
54
55
  import './account-chips.js';
55
56
  import '../lib/format.js';
56
57
  import '../data-table-3mi2R3so.js';
@@ -59,3 +60,5 @@ import './swipeable-row.js';
59
60
  import './reauth-dialog.js';
60
61
  import './server-wake.js';
61
62
  import '../lib/server-wake.js';
63
+ import './translation-review-labels.js';
64
+ import '../lib/translation-review.js';
@@ -6,7 +6,7 @@ import './series-chart-labels.js';
6
6
  import './toggle-legend.js';
7
7
  import './series-chart-ticks.js';
8
8
  import './series-chart-budget.js';
9
- import '../kit-labels-CPbsjlXr.js';
9
+ import '../kit-labels-D1J2_jyD.js';
10
10
  import './data-table-labels.js';
11
11
  import './mini-calendar.js';
12
12
  import './calendar-heatmap.js';
@@ -57,6 +57,7 @@ import '../hooks/use-authed-src.js';
57
57
  import './image-grid.js';
58
58
  import './lightbox.js';
59
59
  import './write-lock.js';
60
+ import './legal.js';
60
61
  import './account-chips.js';
61
62
  import '../lib/format.js';
62
63
  import '../data-table-3mi2R3so.js';
@@ -65,6 +66,8 @@ import './swipeable-row.js';
65
66
  import './reauth-dialog.js';
66
67
  import './server-wake.js';
67
68
  import '../lib/server-wake.js';
69
+ import './translation-review-labels.js';
70
+ import '../lib/translation-review.js';
68
71
 
69
72
  /** Which chart of a facing pair. Physical, like the axes: charts do not mirror in RTL. */
70
73
  type FacingSide = "left" | "right";
@@ -1,5 +1,5 @@
1
1
  import 'react';
2
- export { P as DEFAULT_FILE_PICKER_LABELS, ae as FileButton, af as FileButtonProps, bu as FilePickHandler, ah as FilePickerLabels, ai as FileRejection, aj as FileRejectionReason, ak as FileScreenOptions, bf as UseFilePickerOptions, bg as UseFilePickerReturn, bv as formatAccept, bw as judgePick, bj as matchesAccept, bx as screenFiles, by as summariseRejections, bm as useFilePicker } from '../kit-labels-CPbsjlXr.js';
2
+ export { P as DEFAULT_FILE_PICKER_LABELS, ae as FileButton, af as FileButtonProps, bu as FilePickHandler, ah as FilePickerLabels, ai as FileRejection, aj as FileRejectionReason, ak as FileScreenOptions, bf as UseFilePickerOptions, bg as UseFilePickerReturn, bv as formatAccept, bw as judgePick, bj as matchesAccept, bx as screenFiles, by as summariseRejections, bm as useFilePicker } from '../kit-labels-D1J2_jyD.js';
3
3
  import './data-table-labels.js';
4
4
  import './mini-calendar.js';
5
5
  import './calendar-heatmap.js';
@@ -51,6 +51,7 @@ import '../hooks/use-authed-src.js';
51
51
  import './image-grid.js';
52
52
  import './lightbox.js';
53
53
  import './write-lock.js';
54
+ import './legal.js';
54
55
  import './account-chips.js';
55
56
  import '../lib/format.js';
56
57
  import '../data-table-3mi2R3so.js';
@@ -59,3 +60,5 @@ import './swipeable-row.js';
59
60
  import './reauth-dialog.js';
60
61
  import './server-wake.js';
61
62
  import '../lib/server-wake.js';
63
+ import './translation-review-labels.js';
64
+ import '../lib/translation-review.js';
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
- import { ai as FileRejection, bu as FilePickHandler, ah as FilePickerLabels } from '../kit-labels-CPbsjlXr.js';
3
+ import { ai as FileRejection, bu as FilePickHandler, ah as FilePickerLabels } from '../kit-labels-D1J2_jyD.js';
4
4
  import './data-table-labels.js';
5
5
  import './mini-calendar.js';
6
6
  import './calendar-heatmap.js';
@@ -52,6 +52,7 @@ import '../hooks/use-authed-src.js';
52
52
  import './image-grid.js';
53
53
  import './lightbox.js';
54
54
  import './write-lock.js';
55
+ import './legal.js';
55
56
  import './account-chips.js';
56
57
  import '../lib/format.js';
57
58
  import '../data-table-3mi2R3so.js';
@@ -60,6 +61,8 @@ import './swipeable-row.js';
60
61
  import './reauth-dialog.js';
61
62
  import './server-wake.js';
62
63
  import '../lib/server-wake.js';
64
+ import './translation-review-labels.js';
65
+ import '../lib/translation-review.js';
63
66
 
64
67
  /**
65
68
  * Where a refused file's message goes.
@@ -114,8 +114,11 @@ interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButton
114
114
  *
115
115
  * Portalled, because `position: fixed` inside any transformed ancestor (an animated
116
116
  * page transition) is fixed to that ancestor, not to the screen.
117
+ *
118
+ * While mounted it reports its box (`lib/floating-stack.ts`), so `ServerWakeNotice`
119
+ * stacks above it rather than over it.
117
120
  */
118
- declare function FloatingActionButton({ label, icon, corner, offset, nativeTitle, pressed, extended, live, variant, tooltip, tooltipSide, className, style, type, title, ...rest }: FloatingActionButtonProps): react.JSX.Element;
121
+ declare function FloatingActionButton({ label, icon, corner, offset, nativeTitle, pressed, extended, live, variant, tooltip, tooltipSide, className, style, type, title, ref, ...rest }: FloatingActionButtonProps): react.JSX.Element;
119
122
  interface FloatingPanelProps {
120
123
  /** The panel's heading, and its accessible name. */
121
124
  title: ReactNode;
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { dirOf } from "../lib/direction.js";
8
+ import { registerFloating } from "../lib/floating-stack.js";
8
9
  import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
10
+ import { assignRef } from "./choice-parts.js";
9
11
  import { pickLinkRenderer } from "./text-link.js";
10
12
  import { Tooltip } from "./tooltip.js";
11
13
  const DEFAULT_FLOATING_PANEL_LABELS = {
@@ -46,17 +48,28 @@ function FloatingActionButton({
46
48
  style,
47
49
  type = "button",
48
50
  title,
51
+ ref,
49
52
  ...rest
50
53
  }) {
51
54
  const [marker, dir] = usePortalDir();
52
55
  const toggle = pressed !== void 0;
53
56
  const tip = tooltip === true ? label : tooltip === false || tooltip === "" ? void 0 : tooltip;
54
57
  const wrapped = tip !== void 0 && tip !== null;
58
+ const [fixed, setFixed] = useState(null);
59
+ useEffect(() => fixed ? registerFloating(fixed) : void 0, [fixed]);
60
+ const buttonRef = useCallback(
61
+ (node) => {
62
+ assignRef(ref, node);
63
+ if (!wrapped) setFixed(node);
64
+ },
65
+ [ref, wrapped]
66
+ );
55
67
  const nativeTip = !wrapped && nativeTitle && title !== "" ? title ?? label : void 0;
56
68
  const button = /* @__PURE__ */ jsxs(
57
69
  "button",
58
70
  {
59
71
  ...rest,
72
+ ref: buttonRef,
60
73
  type,
61
74
  dir,
62
75
  "aria-label": label,
@@ -92,6 +105,7 @@ function FloatingActionButton({
92
105
  wrapped ? /* @__PURE__ */ jsx(
93
106
  "div",
94
107
  {
108
+ ref: setFixed,
95
109
  dir,
96
110
  hidden: rest.hidden,
97
111
  style: cornerStyle(corner, offset),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/floating-panel.tsx"],"sourcesContent":["import { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n KeyboardEvent,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\n\nexport interface FloatingPanelLabels {\n /** Accessible name of the panel header's X. */\n close: string;\n /**\n * What a {@link FloatingAction}'s count badge adds to its accessible name (\"3 new\"),\n * read after the label: \"Invoices, 3 new\". The dot itself is hidden from a reader —\n * a bare \"3\" read out of context says nothing about what is counted.\n */\n badge: (count: number) => string;\n}\n\nexport const DEFAULT_FLOATING_PANEL_LABELS: FloatingPanelLabels = {\n close: \"Close\",\n badge: (count) => `${count} new`,\n};\n\n/** A LOGICAL corner: `bottom-end` is bottom-right in LTR and bottom-left in RTL. */\nexport type FloatingCorner = \"bottom-end\" | \"bottom-start\";\n\n/**\n * How far off the bottom edge anything floating here sits: clear of `AppShell`'s phone\n * nav (`--app-nav-h`, `0px` above `md` where the nav is not rendered) and of the home\n * indicator, whichever reaches higher — the nav does not pad itself for the inset, so\n * with no nav on screen the inset is all there is to clear. `max`, not a sum: where the\n * nav does cover the inset, adding both would float the button a nav-height too high.\n */\nconst BOTTOM_CLEARANCE = \"max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px))\";\n/** The side gutter, widened for a landscape notch. Both insets, because a logical\n * side has no one physical inset to name. */\nconst SIDE_GUTTER = \"max(1rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\";\n\nfunction cornerStyle(corner: FloatingCorner, lift: string): CSSProperties {\n return {\n bottom: `calc(${BOTTOM_CLEARANCE} + ${lift})`,\n // Logical properties, so the corner follows the reading direction with no JS. The\n // portal carries the `dir` across (see `usePortalDir`).\n [corner === \"bottom-start\" ? \"insetInlineStart\" : \"insetInlineEnd\"]: SIDE_GUTTER,\n };\n}\n\n/**\n * The reading direction where the component was RENDERED, for its portalled root: a\n * portal to `<body>` leaves the subtree whose `dir` it inherited, and a logical\n * `inset-inline-end` resolves against the portal's own direction. Read off a hidden\n * inline marker in a layout effect, so the first painted frame already has it.\n */\nfunction usePortalDir(): [Ref<HTMLSpanElement>, Direction | undefined] {\n const marker = useRef<HTMLSpanElement>(null);\n const [dir, setDir] = useState<Direction | undefined>(undefined);\n useLayoutEffect(() => {\n setDir(dirOf(marker.current));\n }, []);\n return [marker, dir];\n}\n\nexport interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n /** The accessible name — the button shows only its icon. */\n label: string;\n icon: ReactNode;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /**\n * Distance above the nav / home indicator, as a CSS length. Default `1rem`. Raise it\n * to stack above another floating control in the same corner — keksdose's assistant\n * clears the transactions page's own action group this way (`calc(1rem + 4rem)`).\n */\n offset?: string;\n /**\n * Whether the button carries the browser's own tooltip (`title`, the `label` unless a\n * `title` is passed). Default `true`, so an icon-only button keeps a hover hint for a\n * mouse user with no other way to learn what it is. `false` renders no `title` at all:\n * keksdose has ONE tooltip, the kit's `Tooltip` (dev#523), and its source scan fails a\n * native `title=` anywhere — so a flag, not `title=\"\"`, which the scan would still\n * see. An empty `title` renders no attribute either, for the caller who has no scan.\n *\n * Not flipped to off by default: a FAB wrapped in nothing would lose the only hover\n * hint it has, silently, in every app already on it. And the kit cannot wrap itself\n * in its `Tooltip` — the bubble's anchor is a `relative inline-flex` span, and the\n * button is `fixed` and portalled — so the one-tooltip caller wraps it.\n */\n nativeTitle?: boolean;\n /**\n * Make it a toggle. `true`/`false` set `aria-pressed` and swap the solid brand disc for\n * a surface disc — the glyph in brand on the quiet brand fill when on, the secondary\n * text colour when off — the \"on\" look of `IconButton`'s `pressed` and of a selected\n * Chip. Left out, it is the ordinary action FAB. For keksdose's corner filter toggles\n * (feedback-page's \"awaiting only\", the /transactions pending and upcoming toggles),\n * which paint `text-[var(--brand)]` on a surface button by hand. Keep `label` the same\n * in both states; `aria-pressed` already says which one it is in.\n */\n pressed?: boolean;\n /**\n * Show `label` beside the icon: the EXTENDED FAB, a pill rather than a disc. For a\n * corner control whose words are the point — kastlan's offline indicator\n * (shared/offline/offline-indicator.tsx:39) is a pill saying \"Offline — 2 photo(s)\n * queued\" / \"Syncing…\", hand-placed at `fixed bottom-4 left-4`, i.e. over the\n * bottom nav and on the wrong side in RTL. Same height as the disc (3rem), so a\n * {@link FloatingPanel} card above it still clears it.\n */\n extended?: boolean;\n /**\n * Announce `label` whenever it changes, through a polite `role=\"status\"` region — for\n * an extended FAB that REPORTS something (kastlan's offline/sync state), where a\n * sighted user sees the words change and a screen reader user otherwise hears\n * nothing until they happen to land on the button.\n *\n * ⚠️ A live region announces changes, not its own arrival: a FAB that mounts already\n * saying \"Offline\" says it to nobody. Keep it mounted and pass `hidden` while there\n * is nothing to report (kastlan returns `null` when online and idle); the region\n * stays in the page, and the first real status is heard.\n */\n live?: boolean;\n /**\n * `primary` (default): the solid brand disc of a page's main action. `surface`: the\n * page's surface with a hairline border — for a FAB that is a status rather than a\n * call to action, as kastlan's offline pill is (a brand pill would shout \"press me\"\n * at a user who is merely offline). Ignored while `pressed` is set; a toggle has its\n * own two looks.\n */\n variant?: \"primary\" | \"surface\";\n /**\n * Hover and focus label in the kit {@link Tooltip}, instead of the native `title` —\n * keksdose has ONE tooltip (dev#523) and wrapped its assistant launcher by hand,\n * which cannot work: the tooltip's anchor is a `relative inline-flex` span, and this\n * button is `fixed` and portalled. With `tooltip` the FAB positions a fixed wrapper\n * instead and the button sits in the tooltip inside it. `true` shows the `label`;\n * any other content shows that. Implies no native `title` (whatever `nativeTitle`\n * says): one hover hint, not two. `style` stays on the button; the corner and\n * `offset` move to the wrapper.\n */\n tooltip?: ReactNode;\n /** Where the {@link tooltip} opens. Default `top`; the bubble is portalled and kept on\n * screen, so a corner button's label is not pushed off the edge. */\n tooltipSide?: TooltipSide;\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * A round, icon-only button pinned to a corner of the viewport — the trigger of a\n * {@link FloatingPanel}, and usable alone for a page's one primary action.\n *\n * keksdose hand-placed each of its floating controls: the assistant launcher at\n * `fixed bottom-36 right-4 md:bottom-6`, the feedback page's and the transactions\n * page's action groups at `right-4 bottom-20`. Every one of those numbers is a guess\n * at the phone nav's height plus whatever else floats there (Keksdose live #314 found\n * out what such a guess is worth for a footer), `right` rather than `end` puts it over\n * the page's leading edge in RTL, and none allows for the home indicator. Here the\n * base is `--app-nav-h` and the safe-area inset, the corner is logical, and\n * {@link FloatingActionButtonProps.offset} is the one number left to the caller — the\n * height of whatever else it has to clear, not the nav's.\n *\n * Portalled, because `position: fixed` inside any transformed ancestor (an animated\n * page transition) is fixed to that ancestor, not to the screen.\n */\nexport function FloatingActionButton({\n label,\n icon,\n corner = \"bottom-end\",\n offset = \"1rem\",\n nativeTitle = true,\n pressed,\n extended = false,\n live = false,\n variant = \"primary\",\n tooltip,\n tooltipSide = \"top\",\n className,\n style,\n type = \"button\",\n title,\n ...rest\n}: FloatingActionButtonProps) {\n const [marker, dir] = usePortalDir();\n const toggle = pressed !== undefined;\n const tip = tooltip === true ? label : tooltip === false || tooltip === \"\" ? undefined : tooltip;\n const wrapped = tip !== undefined && tip !== null;\n // An empty title is \"no tooltip\", not an attribute with nothing in it. The kit\n // tooltip replaces it outright.\n const nativeTip = !wrapped && nativeTitle && title !== \"\" ? (title ?? label) : undefined;\n const button = (\n <button\n {...rest}\n type={type}\n dir={dir}\n aria-label={label}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n title={nativeTip}\n style={wrapped ? style : { ...cornerStyle(corner, offset), ...style }}\n className={cn(\n // Wrapped, the wrapper is the fixed thing and the button sits in it.\n !wrapped && \"fixed z-40\",\n \"inline-flex items-center justify-center rounded-full\",\n extended ? \"h-12 gap-2 px-4 text-sm font-medium [&_svg]:size-5\" : \"size-12 [&_svg]:size-6\",\n \"shadow-lg transition-colors disabled:cursor-default\",\n !toggle &&\n variant === \"primary\" &&\n \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\",\n !toggle &&\n variant === \"surface\" &&\n \"border border-[var(--border)] bg-[var(--bg-surface)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)] disabled:hover:bg-[var(--bg-surface)]\",\n // A toggle sits on the page's surface — a solid brand disc would read as\n // \"on\" in both states — and says \"on\" in the brand family.\n toggle && \"border border-[var(--border)]\",\n pressed === false &&\n \"bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\",\n pressed === true &&\n \"border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2\",\n className,\n )}\n >\n {icon}\n {/* Visible words, not the name: `aria-label` already is the name, and the\n span would only repeat it. */}\n {extended && (\n <span aria-hidden className=\"whitespace-nowrap\">\n {label}\n </span>\n )}\n </button>\n );\n return (\n <>\n <span ref={marker} hidden />\n {typeof document !== \"undefined\" &&\n createPortal(\n <>\n {wrapped ? (\n <div\n dir={dir}\n hidden={rest.hidden}\n style={cornerStyle(corner, offset)}\n className=\"fixed z-40 inline-flex\"\n >\n <Tooltip label={tip} side={tooltipSide} portal>\n {/* A fragment when the bubble says the name again, so the tooltip\n does not describe the button with its own label — a reader would\n hear it twice. Other text is a real description. */}\n {tip === label ? <>{button}</> : button}\n </Tooltip>\n </div>\n ) : (\n button\n )}\n {live && (\n <span role=\"status\" className=\"sr-only\">\n {label}\n </span>\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\nexport interface FloatingPanelProps {\n /** The panel's heading, and its accessible name. */\n title: ReactNode;\n /** The trigger's accessible name (\"Assistant\", \"Send feedback\"). */\n fabLabel: string;\n fabIcon: ReactNode;\n /** Controlled open state. Leave it out to let the panel own it. */\n open?: boolean;\n /** Uncontrolled initial state. Default closed. */\n defaultOpen?: boolean;\n /** Every request to open or close — the FAB, the X, Escape. */\n onOpenChange?: (open: boolean) => void;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /** The FAB's distance above the nav — see {@link FloatingActionButtonProps.offset}.\n * From `md` up the card sits above the FAB, so it moves with it. */\n offset?: string;\n /** The FAB's native `title` — see {@link FloatingActionButtonProps.nativeTitle}.\n * `false` for keksdose's assistant launcher, under its one-tooltip rule (dev#523). */\n fabNativeTitle?: boolean;\n /** The FAB's kit tooltip — see {@link FloatingActionButtonProps.tooltip}. `true` for\n * keksdose's assistant launcher, which wanted the kit bubble and had no way to\n * put one on a fixed, portalled button (dev#523). */\n fabTooltip?: ReactNode;\n /** Default: `floatingPanel.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /**\n * Where focus goes on open. Default the panel itself, so a phone's keyboard does not\n * rise over the sheet before the user has asked to type; pass the chat field for a\n * panel whose one job is typing.\n */\n initialFocus?: () => HTMLElement | null;\n /** Extra classes for the panel. */\n className?: string;\n /** Extra classes for the scrolling body. */\n bodyClassName?: string;\n children: ReactNode;\n}\n\n/**\n * A docked, NON-MODAL panel in a corner of the screen, opened by a\n * {@link FloatingActionButton} — keksdose's assistant launcher and its feedback page,\n * which each built one.\n *\n * ## Not a Modal\n *\n * The page stays usable while it is open: that is the point of an assistant beside the\n * data it is asked about. So there is no backdrop, no scroll lock, no `aria-modal` and\n * no focus trap — Tab walks out of the panel into the page and back, and a click on the\n * page does not close it. It is `role=\"dialog\"` without `aria-modal`, which is what\n * ARIA calls a non-modal dialog, named by its heading.\n *\n * What it keeps from a dialog is the focus HANDOFF: opening moves focus into it (a\n * keyboard user who pressed the FAB should not have to hunt for what appeared), and\n * closing gives it back to the FAB — but only when focus was in the panel, so a panel\n * closed from the page (a controlled owner) never pulls focus out of the field the\n * user is typing in. Escape closes it while focus is inside, and only then: on the\n * panel element, not the document, so Escape in the page keeps meaning whatever the\n * page means by it.\n *\n * ## Layout\n *\n * From `md` up, a card above the FAB in its corner. Below `md`, a full-width sheet\n * docked to the bottom, sitting ON the `AppShell` bottom nav (`--app-nav-h`) and\n * clear of the home indicator. Pure CSS — no width is measured in JavaScript.\n *\n * The FAB carries `aria-expanded` and, while the panel exists, `aria-controls`. The\n * panel unmounts when closed; an app that must keep a draft across a close keeps it in\n * its own state, where it also survives a reload.\n */\nexport function FloatingPanel({\n title,\n fabLabel,\n fabIcon,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n corner = \"bottom-end\",\n offset = \"1rem\",\n fabNativeTitle,\n fabTooltip,\n closeLabel,\n initialFocus,\n className,\n bodyClassName,\n children,\n}: FloatingPanelProps) {\n const labels = useKitLabels(\n \"floatingPanel\",\n DEFAULT_FLOATING_PANEL_LABELS,\n closeLabel === undefined ? undefined : { close: closeLabel },\n );\n const [ownOpen, setOwnOpen] = useState(defaultOpen);\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n };\n\n const uid = useId();\n const panelId = `${uid}-panel`;\n const titleId = `${uid}-title`;\n const fabRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [marker, dir] = usePortalDir();\n\n const initialFocusRef = useRef(initialFocus);\n useEffect(() => {\n initialFocusRef.current = initialFocus;\n });\n\n // Focus follows the open/closed TRANSITION, never the mount: a panel that renders\n // open (`defaultOpen`, a restored route) does not take the page's focus by existing.\n const wasOpen = useRef(open);\n // Whether focus was inside the panel when it last changed — read at close time, when\n // the panel (and whatever had focus in it) is already gone.\n const focusInside = useRef(false);\n useEffect(() => {\n const changed = wasOpen.current !== open;\n wasOpen.current = open;\n if (!changed) return;\n if (open) {\n (initialFocusRef.current?.() ?? panelRef.current)?.focus();\n return;\n }\n const active = document.activeElement;\n const lost = active === null || active === document.body;\n if (focusInside.current || lost) fabRef.current?.focus();\n focusInside.current = false;\n }, [open]);\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\" || e.defaultPrevented) return;\n // Stopped here, so an overlay underneath with a document-level Escape listener does\n // not close too — the innermost thing that has focus is the one that closes.\n e.stopPropagation();\n focusInside.current = true;\n setOpen(false);\n };\n\n return (\n <>\n <span ref={marker} hidden />\n <FloatingActionButton\n ref={fabRef}\n label={fabLabel}\n icon={fabIcon}\n corner={corner}\n offset={offset}\n nativeTitle={fabNativeTitle}\n tooltip={fabTooltip}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n onClick={() => {\n // A press ON the FAB closing the panel: the FAB keeps focus by being clicked,\n // nothing to hand back.\n focusInside.current = false;\n setOpen(!open);\n }}\n />\n {open &&\n typeof document !== \"undefined\" &&\n createPortal(\n // A container for Escape bubbling up from anything inside it, as a dialog is.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={panelRef}\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n tabIndex={-1}\n dir={dir}\n data-corner={corner}\n onKeyDown={onPanelKeyDown}\n onFocus={() => {\n focusInside.current = true;\n }}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) focusInside.current = false;\n }}\n // The phone's sheet sits on the nav; from `md` up the card floats above the\n // FAB (its offset + the 3rem button + a 0.75rem gap).\n style={\n {\n \"--fp-bottom\": `calc(${BOTTOM_CLEARANCE})`,\n \"--fp-bottom-md\": `calc(${BOTTOM_CLEARANCE} + ${offset} + 3.75rem)`,\n \"--fp-side\": SIDE_GUTTER,\n } as CSSProperties\n }\n className={cn(\n \"animate-sheet fixed z-40 flex flex-col overflow-hidden outline-none\",\n \"border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl\",\n // Phone: full width, docked, rounded only where it meets the page.\n \"inset-x-0 bottom-[var(--fp-bottom)] max-h-[min(85dvh,calc(100dvh_-_var(--fp-bottom)_-_1rem))] rounded-t-xl border-x-0 border-b-0\",\n \"pb-[env(safe-area-inset-bottom,0px)] md:pb-0\",\n // md+: a card in the corner, above the FAB.\n \"md:inset-x-auto md:bottom-[var(--fp-bottom-md)] md:w-[24rem] md:max-h-[min(36rem,calc(100dvh_-_var(--fp-bottom-md)_-_1rem))] md:rounded-xl md:border\",\n corner === \"bottom-start\" ? \"md:start-[var(--fp-side)]\" : \"md:end-[var(--fp-side)]\",\n className,\n )}\n >\n <div className=\"flex shrink-0 items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-3\">\n <h2 id={titleId} className=\"min-w-0 truncate text-base font-semibold text-[var(--text-primary)]\">\n {title}\n </h2>\n <button\n type=\"button\"\n onClick={() => {\n focusInside.current = true;\n setOpen(false);\n }}\n aria-label={labels.close}\n className=\"-me-1.5 shrink-0 rounded p-1.5 text-[var(--text-muted)] outline-none hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n </div>\n <div className={cn(\"min-h-0 flex-1 overflow-y-auto overscroll-contain p-4\", bodyClassName)}>\n {children}\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n}\n\nexport interface FloatingActionGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, \"role\"> {\n /** Name the group (\"Register actions\"); a reader then introduces the members as one\n * set. A group with no name is one more unlabelled wrapper. */\n \"aria-label\"?: string;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /** Distance above the nav / home indicator — see {@link FloatingActionButtonProps.offset}. */\n offset?: string;\n /** {@link FloatingAction}s, in reading order. */\n children: ReactNode;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A floating pill of {@link FloatingAction}s — one to four `size-12` toggles, actions\n * or links with hairline dividers — pinned to a corner exactly as the\n * {@link FloatingActionButton} is: `--app-nav-h` and the home indicator cleared, a\n * logical corner, portalled.\n *\n * keksdose built it twice by hand: the /transactions phone toolbar\n * (transactions-page.tsx:644 — upcoming and pending toggles, an invoices link with a\n * pending count, and the brand \"+\") and the feedback page's \"awaiting only\" toggle\n * (feedback-page.tsx:854, one button in the group anyway, so the two pages look\n * alike). Both at `right-4 bottom-20` — `right`, not `end`, so over the page's leading\n * edge in RTL — with `divide-x`, slate hovers, and each member's `Tooltip` spelled out.\n *\n * `role=\"group\"`, NOT a toolbar: every member keeps its own tab stop, as the\n * hand-built ones did, and there is no arrow-key roving to learn. Hide it where it\n * does not belong with a class (`md:hidden`, as both keksdose pages do).\n */\nexport function FloatingActionGroup({\n corner = \"bottom-end\",\n offset = \"1rem\",\n className,\n style,\n children,\n ...rest\n}: FloatingActionGroupProps) {\n const [marker, dir] = usePortalDir();\n return (\n <>\n <span ref={marker} hidden />\n {typeof document !== \"undefined\" &&\n createPortal(\n <div\n {...rest}\n role=\"group\"\n dir={dir}\n data-corner={corner}\n style={{ ...cornerStyle(corner, offset), ...style }}\n className={cn(\n \"fixed z-40 inline-flex flex-row overflow-hidden rounded-2xl\",\n \"border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\",\n // Dividers on the logical start edge of every member but the first, so\n // they sit between the same two members in RTL. The member is the\n // tooltip's wrapper span, which is the direct child.\n \"[&>*:not(:first-child)]:border-s [&>*]:border-[var(--border)]\",\n className,\n )}\n >\n {children}\n </div>,\n document.body,\n )}\n </>\n );\n}\n\n/** What {@link FloatingActionProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface FloatingActionLinkProps {\n href: string;\n /** The member's whole look — keep it. */\n className: string;\n children: ReactNode;\n \"aria-label\": string;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | \"onClick\"> {\n /** The accessible name, and the tooltip's text. Keep it the same in both states of a\n * toggle; `aria-pressed` says which one it is in. */\n label: string;\n icon: ReactNode;\n /** Make it a toggle: `aria-pressed` and the brand \"on\" look (the brand glyph on the\n * quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.\n * For the state without the tint, see {@link pressedStyle}. */\n pressed?: boolean;\n /**\n * How `pressed` looks. `tint` (default): the brand \"on\" look. `plain`: the member\n * keeps its quiet look in both states and only `aria-pressed` changes — for a toggle\n * whose icon already says which state it is in (keksdose's /transactions \"Upcoming\"\n * swaps two icons; a tint on top said it twice). The same as passing `aria-pressed`\n * without `pressed`, which also rides through untinted.\n */\n pressedStyle?: \"tint\" | \"plain\";\n /**\n * `primary`: the brand-filled member, for the group's main action (the /transactions\n * \"+\"). Default: the quiet one.\n */\n variant?: \"default\" | \"primary\";\n /**\n * A count in a dot at the member's top end corner — the /transactions invoices link's\n * pending count. Nothing is drawn for `0` or less; above 99 it reads `99+`. The count\n * joins the accessible name through `floatingPanel.badge` (\"Invoices, 3 new\"), or\n * `badgeLabel` when passed.\n */\n badge?: number;\n /** The badge's words for a reader, replacing `floatingPanel.badge` — \"3 to review\". */\n badgeLabel?: string;\n /** The dot's colour. Default `warning`, the pending count's. */\n badgeTone?: \"warning\" | \"danger\" | \"brand\";\n /** Show `label` in the kit {@link Tooltip} on hover and focus. Default `true`: the\n * members are icon-only and this is their only visible label. */\n tooltip?: boolean;\n /** Where the tooltip opens. Default `top` — the group sits at the bottom of the screen. */\n tooltipSide?: TooltipSide;\n /** Make it a link — the /transactions invoices member. Drawn by `renderLink`, else\n * the `<UiKitProvider linkComponent>` (an in-app `href`), else a plain `<a>`. */\n href?: string;\n /** Render the link through your router's `Link` — see {@link FloatingActionLinkProps}. */\n renderLink?: (props: FloatingActionLinkProps) => ReactElement;\n onClick?: (event: MouseEvent<HTMLElement>) => void;\n ref?: Ref<HTMLButtonElement>;\n}\n\nconst BADGE_TONE: Record<NonNullable<FloatingActionProps[\"badgeTone\"]>, string> = {\n warning: \"bg-[var(--warning)] text-[var(--text-inverse)]\",\n danger: \"bg-[var(--danger)] text-[var(--danger-contrast)]\",\n brand: \"bg-[var(--brand)] text-[var(--brand-contrast)]\",\n};\n\n/**\n * One member of a {@link FloatingActionGroup}: a `size-12` icon button, toggle or link\n * with its label in a styled tooltip — what each keksdose member wrapped by hand in\n * `<Tooltip portal>`. The tooltip is portalled because the group clips (`overflow-hidden`\n * keeps the brand member's fill inside the pill's corners), and the focus ring is inset\n * for the same reason.\n */\nexport function FloatingAction({\n label,\n icon,\n pressed,\n pressedStyle = \"tint\",\n variant = \"default\",\n badge,\n badgeLabel,\n badgeTone = \"warning\",\n tooltip = true,\n tooltipSide = \"top\",\n href,\n renderLink,\n className,\n type = \"button\",\n onClick,\n ref,\n ...rest\n}: FloatingActionProps) {\n const labels = useKitLabels(\"floatingPanel\", DEFAULT_FLOATING_PANEL_LABELS);\n const kitLink = useKitLink();\n const counted = badge !== undefined && badge > 0;\n const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;\n const isLink = href !== undefined;\n const look = cn(\n \"relative flex size-12 items-center justify-center transition-colors\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\",\n \"disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-5\",\n variant === \"primary\"\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\"\n : pressed && !isLink && pressedStyle === \"tint\"\n ? \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\",\n className,\n );\n const body = (\n <>\n {icon}\n {counted && (\n <span\n aria-hidden\n data-badge=\"\"\n className={cn(\n \"pointer-events-none absolute end-1 top-1 h-4 min-w-4 rounded-full px-1 text-center text-[10px] leading-4 font-semibold\",\n BADGE_TONE[badgeTone],\n )}\n >\n {badge > 99 ? \"99+\" : badge}\n </span>\n )}\n </>\n );\n\n let member: ReactElement;\n if (isLink) {\n // The button-only attributes stay behind; `aria-*`, `data-*` and the like ride along.\n const { disabled: _disabled, form: _form, value: _value, name: _name, ...anchorRest } = rest;\n const linkProps: FloatingActionLinkProps = {\n ...(anchorRest as AnchorHTMLAttributes<HTMLAnchorElement>),\n href,\n \"aria-label\": name,\n onClick,\n className: look,\n children: body,\n };\n const { children: content, ...anchor } = linkProps;\n const render = pickLinkRenderer(renderLink, kitLink, href);\n member = render ? (\n <RenderedFloatingLink render={render} {...linkProps} />\n ) : (\n <a {...anchor}>{content}</a>\n );\n } else {\n member = (\n <button\n {...rest}\n ref={ref}\n type={type}\n aria-label={name}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={onClick}\n className={look}\n >\n {body}\n </button>\n );\n }\n if (!tooltip) return member;\n return (\n // A fragment: the bubble says the member's name, and a description that repeats\n // the name is read twice.\n <Tooltip label={label} side={tooltipSide} portal>\n <>{member}</>\n </Tooltip>\n );\n}\n\n/** Calls `renderLink` as a component, so its hooks belong to the link, not to us. */\nfunction RenderedFloatingLink({\n render,\n ...props\n}: FloatingActionLinkProps & { render: (props: FloatingActionLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAyMU,SA0DyB,UAvBrB,KAnCJ;AAzMV,SAAS,WAAW,OAAO,iBAAiB,QAAQ,gBAAgB;AACpE,SAAS,oBAAoB;AAY7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,aAA6B;AACtC,SAAS,cAAc,kBAAkB;AACzC,SAAS,wBAAwB;AACjC,SAAS,eAAiC;AAanC,MAAM,gCAAqD;AAAA,EAChE,OAAO;AAAA,EACP,OAAO,CAAC,UAAU,GAAG,KAAK;AAC5B;AAYA,MAAM,mBAAmB;AAGzB,MAAM,cAAc;AAEpB,SAAS,YAAY,QAAwB,MAA6B;AACxE,SAAO;AAAA,IACL,QAAQ,QAAQ,gBAAgB,MAAM,IAAI;AAAA;AAAA;AAAA,IAG1C,CAAC,WAAW,iBAAiB,qBAAqB,gBAAgB,GAAG;AAAA,EACvE;AACF;AAQA,SAAS,eAA8D;AACrE,QAAM,SAAS,OAAwB,IAAI;AAC3C,QAAM,CAAC,KAAK,MAAM,IAAI,SAAgC,MAAS;AAC/D,kBAAgB,MAAM;AACpB,WAAO,MAAM,OAAO,OAAO,CAAC;AAAA,EAC9B,GAAG,CAAC,CAAC;AACL,SAAO,CAAC,QAAQ,GAAG;AACrB;AAqGO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAA8B;AAC5B,QAAM,CAAC,QAAQ,GAAG,IAAI,aAAa;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,MAAM,YAAY,OAAO,QAAQ,YAAY,SAAS,YAAY,KAAK,SAAY;AACzF,QAAM,UAAU,QAAQ,UAAa,QAAQ;AAG7C,QAAM,YAAY,CAAC,WAAW,eAAe,UAAU,KAAM,SAAS,QAAS;AAC/E,QAAM,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,gBAAc,WAAW,KAAK,cAAc;AAAA,MAC5C,OAAO;AAAA,MACP,OAAO,UAAU,QAAQ,EAAE,GAAG,YAAY,QAAQ,MAAM,GAAG,GAAG,MAAM;AAAA,MACpE,WAAW;AAAA;AAAA,QAET,CAAC,WAAW;AAAA,QACZ;AAAA,QACA,WAAW,uDAAuD;AAAA,QAClE;AAAA,QACA,CAAC,UACC,YAAY,aACZ;AAAA,QACF,CAAC,UACC,YAAY,aACZ;AAAA;AAAA;AAAA,QAGF,UAAU;AAAA,QACV,YAAY,SACV;AAAA,QACF,YAAY,QACV;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,QAGA,YACC,oBAAC,UAAK,eAAW,MAAC,WAAU,qBACzB,iBACH;AAAA;AAAA;AAAA,EAEJ;AAER,SACE,iCACE;AAAA,wBAAC,UAAK,KAAK,QAAQ,QAAM,MAAC;AAAA,IACzB,OAAO,aAAa,eACnB;AAAA,MACE,iCACG;AAAA,kBACC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,QAAQ,KAAK;AAAA,YACb,OAAO,YAAY,QAAQ,MAAM;AAAA,YACjC,WAAU;AAAA,YAEV,8BAAC,WAAQ,OAAO,KAAK,MAAM,aAAa,QAAM,MAI3C,kBAAQ,QAAQ,gCAAG,kBAAO,IAAM,QACnC;AAAA;AAAA,QACF,IAEA;AAAA,QAED,QACC,oBAAC,UAAK,MAAK,UAAS,WAAU,WAC3B,iBACH;AAAA,SAEJ;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;AAwEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,SAAS;AAAA,EACT,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA,eAAe,SAAY,SAAY,EAAE,OAAO,WAAW;AAAA,EAC7D;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,WAAW;AAClD,QAAM,OAAO,YAAY;AACzB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,aAAa,OAAW,YAAW,IAAI;AAC3C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,MAAM,MAAM;AAClB,QAAM,UAAU,GAAG,GAAG;AACtB,QAAM,UAAU,GAAG,GAAG;AACtB,QAAM,SAAS,OAA0B,IAAI;AAC7C,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,QAAQ,GAAG,IAAI,aAAa;AAEnC,QAAM,kBAAkB,OAAO,YAAY;AAC3C,YAAU,MAAM;AACd,oBAAgB,UAAU;AAAA,EAC5B,CAAC;AAID,QAAM,UAAU,OAAO,IAAI;AAG3B,QAAM,cAAc,OAAO,KAAK;AAChC,YAAU,MAAM;AACd,UAAM,UAAU,QAAQ,YAAY;AACpC,YAAQ,UAAU;AAClB,QAAI,CAAC,QAAS;AACd,QAAI,MAAM;AACR,OAAC,gBAAgB,UAAU,KAAK,SAAS,UAAU,MAAM;AACzD;AAAA,IACF;AACA,UAAM,SAAS,SAAS;AACxB,UAAM,OAAO,WAAW,QAAQ,WAAW,SAAS;AACpD,QAAI,YAAY,WAAW,KAAM,QAAO,SAAS,MAAM;AACvD,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,iBAAiB,CAAC,MAAqC;AAC3D,QAAI,EAAE,QAAQ,YAAY,EAAE,iBAAkB;AAG9C,MAAE,gBAAgB;AAClB,gBAAY,UAAU;AACtB,YAAQ,KAAK;AAAA,EACf;AAEA,SACE,iCACE;AAAA,wBAAC,UAAK,KAAK,QAAQ,QAAM,MAAC;AAAA,IAC1B;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,aAAa;AAAA,QACb,SAAS;AAAA,QACT,iBAAe;AAAA,QACf,iBAAe,OAAO,UAAU;AAAA,QAChC,SAAS,MAAM;AAGb,sBAAY,UAAU;AACtB,kBAAQ,CAAC,IAAI;AAAA,QACf;AAAA;AAAA,IACF;AAAA,IACC,QACC,OAAO,aAAa,eACpB;AAAA;AAAA;AAAA,MAGE;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,IAAI;AAAA,UACJ,MAAK;AAAA,UACL,mBAAiB;AAAA,UACjB,UAAU;AAAA,UACV;AAAA,UACA,eAAa;AAAA,UACb,WAAW;AAAA,UACX,SAAS,MAAM;AACb,wBAAY,UAAU;AAAA,UACxB;AAAA,UACA,QAAQ,CAAC,MAAM;AACb,gBAAI,CAAC,EAAE,cAAc,SAAS,EAAE,aAA4B,EAAG,aAAY,UAAU;AAAA,UACvF;AAAA,UAGA,OACE;AAAA,YACE,eAAe,QAAQ,gBAAgB;AAAA,YACvC,kBAAkB,QAAQ,gBAAgB,MAAM,MAAM;AAAA,YACtD,aAAa;AAAA,UACf;AAAA,UAEF,WAAW;AAAA,YACT;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,YACA,WAAW,iBAAiB,8BAA8B;AAAA,YAC1D;AAAA,UACF;AAAA,UAEA;AAAA,iCAAC,SAAI,WAAU,8FACb;AAAA,kCAAC,QAAG,IAAI,SAAS,WAAU,uEACxB,iBACH;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM;AACb,gCAAY,UAAU;AACtB,4BAAQ,KAAK;AAAA,kBACf;AAAA,kBACA,cAAY,OAAO;AAAA,kBACnB,WAAU;AAAA,kBAEV,8BAAC,KAAE,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA,cACpC;AAAA,eACF;AAAA,YACA,oBAAC,SAAI,WAAW,GAAG,yDAAyD,aAAa,GACtF,UACH;AAAA;AAAA;AAAA,MACF;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;AAgCO,SAAS,oBAAoB;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,QAAM,CAAC,QAAQ,GAAG,IAAI,aAAa;AACnC,SACE,iCACE;AAAA,wBAAC,UAAK,KAAK,QAAQ,QAAM,MAAC;AAAA,IACzB,OAAO,aAAa,eACnB;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ,MAAK;AAAA,UACL;AAAA,UACA,eAAa;AAAA,UACb,OAAO,EAAE,GAAG,YAAY,QAAQ,MAAM,GAAG,GAAG,MAAM;AAAA,UAClD,WAAW;AAAA,YACT;AAAA,YACA;AAAA;AAAA;AAAA;AAAA,YAIA;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;AA8DA,MAAM,aAA4E;AAAA,EAChF,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AACT;AASO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,QAAM,SAAS,aAAa,iBAAiB,6BAA6B;AAC1E,QAAM,UAAU,WAAW;AAC3B,QAAM,UAAU,UAAU,UAAa,QAAQ;AAC/C,QAAM,OAAO,UAAU,GAAG,KAAK,KAAK,cAAc,OAAO,MAAM,KAAK,CAAC,KAAK;AAC1E,QAAM,SAAS,SAAS;AACxB,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,YACR,iFACA,WAAW,CAAC,UAAU,iBAAiB,SACrC,8EACA;AAAA,IACN;AAAA,EACF;AACA,QAAM,OACJ,iCACG;AAAA;AAAA,IACA,WACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,cAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,WAAW,SAAS;AAAA,QACtB;AAAA,QAEC,kBAAQ,KAAK,QAAQ;AAAA;AAAA,IACxB;AAAA,KAEJ;AAGF,MAAI;AACJ,MAAI,QAAQ;AAEV,UAAM,EAAE,UAAU,WAAW,MAAM,OAAO,OAAO,QAAQ,MAAM,OAAO,GAAG,WAAW,IAAI;AACxF,UAAM,YAAqC;AAAA,MACzC,GAAI;AAAA,MACJ;AAAA,MACA,cAAc;AAAA,MACd;AAAA,MACA,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AACA,UAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,UAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,aAAS,SACP,oBAAC,wBAAqB,QAAiB,GAAG,WAAW,IAErD,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AAAA,EAE5B,OAAO;AACL,aACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA;AAAA,QACA,cAAY;AAAA,QACZ,gBAAc,WAAW,KAAK,cAAc;AAAA,QAC5C;AAAA,QACA,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,MAAI,CAAC,QAAS,QAAO;AACrB;AAAA;AAAA;AAAA,IAGE,oBAAC,WAAQ,OAAc,MAAM,aAAa,QAAM,MAC9C,0CAAG,kBAAO,GACZ;AAAA;AAEJ;AAGA,SAAS,qBAAqB;AAAA,EAC5B;AAAA,EACA,GAAG;AACL,GAA2F;AACzF,SAAO,OAAO,KAAK;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/components/floating-panel.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n KeyboardEvent,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { registerFloating } from \"../lib/floating-stack\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { assignRef } from \"./choice-parts\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\n\nexport interface FloatingPanelLabels {\n /** Accessible name of the panel header's X. */\n close: string;\n /**\n * What a {@link FloatingAction}'s count badge adds to its accessible name (\"3 new\"),\n * read after the label: \"Invoices, 3 new\". The dot itself is hidden from a reader —\n * a bare \"3\" read out of context says nothing about what is counted.\n */\n badge: (count: number) => string;\n}\n\nexport const DEFAULT_FLOATING_PANEL_LABELS: FloatingPanelLabels = {\n close: \"Close\",\n badge: (count) => `${count} new`,\n};\n\n/** A LOGICAL corner: `bottom-end` is bottom-right in LTR and bottom-left in RTL. */\nexport type FloatingCorner = \"bottom-end\" | \"bottom-start\";\n\n/**\n * How far off the bottom edge anything floating here sits: clear of `AppShell`'s phone\n * nav (`--app-nav-h`, `0px` above `md` where the nav is not rendered) and of the home\n * indicator, whichever reaches higher — the nav does not pad itself for the inset, so\n * with no nav on screen the inset is all there is to clear. `max`, not a sum: where the\n * nav does cover the inset, adding both would float the button a nav-height too high.\n */\nconst BOTTOM_CLEARANCE = \"max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px))\";\n/** The side gutter, widened for a landscape notch. Both insets, because a logical\n * side has no one physical inset to name. */\nconst SIDE_GUTTER = \"max(1rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\";\n\nfunction cornerStyle(corner: FloatingCorner, lift: string): CSSProperties {\n return {\n bottom: `calc(${BOTTOM_CLEARANCE} + ${lift})`,\n // Logical properties, so the corner follows the reading direction with no JS. The\n // portal carries the `dir` across (see `usePortalDir`).\n [corner === \"bottom-start\" ? \"insetInlineStart\" : \"insetInlineEnd\"]: SIDE_GUTTER,\n };\n}\n\n/**\n * The reading direction where the component was RENDERED, for its portalled root: a\n * portal to `<body>` leaves the subtree whose `dir` it inherited, and a logical\n * `inset-inline-end` resolves against the portal's own direction. Read off a hidden\n * inline marker in a layout effect, so the first painted frame already has it.\n */\nfunction usePortalDir(): [Ref<HTMLSpanElement>, Direction | undefined] {\n const marker = useRef<HTMLSpanElement>(null);\n const [dir, setDir] = useState<Direction | undefined>(undefined);\n useLayoutEffect(() => {\n setDir(dirOf(marker.current));\n }, []);\n return [marker, dir];\n}\n\nexport interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n /** The accessible name — the button shows only its icon. */\n label: string;\n icon: ReactNode;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /**\n * Distance above the nav / home indicator, as a CSS length. Default `1rem`. Raise it\n * to stack above another floating control in the same corner — keksdose's assistant\n * clears the transactions page's own action group this way (`calc(1rem + 4rem)`).\n */\n offset?: string;\n /**\n * Whether the button carries the browser's own tooltip (`title`, the `label` unless a\n * `title` is passed). Default `true`, so an icon-only button keeps a hover hint for a\n * mouse user with no other way to learn what it is. `false` renders no `title` at all:\n * keksdose has ONE tooltip, the kit's `Tooltip` (dev#523), and its source scan fails a\n * native `title=` anywhere — so a flag, not `title=\"\"`, which the scan would still\n * see. An empty `title` renders no attribute either, for the caller who has no scan.\n *\n * Not flipped to off by default: a FAB wrapped in nothing would lose the only hover\n * hint it has, silently, in every app already on it. And the kit cannot wrap itself\n * in its `Tooltip` — the bubble's anchor is a `relative inline-flex` span, and the\n * button is `fixed` and portalled — so the one-tooltip caller wraps it.\n */\n nativeTitle?: boolean;\n /**\n * Make it a toggle. `true`/`false` set `aria-pressed` and swap the solid brand disc for\n * a surface disc — the glyph in brand on the quiet brand fill when on, the secondary\n * text colour when off — the \"on\" look of `IconButton`'s `pressed` and of a selected\n * Chip. Left out, it is the ordinary action FAB. For keksdose's corner filter toggles\n * (feedback-page's \"awaiting only\", the /transactions pending and upcoming toggles),\n * which paint `text-[var(--brand)]` on a surface button by hand. Keep `label` the same\n * in both states; `aria-pressed` already says which one it is in.\n */\n pressed?: boolean;\n /**\n * Show `label` beside the icon: the EXTENDED FAB, a pill rather than a disc. For a\n * corner control whose words are the point — kastlan's offline indicator\n * (shared/offline/offline-indicator.tsx:39) is a pill saying \"Offline — 2 photo(s)\n * queued\" / \"Syncing…\", hand-placed at `fixed bottom-4 left-4`, i.e. over the\n * bottom nav and on the wrong side in RTL. Same height as the disc (3rem), so a\n * {@link FloatingPanel} card above it still clears it.\n */\n extended?: boolean;\n /**\n * Announce `label` whenever it changes, through a polite `role=\"status\"` region — for\n * an extended FAB that REPORTS something (kastlan's offline/sync state), where a\n * sighted user sees the words change and a screen reader user otherwise hears\n * nothing until they happen to land on the button.\n *\n * ⚠️ A live region announces changes, not its own arrival: a FAB that mounts already\n * saying \"Offline\" says it to nobody. Keep it mounted and pass `hidden` while there\n * is nothing to report (kastlan returns `null` when online and idle); the region\n * stays in the page, and the first real status is heard.\n */\n live?: boolean;\n /**\n * `primary` (default): the solid brand disc of a page's main action. `surface`: the\n * page's surface with a hairline border — for a FAB that is a status rather than a\n * call to action, as kastlan's offline pill is (a brand pill would shout \"press me\"\n * at a user who is merely offline). Ignored while `pressed` is set; a toggle has its\n * own two looks.\n */\n variant?: \"primary\" | \"surface\";\n /**\n * Hover and focus label in the kit {@link Tooltip}, instead of the native `title` —\n * keksdose has ONE tooltip (dev#523) and wrapped its assistant launcher by hand,\n * which cannot work: the tooltip's anchor is a `relative inline-flex` span, and this\n * button is `fixed` and portalled. With `tooltip` the FAB positions a fixed wrapper\n * instead and the button sits in the tooltip inside it. `true` shows the `label`;\n * any other content shows that. Implies no native `title` (whatever `nativeTitle`\n * says): one hover hint, not two. `style` stays on the button; the corner and\n * `offset` move to the wrapper.\n */\n tooltip?: ReactNode;\n /** Where the {@link tooltip} opens. Default `top`; the bubble is portalled and kept on\n * screen, so a corner button's label is not pushed off the edge. */\n tooltipSide?: TooltipSide;\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * A round, icon-only button pinned to a corner of the viewport — the trigger of a\n * {@link FloatingPanel}, and usable alone for a page's one primary action.\n *\n * keksdose hand-placed each of its floating controls: the assistant launcher at\n * `fixed bottom-36 right-4 md:bottom-6`, the feedback page's and the transactions\n * page's action groups at `right-4 bottom-20`. Every one of those numbers is a guess\n * at the phone nav's height plus whatever else floats there (Keksdose live #314 found\n * out what such a guess is worth for a footer), `right` rather than `end` puts it over\n * the page's leading edge in RTL, and none allows for the home indicator. Here the\n * base is `--app-nav-h` and the safe-area inset, the corner is logical, and\n * {@link FloatingActionButtonProps.offset} is the one number left to the caller — the\n * height of whatever else it has to clear, not the nav's.\n *\n * Portalled, because `position: fixed` inside any transformed ancestor (an animated\n * page transition) is fixed to that ancestor, not to the screen.\n *\n * While mounted it reports its box (`lib/floating-stack.ts`), so `ServerWakeNotice`\n * stacks above it rather than over it.\n */\nexport function FloatingActionButton({\n label,\n icon,\n corner = \"bottom-end\",\n offset = \"1rem\",\n nativeTitle = true,\n pressed,\n extended = false,\n live = false,\n variant = \"primary\",\n tooltip,\n tooltipSide = \"top\",\n className,\n style,\n type = \"button\",\n title,\n ref,\n ...rest\n}: FloatingActionButtonProps) {\n const [marker, dir] = usePortalDir();\n const toggle = pressed !== undefined;\n const tip = tooltip === true ? label : tooltip === false || tooltip === \"\" ? undefined : tooltip;\n const wrapped = tip !== undefined && tip !== null;\n // The fixed element — the button, or the wrapper when there is a tooltip.\n const [fixed, setFixed] = useState<HTMLElement | null>(null);\n useEffect(() => (fixed ? registerFloating(fixed) : undefined), [fixed]);\n const buttonRef = useCallback(\n (node: HTMLButtonElement | null) => {\n assignRef(ref, node);\n if (!wrapped) setFixed(node);\n },\n [ref, wrapped],\n );\n // An empty title is \"no tooltip\", not an attribute with nothing in it. The kit\n // tooltip replaces it outright.\n const nativeTip = !wrapped && nativeTitle && title !== \"\" ? (title ?? label) : undefined;\n const button = (\n <button\n {...rest}\n ref={buttonRef}\n type={type}\n dir={dir}\n aria-label={label}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n title={nativeTip}\n style={wrapped ? style : { ...cornerStyle(corner, offset), ...style }}\n className={cn(\n // Wrapped, the wrapper is the fixed thing and the button sits in it.\n !wrapped && \"fixed z-40\",\n \"inline-flex items-center justify-center rounded-full\",\n extended ? \"h-12 gap-2 px-4 text-sm font-medium [&_svg]:size-5\" : \"size-12 [&_svg]:size-6\",\n \"shadow-lg transition-colors disabled:cursor-default\",\n !toggle &&\n variant === \"primary\" &&\n \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\",\n !toggle &&\n variant === \"surface\" &&\n \"border border-[var(--border)] bg-[var(--bg-surface)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)] disabled:hover:bg-[var(--bg-surface)]\",\n // A toggle sits on the page's surface — a solid brand disc would read as\n // \"on\" in both states — and says \"on\" in the brand family.\n toggle && \"border border-[var(--border)]\",\n pressed === false &&\n \"bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\",\n pressed === true &&\n \"border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2\",\n className,\n )}\n >\n {icon}\n {/* Visible words, not the name: `aria-label` already is the name, and the\n span would only repeat it. */}\n {extended && (\n <span aria-hidden className=\"whitespace-nowrap\">\n {label}\n </span>\n )}\n </button>\n );\n return (\n <>\n <span ref={marker} hidden />\n {typeof document !== \"undefined\" &&\n createPortal(\n <>\n {wrapped ? (\n <div\n ref={setFixed}\n dir={dir}\n hidden={rest.hidden}\n style={cornerStyle(corner, offset)}\n className=\"fixed z-40 inline-flex\"\n >\n <Tooltip label={tip} side={tooltipSide} portal>\n {/* A fragment when the bubble says the name again, so the tooltip\n does not describe the button with its own label — a reader would\n hear it twice. Other text is a real description. */}\n {tip === label ? <>{button}</> : button}\n </Tooltip>\n </div>\n ) : (\n button\n )}\n {live && (\n <span role=\"status\" className=\"sr-only\">\n {label}\n </span>\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\nexport interface FloatingPanelProps {\n /** The panel's heading, and its accessible name. */\n title: ReactNode;\n /** The trigger's accessible name (\"Assistant\", \"Send feedback\"). */\n fabLabel: string;\n fabIcon: ReactNode;\n /** Controlled open state. Leave it out to let the panel own it. */\n open?: boolean;\n /** Uncontrolled initial state. Default closed. */\n defaultOpen?: boolean;\n /** Every request to open or close — the FAB, the X, Escape. */\n onOpenChange?: (open: boolean) => void;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /** The FAB's distance above the nav — see {@link FloatingActionButtonProps.offset}.\n * From `md` up the card sits above the FAB, so it moves with it. */\n offset?: string;\n /** The FAB's native `title` — see {@link FloatingActionButtonProps.nativeTitle}.\n * `false` for keksdose's assistant launcher, under its one-tooltip rule (dev#523). */\n fabNativeTitle?: boolean;\n /** The FAB's kit tooltip — see {@link FloatingActionButtonProps.tooltip}. `true` for\n * keksdose's assistant launcher, which wanted the kit bubble and had no way to\n * put one on a fixed, portalled button (dev#523). */\n fabTooltip?: ReactNode;\n /** Default: `floatingPanel.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /**\n * Where focus goes on open. Default the panel itself, so a phone's keyboard does not\n * rise over the sheet before the user has asked to type; pass the chat field for a\n * panel whose one job is typing.\n */\n initialFocus?: () => HTMLElement | null;\n /** Extra classes for the panel. */\n className?: string;\n /** Extra classes for the scrolling body. */\n bodyClassName?: string;\n children: ReactNode;\n}\n\n/**\n * A docked, NON-MODAL panel in a corner of the screen, opened by a\n * {@link FloatingActionButton} — keksdose's assistant launcher and its feedback page,\n * which each built one.\n *\n * ## Not a Modal\n *\n * The page stays usable while it is open: that is the point of an assistant beside the\n * data it is asked about. So there is no backdrop, no scroll lock, no `aria-modal` and\n * no focus trap — Tab walks out of the panel into the page and back, and a click on the\n * page does not close it. It is `role=\"dialog\"` without `aria-modal`, which is what\n * ARIA calls a non-modal dialog, named by its heading.\n *\n * What it keeps from a dialog is the focus HANDOFF: opening moves focus into it (a\n * keyboard user who pressed the FAB should not have to hunt for what appeared), and\n * closing gives it back to the FAB — but only when focus was in the panel, so a panel\n * closed from the page (a controlled owner) never pulls focus out of the field the\n * user is typing in. Escape closes it while focus is inside, and only then: on the\n * panel element, not the document, so Escape in the page keeps meaning whatever the\n * page means by it.\n *\n * ## Layout\n *\n * From `md` up, a card above the FAB in its corner. Below `md`, a full-width sheet\n * docked to the bottom, sitting ON the `AppShell` bottom nav (`--app-nav-h`) and\n * clear of the home indicator. Pure CSS — no width is measured in JavaScript.\n *\n * The FAB carries `aria-expanded` and, while the panel exists, `aria-controls`. The\n * panel unmounts when closed; an app that must keep a draft across a close keeps it in\n * its own state, where it also survives a reload.\n */\nexport function FloatingPanel({\n title,\n fabLabel,\n fabIcon,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n corner = \"bottom-end\",\n offset = \"1rem\",\n fabNativeTitle,\n fabTooltip,\n closeLabel,\n initialFocus,\n className,\n bodyClassName,\n children,\n}: FloatingPanelProps) {\n const labels = useKitLabels(\n \"floatingPanel\",\n DEFAULT_FLOATING_PANEL_LABELS,\n closeLabel === undefined ? undefined : { close: closeLabel },\n );\n const [ownOpen, setOwnOpen] = useState(defaultOpen);\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n };\n\n const uid = useId();\n const panelId = `${uid}-panel`;\n const titleId = `${uid}-title`;\n const fabRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [marker, dir] = usePortalDir();\n\n const initialFocusRef = useRef(initialFocus);\n useEffect(() => {\n initialFocusRef.current = initialFocus;\n });\n\n // Focus follows the open/closed TRANSITION, never the mount: a panel that renders\n // open (`defaultOpen`, a restored route) does not take the page's focus by existing.\n const wasOpen = useRef(open);\n // Whether focus was inside the panel when it last changed — read at close time, when\n // the panel (and whatever had focus in it) is already gone.\n const focusInside = useRef(false);\n useEffect(() => {\n const changed = wasOpen.current !== open;\n wasOpen.current = open;\n if (!changed) return;\n if (open) {\n (initialFocusRef.current?.() ?? panelRef.current)?.focus();\n return;\n }\n const active = document.activeElement;\n const lost = active === null || active === document.body;\n if (focusInside.current || lost) fabRef.current?.focus();\n focusInside.current = false;\n }, [open]);\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\" || e.defaultPrevented) return;\n // Stopped here, so an overlay underneath with a document-level Escape listener does\n // not close too — the innermost thing that has focus is the one that closes.\n e.stopPropagation();\n focusInside.current = true;\n setOpen(false);\n };\n\n return (\n <>\n <span ref={marker} hidden />\n <FloatingActionButton\n ref={fabRef}\n label={fabLabel}\n icon={fabIcon}\n corner={corner}\n offset={offset}\n nativeTitle={fabNativeTitle}\n tooltip={fabTooltip}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n onClick={() => {\n // A press ON the FAB closing the panel: the FAB keeps focus by being clicked,\n // nothing to hand back.\n focusInside.current = false;\n setOpen(!open);\n }}\n />\n {open &&\n typeof document !== \"undefined\" &&\n createPortal(\n // A container for Escape bubbling up from anything inside it, as a dialog is.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={panelRef}\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n tabIndex={-1}\n dir={dir}\n data-corner={corner}\n onKeyDown={onPanelKeyDown}\n onFocus={() => {\n focusInside.current = true;\n }}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) focusInside.current = false;\n }}\n // The phone's sheet sits on the nav; from `md` up the card floats above the\n // FAB (its offset + the 3rem button + a 0.75rem gap).\n style={\n {\n \"--fp-bottom\": `calc(${BOTTOM_CLEARANCE})`,\n \"--fp-bottom-md\": `calc(${BOTTOM_CLEARANCE} + ${offset} + 3.75rem)`,\n \"--fp-side\": SIDE_GUTTER,\n } as CSSProperties\n }\n className={cn(\n \"animate-sheet fixed z-40 flex flex-col overflow-hidden outline-none\",\n \"border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl\",\n // Phone: full width, docked, rounded only where it meets the page.\n \"inset-x-0 bottom-[var(--fp-bottom)] max-h-[min(85dvh,calc(100dvh_-_var(--fp-bottom)_-_1rem))] rounded-t-xl border-x-0 border-b-0\",\n \"pb-[env(safe-area-inset-bottom,0px)] md:pb-0\",\n // md+: a card in the corner, above the FAB.\n \"md:inset-x-auto md:bottom-[var(--fp-bottom-md)] md:w-[24rem] md:max-h-[min(36rem,calc(100dvh_-_var(--fp-bottom-md)_-_1rem))] md:rounded-xl md:border\",\n corner === \"bottom-start\" ? \"md:start-[var(--fp-side)]\" : \"md:end-[var(--fp-side)]\",\n className,\n )}\n >\n <div className=\"flex shrink-0 items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-3\">\n <h2 id={titleId} className=\"min-w-0 truncate text-base font-semibold text-[var(--text-primary)]\">\n {title}\n </h2>\n <button\n type=\"button\"\n onClick={() => {\n focusInside.current = true;\n setOpen(false);\n }}\n aria-label={labels.close}\n className=\"-me-1.5 shrink-0 rounded p-1.5 text-[var(--text-muted)] outline-none hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n </div>\n <div className={cn(\"min-h-0 flex-1 overflow-y-auto overscroll-contain p-4\", bodyClassName)}>\n {children}\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n}\n\nexport interface FloatingActionGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, \"role\"> {\n /** Name the group (\"Register actions\"); a reader then introduces the members as one\n * set. A group with no name is one more unlabelled wrapper. */\n \"aria-label\"?: string;\n /** Default `bottom-end`. */\n corner?: FloatingCorner;\n /** Distance above the nav / home indicator — see {@link FloatingActionButtonProps.offset}. */\n offset?: string;\n /** {@link FloatingAction}s, in reading order. */\n children: ReactNode;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A floating pill of {@link FloatingAction}s — one to four `size-12` toggles, actions\n * or links with hairline dividers — pinned to a corner exactly as the\n * {@link FloatingActionButton} is: `--app-nav-h` and the home indicator cleared, a\n * logical corner, portalled.\n *\n * keksdose built it twice by hand: the /transactions phone toolbar\n * (transactions-page.tsx:644 — upcoming and pending toggles, an invoices link with a\n * pending count, and the brand \"+\") and the feedback page's \"awaiting only\" toggle\n * (feedback-page.tsx:854, one button in the group anyway, so the two pages look\n * alike). Both at `right-4 bottom-20` — `right`, not `end`, so over the page's leading\n * edge in RTL — with `divide-x`, slate hovers, and each member's `Tooltip` spelled out.\n *\n * `role=\"group\"`, NOT a toolbar: every member keeps its own tab stop, as the\n * hand-built ones did, and there is no arrow-key roving to learn. Hide it where it\n * does not belong with a class (`md:hidden`, as both keksdose pages do).\n */\nexport function FloatingActionGroup({\n corner = \"bottom-end\",\n offset = \"1rem\",\n className,\n style,\n children,\n ...rest\n}: FloatingActionGroupProps) {\n const [marker, dir] = usePortalDir();\n return (\n <>\n <span ref={marker} hidden />\n {typeof document !== \"undefined\" &&\n createPortal(\n <div\n {...rest}\n role=\"group\"\n dir={dir}\n data-corner={corner}\n style={{ ...cornerStyle(corner, offset), ...style }}\n className={cn(\n \"fixed z-40 inline-flex flex-row overflow-hidden rounded-2xl\",\n \"border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\",\n // Dividers on the logical start edge of every member but the first, so\n // they sit between the same two members in RTL. The member is the\n // tooltip's wrapper span, which is the direct child.\n \"[&>*:not(:first-child)]:border-s [&>*]:border-[var(--border)]\",\n className,\n )}\n >\n {children}\n </div>,\n document.body,\n )}\n </>\n );\n}\n\n/** What {@link FloatingActionProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface FloatingActionLinkProps {\n href: string;\n /** The member's whole look — keep it. */\n className: string;\n children: ReactNode;\n \"aria-label\": string;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | \"onClick\"> {\n /** The accessible name, and the tooltip's text. Keep it the same in both states of a\n * toggle; `aria-pressed` says which one it is in. */\n label: string;\n icon: ReactNode;\n /** Make it a toggle: `aria-pressed` and the brand \"on\" look (the brand glyph on the\n * quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.\n * For the state without the tint, see {@link pressedStyle}. */\n pressed?: boolean;\n /**\n * How `pressed` looks. `tint` (default): the brand \"on\" look. `plain`: the member\n * keeps its quiet look in both states and only `aria-pressed` changes — for a toggle\n * whose icon already says which state it is in (keksdose's /transactions \"Upcoming\"\n * swaps two icons; a tint on top said it twice). The same as passing `aria-pressed`\n * without `pressed`, which also rides through untinted.\n */\n pressedStyle?: \"tint\" | \"plain\";\n /**\n * `primary`: the brand-filled member, for the group's main action (the /transactions\n * \"+\"). Default: the quiet one.\n */\n variant?: \"default\" | \"primary\";\n /**\n * A count in a dot at the member's top end corner — the /transactions invoices link's\n * pending count. Nothing is drawn for `0` or less; above 99 it reads `99+`. The count\n * joins the accessible name through `floatingPanel.badge` (\"Invoices, 3 new\"), or\n * `badgeLabel` when passed.\n */\n badge?: number;\n /** The badge's words for a reader, replacing `floatingPanel.badge` — \"3 to review\". */\n badgeLabel?: string;\n /** The dot's colour. Default `warning`, the pending count's. */\n badgeTone?: \"warning\" | \"danger\" | \"brand\";\n /** Show `label` in the kit {@link Tooltip} on hover and focus. Default `true`: the\n * members are icon-only and this is their only visible label. */\n tooltip?: boolean;\n /** Where the tooltip opens. Default `top` — the group sits at the bottom of the screen. */\n tooltipSide?: TooltipSide;\n /** Make it a link — the /transactions invoices member. Drawn by `renderLink`, else\n * the `<UiKitProvider linkComponent>` (an in-app `href`), else a plain `<a>`. */\n href?: string;\n /** Render the link through your router's `Link` — see {@link FloatingActionLinkProps}. */\n renderLink?: (props: FloatingActionLinkProps) => ReactElement;\n onClick?: (event: MouseEvent<HTMLElement>) => void;\n ref?: Ref<HTMLButtonElement>;\n}\n\nconst BADGE_TONE: Record<NonNullable<FloatingActionProps[\"badgeTone\"]>, string> = {\n warning: \"bg-[var(--warning)] text-[var(--text-inverse)]\",\n danger: \"bg-[var(--danger)] text-[var(--danger-contrast)]\",\n brand: \"bg-[var(--brand)] text-[var(--brand-contrast)]\",\n};\n\n/**\n * One member of a {@link FloatingActionGroup}: a `size-12` icon button, toggle or link\n * with its label in a styled tooltip — what each keksdose member wrapped by hand in\n * `<Tooltip portal>`. The tooltip is portalled because the group clips (`overflow-hidden`\n * keeps the brand member's fill inside the pill's corners), and the focus ring is inset\n * for the same reason.\n */\nexport function FloatingAction({\n label,\n icon,\n pressed,\n pressedStyle = \"tint\",\n variant = \"default\",\n badge,\n badgeLabel,\n badgeTone = \"warning\",\n tooltip = true,\n tooltipSide = \"top\",\n href,\n renderLink,\n className,\n type = \"button\",\n onClick,\n ref,\n ...rest\n}: FloatingActionProps) {\n const labels = useKitLabels(\"floatingPanel\", DEFAULT_FLOATING_PANEL_LABELS);\n const kitLink = useKitLink();\n const counted = badge !== undefined && badge > 0;\n const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;\n const isLink = href !== undefined;\n const look = cn(\n \"relative flex size-12 items-center justify-center transition-colors\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\",\n \"disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-5\",\n variant === \"primary\"\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\"\n : pressed && !isLink && pressedStyle === \"tint\"\n ? \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\",\n className,\n );\n const body = (\n <>\n {icon}\n {counted && (\n <span\n aria-hidden\n data-badge=\"\"\n className={cn(\n \"pointer-events-none absolute end-1 top-1 h-4 min-w-4 rounded-full px-1 text-center text-[10px] leading-4 font-semibold\",\n BADGE_TONE[badgeTone],\n )}\n >\n {badge > 99 ? \"99+\" : badge}\n </span>\n )}\n </>\n );\n\n let member: ReactElement;\n if (isLink) {\n // The button-only attributes stay behind; `aria-*`, `data-*` and the like ride along.\n const { disabled: _disabled, form: _form, value: _value, name: _name, ...anchorRest } = rest;\n const linkProps: FloatingActionLinkProps = {\n ...(anchorRest as AnchorHTMLAttributes<HTMLAnchorElement>),\n href,\n \"aria-label\": name,\n onClick,\n className: look,\n children: body,\n };\n const { children: content, ...anchor } = linkProps;\n const render = pickLinkRenderer(renderLink, kitLink, href);\n member = render ? (\n <RenderedFloatingLink render={render} {...linkProps} />\n ) : (\n <a {...anchor}>{content}</a>\n );\n } else {\n member = (\n <button\n {...rest}\n ref={ref}\n type={type}\n aria-label={name}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={onClick}\n className={look}\n >\n {body}\n </button>\n );\n }\n if (!tooltip) return member;\n return (\n // A fragment: the bubble says the member's name, and a description that repeats\n // the name is read twice.\n <Tooltip label={label} side={tooltipSide} portal>\n <>{member}</>\n </Tooltip>\n );\n}\n\n/** Calls `renderLink` as a component, so its hooks belong to the link, not to us. */\nfunction RenderedFloatingLink({\n render,\n ...props\n}: FloatingActionLinkProps & { render: (props: FloatingActionLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import 'react';
2
2
  import 'lucide-react';
3
- export { Q as DEFAULT_FORM_ACTIONS_LABELS, an as FormActions, ao as FormActionsAlign, ap as FormActionsDestructive, aq as FormActionsLabels, ar as FormActionsPlacement, as as FormActionsProps, at as FormActionsStickyWithin, au as FormActionsSubmitProps, aO as ResponsiveFormActionsPlacement } from '../kit-labels-CPbsjlXr.js';
3
+ export { Q as DEFAULT_FORM_ACTIONS_LABELS, an as FormActions, ao as FormActionsAlign, ap as FormActionsDestructive, aq as FormActionsLabels, ar as FormActionsPlacement, as as FormActionsProps, at as FormActionsStickyWithin, au as FormActionsSubmitProps, aO as ResponsiveFormActionsPlacement } from '../kit-labels-D1J2_jyD.js';
4
4
  import './data-table-labels.js';
5
5
  import './mini-calendar.js';
6
6
  import './calendar-heatmap.js';
@@ -51,6 +51,7 @@ import '../hooks/use-authed-src.js';
51
51
  import './image-grid.js';
52
52
  import './lightbox.js';
53
53
  import './write-lock.js';
54
+ import './legal.js';
54
55
  import './account-chips.js';
55
56
  import '../lib/format.js';
56
57
  import '../data-table-3mi2R3so.js';
@@ -59,3 +60,5 @@ import './swipeable-row.js';
59
60
  import './reauth-dialog.js';
60
61
  import './server-wake.js';
61
62
  import '../lib/server-wake.js';
63
+ import './translation-review-labels.js';
64
+ import '../lib/translation-review.js';