@eifi1/ui-kit 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/dist/admin/user-roster.js +31 -12
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/verify-email.d.ts +4 -0
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +9 -0
- package/dist/components/alert-banner.js +9 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/autocomplete.js +13 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -19
- package/dist/components/button-look.js +3 -30
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox.js +3 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +1 -0
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/field-base.d.ts +31 -1
- package/dist/components/field-base.js +28 -0
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/floating-panel.d.ts +6 -13
- package/dist/components/floating-panel.js +2 -9
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/icon-button.js +3 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +12 -0
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/select.js +3 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/stat-tile.d.ts +8 -1
- package/dist/components/stat-tile.js +7 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/german.js +2 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +2 -0
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +2 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +2 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +2 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +2 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +2 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/page-seo.d.ts +19 -11
- package/dist/landing/page-seo.js +11 -7
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/landing/public-page.d.ts +57 -0
- package/dist/landing/public-page.js +45 -0
- package/dist/landing/public-page.js.map +1 -0
- package/dist/lib/quiet-focus.d.ts +4 -0
- package/dist/lib/quiet-focus.js +19 -0
- package/dist/lib/quiet-focus.js.map +1 -0
- package/dist/lib/server-wake.d.ts +3 -2
- package/dist/lib/server-wake.js +19 -13
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -15
- package/dist/shell/app-shell.js +7 -26
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +98 -0
- package/dist/shell/dev-menu.js +133 -0
- package/dist/shell/dev-menu.js.map +1 -0
- package/dist/shell/dev-switchers.d.ts +6 -53
- package/dist/shell/dev-switchers.js +5 -100
- package/dist/shell/dev-switchers.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +4 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +18 -9
- package/dist/shell/top-bar-parts-labels.js +2 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +17 -2
- package/dist/shell/topbar-action-menu.js +3 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +3 -1
- package/dist/shell.js +3 -1
- package/dist/shell.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/tour.d.ts +31 -0
- package/dist/testing/tour.js +12 -0
- package/dist/testing/tour.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +0 -3
- package/dist/theme/palette-presets.js +0 -2
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour-engine.d.ts +12 -0
- package/dist/tour/tour-engine.js +31 -0
- package/dist/tour/tour-engine.js.map +1 -0
- package/dist/tour/tour.d.ts +14 -3
- package/dist/tour/tour.js +60 -45
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -3
- package/dist/tour/use-tour-url-sync.js +21 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/user-roster.tsx +30 -10
- package/src/auth/verify-email.tsx +4 -0
- package/src/components/alert-banner.tsx +37 -10
- package/src/components/autocomplete.tsx +18 -0
- package/src/components/button-look.ts +7 -57
- package/src/components/combobox.tsx +5 -1
- package/src/components/data-table-fit.ts +4 -1
- package/src/components/data-table-virtual.ts +14 -1
- package/src/components/field-base.tsx +53 -1
- package/src/components/floating-panel.tsx +6 -28
- package/src/components/icon-button.tsx +6 -8
- package/src/components/inline-entity-combobox.tsx +17 -0
- package/src/components/select.tsx +3 -6
- package/src/components/stat-tile.tsx +27 -9
- package/src/i18n/german.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -2
- package/src/i18n/locales/es.ts +2 -0
- package/src/i18n/locales/fr.ts +2 -0
- package/src/i18n/locales/hu.ts +2 -0
- package/src/i18n/locales/it.ts +2 -0
- package/src/i18n/locales/zh.ts +2 -0
- package/src/index.ts +4 -1
- package/src/landing/page-seo.ts +28 -16
- package/src/landing/public-page.tsx +106 -0
- package/src/lib/quiet-focus.ts +32 -0
- package/src/lib/server-wake.ts +41 -17
- package/src/shell/app-shell.tsx +11 -48
- package/src/shell/dev-menu.tsx +259 -0
- package/src/shell/dev-switchers.tsx +13 -174
- package/src/shell/top-bar-actions.tsx +4 -1
- package/src/shell/top-bar-parts-labels.ts +20 -9
- package/src/shell/topbar-action-menu.tsx +28 -2
- package/src/shell.ts +5 -1
- package/src/testing/index.ts +8 -0
- package/src/testing/tour.ts +41 -0
- package/src/theme/palette-presets.ts +0 -4
- package/src/tour/tour-engine.ts +41 -0
- package/src/tour/tour.tsx +64 -24
- package/src/tour/use-tour-url-sync.ts +33 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/icon-button.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n KeyboardEvent,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { CommitScope } from \"./write-lock\";\nimport { hasContent } from \"../lib/presence\";\nimport { CompactControlsContext, LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport {\n HOVER_INK,\n ICON_BUTTON_PRESSED,\n RenderedButtonLink,\n HIT_AREA_LARGE,\n TOUCH_TARGET_LARGE,\n buttonVariantClasses,\n resolveButtonVariant,\n} from \"./button-look\";\nimport type { ButtonVariant, CanonicalButtonVariant } from \"./button-look\";\n\n// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2\n// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,\n// text-shaped box. This fixes a square box instead and, crucially, forces the\n// child icon to a size set by the BOX (20px at md/sm) via `[&_svg]:size-*` so a\n// caller cannot under-size it. Use it wherever an action is a bare icon\n// (edit/delete/tools) so they all match the top-bar icon buttons and can never\n// drift apart again.\n//\n// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the\n// two re-skin together with the palette.\nconst ICON_BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed\",\n FOCUS_RING_WIDTH,\n);\n\nexport type IconButtonSize = \"2xl\" | \"xl\" | \"lg\" | \"md\" | \"sm\" | \"xs\" | \"2xs\";\n\n// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon\n// that touches its edges. The two small steps are lenkbank's list-row (28px) and\n// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.\nconst ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {\n // 64px with a 28px glyph: a camera's shutter (keksdose invoices/camera-capture.tsx:248,\n // `size-16` by hand) — the one control of a full-screen capture view, thumb-sized.\n \"2xl\": \"size-16 [&_svg]:size-7\",\n // 48px with a 24px glyph: the one primary action of a phone screen or sheet, a step\n // past the 44px minimum rather than on it.\n xl: \"size-12 [&_svg]:size-6\",\n // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's\n // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.\n lg: \"size-11 [&_svg]:size-5\",\n md: \"size-9 [&_svg]:size-5\",\n sm: \"size-8 [&_svg]:size-5\",\n xs: \"size-7 rounded [&_svg]:size-4\",\n \"2xs\": \"size-6 rounded [&_svg]:size-3.5\",\n};\n\n// `stretch`: the same WIDTH and glyph, and a height taken from the row instead of\n// stated — `size-*` sets a height that `self-stretch` cannot override, so the box is\n// written as a width plus a minimum height (the square it would otherwise be).\nconst ICON_BUTTON_STRETCH_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"w-16 min-h-16 self-stretch [&_svg]:size-7\",\n xl: \"w-12 min-h-12 self-stretch [&_svg]:size-6\",\n lg: \"w-11 min-h-11 self-stretch [&_svg]:size-5\",\n md: \"w-9 min-h-9 self-stretch [&_svg]:size-5\",\n sm: \"w-8 min-h-8 self-stretch [&_svg]:size-5\",\n xs: \"w-7 min-h-7 self-stretch rounded [&_svg]:size-4\",\n \"2xs\": \"w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5\",\n};\n\n// The label as visible text beside the glyph (0.32, §4: at Large \"IconButton shows its\n// label\"): the same height and glyph as the square, a width taken from the words. A\n// map of its own rather than `w-auto` over `size-*`, which tailwind-merge cannot undo\n// in one direction.\nconst ICON_BUTTON_TEXT_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"h-16 gap-2 px-4 text-base [&_svg]:size-7\",\n xl: \"h-12 gap-2 px-3.5 text-sm [&_svg]:size-6\",\n lg: \"h-11 gap-2 px-3 text-sm [&_svg]:size-5\",\n md: \"h-9 gap-1.5 px-2.5 text-sm [&_svg]:size-5\",\n sm: \"h-8 gap-1.5 px-2 text-sm [&_svg]:size-5\",\n xs: \"h-7 gap-1 rounded px-1.5 text-xs [&_svg]:size-4\",\n \"2xs\": \"h-6 gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5\",\n};\nconst ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"min-h-16 self-stretch gap-2 px-4 text-base [&_svg]:size-7\",\n xl: \"min-h-12 self-stretch gap-2 px-3.5 text-sm [&_svg]:size-6\",\n lg: \"min-h-11 self-stretch gap-2 px-3 text-sm [&_svg]:size-5\",\n md: \"min-h-9 self-stretch gap-1.5 px-2.5 text-sm [&_svg]:size-5\",\n sm: \"min-h-8 self-stretch gap-1.5 px-2 text-sm [&_svg]:size-5\",\n xs: \"min-h-7 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-4\",\n \"2xs\": \"min-h-6 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5\",\n};\n\n// The 48 px touch target at Large (§4), per size. `lg` and up are past it already\n// (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON\n// a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and\n// gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the\n// taps a finger lands round the glyph (`HIT_AREA_LARGE`).\nconst ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {\n \"2xl\": \"\",\n xl: \"\",\n lg: \"\",\n md: TOUCH_TARGET_LARGE,\n sm: TOUCH_TARGET_LARGE,\n xs: TOUCH_TARGET_LARGE,\n \"2xs\": HIT_AREA_LARGE,\n};\n\n// A tone re-colours the glyph without changing what the variant draws around it.\n// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):\n// QUIET is placeholder grey until the pointer or focus arrives, then the tone's\n// family; TONED wears the tone's colour at rest. The defaults are the looks each tone\n// had before `quiet` existed: `danger` is quiet — an action in every row of a list\n// must not shout from every row, so the red arrives only on the one row you are about\n// to act on — while `warning` and `info` are toned, because they mark the ONE thing\n// on the row to notice (keksdose's \"needs review\" flag, its reconcile action at\n// accounts-page:867), and quiet grey would hide the very thing they point out.\n// `muted` is quiet by definition and has no toned look; `default` has no tone.\n//\n// `success` is toned at rest too: the green is the news (keksdose's \"in sync\" chip).\n// `custom` paints from `--icon-button-tone`, which `toneColor` sets — for a glyph whose\n// colour FOLLOWS STATE across tones (keksdose's sync chip: rose, amber, sky, green as\n// the sync moves), where swapping `tone` would do but the colour is one variable.\nexport type IconButtonTone = \"default\" | \"muted\" | \"danger\" | \"warning\" | \"info\" | \"success\" | \"custom\";\n\ntype ColouredTone = \"danger\" | \"warning\" | \"info\" | \"success\" | \"custom\";\n\nconst ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {\n danger: {\n quiet:\n \"text-placeholder hover:bg-danger-soft hover:text-danger focus-visible:ring-danger-strong\",\n toned: \"text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n },\n warning: {\n quiet:\n \"text-placeholder hover:bg-warning-soft hover:text-warning focus-visible:ring-warning-strong\",\n // The ring is warning's loud line (0.33, k27): `--warning-border`, amber-300, was a\n // focus ring at 1.1:1 on the light surfaces.\n toned: \"text-warning hover:bg-warning-soft focus-visible:ring-warning-strong\",\n },\n info: {\n quiet:\n \"text-placeholder hover:bg-info-soft hover:text-info focus-visible:ring-[var(--info-border)]\",\n toned: \"text-info hover:bg-info-soft focus-visible:ring-[var(--info-border)]\",\n },\n success: {\n quiet:\n \"text-placeholder hover:bg-success-soft hover:text-success focus-visible:ring-[var(--success-border)]\",\n toned: \"text-success hover:bg-success-soft focus-visible:ring-[var(--success-border)]\",\n },\n // The hover fill and the ring are MIXED from the one colour, since a custom tone\n // brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a\n // `custom` tone with no colour set is the default look rather than invisible.\n custom: {\n quiet:\n \"text-placeholder hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]\",\n toned:\n \"text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]\",\n },\n};\n\n// The translucent ink on hover, as Button's secondary/ghost (B′, see HOVER_INK).\nconst ICON_BUTTON_MUTED = `text-placeholder ${HOVER_INK} hover:text-primary`;\n\n/** Which tones sit quiet at rest when `quiet` is left out. */\nconst QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {\n danger: true,\n warning: false,\n info: false,\n success: false,\n custom: false,\n};\n\nfunction iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined): string {\n if (tone === \"default\") return \"\";\n if (tone === \"muted\") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);\n const isQuiet = quiet ?? QUIET_BY_DEFAULT[tone];\n const look = ICON_BUTTON_TONES[tone];\n return isQuiet ? cn(look.quiet, ICON_BUTTON_QUIET_DISABLED_REST) : look.toned;\n}\n\n// A disabled button must not answer the pointer. The hover classes above are plain\n// `hover:` (so a caller's `className=\"hover:…\"` still replaces them through\n// tailwind-merge), which means they fire on a disabled button too — the old grey\n// icon lit up on hover while refusing the click. Rather than rewrite every hover as\n// `enabled:hover:` (which never matches the `<a>` that `buttonClasses` also styles,\n// and would out-rank a caller's plain `hover:` override), each variant pins its\n// RESTING look under `disabled:hover:`, which out-ranks any `hover:` by specificity\n// and only ever matches a disabled button.\nconst ICON_BUTTON_DISABLED_REST: Record<CanonicalButtonVariant | \"overlay\" | \"shutter\", string> = {\n default: \"disabled:hover:bg-surface-2\",\n secondary: \"disabled:hover:bg-transparent\",\n ghost: \"disabled:hover:bg-transparent\",\n danger: \"disabled:hover:bg-danger\",\n accent: \"disabled:hover:bg-brand\",\n link: \"disabled:hover:bg-transparent disabled:hover:no-underline\",\n overlay: \"disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]\",\n shutter: \"disabled:hover:bg-media-scrim\",\n};\n\n// The quiet looks change the glyph on hover, so they pin their resting glyph the\n// same way. A toned look keeps its glyph on hover and needs no pin.\nconst ICON_BUTTON_QUIET_DISABLED_REST = \"disabled:hover:text-placeholder\";\n\n// `variant=\"overlay\"`: a round, translucent disc for a control that sits ON a photo\n// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).\n// The inverse pair, not the surface one: what has to hold is the contrast between the\n// disc and its glyph, whatever the picture underneath is, and `--bg-inverse` /\n// `--text-inverse` are the one token pair defined as each other's opposite in both\n// themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette\n// can re-point); the blur keeps a busy background from breaking the glyph's edge.\nconst ICON_BUTTON_OVERLAY =\n \"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-inverse backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus-visible:ring-[var(--text-inverse)]\";\n\n// `variant=\"shutter\"`: the camera's release — keksdose's camera-capture.tsx:248 draws\n// it by hand as a 64px circle with a thick white ring round a translucent fill. It sits\n// on a live camera picture, which is dark whatever the theme, so it uses the MEDIA\n// tokens (tokens.css), white ink on a dark scrim in both themes — the inverse pair\n// `overlay` uses would give a dark ring on a dark viewfinder in dark mode. The focus ring stands off by 2px, or it would\n// merge into the ring that is part of the look.\nconst ICON_BUTTON_SHUTTER =\n \"rounded-full border-4 border-media-ink bg-media-scrim text-media-ink backdrop-blur-sm hover:bg-media-scrim-hover focus-visible:ring-media-ink focus-visible:ring-offset-2 focus-visible:ring-offset-media-scrim-hover\";\n\nexport type IconButtonVariant = ButtonVariant | \"overlay\" | \"shutter\";\n\n/**\n * The glyph's size in px, over the one the box size implies — see\n * {@link IconButtonProps.glyphSize}.\n */\nexport type IconButtonGlyphSize = 12 | 14 | 16 | 20 | 24 | 28;\n\nconst ICON_BUTTON_GLYPH_SIZES: Record<IconButtonGlyphSize, string> = {\n 12: \"[&_svg]:size-3\",\n 14: \"[&_svg]:size-3.5\",\n 16: \"[&_svg]:size-4\",\n 20: \"[&_svg]:size-5\",\n 24: \"[&_svg]:size-6\",\n 28: \"[&_svg]:size-7\",\n};\n\n// `pending`'s spinner, the size of the glyph it replaces (the `[&_svg]` rules above\n// size only an svg, and the spinner is a bordered span).\nconst ICON_BUTTON_SPINNER: Record<IconButtonSize, string> = {\n \"2xl\": \"size-7\",\n xl: \"size-6\",\n lg: \"size-5\",\n md: \"size-5\",\n sm: \"size-5\",\n xs: \"size-4\",\n \"2xs\": \"size-3.5\",\n};\nconst ICON_BUTTON_SPINNER_GLYPH: Record<IconButtonGlyphSize, string> = {\n 12: \"size-3\",\n 14: \"size-3.5\",\n 16: \"size-4\",\n 20: \"size-5\",\n 24: \"size-6\",\n 28: \"size-7\",\n};\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Any {@link ButtonVariant}; `overlay` — a round translucent disc for use over\n * an image (see `ICON_BUTTON_OVERLAY`); or `shutter` — a camera's release, a ringed\n * disc over the viewfinder (see `ICON_BUTTON_SHUTTER`; pair it with `size=\"2xl\"`). */\n variant?: IconButtonVariant;\n /** Box size: 2xl = 64px with a 28px icon (a camera shutter), xl = 48px with a 24px\n * icon, lg = 44px (a phone's touch target), md = 36px (matches the top bar),\n * sm = 32px — all three with a 20px icon;\n * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px\n * icon (an action on a chip or a tab). `glyphSize` overrides the icon. */\n size?: IconButtonSize;\n /**\n * The icon's size in px, when the box's own is not the one wanted. keksdose's sync\n * chip (app/sync-status-indicator.tsx:142) is a 24px round chip round a 20px cloud —\n * `size=\"2xs\" glyphSize={20} shape=\"round\"` — where `2xs` alone draws 14px. A number\n * over a new size step because the box and the glyph are two independent choices; a\n * step per combination (`\"sm-lg\"`) would have to be named for every one.\n */\n glyphSize?: IconButtonGlyphSize;\n /**\n * A small mark on the button's corner — the sync chip's 12px check over its cloud\n * (\"all good\", feedback #67). Rendered OUTSIDE the `<button>`, in a wrapper beside\n * it, so the box's `[&_svg]:size-*` rule that sizes the glyph does not blow the badge\n * up to the glyph's size; a bare `<Check />` comes out 12px, on a\n * `--bg-surface` disc that keeps it legible over the glyph. Decorative\n * (`aria-hidden`): the name (`label` / `aria-label`) must already carry the state.\n * With a badge, `className` still lands on the button; the wrapper is the flex item.\n */\n badge?: ReactNode;\n /**\n * `keep`: a disabled button keeps its full colour (no 50% drop) and an ordinary\n * cursor. For a STATUS that is also an action — the sync chip shows \"in sync\" in\n * green and refreshes on click, and is disabled while offline or syncing, when the\n * state it shows matters most; dimmed it read as broken (keksdose wrote\n * `disabled:cursor-default` by hand and left out the dimming). Default `dim`.\n */\n disabledStyle?: \"dim\" | \"keep\";\n /**\n * Fill the height of the flex row it stands in, keeping its width — the delete at\n * the end of keksdose's split line (transaction-fields:162), level with the labelled\n * fields beside it rather than floating at half their height. {@link Button}'s\n * `stretch`; no effect outside a flex row. With a `label`, the tooltip's wrapper is\n * what stretches.\n */\n stretch?: boolean;\n /** `round`: a circle instead of the rounded square — a compact status chip in a top\n * bar (keksdose's sync indicator). `overlay` is always round. */\n shape?: \"square\" | \"round\";\n /**\n * The colour of `tone=\"custom\"`, as any CSS colour — a token (`var(--warning)`),\n * ideally. Sets `--icon-button-tone` on the button, so passing a different one per\n * state re-colours the glyph, its hover fill and its focus ring together. Passing it\n * implies `tone=\"custom\"`. Or leave it out and set the variable yourself from a class\n * (`className=\"[--icon-button-tone:var(--success)]\"`).\n *\n * Or a `{ light, dark }` pair, one colour per theme — keksdose: a raw colour that reads\n * on the light surface (`#be123c`) is too dark on the dark one, and an inline style\n * cannot say \"in dark mode\". See {@link IconButtonToneColor}.\n */\n toneColor?: IconButtonToneColor;\n /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on\n * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a\n * remove/delete that repeats down a list. `warning`: amber at rest — a flag that\n * wants attention. `info`: sky at rest — a notice-worthy but harmless action\n * (keksdose's reconcile). `success`: green at rest — a good state worth seeing.\n * `custom`: the colour of `toneColor` (see there). Default: the variant's own colours. See `quiet` for\n * turning a coloured tone's resting look the other way. */\n tone?: IconButtonTone;\n /**\n * Whether a coloured tone (`danger`, `warning`, `info`) waits for the pointer:\n * `true` is placeholder grey at rest and the tone's colour on hover and focus;\n * `false` wears the tone's colour at rest. Left out, each tone keeps its own\n * default — `danger` quiet, `warning` and `info` not. Ignored for `muted` (quiet by\n * definition) and `default` (no tone).\n *\n * `quiet={false}` on `danger` is for a destructive action that stands ALONE, where\n * hover-to-reveal hides it: keksdose's phone bulk bar (mobile-bulk-bar.tsx) has one\n * \"delete all\" and a touch screen that never hovers, so it painted rose by hand.\n * `quiet` on `warning`/`info` is the same switch the other way, for a flag repeated\n * down a list. A boolean over the tone rather than a new tone (`danger-solid`) or an\n * `emphasis` scale: there are exactly two resting looks, every coloured tone has\n * both, and which one fits is a question about the SITE (alone or repeated, touch or\n * pointer), not about the tone — so it is one switch the family shares.\n */\n quiet?: boolean;\n /**\n * Make it a toggle button. `true` sets `aria-pressed=\"true\"` and draws the \"on\" look\n * (brand glyph on the quiet brand fill); `false` sets `aria-pressed=\"false\"` with the\n * normal look, so a screen reader still hears a toggle that is off. Left out, it is\n * an ordinary button with no `aria-pressed` — or whatever `aria-pressed` the caller\n * passes. For keksdose's budget share toggle (budgets-page:291), which paints its own\n * brand colour over a ghost button today. Keep the `aria-label` the same in both\n * states (\"Share budget\"): the pressed state already says whether it is on.\n */\n pressed?: boolean;\n /** Keep the click (and the Enter/Space that produces it) from reaching an\n * ancestor's handler — for an action inside a clickable table row or card.\n *\n * **Not a licence to put this inside another `<button>`.** HTML forbids any\n * interactive content, and any element with a `tabindex`, inside a button — so\n * the `<span role=\"button\" tabIndex={0}>` workaround is invalid too — and ARIA\n * makes a button's children presentational, so a screen reader flattens the\n * inner one into the outer one's name and it cannot be reached at all. Make the\n * row's main action a button that fills the row and put this one BESIDE it, on\n * top:\n *\n * ```tsx\n * <div className=\"relative\">\n * <button className=\"w-full pe-16 …\" onClick={open}>…row…</button>\n * <div className=\"absolute inset-y-0 end-2 flex items-center gap-0.5\">\n * <IconButton size=\"xs\" tone=\"danger\" aria-label=\"Delete\" onClick={remove}>\n * <Trash2 />\n * </IconButton>\n * </div>\n * </div>\n * ```\n *\n * The pair look exactly like the nested version, click exactly like it, and\n * are two tab stops a screen reader can tell apart. */\n stopPropagation?: boolean;\n /**\n * The button's name, said once: it becomes the `aria-label` AND the text of a kit\n * {@link Tooltip} round the button.\n *\n * Every app writes the pair by hand. lenkbank's nine icon actions all carry\n * `aria-label={t(x)} title={t(x)}` (shared/lib/table-columns.tsx:103,\n * projects-page.tsx:83, setpoint/segment-list.tsx:198/207, profile-bar.tsx:128/142,\n * shortcut-dialog.tsx:815/857/1007) — and `title` is the browser's own tooltip,\n * which shows late, never on focus and never on touch, and looks like no other\n * label in the kit. kastlan's RowAction (shared/components/data-table/row-action.tsx)\n * and keksdose's icon wrappers each exist to put a Tooltip round an IconButton.\n *\n * A caller's own `aria-label` still wins, for the rare name that should be longer\n * than the bubble. The bubble is visual only: it would otherwise describe the button\n * with its own name, and a screen reader would read the same word twice. Tooltip's\n * default placement applies — in place, or portalled inside a scroll container —\n * unless `tooltipPortal` says otherwise.\n */\n label?: string;\n /**\n * Show `label` as visible text beside the icon at Large and Extra large (0.32,\n * docs/text-size-harmonization.md §4, §10.8). A reader who asked for bigger type is\n * the one an unlabelled glyph fails, and on touch the tooltip never opens. Then there\n * is no tooltip: the words are on the button.\n *\n * Default `true` — except for `variant=\"overlay\"` and `\"shutter\"`, which sit ON a\n * picture, and inside {@link CompactControls} (AppShell's top bar, `RowActions`'\n * inline icons). `false`: the icon alone at every size, for an icon on content — a\n * receipt overlay, a map's zoom, a viewfinder, a photo thumbnail. That must be rare;\n * a dense row's actions belong in `RowActions`, which collapses them into a \"⋯\" menu\n * at Large instead (§10.8). Normal never shows it. `tooltip={false}` (a universally\n * read glyph, a dialog's ✕) keeps the icon alone too.\n */\n labelVisible?: boolean;\n /** Show `label` as a tooltip. Default `true`; `false` keeps `label` as the\n * accessible name only — for a button whose glyph is universally read (a close ✕ in\n * a dialog header) or that already sits under a tooltip of its own. It also keeps the\n * label from showing as text at Large (see `labelVisible`). */\n tooltip?: boolean;\n /** Where the `label` tooltip opens. See {@link Tooltip}'s `side`. */\n tooltipSide?: TooltipSide;\n /** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */\n tooltipPortal?: boolean;\n /**\n * Mount the `label` bubble only while it is up — {@link Tooltip}'s `lazy`. Default\n * `true` since 0.16.0; `false` is the always-mounted in-place bubble of before.\n *\n * Why the label bubble can be lazy when Tooltip's default is not: this bubble\n * describes nothing. It is wrapped round the button in a fragment precisely so that\n * it is NOT the button's `aria-describedby` (it would repeat the button's own name),\n * so there is no description for a lazy mount to take away — the name is the\n * `aria-label` either way, read in browse mode and on focus alike. What the\n * always-mounted bubble did do was sit in the DOM as a `role=\"tooltip\"` node full of\n * text: every IconButton in a row added one to `getAllByRole(\"tooltip\")` and its label\n * to the row's `textContent`, and two keksdose tests tripped over that (0.15.5 P6).\n * The cost is the one {@link Tooltip}'s \"Lazy in place\" note names — a test that\n * looked the bubble up without hovering now has to hover (or focus) first; pass\n * `false` while it is migrated. A `disabledReason` bubble is unaffected: that one\n * stays mounted as it was.\n */\n tooltipLazy?: boolean;\n /**\n * Busy — the action is in flight: the glyph is swapped for a spinner of the same\n * size, the button is `aria-busy`, and clicks (and the Enter/Space and form\n * submission they stand for) are swallowed, so a second press cannot start the\n * action twice. {@link Button}'s `pending`, with the same contract: `aria-disabled`\n * rather than `disabled`, so the button that was pressed keeps its focus. The name\n * (`label` / `aria-label`) stays — `aria-busy` is what says it is working. A SWAP\n * rather than Button's overlay: the box is square and fixed, so there is no width\n * for the glyph to hold, and a spinner drawn over a glyph would be two marks in one\n * 20px square.\n */\n pending?: boolean;\n /**\n * Why the action is not available — {@link Button}'s `disabledReason`, on the icon\n * button: keksdose's accounts page cannot hide an account with a balance or delete\n * one with bookings (accounts-page:879/913 — \"hide requires zero\", \"delete blocked\"),\n * nor its budgets page delete the only budget (budgets-page:330), and each wrapped a\n * natively disabled IconButton in a Tooltip that swaps its label for the reason — a\n * bubble a keyboard never opens, since `disabled` leaves the tab order, and a reason\n * a screen reader never hears.\n *\n * The same contract as Button's: `aria-disabled` instead of `disabled`, so it stays\n * focusable and hoverable; clicks (and the Enter/Space they stand for) are swallowed\n * — `stopPropagation` still applies, so a locked action in a clickable row does not\n * open the row either; the reason shows in the kit {@link Tooltip} and is attached\n * through `aria-describedby` as a `hidden` copy. The NAME stays `aria-label` /\n * `label` — \"Delete\", described by \"The active budget cannot be deleted\" — and the\n * bubble shows the reason in place of the label, since the glyph already says what\n * the button is and the reason is the news. Shown even with `tooltip={false}`: a\n * reason nobody can see is not one. Wins over `disabled`, as on Button.\n *\n * 0.32: at Large and on a touch screen the reason is a line under the button rather\n * than a bubble — see `disabledReasonDisplay`.\n */\n disabledReason?: ReactNode;\n /** Where `disabledReason` shows — {@link Button}'s `disabledReasonDisplay`. */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** This action COMMITS — {@link Button}'s `commit`: under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason. No effect without a lock. A {@link CommitScope} since 0.33. */\n commit?: CommitScope;\n /** The `<button>` element — a prop in React 19, as on {@link Button}. */\n ref?: Ref<HTMLButtonElement>;\n /** Only on the link form — see {@link IconButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<IconButton href>`: an icon-only LINK — keksdose F2 and kastlan 45, whose icon\n * actions that navigate (a row's \"open\", a card's \"edit\" that goes to the edit page, a\n * top bar's settings cog) were `<IconButton onClick={() => navigate(…)}>`: a button that\n * changes the page, with no middle click, no \"open in new tab\" and no URL on hover.\n *\n * The same rule as {@link Button}'s `href` (see {@link ButtonLinkProps}): an in-app\n * `href` goes through `renderLink`, else the provider's `linkComponent`; an external one\n * or an in-page `#anchor` stays a plain `<a>`; `external` opens a new tab and says so to\n * a screen reader; `replace` and `reloadDocument` as on Button. The look, the `label` →\n * `aria-label` + Tooltip, the badge, `toneColor` and `stopPropagation` are the button's.\n *\n * `disabled` renders an inert `<a>` with no `href` — `role=\"link\"`, `aria-disabled`, out\n * of the tab order, dimmed — like Button's disabled link. `pressed`, `disabledReason` and\n * `disabledStyle` are a button's and are typed out: a link is not a toggle, and a link\n * that explains why it cannot be followed is a job for a disabled BUTTON.\n */\nexport interface IconButtonLinkProps\n extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\">,\n Pick<\n IconButtonProps,\n | \"variant\"\n | \"size\"\n | \"glyphSize\"\n | \"badge\"\n | \"stretch\"\n | \"shape\"\n | \"toneColor\"\n | \"tone\"\n | \"quiet\"\n | \"stopPropagation\"\n | \"label\"\n | \"labelVisible\"\n | \"tooltip\"\n | \"tooltipSide\"\n | \"tooltipPortal\"\n | \"tooltipLazy\"\n > {\n href: string;\n /** See {@link ButtonLinkProps.renderLink}. */\n renderLink?: KitLinkComponent;\n /** See {@link ButtonLinkProps.external}. */\n external?: boolean;\n /** See {@link ButtonLinkProps.replace}. */\n replace?: boolean;\n /** See {@link ButtonLinkProps.reloadDocument}. */\n reloadDocument?: boolean;\n /** See {@link ButtonLinkProps.disabled}. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pressed?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n disabledStyle?: never;\n pending?: never;\n}\n\n/**\n * Whether an IconButton draws its `label` as text (see `labelVisible`): at Large and\n * above, with a label to draw, not switched off by the caller, the variant (`overlay`,\n * `shutter`), `tooltip={false}` or a {@link CompactControls} region.\n */\nfunction useIconButtonText(\n label: string | undefined,\n labelVisible: boolean | undefined,\n variant: IconButtonVariant,\n tooltip: boolean,\n): boolean {\n const large = useLargeText();\n const compact = useContext(CompactControlsContext);\n if (!large || label === undefined || label === \"\" || !tooltip) return false;\n const visible = labelVisible ?? (!compact && variant !== \"overlay\" && variant !== \"shutter\");\n return visible;\n}\n\n/** The visible label (see `labelVisible`). */\nfunction IconButtonText({ label }: { label: string }) {\n return <span data-slot=\"icon-button-label\">{label}</span>;\n}\n\n/**\n * `toneColor`: one CSS colour, or one per theme.\n *\n * The pair is two custom properties on the element — `--icon-button-tone-light` and\n * `--icon-button-tone-dark` — and a class that points `--icon-button-tone` at one or the\n * other by the kit's `dark` variant (`.dark` on an ancestor, tokens.css), so the colour\n * follows a theme switch in CSS alone, with no re-render and no JS reading the theme.\n * Both values are any CSS colour; a token that already flips (`var(--warning)`) needs no\n * pair.\n */\nexport type IconButtonToneColor = string | { light: string; dark: string };\n\nconst ICON_BUTTON_TONE_PAIR =\n \"[--icon-button-tone:var(--icon-button-tone-light)] dark:[--icon-button-tone:var(--icon-button-tone-dark)]\";\n\n/** The inline custom properties `toneColor` sets over the caller's `style`. */\nfunction toneColorStyle(toneColor: IconButtonToneColor | undefined, style: CSSProperties | undefined) {\n if (toneColor === undefined) return style;\n const vars: Record<string, string> =\n typeof toneColor === \"string\"\n ? { \"--icon-button-tone\": toneColor }\n : { \"--icon-button-tone-light\": toneColor.light, \"--icon-button-tone-dark\": toneColor.dark };\n return { ...style, ...vars } as CSSProperties;\n}\n\n/** Everything but the element: the look both forms of IconButton share. */\nfunction iconButtonLook({\n variant,\n size,\n stretch,\n glyphSize,\n shape,\n tone,\n quiet,\n toneColor,\n withText = false,\n}: {\n variant: IconButtonVariant;\n size: IconButtonSize;\n stretch: boolean | undefined;\n glyphSize: IconButtonGlyphSize | undefined;\n shape: \"square\" | \"round\";\n tone: IconButtonTone;\n quiet: boolean | undefined;\n toneColor: IconButtonToneColor | undefined;\n /** The label is drawn beside the glyph (see `labelVisible`). */\n withText?: boolean;\n}) {\n const look = variant === \"overlay\" || variant === \"shutter\" ? variant : resolveButtonVariant(variant, \"IconButton variant\");\n return cn(\n ICON_BUTTON_BASE,\n withText\n ? cn(\"font-medium whitespace-nowrap\", stretch ? ICON_BUTTON_TEXT_STRETCH_SIZES[size] : ICON_BUTTON_TEXT_SIZES[size])\n : stretch\n ? ICON_BUTTON_STRETCH_SIZES[size]\n : ICON_BUTTON_SIZES[size],\n // With text the pill is a 48 px target at any size; the square follows its size.\n withText ? TOUCH_TARGET_LARGE : ICON_BUTTON_TOUCH_LARGE[size],\n glyphSize !== undefined && ICON_BUTTON_GLYPH_SIZES[glyphSize],\n // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.\n look === \"overlay\"\n ? ICON_BUTTON_OVERLAY\n : look === \"shutter\"\n ? ICON_BUTTON_SHUTTER\n : buttonVariantClasses[look],\n shape === \"round\" && \"rounded-full\",\n ICON_BUTTON_DISABLED_REST[look],\n iconButtonToneClass(tone, quiet),\n toneColor !== undefined && typeof toneColor !== \"string\" && ICON_BUTTON_TONE_PAIR,\n );\n}\n\n/** The badge beside the control, not in it — see `badge`. The wrapper is only a\n * positioning box: the control inside it keeps its own focus ring and hit area. */\nfunction withIconButtonBadge(control: ReactElement, badge: ReactNode, stretch: boolean | undefined) {\n if (!hasContent(badge)) return control;\n return (\n <span className={cn(\"relative inline-flex shrink-0\", stretch && \"self-stretch\")}>\n {control}\n <span\n aria-hidden\n data-slot=\"icon-button-badge\"\n className=\"pointer-events-none absolute -end-0.5 -bottom-0.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n </span>\n );\n}\n\n// Declared link-first, as Button is, so `<IconButton href>` picks the link props and\n// `ComponentProps<typeof IconButton>` (the LAST signature) is still the button's.\n/**\n * An icon-only button whose `label` is its name and its tooltip.\n *\n * Destructive actions (0.49 KIT-12): a delete, revoke or erase is NOT an icon repeated in\n * every row of a list — one stray tap away, and a bin per row draws the eye to the one\n * thing nobody should do by accident. Put it in the row's menu, `RowActions` (where a\n * `tone: \"danger\"` action goes by default and opens a `ConfirmDialog` first), or in the\n * selection bar (\"2 selected\" → Delete), and confirm it through `useConfirm`. A single\n * standalone delete — a card's own, a bulk bar's — may be an IconButton\n * `tone=\"danger\"`; it still asks first.\n */\nexport function IconButton(props: IconButtonLinkProps): ReactElement;\nexport function IconButton(props: IconButtonProps): ReactElement;\nexport function IconButton(props: IconButtonProps | IconButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <IconButtonLink {...(props as IconButtonLinkProps)} />;\n return <IconButtonElement {...(props as IconButtonProps)} />;\n}\n\nfunction IconButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"ghost\",\n size = \"md\",\n tone: toneProp,\n quiet,\n stopPropagation,\n stretch,\n shape = \"square\",\n toneColor,\n label,\n labelVisible,\n tooltip = true,\n tooltipSide,\n tooltipPortal,\n tooltipLazy = true,\n glyphSize,\n badge,\n className,\n style,\n target,\n rel,\n onClick,\n onKeyDown,\n children: glyph,\n ref,\n ...rest\n}: IconButtonLinkProps) {\n const withText = useIconButtonText(label, labelVisible, variant, tooltip);\n const children = withText ? (\n <>\n {glyph}\n <IconButtonText label={label!} />\n </>\n ) : (\n glyph\n );\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const tone = toneProp ?? (toneColor !== undefined ? \"custom\" : \"default\");\n // Button's rule, word for word: `reloadDocument` bypasses the router, an external\n // href or an in-page `#anchor` never reaches it.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const name = rest[\"aria-label\"] ?? label;\n // \"(opens in a new tab)\" joins the NAME here, not the body: an icon link has no text\n // for an sr-only span to follow, and `aria-label` would override one anyway.\n const ariaLabel = name !== undefined && newTab ? `${name} (${common.opensInNewTab})` : name;\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n if (stopPropagation) e.stopPropagation();\n onClick?.(e);\n };\n const handleKeyDown = (e: KeyboardEvent<HTMLAnchorElement>) => {\n // A link activates on Enter only; that key still bubbles to a clickable row.\n if (stopPropagation && e.key === \"Enter\") e.stopPropagation();\n onKeyDown?.(e);\n };\n const cls = cn(\n iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),\n // BASE's `disabled:` classes never match an `<a>` — Button's disabled link look.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const look = { style: toneColorStyle(toneColor, style), className: cls };\n let control: ReactElement;\n if (disabled) {\n control = (\n <a {...rest} {...look} ref={ref} role=\"link\" aria-disabled=\"true\" aria-label={ariaLabel}>\n {children}\n </a>\n );\n } else if (external || !Link) {\n control = (\n <a\n {...rest}\n {...look}\n ref={ref}\n href={href}\n aria-label={ariaLabel}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? handleClick : replacingClick(handleClick, href, replace, target)}\n onKeyDown={handleKeyDown}\n >\n {children}\n </a>\n );\n } else {\n control = (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...look}\n {...routerLinkNavigation(replace)}\n ref={ref}\n href={href}\n aria-label={ariaLabel}\n target={target}\n rel={rel}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n >\n {children}\n </RenderedButtonLink>\n );\n }\n control = withIconButtonBadge(control, badge, stretch);\n // The words are on the link at Large: a bubble would only repeat them.\n if (!tooltip || withText || label === undefined || label === \"\") return control;\n return (\n // A fragment, as on the button: the bubble says what `aria-label` already does.\n <Tooltip\n label={label}\n side={tooltipSide}\n portal={tooltipPortal}\n lazy={tooltipLazy}\n className={stretch ? \"self-stretch\" : undefined}\n >\n <>{control}</>\n </Tooltip>\n );\n}\n\nfunction IconButtonElement(\n {\n variant = \"ghost\",\n size = \"md\",\n tone: toneProp,\n quiet,\n pressed,\n stopPropagation,\n stretch,\n shape = \"square\",\n toneColor,\n label,\n labelVisible,\n tooltip = true,\n tooltipSide,\n tooltipPortal,\n tooltipLazy = true,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n glyphSize,\n badge,\n disabledStyle = \"dim\",\n className,\n style,\n onClick,\n onKeyDown,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ref,\n ...rest\n }: IconButtonProps,\n) {\n const tone = toneProp ?? (toneColor !== undefined ? \"custom\" : \"default\");\n const keep = disabledStyle === \"keep\";\n const withText = useIconButtonText(label, labelVisible, variant, tooltip);\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n // Pending is inert like a locked button — focusable, every activation swallowed —\n // without the locked look: the spinner is the state.\n const inert = locked || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n ref={ref}\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-label={rest[\"aria-label\"] ?? label}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n style={toneColorStyle(toneColor, style)}\n onClick={(e) => {\n if (stopPropagation) e.stopPropagation();\n // `preventDefault` too: a locked (or pending) submit must not submit its form.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n onKeyDown={(e) => {\n // A button turns Enter/Space into a click of its own, but the KEY still\n // bubbles — and a clickable row listening for Enter would act on it too.\n if (stopPropagation && (e.key === \"Enter\" || e.key === \" \")) e.stopPropagation();\n onKeyDown?.(e);\n }}\n className={cn(\n iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),\n pressed && ICON_BUTTON_PRESSED,\n // The disabled look for the focusable kind of disabled, as on Button.\n locked && (keep ? \"cursor-default\" : \"cursor-not-allowed opacity-50\"),\n keep && \"disabled:cursor-default disabled:opacity-100\",\n pending && \"cursor-progress\",\n className,\n )}\n >\n {pending ? (\n // Decorative, as on Button: `aria-busy` says it. Sized to the glyph it stands\n // in for, in the glyph's colour, so the swap does not change the button's look.\n <Spinner\n label={null}\n className={cn(\n \"border-current/30 border-t-current\",\n glyphSize !== undefined ? ICON_BUTTON_SPINNER_GLYPH[glyphSize] : ICON_BUTTON_SPINNER[size],\n )}\n />\n ) : (\n children\n )}\n {withText && <IconButtonText label={label!} />}\n </button>\n );\n const control = withIconButtonBadge(button, badge, stretch);\n if (locked) {\n return (\n // Button's shape: the reason as a line under it, or the bubble with a hidden copy\n // for the description (see LockedReason).\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n side={tooltipSide}\n portal={tooltipPortal}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {control}\n </LockedReason>\n );\n }\n // The words are on the button at Large: a bubble would only repeat them.\n if (!tooltip || withText || label === undefined || label === \"\") return control;\n return (\n // A fragment, so Tooltip leaves the button's description alone: the bubble says\n // exactly what `aria-label` already does. Lazy by default — see `tooltipLazy`.\n <Tooltip\n label={label}\n side={tooltipSide}\n portal={tooltipPortal}\n lazy={tooltipLazy}\n className={stretch ? \"self-stretch\" : undefined}\n >\n <>{control}</>\n </Tooltip>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/icon-button.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n KeyboardEvent,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { CommitScope } from \"./write-lock\";\nimport { hasContent } from \"../lib/presence\";\nimport { CompactControlsContext, LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport {\n HOVER_INK,\n ICON_BUTTON_PRESSED,\n RenderedButtonLink,\n HIT_AREA_LARGE,\n TOUCH_TARGET_LARGE,\n buttonVariantClasses,\n} from \"./button-look\";\nimport type { ButtonVariant } from \"./button-look\";\n\n// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2\n// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,\n// text-shaped box. This fixes a square box instead and, crucially, forces the\n// child icon to a size set by the BOX (20px at md/sm) via `[&_svg]:size-*` so a\n// caller cannot under-size it. Use it wherever an action is a bare icon\n// (edit/delete/tools) so they all match the top-bar icon buttons and can never\n// drift apart again.\n//\n// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the\n// two re-skin together with the palette.\nconst ICON_BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed\",\n FOCUS_RING_WIDTH,\n);\n\nexport type IconButtonSize = \"2xl\" | \"xl\" | \"lg\" | \"md\" | \"sm\" | \"xs\" | \"2xs\";\n\n// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon\n// that touches its edges. The two small steps are lenkbank's list-row (28px) and\n// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.\nconst ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {\n // 64px with a 28px glyph: a camera's shutter (keksdose invoices/camera-capture.tsx:248,\n // `size-16` by hand) — the one control of a full-screen capture view, thumb-sized.\n \"2xl\": \"size-16 [&_svg]:size-7\",\n // 48px with a 24px glyph: the one primary action of a phone screen or sheet, a step\n // past the 44px minimum rather than on it.\n xl: \"size-12 [&_svg]:size-6\",\n // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's\n // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.\n lg: \"size-11 [&_svg]:size-5\",\n md: \"size-9 [&_svg]:size-5\",\n sm: \"size-8 [&_svg]:size-5\",\n xs: \"size-7 rounded [&_svg]:size-4\",\n \"2xs\": \"size-6 rounded [&_svg]:size-3.5\",\n};\n\n// `stretch`: the same WIDTH and glyph, and a height taken from the row instead of\n// stated — `size-*` sets a height that `self-stretch` cannot override, so the box is\n// written as a width plus a minimum height (the square it would otherwise be).\nconst ICON_BUTTON_STRETCH_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"w-16 min-h-16 self-stretch [&_svg]:size-7\",\n xl: \"w-12 min-h-12 self-stretch [&_svg]:size-6\",\n lg: \"w-11 min-h-11 self-stretch [&_svg]:size-5\",\n md: \"w-9 min-h-9 self-stretch [&_svg]:size-5\",\n sm: \"w-8 min-h-8 self-stretch [&_svg]:size-5\",\n xs: \"w-7 min-h-7 self-stretch rounded [&_svg]:size-4\",\n \"2xs\": \"w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5\",\n};\n\n// The label as visible text beside the glyph (0.32, §4: at Large \"IconButton shows its\n// label\"): the same height and glyph as the square, a width taken from the words. A\n// map of its own rather than `w-auto` over `size-*`, which tailwind-merge cannot undo\n// in one direction.\nconst ICON_BUTTON_TEXT_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"h-16 gap-2 px-4 text-base [&_svg]:size-7\",\n xl: \"h-12 gap-2 px-3.5 text-sm [&_svg]:size-6\",\n lg: \"h-11 gap-2 px-3 text-sm [&_svg]:size-5\",\n md: \"h-9 gap-1.5 px-2.5 text-sm [&_svg]:size-5\",\n sm: \"h-8 gap-1.5 px-2 text-sm [&_svg]:size-5\",\n xs: \"h-7 gap-1 rounded px-1.5 text-xs [&_svg]:size-4\",\n \"2xs\": \"h-6 gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5\",\n};\nconst ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {\n \"2xl\": \"min-h-16 self-stretch gap-2 px-4 text-base [&_svg]:size-7\",\n xl: \"min-h-12 self-stretch gap-2 px-3.5 text-sm [&_svg]:size-6\",\n lg: \"min-h-11 self-stretch gap-2 px-3 text-sm [&_svg]:size-5\",\n md: \"min-h-9 self-stretch gap-1.5 px-2.5 text-sm [&_svg]:size-5\",\n sm: \"min-h-8 self-stretch gap-1.5 px-2 text-sm [&_svg]:size-5\",\n xs: \"min-h-7 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-4\",\n \"2xs\": \"min-h-6 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5\",\n};\n\n// The 48 px touch target at Large (§4), per size. `lg` and up are past it already\n// (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON\n// a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and\n// gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the\n// taps a finger lands round the glyph (`HIT_AREA_LARGE`).\nconst ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {\n \"2xl\": \"\",\n xl: \"\",\n lg: \"\",\n md: TOUCH_TARGET_LARGE,\n sm: TOUCH_TARGET_LARGE,\n xs: TOUCH_TARGET_LARGE,\n \"2xs\": HIT_AREA_LARGE,\n};\n\n// A tone re-colours the glyph without changing what the variant draws around it.\n// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):\n// QUIET is placeholder grey until the pointer or focus arrives, then the tone's\n// family; TONED wears the tone's colour at rest. The defaults are the looks each tone\n// had before `quiet` existed: `danger` is quiet — an action in every row of a list\n// must not shout from every row, so the red arrives only on the one row you are about\n// to act on — while `warning` and `info` are toned, because they mark the ONE thing\n// on the row to notice (keksdose's \"needs review\" flag, its reconcile action at\n// accounts-page:867), and quiet grey would hide the very thing they point out.\n// `muted` is quiet by definition and has no toned look; `default` has no tone.\n//\n// `success` is toned at rest too: the green is the news (keksdose's \"in sync\" chip).\n// `custom` paints from `--icon-button-tone`, which `toneColor` sets — for a glyph whose\n// colour FOLLOWS STATE across tones (keksdose's sync chip: rose, amber, sky, green as\n// the sync moves), where swapping `tone` would do but the colour is one variable.\nexport type IconButtonTone = \"default\" | \"muted\" | \"danger\" | \"warning\" | \"info\" | \"success\" | \"custom\";\n\ntype ColouredTone = \"danger\" | \"warning\" | \"info\" | \"success\" | \"custom\";\n\nconst ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {\n danger: {\n quiet:\n \"text-placeholder hover:bg-danger-soft hover:text-danger focus-visible:ring-danger-strong\",\n toned: \"text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n },\n warning: {\n quiet:\n \"text-placeholder hover:bg-warning-soft hover:text-warning focus-visible:ring-warning-strong\",\n // The ring is warning's loud line (0.33, k27): `--warning-border`, amber-300, was a\n // focus ring at 1.1:1 on the light surfaces.\n toned: \"text-warning hover:bg-warning-soft focus-visible:ring-warning-strong\",\n },\n info: {\n quiet:\n \"text-placeholder hover:bg-info-soft hover:text-info focus-visible:ring-[var(--info-border)]\",\n toned: \"text-info hover:bg-info-soft focus-visible:ring-[var(--info-border)]\",\n },\n success: {\n quiet:\n \"text-placeholder hover:bg-success-soft hover:text-success focus-visible:ring-[var(--success-border)]\",\n toned: \"text-success hover:bg-success-soft focus-visible:ring-[var(--success-border)]\",\n },\n // The hover fill and the ring are MIXED from the one colour, since a custom tone\n // brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a\n // `custom` tone with no colour set is the default look rather than invisible.\n custom: {\n quiet:\n \"text-placeholder hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]\",\n toned:\n \"text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]\",\n },\n};\n\n// The translucent ink on hover, as Button's secondary/ghost (B′, see HOVER_INK).\nconst ICON_BUTTON_MUTED = `text-placeholder ${HOVER_INK} hover:text-primary`;\n\n/** Which tones sit quiet at rest when `quiet` is left out. */\nconst QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {\n danger: true,\n warning: false,\n info: false,\n success: false,\n custom: false,\n};\n\nfunction iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined): string {\n if (tone === \"default\") return \"\";\n if (tone === \"muted\") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);\n const isQuiet = quiet ?? QUIET_BY_DEFAULT[tone];\n const look = ICON_BUTTON_TONES[tone];\n return isQuiet ? cn(look.quiet, ICON_BUTTON_QUIET_DISABLED_REST) : look.toned;\n}\n\n// A disabled button must not answer the pointer. The hover classes above are plain\n// `hover:` (so a caller's `className=\"hover:…\"` still replaces them through\n// tailwind-merge), which means they fire on a disabled button too — the old grey\n// icon lit up on hover while refusing the click. Rather than rewrite every hover as\n// `enabled:hover:` (which never matches the `<a>` that `buttonClasses` also styles,\n// and would out-rank a caller's plain `hover:` override), each variant pins its\n// RESTING look under `disabled:hover:`, which out-ranks any `hover:` by specificity\n// and only ever matches a disabled button.\nconst ICON_BUTTON_DISABLED_REST: Record<ButtonVariant | \"overlay\" | \"shutter\", string> = {\n default: \"disabled:hover:bg-surface-2\",\n secondary: \"disabled:hover:bg-transparent\",\n ghost: \"disabled:hover:bg-transparent\",\n danger: \"disabled:hover:bg-danger\",\n accent: \"disabled:hover:bg-brand\",\n link: \"disabled:hover:bg-transparent disabled:hover:no-underline\",\n overlay: \"disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]\",\n shutter: \"disabled:hover:bg-media-scrim\",\n};\n\n// The quiet looks change the glyph on hover, so they pin their resting glyph the\n// same way. A toned look keeps its glyph on hover and needs no pin.\nconst ICON_BUTTON_QUIET_DISABLED_REST = \"disabled:hover:text-placeholder\";\n\n// `variant=\"overlay\"`: a round, translucent disc for a control that sits ON a photo\n// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).\n// The inverse pair, not the surface one: what has to hold is the contrast between the\n// disc and its glyph, whatever the picture underneath is, and `--bg-inverse` /\n// `--text-inverse` are the one token pair defined as each other's opposite in both\n// themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette\n// can re-point); the blur keeps a busy background from breaking the glyph's edge.\nconst ICON_BUTTON_OVERLAY =\n \"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-inverse backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus-visible:ring-[var(--text-inverse)]\";\n\n// `variant=\"shutter\"`: the camera's release — keksdose's camera-capture.tsx:248 draws\n// it by hand as a 64px circle with a thick white ring round a translucent fill. It sits\n// on a live camera picture, which is dark whatever the theme, so it uses the MEDIA\n// tokens (tokens.css), white ink on a dark scrim in both themes — the inverse pair\n// `overlay` uses would give a dark ring on a dark viewfinder in dark mode. The focus ring stands off by 2px, or it would\n// merge into the ring that is part of the look.\nconst ICON_BUTTON_SHUTTER =\n \"rounded-full border-4 border-media-ink bg-media-scrim text-media-ink backdrop-blur-sm hover:bg-media-scrim-hover focus-visible:ring-media-ink focus-visible:ring-offset-2 focus-visible:ring-offset-media-scrim-hover\";\n\nexport type IconButtonVariant = ButtonVariant | \"overlay\" | \"shutter\";\n\n/**\n * The glyph's size in px, over the one the box size implies — see\n * {@link IconButtonProps.glyphSize}.\n */\nexport type IconButtonGlyphSize = 12 | 14 | 16 | 20 | 24 | 28;\n\nconst ICON_BUTTON_GLYPH_SIZES: Record<IconButtonGlyphSize, string> = {\n 12: \"[&_svg]:size-3\",\n 14: \"[&_svg]:size-3.5\",\n 16: \"[&_svg]:size-4\",\n 20: \"[&_svg]:size-5\",\n 24: \"[&_svg]:size-6\",\n 28: \"[&_svg]:size-7\",\n};\n\n// `pending`'s spinner, the size of the glyph it replaces (the `[&_svg]` rules above\n// size only an svg, and the spinner is a bordered span).\nconst ICON_BUTTON_SPINNER: Record<IconButtonSize, string> = {\n \"2xl\": \"size-7\",\n xl: \"size-6\",\n lg: \"size-5\",\n md: \"size-5\",\n sm: \"size-5\",\n xs: \"size-4\",\n \"2xs\": \"size-3.5\",\n};\nconst ICON_BUTTON_SPINNER_GLYPH: Record<IconButtonGlyphSize, string> = {\n 12: \"size-3\",\n 14: \"size-3.5\",\n 16: \"size-4\",\n 20: \"size-5\",\n 24: \"size-6\",\n 28: \"size-7\",\n};\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Any {@link ButtonVariant}; `overlay` — a round translucent disc for use over\n * an image (see `ICON_BUTTON_OVERLAY`); or `shutter` — a camera's release, a ringed\n * disc over the viewfinder (see `ICON_BUTTON_SHUTTER`; pair it with `size=\"2xl\"`). */\n variant?: IconButtonVariant;\n /** Box size: 2xl = 64px with a 28px icon (a camera shutter), xl = 48px with a 24px\n * icon, lg = 44px (a phone's touch target), md = 36px (matches the top bar),\n * sm = 32px — all three with a 20px icon;\n * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px\n * icon (an action on a chip or a tab). `glyphSize` overrides the icon. */\n size?: IconButtonSize;\n /**\n * The icon's size in px, when the box's own is not the one wanted. keksdose's sync\n * chip (app/sync-status-indicator.tsx:142) is a 24px round chip round a 20px cloud —\n * `size=\"2xs\" glyphSize={20} shape=\"round\"` — where `2xs` alone draws 14px. A number\n * over a new size step because the box and the glyph are two independent choices; a\n * step per combination (`\"sm-lg\"`) would have to be named for every one.\n */\n glyphSize?: IconButtonGlyphSize;\n /**\n * A small mark on the button's corner — the sync chip's 12px check over its cloud\n * (\"all good\", feedback #67). Rendered OUTSIDE the `<button>`, in a wrapper beside\n * it, so the box's `[&_svg]:size-*` rule that sizes the glyph does not blow the badge\n * up to the glyph's size; a bare `<Check />` comes out 12px, on a\n * `--bg-surface` disc that keeps it legible over the glyph. Decorative\n * (`aria-hidden`): the name (`label` / `aria-label`) must already carry the state.\n * With a badge, `className` still lands on the button; the wrapper is the flex item.\n */\n badge?: ReactNode;\n /**\n * `keep`: a disabled button keeps its full colour (no 50% drop) and an ordinary\n * cursor. For a STATUS that is also an action — the sync chip shows \"in sync\" in\n * green and refreshes on click, and is disabled while offline or syncing, when the\n * state it shows matters most; dimmed it read as broken (keksdose wrote\n * `disabled:cursor-default` by hand and left out the dimming). Default `dim`.\n */\n disabledStyle?: \"dim\" | \"keep\";\n /**\n * Fill the height of the flex row it stands in, keeping its width — the delete at\n * the end of keksdose's split line (transaction-fields:162), level with the labelled\n * fields beside it rather than floating at half their height. {@link Button}'s\n * `stretch`; no effect outside a flex row. With a `label`, the tooltip's wrapper is\n * what stretches.\n */\n stretch?: boolean;\n /** `round`: a circle instead of the rounded square — a compact status chip in a top\n * bar (keksdose's sync indicator). `overlay` is always round. */\n shape?: \"square\" | \"round\";\n /**\n * The colour of `tone=\"custom\"`, as any CSS colour — a token (`var(--warning)`),\n * ideally. Sets `--icon-button-tone` on the button, so passing a different one per\n * state re-colours the glyph, its hover fill and its focus ring together. Passing it\n * implies `tone=\"custom\"`. Or leave it out and set the variable yourself from a class\n * (`className=\"[--icon-button-tone:var(--success)]\"`).\n *\n * Or a `{ light, dark }` pair, one colour per theme — keksdose: a raw colour that reads\n * on the light surface (`#be123c`) is too dark on the dark one, and an inline style\n * cannot say \"in dark mode\". See {@link IconButtonToneColor}.\n */\n toneColor?: IconButtonToneColor;\n /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on\n * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a\n * remove/delete that repeats down a list. `warning`: amber at rest — a flag that\n * wants attention. `info`: sky at rest — a notice-worthy but harmless action\n * (keksdose's reconcile). `success`: green at rest — a good state worth seeing.\n * `custom`: the colour of `toneColor` (see there). Default: the variant's own colours. See `quiet` for\n * turning a coloured tone's resting look the other way. */\n tone?: IconButtonTone;\n /**\n * Whether a coloured tone (`danger`, `warning`, `info`) waits for the pointer:\n * `true` is placeholder grey at rest and the tone's colour on hover and focus;\n * `false` wears the tone's colour at rest. Left out, each tone keeps its own\n * default — `danger` quiet, `warning` and `info` not. Ignored for `muted` (quiet by\n * definition) and `default` (no tone).\n *\n * `quiet={false}` on `danger` is for a destructive action that stands ALONE, where\n * hover-to-reveal hides it: keksdose's phone bulk bar (mobile-bulk-bar.tsx) has one\n * \"delete all\" and a touch screen that never hovers, so it painted rose by hand.\n * `quiet` on `warning`/`info` is the same switch the other way, for a flag repeated\n * down a list. A boolean over the tone rather than a new tone (`danger-solid`) or an\n * `emphasis` scale: there are exactly two resting looks, every coloured tone has\n * both, and which one fits is a question about the SITE (alone or repeated, touch or\n * pointer), not about the tone — so it is one switch the family shares.\n */\n quiet?: boolean;\n /**\n * Make it a toggle button. `true` sets `aria-pressed=\"true\"` and draws the \"on\" look\n * (brand glyph on the quiet brand fill); `false` sets `aria-pressed=\"false\"` with the\n * normal look, so a screen reader still hears a toggle that is off. Left out, it is\n * an ordinary button with no `aria-pressed` — or whatever `aria-pressed` the caller\n * passes. For keksdose's budget share toggle (budgets-page:291), which paints its own\n * brand colour over a ghost button today. Keep the `aria-label` the same in both\n * states (\"Share budget\"): the pressed state already says whether it is on.\n */\n pressed?: boolean;\n /** Keep the click (and the Enter/Space that produces it) from reaching an\n * ancestor's handler — for an action inside a clickable table row or card.\n *\n * **Not a licence to put this inside another `<button>`.** HTML forbids any\n * interactive content, and any element with a `tabindex`, inside a button — so\n * the `<span role=\"button\" tabIndex={0}>` workaround is invalid too — and ARIA\n * makes a button's children presentational, so a screen reader flattens the\n * inner one into the outer one's name and it cannot be reached at all. Make the\n * row's main action a button that fills the row and put this one BESIDE it, on\n * top:\n *\n * ```tsx\n * <div className=\"relative\">\n * <button className=\"w-full pe-16 …\" onClick={open}>…row…</button>\n * <div className=\"absolute inset-y-0 end-2 flex items-center gap-0.5\">\n * <IconButton size=\"xs\" tone=\"danger\" aria-label=\"Delete\" onClick={remove}>\n * <Trash2 />\n * </IconButton>\n * </div>\n * </div>\n * ```\n *\n * The pair look exactly like the nested version, click exactly like it, and\n * are two tab stops a screen reader can tell apart. */\n stopPropagation?: boolean;\n /**\n * The button's name, said once: it becomes the `aria-label` AND the text of a kit\n * {@link Tooltip} round the button.\n *\n * Every app writes the pair by hand. lenkbank's nine icon actions all carry\n * `aria-label={t(x)} title={t(x)}` (shared/lib/table-columns.tsx:103,\n * projects-page.tsx:83, setpoint/segment-list.tsx:198/207, profile-bar.tsx:128/142,\n * shortcut-dialog.tsx:815/857/1007) — and `title` is the browser's own tooltip,\n * which shows late, never on focus and never on touch, and looks like no other\n * label in the kit. kastlan's RowAction (shared/components/data-table/row-action.tsx)\n * and keksdose's icon wrappers each exist to put a Tooltip round an IconButton.\n *\n * A caller's own `aria-label` still wins, for the rare name that should be longer\n * than the bubble. The bubble is visual only: it would otherwise describe the button\n * with its own name, and a screen reader would read the same word twice. Tooltip's\n * default placement applies — in place, or portalled inside a scroll container —\n * unless `tooltipPortal` says otherwise.\n */\n label?: string;\n /**\n * Show `label` as visible text beside the icon at Large and Extra large (0.32,\n * docs/text-size-harmonization.md §4, §10.8). A reader who asked for bigger type is\n * the one an unlabelled glyph fails, and on touch the tooltip never opens. Then there\n * is no tooltip: the words are on the button.\n *\n * Default `true` — except for `variant=\"overlay\"` and `\"shutter\"`, which sit ON a\n * picture, and inside {@link CompactControls} (AppShell's top bar, `RowActions`'\n * inline icons). `false`: the icon alone at every size, for an icon on content — a\n * receipt overlay, a map's zoom, a viewfinder, a photo thumbnail. That must be rare;\n * a dense row's actions belong in `RowActions`, which collapses them into a \"⋯\" menu\n * at Large instead (§10.8). Normal never shows it. `tooltip={false}` (a universally\n * read glyph, a dialog's ✕) keeps the icon alone too.\n */\n labelVisible?: boolean;\n /** Show `label` as a tooltip. Default `true`; `false` keeps `label` as the\n * accessible name only — for a button whose glyph is universally read (a close ✕ in\n * a dialog header) or that already sits under a tooltip of its own. It also keeps the\n * label from showing as text at Large (see `labelVisible`). */\n tooltip?: boolean;\n /** Where the `label` tooltip opens. See {@link Tooltip}'s `side`. */\n tooltipSide?: TooltipSide;\n /** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */\n tooltipPortal?: boolean;\n /**\n * Mount the `label` bubble only while it is up — {@link Tooltip}'s `lazy`. Default\n * `true` since 0.16.0; `false` is the always-mounted in-place bubble of before.\n *\n * Why the label bubble can be lazy when Tooltip's default is not: this bubble\n * describes nothing. It is wrapped round the button in a fragment precisely so that\n * it is NOT the button's `aria-describedby` (it would repeat the button's own name),\n * so there is no description for a lazy mount to take away — the name is the\n * `aria-label` either way, read in browse mode and on focus alike. What the\n * always-mounted bubble did do was sit in the DOM as a `role=\"tooltip\"` node full of\n * text: every IconButton in a row added one to `getAllByRole(\"tooltip\")` and its label\n * to the row's `textContent`, and two keksdose tests tripped over that (0.15.5 P6).\n * The cost is the one {@link Tooltip}'s \"Lazy in place\" note names — a test that\n * looked the bubble up without hovering now has to hover (or focus) first; pass\n * `false` while it is migrated. A `disabledReason` bubble is unaffected: that one\n * stays mounted as it was.\n */\n tooltipLazy?: boolean;\n /**\n * Busy — the action is in flight: the glyph is swapped for a spinner of the same\n * size, the button is `aria-busy`, and clicks (and the Enter/Space and form\n * submission they stand for) are swallowed, so a second press cannot start the\n * action twice. {@link Button}'s `pending`, with the same contract: `aria-disabled`\n * rather than `disabled`, so the button that was pressed keeps its focus. The name\n * (`label` / `aria-label`) stays — `aria-busy` is what says it is working. A SWAP\n * rather than Button's overlay: the box is square and fixed, so there is no width\n * for the glyph to hold, and a spinner drawn over a glyph would be two marks in one\n * 20px square.\n */\n pending?: boolean;\n /**\n * Why the action is not available — {@link Button}'s `disabledReason`, on the icon\n * button: keksdose's accounts page cannot hide an account with a balance or delete\n * one with bookings (accounts-page:879/913 — \"hide requires zero\", \"delete blocked\"),\n * nor its budgets page delete the only budget (budgets-page:330), and each wrapped a\n * natively disabled IconButton in a Tooltip that swaps its label for the reason — a\n * bubble a keyboard never opens, since `disabled` leaves the tab order, and a reason\n * a screen reader never hears.\n *\n * The same contract as Button's: `aria-disabled` instead of `disabled`, so it stays\n * focusable and hoverable; clicks (and the Enter/Space they stand for) are swallowed\n * — `stopPropagation` still applies, so a locked action in a clickable row does not\n * open the row either; the reason shows in the kit {@link Tooltip} and is attached\n * through `aria-describedby` as a `hidden` copy. The NAME stays `aria-label` /\n * `label` — \"Delete\", described by \"The active budget cannot be deleted\" — and the\n * bubble shows the reason in place of the label, since the glyph already says what\n * the button is and the reason is the news. Shown even with `tooltip={false}`: a\n * reason nobody can see is not one. Wins over `disabled`, as on Button.\n *\n * 0.32: at Large and on a touch screen the reason is a line under the button rather\n * than a bubble — see `disabledReasonDisplay`.\n */\n disabledReason?: ReactNode;\n /** Where `disabledReason` shows — {@link Button}'s `disabledReasonDisplay`. */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** This action COMMITS — {@link Button}'s `commit`: under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason. No effect without a lock. A {@link CommitScope} since 0.33. */\n commit?: CommitScope;\n /** The `<button>` element — a prop in React 19, as on {@link Button}. */\n ref?: Ref<HTMLButtonElement>;\n /** Only on the link form — see {@link IconButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<IconButton href>`: an icon-only LINK — keksdose F2 and kastlan 45, whose icon\n * actions that navigate (a row's \"open\", a card's \"edit\" that goes to the edit page, a\n * top bar's settings cog) were `<IconButton onClick={() => navigate(…)}>`: a button that\n * changes the page, with no middle click, no \"open in new tab\" and no URL on hover.\n *\n * The same rule as {@link Button}'s `href` (see {@link ButtonLinkProps}): an in-app\n * `href` goes through `renderLink`, else the provider's `linkComponent`; an external one\n * or an in-page `#anchor` stays a plain `<a>`; `external` opens a new tab and says so to\n * a screen reader; `replace` and `reloadDocument` as on Button. The look, the `label` →\n * `aria-label` + Tooltip, the badge, `toneColor` and `stopPropagation` are the button's.\n *\n * `disabled` renders an inert `<a>` with no `href` — `role=\"link\"`, `aria-disabled`, out\n * of the tab order, dimmed — like Button's disabled link. `pressed`, `disabledReason` and\n * `disabledStyle` are a button's and are typed out: a link is not a toggle, and a link\n * that explains why it cannot be followed is a job for a disabled BUTTON.\n */\nexport interface IconButtonLinkProps\n extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\">,\n Pick<\n IconButtonProps,\n | \"variant\"\n | \"size\"\n | \"glyphSize\"\n | \"badge\"\n | \"stretch\"\n | \"shape\"\n | \"toneColor\"\n | \"tone\"\n | \"quiet\"\n | \"stopPropagation\"\n | \"label\"\n | \"labelVisible\"\n | \"tooltip\"\n | \"tooltipSide\"\n | \"tooltipPortal\"\n | \"tooltipLazy\"\n > {\n href: string;\n /** See {@link ButtonLinkProps.renderLink}. */\n renderLink?: KitLinkComponent;\n /** See {@link ButtonLinkProps.external}. */\n external?: boolean;\n /** See {@link ButtonLinkProps.replace}. */\n replace?: boolean;\n /** See {@link ButtonLinkProps.reloadDocument}. */\n reloadDocument?: boolean;\n /** See {@link ButtonLinkProps.disabled}. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pressed?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n disabledStyle?: never;\n pending?: never;\n}\n\n/**\n * Whether an IconButton draws its `label` as text (see `labelVisible`): at Large and\n * above, with a label to draw, not switched off by the caller, the variant (`overlay`,\n * `shutter`), `tooltip={false}` or a {@link CompactControls} region.\n */\nfunction useIconButtonText(\n label: string | undefined,\n labelVisible: boolean | undefined,\n variant: IconButtonVariant,\n tooltip: boolean,\n): boolean {\n const large = useLargeText();\n const compact = useContext(CompactControlsContext);\n if (!large || label === undefined || label === \"\" || !tooltip) return false;\n const visible = labelVisible ?? (!compact && variant !== \"overlay\" && variant !== \"shutter\");\n return visible;\n}\n\n/** The visible label (see `labelVisible`). */\nfunction IconButtonText({ label }: { label: string }) {\n return <span data-slot=\"icon-button-label\">{label}</span>;\n}\n\n/**\n * `toneColor`: one CSS colour, or one per theme.\n *\n * The pair is two custom properties on the element — `--icon-button-tone-light` and\n * `--icon-button-tone-dark` — and a class that points `--icon-button-tone` at one or the\n * other by the kit's `dark` variant (`.dark` on an ancestor, tokens.css), so the colour\n * follows a theme switch in CSS alone, with no re-render and no JS reading the theme.\n * Both values are any CSS colour; a token that already flips (`var(--warning)`) needs no\n * pair.\n */\nexport type IconButtonToneColor = string | { light: string; dark: string };\n\nconst ICON_BUTTON_TONE_PAIR =\n \"[--icon-button-tone:var(--icon-button-tone-light)] dark:[--icon-button-tone:var(--icon-button-tone-dark)]\";\n\n/** The inline custom properties `toneColor` sets over the caller's `style`. */\nfunction toneColorStyle(toneColor: IconButtonToneColor | undefined, style: CSSProperties | undefined) {\n if (toneColor === undefined) return style;\n const vars: Record<string, string> =\n typeof toneColor === \"string\"\n ? { \"--icon-button-tone\": toneColor }\n : { \"--icon-button-tone-light\": toneColor.light, \"--icon-button-tone-dark\": toneColor.dark };\n return { ...style, ...vars } as CSSProperties;\n}\n\n/** Everything but the element: the look both forms of IconButton share. */\nfunction iconButtonLook({\n variant,\n size,\n stretch,\n glyphSize,\n shape,\n tone,\n quiet,\n toneColor,\n withText = false,\n}: {\n variant: IconButtonVariant;\n size: IconButtonSize;\n stretch: boolean | undefined;\n glyphSize: IconButtonGlyphSize | undefined;\n shape: \"square\" | \"round\";\n tone: IconButtonTone;\n quiet: boolean | undefined;\n toneColor: IconButtonToneColor | undefined;\n /** The label is drawn beside the glyph (see `labelVisible`). */\n withText?: boolean;\n}) {\n return cn(\n ICON_BUTTON_BASE,\n withText\n ? cn(\"font-medium whitespace-nowrap\", stretch ? ICON_BUTTON_TEXT_STRETCH_SIZES[size] : ICON_BUTTON_TEXT_SIZES[size])\n : stretch\n ? ICON_BUTTON_STRETCH_SIZES[size]\n : ICON_BUTTON_SIZES[size],\n // With text the pill is a 48 px target at any size; the square follows its size.\n withText ? TOUCH_TARGET_LARGE : ICON_BUTTON_TOUCH_LARGE[size],\n glyphSize !== undefined && ICON_BUTTON_GLYPH_SIZES[glyphSize],\n // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.\n variant === \"overlay\"\n ? ICON_BUTTON_OVERLAY\n : variant === \"shutter\"\n ? ICON_BUTTON_SHUTTER\n : buttonVariantClasses[variant],\n shape === \"round\" && \"rounded-full\",\n ICON_BUTTON_DISABLED_REST[variant],\n iconButtonToneClass(tone, quiet),\n toneColor !== undefined && typeof toneColor !== \"string\" && ICON_BUTTON_TONE_PAIR,\n );\n}\n\n/** The badge beside the control, not in it — see `badge`. The wrapper is only a\n * positioning box: the control inside it keeps its own focus ring and hit area. */\nfunction withIconButtonBadge(control: ReactElement, badge: ReactNode, stretch: boolean | undefined) {\n if (!hasContent(badge)) return control;\n return (\n <span className={cn(\"relative inline-flex shrink-0\", stretch && \"self-stretch\")}>\n {control}\n <span\n aria-hidden\n data-slot=\"icon-button-badge\"\n className=\"pointer-events-none absolute -end-0.5 -bottom-0.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n </span>\n );\n}\n\n// Declared link-first, as Button is, so `<IconButton href>` picks the link props and\n// `ComponentProps<typeof IconButton>` (the LAST signature) is still the button's.\n/**\n * An icon-only button whose `label` is its name and its tooltip.\n *\n * Destructive actions (0.49 KIT-12): a delete, revoke or erase is NOT an icon repeated in\n * every row of a list — one stray tap away, and a bin per row draws the eye to the one\n * thing nobody should do by accident. Put it in the row's menu, `RowActions` (where a\n * `tone: \"danger\"` action goes by default and opens a `ConfirmDialog` first), or in the\n * selection bar (\"2 selected\" → Delete), and confirm it through `useConfirm`. A single\n * standalone delete — a card's own, a bulk bar's — may be an IconButton\n * `tone=\"danger\"`; it still asks first.\n */\nexport function IconButton(props: IconButtonLinkProps): ReactElement;\nexport function IconButton(props: IconButtonProps): ReactElement;\nexport function IconButton(props: IconButtonProps | IconButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <IconButtonLink {...(props as IconButtonLinkProps)} />;\n return <IconButtonElement {...(props as IconButtonProps)} />;\n}\n\nfunction IconButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"ghost\",\n size = \"md\",\n tone: toneProp,\n quiet,\n stopPropagation,\n stretch,\n shape = \"square\",\n toneColor,\n label,\n labelVisible,\n tooltip = true,\n tooltipSide,\n tooltipPortal,\n tooltipLazy = true,\n glyphSize,\n badge,\n className,\n style,\n target,\n rel,\n onClick,\n onKeyDown,\n children: glyph,\n ref,\n ...rest\n}: IconButtonLinkProps) {\n const withText = useIconButtonText(label, labelVisible, variant, tooltip);\n const children = withText ? (\n <>\n {glyph}\n <IconButtonText label={label!} />\n </>\n ) : (\n glyph\n );\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const tone = toneProp ?? (toneColor !== undefined ? \"custom\" : \"default\");\n // Button's rule, word for word: `reloadDocument` bypasses the router, an external\n // href or an in-page `#anchor` never reaches it.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const name = rest[\"aria-label\"] ?? label;\n // \"(opens in a new tab)\" joins the NAME here, not the body: an icon link has no text\n // for an sr-only span to follow, and `aria-label` would override one anyway.\n const ariaLabel = name !== undefined && newTab ? `${name} (${common.opensInNewTab})` : name;\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n if (stopPropagation) e.stopPropagation();\n onClick?.(e);\n };\n const handleKeyDown = (e: KeyboardEvent<HTMLAnchorElement>) => {\n // A link activates on Enter only; that key still bubbles to a clickable row.\n if (stopPropagation && e.key === \"Enter\") e.stopPropagation();\n onKeyDown?.(e);\n };\n const cls = cn(\n iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),\n // BASE's `disabled:` classes never match an `<a>` — Button's disabled link look.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const look = { style: toneColorStyle(toneColor, style), className: cls };\n let control: ReactElement;\n if (disabled) {\n control = (\n <a {...rest} {...look} ref={ref} role=\"link\" aria-disabled=\"true\" aria-label={ariaLabel}>\n {children}\n </a>\n );\n } else if (external || !Link) {\n control = (\n <a\n {...rest}\n {...look}\n ref={ref}\n href={href}\n aria-label={ariaLabel}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? handleClick : replacingClick(handleClick, href, replace, target)}\n onKeyDown={handleKeyDown}\n >\n {children}\n </a>\n );\n } else {\n control = (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...look}\n {...routerLinkNavigation(replace)}\n ref={ref}\n href={href}\n aria-label={ariaLabel}\n target={target}\n rel={rel}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n >\n {children}\n </RenderedButtonLink>\n );\n }\n control = withIconButtonBadge(control, badge, stretch);\n // The words are on the link at Large: a bubble would only repeat them.\n if (!tooltip || withText || label === undefined || label === \"\") return control;\n return (\n // A fragment, as on the button: the bubble says what `aria-label` already does.\n <Tooltip\n label={label}\n side={tooltipSide}\n portal={tooltipPortal}\n lazy={tooltipLazy}\n className={stretch ? \"self-stretch\" : undefined}\n >\n <>{control}</>\n </Tooltip>\n );\n}\n\nfunction IconButtonElement(\n {\n variant = \"ghost\",\n size = \"md\",\n tone: toneProp,\n quiet,\n pressed,\n stopPropagation,\n stretch,\n shape = \"square\",\n toneColor,\n label,\n labelVisible,\n tooltip = true,\n tooltipSide,\n tooltipPortal,\n tooltipLazy = true,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n glyphSize,\n badge,\n disabledStyle = \"dim\",\n className,\n style,\n onClick,\n onKeyDown,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ref,\n ...rest\n }: IconButtonProps,\n) {\n const tone = toneProp ?? (toneColor !== undefined ? \"custom\" : \"default\");\n const keep = disabledStyle === \"keep\";\n const withText = useIconButtonText(label, labelVisible, variant, tooltip);\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n // Pending is inert like a locked button — focusable, every activation swallowed —\n // without the locked look: the spinner is the state.\n const inert = locked || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n ref={ref}\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-label={rest[\"aria-label\"] ?? label}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n style={toneColorStyle(toneColor, style)}\n onClick={(e) => {\n if (stopPropagation) e.stopPropagation();\n // `preventDefault` too: a locked (or pending) submit must not submit its form.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n onKeyDown={(e) => {\n // A button turns Enter/Space into a click of its own, but the KEY still\n // bubbles — and a clickable row listening for Enter would act on it too.\n if (stopPropagation && (e.key === \"Enter\" || e.key === \" \")) e.stopPropagation();\n onKeyDown?.(e);\n }}\n className={cn(\n iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),\n pressed && ICON_BUTTON_PRESSED,\n // The disabled look for the focusable kind of disabled, as on Button.\n locked && (keep ? \"cursor-default\" : \"cursor-not-allowed opacity-50\"),\n keep && \"disabled:cursor-default disabled:opacity-100\",\n pending && \"cursor-progress\",\n className,\n )}\n >\n {pending ? (\n // Decorative, as on Button: `aria-busy` says it. Sized to the glyph it stands\n // in for, in the glyph's colour, so the swap does not change the button's look.\n <Spinner\n label={null}\n className={cn(\n \"border-current/30 border-t-current\",\n glyphSize !== undefined ? ICON_BUTTON_SPINNER_GLYPH[glyphSize] : ICON_BUTTON_SPINNER[size],\n )}\n />\n ) : (\n children\n )}\n {withText && <IconButtonText label={label!} />}\n </button>\n );\n const control = withIconButtonBadge(button, badge, stretch);\n if (locked) {\n return (\n // Button's shape: the reason as a line under it, or the bubble with a hidden copy\n // for the description (see LockedReason).\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n side={tooltipSide}\n portal={tooltipPortal}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {control}\n </LockedReason>\n );\n }\n // The words are on the button at Large: a bubble would only repeat them.\n if (!tooltip || withText || label === undefined || label === \"\") return control;\n return (\n // A fragment, so Tooltip leaves the button's description alone: the bubble says\n // exactly what `aria-label` already does. Lazy by default — see `tooltipLazy`.\n <Tooltip\n label={label}\n side={tooltipSide}\n portal={tooltipPortal}\n lazy={tooltipLazy}\n className={stretch ? \"self-stretch\" : undefined}\n >\n <>{control}</>\n </Tooltip>\n );\n}\n"],"mappings":";AAwkBS,SAmJL,UAnJK,KA+EL,YA/EK;AAxkBT,SAAS,YAAY,aAAa;AAWlC,SAAS,UAAU;AACnB,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAC7B,SAAS,eAAiC;AAC1C,SAAS,uBAAuB,cAAc,kBAAkB;AAEhE,SAAS,kBAAkB,gBAAgB,4BAA4B;AACvE,SAAS,uBAAuB;AAEhC,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB,oBAAgD;AACjF,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAaP,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AACF;AAOA,MAAM,oBAAoD;AAAA;AAAA;AAAA,EAGxD,OAAO;AAAA;AAAA;AAAA,EAGP,IAAI;AAAA;AAAA;AAAA,EAGJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AAKA,MAAM,4BAA4D;AAAA,EAChE,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AAMA,MAAM,yBAAyD;AAAA,EAC7D,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AACA,MAAM,iCAAiE;AAAA,EACrE,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AAOA,MAAM,0BAA0D;AAAA,EAC9D,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AAqBA,MAAM,oBAA4E;AAAA,EAChF,QAAQ;AAAA,IACN,OACE;AAAA,IACF,OAAO;AAAA,EACT;AAAA,EACA,SAAS;AAAA,IACP,OACE;AAAA;AAAA;AAAA,IAGF,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,OACE;AAAA,IACF,OAAO;AAAA,EACT;AAAA,EACA,SAAS;AAAA,IACP,OACE;AAAA,IACF,OAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAIA,QAAQ;AAAA,IACN,OACE;AAAA,IACF,OACE;AAAA,EACJ;AACF;AAGA,MAAM,oBAAoB,oBAAoB,SAAS;AAGvD,MAAM,mBAAkD;AAAA,EACtD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,QAAQ;AACV;AAEA,SAAS,oBAAoB,MAAsB,OAAoC;AACrF,MAAI,SAAS,UAAW,QAAO;AAC/B,MAAI,SAAS,QAAS,QAAO,GAAG,mBAAmB,+BAA+B;AAClF,QAAM,UAAU,SAAS,iBAAiB,IAAI;AAC9C,QAAM,OAAO,kBAAkB,IAAI;AACnC,SAAO,UAAU,GAAG,KAAK,OAAO,+BAA+B,IAAI,KAAK;AAC1E;AAUA,MAAM,4BAAmF;AAAA,EACvF,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACX;AAIA,MAAM,kCAAkC;AASxC,MAAM,sBACJ;AAQF,MAAM,sBACJ;AAUF,MAAM,0BAA+D;AAAA,EACnE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAIA,MAAM,sBAAsD;AAAA,EAC1D,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AACT;AACA,MAAM,4BAAiE;AAAA,EACrE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AA2SA,SAAS,kBACP,OACA,cACA,SACA,SACS;AACT,QAAM,QAAQ,aAAa;AAC3B,QAAM,UAAU,WAAW,sBAAsB;AACjD,MAAI,CAAC,SAAS,UAAU,UAAa,UAAU,MAAM,CAAC,QAAS,QAAO;AACtE,QAAM,UAAU,iBAAiB,CAAC,WAAW,YAAY,aAAa,YAAY;AAClF,SAAO;AACT;AAGA,SAAS,eAAe,EAAE,MAAM,GAAsB;AACpD,SAAO,oBAAC,UAAK,aAAU,qBAAqB,iBAAM;AACpD;AAcA,MAAM,wBACJ;AAGF,SAAS,eAAe,WAA4C,OAAkC;AACpG,MAAI,cAAc,OAAW,QAAO;AACpC,QAAM,OACJ,OAAO,cAAc,WACjB,EAAE,sBAAsB,UAAU,IAClC,EAAE,4BAA4B,UAAU,OAAO,2BAA2B,UAAU,KAAK;AAC/F,SAAO,EAAE,GAAG,OAAO,GAAG,KAAK;AAC7B;AAGA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAWG;AACD,SAAO;AAAA,IACL;AAAA,IACA,WACI,GAAG,iCAAiC,UAAU,+BAA+B,IAAI,IAAI,uBAAuB,IAAI,CAAC,IACjH,UACE,0BAA0B,IAAI,IAC9B,kBAAkB,IAAI;AAAA;AAAA,IAE5B,WAAW,qBAAqB,wBAAwB,IAAI;AAAA,IAC5D,cAAc,UAAa,wBAAwB,SAAS;AAAA;AAAA,IAE5D,YAAY,YACR,sBACA,YAAY,YACV,sBACA,qBAAqB,OAAO;AAAA,IAClC,UAAU,WAAW;AAAA,IACrB,0BAA0B,OAAO;AAAA,IACjC,oBAAoB,MAAM,KAAK;AAAA,IAC/B,cAAc,UAAa,OAAO,cAAc,YAAY;AAAA,EAC9D;AACF;AAIA,SAAS,oBAAoB,SAAuB,OAAkB,SAA8B;AAClG,MAAI,CAAC,WAAW,KAAK,EAAG,QAAO;AAC/B,SACE,qBAAC,UAAK,WAAW,GAAG,iCAAiC,WAAW,cAAc,GAC3E;AAAA;AAAA,IACD;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,aAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAiBO,SAAS,WAAW,OAA4D;AACrF,MAAI,MAAM,SAAS,OAAW,QAAO,oBAAC,kBAAgB,GAAI,OAA+B;AACzF,SAAO,oBAAC,qBAAmB,GAAI,OAA2B;AAC5D;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,QAAM,WAAW,kBAAkB,OAAO,cAAc,SAAS,OAAO;AACxE,QAAM,WAAW,WACf,iCACG;AAAA;AAAA,IACD,oBAAC,kBAAe,OAAe;AAAA,KACjC,IAEA;AAEF,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,OAAO,aAAa,cAAc,SAAY,WAAW;AAG/D,QAAM,OAAO,iBAAiB,SAAY,iBAA+B,YAAY,SAAS,IAAI;AAClG,QAAM,SAAS,CAAC,aAAa,YAAY,WAAW;AACpD,QAAM,OAAO,KAAK,YAAY,KAAK;AAGnC,QAAM,YAAY,SAAS,UAAa,SAAS,GAAG,IAAI,KAAK,OAAO,aAAa,MAAM;AACvF,QAAM,cAAc,CAAC,MAAqC;AACxD,QAAI,gBAAiB,GAAE,gBAAgB;AACvC,cAAU,CAAC;AAAA,EACb;AACA,QAAM,gBAAgB,CAAC,MAAwC;AAE7D,QAAI,mBAAmB,EAAE,QAAQ,QAAS,GAAE,gBAAgB;AAC5D,gBAAY,CAAC;AAAA,EACf;AACA,QAAM,MAAM;AAAA,IACV,eAAe,EAAE,SAAS,MAAM,SAAS,WAAW,OAAO,MAAM,OAAO,WAAW,SAAS,CAAC;AAAA;AAAA,IAE7F,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OAAO,EAAE,OAAO,eAAe,WAAW,KAAK,GAAG,WAAW,IAAI;AACvE,MAAI;AACJ,MAAI,UAAU;AACZ,cACE,oBAAC,OAAG,GAAG,MAAO,GAAG,MAAM,KAAU,MAAK,QAAO,iBAAc,QAAO,cAAY,WAC3E,UACH;AAAA,EAEJ,WAAW,YAAY,CAAC,MAAM;AAC5B,cACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACH,GAAG;AAAA,QACJ;AAAA,QACA;AAAA,QACA,cAAY;AAAA,QACZ,QAAQ,WAAW,WAAW;AAAA,QAC9B,KAAK,WAAY,OAAO,wBAAyB;AAAA,QACjD,SAAS,WAAW,cAAc,eAAe,aAAa,MAAM,SAAS,MAAM;AAAA,QACnF,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ,OAAO;AACL,cACE;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ;AAAA,QACP,GAAG;AAAA,QACH,GAAG;AAAA,QACH,GAAG,qBAAqB,OAAO;AAAA,QAChC;AAAA,QACA;AAAA,QACA,cAAY;AAAA,QACZ;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,YAAU,oBAAoB,SAAS,OAAO,OAAO;AAErD,MAAI,CAAC,WAAW,YAAY,UAAU,UAAa,UAAU,GAAI,QAAO;AACxE;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,WAAW,UAAU,iBAAiB;AAAA,QAEtC,0CAAG,mBAAQ;AAAA;AAAA,IACb;AAAA;AAEJ;AAEA,SAAS,kBACP;AAAA,EACE,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,GACA;AACA,QAAM,OAAO,aAAa,cAAc,SAAY,WAAW;AAC/D,QAAM,OAAO,kBAAkB;AAC/B,QAAM,WAAW,kBAAkB,OAAO,cAAc,SAAS,OAAO;AACxE,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,gBAAgB,QAAQ,iBAAiB;AAChE,QAAM,SAAS,WAAW,cAAc;AAGxC,QAAM,QAAQ,UAAU,QAAQ,OAAO;AACvC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,UAAU,QAAQ,SAAY,KAAK;AAAA,MACnC,iBAAe,SAAS,KAAK,eAAe;AAAA,MAC5C,aAAW,WAAW,KAAK,WAAW;AAAA,MACtC,oBAAkB,SAAU,iBAAiB,GAAG,cAAc,IAAI,QAAQ,KAAK,WAAY;AAAA,MAC3F,cAAY,KAAK,YAAY,KAAK;AAAA,MAClC,gBAAc,WAAW,KAAK,cAAc;AAAA,MAC5C,OAAO,eAAe,WAAW,KAAK;AAAA,MACtC,SAAS,CAAC,MAAM;AACd,YAAI,gBAAiB,GAAE,gBAAgB;AAEvC,YAAI,OAAO;AACT,YAAE,eAAe;AACjB;AAAA,QACF;AACA,kBAAU,CAAC;AAAA,MACb;AAAA,MACA,WAAW,CAAC,MAAM;AAGhB,YAAI,oBAAoB,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAM,GAAE,gBAAgB;AAC/E,oBAAY,CAAC;AAAA,MACf;AAAA,MACA,WAAW;AAAA,QACT,eAAe,EAAE,SAAS,MAAM,SAAS,WAAW,OAAO,MAAM,OAAO,WAAW,SAAS,CAAC;AAAA,QAC7F,WAAW;AAAA;AAAA,QAEX,WAAW,OAAO,mBAAmB;AAAA,QACrC,QAAQ;AAAA,QACR,WAAW;AAAA,QACX;AAAA,MACF;AAAA,MAEC;AAAA;AAAA;AAAA;AAAA,UAGC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,SAAY,0BAA0B,SAAS,IAAI,oBAAoB,IAAI;AAAA,cAC3F;AAAA;AAAA,UACF;AAAA,YAEA;AAAA,QAED,YAAY,oBAAC,kBAAe,OAAe;AAAA;AAAA;AAAA,EAC9C;AAEF,QAAM,UAAU,oBAAoB,QAAQ,OAAO,OAAO;AAC1D,MAAI,QAAQ;AACV;AAAA;AAAA;AAAA,MAGE;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,EAAE,QAAQ,gBAAgB,SAAS;AAAA,UACzC,SAAS;AAAA,UACT,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,WAAW,UAAU,iBAAiB;AAAA,UAErC;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEA,MAAI,CAAC,WAAW,YAAY,UAAU,UAAa,UAAU,GAAI,QAAO;AACxE;AAAA;AAAA;AAAA,IAGE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,WAAW,UAAU,iBAAiB;AAAA,QAEtC,0CAAG,mBAAQ;AAAA;AAAA,IACb;AAAA;AAEJ;","names":[]}
|
|
@@ -7,6 +7,7 @@ import { cn } from "../lib/cn.js";
|
|
|
7
7
|
import { useDropdown } from "./dropdown.js";
|
|
8
8
|
import { takeEscape, useDismissLayer } from "../hooks/use-dismiss.js";
|
|
9
9
|
import { isImeEnter } from "../lib/ime.js";
|
|
10
|
+
import { isQuietFocus } from "../lib/quiet-focus.js";
|
|
10
11
|
import { usePhoneLayout } from "../hooks/use-breakpoint.js";
|
|
11
12
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet.js";
|
|
12
13
|
import {
|
|
@@ -85,6 +86,7 @@ function InlineEntityCombobox(props) {
|
|
|
85
86
|
const isPhone = usePhoneLayout();
|
|
86
87
|
const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });
|
|
87
88
|
const primaryOnly = usePrimaryPressOnly();
|
|
89
|
+
const quietRef = useRef(false);
|
|
88
90
|
const [active, setActive] = useState(-1);
|
|
89
91
|
const [text, setText] = useState(null);
|
|
90
92
|
const sheetInputRef = useRef(null);
|
|
@@ -235,11 +237,21 @@ function InlineEntityCombobox(props) {
|
|
|
235
237
|
onFocus: (e) => {
|
|
236
238
|
if (primaryOnly.fromAuxButton() || locked) return;
|
|
237
239
|
setText(shown);
|
|
240
|
+
if (isQuietFocus(e.currentTarget)) {
|
|
241
|
+
quietRef.current = true;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
238
244
|
e.currentTarget.select();
|
|
239
245
|
setOpen(true);
|
|
240
246
|
if (isPhone) sheetInputRef.current?.focus();
|
|
241
247
|
},
|
|
248
|
+
onClick: () => {
|
|
249
|
+
if (!quietRef.current || locked) return;
|
|
250
|
+
quietRef.current = false;
|
|
251
|
+
setOpen(true);
|
|
252
|
+
},
|
|
242
253
|
onBlur: () => {
|
|
254
|
+
quietRef.current = false;
|
|
243
255
|
if (!isPhone) reconcile();
|
|
244
256
|
},
|
|
245
257
|
onChange: (e) => {
|
|
@@ -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 { 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 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 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 onBlur={() => {\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 { 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"]}
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
FieldChevron,
|
|
17
17
|
FieldGroup,
|
|
18
18
|
FloatingField,
|
|
19
|
+
SELECT_MENU_DRESS,
|
|
20
|
+
SELECT_SM,
|
|
19
21
|
fieldBelow
|
|
20
22
|
} from "./field-base.js";
|
|
21
|
-
const SELECT_SM = "h-7 py-0 ps-2 pe-7 text-xs";
|
|
22
23
|
const LOCKED_SELECT_KEYS = /* @__PURE__ */ new Set(["Tab", "Escape", "Shift"]);
|
|
23
24
|
const FIELD_LOCKED = "cursor-not-allowed bg-surface-2 text-muted";
|
|
24
25
|
function Select(props) {
|
|
@@ -63,7 +64,7 @@ function Select(props) {
|
|
|
63
64
|
);
|
|
64
65
|
const below = fieldBelow(hintParts, null, errorEl);
|
|
65
66
|
const chevron = rest.disabled || listBox || locked ? null : /* @__PURE__ */ jsx(FieldChevron, { className: small ? "end-1.5 size-3.5" : void 0 });
|
|
66
|
-
const dress = listBox ? "overflow-y-auto [&_option]:py-1" :
|
|
67
|
+
const dress = listBox ? "overflow-y-auto [&_option]:py-1" : SELECT_MENU_DRESS;
|
|
67
68
|
const interaction = locked ? {
|
|
68
69
|
disabled: void 0,
|
|
69
70
|
"aria-disabled": true,
|