@eifi1/ui-kit 0.32.0 → 0.32.1
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/account/delete-account-setting.d.ts +1 -1
- package/dist/admin/admin-action-confirm.d.ts +1 -1
- package/dist/admin/admin-action-log.d.ts +1 -1
- package/dist/admin/admin-action-log.js +4 -2
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +1 -1
- package/dist/admin/role-select.d.ts +1 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +1 -1
- package/dist/admin/user-roster.d.ts +1 -1
- package/dist/auth/register-form.d.ts +1 -1
- package/dist/billing/billing-banner.d.ts +1 -1
- package/dist/billing/billing-labels.d.ts +1 -1
- package/dist/billing/billing-labels.js +3 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-lock.d.ts +1 -1
- package/dist/billing/billing-standing.d.ts +1 -1
- package/dist/billing/plan-card.d.ts +1 -1
- package/dist/billing/plan-card.js +1 -1
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-notice.d.ts +1 -1
- package/dist/billing/subscription-actions.d.ts +1 -1
- package/dist/billing/subscription-status-chip.d.ts +1 -1
- package/dist/chart.d.ts +2 -2
- package/dist/chart.js.map +1 -1
- package/dist/components/alert-banner.js +1 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +1 -1
- package/dist/components/appearance-labels.d.ts +6 -0
- package/dist/components/appearance-labels.js +2 -1
- package/dist/components/appearance-labels.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/button-group.js +7 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/chip.js +7 -2
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/danger-confirm.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +2 -1
- package/dist/components/data-table-pagination.js +15 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.js +2 -2
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +11 -1
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/iban-input.d.ts +1 -1
- package/dist/components/inline-edit-field.js +4 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +1 -1
- package/dist/components/legal-page.d.ts +1 -1
- package/dist/components/legal.d.ts +1 -1
- package/dist/components/money-field.d.ts +1 -1
- package/dist/components/month-picker.js +41 -34
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/numpad-sheet.d.ts +1 -1
- package/dist/components/phone-input.d.ts +1 -1
- package/dist/components/phone-input.js +2 -2
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/row-actions.d.ts +1 -1
- package/dist/components/row-actions.js +103 -37
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/series-chart.d.ts +1 -1
- package/dist/components/settings-fields.d.ts +7 -3
- package/dist/components/settings-fields.js +2 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +1 -1
- package/dist/components/share-card.js +6 -2
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/status-dot.js +9 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swipeable-row.js +8 -1
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.d.ts +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/toggle-group.d.ts +9 -2
- package/dist/components/toggle-group.js +3 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -1
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +5 -5
- package/dist/components/ui.js.map +1 -1
- package/dist/components/wizard-stepper.js +20 -14
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{delete-account-setting-BvdQdbv3.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +43 -8
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-context.d.ts +1 -1
- package/dist/feedback/feedback-crash.d.ts +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-labels.d.ts +1 -1
- package/dist/feedback/feedback-menu.d.ts +1 -1
- package/dist/feedback/feedback-record.d.ts +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +1 -1
- package/dist/feedback/feedback-submit.d.ts +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-table.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-breakpoint.d.ts +19 -8
- package/dist/hooks/use-breakpoint.js +1 -1
- package/dist/hooks/use-breakpoint.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/german.d.ts +1 -1
- package/dist/i18n/german.js +7 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/languages.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/en.d.ts +1 -1
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/es.js +7 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/fr.js +7 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/hu.js +7 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/it.js +7 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/i18n/locales/zh.js +7 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +1 -1
- package/dist/landing/landing-sections.d.ts +1 -1
- package/dist/landing/landing-sections.js +7 -3
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/public-header.d.ts +1 -1
- package/dist/landing/routing.d.ts +1 -1
- package/dist/rhf/fields.d.ts +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf.d.ts +1 -1
- package/dist/settings/settings-section.d.ts +1 -1
- package/dist/settings/use-account-appearance.d.ts +1 -1
- package/dist/settings/use-account-appearance.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/theme/contrast.d.ts +3 -2
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/text-size.d.ts +19 -3
- package/dist/theme/text-size.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +1 -1
- package/src/admin/admin-action-log.tsx +6 -1
- package/src/billing/billing-labels.ts +16 -0
- package/src/billing/plan-card.tsx +1 -1
- package/src/chart.ts +2 -0
- package/src/components/alert-banner.tsx +5 -1
- package/src/components/appearance-labels.ts +7 -0
- package/src/components/button-group.tsx +21 -0
- package/src/components/chip.tsx +34 -6
- package/src/components/choice-card.tsx +1 -1
- package/src/components/data-table-pagination.tsx +60 -31
- package/src/components/data-table.tsx +8 -4
- package/src/components/date-picker.tsx +32 -1
- package/src/components/inline-edit-field.tsx +20 -1
- package/src/components/month-picker.tsx +6 -1
- package/src/components/phone-input.tsx +7 -2
- package/src/components/row-actions.tsx +155 -49
- package/src/components/settings-fields.tsx +8 -3
- package/src/components/share-card.tsx +21 -2
- package/src/components/status-dot.tsx +25 -2
- package/src/components/swipeable-row.tsx +8 -1
- package/src/components/toggle-group.tsx +26 -12
- package/src/components/translation-review-editor.tsx +1 -1
- package/src/components/translation-review.tsx +5 -4
- package/src/components/ui.tsx +22 -8
- package/src/components/wizard-stepper.tsx +5 -1
- package/src/hooks/use-breakpoint.ts +20 -9
- package/src/i18n/german.ts +4 -0
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/index.ts +4 -2
- package/src/landing/landing-sections.tsx +7 -3
- package/src/settings/use-account-appearance.ts +1 -1
- package/src/theme/contrast.ts +3 -2
- package/src/theme/text-size.ts +19 -3
- package/src/wizard/wizard-summary.tsx +4 -2
- package/tokens.css +15 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import '../lib/phone.js';
|
|
3
|
-
export { cJ as DEFAULT_PHONE_INPUT_LABELS, eq as PhoneInput, er as PhoneInputLabels, es as PhoneInputProps } from '../delete-account-setting-
|
|
3
|
+
export { cJ as DEFAULT_PHONE_INPUT_LABELS, eq as PhoneInput, er as PhoneInputLabels, es as PhoneInputProps } from '../delete-account-setting-FLyJf-kZ.js';
|
|
4
4
|
import './tooltip.js';
|
|
5
5
|
import '../hooks/use-anchored-rect.js';
|
|
6
6
|
import '../lib/clipping.js';
|
|
@@ -119,7 +119,7 @@ const PhoneInput = forwardRef(function PhoneInput2({
|
|
|
119
119
|
role: named ? "group" : void 0,
|
|
120
120
|
"aria-label": named ? label : void 0,
|
|
121
121
|
children: [
|
|
122
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2", children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2 large:max-sm:flex-wrap", children: [
|
|
123
123
|
/* @__PURE__ */ jsxs(
|
|
124
124
|
Select,
|
|
125
125
|
{
|
|
@@ -135,7 +135,7 @@ const PhoneInput = forwardRef(function PhoneInput2({
|
|
|
135
135
|
]
|
|
136
136
|
}
|
|
137
137
|
),
|
|
138
|
-
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
138
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 large:max-sm:basis-full", children: /* @__PURE__ */ jsx(
|
|
139
139
|
Input,
|
|
140
140
|
{
|
|
141
141
|
ref,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { Input, Select, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\nexport const DEFAULT_PHONE_INPUT_LABELS: PhoneInputLabels = {\n countryCode: \"Country code\",\n other: \"Other\",\n};\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** The caption and the error under the row — the type every kit field uses for them\n * (Select's caption, Input's `error`). */\nconst CAPTION_CLASS = \"mt-1 text-caption leading-tight text-[var(--text-muted)]\";\nconst ERROR_CLASS = \"mt-1 text-caption leading-tight text-[var(--danger)]\";\n\nconst isTextHint = (hint: unknown): hint is string | number =>\n (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: Partial<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput(\n {\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n },\n ref,\n) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n const captionId = useId();\n const errorId = useId();\n const caption = isTextHint(hint);\n const showError = hasMessage(error);\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n <div className=\"flex items-stretch gap-2\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n // A FieldHint \"?\" rides the label line; text goes under the whole row.\n hint={caption ? undefined : hint}\n invalid={invalid || showError}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId)}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {caption && (\n <p id={captionId} className={CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {showError && (\n <p id={errorId} className={ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n});\nPhoneInput.displayName = \"PhoneInput\";\n"],"mappings":"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","names":["PhoneInput"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { Input, Select, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\nexport const DEFAULT_PHONE_INPUT_LABELS: PhoneInputLabels = {\n countryCode: \"Country code\",\n other: \"Other\",\n};\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** The caption and the error under the row — the type every kit field uses for them\n * (Select's caption, Input's `error`). */\nconst CAPTION_CLASS = \"mt-1 text-caption leading-tight text-[var(--text-muted)]\";\nconst ERROR_CLASS = \"mt-1 text-caption leading-tight text-[var(--danger)]\";\n\nconst isTextHint = (hint: unknown): hint is string | number =>\n (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: Partial<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput(\n {\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n },\n ref,\n) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n const captionId = useId();\n const errorId = useId();\n const caption = isTextHint(hint);\n const showError = hasMessage(error);\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n // A FieldHint \"?\" rides the label line; text goes under the whole row.\n hint={caption ? undefined : hint}\n invalid={invalid || showError}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId)}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {caption && (\n <p id={captionId} className={CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {showError && (\n <p id={errorId} className={ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n});\nPhoneInput.displayName = \"PhoneInput\";\n"],"mappings":"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","names":["PhoneInput"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import 'lucide-react';
|
|
3
|
-
export { cO as DEFAULT_ROW_ACTIONS_LABELS, eO as RowAction, eP as RowActionList, eQ as RowActions, eR as RowActionsCollapse, eS as RowActionsColumnOptions, eT as RowActionsLabels, eU as RowActionsProps, eV as RowActionsSize, fY as rowActionsColumn } from '../delete-account-setting-
|
|
3
|
+
export { cO as DEFAULT_ROW_ACTIONS_LABELS, eO as RowAction, eP as RowActionList, eQ as RowActions, eR as RowActionsCollapse, eS as RowActionsColumnOptions, eT as RowActionsLabels, eU as RowActionsProps, eV as RowActionsSize, fY as rowActionsColumn } from '../delete-account-setting-FLyJf-kZ.js';
|
|
4
4
|
import '../data-table-labels-kpAAVi6g.js';
|
|
5
5
|
import './tooltip.js';
|
|
6
6
|
import '../hooks/use-anchored-rect.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { isValidElement } from "react";
|
|
4
4
|
import { MoreHorizontal } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
@@ -7,6 +7,7 @@ import { useLargeText } from "../hooks/use-large-text.js";
|
|
|
7
7
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
8
|
import { Popover } from "./popover.js";
|
|
9
9
|
import { Button, CompactControls, IconButton } from "./ui.js";
|
|
10
|
+
import { useCommitReason } from "./write-lock.js";
|
|
10
11
|
const DEFAULT_ROW_ACTIONS_LABELS = {
|
|
11
12
|
actions: "Actions",
|
|
12
13
|
actionsFor: (name) => `Actions for ${name}`
|
|
@@ -62,72 +63,137 @@ function RowActions({
|
|
|
62
63
|
children: /* @__PURE__ */ jsx(MoreHorizontal, {})
|
|
63
64
|
}
|
|
64
65
|
),
|
|
65
|
-
children: (close) =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
66
|
+
children: (close) => (
|
|
67
|
+
// React carries an event out of a portal along the COMPONENT tree, so a click on
|
|
68
|
+
// an entry (or between two) reached a clickable row or a DataTable `onRowClick`
|
|
69
|
+
// the menu was opened from, though the panel is nowhere near it in the page —
|
|
70
|
+
// only the "⋯" trigger stopped its own (0.32.1, keksdose's 0.32 report). Stopped
|
|
71
|
+
// here for the whole panel: clicks, and the Enter / Space a row also listens for.
|
|
72
|
+
// Escape goes on, to the popover that closes on it.
|
|
73
|
+
/* @__PURE__ */ jsx("div", { role: "presentation", onClick: stopClick, onKeyDown: stopActivationKeys, children: /* @__PURE__ */ jsx("ul", { "data-slot": "row-actions-menu", className: "space-y-0.5", children: entries.map((action) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MenuAction, { action, close }) }, action.key ?? action.label)) }) })
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function stopClick(e) {
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
}
|
|
81
|
+
function stopActivationKeys(e) {
|
|
82
|
+
if (e.key === "Enter" || e.key === " ") e.stopPropagation();
|
|
83
|
+
}
|
|
84
|
+
function useActionHref(action) {
|
|
85
|
+
const reason = useCommitReason(action.commit, action.disabledReason);
|
|
86
|
+
const refused = reason !== void 0 && reason !== null && reason !== false && reason !== "";
|
|
87
|
+
return action.href !== void 0 && !action.pending && !refused ? action.href : void 0;
|
|
88
|
+
}
|
|
89
|
+
function MenuAction({ action, close }) {
|
|
90
|
+
const href = useActionHref(action);
|
|
91
|
+
const glyph = glyphOf(action.icon);
|
|
92
|
+
const words = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
93
|
+
glyph && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex shrink-0 [&_svg]:size-4", children: glyph }),
|
|
94
|
+
action.label
|
|
95
|
+
] });
|
|
96
|
+
const tone = action.tone === "danger" ? "danger" : void 0;
|
|
97
|
+
const select = () => {
|
|
98
|
+
close();
|
|
99
|
+
action.onSelect?.();
|
|
100
|
+
};
|
|
101
|
+
if (href !== void 0) {
|
|
102
|
+
return /* @__PURE__ */ jsx(
|
|
103
|
+
Button,
|
|
104
|
+
{
|
|
105
|
+
href,
|
|
106
|
+
external: action.external,
|
|
107
|
+
variant: "ghost",
|
|
108
|
+
size: "sm",
|
|
109
|
+
stretch: true,
|
|
110
|
+
tone,
|
|
111
|
+
disabled: action.disabled,
|
|
112
|
+
className: "w-full justify-start text-start",
|
|
113
|
+
onClick: select,
|
|
114
|
+
children: words
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return /* @__PURE__ */ jsx(
|
|
119
|
+
Button,
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
variant: "ghost",
|
|
123
|
+
size: "sm",
|
|
124
|
+
stretch: true,
|
|
125
|
+
tone,
|
|
126
|
+
disabledReason: action.disabledReason,
|
|
127
|
+
commit: action.commit,
|
|
128
|
+
pending: action.pending,
|
|
129
|
+
disabled: action.disabled,
|
|
130
|
+
className: "w-full justify-start text-start",
|
|
131
|
+
onClick: select,
|
|
132
|
+
children: words
|
|
91
133
|
}
|
|
92
134
|
);
|
|
93
135
|
}
|
|
94
136
|
function InlineAction({ action, size }) {
|
|
137
|
+
const href = useActionHref(action);
|
|
95
138
|
const glyph = glyphOf(action.icon);
|
|
139
|
+
const tone = action.tone === "danger" ? "danger" : void 0;
|
|
96
140
|
if (glyph) {
|
|
141
|
+
if (href !== void 0) {
|
|
142
|
+
return /* @__PURE__ */ jsx(
|
|
143
|
+
IconButton,
|
|
144
|
+
{
|
|
145
|
+
href,
|
|
146
|
+
external: action.external,
|
|
147
|
+
size,
|
|
148
|
+
label: action.label,
|
|
149
|
+
tone,
|
|
150
|
+
disabled: action.disabled,
|
|
151
|
+
stopPropagation: true,
|
|
152
|
+
onClick: () => action.onSelect?.(),
|
|
153
|
+
children: glyph
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
97
157
|
return /* @__PURE__ */ jsx(
|
|
98
158
|
IconButton,
|
|
99
159
|
{
|
|
100
160
|
size,
|
|
101
161
|
label: action.label,
|
|
102
|
-
tone
|
|
162
|
+
tone,
|
|
103
163
|
disabledReason: action.disabledReason,
|
|
104
164
|
commit: action.commit,
|
|
105
165
|
pending: action.pending,
|
|
106
166
|
disabled: action.disabled,
|
|
107
167
|
stopPropagation: true,
|
|
108
|
-
onClick: action.onSelect,
|
|
168
|
+
onClick: () => action.onSelect?.(),
|
|
109
169
|
children: glyph
|
|
110
170
|
}
|
|
111
171
|
);
|
|
112
172
|
}
|
|
173
|
+
const stop = {
|
|
174
|
+
onClick: (e) => {
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
action.onSelect?.();
|
|
177
|
+
},
|
|
178
|
+
onKeyDown: (e) => {
|
|
179
|
+
if (e.key === "Enter" || e.key === " ") e.stopPropagation();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (href !== void 0) {
|
|
183
|
+
return /* @__PURE__ */ jsx(Button, { href, external: action.external, variant: "ghost", size: "sm", tone, disabled: action.disabled, ...stop, children: action.label });
|
|
184
|
+
}
|
|
113
185
|
return /* @__PURE__ */ jsx(
|
|
114
186
|
Button,
|
|
115
187
|
{
|
|
116
188
|
type: "button",
|
|
117
189
|
variant: "ghost",
|
|
118
190
|
size: "sm",
|
|
119
|
-
tone
|
|
191
|
+
tone,
|
|
120
192
|
disabledReason: action.disabledReason,
|
|
121
193
|
commit: action.commit,
|
|
122
194
|
pending: action.pending,
|
|
123
195
|
disabled: action.disabled,
|
|
124
|
-
|
|
125
|
-
e.stopPropagation();
|
|
126
|
-
action.onSelect();
|
|
127
|
-
},
|
|
128
|
-
onKeyDown: (e) => {
|
|
129
|
-
if (e.key === "Enter" || e.key === " ") e.stopPropagation();
|
|
130
|
-
},
|
|
196
|
+
...stop,
|
|
131
197
|
children: action.label
|
|
132
198
|
}
|
|
133
199
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/row-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Popover } from \"./popover\";\nimport { Button, CompactControls, IconButton } from \"./ui\";\nimport type { IconButtonSize } from \"./ui\";\nimport type { DataTableColumn } from \"./data-table\";\n\n/**\n * A row's actions — inline icons at Normal, one \"⋯\" menu at Large and above\n * (docs/text-size-harmonization.md §10.8).\n *\n * At Large an IconButton shows its label as text (§4). In a dense row — a register's\n * action column, a list row's end — every label at once does not fit: Kurvenschmiede\n * has 44 IconButtons in rows, kastlan 119. So a row's actions collapse instead, into\n * the menu the user roster has used since 0.31 (`UserRowActions`, now built on this):\n * one \"⋯\" button, and a menu with each action's icon AND words, where a locked one stays\n * listed and says why.\n *\n * The data is the roster's `UserRowActionList`, generalised: entries, and `false` /\n * `null` for the ones a condition left out (`row.locked || {…}`).\n */\n\n/** One action of a row. */\nexport interface RowAction {\n /** React key. Default: the label. */\n key?: string;\n /** The action's name: the inline icon's name and tooltip, the menu entry's words. */\n label: string;\n /** Runs it. The kit sends nothing — the app opens its confirmation or calls its API. */\n onSelect: () => void;\n /**\n * The glyph: a Lucide icon (`Pencil`), or an element (`<Pencil />`, an app's own\n * svg) — DataTable's `rowActions` hand their `icon` node through. Inline, an action\n * without one is a small text button; in the menu it is the words alone.\n */\n icon?: LucideIcon | ReactElement;\n /** `danger` for delete, revoke, erase: the quiet red of IconButton's `tone=\"danger\"`\n * inline, the danger text tone in the menu. */\n tone?: \"default\" | \"danger\";\n /** Why it is not available for this row. It stays listed, focusable, and says so — an\n * action that silently vanished would leave the reader looking for it. */\n disabledReason?: ReactNode;\n /**\n * This action COMMITS — it saves, sends, deletes: under a locked\n * {@link WriteLockProvider} it is refused with the lock's reason, as {@link Button}'s\n * `commit` is, inline and in the menu alike. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n /**\n * In flight — the request it started has not answered. Inline, the icon turns into\n * IconButton's spinner (`pending`); collapsed, the \"⋯\" button does, since the menu\n * closed when the action was chosen. Either way nothing in the row can be pressed again\n * until it settles: the \"⋯\" opens no menu while busy.\n */\n pending?: boolean;\n /**\n * Not available for a moment, with nothing to explain — another action of the list\n * is in flight (an admin panel's `busy`). The plain `disabled`; a lasting refusal is a\n * `disabledReason`, which says why.\n */\n disabled?: boolean;\n /** Leave it out for this row. */\n hidden?: boolean;\n}\n\n/** What a row's actions may be: entries, and `false` / `null` / `undefined` for the ones\n * a condition left out. */\nexport type RowActionList = readonly (RowAction | false | null | undefined)[];\n\nexport interface RowActionsLabels {\n /** The menu button's name when the row has no name of its own; also the actions\n * column's (hidden) header in {@link rowActionsColumn}. */\n actions: string;\n /** The menu button's name, given the row's: \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\nexport const DEFAULT_ROW_ACTIONS_LABELS: RowActionsLabels = {\n actions: \"Actions\",\n actionsFor: (name) => `Actions for ${name}`,\n};\n\n/**\n * - `\"auto\"` (default): the icons inline at Normal; the \"⋯\" menu at Large and Extra\n * large once there are two or more (§10.8). A single action stays inline at every\n * size, with its label as text at Large — one label fits a row, and a menu of one is\n * a detour.\n * - `\"menu\"`: the menu at every size — the roster's choice, whose actions grow (§2.4\n * there) and do not fit a phone card as a strip.\n * - `\"inline\"`: the icons at every size — for a row the app knows has room; at Large\n * each then shows its label (IconButton's `labelVisible`).\n */\nexport type RowActionsCollapse = \"auto\" | \"menu\" | \"inline\";\n\n/** The sizes a row action comes in: IconButton's row steps. `2xs` is DataTable's\n * compact density. At Large every one is at least a 48 px target (IconButton). */\nexport type RowActionsSize = Extract<IconButtonSize, \"md\" | \"sm\" | \"xs\" | \"2xs\">;\n\nexport interface RowActionsProps {\n actions: RowActionList;\n /** The row's name, for the menu button's name — \"Actions for Ada Example\". Without it\n * the button is \"Actions\". */\n name?: string;\n /** When the actions collapse into the menu — see {@link RowActionsCollapse}. */\n collapse?: RowActionsCollapse;\n /** The inline icons' and the \"⋯\" button's size. Default `xs`, a list row's action. */\n size?: RowActionsSize;\n /** The menu's width. Default `\"15rem\"`, in rem so it grows with the words (§3.2). */\n menuWidth?: `${number}rem`;\n /** On the wrapper: the inline strip, or the \"⋯\" button's box. */\n className?: string;\n labels?: Partial<RowActionsLabels>;\n}\n\nfunction visibleActions(actions: RowActionList): RowAction[] {\n return actions.filter((a): a is RowAction => Boolean(a) && !(a as RowAction).hidden);\n}\n\n/** An action's glyph as a node: a component is rendered, an element is used as is. */\nfunction glyphOf(icon: RowAction[\"icon\"]): ReactNode {\n if (icon === undefined) return null;\n if (isValidElement(icon)) return icon;\n const Icon = icon as LucideIcon;\n return <Icon />;\n}\n\n/**\n * A row's actions, inline or as a \"⋯\" menu — see {@link RowActionsCollapse}. Every\n * control stops its click (and the Enter/Space it stands for) from reaching the row, so\n * a clickable row or a DataTable `rowLink` does not open as well; in a DataTable column\n * also set `noRowLink: true`, as {@link rowActionsColumn} does.\n *\n * Inline, the icons sit in a {@link CompactControls} region at Normal: their labels\n * stay the names and the tooltips, and a locked one keeps its reason in the tooltip — a\n * line under one icon of a row would break the row. In the menu a locked action's\n * reason is a line under its words at Large and on touch (Button's\n * `disabledReasonDisplay`).\n *\n * The menu is portalled, so a table's scroller cannot clip it; Escape and an outside\n * press close it, and focus goes back to the \"⋯\" button, which is where a confirmation\n * dialog then returns it.\n */\nexport function RowActions({\n actions,\n name,\n collapse = \"auto\",\n size = \"xs\",\n menuWidth = \"15rem\",\n className,\n labels: labelsProp,\n}: RowActionsProps) {\n const labels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);\n const large = useLargeText();\n const entries = visibleActions(actions);\n if (entries.length === 0) return null;\n const asMenu = collapse === \"menu\" || (collapse === \"auto\" && large && entries.length > 1);\n if (!asMenu) {\n const strip = (\n <div data-slot=\"row-actions\" className={cn(\"inline-flex items-center gap-0.5\", className)}>\n {entries.map((action) => (\n <InlineAction key={action.key ?? action.label} action={action} size={size} />\n ))}\n </div>\n );\n // Compact at Normal (no room for a reason line in a row); at Large an inline action\n // is the app's choice (\"inline\") or the row's only one, and shows its words.\n return large ? strip : <CompactControls>{strip}</CompactControls>;\n }\n const title = name ? labels.actionsFor(name) : labels.actions;\n // The menu closed when the action was chosen, so what is in flight shows on \"⋯\".\n const busy = entries.some((action) => action.pending);\n return (\n <Popover\n width={menuWidth}\n aria-label={title}\n className=\"p-1\"\n trigger={({ open, toggle, ref }) => (\n <IconButton\n ref={ref}\n size={size}\n label={title}\n // \"⋯\" is read by everyone as \"more actions\"; the menu it opens is the words.\n labelVisible={false}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n pending={busy}\n stopPropagation\n onClick={toggle}\n className={className}\n >\n <MoreHorizontal />\n </IconButton>\n )}\n >\n {(close) => (\n <ul data-slot=\"row-actions-menu\" className=\"space-y-0.5\">\n {entries.map((action) => {\n const glyph = glyphOf(action.icon);\n return (\n <li key={action.key ?? action.label}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={action.tone === \"danger\" ? \"danger\" : undefined}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={() => {\n close();\n action.onSelect();\n }}\n >\n {glyph && (\n <span aria-hidden className=\"flex shrink-0 [&_svg]:size-4\">\n {glyph}\n </span>\n )}\n {action.label}\n </Button>\n </li>\n );\n })}\n </ul>\n )}\n </Popover>\n );\n}\n\n/** One inline action: an IconButton with its icon, else a small text button. */\nfunction InlineAction({ action, size }: { action: RowAction; size: RowActionsSize }) {\n const glyph = glyphOf(action.icon);\n if (glyph) {\n return (\n <IconButton\n size={size}\n label={action.label}\n tone={action.tone === \"danger\" ? \"danger\" : undefined}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n stopPropagation\n onClick={action.onSelect}\n >\n {glyph}\n </IconButton>\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n tone={action.tone === \"danger\" ? \"danger\" : undefined}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n onClick={(e) => {\n // A row listening for clicks must not open as well (IconButton's stopPropagation).\n e.stopPropagation();\n action.onSelect();\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n }}\n >\n {action.label}\n </Button>\n );\n}\n\n/** The actions column's hidden header, in the provider's words. */\nfunction RowActionsHeader({ text, labels }: { text?: string; labels?: Partial<RowActionsLabels> }) {\n const resolved = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labels);\n return <span className=\"sr-only\">{text ?? resolved.actions}</span>;\n}\n\nexport interface RowActionsColumnOptions<T> {\n /** The column's key. Default `\"actions\"`. */\n key?: string;\n /** The row's actions — `false` / `null` for the ones a condition leaves out. */\n actions: (row: T) => RowActionList;\n /** The row's name, for the \"⋯\" button's name (\"Actions for Ada Example\"). */\n name?: (row: T) => string | undefined;\n collapse?: RowActionsCollapse;\n /** Default `xs`. */\n size?: RowActionsSize;\n /**\n * The column's name as plain text — the column-settings checklist and the phone\n * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left\n * out, the header shows the provider's `rowActions.actions` to a screen reader and\n * the plain-text places fall back to the column's `key`.\n */\n headerText?: string;\n labels?: Partial<RowActionsLabels>;\n /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A DataTable column of {@link RowActions} — an app's action column in one line\n * (§10.8): inline icons at Normal, the \"⋯\" menu at Large with two or more actions.\n *\n * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}\n *\n * The header is visually hidden (the icons say what the column is), the column hugs its\n * content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the\n * buttons keep their own clicks. On a phone it is a line of the card like any column;\n * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it\n * then shows in the opened row instead).\n */\nexport function rowActionsColumn<T>({\n key = \"actions\",\n actions,\n name,\n collapse,\n size,\n headerText,\n labels,\n column,\n}: RowActionsColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header: <RowActionsHeader text={headerText} labels={labels} />,\n headerText,\n cell: (row) => (\n <RowActions actions={actions(row)} name={name?.(row)} collapse={collapse} size={size} labels={labels} />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n noRowLink: true,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/row-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Popover } from \"./popover\";\nimport { Button, CompactControls, IconButton } from \"./ui\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { IconButtonSize } from \"./ui\";\nimport type { DataTableColumn } from \"./data-table\";\n\n/**\n * A row's actions — inline icons at Normal, one \"⋯\" menu at Large and above\n * (docs/text-size-harmonization.md §10.8).\n *\n * At Large an IconButton shows its label as text (§4). In a dense row — a register's\n * action column, a list row's end — every label at once does not fit: Kurvenschmiede\n * has 44 IconButtons in rows, kastlan 119. So a row's actions collapse instead, into\n * the menu the user roster has used since 0.31 (`UserRowActions`, now built on this):\n * one \"⋯\" button, and a menu with each action's icon AND words, where a locked one stays\n * listed and says why.\n *\n * The data is the roster's `UserRowActionList`, generalised: entries, and `false` /\n * `null` for the ones a condition left out (`row.locked || {…}`).\n */\n\n/** One action of a row. */\nexport interface RowAction {\n /** React key. Default: the label. */\n key?: string;\n /** The action's name: the inline icon's name and tooltip, the menu entry's words. */\n label: string;\n /**\n * Runs it. The kit sends nothing — the app opens its confirmation or calls its API.\n * Optional only beside `href`; an action has one or the other (or both: `onSelect`\n * then runs as the link is followed).\n */\n onSelect?: () => void;\n /**\n * The action NAVIGATES — an edit page, a mail to compose (0.32.1, kastlan's 0.32\n * report): a real link inline and in the menu, so open in a new tab, a middle click\n * and the URL on hover work, which `onSelect` + `navigate` lost. The kit's link rule\n * applies ({@link Button}'s `href`): in-app through the provider's `linkComponent`, an\n * external or `#anchor` one a plain `<a>`, `mailto:` included.\n *\n * A link cannot carry a reason or a spinner, so an action with a `disabledReason`\n * (or under a write lock, with `commit`) or `pending` stays a button and says why.\n */\n href?: string;\n /** With `href`: leaves the app — a new tab, and a screen reader is told so. */\n external?: boolean;\n /**\n * The glyph: a Lucide icon (`Pencil`), or an element (`<Pencil />`, an app's own\n * svg) — DataTable's `rowActions` hand their `icon` node through. Inline, an action\n * without one is a small text button; in the menu it is the words alone.\n */\n icon?: LucideIcon | ReactElement;\n /** `danger` for delete, revoke, erase: the quiet red of IconButton's `tone=\"danger\"`\n * inline, the danger text tone in the menu. */\n tone?: \"default\" | \"danger\";\n /** Why it is not available for this row. It stays listed, focusable, and says so — an\n * action that silently vanished would leave the reader looking for it. */\n disabledReason?: ReactNode;\n /**\n * This action COMMITS — it saves, sends, deletes: under a locked\n * {@link WriteLockProvider} it is refused with the lock's reason, as {@link Button}'s\n * `commit` is, inline and in the menu alike. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n /**\n * In flight — the request it started has not answered. Inline, the icon turns into\n * IconButton's spinner (`pending`); collapsed, the \"⋯\" button does, since the menu\n * closed when the action was chosen. Either way nothing in the row can be pressed again\n * until it settles: the \"⋯\" opens no menu while busy.\n */\n pending?: boolean;\n /**\n * Not available for a moment, with nothing to explain — another action of the list\n * is in flight (an admin panel's `busy`). The plain `disabled`; a lasting refusal is a\n * `disabledReason`, which says why.\n */\n disabled?: boolean;\n /** Leave it out for this row. */\n hidden?: boolean;\n}\n\n/** What a row's actions may be: entries, and `false` / `null` / `undefined` for the ones\n * a condition left out. */\nexport type RowActionList = readonly (RowAction | false | null | undefined)[];\n\nexport interface RowActionsLabels {\n /** The menu button's name when the row has no name of its own; also the actions\n * column's (hidden) header in {@link rowActionsColumn}. */\n actions: string;\n /** The menu button's name, given the row's: \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\nexport const DEFAULT_ROW_ACTIONS_LABELS: RowActionsLabels = {\n actions: \"Actions\",\n actionsFor: (name) => `Actions for ${name}`,\n};\n\n/**\n * - `\"auto\"` (default): the icons inline at Normal; the \"⋯\" menu at Large and Extra\n * large once there are two or more (§10.8). A single action stays inline at every\n * size, with its label as text at Large — one label fits a row, and a menu of one is\n * a detour.\n * - `\"menu\"`: the menu at every size — the roster's choice, whose actions grow (§2.4\n * there) and do not fit a phone card as a strip.\n * - `\"inline\"`: the icons at every size — for a row the app knows has room; at Large\n * each then shows its label (IconButton's `labelVisible`).\n */\nexport type RowActionsCollapse = \"auto\" | \"menu\" | \"inline\";\n\n/** The sizes a row action comes in: IconButton's row steps. `2xs` is DataTable's\n * compact density. At Large every one is at least a 48 px target (IconButton). */\nexport type RowActionsSize = Extract<IconButtonSize, \"md\" | \"sm\" | \"xs\" | \"2xs\">;\n\nexport interface RowActionsProps {\n actions: RowActionList;\n /** The row's name, for the menu button's name — \"Actions for Ada Example\". Without it\n * (or `null`, as API fields often are) the button is \"Actions\". */\n name?: string | null;\n /** When the actions collapse into the menu — see {@link RowActionsCollapse}. */\n collapse?: RowActionsCollapse;\n /** The inline icons' and the \"⋯\" button's size. Default `xs`, a list row's action. */\n size?: RowActionsSize;\n /** The menu's width. Default `\"15rem\"`, in rem so it grows with the words (§3.2). */\n menuWidth?: `${number}rem`;\n /** On the wrapper: the inline strip, or the \"⋯\" button's box. */\n className?: string;\n labels?: Partial<RowActionsLabels>;\n}\n\nfunction visibleActions(actions: RowActionList): RowAction[] {\n return actions.filter((a): a is RowAction => Boolean(a) && !(a as RowAction).hidden);\n}\n\n/** An action's glyph as a node: a component is rendered, an element is used as is. */\nfunction glyphOf(icon: RowAction[\"icon\"]): ReactNode {\n if (icon === undefined) return null;\n if (isValidElement(icon)) return icon;\n const Icon = icon as LucideIcon;\n return <Icon />;\n}\n\n/**\n * A row's actions, inline or as a \"⋯\" menu — see {@link RowActionsCollapse}. Every\n * control stops its click (and the Enter/Space it stands for) from reaching the row, so\n * a clickable row or a DataTable `rowLink` does not open as well; in a DataTable column\n * also set `noRowLink: true`, as {@link rowActionsColumn} does.\n *\n * Inline, the icons sit in a {@link CompactControls} region at Normal: their labels\n * stay the names and the tooltips, and a locked one keeps its reason in the tooltip — a\n * line under one icon of a row would break the row. In the menu a locked action's\n * reason is a line under its words at Large and on touch (Button's\n * `disabledReasonDisplay`).\n *\n * The menu is portalled, so a table's scroller cannot clip it; Escape and an outside\n * press close it, and focus goes back to the \"⋯\" button, which is where a confirmation\n * dialog then returns it.\n */\nexport function RowActions({\n actions,\n name,\n collapse = \"auto\",\n size = \"xs\",\n menuWidth = \"15rem\",\n className,\n labels: labelsProp,\n}: RowActionsProps) {\n const labels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);\n const large = useLargeText();\n const entries = visibleActions(actions);\n if (entries.length === 0) return null;\n const asMenu = collapse === \"menu\" || (collapse === \"auto\" && large && entries.length > 1);\n if (!asMenu) {\n const strip = (\n <div data-slot=\"row-actions\" className={cn(\"inline-flex items-center gap-0.5\", className)}>\n {entries.map((action) => (\n <InlineAction key={action.key ?? action.label} action={action} size={size} />\n ))}\n </div>\n );\n // Compact at Normal (no room for a reason line in a row); at Large an inline action\n // is the app's choice (\"inline\") or the row's only one, and shows its words.\n return large ? strip : <CompactControls>{strip}</CompactControls>;\n }\n const title = name ? labels.actionsFor(name) : labels.actions;\n // The menu closed when the action was chosen, so what is in flight shows on \"⋯\".\n const busy = entries.some((action) => action.pending);\n return (\n <Popover\n width={menuWidth}\n aria-label={title}\n className=\"p-1\"\n trigger={({ open, toggle, ref }) => (\n <IconButton\n ref={ref}\n size={size}\n label={title}\n // \"⋯\" is read by everyone as \"more actions\"; the menu it opens is the words.\n labelVisible={false}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n pending={busy}\n stopPropagation\n onClick={toggle}\n className={className}\n >\n <MoreHorizontal />\n </IconButton>\n )}\n >\n {(close) => (\n // React carries an event out of a portal along the COMPONENT tree, so a click on\n // an entry (or between two) reached a clickable row or a DataTable `onRowClick`\n // the menu was opened from, though the panel is nowhere near it in the page —\n // only the \"⋯\" trigger stopped its own (0.32.1, keksdose's 0.32 report). Stopped\n // here for the whole panel: clicks, and the Enter / Space a row also listens for.\n // Escape goes on, to the popover that closes on it.\n <div role=\"presentation\" onClick={stopClick} onKeyDown={stopActivationKeys}>\n <ul data-slot=\"row-actions-menu\" className=\"space-y-0.5\">\n {entries.map((action) => (\n <li key={action.key ?? action.label}>\n <MenuAction action={action} close={close} />\n </li>\n ))}\n </ul>\n </div>\n )}\n </Popover>\n );\n}\n\nfunction stopClick(e: MouseEvent) {\n e.stopPropagation();\n}\n\nfunction stopActivationKeys(e: KeyboardEvent) {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n}\n\n/**\n * The `href` an action renders as a link with, or `undefined` for a button: a link\n * cannot carry a reason or a spinner (Button's link form has neither), so a refused\n * action — its own `disabledReason`, or a write lock on a `commit` — and a pending one\n * stay buttons and say so.\n */\nfunction useActionHref(action: RowAction): string | undefined {\n const reason = useCommitReason(action.commit, action.disabledReason);\n const refused = reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n return action.href !== undefined && !action.pending && !refused ? action.href : undefined;\n}\n\n/** One entry of the \"⋯\" menu: the words, as a link for an `href`, else a button. */\nfunction MenuAction({ action, close }: { action: RowAction; close: () => void }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const words = (\n <>\n {glyph && (\n <span aria-hidden className=\"flex shrink-0 [&_svg]:size-4\">\n {glyph}\n </span>\n )}\n {action.label}\n </>\n );\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n const select = () => {\n close();\n action.onSelect?.();\n };\n if (href !== undefined) {\n return (\n <Button\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n}\n\n/** One inline action: an IconButton with its icon, else a small text button — each a\n * link for an `href` (see {@link useActionHref}). */\nfunction InlineAction({ action, size }: { action: RowAction; size: RowActionsSize }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n if (glyph) {\n if (href !== undefined) {\n return (\n <IconButton\n href={href}\n external={action.external}\n size={size}\n label={action.label}\n tone={tone}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n return (\n <IconButton\n size={size}\n label={action.label}\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n // A row listening for clicks must not open as well (IconButton's stopPropagation).\n const stop = {\n onClick: (e: MouseEvent) => {\n e.stopPropagation();\n action.onSelect?.();\n },\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n },\n };\n if (href !== undefined) {\n return (\n <Button href={href} external={action.external} variant=\"ghost\" size=\"sm\" tone={tone} disabled={action.disabled} {...stop}>\n {action.label}\n </Button>\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n}\n\n/** The actions column's hidden header, in the provider's words. */\nfunction RowActionsHeader({ text, labels }: { text?: string; labels?: Partial<RowActionsLabels> }) {\n const resolved = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labels);\n return <span className=\"sr-only\">{text ?? resolved.actions}</span>;\n}\n\nexport interface RowActionsColumnOptions<T> {\n /** The column's key. Default `\"actions\"`. */\n key?: string;\n /** The row's actions — `false` / `null` for the ones a condition leaves out. */\n actions: (row: T) => RowActionList;\n /** The row's name, for the \"⋯\" button's name (\"Actions for Ada Example\"). `null` and\n * `undefined` both mean \"Actions\". */\n name?: (row: T) => string | null | undefined;\n collapse?: RowActionsCollapse;\n /** Default `xs`. */\n size?: RowActionsSize;\n /**\n * The column's name as plain text — the column-settings checklist and the phone\n * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left\n * out, the header shows the provider's `rowActions.actions` to a screen reader and\n * the plain-text places fall back to the column's `key`.\n */\n headerText?: string;\n labels?: Partial<RowActionsLabels>;\n /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A DataTable column of {@link RowActions} — an app's action column in one line\n * (§10.8): inline icons at Normal, the \"⋯\" menu at Large with two or more actions.\n *\n * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}\n *\n * The header is visually hidden (the icons say what the column is), the column hugs its\n * content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the\n * buttons keep their own clicks. On a phone it is a line of the card like any column;\n * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it\n * then shows in the opened row instead).\n */\nexport function rowActionsColumn<T>({\n key = \"actions\",\n actions,\n name,\n collapse,\n size,\n headerText,\n labels,\n column,\n}: RowActionsColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header: <RowActionsHeader text={headerText} labels={labels} />,\n headerText,\n cell: (row) => (\n <RowActions actions={actions(row)} name={name?.(row)} collapse={collapse} size={size} labels={labels} />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n noRowLink: true,\n ...column,\n };\n}\n"],"mappings":";AAkJS,SAqHL,UArHK,KAqHL,YArHK;AAlJT,SAAS,sBAAsB;AAE/B,SAAS,sBAAsB;AAE/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,QAAQ,iBAAiB,kBAAkB;AACpD,SAAS,uBAAuB;AA2FzB,MAAM,6BAA+C;AAAA,EAC1D,SAAS;AAAA,EACT,YAAY,CAAC,SAAS,eAAe,IAAI;AAC3C;AAkCA,SAAS,eAAe,SAAqC;AAC3D,SAAO,QAAQ,OAAO,CAAC,MAAsB,QAAQ,CAAC,KAAK,CAAE,EAAgB,MAAM;AACrF;AAGA,SAAS,QAAQ,MAAoC;AACnD,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,eAAe,IAAI,EAAG,QAAO;AACjC,QAAM,OAAO;AACb,SAAO,oBAAC,QAAK;AACf;AAkBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,OAAO;AAAA,EACP,YAAY;AAAA,EACZ;AAAA,EACA,QAAQ;AACV,GAAoB;AAClB,QAAM,SAAS,aAAa,cAAc,4BAA4B,UAAU;AAChF,QAAM,QAAQ,aAAa;AAC3B,QAAM,UAAU,eAAe,OAAO;AACtC,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,SAAS,aAAa,UAAW,aAAa,UAAU,SAAS,QAAQ,SAAS;AACxF,MAAI,CAAC,QAAQ;AACX,UAAM,QACJ,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,oCAAoC,SAAS,GACrF,kBAAQ,IAAI,CAAC,WACZ,oBAAC,gBAA8C,QAAgB,QAA5C,OAAO,OAAO,OAAO,KAAmC,CAC5E,GACH;AAIF,WAAO,QAAQ,QAAQ,oBAAC,mBAAiB,iBAAM;AAAA,EACjD;AACA,QAAM,QAAQ,OAAO,OAAO,WAAW,IAAI,IAAI,OAAO;AAEtD,QAAM,OAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO;AACpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,MACP,cAAY;AAAA,MACZ,WAAU;AAAA,MACV,SAAS,CAAC,EAAE,MAAM,QAAQ,IAAI,MAC5B;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UAEP,cAAc;AAAA,UACd,iBAAc;AAAA,UACd,iBAAe;AAAA,UACf,SAAS;AAAA,UACT,iBAAe;AAAA,UACf,SAAS;AAAA,UACT;AAAA,UAEA,8BAAC,kBAAe;AAAA;AAAA,MAClB;AAAA,MAGD,WAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOA,oBAAC,SAAI,MAAK,gBAAe,SAAS,WAAW,WAAW,oBACtD,8BAAC,QAAG,aAAU,oBAAmB,WAAU,eACxC,kBAAQ,IAAI,CAAC,WACZ,oBAAC,QACC,8BAAC,cAAW,QAAgB,OAAc,KADnC,OAAO,OAAO,OAAO,KAE9B,CACD,GACH,GACF;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,UAAU,GAAe;AAChC,IAAE,gBAAgB;AACpB;AAEA,SAAS,mBAAmB,GAAkB;AAC5C,MAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,IAAK,GAAE,gBAAgB;AAC5D;AAQA,SAAS,cAAc,QAAuC;AAC5D,QAAM,SAAS,gBAAgB,OAAO,QAAQ,OAAO,cAAc;AACnE,QAAM,UAAU,WAAW,UAAa,WAAW,QAAQ,WAAW,SAAS,WAAW;AAC1F,SAAO,OAAO,SAAS,UAAa,CAAC,OAAO,WAAW,CAAC,UAAU,OAAO,OAAO;AAClF;AAGA,SAAS,WAAW,EAAE,QAAQ,MAAM,GAA6C;AAC/E,QAAM,OAAO,cAAc,MAAM;AACjC,QAAM,QAAQ,QAAQ,OAAO,IAAI;AACjC,QAAM,QACJ,iCACG;AAAA,aACC,oBAAC,UAAK,eAAW,MAAC,WAAU,gCACzB,iBACH;AAAA,IAED,OAAO;AAAA,KACV;AAEF,QAAM,OAAO,OAAO,SAAS,WAAW,WAAW;AACnD,QAAM,SAAS,MAAM;AACnB,UAAM;AACN,WAAO,WAAW;AAAA,EACpB;AACA,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAO;AAAA,QACP;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,WAAU;AAAA,QACV,SAAS;AAAA,QAER;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAO;AAAA,MACP;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,MACjB,WAAU;AAAA,MACV,SAAS;AAAA,MAER;AAAA;AAAA,EACH;AAEJ;AAIA,SAAS,aAAa,EAAE,QAAQ,KAAK,GAAgD;AACnF,QAAM,OAAO,cAAc,MAAM;AACjC,QAAM,QAAQ,QAAQ,OAAO,IAAI;AACjC,QAAM,OAAO,OAAO,SAAS,WAAW,WAAW;AACnD,MAAI,OAAO;AACT,QAAI,SAAS,QAAW;AACtB,aACE;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,UAAU,OAAO;AAAA,UACjB;AAAA,UACA,OAAO,OAAO;AAAA,UACd;AAAA,UACA,UAAU,OAAO;AAAA,UACjB,iBAAe;AAAA,UACf,SAAS,MAAM,OAAO,WAAW;AAAA,UAEhC;AAAA;AAAA,MACH;AAAA,IAEJ;AACA,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,OAAO,OAAO;AAAA,QACd;AAAA,QACA,gBAAgB,OAAO;AAAA,QACvB,QAAQ,OAAO;AAAA,QACf,SAAS,OAAO;AAAA,QAChB,UAAU,OAAO;AAAA,QACjB,iBAAe;AAAA,QACf,SAAS,MAAM,OAAO,WAAW;AAAA,QAEhC;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,OAAO;AAAA,IACX,SAAS,CAAC,MAAkB;AAC1B,QAAE,gBAAgB;AAClB,aAAO,WAAW;AAAA,IACpB;AAAA,IACA,WAAW,CAAC,MAAqB;AAC/B,UAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,IAAK,GAAE,gBAAgB;AAAA,IAC5D;AAAA,EACF;AACA,MAAI,SAAS,QAAW;AACtB,WACE,oBAAC,UAAO,MAAY,UAAU,OAAO,UAAU,SAAQ,SAAQ,MAAK,MAAK,MAAY,UAAU,OAAO,UAAW,GAAG,MACjH,iBAAO,OACV;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,MAChB,GAAG;AAAA,MAEH,iBAAO;AAAA;AAAA,EACV;AAEJ;AAGA,SAAS,iBAAiB,EAAE,MAAM,OAAO,GAA0D;AACjG,QAAM,WAAW,aAAa,cAAc,4BAA4B,MAAM;AAC9E,SAAO,oBAAC,UAAK,WAAU,WAAW,kBAAQ,SAAS,SAAQ;AAC7D;AAqCO,SAAS,iBAAoB;AAAA,EAClC,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAmD;AACjD,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,oBAAC,oBAAiB,MAAM,YAAY,QAAgB;AAAA,IAC5D;AAAA,IACA,MAAM,CAAC,QACL,oBAAC,cAAW,SAAS,QAAQ,GAAG,GAAG,MAAM,OAAO,GAAG,GAAG,UAAoB,MAAY,QAAgB;AAAA,IAExG,WAAW;AAAA,IACX,eAAe;AAAA,IACf,WAAW;AAAA,IACX,GAAG;AAAA,EACL;AACF;","names":[]}
|
|
@@ -5,7 +5,7 @@ import { LegendEntry } from './toggle-legend.js';
|
|
|
5
5
|
import { SeriesChartLabels } from './series-chart-labels.js';
|
|
6
6
|
import { TimeTickUnit } from './series-chart-ticks.js';
|
|
7
7
|
import { SeriesChartAxisBudget } from './series-chart-budget.js';
|
|
8
|
-
import { c6 as ChartTooltipPlacement } from '../delete-account-setting-
|
|
8
|
+
import { c6 as ChartTooltipPlacement } from '../delete-account-setting-FLyJf-kZ.js';
|
|
9
9
|
import { ChartHeight } from './chart-height.js';
|
|
10
10
|
import 'recharts';
|
|
11
11
|
import './tooltip.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { eY as SelectProps } from '../delete-account-setting-
|
|
3
|
+
import { eY as SelectProps } from '../delete-account-setting-FLyJf-kZ.js';
|
|
4
4
|
import { ToggleGroupBaseProps } from './toggle-group.js';
|
|
5
5
|
import { ThemePreference } from '../theme/theme-store.js';
|
|
6
6
|
import { TextSize } from '../theme/text-size.js';
|
|
@@ -136,8 +136,12 @@ interface ThemeSettingProps extends Omit<SelectProps, "value" | "onChange" | "ch
|
|
|
136
136
|
* one segmented control instead of a select — system, light and dark side by side, the
|
|
137
137
|
* choice visible without opening anything, in every app (keksdose offered light and dark
|
|
138
138
|
* only; its store already knew "system"). A {@link ToggleGroup}'s props pass through —
|
|
139
|
-
* `labelPlacement`, `disabledReason`, `commit`, `size` — minus the options
|
|
140
|
-
* which this owns.
|
|
139
|
+
* `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the options
|
|
140
|
+
* and the value, which this owns.
|
|
141
|
+
*
|
|
142
|
+
* The options wrap rather than truncate (`overflow="wrap"`), as in its two siblings
|
|
143
|
+
* {@link TextSizeSetting} and {@link ContrastSetting} (0.32.1, Kurvenschmiede's 0.32
|
|
144
|
+
* report: "Syst… Li… D…" in a card at Extra large on a 360 px phone).
|
|
141
145
|
*
|
|
142
146
|
* Bind it to the PREFERENCE (`"system"` included), never to the resolved mode: on a
|
|
143
147
|
* system-dark device a control bound to the mode shows "dark" and cannot say "follow the
|
|
@@ -7,11 +7,12 @@ import { CONTRAST_MODES } from "../theme/contrast.js";
|
|
|
7
7
|
import { useAppearanceLabels } from "./appearance-labels.js";
|
|
8
8
|
function ThemeSetting(props) {
|
|
9
9
|
if (props.variant === "toggle") {
|
|
10
|
-
const { variant: _variant2, value: value2, onChange: onChange2, label: label2, optionLabels: optionLabels2, ...rest2 } = props;
|
|
10
|
+
const { variant: _variant2, value: value2, onChange: onChange2, label: label2, optionLabels: optionLabels2, overflow = "wrap", ...rest2 } = props;
|
|
11
11
|
return /* @__PURE__ */ jsx(
|
|
12
12
|
ToggleGroup,
|
|
13
13
|
{
|
|
14
14
|
...rest2,
|
|
15
|
+
overflow,
|
|
15
16
|
label: label2,
|
|
16
17
|
value: value2,
|
|
17
18
|
onChange: onChange2,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport type { ToggleGroupBaseProps } from \"./toggle-group\";\nimport type { ThemePreference } from \"../theme/theme-store\";\nimport { TEXT_SIZES } from \"../theme/text-size\";\nimport type { TextSize } from \"../theme/text-size\";\nimport { CONTRAST_MODES } from \"../theme/contrast\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport { useAppearanceLabels } from \"./appearance-labels\";\nimport type { AppearanceLabels } from \"./appearance-labels\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs — and, since 0.32, the text size and\n * the contrast (docs/text-size-harmonization.md §6), whose words are the kit's own\n * `appearance` namespace — so they live in @hb/ui and the app just supplies the\n * current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\n/**\n * Everything a {@link Select} takes, minus the two this component owns: the value is a\n * {@link ThemePreference} rather than a string, and `onChange` hands over the preference\n * itself because no caller of this wants the event. The rest — `disabled`, `required`,\n * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes\n * this a settings FIELD rather than a fixed widget.\n */\nexport interface ThemeSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n /** The `Select` (default). See {@link ThemeSettingToggleProps} for `\"toggle\"`. */\n variant?: \"select\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\n/**\n * `variant=\"toggle\"` (0.31.0, docs/settings-harmonization.md §2.8): the three choices as\n * one segmented control instead of a select — system, light and dark side by side, the\n * choice visible without opening anything, in every app (keksdose offered light and dark\n * only; its store already knew \"system\"). A {@link ToggleGroup}'s props pass through —\n * `labelPlacement`, `disabledReason`, `commit`, `size` — minus the options and the value,\n * which this owns.\n *\n * Bind it to the PREFERENCE (`\"system\"` included), never to the resolved mode: on a\n * system-dark device a control bound to the mode shows \"dark\" and cannot say \"follow the\n * system\" (keksdose's control, §8).\n */\nexport interface ThemeSettingToggleProps\n extends Omit<ToggleGroupBaseProps<ThemePreference>, \"options\" | \"label\" | \"ariaLabel\"> {\n variant: \"toggle\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n /**\n * Names the group: the field's label, or the label above it (`labelPlacement`). Leave\n * it out inside a card already titled \"Theme\" and name the group with `aria-label` or\n * `aria-labelledby` instead, so the card does not say \"Theme\" twice.\n */\n label?: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting(props: ThemeSettingProps | ThemeSettingToggleProps) {\n if (props.variant === \"toggle\") {\n const { variant: _variant, value, onChange, label, optionLabels, ...rest } = props;\n return (\n <ToggleGroup<ThemePreference>\n {...rest}\n label={label}\n value={value}\n onChange={onChange}\n options={[\n { value: \"system\", label: optionLabels.system },\n { value: \"light\", label: optionLabels.light },\n { value: \"dark\", label: optionLabels.dark },\n ]}\n />\n );\n }\n const { variant: _variant, value, onChange, label, optionLabels, id, ...rest } = props;\n return (\n <Select\n {...rest}\n id={id}\n label={label}\n value={value}\n onChange={(e) => onChange(e.target.value as ThemePreference)}\n >\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\n/** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change\n * handler re-typed to the language CODE the caller stores. */\nexport interface LanguageSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps) {\n return (\n <Select {...rest} id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n\n/**\n * What {@link TextSizeSetting} and {@link ContrastSetting} take: a {@link ToggleGroup}'s\n * props — `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the\n * options and the value, which they own, as {@link ThemeSettingToggleProps} does.\n */\ntype AppearanceToggleProps<T extends string> = Omit<ToggleGroupBaseProps<T>, \"options\" | \"label\" | \"ariaLabel\"> & {\n value: T;\n onChange: (value: T) => void;\n /**\n * The field's visible label. Leave it out inside a row already titled with the\n * setting's name: the group is then named by `aria-label` / `aria-labelledby` if\n * given, else by the kit's own word (\"Text size\", \"Contrast\").\n */\n label?: ReactNode;\n /** The `appearance` namespace, for this instance. */\n labels?: Partial<AppearanceLabels>;\n};\n\nexport type TextSizeSettingProps = AppearanceToggleProps<TextSize>;\n\n/**\n * The text size (0.32, docs/text-size-harmonization.md §6): Normal, Large, Extra large as\n * one segmented control, in the core `appearance` group after the theme. Bind it to the\n * size IN FORCE — `useAccountAppearance().textSize` — and hand a pick to its\n * `pickTextSize`, which stores the device's choice and, signed in, the account's.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`): at Extra large on a 360 px\n * phone the group has 240 px, and \"Extra large\" cut to \"Extra l…\" would be the one\n * setting that cannot read its own value.\n */\nexport function TextSizeSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: TextSizeSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<TextSize>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.textSize)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={TEXT_SIZES.map((size) => ({ value: size, label: labels.textSizes[size] }))}\n />\n );\n}\n\nexport type ContrastSettingProps = AppearanceToggleProps<ContrastMode>;\n\n/**\n * More contrast (0.32, §5, §6): System, Standard, More as one segmented control, beside\n * the text size. Bind it to the stored MODE, \"system\" included — never to whether more\n * contrast is on at the moment, or a device that asks for more contrast would show\n * \"More\" and could not say \"follow the device\" (the theme's lesson, §8 of the settings\n * contract). `useAccountAppearance().contrast` is that mode.\n */\nexport function ContrastSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: ContrastSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<ContrastMode>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.contrast)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={CONTRAST_MODES.map((mode) => ({ value: mode, label: labels.contrastModes[mode] }))}\n />\n );\n}\n"],"mappings":";AA0EM,cAeF,YAfE;AAzEN,SAAS,cAAc;AAEvB,SAAS,mBAAmB;AAG5B,SAAS,kBAAkB;AAE3B,SAAS,sBAAsB;AAE/B,SAAS,2BAA2B;AA4D7B,SAAS,aAAa,OAAoD;AAC/E,MAAI,MAAM,YAAY,UAAU;AAC9B,UAAM,EAAE,SAASA,WAAU,OAAAC,QAAO,UAAAC,WAAU,OAAAC,QAAO,cAAAC,eAAc,GAAGC,MAAK,IAAI;AAC7E,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAGA;AAAA,QACJ,OAAOF;AAAA,QACP,OAAOF;AAAA,QACP,UAAUC;AAAA,QACV,SAAS;AAAA,UACP,EAAE,OAAO,UAAU,OAAOE,cAAa,OAAO;AAAA,UAC9C,EAAE,OAAO,SAAS,OAAOA,cAAa,MAAM;AAAA,UAC5C,EAAE,OAAO,QAAQ,OAAOA,cAAa,KAAK;AAAA,QAC5C;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,QAAM,EAAE,SAAS,UAAU,OAAO,UAAU,OAAO,cAAc,IAAI,GAAG,KAAK,IAAI;AACjF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB;AAAA,MAE3D;AAAA,4BAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,QAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,QAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA;AAAA;AAAA,EAC1C;AAEJ;AAaO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,IAAI,GAAG,KAAK,GAAyB;AACtG,SACE,oBAAC,UAAQ,GAAG,MAAM,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC3F,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;AAgCO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,UAAU,IAAI,EAAE,EAAE;AAAA;AAAA,EACpF;AAEJ;AAWO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,eAAe,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,cAAc,IAAI,EAAE,EAAE;AAAA;AAAA,EAC5F;AAEJ;","names":["_variant","value","onChange","label","optionLabels","rest"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport type { ToggleGroupBaseProps } from \"./toggle-group\";\nimport type { ThemePreference } from \"../theme/theme-store\";\nimport { TEXT_SIZES } from \"../theme/text-size\";\nimport type { TextSize } from \"../theme/text-size\";\nimport { CONTRAST_MODES } from \"../theme/contrast\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport { useAppearanceLabels } from \"./appearance-labels\";\nimport type { AppearanceLabels } from \"./appearance-labels\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs — and, since 0.32, the text size and\n * the contrast (docs/text-size-harmonization.md §6), whose words are the kit's own\n * `appearance` namespace — so they live in @hb/ui and the app just supplies the\n * current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\n/**\n * Everything a {@link Select} takes, minus the two this component owns: the value is a\n * {@link ThemePreference} rather than a string, and `onChange` hands over the preference\n * itself because no caller of this wants the event. The rest — `disabled`, `required`,\n * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes\n * this a settings FIELD rather than a fixed widget.\n */\nexport interface ThemeSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n /** The `Select` (default). See {@link ThemeSettingToggleProps} for `\"toggle\"`. */\n variant?: \"select\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\n/**\n * `variant=\"toggle\"` (0.31.0, docs/settings-harmonization.md §2.8): the three choices as\n * one segmented control instead of a select — system, light and dark side by side, the\n * choice visible without opening anything, in every app (keksdose offered light and dark\n * only; its store already knew \"system\"). A {@link ToggleGroup}'s props pass through —\n * `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the options\n * and the value, which this owns.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`), as in its two siblings\n * {@link TextSizeSetting} and {@link ContrastSetting} (0.32.1, Kurvenschmiede's 0.32\n * report: \"Syst… Li… D…\" in a card at Extra large on a 360 px phone).\n *\n * Bind it to the PREFERENCE (`\"system\"` included), never to the resolved mode: on a\n * system-dark device a control bound to the mode shows \"dark\" and cannot say \"follow the\n * system\" (keksdose's control, §8).\n */\nexport interface ThemeSettingToggleProps\n extends Omit<ToggleGroupBaseProps<ThemePreference>, \"options\" | \"label\" | \"ariaLabel\"> {\n variant: \"toggle\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n /**\n * Names the group: the field's label, or the label above it (`labelPlacement`). Leave\n * it out inside a card already titled \"Theme\" and name the group with `aria-label` or\n * `aria-labelledby` instead, so the card does not say \"Theme\" twice.\n */\n label?: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting(props: ThemeSettingProps | ThemeSettingToggleProps) {\n if (props.variant === \"toggle\") {\n const { variant: _variant, value, onChange, label, optionLabels, overflow = \"wrap\", ...rest } = props;\n return (\n <ToggleGroup<ThemePreference>\n {...rest}\n overflow={overflow}\n label={label}\n value={value}\n onChange={onChange}\n options={[\n { value: \"system\", label: optionLabels.system },\n { value: \"light\", label: optionLabels.light },\n { value: \"dark\", label: optionLabels.dark },\n ]}\n />\n );\n }\n const { variant: _variant, value, onChange, label, optionLabels, id, ...rest } = props;\n return (\n <Select\n {...rest}\n id={id}\n label={label}\n value={value}\n onChange={(e) => onChange(e.target.value as ThemePreference)}\n >\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\n/** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change\n * handler re-typed to the language CODE the caller stores. */\nexport interface LanguageSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps) {\n return (\n <Select {...rest} id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n\n/**\n * What {@link TextSizeSetting} and {@link ContrastSetting} take: a {@link ToggleGroup}'s\n * props — `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the\n * options and the value, which they own, as {@link ThemeSettingToggleProps} does.\n */\ntype AppearanceToggleProps<T extends string> = Omit<ToggleGroupBaseProps<T>, \"options\" | \"label\" | \"ariaLabel\"> & {\n value: T;\n onChange: (value: T) => void;\n /**\n * The field's visible label. Leave it out inside a row already titled with the\n * setting's name: the group is then named by `aria-label` / `aria-labelledby` if\n * given, else by the kit's own word (\"Text size\", \"Contrast\").\n */\n label?: ReactNode;\n /** The `appearance` namespace, for this instance. */\n labels?: Partial<AppearanceLabels>;\n};\n\nexport type TextSizeSettingProps = AppearanceToggleProps<TextSize>;\n\n/**\n * The text size (0.32, docs/text-size-harmonization.md §6): Normal, Large, Extra large as\n * one segmented control, in the core `appearance` group after the theme. Bind it to the\n * size IN FORCE — `useAccountAppearance().textSize` — and hand a pick to its\n * `pickTextSize`, which stores the device's choice and, signed in, the account's.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`): at Extra large on a 360 px\n * phone the group has 240 px, and \"Extra large\" cut to \"Extra l…\" would be the one\n * setting that cannot read its own value.\n */\nexport function TextSizeSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: TextSizeSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<TextSize>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.textSize)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={TEXT_SIZES.map((size) => ({ value: size, label: labels.textSizes[size] }))}\n />\n );\n}\n\nexport type ContrastSettingProps = AppearanceToggleProps<ContrastMode>;\n\n/**\n * More contrast (0.32, §5, §6): System, Standard, More as one segmented control, beside\n * the text size. Bind it to the stored MODE, \"system\" included — never to whether more\n * contrast is on at the moment, or a device that asks for more contrast would show\n * \"More\" and could not say \"follow the device\" (the theme's lesson, §8 of the settings\n * contract). `useAccountAppearance().contrast` is that mode.\n */\nexport function ContrastSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: ContrastSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<ContrastMode>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.contrast)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={CONTRAST_MODES.map((mode) => ({ value: mode, label: labels.contrastModes[mode] }))}\n />\n );\n}\n"],"mappings":";AA8EM,cAgBF,YAhBE;AA7EN,SAAS,cAAc;AAEvB,SAAS,mBAAmB;AAG5B,SAAS,kBAAkB;AAE3B,SAAS,sBAAsB;AAE/B,SAAS,2BAA2B;AAgE7B,SAAS,aAAa,OAAoD;AAC/E,MAAI,MAAM,YAAY,UAAU;AAC9B,UAAM,EAAE,SAASA,WAAU,OAAAC,QAAO,UAAAC,WAAU,OAAAC,QAAO,cAAAC,eAAc,WAAW,QAAQ,GAAGC,MAAK,IAAI;AAChG,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAGA;AAAA,QACJ;AAAA,QACA,OAAOF;AAAA,QACP,OAAOF;AAAA,QACP,UAAUC;AAAA,QACV,SAAS;AAAA,UACP,EAAE,OAAO,UAAU,OAAOE,cAAa,OAAO;AAAA,UAC9C,EAAE,OAAO,SAAS,OAAOA,cAAa,MAAM;AAAA,UAC5C,EAAE,OAAO,QAAQ,OAAOA,cAAa,KAAK;AAAA,QAC5C;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,QAAM,EAAE,SAAS,UAAU,OAAO,UAAU,OAAO,cAAc,IAAI,GAAG,KAAK,IAAI;AACjF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB;AAAA,MAE3D;AAAA,4BAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,QAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,QAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA;AAAA;AAAA,EAC1C;AAEJ;AAaO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,IAAI,GAAG,KAAK,GAAyB;AACtG,SACE,oBAAC,UAAQ,GAAG,MAAM,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC3F,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;AAgCO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,UAAU,IAAI,EAAE,EAAE;AAAA;AAAA,EACpF;AAEJ;AAWO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,eAAe,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,cAAc,IAAI,EAAE,EAAE;AAAA;AAAA,EAC5F;AAEJ;","names":["_variant","value","onChange","label","optionLabels","rest"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import './account-chips.js';
|
|
3
3
|
import './dialog-frame.js';
|
|
4
|
-
export { cP as DEFAULT_SHARE_CARD_LABELS, eZ as ShareAddRequest, e_ as ShareCandidate, e$ as ShareCard, f0 as ShareCardLabels, f1 as ShareCardProps, f2 as ShareDialog, f3 as ShareDialogProps, f4 as ShareGrantee, f5 as SharePanel, f6 as SharePanelProps, f7 as SharePendingGrant, f8 as ShareRole } from '../delete-account-setting-
|
|
4
|
+
export { cP as DEFAULT_SHARE_CARD_LABELS, eZ as ShareAddRequest, e_ as ShareCandidate, e$ as ShareCard, f0 as ShareCardLabels, f1 as ShareCardProps, f2 as ShareDialog, f3 as ShareDialogProps, f4 as ShareGrantee, f5 as SharePanel, f6 as SharePanelProps, f7 as SharePendingGrant, f8 as ShareRole } from '../delete-account-setting-FLyJf-kZ.js';
|
|
5
5
|
import 'lucide-react';
|
|
6
6
|
import '../lib/format.js';
|
|
7
7
|
import './chip.js';
|
|
@@ -46,6 +46,7 @@ function ShareRoleChip({ role }) {
|
|
|
46
46
|
const chip = /* @__PURE__ */ jsx(RoleChip, { value: role.key, roles: { [role.key]: role } });
|
|
47
47
|
return role.description ? /* @__PURE__ */ jsx(Tooltip, { label: role.description, children: chip }) : chip;
|
|
48
48
|
}
|
|
49
|
+
const ROW_CONTROLS = "ms-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-1";
|
|
49
50
|
function LockedRoleChoice({ reason, children }) {
|
|
50
51
|
return reason === void 0 ? children : /* @__PURE__ */ jsx(Tooltip, { label: reason, children });
|
|
51
52
|
}
|
|
@@ -81,6 +82,7 @@ function SharePanel({
|
|
|
81
82
|
const roleOf = (key) => roles.find((r) => r.key === key);
|
|
82
83
|
const choosable = roles.length > 1;
|
|
83
84
|
const canAdd = !readOnly && onAdd !== void 0;
|
|
85
|
+
const rowReason = canAdd ? "tooltip" : "auto";
|
|
84
86
|
const run = async (key, action) => {
|
|
85
87
|
setBusy(key);
|
|
86
88
|
setFailure(null);
|
|
@@ -230,7 +232,7 @@ function SharePanel({
|
|
|
230
232
|
] }),
|
|
231
233
|
(g.kind === "team" || g.email) && /* @__PURE__ */ jsx("div", { "data-private": true, className: "truncate text-xs text-[var(--text-muted)]", children: g.kind === "team" ? labels.team : g.email })
|
|
232
234
|
] }),
|
|
233
|
-
(changeable || editable && onRemove) && /* @__PURE__ */ jsxs("div", { className:
|
|
235
|
+
(changeable || editable && onRemove) && /* @__PURE__ */ jsxs("div", { className: ROW_CONTROLS, children: [
|
|
234
236
|
changeable && /* @__PURE__ */ jsx(LockedRoleChoice, { reason: lock.locked ? lock.reason : void 0, children: /* @__PURE__ */ jsx(
|
|
235
237
|
ToggleGroup,
|
|
236
238
|
{
|
|
@@ -252,6 +254,7 @@ function SharePanel({
|
|
|
252
254
|
commit: true,
|
|
253
255
|
label: labels.remove,
|
|
254
256
|
labelVisible: false,
|
|
257
|
+
disabledReasonDisplay: rowReason,
|
|
255
258
|
pending: busy === key,
|
|
256
259
|
disabled: busy !== null,
|
|
257
260
|
onClick: async () => {
|
|
@@ -291,7 +294,7 @@ function SharePanel({
|
|
|
291
294
|
/* @__PURE__ */ jsx(ShareRoleChip, { role: roleOf(p.role) })
|
|
292
295
|
] })
|
|
293
296
|
] }),
|
|
294
|
-
(p.link || !readOnly && onRevokePending) && /* @__PURE__ */ jsxs("div", { className:
|
|
297
|
+
(p.link || !readOnly && onRevokePending) && /* @__PURE__ */ jsxs("div", { className: ROW_CONTROLS, children: [
|
|
295
298
|
p.link && /* @__PURE__ */ jsx(CopyButton, { size: "xs", tone: "muted", text: p.link, label: labels.copyLink }),
|
|
296
299
|
!readOnly && onRevokePending && /* @__PURE__ */ jsx(
|
|
297
300
|
IconButton,
|
|
@@ -301,6 +304,7 @@ function SharePanel({
|
|
|
301
304
|
commit: true,
|
|
302
305
|
label: labels.revokePending,
|
|
303
306
|
labelVisible: false,
|
|
307
|
+
disabledReasonDisplay: rowReason,
|
|
304
308
|
pending: busy === key,
|
|
305
309
|
disabled: busy !== null,
|
|
306
310
|
onClick: async () => {
|