@eifi1/ui-kit 0.17.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.
- package/README.md +83 -4
- package/dist/chart.d.ts +13 -1
- package/dist/components/account-chips.d.ts +153 -0
- package/dist/components/account-chips.js +135 -0
- package/dist/components/account-chips.js.map +1 -0
- package/dist/components/account-settings-labels.d.ts +10 -2
- package/dist/components/account-settings-labels.js +3 -0
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -1
- package/dist/components/button-group.d.ts +13 -1
- package/dist/components/calculator.d.ts +13 -1
- package/dist/components/chip.d.ts +25 -1
- package/dist/components/chip.js +143 -6
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +42 -1
- package/dist/components/confirm-dialog.js +36 -5
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -1
- package/dist/components/danger-confirm.d.ts +55 -4
- package/dist/components/danger-confirm.js +67 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +13 -1
- package/dist/components/file-button.d.ts +13 -1
- package/dist/components/file-dropzone.d.ts +13 -1
- package/dist/components/floating-panel.d.ts +4 -1
- package/dist/components/floating-panel.js +15 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +13 -1
- package/dist/components/form-actions.js +3 -0
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/legal.d.ts +84 -0
- package/dist/components/legal.js +55 -0
- package/dist/components/legal.js.map +1 -0
- package/dist/components/money-field.d.ts +13 -1
- package/dist/components/number-field.d.ts +14 -2
- package/dist/components/number-field.js +23 -22
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +13 -1
- package/dist/components/numpad-sheet.d.ts +13 -1
- package/dist/components/passkeys-setting.d.ts +27 -1
- package/dist/components/passkeys-setting.js +31 -5
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +15 -1
- package/dist/components/progress-bar.js +8 -1
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +90 -0
- package/dist/components/reauth-dialog.js +158 -0
- package/dist/components/reauth-dialog.js.map +1 -0
- package/dist/components/series-chart.d.ts +13 -1
- package/dist/components/server-wake.d.ts +60 -0
- package/dist/components/server-wake.js +79 -0
- package/dist/components/server-wake.js.map +1 -0
- package/dist/components/settings-fields.d.ts +13 -1
- package/dist/components/share-card.d.ts +64 -0
- package/dist/components/share-card.js +350 -0
- package/dist/components/share-card.js.map +1 -0
- package/dist/components/status-dot.d.ts +2 -0
- package/dist/components/text-link.d.ts +13 -1
- package/dist/components/time-input.d.ts +13 -1
- package/dist/components/toast.d.ts +10 -1
- package/dist/components/toast.js +42 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +5 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +56 -0
- package/dist/components/translation-review-editor.js +159 -0
- package/dist/components/translation-review-editor.js.map +1 -0
- package/dist/components/translation-review-labels.d.ts +92 -0
- package/dist/components/translation-review-labels.js +67 -0
- package/dist/components/translation-review-labels.js.map +1 -0
- package/dist/components/translation-review-parts.d.ts +97 -0
- package/dist/components/translation-review-parts.js +163 -0
- package/dist/components/translation-review-parts.js.map +1 -0
- package/dist/components/translation-review.d.ts +94 -0
- package/dist/components/translation-review.js +344 -0
- package/dist/components/translation-review.js.map +1 -0
- package/dist/components/ui.d.ts +13 -1
- package/dist/components/ui.js +43 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +2 -0
- package/dist/components/write-lock.d.ts +68 -0
- package/dist/components/write-lock.js +33 -0
- package/dist/components/write-lock.js.map +1 -0
- package/dist/{data-table-CpkWUJBY.d.ts → data-table-3mi2R3so.d.ts} +19 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +13 -1
- package/dist/hooks/use-overlay-history.js +16 -0
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +13 -1
- package/dist/i18n/defaults.js +15 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +81 -0
- package/dist/i18n/{locales/de.js → german.js} +110 -9
- package/dist/i18n/german.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +13 -1
- package/dist/i18n/kit-labels.js +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +243 -0
- package/dist/i18n/languages.js +70 -0
- package/dist/i18n/languages.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +26 -8
- package/dist/i18n/locales/de-CH.js +7 -3
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/en.d.ts +13 -1
- package/dist/i18n/locales/en.js +8 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +13 -1
- package/dist/i18n/locales/es.js +127 -27
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +13 -1
- package/dist/i18n/locales/fr.js +101 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +13 -1
- package/dist/i18n/locales/hu.js +105 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +13 -1
- package/dist/i18n/locales/it.js +134 -34
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +13 -1
- package/dist/i18n/locales/zh.js +101 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +102 -0
- package/dist/i18n/review.js +158 -0
- package/dist/i18n/review.js.map +1 -0
- package/dist/i18n/swiss.d.ts +3 -3
- package/dist/i18n/swiss.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +97 -1
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-C4uB_zmg.d.ts → kit-labels-D1J2_jyD.d.ts} +236 -9
- package/dist/lib/floating-stack.d.ts +31 -0
- package/dist/lib/floating-stack.js +42 -0
- package/dist/lib/floating-stack.js.map +1 -0
- package/dist/lib/server-wake.d.ts +146 -0
- package/dist/lib/server-wake.js +122 -0
- package/dist/lib/server-wake.js.map +1 -0
- package/dist/lib/translation-review.d.ts +232 -0
- package/dist/lib/translation-review.js +194 -0
- package/dist/lib/translation-review.js.map +1 -0
- package/dist/rhf/fields.d.ts +13 -1
- package/dist/rhf/form.d.ts +13 -1
- package/dist/rhf.d.ts +13 -1
- package/dist/search/global-search.d.ts +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/top-bar-brand.d.ts +13 -1
- package/dist/shell/topbar-action-menu.d.ts +2 -0
- package/dist/shell.d.ts +13 -1
- package/dist/wizard/stepper-nav.d.ts +13 -1
- package/dist/wizard.d.ts +13 -1
- package/package.json +1 -1
- package/src/components/account-chips.tsx +302 -0
- package/src/components/account-settings-labels.ts +16 -2
- package/src/components/chip.tsx +239 -6
- package/src/components/confirm-dialog.tsx +82 -7
- package/src/components/danger-confirm.tsx +118 -33
- package/src/components/data-table.tsx +20 -0
- package/src/components/floating-panel.tsx +19 -1
- package/src/components/form-actions.tsx +22 -3
- package/src/components/legal.tsx +159 -0
- package/src/components/number-field.tsx +32 -26
- package/src/components/passkeys-setting.tsx +68 -7
- package/src/components/progress-bar.tsx +21 -1
- package/src/components/reauth-dialog.tsx +273 -0
- package/src/components/server-wake.tsx +156 -0
- package/src/components/share-card.tsx +615 -0
- package/src/components/toast.tsx +72 -2
- package/src/components/toggle-group.tsx +5 -2
- package/src/components/translation-review-editor.tsx +261 -0
- package/src/components/translation-review-labels.ts +152 -0
- package/src/components/translation-review-parts.tsx +269 -0
- package/src/components/translation-review.tsx +502 -0
- package/src/components/ui.tsx +89 -32
- package/src/components/write-lock.tsx +96 -0
- package/src/hooks/use-overlay-history.ts +51 -0
- package/src/i18n/defaults.ts +14 -0
- package/src/i18n/{locales/de.ts → german.ts} +120 -13
- package/src/i18n/kit-labels.tsx +29 -2
- package/src/i18n/languages.ts +285 -0
- package/src/i18n/locales/de-CH.ts +17 -8
- package/src/i18n/locales/en.ts +8 -0
- package/src/i18n/locales/es.ts +128 -26
- package/src/i18n/locales/fr.ts +103 -0
- package/src/i18n/locales/hu.ts +107 -0
- package/src/i18n/locales/it.ts +135 -33
- package/src/i18n/locales/zh.ts +102 -0
- package/src/i18n/review.ts +193 -0
- package/src/i18n/swiss.ts +3 -3
- package/src/index.ts +86 -1
- package/src/lib/floating-stack.ts +66 -0
- package/src/lib/server-wake.ts +291 -0
- package/src/lib/translation-review.ts +465 -0
- package/src/search/global-search.tsx +3 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
- package/dist/i18n/locales/de-CH-informal.js +0 -8
- package/dist/i18n/locales/de-CH-informal.js.map +0 -1
- package/dist/i18n/locales/de-informal.d.ts +0 -82
- package/dist/i18n/locales/de-informal.js +0 -37
- package/dist/i18n/locales/de-informal.js.map +0 -1
- package/dist/i18n/locales/de.d.ts +0 -66
- package/dist/i18n/locales/de.js.map +0 -1
- package/src/i18n/locales/de-CH-informal.ts +0 -10
- package/src/i18n/locales/de-informal.ts +0 -61
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { AnchorHTMLAttributes, Ref, ReactElement, ButtonHTMLAttributes, ReactNode, SelectHTMLAttributes,
|
|
2
|
+
import { AnchorHTMLAttributes, Ref, ReactElement, ButtonHTMLAttributes, ReactNode, SelectHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, TextareaHTMLAttributes } from 'react';
|
|
3
3
|
import { DataTableLabels } from './components/data-table-labels.js';
|
|
4
4
|
import { MiniCalendarLabels, WeekDay } from './components/mini-calendar.js';
|
|
5
5
|
import { CalendarHeatmapLabels } from './components/calendar-heatmap.js';
|
|
@@ -22,7 +22,7 @@ import { PasswordStrengthLabels } from './components/password-strength.js';
|
|
|
22
22
|
import { DangerConfirmLabels } from './components/danger-confirm.js';
|
|
23
23
|
import { SwatchPickerLabels } from './components/swatch-picker.js';
|
|
24
24
|
import { IconPickerLabels } from './components/icon-picker.js';
|
|
25
|
-
import { DialogFrameLabels } from './components/dialog-frame.js';
|
|
25
|
+
import { DialogFrameProps, DialogFrameLabels } from './components/dialog-frame.js';
|
|
26
26
|
import { MeasuredGridLabels } from './components/measured-grid.js';
|
|
27
27
|
import { f as FeedbackAttachmentFieldLabels, q as FeedbackDialogTextLabels } from './feedback-attachment-WFiXJ8We.js';
|
|
28
28
|
import { FeedbackThreadLabels, FeedbackComposerLabels } from './feedback/feedback-thread.js';
|
|
@@ -42,6 +42,12 @@ import { ErrorBoundaryLabels } from './components/error-boundary.js';
|
|
|
42
42
|
import { AuthedImageLabels } from './components/authed-image.js';
|
|
43
43
|
import { ImageGridLabels } from './components/image-grid.js';
|
|
44
44
|
import { LightboxLabels } from './components/lightbox.js';
|
|
45
|
+
import { WriteLockLabels } from './components/write-lock.js';
|
|
46
|
+
import { LegalLabels } from './components/legal.js';
|
|
47
|
+
import { RoleDefinition, AccountStateLabels } from './components/account-chips.js';
|
|
48
|
+
import { ReauthDialogLabels } from './components/reauth-dialog.js';
|
|
49
|
+
import { ServerWakeLabels } from './components/server-wake.js';
|
|
50
|
+
import { TranslationReviewLabels } from './components/translation-review-labels.js';
|
|
45
51
|
|
|
46
52
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
|
|
47
53
|
type ButtonSize = "sm" | "md";
|
|
@@ -145,6 +151,13 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
145
151
|
* DangerConfirm keeps its own.
|
|
146
152
|
*/
|
|
147
153
|
disabledReason?: ReactNode;
|
|
154
|
+
/**
|
|
155
|
+
* This button COMMITS — it saves, creates, deletes. Under a locked
|
|
156
|
+
* {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's
|
|
157
|
+
* reason (which wins over a `disabledReason` of its own). No provider, or an
|
|
158
|
+
* unlocked one: no effect. See {@link WriteLockProvider}.
|
|
159
|
+
*/
|
|
160
|
+
commit?: boolean;
|
|
148
161
|
/**
|
|
149
162
|
* Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.
|
|
150
163
|
*
|
|
@@ -219,6 +232,7 @@ interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
|
219
232
|
pending?: never;
|
|
220
233
|
pressed?: never;
|
|
221
234
|
disabledReason?: never;
|
|
235
|
+
commit?: never;
|
|
222
236
|
}
|
|
223
237
|
declare function Button(props: ButtonLinkProps): ReactElement;
|
|
224
238
|
declare function Button(props: ButtonProps): ReactElement;
|
|
@@ -427,6 +441,10 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
427
441
|
* reason nobody can see is not one. Wins over `disabled`, as on Button.
|
|
428
442
|
*/
|
|
429
443
|
disabledReason?: ReactNode;
|
|
444
|
+
/** This action COMMITS — {@link Button}'s `commit`: under a locked
|
|
445
|
+
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
446
|
+
* reason. No effect without a lock. */
|
|
447
|
+
commit?: boolean;
|
|
430
448
|
/** The `<button>` element — a prop in React 19, as on {@link Button}. */
|
|
431
449
|
ref?: Ref<HTMLButtonElement>;
|
|
432
450
|
/** Only on the link form — see {@link IconButtonLinkProps}. */
|
|
@@ -470,6 +488,7 @@ interface IconButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElemen
|
|
|
470
488
|
form?: never;
|
|
471
489
|
pressed?: never;
|
|
472
490
|
disabledReason?: never;
|
|
491
|
+
commit?: never;
|
|
473
492
|
disabledStyle?: never;
|
|
474
493
|
pending?: never;
|
|
475
494
|
}
|
|
@@ -1446,7 +1465,8 @@ type FormActionsPlacement = "inline" | "sticky" | "dialog";
|
|
|
1446
1465
|
* md 768, lg 1024, xl 1280px), as `BulkActionBar`'s `variant` breakpoints are, and
|
|
1447
1466
|
* resolved the same way, in JS: the sticky row is positioned by inline style, which a
|
|
1448
1467
|
* `md:` class cannot reach. keksdose switched `placement` on its own media query at
|
|
1449
|
-
* every long form for want of this.
|
|
1468
|
+
* every long form for want of this. The breakpoints are the VIEWPORT's, not the
|
|
1469
|
+
* container's: a form in a narrow pane on a wide screen resolves to `md`.
|
|
1450
1470
|
*/
|
|
1451
1471
|
interface ResponsiveFormActionsPlacement {
|
|
1452
1472
|
base: FormActionsPlacement;
|
|
@@ -1468,7 +1488,7 @@ type FormActionsStickyWithin = "viewport" | "container";
|
|
|
1468
1488
|
* analytics, an `aria-describedby`. The props FormActions drives itself (type, click,
|
|
1469
1489
|
* disabled, variant, form, children, the busy state) are not among them.
|
|
1470
1490
|
*/
|
|
1471
|
-
type FormActionsSubmitProps = Omit<ButtonProps, "type" | "onClick" | "disabled" | "disabledReason" | "variant" | "form" | "children" | "aria-busy"> & {
|
|
1491
|
+
type FormActionsSubmitProps = Omit<ButtonProps, "type" | "onClick" | "disabled" | "disabledReason" | "variant" | "form" | "children" | "aria-busy" | "commit"> & {
|
|
1472
1492
|
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
1473
1493
|
};
|
|
1474
1494
|
/** The start-side action of {@link FormActionsProps.destructive}, as data. */
|
|
@@ -1515,6 +1535,18 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
1515
1535
|
submitDisabled?: boolean;
|
|
1516
1536
|
/** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */
|
|
1517
1537
|
submitDisabledReason?: ReactNode;
|
|
1538
|
+
/**
|
|
1539
|
+
* Save is a commit: under a locked {@link WriteLockProvider} it is disabled with the
|
|
1540
|
+
* lock's reason (over `submitDisabledReason`), focusable, the reason in its tooltip —
|
|
1541
|
+
* Button's `commit`. The `{ label, onClick }` form of {@link destructive} is a
|
|
1542
|
+
* commit too and is locked with it; Cancel and `children` are not (closing a form
|
|
1543
|
+
* writes nothing). No provider, or an unlocked one: no effect. Default `false`.
|
|
1544
|
+
*
|
|
1545
|
+
* keksdose wrote `submitDisabled={lock.locked || …}` and
|
|
1546
|
+
* `submitDisabledReason={lock.locked ? lock.reason : undefined}` at each form; this is
|
|
1547
|
+
* that pair, read from the provider.
|
|
1548
|
+
*/
|
|
1549
|
+
commit?: boolean;
|
|
1518
1550
|
/** The save button's variant. Default `brand`; `danger` for a save that destroys
|
|
1519
1551
|
* (kastlan's `destructive` flag on its own FormActions). */
|
|
1520
1552
|
submitVariant?: ButtonVariant;
|
|
@@ -1545,7 +1577,8 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
1545
1577
|
* from both sides, with the content scrolling past it in the gutters (keksdose wrote
|
|
1546
1578
|
* `@max-md:-mx-3 @max-md:px-3` by hand). Pass the container's padding (`"0.75rem"`,
|
|
1547
1579
|
* `12`, `"var(--pane-px)"`). Applied only while the resolved placement is sticky, so
|
|
1548
|
-
* `placement={{ base: "sticky", md: "inline" }}` bleeds on the phone alone.
|
|
1580
|
+
* `placement={{ base: "sticky", md: "inline" }}` bleeds on the phone alone. The margin
|
|
1581
|
+
* is `calc(-1 * X)`; jsdom folds that to `calc(-X)`, so match it loosely in tests.
|
|
1549
1582
|
*/
|
|
1550
1583
|
bleed?: string | number;
|
|
1551
1584
|
/**
|
|
@@ -1591,7 +1624,187 @@ interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
1591
1624
|
* The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`
|
|
1592
1625
|
* so the button's name stays its text; `aria-busy` is what says it is working.
|
|
1593
1626
|
*/
|
|
1594
|
-
declare function FormActions({ onCancel, onSubmit, submitLabel, cancelLabel, pendingLabel, submitIcon: SubmitIcon, submitProps, pending, submitDisabled, submitDisabledReason, submitVariant, destructive, start: startSlot, align, placement: placementProp, stickyWithin, bleed, form, children, className, style, ...rest }: FormActionsProps): react.JSX.Element;
|
|
1627
|
+
declare function FormActions({ onCancel, onSubmit, submitLabel, cancelLabel, pendingLabel, submitIcon: SubmitIcon, submitProps, pending, submitDisabled, submitDisabledReason, commit, submitVariant, destructive, start: startSlot, align, placement: placementProp, stickyWithin, bleed, form, children, className, style, ...rest }: FormActionsProps): react.JSX.Element;
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Who holds one record, and the owner's every way of changing that — Kurvenschmiede's
|
|
1631
|
+
* `features/sharing/share-dialog.tsx` and keksdose's `features/budgets/budget-share-card.tsx`,
|
|
1632
|
+
* which are the same component twice (Kurvenschmiede's own doc comment says so).
|
|
1633
|
+
*
|
|
1634
|
+
* Presentational: the kit draws the rows, asks before a removal (the kit's
|
|
1635
|
+
* {@link useConfirm}), keeps the field and the busy state, and checks only that an
|
|
1636
|
+
* address is SHAPED like one. Everything that is policy stays with the app — who may
|
|
1637
|
+
* share, whether an address has an account (that is what makes a grant pending), what
|
|
1638
|
+
* a role permits, every request — through the callbacks, which may return a promise.
|
|
1639
|
+
*/
|
|
1640
|
+
interface ShareCardLabels {
|
|
1641
|
+
/** ShareDialog's heading. */
|
|
1642
|
+
dialogTitle: string;
|
|
1643
|
+
/** ShareDialog's close button. */
|
|
1644
|
+
close: string;
|
|
1645
|
+
/** The add field's label. */
|
|
1646
|
+
email: string;
|
|
1647
|
+
/** The add field's label when `addWithoutEmail` lets it stay empty. */
|
|
1648
|
+
emailOptional: string;
|
|
1649
|
+
emailPlaceholder: string;
|
|
1650
|
+
/** Under the field when what was typed is not an address. */
|
|
1651
|
+
invalidEmail: string;
|
|
1652
|
+
/** Accessible name of the role choice beside the field. */
|
|
1653
|
+
role: string;
|
|
1654
|
+
/** Accessible name of one grantee's role choice. */
|
|
1655
|
+
roleOf: (name: string) => string;
|
|
1656
|
+
/** The add button. */
|
|
1657
|
+
add: string;
|
|
1658
|
+
/** Heading over the list. */
|
|
1659
|
+
whoHasAccess: string;
|
|
1660
|
+
/** The list is empty. */
|
|
1661
|
+
nobodyYet: string;
|
|
1662
|
+
/** Chip on a grant whose address has no account yet. */
|
|
1663
|
+
pending: string;
|
|
1664
|
+
/** A pending grant with no address — a link anybody holding it can redeem. */
|
|
1665
|
+
openInvite: string;
|
|
1666
|
+
copyLink: string;
|
|
1667
|
+
/** A candidate that is a team (its sublabel, and the line under a team row). */
|
|
1668
|
+
team: string;
|
|
1669
|
+
/** Under the field while a team is the one named. */
|
|
1670
|
+
teamHint: (name: string) => string;
|
|
1671
|
+
/** The remove button on a grantee. */
|
|
1672
|
+
remove: string;
|
|
1673
|
+
removeConfirm: (name: string) => string;
|
|
1674
|
+
/** The remove button on a pending grant. */
|
|
1675
|
+
revokePending: string;
|
|
1676
|
+
revokePendingConfirm: (name: string) => string;
|
|
1677
|
+
/** A callback rejected and no `formatError` says better. */
|
|
1678
|
+
failed: string;
|
|
1679
|
+
/** Shown while `loading`. */
|
|
1680
|
+
loading: string;
|
|
1681
|
+
}
|
|
1682
|
+
declare const DEFAULT_SHARE_CARD_LABELS: ShareCardLabels;
|
|
1683
|
+
/**
|
|
1684
|
+
* One role of the app's vocabulary: Kurvenschmiede's viewer/editor, keksdose's guest.
|
|
1685
|
+
*
|
|
1686
|
+
* The admin roster's {@link RoleDefinition} (label, tone, icon) plus the key it is
|
|
1687
|
+
* filed under — an ARRAY here rather than the keyed `RoleVocabulary` record,
|
|
1688
|
+
* because the order is the order of the role choice's segments.
|
|
1689
|
+
*/
|
|
1690
|
+
type ShareRole = RoleDefinition & {
|
|
1691
|
+
key: string;
|
|
1692
|
+
/** What the role allows — under the role choice once chosen, and the chip's tooltip. */
|
|
1693
|
+
description?: string;
|
|
1694
|
+
};
|
|
1695
|
+
/** Somebody (or a team) who holds the record now. */
|
|
1696
|
+
interface ShareGrantee {
|
|
1697
|
+
id: string | number;
|
|
1698
|
+
name: string;
|
|
1699
|
+
/** The second line. A team row says {@link ShareCardLabels.team} instead. */
|
|
1700
|
+
email?: string | null;
|
|
1701
|
+
/** A {@link ShareRole.key}. */
|
|
1702
|
+
role: string;
|
|
1703
|
+
/** `team`: the grant reaches everybody in it (Kurvenschmiede). Default `person`. */
|
|
1704
|
+
kind?: "person" | "team";
|
|
1705
|
+
/** Shown, but neither its role nor its access can be changed here (an owner row). */
|
|
1706
|
+
locked?: boolean;
|
|
1707
|
+
/** The app's own line under the row — keksdose's per-guest key delivery. */
|
|
1708
|
+
extra?: ReactNode;
|
|
1709
|
+
}
|
|
1710
|
+
/** A grant waiting for its address to have an account — or, with no address, an
|
|
1711
|
+
* open invite link (keksdose). */
|
|
1712
|
+
interface SharePendingGrant {
|
|
1713
|
+
id: string | number;
|
|
1714
|
+
email?: string | null;
|
|
1715
|
+
/** A {@link ShareRole.key}; left out when the app has only one role. */
|
|
1716
|
+
role?: string;
|
|
1717
|
+
/** The invite link, offered with a copy button. */
|
|
1718
|
+
link?: string;
|
|
1719
|
+
}
|
|
1720
|
+
/** A completion offered while typing: a past grantee, or one of the sharer's teams. */
|
|
1721
|
+
interface ShareCandidate {
|
|
1722
|
+
id: string | number;
|
|
1723
|
+
name: string;
|
|
1724
|
+
/** A person's address: taking the row fills the field with it, and the grant is
|
|
1725
|
+
* then an ordinary address grant. */
|
|
1726
|
+
email?: string | null;
|
|
1727
|
+
/** `team` (or a person with no address): taking the row names THIS candidate, and
|
|
1728
|
+
* `onAdd` receives it instead of an address. */
|
|
1729
|
+
kind?: "person" | "team";
|
|
1730
|
+
icon?: ReactNode;
|
|
1731
|
+
}
|
|
1732
|
+
/** What the add button asks for. Exactly one of `email` / `candidate` is set, except
|
|
1733
|
+
* under `addWithoutEmail`, where both may be null (an open invite). */
|
|
1734
|
+
interface ShareAddRequest {
|
|
1735
|
+
email: string | null;
|
|
1736
|
+
candidate: ShareCandidate | null;
|
|
1737
|
+
/** A {@link ShareRole.key}. */
|
|
1738
|
+
role: string;
|
|
1739
|
+
}
|
|
1740
|
+
type MaybePromise = void | Promise<unknown>;
|
|
1741
|
+
interface SharePanelProps {
|
|
1742
|
+
grantees: ShareGrantee[];
|
|
1743
|
+
/** The role vocabulary, in display order. One role: no choice is offered and each
|
|
1744
|
+
* row shows its chip. */
|
|
1745
|
+
roles: ShareRole[];
|
|
1746
|
+
pending?: SharePendingGrant[];
|
|
1747
|
+
/** Completions for the field. Left out: the field is a plain address field. */
|
|
1748
|
+
candidates?: ShareCandidate[];
|
|
1749
|
+
/** The role a new grant starts with. Default: the first of `roles`. */
|
|
1750
|
+
defaultRole?: string;
|
|
1751
|
+
/** Resolve to clear the field; reject to keep it and show the error. Left out (or
|
|
1752
|
+
* `readOnly`): no add form. */
|
|
1753
|
+
onAdd?: (request: ShareAddRequest) => MaybePromise;
|
|
1754
|
+
/** Left out: roles are shown as chips. */
|
|
1755
|
+
onRoleChange?: (grantee: ShareGrantee, role: string) => MaybePromise;
|
|
1756
|
+
/** Called after the kit's confirm. Left out: no remove button. */
|
|
1757
|
+
onRemove?: (grantee: ShareGrantee) => MaybePromise;
|
|
1758
|
+
/** Called after the kit's confirm. Left out: no withdraw button. */
|
|
1759
|
+
onRevokePending?: (grant: SharePendingGrant) => MaybePromise;
|
|
1760
|
+
/** keksdose: an empty field is a request too — an invite link with no address. */
|
|
1761
|
+
addWithoutEmail?: boolean;
|
|
1762
|
+
/** Under the add field: whatever the app needs said there (Kurvenschmiede's "your
|
|
1763
|
+
* pending grant does not let anybody register"). A function receives the taken
|
|
1764
|
+
* candidate, so it can say something else while a team is named. */
|
|
1765
|
+
caption?: ReactNode | ((candidate: ShareCandidate | null) => ReactNode);
|
|
1766
|
+
/** Above everything — what sharing means here (keksdose's guest explanation). */
|
|
1767
|
+
intro?: ReactNode;
|
|
1768
|
+
/** The list is being fetched: a spinner instead of the rows. */
|
|
1769
|
+
loading?: boolean;
|
|
1770
|
+
/** The app's own error, shown in the error box (wins over a caught rejection). */
|
|
1771
|
+
error?: ReactNode;
|
|
1772
|
+
/** Turns a callback's rejection into words. Default {@link ShareCardLabels.failed}. */
|
|
1773
|
+
formatError?: (error: unknown) => ReactNode;
|
|
1774
|
+
/** Look, do not touch: no form, chips for roles, no remove buttons. Copying an
|
|
1775
|
+
* invite link still works. */
|
|
1776
|
+
readOnly?: boolean;
|
|
1777
|
+
/** Focus the field on mount — a dialog that exists to add somebody. */
|
|
1778
|
+
autoFocus?: boolean;
|
|
1779
|
+
labels?: Partial<ShareCardLabels>;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* The body both forms share: the add form, then the list. Exported for an app that
|
|
1783
|
+
* frames it itself (a settings section, a sheet).
|
|
1784
|
+
*/
|
|
1785
|
+
declare function SharePanel({ grantees, roles, pending, candidates, defaultRole, onAdd, onRoleChange, onRemove, onRevokePending, addWithoutEmail, caption, intro, loading, error, formatError, readOnly, autoFocus, labels: labelsProp, }: SharePanelProps): react.JSX.Element;
|
|
1786
|
+
interface ShareCardProps extends SharePanelProps {
|
|
1787
|
+
/** A heading over the card. */
|
|
1788
|
+
title?: ReactNode;
|
|
1789
|
+
/** The Card's variant. Default `outline` — a block inside a section, as keksdose's
|
|
1790
|
+
* card under a budget row is. */
|
|
1791
|
+
variant?: CardProps["variant"];
|
|
1792
|
+
className?: string;
|
|
1793
|
+
}
|
|
1794
|
+
/** {@link SharePanel} in a card — keksdose's inline form, under the row it shares. */
|
|
1795
|
+
declare function ShareCard({ title, variant, className, ...panel }: ShareCardProps): react.JSX.Element;
|
|
1796
|
+
interface ShareDialogProps extends SharePanelProps, Pick<DialogFrameProps, "open" | "onClose" | "size" | "className"> {
|
|
1797
|
+
/** The heading. Default {@link ShareCardLabels.dialogTitle}. */
|
|
1798
|
+
title?: ReactNode;
|
|
1799
|
+
/** Under the heading — what is being shared (Kurvenschmiede passes its name). */
|
|
1800
|
+
description?: ReactNode;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* {@link SharePanel} in the kit's {@link DialogFrame} with a Close button —
|
|
1804
|
+
* Kurvenschmiede's form. Sharing commits as it goes, so there is nothing to save or
|
|
1805
|
+
* cancel: Close is the only action.
|
|
1806
|
+
*/
|
|
1807
|
+
declare function ShareDialog({ open, onClose, size, className, title, description, ...panel }: ShareDialogProps): react.JSX.Element;
|
|
1595
1808
|
|
|
1596
1809
|
/**
|
|
1597
1810
|
* EVERY string the kit renders, as one typed tree — and an optional provider that
|
|
@@ -1806,6 +2019,19 @@ interface UiKitLabels {
|
|
|
1806
2019
|
authedImage: AuthedImageLabels;
|
|
1807
2020
|
imageGrid: ImageGridLabels;
|
|
1808
2021
|
lightbox: LightboxLabels;
|
|
2022
|
+
/** 0.18.0: `WriteLockProvider`'s fallback reason, `AccountStateChip`, `SharePanel` /
|
|
2023
|
+
* `ShareCard` / `ShareDialog`, `ReauthDialog`. */
|
|
2024
|
+
writeLock: WriteLockLabels;
|
|
2025
|
+
accountState: AccountStateLabels;
|
|
2026
|
+
shareCard: ShareCardLabels;
|
|
2027
|
+
reauthDialog: ReauthDialogLabels;
|
|
2028
|
+
/** 0.18.0: `ServerWakeNotice` — keksdose's cold-start notice, for every app. */
|
|
2029
|
+
serverWake: ServerWakeLabels;
|
|
2030
|
+
/** 0.19.0: the translation-review parts (`TranslationReviewPanel`, its editor, chip,
|
|
2031
|
+
* progress, locale tabs and export) — keksdose's /translations, for every app. */
|
|
2032
|
+
translationReview: TranslationReviewLabels;
|
|
2033
|
+
/** 0.19.0: `LegalLinks`' navigation name — the legal pages' shell, for every app. */
|
|
2034
|
+
legal: LegalLabels;
|
|
1809
2035
|
}
|
|
1810
2036
|
/**
|
|
1811
2037
|
* A label override, as deep as the labels go: an object of labels (a namespace, or a
|
|
@@ -1939,13 +2165,14 @@ declare function useKitFileLabels(prop?: Partial<FileLabels>): FileLabels;
|
|
|
1939
2165
|
* The dot paths of every key `labels` does NOT supply — the ones a translated app is
|
|
1940
2166
|
* still showing in English. Assert on it in the app's own test:
|
|
1941
2167
|
*
|
|
1942
|
-
* expect(missingKitLabels(
|
|
2168
|
+
* expect(missingKitLabels(labels, DEFAULT_UI_KIT_LABELS)).toEqual([]);
|
|
1943
2169
|
*
|
|
1944
2170
|
* `reference` is the complete tree to check against — pass
|
|
1945
2171
|
* {@link DEFAULT_UI_KIT_LABELS} (src/i18n/defaults.ts). It is a parameter rather than an import
|
|
1946
2172
|
* because the defaults live beside their components, and this module is imported BY
|
|
1947
|
-
* those components.
|
|
2173
|
+
* those components. Left out (a test file is not type-checked), it throws saying so
|
|
2174
|
+
* rather than "Cannot convert undefined or null to object" (keksdose, 0.18).
|
|
1948
2175
|
*/
|
|
1949
2176
|
declare function missingKitLabels(labels: UiKitLabelOverrides | undefined, reference: UiKitLabels): string[];
|
|
1950
2177
|
|
|
1951
|
-
export {
|
|
2178
|
+
export { FIELD_BASE as $, type AppShellLabels as A, Button as B, type CalculatorLabels as C, CopyButton as D, type CopyButtonLabels as E, type CopyButtonProps as F, type CurrencyLabels as G, DEFAULT_APP_SHELL_LABELS as H, DEFAULT_CALCULATOR_LABELS as I, DEFAULT_COMBOBOX_LABELS as J, DEFAULT_COMMON_LABELS as K, DEFAULT_COPY_BUTTON_LABELS as L, DEFAULT_CURRENCY_LABELS as M, DEFAULT_DATE_PICKER_LABELS as N, DEFAULT_FILE_LABELS as O, DEFAULT_FILE_PICKER_LABELS as P, DEFAULT_FORM_ACTIONS_LABELS as Q, DEFAULT_MULTI_SELECT_LABELS as R, DEFAULT_PASSWORD_REVEAL_LABELS as S, DEFAULT_PICKER_SHEET_LABELS as T, DEFAULT_SHARE_CARD_LABELS as U, DEFAULT_SWIPEABLE_ROW_LABELS as V, DEFAULT_TABS_LABELS as W, DEFAULT_TOP_BAR_LABELS as X, type DatePickerLabels as Y, EmptyState as Z, type EmptyStateProps as _, type ButtonClassesOptions as a, type SharePendingGrant as a$, FIELD_DISPLAY as a0, FIELD_FLOATING_PAD as a1, FIELD_INVALID as a2, FIELD_TRIGGER as a3, FIELD_WRITABLE_LOOK as a4, FLOATING_INPUT_CLASS as a5, FLOATING_LABEL_CLASS as a6, FLOATING_LABEL_STATIC as a7, FieldChevron as a8, type FieldChevronProps as a9, type IconButtonTone as aA, type IconButtonToneColor as aB, type IconButtonVariant as aC, Input as aD, type InputProps as aE, type KitLinkComponent as aF, type KitLinkProps as aG, Label as aH, type LabelOverride as aI, type LabelProps as aJ, type MultiSelectLabels as aK, PHONE_QUERY as aL, type PasswordRevealLabels as aM, type PickerSheetLabels as aN, type ResponsiveFormActionsPlacement as aO, Select as aP, type SelectProps as aQ, type ShareAddRequest as aR, type ShareCandidate as aS, ShareCard as aT, type ShareCardLabels as aU, type ShareCardProps as aV, ShareDialog as aW, type ShareDialogProps as aX, type ShareGrantee as aY, SharePanel as aZ, type SharePanelProps as a_, FieldHint as aa, type FieldHintProps as ab, FieldLabel as ac, type FieldLabelProps as ad, FileButton as ae, type FileButtonProps as af, type FileLabels as ag, type FilePickerLabels as ah, type FileRejection as ai, type FileRejectionReason as aj, type FileScreenOptions as ak, FloatingField as al, type FloatingFieldProps as am, FormActions as an, type FormActionsAlign as ao, type FormActionsDestructive as ap, type FormActionsLabels as aq, type FormActionsPlacement as ar, type FormActionsProps as as, type FormActionsStickyWithin as at, type FormActionsSubmitProps as au, IconButton as av, type IconButtonGlyphSize as aw, type IconButtonLinkProps as ax, type IconButtonProps as ay, type IconButtonSize as az, type ButtonLinkProps as b, type ShareRole as b0, Spinner as b1, type SpinnerProps as b2, type SwipeableRowLabels as b3, type TabItem as b4, Tabs as b5, type TabsLabels as b6, type TabsProps as b7, Textarea as b8, type TextareaProps as b9, type TopBarLabels as ba, type UiKitLabelOverrides as bb, type UiKitLabels as bc, UiKitProvider as bd, type UiKitProviderProps as be, type UseFilePickerOptions as bf, type UseFilePickerReturn as bg, buttonClasses as bh, formatFileSize as bi, matchesAccept as bj, missingKitLabels as bk, resolvePasswordRevealLabels as bl, useFilePicker as bm, useKitChartTooltipPlacement as bn, useKitFileLabels as bo, useKitLabelOverrides as bp, useKitLabels as bq, useKitLink as br, useKitLocale as bs, useKitWeekStart as bt, type FilePickHandler as bu, formatAccept as bv, judgePick as bw, screenFiles as bx, summariseRejections as by, type ButtonProps as c, type ButtonSize as d, type ButtonTone as e, type ButtonVariant as f, Card as g, CardAction as h, type CardActionProps as i, CardContent as j, type CardContentProps as k, type CardDensity as l, CardDescription as m, type CardDescriptionProps as n, CardFooter as o, type CardFooterProps as p, CardHeader as q, type CardHeaderProps as r, type CardProps as s, CardTitle as t, type CardTitleLevel as u, type CardTitleProps as v, type CardTone as w, type ChartTooltipPlacement as x, type ComboboxLabels as y, type CommonLabels as z };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What floats over the bottom of the screen right now, so a notice that comes and goes
|
|
3
|
+
* can sit above it instead of on it.
|
|
4
|
+
*
|
|
5
|
+
* kastlan 0.18: its offline pill is a `bottom-start` FloatingActionButton, and
|
|
6
|
+
* `ServerWakeNotice` sits in the same corner — right after a break both show at once
|
|
7
|
+
* (the photo outbox syncs while the first reads wake the server), one over the other.
|
|
8
|
+
* The FAB's `offset` cannot help: the notice comes and goes, and on a phone it is
|
|
9
|
+
* nearly as wide as the screen, so a FAB in EITHER corner can be under it.
|
|
10
|
+
*
|
|
11
|
+
* Measured, not declared: a FloatingActionButton registers its fixed element while it
|
|
12
|
+
* is mounted, and a notice asks {@link clearanceAbove} for the height it has to clear —
|
|
13
|
+
* whatever offset, `extended` label or `hidden` state the controls are in. A control
|
|
14
|
+
* that is `hidden` measures 0 × 0 and is skipped, so kastlan's pill, which stays
|
|
15
|
+
* mounted and hidden while there is nothing to report, costs the notice nothing.
|
|
16
|
+
*
|
|
17
|
+
* Internal: not exported from the barrel.
|
|
18
|
+
*/
|
|
19
|
+
/** Track `element` until the returned function is called. Size changes — a label that
|
|
20
|
+
* grows, `hidden` toggling — notify the subscribers too. */
|
|
21
|
+
declare function registerFloating(element: HTMLElement): () => void;
|
|
22
|
+
/** Hear every registration, removal and size change. */
|
|
23
|
+
declare function subscribeFloating(listener: () => void): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* The distance from the viewport's bottom edge to the top of the highest visible
|
|
26
|
+
* floating control that shares a column with `element` — what `element` has to clear —
|
|
27
|
+
* or `null` when nothing is under it.
|
|
28
|
+
*/
|
|
29
|
+
declare function clearanceAbove(element: HTMLElement | null): number | null;
|
|
30
|
+
|
|
31
|
+
export { clearanceAbove, registerFloating, subscribeFloating };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const occupants = /* @__PURE__ */ new Set();
|
|
3
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
let observer = null;
|
|
5
|
+
function notify() {
|
|
6
|
+
for (const listener of listeners) listener();
|
|
7
|
+
}
|
|
8
|
+
function registerFloating(element) {
|
|
9
|
+
occupants.add(element);
|
|
10
|
+
if (observer === null && typeof ResizeObserver !== "undefined") observer = new ResizeObserver(notify);
|
|
11
|
+
observer?.observe(element);
|
|
12
|
+
notify();
|
|
13
|
+
return () => {
|
|
14
|
+
occupants.delete(element);
|
|
15
|
+
observer?.unobserve(element);
|
|
16
|
+
notify();
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function subscribeFloating(listener) {
|
|
20
|
+
listeners.add(listener);
|
|
21
|
+
return () => {
|
|
22
|
+
listeners.delete(listener);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function clearanceAbove(element) {
|
|
26
|
+
if (!element) return null;
|
|
27
|
+
const own = element.getBoundingClientRect();
|
|
28
|
+
let top = Infinity;
|
|
29
|
+
for (const other of occupants) {
|
|
30
|
+
const box = other.getBoundingClientRect();
|
|
31
|
+
if (box.width === 0 || box.height === 0) continue;
|
|
32
|
+
if (box.right <= own.left || box.left >= own.right) continue;
|
|
33
|
+
top = Math.min(top, box.top);
|
|
34
|
+
}
|
|
35
|
+
return top === Infinity ? null : Math.max(0, window.innerHeight - top);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
clearanceAbove,
|
|
39
|
+
registerFloating,
|
|
40
|
+
subscribeFloating
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=floating-stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/floating-stack.ts"],"sourcesContent":["/**\n * What floats over the bottom of the screen right now, so a notice that comes and goes\n * can sit above it instead of on it.\n *\n * kastlan 0.18: its offline pill is a `bottom-start` FloatingActionButton, and\n * `ServerWakeNotice` sits in the same corner — right after a break both show at once\n * (the photo outbox syncs while the first reads wake the server), one over the other.\n * The FAB's `offset` cannot help: the notice comes and goes, and on a phone it is\n * nearly as wide as the screen, so a FAB in EITHER corner can be under it.\n *\n * Measured, not declared: a FloatingActionButton registers its fixed element while it\n * is mounted, and a notice asks {@link clearanceAbove} for the height it has to clear —\n * whatever offset, `extended` label or `hidden` state the controls are in. A control\n * that is `hidden` measures 0 × 0 and is skipped, so kastlan's pill, which stays\n * mounted and hidden while there is nothing to report, costs the notice nothing.\n *\n * Internal: not exported from the barrel.\n */\n\nconst occupants = new Set<HTMLElement>();\nconst listeners = new Set<() => void>();\nlet observer: ResizeObserver | null = null;\n\nfunction notify() {\n for (const listener of listeners) listener();\n}\n\n/** Track `element` until the returned function is called. Size changes — a label that\n * grows, `hidden` toggling — notify the subscribers too. */\nexport function registerFloating(element: HTMLElement): () => void {\n occupants.add(element);\n if (observer === null && typeof ResizeObserver !== \"undefined\") observer = new ResizeObserver(notify);\n observer?.observe(element);\n notify();\n return () => {\n occupants.delete(element);\n observer?.unobserve(element);\n notify();\n };\n}\n\n/** Hear every registration, removal and size change. */\nexport function subscribeFloating(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * The distance from the viewport's bottom edge to the top of the highest visible\n * floating control that shares a column with `element` — what `element` has to clear —\n * or `null` when nothing is under it.\n */\nexport function clearanceAbove(element: HTMLElement | null): number | null {\n if (!element) return null;\n const own = element.getBoundingClientRect();\n let top = Infinity;\n for (const other of occupants) {\n const box = other.getBoundingClientRect();\n if (box.width === 0 || box.height === 0) continue;\n if (box.right <= own.left || box.left >= own.right) continue;\n top = Math.min(top, box.top);\n }\n return top === Infinity ? null : Math.max(0, window.innerHeight - top);\n}\n"],"mappings":";AAmBA,MAAM,YAAY,oBAAI,IAAiB;AACvC,MAAM,YAAY,oBAAI,IAAgB;AACtC,IAAI,WAAkC;AAEtC,SAAS,SAAS;AAChB,aAAW,YAAY,UAAW,UAAS;AAC7C;AAIO,SAAS,iBAAiB,SAAkC;AACjE,YAAU,IAAI,OAAO;AACrB,MAAI,aAAa,QAAQ,OAAO,mBAAmB,YAAa,YAAW,IAAI,eAAe,MAAM;AACpG,YAAU,QAAQ,OAAO;AACzB,SAAO;AACP,SAAO,MAAM;AACX,cAAU,OAAO,OAAO;AACxB,cAAU,UAAU,OAAO;AAC3B,WAAO;AAAA,EACT;AACF;AAGO,SAAS,kBAAkB,UAAkC;AAClE,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAOO,SAAS,eAAe,SAA4C;AACzE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,MAAM,QAAQ,sBAAsB;AAC1C,MAAI,MAAM;AACV,aAAW,SAAS,WAAW;AAC7B,UAAM,MAAM,MAAM,sBAAsB;AACxC,QAAI,IAAI,UAAU,KAAK,IAAI,WAAW,EAAG;AACzC,QAAI,IAAI,SAAS,IAAI,QAAQ,IAAI,QAAQ,IAAI,MAAO;AACpD,UAAM,KAAK,IAAI,KAAK,IAAI,GAAG;AAAA,EAC7B;AACA,SAAO,QAAQ,WAAW,OAAO,KAAK,IAAI,GAAG,OAAO,cAAc,GAAG;AACvE;","names":[]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is the server asleep, or is the app broken?" — the cold-start watchdog, lifted
|
|
3
|
+
* from keksdose (feedback #199) so kastlan and Kurvenschmiede get it too.
|
|
4
|
+
*
|
|
5
|
+
* All three backends run on Cloud Run with `--min-instances=0`: after an idle
|
|
6
|
+
* period the container is gone and the first request has to start it, which takes
|
|
7
|
+
* several seconds and occasionally more. Nothing in the UI said so, and none of the
|
|
8
|
+
* axios instances has a timeout, so the honest experience was a spinner that looked
|
|
9
|
+
* stuck. This counts the in-flight requests a cold start would actually delay and
|
|
10
|
+
* escalates in two steps, so a merely slow response says "still loading" and only a
|
|
11
|
+
* genuinely long wait blames the sleeping server.
|
|
12
|
+
*
|
|
13
|
+
* Framework-free on purpose (no zustand, no axios, no React): a counter, two timers
|
|
14
|
+
* and a listener set, read by `useServerWakeStage` through `useSyncExternalStore`.
|
|
15
|
+
* The adapters below take the app's axios instance / fetch as an ARGUMENT, so the
|
|
16
|
+
* kit never imports either.
|
|
17
|
+
*
|
|
18
|
+
* WHAT ARMS IT, and why the default is narrow:
|
|
19
|
+
* - reads (GET) — what every page does on open — always;
|
|
20
|
+
* - writes only when the app says so (`watchReadsAnd(/\/auth\/login\b/)`): the
|
|
21
|
+
* auth POSTs that gate the app are the only writes a user waits on before the
|
|
22
|
+
* app exists at all, so a cold start there looks exactly like a broken login.
|
|
23
|
+
* An UPLOAD must never be watched — a statement PDF or a photo takes seconds on a
|
|
24
|
+
* perfectly warm server, and "the server is waking up" during a 4 s upload would
|
|
25
|
+
* simply be false. With the default that holds by itself (an upload is a POST);
|
|
26
|
+
* an app widening `shouldWatch` keeps its upload routes out of it.
|
|
27
|
+
*
|
|
28
|
+
* Nor a DOWNLOAD, for the same reason (kastlan 0.18): axios answers only once the
|
|
29
|
+
* whole body is in, so a generated PDF or a large photo — a GET — ran past both
|
|
30
|
+
* thresholds on a warm server, and held the count for every read beside it. The
|
|
31
|
+
* default filter skips a request whose `responseType` is `"blob"`, `"arraybuffer"` or
|
|
32
|
+
* `"stream"`; a custom filter gets the request as its third argument to do the same.
|
|
33
|
+
* (`wrapFetch` ends the wait at the headers, so a download through it is harmless.)
|
|
34
|
+
*
|
|
35
|
+
* It never arms while the browser is offline, whatever `shouldWatch` says: that state
|
|
36
|
+
* has its own, truthful indicator, and a request that never left the device is not
|
|
37
|
+
* evidence of anything about the server.
|
|
38
|
+
*/
|
|
39
|
+
/** `slow` = longer than a warm server ever takes; `waking` = long enough that a cold
|
|
40
|
+
* start is the only sensible explanation. */
|
|
41
|
+
type ServerWakeStage = "idle" | "slow" | "waking";
|
|
42
|
+
/** Decides which requests count. `method` arrives lower-cased (`"get"` when the
|
|
43
|
+
* request named none — axios's and fetch's default); `url` as the client had it;
|
|
44
|
+
* `request` is what the adapter saw — for axios the request config itself, so
|
|
45
|
+
* `responseType` or the app's own fields can decide. */
|
|
46
|
+
type ServerWakeFilter = (method: string, url: string, request: ServerWakeRequest) => boolean;
|
|
47
|
+
interface ServerWakeOptions {
|
|
48
|
+
/** Past this, a warm round trip is out of the question (a cold database query on a
|
|
49
|
+
* free tier can still take a second, so it is not tighter). Default 2000 ms. */
|
|
50
|
+
slowMs?: number;
|
|
51
|
+
/** Past this, the container was almost certainly scaled to zero. Default 7000 ms. */
|
|
52
|
+
wakingMs?: number;
|
|
53
|
+
/** Which requests count. Default {@link watchReadsAnd}`()` — GETs only, downloads
|
|
54
|
+
* excluded. The offline guard runs before it and cannot be overridden. */
|
|
55
|
+
shouldWatch?: ServerWakeFilter;
|
|
56
|
+
}
|
|
57
|
+
/** What a request is, for {@link ServerWakeWatcher.track} and a {@link ServerWakeFilter}. */
|
|
58
|
+
interface ServerWakeRequest {
|
|
59
|
+
method?: string;
|
|
60
|
+
url?: string;
|
|
61
|
+
/** axios's `responseType`. `"blob"`, `"arraybuffer"` and `"stream"` mark a download,
|
|
62
|
+
* which the default filter does not count. */
|
|
63
|
+
responseType?: string;
|
|
64
|
+
}
|
|
65
|
+
interface ServerWakeWatcher {
|
|
66
|
+
/**
|
|
67
|
+
* Count a request that is going out, and arm the escalation if it is the first.
|
|
68
|
+
* Returns whether it was counted — the caller MUST call {@link end} exactly once
|
|
69
|
+
* for every `true`, or the counter never returns to zero and the notice sticks.
|
|
70
|
+
* (The axios adapter stashes it on the request config for that reason.)
|
|
71
|
+
* `request` reaches the filter as its third argument; default `{ method, url }`.
|
|
72
|
+
*/
|
|
73
|
+
start(method?: string, url?: string, request?: ServerWakeRequest): boolean;
|
|
74
|
+
/** A counted request settled — either way: an error still proves the server
|
|
75
|
+
* answered or gave up. When the last one does, the notice goes away. Never takes
|
|
76
|
+
* the counter below zero. */
|
|
77
|
+
end(): void;
|
|
78
|
+
/** `start` + `end` around a promise (or a function returning one), for a request
|
|
79
|
+
* that does not go through an adapter. Resolves and rejects as the work does. */
|
|
80
|
+
track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request?: ServerWakeRequest): Promise<T>;
|
|
81
|
+
/** Back to a pristine watchdog — for tests, which share an instance across cases. */
|
|
82
|
+
reset(): void;
|
|
83
|
+
/** `useSyncExternalStore`'s pair. Listeners hear stage changes only. */
|
|
84
|
+
subscribe(listener: () => void): () => void;
|
|
85
|
+
getStage(): ServerWakeStage;
|
|
86
|
+
/** Counted requests currently outstanding. */
|
|
87
|
+
getInFlight(): number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The usual filter: every GET, plus any other request whose URL matches `writes` —
|
|
91
|
+
* keksdose's `/\/auth\/(login|register|demo-session)\b/` — but never a download
|
|
92
|
+
* (`responseType` `"blob"`, `"arraybuffer"` or `"stream"`). Keep upload routes out of
|
|
93
|
+
* the pattern (see the module comment).
|
|
94
|
+
*/
|
|
95
|
+
declare function watchReadsAnd(writes?: RegExp): ServerWakeFilter;
|
|
96
|
+
declare function createServerWake({ slowMs, wakingMs, shouldWatch, }?: ServerWakeOptions): ServerWakeWatcher;
|
|
97
|
+
/**
|
|
98
|
+
* The shared instance: what `<ServerWakeNotice />`, `useServerWakeStage()`,
|
|
99
|
+
* {@link attachServerWake} and {@link wrapFetch} use when no watcher is passed. GETs
|
|
100
|
+
* only, default timings — an app that also watches its login POST creates its own
|
|
101
|
+
* with {@link createServerWake} and passes it to both ends.
|
|
102
|
+
*/
|
|
103
|
+
declare const serverWake: ServerWakeWatcher;
|
|
104
|
+
/** The config fields the watchdog reads — what axios's request config has. The whole
|
|
105
|
+
* config reaches the filter, so an app's filter can read the rest of it. */
|
|
106
|
+
interface ServerWakeAxiosConfig {
|
|
107
|
+
method?: string;
|
|
108
|
+
url?: string;
|
|
109
|
+
responseType?: string;
|
|
110
|
+
}
|
|
111
|
+
/** The slice of an interceptor manager the adapter uses. A method (not a function
|
|
112
|
+
* property) so axios's wider `use(onFulfilled?, onRejected?, options?)` fits it. */
|
|
113
|
+
interface InterceptorManagerLike<V> {
|
|
114
|
+
use(onFulfilled: (value: V) => V | Promise<V>, onRejected: (error: unknown) => unknown): number;
|
|
115
|
+
eject(id: number): void;
|
|
116
|
+
}
|
|
117
|
+
/** An axios instance, structurally — so the kit types the adapter without importing
|
|
118
|
+
* axios. `axios.create()`'s result fits it as is. */
|
|
119
|
+
interface AxiosLikeInstance<C extends ServerWakeAxiosConfig, R extends {
|
|
120
|
+
config: C;
|
|
121
|
+
}> {
|
|
122
|
+
interceptors: {
|
|
123
|
+
request: InterceptorManagerLike<C>;
|
|
124
|
+
response: InterceptorManagerLike<R>;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Drive a watcher from an axios instance's interceptors: `attachServerWake(api)`.
|
|
129
|
+
* Returns a function that ejects both interceptors again.
|
|
130
|
+
*
|
|
131
|
+
* Attach it BEFORE the app's own response interceptors: axios runs those in the order
|
|
132
|
+
* they were added, so the wake one ends the wait before a 401 handler awaits a token
|
|
133
|
+
* refresh — otherwise the notice would keep counting a request that has already been
|
|
134
|
+
* answered. (Request-side order does not matter.)
|
|
135
|
+
*/
|
|
136
|
+
declare function attachServerWake<C extends ServerWakeAxiosConfig, R extends {
|
|
137
|
+
config: C;
|
|
138
|
+
}>(instance: AxiosLikeInstance<C, R>, watcher?: ServerWakeWatcher): () => void;
|
|
139
|
+
/**
|
|
140
|
+
* A `fetch` that drives a watcher: `const apiFetch = wrapFetch(fetch)`. The wait ends
|
|
141
|
+
* when the response's HEADERS arrive (or the call fails) — the server is awake by then,
|
|
142
|
+
* however long the body takes.
|
|
143
|
+
*/
|
|
144
|
+
declare function wrapFetch(fetchImpl?: typeof fetch, watcher?: ServerWakeWatcher): typeof fetch;
|
|
145
|
+
|
|
146
|
+
export { type AxiosLikeInstance, type ServerWakeAxiosConfig, type ServerWakeFilter, type ServerWakeOptions, type ServerWakeRequest, type ServerWakeStage, type ServerWakeWatcher, attachServerWake, createServerWake, serverWake, watchReadsAnd, wrapFetch };
|