@eifi1/ui-kit 0.21.0 → 0.22.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/dist/chart.d.ts +10 -8
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +66 -166
- package/dist/components/danger-confirm.js +133 -49
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +12 -2
- package/dist/components/icon-picker.js +9 -2
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +127 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +27 -2
- package/dist/components/swatch-picker.js +73 -31
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +210 -3
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +49 -56
- package/src/components/autocomplete.tsx +133 -110
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +10 -0
- package/src/components/month-picker.tsx +399 -63
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/tour/tour.tsx +66 -28
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
|
-
import type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
|
+
import type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
5
|
import { Button, Input, Label, Spinner } from "./ui";
|
|
6
|
+
import type { InputProps } from "./ui";
|
|
7
|
+
import { Checkbox } from "./checkbox";
|
|
6
8
|
import { Tooltip } from "./tooltip";
|
|
9
|
+
import { useCommitReason } from "./write-lock";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* How typed text is compared with the text it has to repeat — shared by
|
|
@@ -67,47 +70,229 @@ export function usePromisePending() {
|
|
|
67
70
|
return { pending, run };
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** A label that is really there — `null`, `false` and `""` are what a caller's
|
|
74
|
+
* `cond && reason` evaluates to on the path where there is none. */
|
|
75
|
+
function hasContent(node: ReactNode): boolean {
|
|
76
|
+
return node !== undefined && node !== null && node !== false && node !== "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The one word {@link CurrentPasswordInput} says on its own behalf — the
|
|
80
|
+
* `dangerConfirm.password` label, shared with DangerConfirm's password field, so an
|
|
81
|
+
* app translates "Password" once. */
|
|
82
|
+
export type CurrentPasswordInputLabels = Pick<DangerConfirmLabels, "password">;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Everything {@link Input} takes except what makes this field what it is: `type` is
|
|
86
|
+
* always `"password"` and `autoComplete` always `"current-password"`.
|
|
87
|
+
*/
|
|
88
|
+
export interface CurrentPasswordInputProps extends Omit<InputProps, "type" | "autoComplete" | "label"> {
|
|
89
|
+
/**
|
|
90
|
+
* The visible label. Default `dangerConfirm.password` ("Password") — unless the field
|
|
91
|
+
* is named another way (`aria-label` / `aria-labelledby`), which then stays the only
|
|
92
|
+
* name. Pass the app's own word on a sign-in form ("Password", "Your password"), or
|
|
93
|
+
* `null` for a field whose `<label htmlFor>` is drawn elsewhere (a form library's
|
|
94
|
+
* `FormLabel`), which would otherwise be labelled twice.
|
|
95
|
+
*/
|
|
96
|
+
label?: ReactNode;
|
|
97
|
+
/** The value as a string — the `(next) => …` shape, so a caller holding it in
|
|
98
|
+
* `useState` need not unwrap `event.target.value`. Fires alongside `onChange`,
|
|
99
|
+
* never instead of it, so a form library's `onChange` keeps working. */
|
|
100
|
+
onValueChange?: (value: string) => void;
|
|
101
|
+
/**
|
|
102
|
+
* A check is running (the sign-in request, the re-auth). The field turns `readOnly`,
|
|
103
|
+
* NOT `disabled`: the field being submitted is usually the focused one (Enter was
|
|
104
|
+
* pressed in it), and disabling a focused field drops focus to `<body>` — the user
|
|
105
|
+
* lands nowhere when the "wrong password" answer comes back.
|
|
106
|
+
*/
|
|
107
|
+
busy?: boolean;
|
|
108
|
+
/** Per-instance override of `dangerConfirm.password`. */
|
|
109
|
+
labels?: Partial<CurrentPasswordInputLabels>;
|
|
110
|
+
}
|
|
111
|
+
|
|
70
112
|
/**
|
|
71
|
-
* The "prove it is you" field: a password input
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* field
|
|
113
|
+
* The "prove it is you" field: a password input that a password manager fills with
|
|
114
|
+
* the CURRENT password and never offers to generate a new one for.
|
|
115
|
+
*
|
|
116
|
+
* It is the field of a sign-in form, a re-auth step ({@link ReauthDialog} draws the same
|
|
117
|
+
* field), and {@link DangerConfirm}'s `requirePassword`. Public since 0.22 for kastlan's
|
|
118
|
+
* sign-in and re-auth (kastlan 1), which wrote `type="password"` +
|
|
119
|
+
* `autoComplete="current-password"` by hand — and a sign-in field that forgets the
|
|
120
|
+
* second half gets the browser's "suggest a strong password" offer on the one form where
|
|
121
|
+
* a NEW password is never what is wanted. The reverse matters as much: a field that sets
|
|
122
|
+
* a NEW password (a passphrase, a sign-up) must NOT be this one — keksdose #170 keeps
|
|
123
|
+
* its passphrase doors on `new-password` / `off` for that reason.
|
|
124
|
+
*
|
|
125
|
+
* Everything else is {@link Input}: the reveal toggle, `error` (under the field,
|
|
126
|
+
* `aria-describedby`, `aria-invalid`), `invalid`, `name`, `id`, `autoFocus`, `required`.
|
|
127
|
+
* The ref is the `<input>`, so it works with `@eifi1/ui-kit/rhf`'s `FormControl` /
|
|
128
|
+
* a `register()` spread, and controlled or uncontrolled.
|
|
75
129
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <CurrentPasswordInput name="password" label={t("password")} error={errors.password?.message}
|
|
132
|
+
* busy={isSubmitting} {...register("password")} />
|
|
133
|
+
* ```
|
|
78
134
|
*/
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
export const CurrentPasswordInput = forwardRef<HTMLInputElement, CurrentPasswordInputProps>(
|
|
136
|
+
function CurrentPasswordInput({ label, onValueChange, onChange, busy, readOnly, labels: labelsProp, ...rest }, ref) {
|
|
137
|
+
const labels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);
|
|
138
|
+
const namedOtherwise = rest["aria-label"] !== undefined || rest["aria-labelledby"] !== undefined;
|
|
139
|
+
return (
|
|
140
|
+
<Input
|
|
141
|
+
{...rest}
|
|
142
|
+
ref={ref}
|
|
143
|
+
// After the spread: a props object spread at the field must not turn it into a
|
|
144
|
+
// text field, or into one the password manager offers a new password for.
|
|
145
|
+
type="password"
|
|
146
|
+
autoComplete="current-password"
|
|
147
|
+
label={label === undefined ? (namedOtherwise ? undefined : labels.password) : hasContent(label) ? label : undefined}
|
|
148
|
+
readOnly={Boolean(busy) || readOnly}
|
|
149
|
+
onChange={(e) => {
|
|
150
|
+
onChange?.(e);
|
|
151
|
+
onValueChange?.(e.target.value);
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
CurrentPasswordInput.displayName = "CurrentPasswordInput";
|
|
158
|
+
|
|
159
|
+
/** The words {@link TypedConfirmField} says on its own behalf — the `dangerConfirm`
|
|
160
|
+
* namespace's `phrase` and `phrasePlaceholder`, shared with DangerConfirm's own field,
|
|
161
|
+
* so the sentence "Type “X” to confirm" is translated once. */
|
|
162
|
+
export type TypedConfirmFieldLabels = Pick<DangerConfirmLabels, "phrase" | "phrasePlaceholder">;
|
|
163
|
+
|
|
164
|
+
export interface TypedConfirmFieldProps
|
|
165
|
+
extends Omit<InputProps, "type" | "value" | "defaultValue" | "label" | "placeholder"> {
|
|
166
|
+
/** The text to repeat — a word ("DELETE"), a name, an e-mail address. */
|
|
167
|
+
target: string;
|
|
168
|
+
/** How the typed text is compared with `target` — {@link TypedMatch}. Default
|
|
169
|
+
* `"trim"`, as {@link typedMatches}; `"caseless"` for an address or an
|
|
170
|
+
* acknowledgement word a user may type in any case. */
|
|
171
|
+
match?: TypedMatch;
|
|
172
|
+
/** Controlled value. Left out, the field keeps its own (start: `defaultValue`). */
|
|
173
|
+
value?: string;
|
|
174
|
+
defaultValue?: string;
|
|
175
|
+
/** The typed text as a string, on every keystroke. Fires alongside `onChange`. */
|
|
176
|
+
onValueChange?: (value: string) => void;
|
|
177
|
+
/**
|
|
178
|
+
* The typed text started or stopped matching `target` — fired from the keystroke
|
|
179
|
+
* that flipped it, so an uncontrolled field can still drive the button it guards. A
|
|
180
|
+
* controlled caller that resets the value itself (on a close) knows the answer
|
|
181
|
+
* already, and gets no call for that; it can also compute it with {@link typedMatches}.
|
|
182
|
+
*/
|
|
183
|
+
onMatchedChange?: (matched: boolean) => void;
|
|
184
|
+
/** The label. Default `dangerConfirm.phrase(target)` — "Type “DELETE” to confirm". */
|
|
185
|
+
label?: ReactNode;
|
|
186
|
+
/** Text inside the empty field. Default `dangerConfirm.phrasePlaceholder` (unset).
|
|
187
|
+
* Given, the label moves ABOVE the field: a floating label occupies the empty
|
|
188
|
+
* field, which is exactly where a placeholder shows. `""` means none. */
|
|
189
|
+
placeholder?: string;
|
|
190
|
+
/** The guarded action is running: the field turns `readOnly` (not `disabled`, which
|
|
191
|
+
* would drop the focus Enter left in it). */
|
|
192
|
+
busy?: boolean;
|
|
193
|
+
labels?: Partial<TypedConfirmFieldLabels>;
|
|
109
194
|
}
|
|
110
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The "type this to confirm" field — {@link DangerConfirm}'s `phrase` field and
|
|
198
|
+
* `useConfirm({ requireTyped })`'s, on its own (keksdose K7).
|
|
199
|
+
*
|
|
200
|
+
* keksdose writes it by hand four times: the privacy enrol, upgrade and sweep dialogs
|
|
201
|
+
* (type the acknowledgement word, then the key doors appear) and the admin's "type the
|
|
202
|
+
* user's address" (`UserActionConfirm`), each an `Input` plus its own
|
|
203
|
+
* `trim().toUpperCase()` / `toLowerCase()` comparison. None of those places is an
|
|
204
|
+
* arm → confirm tile or a modal confirm — what is guarded is a whole form, or the
|
|
205
|
+
* next step of a dialog — so the field had to stand alone. What it brings over a plain
|
|
206
|
+
* `Input`: one match rule ({@link typedMatches}, which lower-cases locale-independently
|
|
207
|
+
* on purpose), the label worded from the target in the app's language, and the
|
|
208
|
+
* attributes a phone keyboard needs to leave the text alone (no autocomplete, no
|
|
209
|
+
* auto-capital, no autocorrect, no spellcheck — a keyboard that "corrects" an e-mail
|
|
210
|
+
* address makes the guard unpassable).
|
|
211
|
+
*
|
|
212
|
+
* The field reports whether it matches (`onMatchedChange`, and `data-matched` on the
|
|
213
|
+
* `<input>` for styling); what the match unlocks is the caller's. The ref is the
|
|
214
|
+
* `<input>`.
|
|
215
|
+
*
|
|
216
|
+
* ```tsx
|
|
217
|
+
* const [ok, setOk] = useState(false);
|
|
218
|
+
* <TypedConfirmField target={t("privacy.enroll_ack_word")} match="caseless" onMatchedChange={setOk} />
|
|
219
|
+
* {ok && <KeyDoorsForm … />}
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export const TypedConfirmField = forwardRef<HTMLInputElement, TypedConfirmFieldProps>(function TypedConfirmField(
|
|
223
|
+
{
|
|
224
|
+
target,
|
|
225
|
+
match = "trim",
|
|
226
|
+
value: valueProp,
|
|
227
|
+
defaultValue = "",
|
|
228
|
+
onValueChange,
|
|
229
|
+
onMatchedChange,
|
|
230
|
+
onChange,
|
|
231
|
+
label: labelProp,
|
|
232
|
+
placeholder: placeholderProp,
|
|
233
|
+
busy,
|
|
234
|
+
readOnly,
|
|
235
|
+
labels: labelsProp,
|
|
236
|
+
id,
|
|
237
|
+
className,
|
|
238
|
+
...rest
|
|
239
|
+
},
|
|
240
|
+
ref,
|
|
241
|
+
) {
|
|
242
|
+
const labels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);
|
|
243
|
+
const [own, setOwn] = useState(defaultValue);
|
|
244
|
+
const value = valueProp ?? own;
|
|
245
|
+
const matched = typedMatches(value, target, match);
|
|
246
|
+
const generated = useId();
|
|
247
|
+
const fieldId = id ?? generated;
|
|
248
|
+
const label =
|
|
249
|
+
labelProp !== undefined
|
|
250
|
+
? labelProp
|
|
251
|
+
: typeof labels.phrase === "function"
|
|
252
|
+
? labels.phrase(target)
|
|
253
|
+
: labels.phrase;
|
|
254
|
+
const fromLabels =
|
|
255
|
+
labels.phrasePlaceholder === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: typeof labels.phrasePlaceholder === "function"
|
|
258
|
+
? labels.phrasePlaceholder(target)
|
|
259
|
+
: labels.phrasePlaceholder;
|
|
260
|
+
const placeholder = placeholderProp ?? fromLabels;
|
|
261
|
+
|
|
262
|
+
const fieldProps = {
|
|
263
|
+
...rest,
|
|
264
|
+
ref,
|
|
265
|
+
id: fieldId,
|
|
266
|
+
value,
|
|
267
|
+
autoComplete: "off",
|
|
268
|
+
autoCapitalize: "off",
|
|
269
|
+
autoCorrect: "off",
|
|
270
|
+
spellCheck: false,
|
|
271
|
+
readOnly: Boolean(busy) || readOnly,
|
|
272
|
+
"data-matched": matched || undefined,
|
|
273
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
274
|
+
onChange?.(e);
|
|
275
|
+
const next = e.target.value;
|
|
276
|
+
if (valueProp === undefined) setOwn(next);
|
|
277
|
+
onValueChange?.(next);
|
|
278
|
+
const nextMatched = typedMatches(next, target, match);
|
|
279
|
+
if (nextMatched !== matched) onMatchedChange?.(nextMatched);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (placeholder) {
|
|
284
|
+
return (
|
|
285
|
+
// A static label above, so the placeholder has the empty field to itself.
|
|
286
|
+
<div className={cn("space-y-1.5", className)}>
|
|
287
|
+
<Label htmlFor={fieldId}>{label}</Label>
|
|
288
|
+
<Input {...fieldProps} placeholder={placeholder} />
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return <Input {...fieldProps} label={label} className={className} />;
|
|
293
|
+
});
|
|
294
|
+
TypedConfirmField.displayName = "TypedConfirmField";
|
|
295
|
+
|
|
111
296
|
/**
|
|
112
297
|
* Every string the tile renders — the `dangerConfirm` namespace of
|
|
113
298
|
* `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and
|
|
@@ -136,6 +321,9 @@ export interface DangerConfirmLabels {
|
|
|
136
321
|
* label occupies the empty field, which is exactly where a placeholder shows) and
|
|
137
322
|
* the placeholder fills the field. An empty string means none. */
|
|
138
323
|
phrasePlaceholder?: string | ((phrase: string) => string);
|
|
324
|
+
/** 0.22: the checkbox `requireAcknowledge={true}` shows — "I have read what this does
|
|
325
|
+
* and want to continue" (keksdose's `admin.users.confirm_ack`). */
|
|
326
|
+
acknowledge: string;
|
|
139
327
|
}
|
|
140
328
|
|
|
141
329
|
/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`
|
|
@@ -147,8 +335,23 @@ export const DEFAULT_DANGER_CONFIRM_LABELS = {
|
|
|
147
335
|
prompt: "This cannot be undone.",
|
|
148
336
|
password: "Password",
|
|
149
337
|
phrase: (phrase: string) => `Type “${phrase}” to confirm`,
|
|
338
|
+
acknowledge: "I have read what this does and want to continue.",
|
|
150
339
|
} satisfies DangerConfirmLabels;
|
|
151
340
|
|
|
341
|
+
/**
|
|
342
|
+
* One line of {@link DangerConfirmProps.consequences}: what the action will do. A plain
|
|
343
|
+
* string, or this, for a key of its own and the `severe` mark.
|
|
344
|
+
*/
|
|
345
|
+
export interface DangerConsequence {
|
|
346
|
+
/** React key — keksdose keys its lines by what they say ("api_tokens", "only_door").
|
|
347
|
+
* Default: the text when it is a string, else the position. */
|
|
348
|
+
key?: string;
|
|
349
|
+
text: ReactNode;
|
|
350
|
+
/** "There is no way back" — painted in the danger colour rather than the list's
|
|
351
|
+
* muted one. keksdose reserves it for the one line that loses something for good. */
|
|
352
|
+
severe?: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
152
355
|
export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
|
|
153
356
|
/**
|
|
154
357
|
* Runs the action, with the entered password when `requirePassword` is on.
|
|
@@ -161,6 +364,28 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
161
364
|
onConfirm: (password?: string) => void | Promise<unknown>;
|
|
162
365
|
/** Show a password field; confirm stays disabled until it is filled. */
|
|
163
366
|
requirePassword?: boolean;
|
|
367
|
+
/**
|
|
368
|
+
* Show an "I understand" checkbox; confirm stays disabled until it is ticked
|
|
369
|
+
* (keksdose K7). `true` words it with `labels.acknowledge`; any other node IS the
|
|
370
|
+
* checkbox's label ("I understand Anna will be signed out everywhere").
|
|
371
|
+
*
|
|
372
|
+
* The guard for an action whose risk is MIS-TARGETING less than carelessness:
|
|
373
|
+
* keksdose's admin password reset asks for a typed address when the account has an
|
|
374
|
+
* encrypted door to lose, and a tick otherwise (`UserActionConfirm`), and its plan
|
|
375
|
+
* editor asks for the tick alone (`UserPlanEditor`). Both were hand-built beside this
|
|
376
|
+
* tile because it had no tick. It combines with `phrase` and `requirePassword`: every
|
|
377
|
+
* guard given must be satisfied.
|
|
378
|
+
*/
|
|
379
|
+
requireAcknowledge?: ReactNode;
|
|
380
|
+
/**
|
|
381
|
+
* What the action will do, said before it happens — a list under the prompt, shown
|
|
382
|
+
* once armed (keksdose K7, `UserActionConfirm`'s consequences). Strings, or
|
|
383
|
+
* {@link DangerConsequence}s for a `severe` line in the danger colour.
|
|
384
|
+
*
|
|
385
|
+
* Derive them from the row's own facts rather than reciting the worst case: an admin
|
|
386
|
+
* who reads the same paragraph on every row stops reading it (keksdose dev#488).
|
|
387
|
+
*/
|
|
388
|
+
consequences?: readonly (string | DangerConsequence)[];
|
|
164
389
|
/** Show a "type <phrase> to confirm" field; confirm stays disabled until the field
|
|
165
390
|
* matches (case-sensitive; surrounding spaces ignored unless `phraseMatch="exact"`). */
|
|
166
391
|
phrase?: string;
|
|
@@ -195,9 +420,20 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
195
420
|
* stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.
|
|
196
421
|
*
|
|
197
422
|
* On the ARM button rather than the confirm: arming asks for a password, and asking
|
|
198
|
-
* for a password for a write that can never land is the worse of the two.
|
|
423
|
+
* for a password for a write that can never land is the worse of the two. A tile that
|
|
424
|
+
* is ALREADY armed when the reason arrives (controlled `armed`, or a lock that landed
|
|
425
|
+
* while it was open) keeps its fields but its confirm takes the reason the same way
|
|
426
|
+
* — focusable, `aria-disabled`, the reason in its tooltip — and Enter does nothing.
|
|
199
427
|
*/
|
|
200
428
|
lockedReason?: ReactNode;
|
|
429
|
+
/**
|
|
430
|
+
* This tile COMMITS — the 0.18 write-lock opt-in that {@link Button} has (keksdose
|
|
431
|
+
* K3). Under a locked {@link WriteLockProvider} the lock's reason becomes
|
|
432
|
+
* `lockedReason` (and wins over one of the tile's own), so a page that mounts the
|
|
433
|
+
* provider no longer threads `lockedReason={lock.locked ? lock.reason : undefined}`
|
|
434
|
+
* into every tile. No provider, or an unlocked one: no effect.
|
|
435
|
+
*/
|
|
436
|
+
commit?: boolean;
|
|
201
437
|
/** Controlled armed state. The parent can then collapse the tile from a mutation's
|
|
202
438
|
* own `onSuccess` without returning a promise. */
|
|
203
439
|
armed?: boolean;
|
|
@@ -209,8 +445,9 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
209
445
|
|
|
210
446
|
/**
|
|
211
447
|
* An "arm → confirm" tile for destructive actions: one button, which expands into a
|
|
212
|
-
* warning, an optional
|
|
213
|
-
* confirm
|
|
448
|
+
* warning, an optional list of consequences, an optional "I understand" tick, an
|
|
449
|
+
* optional type-to-confirm field, an optional password field and a confirm that stays
|
|
450
|
+
* disabled until every guard is satisfied.
|
|
214
451
|
*
|
|
215
452
|
* Keksdose hand-rolled it three times (load demo data, wipe everything, reset a
|
|
216
453
|
* budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific
|
|
@@ -225,6 +462,8 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
225
462
|
export function DangerConfirm({
|
|
226
463
|
onConfirm,
|
|
227
464
|
requirePassword,
|
|
465
|
+
requireAcknowledge,
|
|
466
|
+
consequences,
|
|
228
467
|
phrase,
|
|
229
468
|
phraseMatch = "trim",
|
|
230
469
|
prompt,
|
|
@@ -233,7 +472,8 @@ export function DangerConfirm({
|
|
|
233
472
|
confirmLabel,
|
|
234
473
|
busy: busyProp,
|
|
235
474
|
disabled,
|
|
236
|
-
lockedReason,
|
|
475
|
+
lockedReason: ownLockedReason,
|
|
476
|
+
commit,
|
|
237
477
|
armed: armedProp,
|
|
238
478
|
onArmedChange,
|
|
239
479
|
labels: labelsProp,
|
|
@@ -241,17 +481,19 @@ export function DangerConfirm({
|
|
|
241
481
|
...rest
|
|
242
482
|
}: DangerConfirmProps) {
|
|
243
483
|
const labels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);
|
|
484
|
+
const lockedReason = useCommitReason(commit, ownLockedReason);
|
|
244
485
|
const [armedState, setArmedState] = useState(false);
|
|
245
486
|
const armed = armedProp ?? armedState;
|
|
246
487
|
const [password, setPassword] = useState("");
|
|
247
488
|
const [typed, setTyped] = useState("");
|
|
489
|
+
const [acknowledged, setAcknowledged] = useState(false);
|
|
248
490
|
const { pending, run } = usePromisePending();
|
|
249
491
|
const busy = Boolean(busyProp) || pending;
|
|
250
492
|
// `readOnly` rather than `disabled` on the fields while busy: disabling the field
|
|
251
493
|
// that has focus (Enter was pressed in it) drops focus to <body>.
|
|
252
494
|
const promptId = useId();
|
|
495
|
+
const consequencesId = useId();
|
|
253
496
|
const reasonId = useId();
|
|
254
|
-
const phraseFieldId = useId();
|
|
255
497
|
|
|
256
498
|
// The kit's Button takes no ref, so the two buttons focus is moved to are found by id.
|
|
257
499
|
const armId = useId();
|
|
@@ -270,6 +512,7 @@ export function DangerConfirm({
|
|
|
270
512
|
if (!armed) {
|
|
271
513
|
setPassword("");
|
|
272
514
|
setTyped("");
|
|
515
|
+
setAcknowledged(false);
|
|
273
516
|
}
|
|
274
517
|
}
|
|
275
518
|
|
|
@@ -296,37 +539,34 @@ export function DangerConfirm({
|
|
|
296
539
|
onArmedChange?.(next);
|
|
297
540
|
};
|
|
298
541
|
|
|
299
|
-
const locked = lockedReason
|
|
542
|
+
const locked = hasContent(lockedReason);
|
|
543
|
+
// `true` words the tick with the namespace; any other node is its label; `false`,
|
|
544
|
+
// `null` and `""` are no tick at all.
|
|
545
|
+
const acknowledgeLabel: ReactNode =
|
|
546
|
+
requireAcknowledge === true ? labels.acknowledge : hasContent(requireAcknowledge) ? requireAcknowledge : null;
|
|
547
|
+
const asksAcknowledge = acknowledgeLabel !== null;
|
|
300
548
|
const passwordOk = !requirePassword || password !== "";
|
|
301
549
|
const phraseOk = phrase === undefined || typedMatches(typed, phrase, phraseMatch);
|
|
302
|
-
const
|
|
550
|
+
const acknowledgeOk = !asksAcknowledge || acknowledged;
|
|
551
|
+
const canConfirm = passwordOk && phraseOk && acknowledgeOk && !busy;
|
|
303
552
|
|
|
304
553
|
const submit = (e: FormEvent) => {
|
|
305
554
|
e.preventDefault();
|
|
306
|
-
|
|
555
|
+
// `locked` too: Enter in a field submits the form without the button's say.
|
|
556
|
+
if (!canConfirm || locked) return;
|
|
307
557
|
// Disarms when it resolves; a rejection leaves it armed, fields kept: the caller
|
|
308
558
|
// shows why, the user retries.
|
|
309
559
|
run(onConfirm(requirePassword ? password : undefined), () => setArmed(false));
|
|
310
560
|
};
|
|
311
561
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
phrase === undefined ? "" : typeof labels.phrase === "function" ? labels.phrase(phrase) : labels.phrase;
|
|
321
|
-
const phraseFieldProps = {
|
|
322
|
-
ref: firstFieldRef,
|
|
323
|
-
value: typed,
|
|
324
|
-
autoComplete: "off",
|
|
325
|
-
autoCapitalize: "off",
|
|
326
|
-
spellCheck: false,
|
|
327
|
-
readOnly: busy,
|
|
328
|
-
onChange: (e: ChangeEvent<HTMLInputElement>) => setTyped(e.target.value),
|
|
329
|
-
};
|
|
562
|
+
// The first field takes focus on arm: the tick, then the phrase, then the password —
|
|
563
|
+
// the order they are drawn in.
|
|
564
|
+
const firstField = asksAcknowledge ? "acknowledge" : phrase !== undefined ? "phrase" : "password";
|
|
565
|
+
const lines = (consequences ?? []).map((item, index) =>
|
|
566
|
+
typeof item === "string"
|
|
567
|
+
? { key: item, text: item as ReactNode, severe: false }
|
|
568
|
+
: { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },
|
|
569
|
+
);
|
|
330
570
|
|
|
331
571
|
const toneText = tone === "warning" ? "text-[var(--warning)]" : "text-[var(--danger)]";
|
|
332
572
|
|
|
@@ -377,7 +617,7 @@ export function DangerConfirm({
|
|
|
377
617
|
<div {...rest} className={cn("space-y-2", className)}>
|
|
378
618
|
<form
|
|
379
619
|
className="space-y-2"
|
|
380
|
-
aria-describedby={promptId}
|
|
620
|
+
aria-describedby={lines.length > 0 ? `${promptId} ${consequencesId}` : promptId}
|
|
381
621
|
aria-busy={busy || undefined}
|
|
382
622
|
onSubmit={submit}
|
|
383
623
|
noValidate
|
|
@@ -385,20 +625,43 @@ export function DangerConfirm({
|
|
|
385
625
|
<p id={promptId} className={cn("text-xs font-medium", toneText)}>
|
|
386
626
|
{prompt ?? labels.prompt}
|
|
387
627
|
</p>
|
|
388
|
-
{
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
628
|
+
{lines.length > 0 && (
|
|
629
|
+
<ul id={consequencesId} className="list-disc space-y-1 ps-5 text-xs text-[var(--text-secondary)]">
|
|
630
|
+
{lines.map((line) => (
|
|
631
|
+
<li key={line.key} className={line.severe ? "text-[var(--danger)]" : undefined}>
|
|
632
|
+
{line.text}
|
|
633
|
+
</li>
|
|
634
|
+
))}
|
|
635
|
+
</ul>
|
|
636
|
+
)}
|
|
637
|
+
{asksAcknowledge && (
|
|
638
|
+
<Checkbox
|
|
639
|
+
ref={firstField === "acknowledge" ? firstFieldRef : undefined}
|
|
640
|
+
label={acknowledgeLabel}
|
|
641
|
+
checked={acknowledged}
|
|
642
|
+
// Not `disabled` while busy, for the same reason the fields are readOnly:
|
|
643
|
+
// the box may hold the focus. A change while busy is simply not taken.
|
|
644
|
+
onCheckedChange={(next) => {
|
|
645
|
+
if (!busy) setAcknowledged(next);
|
|
646
|
+
}}
|
|
647
|
+
/>
|
|
648
|
+
)}
|
|
649
|
+
{phrase !== undefined && (
|
|
650
|
+
<TypedConfirmField
|
|
651
|
+
ref={firstField === "phrase" ? firstFieldRef : undefined}
|
|
652
|
+
target={phrase}
|
|
653
|
+
match={phraseMatch}
|
|
654
|
+
value={typed}
|
|
655
|
+
onValueChange={setTyped}
|
|
656
|
+
busy={busy}
|
|
657
|
+
// Already resolved through this tile's own `labels` prop; handed on so a
|
|
658
|
+
// per-instance phrase wording reaches the field.
|
|
659
|
+
labels={{ phrase: labels.phrase, phrasePlaceholder: labels.phrasePlaceholder }}
|
|
660
|
+
/>
|
|
661
|
+
)}
|
|
398
662
|
{requirePassword && (
|
|
399
663
|
<CurrentPasswordInput
|
|
400
|
-
|
|
401
|
-
inputRef={phrase === undefined ? firstFieldRef : undefined}
|
|
664
|
+
ref={firstField === "password" ? firstFieldRef : undefined}
|
|
402
665
|
label={labels.password}
|
|
403
666
|
value={password}
|
|
404
667
|
busy={busy}
|
|
@@ -413,12 +676,17 @@ export function DangerConfirm({
|
|
|
413
676
|
type="submit"
|
|
414
677
|
variant={tone === "warning" ? "primary" : "danger"}
|
|
415
678
|
disabled={!canConfirm}
|
|
679
|
+
// Armed, then locked (or rendered armed under a lock): the confirm says why
|
|
680
|
+
// the way Button does — focusable, `aria-disabled`, the reason in its
|
|
681
|
+
// tooltip and description — and the fields stay as typed.
|
|
682
|
+
disabledReason={locked ? lockedReason : undefined}
|
|
416
683
|
aria-busy={busy || undefined}
|
|
417
684
|
>
|
|
418
685
|
{busy && <Spinner label={null} className="h-4 w-4" />}
|
|
419
686
|
{confirmLabel ?? labels.confirm}
|
|
420
687
|
</Button>
|
|
421
688
|
</div>
|
|
689
|
+
{locked && <p className="text-xs text-[var(--text-muted)]">{lockedReason}</p>}
|
|
422
690
|
</form>
|
|
423
691
|
</div>
|
|
424
692
|
);
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
// ships no translations; the English defaults below make the table usable with no
|
|
4
4
|
// wiring.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// A type-only import: erased at build time, so this module stays free of the table
|
|
7
|
+
// itself (and of react-router), which `i18n/defaults` relies on.
|
|
8
|
+
import type { DataTableSortLabels } from "./data-table";
|
|
9
|
+
|
|
10
|
+
/** The phone sort control's four keys (`sortBy`, `sortDefault`, `sortAscending`,
|
|
11
|
+
* `sortDescending`, 0.22.0) are {@link DataTableSortLabels}', declared beside the
|
|
12
|
+
* control and part of this namespace like every other key. */
|
|
13
|
+
export interface DataTableLabels extends DataTableSortLabels {
|
|
7
14
|
// Table chrome
|
|
8
15
|
columns: string;
|
|
9
16
|
selectAllRows: string;
|
|
@@ -143,6 +150,14 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
|
143
150
|
booleanTrue: "Yes",
|
|
144
151
|
booleanFalse: "No",
|
|
145
152
|
booleanUnset: "Not set",
|
|
153
|
+
// The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`
|
|
154
|
+
// in data-table.tsx, written out rather than spread in: importing that VALUE here
|
|
155
|
+
// would make data-table.tsx and this module import each other (data-table.tsx reads
|
|
156
|
+
// its resolver from here), and would pull the whole table into `i18n/defaults`.
|
|
157
|
+
sortBy: "Sort by",
|
|
158
|
+
sortDefault: "Default order",
|
|
159
|
+
sortAscending: "Ascending",
|
|
160
|
+
sortDescending: "Descending",
|
|
146
161
|
};
|
|
147
162
|
|
|
148
163
|
/**
|