@eifi1/ui-kit 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/chart.d.ts +2 -4
- package/dist/components/amount-input.d.ts +16 -9
- package/dist/components/amount-input.js +13 -11
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +5 -0
- package/dist/components/autocomplete.js +39 -37
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +2 -4
- package/dist/components/calculator.d.ts +2 -4
- package/dist/components/column-mapper.d.ts +62 -9
- package/dist/components/column-mapper.js +85 -49
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +33 -1
- package/dist/components/combobox.js +8 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -4
- package/dist/components/copy-button.d.ts +2 -4
- package/dist/components/danger-confirm.d.ts +2 -4
- package/dist/components/danger-confirm.js +52 -5
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +13 -1
- package/dist/components/entity-combobox.js +13 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -4
- package/dist/components/file-button.d.ts +2 -4
- package/dist/components/file-dropzone.d.ts +2 -4
- package/dist/components/form-actions.d.ts +1 -3
- package/dist/components/form-actions.js +15 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +2 -4
- package/dist/components/language-select.d.ts +2 -4
- package/dist/components/money-field.d.ts +2 -4
- package/dist/components/multi-entity-combobox.d.ts +5 -1
- package/dist/components/multi-entity-combobox.js +11 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -4
- package/dist/components/number-input.d.ts +2 -4
- package/dist/components/numpad-sheet.d.ts +2 -4
- package/dist/components/phone-input.d.ts +2 -4
- package/dist/components/series-chart.d.ts +2 -4
- package/dist/components/settings-fields.d.ts +2 -4
- package/dist/components/share-card.d.ts +1 -3
- package/dist/components/text-link.d.ts +1 -3
- package/dist/components/time-input.d.ts +2 -4
- package/dist/components/ui.d.ts +2 -4
- package/dist/components/ui.js +55 -17
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +32 -12
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -253
- package/dist/feedback/feedback-thread.d.ts +1 -3
- package/dist/feedback/feedback-thread.js +8 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-BxeQVzwq.d.ts → feedback-DOwPu-Il.d.ts} +878 -18
- package/dist/feedback.d.ts +2 -4
- package/dist/hooks/use-file-drop.d.ts +2 -4
- package/dist/i18n/defaults.d.ts +2 -4
- package/dist/i18n/german.d.ts +2 -4
- package/dist/i18n/kit-labels.d.ts +1 -3
- package/dist/i18n/languages.d.ts +2 -4
- package/dist/i18n/locales/de-CH.d.ts +2 -4
- package/dist/i18n/locales/en.d.ts +2 -4
- package/dist/i18n/locales/es.d.ts +2 -4
- package/dist/i18n/locales/fr.d.ts +2 -4
- package/dist/i18n/locales/hu.d.ts +2 -4
- package/dist/i18n/locales/it.d.ts +2 -4
- package/dist/i18n/locales/zh.d.ts +2 -4
- package/dist/i18n/review.d.ts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/lib/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -8
- package/dist/rhf/fields.js +61 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +2 -4
- package/dist/rhf.d.ts +3 -5
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -3
- package/dist/shell/top-bar-brand.d.ts +2 -4
- package/dist/shell.d.ts +1 -3
- package/dist/wizard/stepper-nav.d.ts +2 -4
- package/dist/wizard.d.ts +2 -4
- package/package.json +2 -2
- package/src/components/amount-input.tsx +37 -20
- package/src/components/autocomplete.tsx +16 -5
- package/src/components/column-mapper.tsx +181 -79
- package/src/components/combobox.tsx +41 -3
- package/src/components/danger-confirm.tsx +193 -22
- package/src/components/entity-combobox.tsx +28 -5
- package/src/components/form-actions.tsx +20 -1
- package/src/components/multi-entity-combobox.tsx +16 -3
- package/src/components/ui.tsx +107 -0
- package/src/feedback/feedback-attachment.tsx +65 -12
- package/src/feedback/feedback-thread.tsx +66 -6
- package/src/lib/strip-fade.ts +2 -2
- package/src/rhf/fields.tsx +167 -6
- package/src/rhf.ts +1 -1
- package/dist/feedback-attachment-fGAzZPf0.d.ts +0 -469
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n }: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow hint={hasLabel ? undefined : hintParts.labelHint}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-[11px]\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n props: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-[11px]\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, ReactElement } from 'react';
|
|
3
|
-
import { f as ButtonVariant, d as ButtonSize } from '../feedback-
|
|
4
|
-
import '../feedback-attachment-fGAzZPf0.js';
|
|
5
|
-
import '../feedback/feedback-inbox.js';
|
|
6
|
-
import 'lucide-react';
|
|
3
|
+
import { f as ButtonVariant, d as ButtonSize } from '../feedback-DOwPu-Il.js';
|
|
7
4
|
import './tooltip.js';
|
|
8
5
|
import '../hooks/use-anchored-rect.js';
|
|
9
6
|
import '../lib/clipping.js';
|
|
@@ -14,6 +11,7 @@ import './mini-calendar.js';
|
|
|
14
11
|
import './calendar-heatmap.js';
|
|
15
12
|
import './popover.js';
|
|
16
13
|
import './chip.js';
|
|
14
|
+
import 'lucide-react';
|
|
17
15
|
import './field-sync.js';
|
|
18
16
|
import '../wizard/types.js';
|
|
19
17
|
import '../tour/tour.js';
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { C as CalculatorLabels } from '../feedback-
|
|
3
|
-
import '../feedback-attachment-fGAzZPf0.js';
|
|
4
|
-
import '../feedback/feedback-inbox.js';
|
|
5
|
-
import 'lucide-react';
|
|
2
|
+
import { C as CalculatorLabels } from '../feedback-DOwPu-Il.js';
|
|
6
3
|
import './tooltip.js';
|
|
7
4
|
import '../hooks/use-anchored-rect.js';
|
|
8
5
|
import '../lib/clipping.js';
|
|
@@ -13,6 +10,7 @@ import './mini-calendar.js';
|
|
|
13
10
|
import './calendar-heatmap.js';
|
|
14
11
|
import './popover.js';
|
|
15
12
|
import './chip.js';
|
|
13
|
+
import 'lucide-react';
|
|
16
14
|
import './field-sync.js';
|
|
17
15
|
import '../wizard/types.js';
|
|
18
16
|
import '../tour/tour.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, HTMLAttributes } from 'react';
|
|
3
3
|
import { ColumnRole, ColumnMapperResult, ColumnMapping } from '../lib/column-mapping.js';
|
|
4
4
|
import { TextTable } from '../lib/table-text.js';
|
|
5
5
|
|
|
@@ -67,7 +67,8 @@ interface ColumnMapperLabels {
|
|
|
67
67
|
roleOf: (column: string) => string;
|
|
68
68
|
/** The role select's "no role" option. */
|
|
69
69
|
ignore: string;
|
|
70
|
-
/** A required role in the select
|
|
70
|
+
/** A required role in the select — only while some role is NOT required: when
|
|
71
|
+
* every one is, the mark tells no role from another (0.24, Kurvenschmiede). */
|
|
71
72
|
requiredRole: (role: string) => string;
|
|
72
73
|
/** Under the preview, when it shows fewer rows than were read. */
|
|
73
74
|
previewOf: (shown: number, total: number) => string;
|
|
@@ -77,6 +78,20 @@ interface ColumnMapperLabels {
|
|
|
77
78
|
missing: (roles: string) => string;
|
|
78
79
|
}
|
|
79
80
|
declare const DEFAULT_COLUMN_MAPPER_LABELS: ColumnMapperLabels;
|
|
81
|
+
/** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
|
|
82
|
+
type DataAttributes = {
|
|
83
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`
|
|
87
|
+
* hooks, an `id`, a class, a style.
|
|
88
|
+
*/
|
|
89
|
+
type ColumnRoleTableBodyProps = Omit<HTMLAttributes<HTMLTableSectionElement>, "children" | "dangerouslySetInnerHTML"> & DataAttributes;
|
|
90
|
+
/**
|
|
91
|
+
* What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —
|
|
92
|
+
* the same shape as LineItems' `LineItemsRowProps`, on a table row.
|
|
93
|
+
*/
|
|
94
|
+
type ColumnRoleTableRowProps = Omit<HTMLAttributes<HTMLTableRowElement>, "children" | "dangerouslySetInnerHTML"> & DataAttributes;
|
|
80
95
|
interface ColumnRoleTableProps<R extends string, M extends ColumnMapping<R> = ColumnMapping<R>> {
|
|
81
96
|
/** The columns' names — the file's header line — or `null` for a table without one
|
|
82
97
|
* (each column is then "Column 1", "Column 2", …). */
|
|
@@ -100,6 +115,26 @@ interface ColumnRoleTableProps<R extends string, M extends ColumnMapping<R> = Co
|
|
|
100
115
|
/** Every role select disabled — keksdose locks them while a re-sniff replaces the
|
|
101
116
|
* table they point into. */
|
|
102
117
|
disabled?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are
|
|
120
|
+
* the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly
|
|
121
|
+
* that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,
|
|
122
|
+
* its map step reached into the table with a `useLayoutEffect` that set it on the
|
|
123
|
+
* `<tbody>` after every commit; `bodyProps={{ "data-private": "" }}` declares it.
|
|
124
|
+
* There is deliberately no head counterpart: the column names and the role selects
|
|
125
|
+
* are what such a mode is there to show.
|
|
126
|
+
*
|
|
127
|
+
* Spread on the kit `TableBody`; a `className` is added to its own.
|
|
128
|
+
*/
|
|
129
|
+
bodyProps?: ColumnRoleTableBodyProps;
|
|
130
|
+
/**
|
|
131
|
+
* Extra attributes for one preview row's `<tr>`, from its cells and its index in the
|
|
132
|
+
* preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:
|
|
133
|
+
* `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for
|
|
134
|
+
* the rows drawn, the first {@link previewRows}. The row has no attributes of its own
|
|
135
|
+
* to protect; a `className` is added after its classes.
|
|
136
|
+
*/
|
|
137
|
+
rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
|
|
103
138
|
labels?: Partial<ColumnMapperLabels>;
|
|
104
139
|
className?: string;
|
|
105
140
|
}
|
|
@@ -114,15 +149,27 @@ interface ColumnRoleTableProps<R extends string, M extends ColumnMapping<R> = Co
|
|
|
114
149
|
* not after the import. A column with no role says "Ignore" and its values are dimmed:
|
|
115
150
|
* the state that most needs to be visible in a twelve-column bank export.
|
|
116
151
|
*
|
|
117
|
-
* Picking a role another column holds MOVES it ({@link assignColumnRole}).
|
|
118
|
-
*
|
|
119
|
-
*
|
|
152
|
+
* Picking a role another column holds MOVES it ({@link assignColumnRole}). What the
|
|
153
|
+
* table still needs is named under it — a required group as "either Amount, Debit or
|
|
154
|
+
* Credit". Required roles say "(required)" in the list only where some role is not
|
|
155
|
+
* (0.24): Kurvenschmiede's roles are all required, so the mark told no option from
|
|
156
|
+
* another, and on a phone it cut the role's own name short ("Zeit (s) (erforderl…")
|
|
157
|
+
* — the part of the option the reader has to read. The "Still needed" line already
|
|
158
|
+
* says what is missing; a mark that distinguishes nothing is only noise.
|
|
120
159
|
*
|
|
121
160
|
* The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a
|
|
122
161
|
* named, keyboard-reachable region while it overflows), so a wide export never widens
|
|
123
|
-
* the page at 390px.
|
|
162
|
+
* the page at 390px. An edge with columns behind it fades out (0.24, Kurvenschmiede:
|
|
163
|
+
* on a phone the fourth column's select sat off-screen and nothing said there was more
|
|
164
|
+
* to the right; phone scrollbars are overlays that show only while you drag). It is
|
|
165
|
+
* the kit Tabs strip's measured fade: drawn only while something is hidden on that
|
|
166
|
+
* side, so a table that fits is painted as before; both ends once scrolled into the
|
|
167
|
+
* middle; the reading direction's own in RTL; never animated, so there is nothing for
|
|
168
|
+
* reduced motion to turn off; and a `mask-image`, which changes no layout and needs no
|
|
169
|
+
* background colour to fade to. A role select focused behind the fade is scrolled
|
|
170
|
+
* clear of it.
|
|
124
171
|
*/
|
|
125
|
-
declare function ColumnRoleTable<R extends string, M extends ColumnMapping<R> = ColumnMapping<R>>({ header, rows, width: widthProp, roles, mapping, onMappingChange, previewRows, totalRows, disabled, labels: labelsProp, className, }: ColumnRoleTableProps<R, M>): react.JSX.Element;
|
|
172
|
+
declare function ColumnRoleTable<R extends string, M extends ColumnMapping<R> = ColumnMapping<R>>({ header, rows, width: widthProp, roles, mapping, onMappingChange, previewRows, totalRows, disabled, bodyProps, rowProps, labels: labelsProp, className, }: ColumnRoleTableProps<R, M>): react.JSX.Element;
|
|
126
173
|
interface ColumnMapperProps<R extends string> {
|
|
127
174
|
/** The parts the columns play — translated labels, which are required, which must
|
|
128
175
|
* be numbers. See {@link ColumnRole}. */
|
|
@@ -154,6 +201,12 @@ interface ColumnMapperProps<R extends string> {
|
|
|
154
201
|
* order. */
|
|
155
202
|
guess?: (table: TextTable) => ColumnMapping<R>;
|
|
156
203
|
disabled?: boolean;
|
|
204
|
+
/** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}
|
|
205
|
+
* (0.24): `{ "data-private": "" }` for a table of someone's own data. */
|
|
206
|
+
bodyProps?: ColumnRoleTableBodyProps;
|
|
207
|
+
/** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}
|
|
208
|
+
* (0.24). `row` is the row as read, `index` its place in the preview. */
|
|
209
|
+
rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
|
|
157
210
|
labels?: Partial<ColumnMapperLabels>;
|
|
158
211
|
className?: string;
|
|
159
212
|
}
|
|
@@ -177,6 +230,6 @@ interface ColumnMapperProps<R extends string> {
|
|
|
177
230
|
* parts: {@link parseTextTable} or its own server's sniff, then {@link ColumnRoleTable},
|
|
178
231
|
* then {@link readMappedTable}.
|
|
179
232
|
*/
|
|
180
|
-
declare function ColumnMapper<R extends string>({ roles, onChange, label, hint, accept, maxSize, defaultText, previewRows, guess, disabled, labels: labelsProp, className, }: ColumnMapperProps<R>): react.JSX.Element;
|
|
233
|
+
declare function ColumnMapper<R extends string>({ roles, onChange, label, hint, accept, maxSize, defaultText, previewRows, guess, disabled, bodyProps, rowProps, labels: labelsProp, className, }: ColumnMapperProps<R>): react.JSX.Element;
|
|
181
234
|
|
|
182
|
-
export { ColumnMapper, type ColumnMapperLabels, type ColumnMapperProps, ColumnRoleTable, type ColumnRoleTableProps, DEFAULT_COLUMN_MAPPER_LABELS };
|
|
235
|
+
export { ColumnMapper, type ColumnMapperLabels, type ColumnMapperProps, ColumnRoleTable, type ColumnRoleTableBodyProps, type ColumnRoleTableProps, type ColumnRoleTableRowProps, DEFAULT_COLUMN_MAPPER_LABELS };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
4
5
|
import { Upload } from "lucide-react";
|
|
5
6
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
6
7
|
import { cn } from "../lib/cn.js";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
readTextFile,
|
|
13
14
|
roleOfColumn
|
|
14
15
|
} from "../lib/column-mapping.js";
|
|
16
|
+
import { scrollIntoStrip, useStripFade } from "../lib/strip-fade.js";
|
|
15
17
|
import { parseTextTable } from "../lib/table-text.js";
|
|
16
18
|
import { AlertBanner } from "./alert-banner.js";
|
|
17
19
|
import { Checkbox } from "./checkbox.js";
|
|
@@ -77,6 +79,8 @@ function ColumnRoleTable({
|
|
|
77
79
|
previewRows = 5,
|
|
78
80
|
totalRows,
|
|
79
81
|
disabled,
|
|
82
|
+
bodyProps,
|
|
83
|
+
rowProps,
|
|
80
84
|
labels: labelsProp,
|
|
81
85
|
className
|
|
82
86
|
}) {
|
|
@@ -98,65 +102,93 @@ function ColumnRoleTable({
|
|
|
98
102
|
const gaps = missingRoles(roles, mapping, width).map(
|
|
99
103
|
(gap) => gap.length === 1 ? labelOf(gap[0]) : labels.oneOf(formatList(gap.map(labelOf), "disjunction", locale))
|
|
100
104
|
);
|
|
105
|
+
const markRequired = roles.some((role) => role.required !== true);
|
|
106
|
+
const scroller = useRef(null);
|
|
107
|
+
const tableRef = useCallback((table) => {
|
|
108
|
+
scroller.current = table?.parentElement ?? null;
|
|
109
|
+
}, []);
|
|
110
|
+
const fade = useStripFade(scroller);
|
|
101
111
|
return /* @__PURE__ */ jsxs(
|
|
102
112
|
"div",
|
|
103
113
|
{
|
|
104
114
|
className: cn("min-w-0 space-y-1.5", className),
|
|
105
115
|
"data-slot": "column-role-table",
|
|
106
116
|
children: [
|
|
107
|
-
/* @__PURE__ */
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
"data-slot": "column-role-table-scroll",
|
|
121
|
+
"data-overflow": fade.overflow,
|
|
122
|
+
style: fade.mask ? { maskImage: fade.mask, WebkitMaskImage: fade.mask } : void 0,
|
|
123
|
+
children: /* @__PURE__ */ jsxs(
|
|
124
|
+
Table,
|
|
113
125
|
{
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
ref: tableRef,
|
|
127
|
+
density: "compact",
|
|
128
|
+
framed: true,
|
|
129
|
+
"aria-label": labels.table,
|
|
116
130
|
children: [
|
|
117
|
-
/* @__PURE__ */ jsx(
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => {
|
|
132
|
+
const role = roleOfColumn(roles, mapping, column);
|
|
133
|
+
const name = nameOf(column);
|
|
134
|
+
return /* @__PURE__ */ jsxs(
|
|
135
|
+
TableHeaderCell,
|
|
136
|
+
{
|
|
137
|
+
scope: "col",
|
|
138
|
+
className: "min-w-36 py-2 align-top",
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]", children: name }),
|
|
141
|
+
/* @__PURE__ */ jsxs(
|
|
142
|
+
Select,
|
|
143
|
+
{
|
|
144
|
+
size: "sm",
|
|
145
|
+
className: "w-full",
|
|
146
|
+
"aria-label": labels.roleOf(name),
|
|
147
|
+
value: role ?? "",
|
|
148
|
+
disabled,
|
|
149
|
+
onFocus: (event) => {
|
|
150
|
+
const cell = event.currentTarget.closest("th");
|
|
151
|
+
if (scroller.current && cell)
|
|
152
|
+
scrollIntoStrip(scroller.current, cell);
|
|
153
|
+
},
|
|
154
|
+
onChange: (event) => onMappingChange(
|
|
155
|
+
assignColumnRole(
|
|
156
|
+
roles,
|
|
157
|
+
mapping,
|
|
158
|
+
column,
|
|
159
|
+
event.target.value === "" ? null : event.target.value
|
|
160
|
+
)
|
|
161
|
+
),
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsx("option", { value: "", children: labels.ignore }),
|
|
164
|
+
roles.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.required === true && markRequired ? labels.requiredRole(option.label) : option.label }, option.value))
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
column
|
|
171
|
+
);
|
|
172
|
+
}) }) }),
|
|
173
|
+
/* @__PURE__ */ jsx(TableBody, { ...bodyProps, children: shown.map((row, index) => /* @__PURE__ */ createElement(TableRow, { ...rowProps?.(row, index), key: index }, columns.map((column) => /* @__PURE__ */ jsx(
|
|
174
|
+
TableCell,
|
|
120
175
|
{
|
|
121
|
-
|
|
122
|
-
className:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assignColumnRole(
|
|
128
|
-
roles,
|
|
129
|
-
mapping,
|
|
130
|
-
column,
|
|
131
|
-
event.target.value === "" ? null : event.target.value
|
|
132
|
-
)
|
|
176
|
+
"data-ignored": roleOfColumn(roles, mapping, column) === null || void 0,
|
|
177
|
+
className: cn(
|
|
178
|
+
"font-mono whitespace-nowrap",
|
|
179
|
+
// Dimmed, not hidden: its values are what tell you whether ignoring
|
|
180
|
+
// it was right.
|
|
181
|
+
roleOfColumn(roles, mapping, column) === null && "text-[var(--text-muted)]"
|
|
133
182
|
),
|
|
134
|
-
children: [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
)
|
|
183
|
+
children: /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate", children: row[column] ?? "" })
|
|
184
|
+
},
|
|
185
|
+
column
|
|
186
|
+
)))) })
|
|
140
187
|
]
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/* @__PURE__ */ jsx(TableBody, { children: shown.map((row, index) => /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(
|
|
146
|
-
TableCell,
|
|
147
|
-
{
|
|
148
|
-
"data-ignored": roleOfColumn(roles, mapping, column) === null || void 0,
|
|
149
|
-
className: cn(
|
|
150
|
-
"font-mono whitespace-nowrap",
|
|
151
|
-
// Dimmed, not hidden: its values are what tell you whether ignoring
|
|
152
|
-
// it was right.
|
|
153
|
-
roleOfColumn(roles, mapping, column) === null && "text-[var(--text-muted)]"
|
|
154
|
-
),
|
|
155
|
-
children: /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate", children: row[column] ?? "" })
|
|
156
|
-
},
|
|
157
|
-
column
|
|
158
|
-
)) }, index)) })
|
|
159
|
-
] }),
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
),
|
|
160
192
|
total > shown.length && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.previewOf(shown.length, total) }),
|
|
161
193
|
gaps.length > 0 && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, children: labels.missing(formatList(gaps, "conjunction", locale)) })
|
|
162
194
|
]
|
|
@@ -175,6 +207,8 @@ function ColumnMapper({
|
|
|
175
207
|
previewRows = 5,
|
|
176
208
|
guess,
|
|
177
209
|
disabled = false,
|
|
210
|
+
bodyProps,
|
|
211
|
+
rowProps,
|
|
178
212
|
labels: labelsProp,
|
|
179
213
|
className
|
|
180
214
|
}) {
|
|
@@ -344,6 +378,8 @@ function ColumnMapper({
|
|
|
344
378
|
onMappingChange: (mapping) => commit({ ...state, mapping }),
|
|
345
379
|
previewRows,
|
|
346
380
|
disabled,
|
|
381
|
+
bodyProps,
|
|
382
|
+
rowProps,
|
|
347
383
|
labels: labelsProp
|
|
348
384
|
}
|
|
349
385
|
),
|