@eifi1/ui-kit 0.51.2 → 0.51.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/components/combobox-core.js +4 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/currency-option-list.js +3 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/data-table-fit.js +7 -5
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-head.js +5 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table.d.ts +8 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/grouped-picker.js +5 -4
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/icon-picker.js +5 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +4 -4
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/legal-labels.js +9 -2
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.d.ts +35 -3
- package/dist/components/legal-page.js +22 -0
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +49 -5
- package/dist/components/legal.js +31 -3
- package/dist/components/legal.js.map +1 -1
- package/dist/components/list-labels.js +3 -1
- package/dist/components/list-labels.js.map +1 -1
- package/dist/components/list.d.ts +18 -1
- package/dist/components/list.js +32 -3
- package/dist/components/list.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-table.d.ts +3 -1
- package/dist/feedback/feedback-table.js +18 -4
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/i18n/german.js +12 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +16 -3
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +12 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +12 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +12 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +12 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +12 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +4 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/text-match.d.ts +16 -0
- package/dist/lib/text-match.js +11 -1
- package/dist/lib/text-match.js.map +1 -1
- package/dist/settings/use-settings-focus.d.ts +7 -0
- package/dist/settings/use-settings-focus.js +2 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +5 -0
- package/dist/shell/dev-menu.js +2 -1
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +14 -1
- package/dist/shell/topbar-action-menu.js +85 -34
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +2 -1
- package/dist/shell.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox-core.tsx +5 -5
- package/src/components/currency-option-list.tsx +4 -5
- package/src/components/data-table-fit.ts +7 -5
- package/src/components/data-table-head.tsx +5 -1
- package/src/components/data-table.tsx +8 -0
- package/src/components/grouped-picker.tsx +15 -8
- package/src/components/icon-picker.tsx +6 -4
- package/src/components/inline-entity-combobox.tsx +5 -4
- package/src/components/legal-labels.ts +8 -1
- package/src/components/legal-page.tsx +72 -2
- package/src/components/legal.tsx +80 -5
- package/src/components/list-labels.ts +2 -0
- package/src/components/list.tsx +52 -2
- package/src/feedback/feedback-table.tsx +17 -1
- package/src/i18n/german.ts +10 -1
- package/src/i18n/locales/en.ts +4 -0
- package/src/i18n/locales/es.ts +10 -1
- package/src/i18n/locales/fr.ts +10 -1
- package/src/i18n/locales/hu.ts +10 -1
- package/src/i18n/locales/it.ts +10 -1
- package/src/i18n/locales/zh.ts +10 -1
- package/src/i18n/review.ts +4 -0
- package/src/index.ts +1 -0
- package/src/lib/text-match.ts +30 -0
- package/src/settings/use-settings-focus.ts +8 -0
- package/src/shell/dev-menu.tsx +6 -0
- package/src/shell/topbar-action-menu.tsx +122 -38
- package/src/shell.ts +1 -1
|
@@ -3,7 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useCallback, useId, useMemo } from "react";
|
|
4
4
|
import { ChevronDown } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
-
import {
|
|
6
|
+
import { matchesSearch, searchWords } from "../lib/text-match.js";
|
|
7
7
|
import { ComboboxPanel, useComboboxCore } from "./combobox-core.js";
|
|
8
8
|
import { assignRef } from "./field-parts.js";
|
|
9
9
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
@@ -31,9 +31,10 @@ function GroupedPicker({
|
|
|
31
31
|
const comboText = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS);
|
|
32
32
|
const locale = useKitLocale();
|
|
33
33
|
const results = useMemo(() => {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
34
|
+
const words = searchWords(query, locale);
|
|
35
|
+
const kept = words.length > 0 ? groups.map(
|
|
36
|
+
(g) => matchesSearch(words, [g.label], locale) ? g : { ...g, items: g.items.filter((i) => matchesSearch(words, [g.label, i.label], locale)) }
|
|
37
|
+
).filter((g) => g.items.length > 0) : groups;
|
|
37
38
|
return kept.flatMap(
|
|
38
39
|
(g) => g.items.map(
|
|
39
40
|
(item) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/grouped-picker.tsx"],"sourcesContent":["import { useCallback, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { ComboboxPanel, useComboboxCore, type ComboOption } from \"./combobox-core\";\nimport { assignRef } from \"./field-parts\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\nexport interface PickerGroup {\n key: string;\n label: string;\n /** `icon` is an optional leading glyph, drawn before the label — the same slot\n * `ComboOption` has always had, so the two pickers can present one option set\n * the same way. Omitted, a row is exactly what it was: label only, with no\n * reserved space, so a list where nothing has an icon does not indent. */\n items: { key: string; label: string; icon?: ReactNode }[];\n}\n\n/**\n * `onSelect` is omitted from the div's own attributes because this component's\n * means \"an item was chosen\", not the DOM's \"text was selected inside me\" — two\n * different signatures under one name, and the kit's meaning is the one three apps\n * are written against.\n */\nexport interface GroupedPickerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSelect\"> {\n groups: PickerGroup[];\n /** Face of the closed picker button. A string also names the trigger when no\n * `aria-label` does. */\n buttonLabel: ReactNode;\n /** Keys of the current selection, if any. */\n selected: { group: string; item: string } | null;\n onSelect: (groupKey: string, itemKey: string) => void;\n /** The filter box's prompt, and its accessible name. Default: `combobox.search` from\n * the {@link UiKitProvider}, else \"Search\". */\n filterPlaceholder?: string;\n /** The TRIGGER — the focusable `<button>` — as on every picker of the kit (MultiSelect,\n * CountrySelect, CurrencySelect), not the wrapper the rest of the props go on (0.34,\n * API-2, refactor audit 2026-10). */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/** An item's value in the list: the group's key and the item's, which together are\n * unique where an item key alone is not (two groups may each have an \"Other\"). */\nconst itemValue = (group: string, item: string) => `${group}\\u0000${item}`;\n\n/** Panel picker for large grouped option sets: instead of one long flat\n * dropdown, the groups sit side by side as columns with their items listed\n * beneath (feedback #267/#276), narrowed by a filter box at the top (feedback\n * #267 rework). A query that matches a group's name keeps the whole group;\n * otherwise groups are reduced to their matching items.\n *\n * On the picker family's foundation since round 0.34 (Wave 2, DUP-4 + BUG-12):\n * `useComboboxCore` + `ComboboxPanel` in its `columns` layout. Its trigger promised a\n * dialog that was no dialog, and the choice showed by colour alone; it is a combobox over\n * a listbox now — one `role=\"group\"` per column, named by its heading, the chosen item\n * `aria-selected` and checked — with the arrow keys, Enter, a named filter box, the live\n * result count, portal anchoring (dev#548) and, on a phone, the full-screen sheet\n * (live #200) in one column. */\nexport function GroupedPicker({\n groups,\n buttonLabel,\n selected,\n onSelect,\n \"aria-label\": ariaLabel,\n filterPlaceholder,\n className,\n ref,\n ...rest\n}: GroupedPickerProps) {\n // `filter: false`: the filter below keeps a whole group when its name matches, so the\n // panel is handed the rows, as CountrySelect's are. Escape is the core's layer on the\n // kit's stack — this kept an Escape handler of its own on its panel (round 0.34 BUG-1),\n // and the stack is the one answer to it now (DUP-1). Back closes the list, as it did on\n // `useDropdown`.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n const { open, setOpen, triggerRef, closeToTrigger, query } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both, as on MultiSelect.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n const comboText = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS);\n\n // Folded as every kit list folds (DUP-5): the locale's case (the Turkish \"İ\" that\n // plain `toLowerCase` gets wrong), and now the accents too — \"zurich\" finds Zürich.\n const locale = useKitLocale();\n const results = useMemo(() => {\n const q = foldForSearch(query.trim(), locale);\n const has = (s: string) => foldForSearch(s, locale).includes(q);\n const kept = q\n ? groups\n .map((g) => (has(g.label) ? g : { ...g, items: g.items.filter((i) => has(i.label)) }))\n .filter((g) => g.items.length > 0)\n : groups;\n return kept.flatMap((g) =>\n g.items.map(\n (item): ComboOption<string> => ({\n value: itemValue(g.key, item.key),\n label: item.label,\n icon: item.icon,\n group: g.label,\n }),\n ),\n );\n }, [groups, query, locale]);\n // Back to keys for `onSelect`, by the value the row carries.\n const keysOf = useMemo(() => {\n const map = new Map<string, [string, string]>();\n for (const g of groups) for (const item of g.items) map.set(itemValue(g.key, item.key), [g.key, item.key]);\n return map;\n }, [groups]);\n const chosen = selected ? itemValue(selected.group, selected.item) : null;\n\n const listboxId = `${useId()}-listbox`;\n // The trigger's NAME: a combobox takes none from its content. The caller's\n // `aria-label`, as on every picker of the kit, else the face when it is text.\n const name = ariaLabel ?? (typeof buttonLabel === \"string\" ? buttonLabel : undefined);\n\n return (\n // `rest` goes on the wrapper, but the accessible NAME does not: this div is a\n // positioning box with no role, so a label parked on it would be announced by\n // nothing. It belongs to the control — the same split every picker in this wave\n // makes, and the reason `aria-label` is destructured out rather than left in\n // `rest`. Spread FIRST so the attributes below, each of them a fix with an\n // incident behind it, cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n <button\n ref={setTrigger}\n type=\"button\"\n // A closed choice that expands into the list below — the role every trigger\n // picker in the kit wears. It said `aria-haspopup=\"dialog\"` over a panel that was\n // no dialog (BUG-12).\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-label={name}\n onClick={() => setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"flex w-full items-center justify-between gap-2 rounded-md border border-strong bg-surface px-2.5 py-1.5 text-sm text-primary hover:bg-hover\"\n >\n {/* The trigger's label and each item wrap from Large up (text size §10.17,\n round 0.34 T1: the sweep read \"Choose a cat…\" at 360 px / Extra large). */}\n <span className=\"truncate-until-large\">{buttonLabel}</span>\n <ChevronDown className=\"size-4 shrink-0 text-placeholder\" />\n </button>\n <ComboboxPanel\n core={{ ...core, results }}\n listboxId={listboxId}\n columns\n // As wide as four columns want (56rem), within the glass, from the trigger's\n // start — the panel it always was.\n width={{ minRem: 56 }}\n searchPlaceholder={filterPlaceholder ?? comboText.search}\n sheetTitle={ariaLabel ?? buttonLabel}\n isSelected={(v) => v === chosen}\n onChoose={(o) => {\n const keys = keysOf.get(o.value);\n if (keys) onSelect(keys[0], keys[1]);\n closeToTrigger();\n }}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/grouped-picker.tsx"],"sourcesContent":["import { useCallback, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { matchesSearch, searchWords } from \"../lib/text-match\";\nimport { ComboboxPanel, useComboboxCore, type ComboOption } from \"./combobox-core\";\nimport { assignRef } from \"./field-parts\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\nexport interface PickerGroup {\n key: string;\n label: string;\n /** `icon` is an optional leading glyph, drawn before the label — the same slot\n * `ComboOption` has always had, so the two pickers can present one option set\n * the same way. Omitted, a row is exactly what it was: label only, with no\n * reserved space, so a list where nothing has an icon does not indent. */\n items: { key: string; label: string; icon?: ReactNode }[];\n}\n\n/**\n * `onSelect` is omitted from the div's own attributes because this component's\n * means \"an item was chosen\", not the DOM's \"text was selected inside me\" — two\n * different signatures under one name, and the kit's meaning is the one three apps\n * are written against.\n */\nexport interface GroupedPickerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSelect\"> {\n groups: PickerGroup[];\n /** Face of the closed picker button. A string also names the trigger when no\n * `aria-label` does. */\n buttonLabel: ReactNode;\n /** Keys of the current selection, if any. */\n selected: { group: string; item: string } | null;\n onSelect: (groupKey: string, itemKey: string) => void;\n /** The filter box's prompt, and its accessible name. Default: `combobox.search` from\n * the {@link UiKitProvider}, else \"Search\". */\n filterPlaceholder?: string;\n /** The TRIGGER — the focusable `<button>` — as on every picker of the kit (MultiSelect,\n * CountrySelect, CurrencySelect), not the wrapper the rest of the props go on (0.34,\n * API-2, refactor audit 2026-10). */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/** An item's value in the list: the group's key and the item's, which together are\n * unique where an item key alone is not (two groups may each have an \"Other\"). */\nconst itemValue = (group: string, item: string) => `${group}\\u0000${item}`;\n\n/** Panel picker for large grouped option sets: instead of one long flat\n * dropdown, the groups sit side by side as columns with their items listed\n * beneath (feedback #267/#276), narrowed by a filter box at the top (feedback\n * #267 rework). A query that matches a group's name keeps the whole group;\n * otherwise groups are reduced to their matching items.\n *\n * On the picker family's foundation since round 0.34 (Wave 2, DUP-4 + BUG-12):\n * `useComboboxCore` + `ComboboxPanel` in its `columns` layout. Its trigger promised a\n * dialog that was no dialog, and the choice showed by colour alone; it is a combobox over\n * a listbox now — one `role=\"group\"` per column, named by its heading, the chosen item\n * `aria-selected` and checked — with the arrow keys, Enter, a named filter box, the live\n * result count, portal anchoring (dev#548) and, on a phone, the full-screen sheet\n * (live #200) in one column. */\nexport function GroupedPicker({\n groups,\n buttonLabel,\n selected,\n onSelect,\n \"aria-label\": ariaLabel,\n filterPlaceholder,\n className,\n ref,\n ...rest\n}: GroupedPickerProps) {\n // `filter: false`: the filter below keeps a whole group when its name matches, so the\n // panel is handed the rows, as CountrySelect's are. Escape is the core's layer on the\n // kit's stack — this kept an Escape handler of its own on its panel (round 0.34 BUG-1),\n // and the stack is the one answer to it now (DUP-1). Back closes the list, as it did on\n // `useDropdown`.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n const { open, setOpen, triggerRef, closeToTrigger, query } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both, as on MultiSelect.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n const comboText = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS);\n\n // Folded as every kit list folds (DUP-5): the locale's case (the Turkish \"İ\" that\n // plain `toLowerCase` gets wrong), and now the accents too — \"zurich\" finds Zürich.\n const locale = useKitLocale();\n const results = useMemo(() => {\n // Every word of the query (0.51.4): all in the group's name keeps the whole group;\n // otherwise an item stays when each word is in its label or its group's name, so\n // \"migros 8004\" finds Migros' 8004 shop.\n const words = searchWords(query, locale);\n const kept =\n words.length > 0\n ? groups\n .map((g) =>\n matchesSearch(words, [g.label], locale)\n ? g\n : { ...g, items: g.items.filter((i) => matchesSearch(words, [g.label, i.label], locale)) },\n )\n .filter((g) => g.items.length > 0)\n : groups;\n return kept.flatMap((g) =>\n g.items.map(\n (item): ComboOption<string> => ({\n value: itemValue(g.key, item.key),\n label: item.label,\n icon: item.icon,\n group: g.label,\n }),\n ),\n );\n }, [groups, query, locale]);\n // Back to keys for `onSelect`, by the value the row carries.\n const keysOf = useMemo(() => {\n const map = new Map<string, [string, string]>();\n for (const g of groups) for (const item of g.items) map.set(itemValue(g.key, item.key), [g.key, item.key]);\n return map;\n }, [groups]);\n const chosen = selected ? itemValue(selected.group, selected.item) : null;\n\n const listboxId = `${useId()}-listbox`;\n // The trigger's NAME: a combobox takes none from its content. The caller's\n // `aria-label`, as on every picker of the kit, else the face when it is text.\n const name = ariaLabel ?? (typeof buttonLabel === \"string\" ? buttonLabel : undefined);\n\n return (\n // `rest` goes on the wrapper, but the accessible NAME does not: this div is a\n // positioning box with no role, so a label parked on it would be announced by\n // nothing. It belongs to the control — the same split every picker in this wave\n // makes, and the reason `aria-label` is destructured out rather than left in\n // `rest`. Spread FIRST so the attributes below, each of them a fix with an\n // incident behind it, cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n <button\n ref={setTrigger}\n type=\"button\"\n // A closed choice that expands into the list below — the role every trigger\n // picker in the kit wears. It said `aria-haspopup=\"dialog\"` over a panel that was\n // no dialog (BUG-12).\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-label={name}\n onClick={() => setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"flex w-full items-center justify-between gap-2 rounded-md border border-strong bg-surface px-2.5 py-1.5 text-sm text-primary hover:bg-hover\"\n >\n {/* The trigger's label and each item wrap from Large up (text size §10.17,\n round 0.34 T1: the sweep read \"Choose a cat…\" at 360 px / Extra large). */}\n <span className=\"truncate-until-large\">{buttonLabel}</span>\n <ChevronDown className=\"size-4 shrink-0 text-placeholder\" />\n </button>\n <ComboboxPanel\n core={{ ...core, results }}\n listboxId={listboxId}\n columns\n // As wide as four columns want (56rem), within the glass, from the trigger's\n // start — the panel it always was.\n width={{ minRem: 56 }}\n searchPlaceholder={filterPlaceholder ?? comboText.search}\n sheetTitle={ariaLabel ?? buttonLabel}\n isSelected={(v) => v === chosen}\n onChoose={(o) => {\n const keys = keysOf.get(o.value);\n if (keys) onSelect(keys[0], keys[1]);\n closeToTrigger();\n }}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,7 +6,7 @@ import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-l
|
|
|
6
6
|
import { SearchField } from "./search-field.js";
|
|
7
7
|
import { TILE_SIZE, TileRadioGroup } from "./tile-radio.js";
|
|
8
8
|
import { hasContent } from "../lib/presence.js";
|
|
9
|
-
import {
|
|
9
|
+
import { matchesSearch, searchWords } from "../lib/text-match.js";
|
|
10
10
|
import { LabelStrip, useFieldMessages } from "./field-parts.js";
|
|
11
11
|
import { DEFAULT_ICON_PICKER_LABELS } from "./icon-picker-labels.js";
|
|
12
12
|
function IconPicker({
|
|
@@ -44,12 +44,13 @@ function IconPicker({
|
|
|
44
44
|
reasonId: mixed && mixedId
|
|
45
45
|
});
|
|
46
46
|
const [query, setQuery] = useState("");
|
|
47
|
-
const
|
|
47
|
+
const words = useMemo(() => searchWords(query, locale), [query, locale]);
|
|
48
|
+
const needle = words.length > 0;
|
|
48
49
|
const visible = useMemo(
|
|
49
50
|
() => needle ? options.filter(
|
|
50
|
-
(o) => [o.label, ...o.keywords ?? []]
|
|
51
|
+
(o) => matchesSearch(words, [o.label, ...o.keywords ?? []], locale)
|
|
51
52
|
) : options,
|
|
52
|
-
[options, needle, locale]
|
|
53
|
+
[options, needle, words, locale]
|
|
53
54
|
);
|
|
54
55
|
const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
|
|
55
56
|
const items = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/icon-picker.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type { ComponentPropsWithRef, ComponentType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { SearchField } from \"./search-field\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { hasContent } from \"../lib/presence\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { LabelStrip, useFieldMessages, type FieldHelpProps } from \"./field-parts\";\nimport { DEFAULT_ICON_PICKER_LABELS } from \"./icon-picker-labels\";\n\nexport interface IconPickerLabels {\n /** The \"no icon\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n /** The search box's accessible name and placeholder, when `searchable` is set. */\n search: string;\n /** Shown in place of the tiles when the search matches nothing. */\n noResults: string;\n /** Announced as the search narrows the grid. Receives the number of matches. */\n resultCount: (count: number) => string;\n}\n\n// Defined in ./icon-picker-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ICON_PICKER_LABELS };\n\nexport interface IconOption<T extends string> {\n value: T;\n /** A Lucide icon, or any component that takes a `className`. Rendered `aria-hidden`\n * — the tile's name is `label`. */\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** What the symbol IS, in the user's language (\"Game controller\") — not the icon\n * library's identifier (`Gamepad2`), which is nobody's word for anything. */\n label: string;\n /** More words the search matches (\"games\", \"play\"), besides `label`. */\n keywords?: string[];\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Leisure\"). The tile wears a dot while it has one. */\n note?: string;\n /** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a\n * `disabledReason` the tile stays reachable instead and says why. */\n disabled?: boolean;\n /**\n * Why THIS symbol cannot be chosen right now — {@link SwatchOption.disabledReason}'s\n * twin: the tile is `aria-disabled` but reachable by Tab and the arrow keys, the reason\n * shows in its bubble under its name and describes it, and choosing it does nothing.\n * Wins over `disabled`. Kept through a search like any other tile.\n */\n disabledReason?: ReactNode;\n}\n\nexport interface IconPickerProps<T extends string>\n extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\">,\n FieldHelpProps {\n options: IconOption<T>[];\n /** The chosen icon, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no icon\" tile whose value is `null`. */\n allowNone?: boolean;\n /** See {@link SwatchPickerProps.mixed}: nothing is checked, and the group is\n * described as mixed. */\n mixed?: boolean;\n /**\n * \"automatic\" (default): an arrow key moves AND chooses, the APG radio group.\n * \"manual\": arrows move focus only; Space or Enter chooses. For a picker whose every\n * change is expensive — a bulk edit that saves as it goes.\n */\n activation?: \"automatic\" | \"manual\";\n /** A search box above the grid, matching `label` and `keywords`. For a set large\n * enough that scanning it is slower than typing. */\n searchable?: boolean;\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: LabelOverride<IconPickerLabels>;\n /**\n * An 11px static label in a strip over the picker, naming the grid — keksdose K2,\n * {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row\n * beside labelled fields, with its label on their line. With `searchable` the strip\n * is over the search box too, which keeps its own name. With a label, `className`\n * styles the wrapper.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. {@link Select}'s rule since\n * 0.34 (DUP-2, refactor audit 2026-10): plain TEXT, and a FieldHint at Large and on a\n * touch screen (§4), is a caption under the grid, describing it. */\n hint?: ReactNode;\n}\n\n/**\n * A grid of symbols that is one radio group, with an optional search.\n *\n * Name the group with `aria-label` or `aria-labelledby`. The name lands on the\n * radiogroup itself, not on the outer box: with `searchable` the outer box also holds\n * the search field, which has a name of its own.\n *\n * Keyboard is {@link SwatchPicker}'s: one tab stop, arrows to move (flipped in a\n * right-to-left page), Home and End.\n */\nexport function IconPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n searchable = false,\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n help,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: IconPickerProps<T>) {\n const text = useKitLabels(\"iconPicker\", DEFAULT_ICON_PICKER_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n const mixedId = useId();\n const labelId = useId();\n const labelled = hasContent(label);\n // The label line's \"?\", or the caption under the grid (DUP-2). Only with a label, the\n // help's \"?\" too (0.36).\n const messages = useFieldMessages({\n hint: labelled ? hint : undefined,\n help: labelled ? help : undefined,\n name: label,\n describedBy: ariaDescribedBy,\n reasonId: mixed && mixedId,\n });\n const [query, setQuery] = useState(\"\");\n // Case- and accent-insensitive, so \"cafe\" finds \"Café\" — every kit list's fold (DUP-5).\n const needle = foldForSearch(query.trim(), locale);\n\n const visible = useMemo(\n () =>\n needle\n ? options.filter((o) =>\n [o.label, ...(o.keywords ?? [])].some((w) => foldForSearch(w, locale).includes(needle)),\n )\n : options,\n [options, needle, locale],\n );\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n // \"None\" stays through a search: it is the way back to no icon, and it matches\n // no word anybody would type for it.\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...visible.map((o) => ({\n key: o.value,\n label: o.label,\n note: o.note,\n disabled: o.disabled,\n disabledReason: o.disabledReason,\n })),\n ];\n\n const picker = (\n <div {...rest} className={cn(\"space-y-2\", !labelled && className)}>\n {searchable && (\n <SearchField\n value={query}\n onChange={setQuery}\n aria-label={text.search}\n clearLabel={common.clear}\n disabled={disabled}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n // The strip's label names the GRID, unless the caller named it already.\n aria-labelledby={labelled && ariaLabel === undefined && ariaLabelledBy === undefined ? labelId : ariaLabelledBy}\n aria-describedby={messages.describedBy}\n aria-disabled={disabled || undefined}\n className=\"flex flex-wrap items-center gap-1.5\"\n >\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked tile is refused by the tiles themselves; asked again here so a\n // reason can never become a change, whatever the tiles do.\n onSelect={(next) => {\n if (!hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);\n }}\n activation={activation}\n disabled={disabled}\n size={size}\n renderTile={(item) => {\n const Icon = item.key === null ? undefined : byValue.get(item.key)?.icon;\n return Icon ? <Icon aria-hidden className={TILE_SIZE[size].glyph} /> : null;\n }}\n tileClassName={(_item, selected) =>\n cn(\n selected ? \"text-primary\" : \"text-secondary\",\n tileClassName,\n )\n }\n />\n </div>\n {needle && visible.length === 0 && (\n <p className=\"text-xs text-muted\">{text.noResults}</p>\n )}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n {searchable && (\n // Typing moves no focus and the grid changes silently, so the count is said.\n // `sr-only-fixed`: this sits inside a consumer's markup and must not extend\n // the document height (see tokens.css).\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {needle ? text.resultCount(visible.length) : \"\"}\n </span>\n )}\n </div>\n );\n if (!labelled) return picker;\n return (\n <LabelStrip labelId={labelId} label={label} hint={messages.labelHint} disabled={disabled} className={className}>\n {picker}\n {messages.below}\n </LabelStrip>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/icon-picker.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type { ComponentPropsWithRef, ComponentType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { SearchField } from \"./search-field\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { hasContent } from \"../lib/presence\";\nimport { matchesSearch, searchWords } from \"../lib/text-match\";\nimport { LabelStrip, useFieldMessages, type FieldHelpProps } from \"./field-parts\";\nimport { DEFAULT_ICON_PICKER_LABELS } from \"./icon-picker-labels\";\n\nexport interface IconPickerLabels {\n /** The \"no icon\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n /** The search box's accessible name and placeholder, when `searchable` is set. */\n search: string;\n /** Shown in place of the tiles when the search matches nothing. */\n noResults: string;\n /** Announced as the search narrows the grid. Receives the number of matches. */\n resultCount: (count: number) => string;\n}\n\n// Defined in ./icon-picker-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ICON_PICKER_LABELS };\n\nexport interface IconOption<T extends string> {\n value: T;\n /** A Lucide icon, or any component that takes a `className`. Rendered `aria-hidden`\n * — the tile's name is `label`. */\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** What the symbol IS, in the user's language (\"Game controller\") — not the icon\n * library's identifier (`Gamepad2`), which is nobody's word for anything. */\n label: string;\n /** More words the search matches (\"games\", \"play\"), besides `label`. */\n keywords?: string[];\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Leisure\"). The tile wears a dot while it has one. */\n note?: string;\n /** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a\n * `disabledReason` the tile stays reachable instead and says why. */\n disabled?: boolean;\n /**\n * Why THIS symbol cannot be chosen right now — {@link SwatchOption.disabledReason}'s\n * twin: the tile is `aria-disabled` but reachable by Tab and the arrow keys, the reason\n * shows in its bubble under its name and describes it, and choosing it does nothing.\n * Wins over `disabled`. Kept through a search like any other tile.\n */\n disabledReason?: ReactNode;\n}\n\nexport interface IconPickerProps<T extends string>\n extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\">,\n FieldHelpProps {\n options: IconOption<T>[];\n /** The chosen icon, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no icon\" tile whose value is `null`. */\n allowNone?: boolean;\n /** See {@link SwatchPickerProps.mixed}: nothing is checked, and the group is\n * described as mixed. */\n mixed?: boolean;\n /**\n * \"automatic\" (default): an arrow key moves AND chooses, the APG radio group.\n * \"manual\": arrows move focus only; Space or Enter chooses. For a picker whose every\n * change is expensive — a bulk edit that saves as it goes.\n */\n activation?: \"automatic\" | \"manual\";\n /** A search box above the grid, matching `label` and `keywords`. For a set large\n * enough that scanning it is slower than typing. */\n searchable?: boolean;\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: LabelOverride<IconPickerLabels>;\n /**\n * An 11px static label in a strip over the picker, naming the grid — keksdose K2,\n * {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row\n * beside labelled fields, with its label on their line. With `searchable` the strip\n * is over the search box too, which keeps its own name. With a label, `className`\n * styles the wrapper.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. {@link Select}'s rule since\n * 0.34 (DUP-2, refactor audit 2026-10): plain TEXT, and a FieldHint at Large and on a\n * touch screen (§4), is a caption under the grid, describing it. */\n hint?: ReactNode;\n}\n\n/**\n * A grid of symbols that is one radio group, with an optional search.\n *\n * Name the group with `aria-label` or `aria-labelledby`. The name lands on the\n * radiogroup itself, not on the outer box: with `searchable` the outer box also holds\n * the search field, which has a name of its own.\n *\n * Keyboard is {@link SwatchPicker}'s: one tab stop, arrows to move (flipped in a\n * right-to-left page), Home and End.\n */\nexport function IconPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n searchable = false,\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n help,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: IconPickerProps<T>) {\n const text = useKitLabels(\"iconPicker\", DEFAULT_ICON_PICKER_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n const mixedId = useId();\n const labelId = useId();\n const labelled = hasContent(label);\n // The label line's \"?\", or the caption under the grid (DUP-2). Only with a label, the\n // help's \"?\" too (0.36).\n const messages = useFieldMessages({\n hint: labelled ? hint : undefined,\n help: labelled ? help : undefined,\n name: label,\n describedBy: ariaDescribedBy,\n reasonId: mixed && mixedId,\n });\n const [query, setQuery] = useState(\"\");\n // Case- and accent-insensitive, so \"cafe\" finds \"Café\" — every kit list's fold (DUP-5).\n // Every word of the query, in the label or a keyword (0.51.4).\n const words = useMemo(() => searchWords(query, locale), [query, locale]);\n const needle = words.length > 0;\n\n const visible = useMemo(\n () =>\n needle\n ? options.filter((o) =>\n matchesSearch(words, [o.label, ...(o.keywords ?? [])], locale),\n )\n : options,\n [options, needle, words, locale],\n );\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n // \"None\" stays through a search: it is the way back to no icon, and it matches\n // no word anybody would type for it.\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...visible.map((o) => ({\n key: o.value,\n label: o.label,\n note: o.note,\n disabled: o.disabled,\n disabledReason: o.disabledReason,\n })),\n ];\n\n const picker = (\n <div {...rest} className={cn(\"space-y-2\", !labelled && className)}>\n {searchable && (\n <SearchField\n value={query}\n onChange={setQuery}\n aria-label={text.search}\n clearLabel={common.clear}\n disabled={disabled}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n // The strip's label names the GRID, unless the caller named it already.\n aria-labelledby={labelled && ariaLabel === undefined && ariaLabelledBy === undefined ? labelId : ariaLabelledBy}\n aria-describedby={messages.describedBy}\n aria-disabled={disabled || undefined}\n className=\"flex flex-wrap items-center gap-1.5\"\n >\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked tile is refused by the tiles themselves; asked again here so a\n // reason can never become a change, whatever the tiles do.\n onSelect={(next) => {\n if (!hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);\n }}\n activation={activation}\n disabled={disabled}\n size={size}\n renderTile={(item) => {\n const Icon = item.key === null ? undefined : byValue.get(item.key)?.icon;\n return Icon ? <Icon aria-hidden className={TILE_SIZE[size].glyph} /> : null;\n }}\n tileClassName={(_item, selected) =>\n cn(\n selected ? \"text-primary\" : \"text-secondary\",\n tileClassName,\n )\n }\n />\n </div>\n {needle && visible.length === 0 && (\n <p className=\"text-xs text-muted\">{text.noResults}</p>\n )}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n {searchable && (\n // Typing moves no focus and the grid changes silently, so the count is said.\n // `sr-only-fixed`: this sits inside a consumer's markup and must not extend\n // the document height (see tokens.css).\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {needle ? text.resultCount(visible.length) : \"\"}\n </span>\n )}\n </div>\n );\n if (!labelled) return picker;\n return (\n <LabelStrip labelId={labelId} label={label} hint={messages.labelHint} disabled={disabled} className={className}>\n {picker}\n {messages.below}\n </LabelStrip>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
useActiveOptionScroll
|
|
22
22
|
} from "./combobox-core.js";
|
|
23
23
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
24
|
-
import {
|
|
24
|
+
import { matchesSearch, rankMatch, searchWords } from "../lib/text-match.js";
|
|
25
25
|
import {
|
|
26
26
|
EndHintRow,
|
|
27
27
|
FieldCaption,
|
|
@@ -119,14 +119,14 @@ function InlineEntityCombobox(props) {
|
|
|
119
119
|
const matches = useMemo(() => {
|
|
120
120
|
let ranked = options;
|
|
121
121
|
if (query) {
|
|
122
|
-
const
|
|
123
|
-
const hit = (
|
|
122
|
+
const words = searchWords(query, locale);
|
|
123
|
+
const hit = (o) => matchesSearch(words, [o.label, o.sublabel], locale);
|
|
124
124
|
const leads = (o) => {
|
|
125
125
|
const rank = rankMatch(o.label, query, { locale });
|
|
126
126
|
return rank === 0 || rank === 1;
|
|
127
127
|
};
|
|
128
128
|
const starts = options.filter(leads);
|
|
129
|
-
const rest2 = options.filter((o) => !leads(o) &&
|
|
129
|
+
const rest2 = options.filter((o) => !leads(o) && hit(o));
|
|
130
130
|
ranked = [...starts, ...rest2];
|
|
131
131
|
}
|
|
132
132
|
if (!ranked.some((o) => o.group)) return ranked;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { foldForSearch, rankMatch } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\">,\n FieldHelpProps {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a bound field's label;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n help,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n const q = foldForSearch(query, locale);\n const hit = (s: string | undefined) => s !== undefined && foldForSearch(s, locale).includes(q);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && (hit(o.label) || hit(o.sublabel)));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate\n ? [...matches, { disabled: createLocked }]\n : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter(\n (o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),\n );\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">\n {createReason}\n </span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(\n SHEET_ROW_CLASS,\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ms-2 text-xs text-placeholder\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { matchesSearch, rankMatch, searchWords } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\">,\n FieldHelpProps {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a bound field's label;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n help,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n // Every word of the query, in the label or the sublabel (0.51.4).\n const words = searchWords(query, locale);\n const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && hit(o));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate\n ? [...matches, { disabled: createLocked }]\n : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter(\n (o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),\n );\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">\n {createReason}\n </span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(\n SHEET_ROW_CLASS,\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ms-2 text-xs text-placeholder\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
|
|
@@ -4,7 +4,9 @@ const DEFAULT_LEGAL_LABELS = {
|
|
|
4
4
|
links: {
|
|
5
5
|
impressum: "Imprint",
|
|
6
6
|
privacy: "Privacy Policy",
|
|
7
|
-
terms: "Terms"
|
|
7
|
+
terms: "Terms",
|
|
8
|
+
security: "Security",
|
|
9
|
+
accessibility: "Accessibility"
|
|
8
10
|
},
|
|
9
11
|
titles: {
|
|
10
12
|
impressum: "Imprint",
|
|
@@ -21,6 +23,11 @@ const DEFAULT_LEGAL_LABELS = {
|
|
|
21
23
|
note: (o) => `This is a translation for your convenience. The ${o.bindingLanguage} version is binding.`,
|
|
22
24
|
show: (o) => `Show the ${o.bindingLanguage} version`
|
|
23
25
|
},
|
|
26
|
+
updated: (date) => `Last updated: ${date}`,
|
|
27
|
+
summary: {
|
|
28
|
+
title: "In short",
|
|
29
|
+
note: "This summary is for orientation; only the full text is binding."
|
|
30
|
+
},
|
|
24
31
|
sections: {
|
|
25
32
|
impressum: {
|
|
26
33
|
operator: {
|
|
@@ -81,7 +88,7 @@ The full postal address is supplied on request to data subjects and supervisory
|
|
|
81
88
|
},
|
|
82
89
|
changes: {
|
|
83
90
|
title: "Changes to these terms",
|
|
84
|
-
body: "
|
|
91
|
+
body: "We may update these terms as the service evolves. We announce a material change at least 30 days before it takes effect, by email and in the app, together with the new text. If you do not agree, you can object or delete your account before that date, and you can export your data until then. If you keep using the service after that date, the changed terms apply. Changes that only benefit you, or that the law requires, apply without this notice and cannot be objected to. If you object to a change, we may end your account on the date it takes effect. If you have paid for a period that runs beyond that date, we refund the unused part."
|
|
85
92
|
},
|
|
86
93
|
language: {
|
|
87
94
|
title: "Language",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/legal-labels.ts"],"sourcesContent":["// The English words of ./legal, in a module with no component code: the English catalogue\n// (src/i18n/defaults.ts) imports them without the component, so `en` loads as one chunk\n// like the other languages (0.34, KS U8). ./legal re-exports them.\n\nimport type { LegalLabels } from \"./legal\";\n\nexport const DEFAULT_LEGAL_LABELS: LegalLabels = {\n navLabel: \"Legal\",\n links: {\n impressum: \"Imprint\",\n privacy: \"Privacy Policy\",\n terms: \"Terms\",\n },\n titles: {\n impressum: \"Imprint\",\n privacy: \"Privacy Policy\",\n terms: \"Terms of Service\",\n },\n backHome: \"Back to home\",\n accept: \"I accept the {terms} and the {privacy}\",\n notice: {\n beta: \"Closed beta. These texts have not been reviewed by a lawyer yet and will be before a public launch.\",\n privacy:\n \"Closed beta. The legal wording below has not been reviewed by a lawyer yet and will be before a public launch. The technical descriptions — what is stored, where, and who can read it — describe what the software actually does today, and are meant to be checked against it.\",\n },\n translation: {\n note: (o) =>\n `This is a translation for your convenience. The ${o.bindingLanguage} version is binding.`,\n show: (o) => `Show the ${o.bindingLanguage} version`,\n },\n sections: {\n impressum: {\n operator: {\n title: \"Operator\",\n body: (o) =>\n `${o.name}\\n${o.postalCode} ${o.city}\\n${o.country}\\n\\nThe full postal address is supplied on request to anyone with a legitimate legal interest; write to the contact address below.`,\n },\n contact: {\n title: \"Contact\",\n body: (o) => `Email: ${o.email}`,\n },\n disclaimer: {\n title: \"Liability for content and links\",\n body: \"This is a private, non-commercial project offered during a closed beta, without warranty. External sites we link to are the responsibility of their respective operators; we have no control over their content.\",\n commercial:\n \"This is a commercial service. Paid plans are sold by our reseller, which acts as the Merchant of Record: it sells the subscription in its own name, takes the payment, issues the invoice and handles VAT, and its terms of sale apply to the purchase. We take care that the content of this service is accurate, but cannot guarantee that it is complete or up to date. External sites we link to are the responsibility of their respective operators; we have no control over their content.\",\n },\n },\n privacy: {\n controller: {\n title: \"Controller\",\n body: (o) =>\n `The party responsible for processing personal data in this service is:\\n${o.name}\\n${o.postalCode} ${o.city}, ${o.country}\\nEmail: ${o.email}\\n\\nThe full postal address is supplied on request to data subjects and supervisory authorities.`,\n },\n legal_basis: {\n title: \"Legal basis\",\n body: \"As the operator is based in Switzerland, processing is governed by the Swiss Federal Act on Data Protection (FADP). Where the EU General Data Protection Regulation (GDPR) applies to you, we rely on the performance of a contract to provide the service (Art. 6(1)(b) GDPR) and our legitimate interest in operating and securing it (Art. 6(1)(f) GDPR).\",\n },\n browser: {\n title: \"What your browser stores\",\n lead: \"No cookies. The app keeps the following in your browser's storage:\",\n tail: \"None of it is used to track you. Entries waiting to be sent are sent to us as soon as they can be; everything else stays on your device. All of it disappears when you clear the site's data, and signing out removes the sign-in tokens.\",\n },\n rights: {\n title: \"Your rights\",\n lead: \"You have the right to access, rectification, erasure, restriction, data portability and objection.\",\n tail: \"If you are in Switzerland you may contact the Federal Data Protection and Information Commissioner (FDPIC); if you are in the EU you may lodge a complaint with your local supervisory authority.\",\n },\n contact: {\n title: \"Contact for data protection\",\n body: (o) => `For any privacy request, contact: ${o.email}`,\n },\n },\n terms: {\n warranty: {\n title: \"No warranty\",\n body: 'The service is provided \"as is\" and \"as available\", without warranties of any kind to the extent permitted by law. As a beta it may contain errors, change, or be interrupted at any time — keep your own backups of important data.',\n },\n liability: {\n title: \"Limitation of liability\",\n body: \"To the extent permitted by applicable law, the operator is not liable for any indirect or consequential damage arising from the use of, or inability to use, the service. Nothing here limits liability that cannot be limited by law.\",\n },\n changes: {\n title: \"Changes to these terms\",\n body: \"
|
|
1
|
+
{"version":3,"sources":["../../src/components/legal-labels.ts"],"sourcesContent":["// The English words of ./legal, in a module with no component code: the English catalogue\n// (src/i18n/defaults.ts) imports them without the component, so `en` loads as one chunk\n// like the other languages (0.34, KS U8). ./legal re-exports them.\n\nimport type { LegalLabels } from \"./legal\";\n\nexport const DEFAULT_LEGAL_LABELS: LegalLabels = {\n navLabel: \"Legal\",\n links: {\n impressum: \"Imprint\",\n privacy: \"Privacy Policy\",\n terms: \"Terms\",\n security: \"Security\",\n accessibility: \"Accessibility\",\n },\n titles: {\n impressum: \"Imprint\",\n privacy: \"Privacy Policy\",\n terms: \"Terms of Service\",\n },\n backHome: \"Back to home\",\n accept: \"I accept the {terms} and the {privacy}\",\n notice: {\n beta: \"Closed beta. These texts have not been reviewed by a lawyer yet and will be before a public launch.\",\n privacy:\n \"Closed beta. The legal wording below has not been reviewed by a lawyer yet and will be before a public launch. The technical descriptions — what is stored, where, and who can read it — describe what the software actually does today, and are meant to be checked against it.\",\n },\n translation: {\n note: (o) =>\n `This is a translation for your convenience. The ${o.bindingLanguage} version is binding.`,\n show: (o) => `Show the ${o.bindingLanguage} version`,\n },\n updated: (date) => `Last updated: ${date}`,\n summary: {\n title: \"In short\",\n note: \"This summary is for orientation; only the full text is binding.\",\n },\n sections: {\n impressum: {\n operator: {\n title: \"Operator\",\n body: (o) =>\n `${o.name}\\n${o.postalCode} ${o.city}\\n${o.country}\\n\\nThe full postal address is supplied on request to anyone with a legitimate legal interest; write to the contact address below.`,\n },\n contact: {\n title: \"Contact\",\n body: (o) => `Email: ${o.email}`,\n },\n disclaimer: {\n title: \"Liability for content and links\",\n body: \"This is a private, non-commercial project offered during a closed beta, without warranty. External sites we link to are the responsibility of their respective operators; we have no control over their content.\",\n commercial:\n \"This is a commercial service. Paid plans are sold by our reseller, which acts as the Merchant of Record: it sells the subscription in its own name, takes the payment, issues the invoice and handles VAT, and its terms of sale apply to the purchase. We take care that the content of this service is accurate, but cannot guarantee that it is complete or up to date. External sites we link to are the responsibility of their respective operators; we have no control over their content.\",\n },\n },\n privacy: {\n controller: {\n title: \"Controller\",\n body: (o) =>\n `The party responsible for processing personal data in this service is:\\n${o.name}\\n${o.postalCode} ${o.city}, ${o.country}\\nEmail: ${o.email}\\n\\nThe full postal address is supplied on request to data subjects and supervisory authorities.`,\n },\n legal_basis: {\n title: \"Legal basis\",\n body: \"As the operator is based in Switzerland, processing is governed by the Swiss Federal Act on Data Protection (FADP). Where the EU General Data Protection Regulation (GDPR) applies to you, we rely on the performance of a contract to provide the service (Art. 6(1)(b) GDPR) and our legitimate interest in operating and securing it (Art. 6(1)(f) GDPR).\",\n },\n browser: {\n title: \"What your browser stores\",\n lead: \"No cookies. The app keeps the following in your browser's storage:\",\n tail: \"None of it is used to track you. Entries waiting to be sent are sent to us as soon as they can be; everything else stays on your device. All of it disappears when you clear the site's data, and signing out removes the sign-in tokens.\",\n },\n rights: {\n title: \"Your rights\",\n lead: \"You have the right to access, rectification, erasure, restriction, data portability and objection.\",\n tail: \"If you are in Switzerland you may contact the Federal Data Protection and Information Commissioner (FDPIC); if you are in the EU you may lodge a complaint with your local supervisory authority.\",\n },\n contact: {\n title: \"Contact for data protection\",\n body: (o) => `For any privacy request, contact: ${o.email}`,\n },\n },\n terms: {\n warranty: {\n title: \"No warranty\",\n body: 'The service is provided \"as is\" and \"as available\", without warranties of any kind to the extent permitted by law. As a beta it may contain errors, change, or be interrupted at any time — keep your own backups of important data.',\n },\n liability: {\n title: \"Limitation of liability\",\n body: \"To the extent permitted by applicable law, the operator is not liable for any indirect or consequential damage arising from the use of, or inability to use, the service. Nothing here limits liability that cannot be limited by law.\",\n },\n changes: {\n title: \"Changes to these terms\",\n body: \"We may update these terms as the service evolves. We announce a material change at least 30 days before it takes effect, by email and in the app, together with the new text. If you do not agree, you can object or delete your account before that date, and you can export your data until then. If you keep using the service after that date, the changed terms apply. Changes that only benefit you, or that the law requires, apply without this notice and cannot be objected to. If you object to a change, we may end your account on the date it takes effect. If you have paid for a period that runs beyond that date, we refund the unused part.\",\n },\n language: {\n title: \"Language\",\n body: (o) =>\n `These terms are written in ${o.bindingLanguage}. Translations into other languages are provided for convenience only; where a translation differs, the ${o.bindingLanguage} version prevails.`,\n },\n law: {\n title: \"Governing law\",\n body: (o) =>\n `These terms are governed by Swiss law, excluding its conflict-of-laws rules. As far as legally permissible, the place of jurisdiction is ${o.city} (${o.region}), ${o.country}.`,\n },\n },\n },\n};\n"],"mappings":";AAMO,MAAM,uBAAoC;AAAA,EAC/C,UAAU;AAAA,EACV,OAAO;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,QAAQ;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,SACE;AAAA,EACJ;AAAA,EACA,aAAa;AAAA,IACX,MAAM,CAAC,MACL,mDAAmD,EAAE,eAAe;AAAA,IACtE,MAAM,CAAC,MAAM,YAAY,EAAE,eAAe;AAAA,EAC5C;AAAA,EACA,SAAS,CAAC,SAAS,iBAAiB,IAAI;AAAA,EACxC,SAAS;AAAA,IACP,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,UAAU;AAAA,IACR,WAAW;AAAA,MACT,UAAU;AAAA,QACR,OAAO;AAAA,QACP,MAAM,CAAC,MACL,GAAG,EAAE,IAAI;AAAA,EAAK,EAAE,UAAU,IAAI,EAAE,IAAI;AAAA,EAAK,EAAE,OAAO;AAAA;AAAA;AAAA,MACtD;AAAA,MACA,SAAS;AAAA,QACP,OAAO;AAAA,QACP,MAAM,CAAC,MAAM,UAAU,EAAE,KAAK;AAAA,MAChC;AAAA,MACA,YAAY;AAAA,QACV,OAAO;AAAA,QACP,MAAM;AAAA,QACN,YACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,YAAY;AAAA,QACV,OAAO;AAAA,QACP,MAAM,CAAC,MACL;AAAA,EAA2E,EAAE,IAAI;AAAA,EAAK,EAAE,UAAU,IAAI,EAAE,IAAI,KAAK,EAAE,OAAO;AAAA,SAAY,EAAE,KAAK;AAAA;AAAA;AAAA,MACjJ;AAAA,MACA,aAAa;AAAA,QACX,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,SAAS;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA,QAAQ;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA,SAAS;AAAA,QACP,OAAO;AAAA,QACP,MAAM,CAAC,MAAM,qCAAqC,EAAE,KAAK;AAAA,MAC3D;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL,UAAU;AAAA,QACR,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,WAAW;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,SAAS;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,UAAU;AAAA,QACR,OAAO;AAAA,QACP,MAAM,CAAC,MACL,8BAA8B,EAAE,eAAe,2GAA2G,EAAE,eAAe;AAAA,MAC/K;AAAA,MACA,KAAK;AAAA,QACH,OAAO;AAAA,QACP,MAAM,CAAC,MACL,4IAA4I,EAAE,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE,OAAO;AAAA,MAClL;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -138,6 +138,34 @@ export interface LegalPageProps {
|
|
|
138
138
|
/** Default: the kit's beta notice on the imprint and the terms, its privacy notice on
|
|
139
139
|
* the privacy policy (§3.2). `null` for none. */
|
|
140
140
|
notice?: ReactNode;
|
|
141
|
+
/**
|
|
142
|
+
* 0.51.3: the day this page's text last changed, "YYYY-MM-DD" (legal benchmark
|
|
143
|
+
* 2026-10-29, gap 8). Shown under the title as `legal.updated` — "Last updated:
|
|
144
|
+
* 29 October 2026", "Stand: 29. Oktober 2026" — with the date written out in `locale`,
|
|
145
|
+
* so it follows the page's language like every other kit word, the switch to the
|
|
146
|
+
* binding version included. Left out, no line. One date per page: the imprint, the
|
|
147
|
+
* privacy policy and the terms change on their own days.
|
|
148
|
+
*
|
|
149
|
+
* An app that records which terms someone accepted (keksdose) stores the terms page's
|
|
150
|
+
* date: it is the version the reader saw.
|
|
151
|
+
*
|
|
152
|
+
* The kit's sections are part of the page, so the date moves with their words too: a
|
|
153
|
+
* kit release that changes them says so in its adoption note ("Legal wording"), and
|
|
154
|
+
* the app sets the page's date to the day it ships that bump (§2, decision 10).
|
|
155
|
+
*/
|
|
156
|
+
updated?: string;
|
|
157
|
+
/**
|
|
158
|
+
* 0.51.4: a plain-language summary before the first section (legal benchmark, Marcel
|
|
159
|
+
* 2026-10-29) — the app's three to six points, each a sentence. The kit draws them as a
|
|
160
|
+
* list under `legal.summary.title` ("In short", "Das Wichtigste in Kürze") and adds
|
|
161
|
+
* `legal.summary.note`: the summary orients, only the full text binds. Worded without
|
|
162
|
+
* "below", because on a translated page the text below is a translation too.
|
|
163
|
+
*
|
|
164
|
+
* A string point is running text with its `{ref:key}` resolved, as in a section:
|
|
165
|
+
* "You can delete your account at any time (section {ref:ending})." Unnumbered, with
|
|
166
|
+
* the id `summary`; the sections' numbers do not count it. Left out or empty, nothing.
|
|
167
|
+
*/
|
|
168
|
+
summary?: readonly ReactNode[];
|
|
141
169
|
/**
|
|
142
170
|
* 0.28.1: switches the app to the operator's `bindingLanguage` — the button after the
|
|
143
171
|
* translation note on a page read in another language. Without it the note stands
|
|
@@ -172,6 +200,8 @@ export interface LegalPageProps {
|
|
|
172
200
|
*
|
|
173
201
|
* What it adds to the column:
|
|
174
202
|
* - the title, from `legal.titles`, and "← Back to home" pointing at `homeHref`;
|
|
203
|
+
* - the as-of line under the title, from `updated` (0.51.3);
|
|
204
|
+
* - the plain-language summary before the sections, from `summary` (0.51.4);
|
|
175
205
|
* - the default notice per page — the beta notice, or the privacy notice on the privacy
|
|
176
206
|
* policy, whose technical sections describe running code (§3.2, §7.3);
|
|
177
207
|
* - numbers: the privacy and terms sections are "N. Title" in render order, the
|
|
@@ -184,7 +214,7 @@ export interface LegalPageProps {
|
|
|
184
214
|
* (see `children` for what that asks of the tree);
|
|
185
215
|
* - `useNoIndex()`.
|
|
186
216
|
*/
|
|
187
|
-
export declare function LegalPage({ page, operator, children, header, footer, homeHref, notice, onShowBindingLanguage, locale: localeProp, headingAs, landmark, noIndex, className, labels: labelsProp, }: LegalPageProps): import("react").JSX.Element;
|
|
217
|
+
export declare function LegalPage({ page, operator, children, header, footer, homeHref, notice, updated, summary, onShowBindingLanguage, locale: localeProp, headingAs, landmark, noIndex, className, labels: labelsProp, }: LegalPageProps): import("react").JSX.Element;
|
|
188
218
|
export interface LegalAcceptCheckboxProps extends Omit<CheckboxProps, "label" | "checked" | "onCheckedChange" | "required"> {
|
|
189
219
|
checked: boolean;
|
|
190
220
|
onCheckedChange: (checked: boolean) => void;
|
|
@@ -201,8 +231,10 @@ export interface LegalAcceptCheckboxProps extends Omit<CheckboxProps, "label" |
|
|
|
201
231
|
*
|
|
202
232
|
* A REQUIRED kit `Checkbox` — not a Switch: a form answer that waits for the submit
|
|
203
233
|
* button. The app keeps its submit disabled while it is unticked, so there is never a
|
|
204
|
-
* failed attempt and no `error` to show.
|
|
205
|
-
*
|
|
234
|
+
* failed attempt and no `error` to show. The checkbox itself sends nothing and asks for
|
|
235
|
+
* no re-acceptance (Marcel, §2.4). An app that records which terms someone accepted
|
|
236
|
+
* stores, with the account, the terms page's `updated` date — the version the reader
|
|
237
|
+
* saw — on its own (keksdose since 2026-10-29; §3.4).
|
|
206
238
|
*
|
|
207
239
|
* The label is ONE template, `legal.accept`, so a language puts the links where its
|
|
208
240
|
* grammar needs them, articles included — fr "J’accepte les {terms} et la {privacy}",
|