@eifi1/ui-kit 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/components/amount-input.d.ts +23 -1
- package/dist/components/amount-input.js +78 -13
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.js +20 -15
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +16 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table.js +28 -20
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/entity-combobox.js +22 -17
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +202 -13
- package/dist/components/error-boundary.js +225 -21
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/list.d.ts +14 -2
- package/dist/components/list.js +3 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +23 -17
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -2
- package/dist/components/numpad-sheet.js +6 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +3 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -1
- package/dist/components/signed-amount.d.ts +2 -1
- package/dist/components/signed-amount.js +1 -1
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.js +1 -1
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.d.ts +73 -11
- package/dist/components/text-link.js +45 -10
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/toggle-group.d.ts +22 -0
- package/dist/components/toggle-group.js +8 -8
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +17 -0
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +166 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +5 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +43 -3
- package/dist/feedback/feedback-thread.js +53 -26
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/de-informal.d.ts +1 -1
- package/dist/i18n/locales/de.d.ts +1 -1
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/{kit-labels-v3biUF1L.d.ts → kit-labels-Mz8zZHIF.d.ts} +102 -3
- package/dist/lib/format.d.ts +23 -5
- package/dist/lib/format.js +4 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -15
- package/dist/rhf/fields.js +41 -11
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf.d.ts +2 -2
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js +62 -54
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +9 -3
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +11 -1
- package/dist/wizard/wizard-step.js +24 -6
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +6 -6
- package/src/components/amount-input.tsx +165 -12
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +6 -0
- package/src/components/combobox-core.tsx +28 -9
- package/src/components/combobox.tsx +2 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table.tsx +6 -0
- package/src/components/entity-combobox.tsx +7 -4
- package/src/components/error-boundary.tsx +470 -30
- package/src/components/field-sync.tsx +48 -40
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/list.tsx +17 -3
- package/src/components/modal.tsx +4 -0
- package/src/components/multi-entity-combobox.tsx +8 -4
- package/src/components/number-input.tsx +1 -0
- package/src/components/numpad-sheet.tsx +17 -3
- package/src/components/page-header.tsx +42 -5
- package/src/components/picker-sheet.tsx +3 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/signed-amount.tsx +12 -3
- package/src/components/stat-tile.tsx +3 -1
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/text-link.tsx +126 -18
- package/src/components/text.tsx +47 -2
- package/src/components/toggle-group.tsx +35 -10
- package/src/components/toggle-legend.tsx +22 -1
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +307 -39
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +7 -4
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +109 -33
- package/src/i18n/kit-labels.tsx +16 -0
- package/src/i18n/locales/de.ts +8 -0
- package/src/i18n/locales/es.ts +8 -0
- package/src/i18n/locales/fr.ts +8 -0
- package/src/i18n/locales/hu.ts +8 -0
- package/src/i18n/locales/it.ts +8 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/lib/format.ts +32 -7
- package/src/rhf/fields.tsx +99 -16
- package/src/rhf.ts +1 -1
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +6 -1
- package/src/shell/topbar-action-menu.tsx +11 -4
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-step.tsx +38 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\"),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/**\n * How tall the palette's rows are. `\"compact\"` (the default) is the desktop list the\n * palette always drew, 36px a row; `\"comfortable\"` gives every row the kit's 44px touch\n * target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row\n * under a thumb picks its neighbour.\n */\nexport type CommandPaletteDensity = \"compact\" | \"comfortable\";\n\n/** A provider's answer is a promise — the only case with anything to wait for. */\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as PromiseLike<unknown> | null)?.then === \"function\";\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * A SYNCHRONOUS provider (one returning an array, not a promise) never shows the\n * \"Searching…\" hint: its answer is there the moment it is asked, and the hint only\n * flashed through the debounce before results that were never late — keksdose's\n * transaction search filters a list already in memory. The palette tells the two\n * apart by what the provider RETURNS, call by call, so there is no prop to keep in\n * step with the implementation: an async provider shows the hint from the first\n * promise on (and during the debounce of every later query), a sync one never.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n /** Row height — see {@link CommandPaletteDensity}. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n density = \"compact\",\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n /** What the provider's last answer was: a promise (`false`), an array (`true`), or\n * not known yet. Only a provider known to be async shows \"Searching…\" while the\n * debounce runs — see `search`. */\n const syncRef = useRef<boolean | undefined>(undefined);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop).\n useEffect(() => {\n if (!open) return;\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n shownQuery.current = null;\n // `controlled` is read at the open, not watched: a caller switching modes while\n // the palette is open is not a reason to wipe what is typed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n if (syncRef.current === false) setLoading(true);\n const run = async () => {\n try {\n const answer = searchRef.current(query);\n syncRef.current = !isThenable(answer);\n if (!syncRef.current) setLoading(true);\n const r = await answer;\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n // Optional-called, as in combobox-core: jsdom has no scrollIntoView, and every\n // consumer's tests crashed on the first highlighted row (kastlan stubbed it).\n el?.scrollIntoView?.({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n const comfortable = density === \"comfortable\";\n let flatIndex = -1;\n return createPortal(\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className={cn(\n \"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]\",\n comfortable ? \"min-h-11 py-2.5\" : \"py-2\",\n )}\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 text-start\",\n comfortable ? \"min-h-11 py-2.5 text-base\" : \"py-2 text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n const flat = order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\");\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat,\n /** Each selectable item's position in `flat` — its `data-index` and keyboard slot. */\n flatIndex: new Map(flat.map((item, index) => [item, index])),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/**\n * How tall the palette's rows are. `\"compact\"` (the default) is the desktop list the\n * palette always drew, 36px a row; `\"comfortable\"` gives every row the kit's 44px touch\n * target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row\n * under a thumb picks its neighbour.\n */\nexport type CommandPaletteDensity = \"compact\" | \"comfortable\";\n\n/** A provider's answer is a promise — the only case with anything to wait for. */\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as PromiseLike<unknown> | null)?.then === \"function\";\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * A SYNCHRONOUS provider (one returning an array, not a promise) never shows the\n * \"Searching…\" hint: its answer is there the moment it is asked, and the hint only\n * flashed through the debounce before results that were never late — keksdose's\n * transaction search filters a list already in memory. The palette tells the two\n * apart by what the provider RETURNS, call by call, so there is no prop to keep in\n * step with the implementation: an async provider shows the hint from the first\n * promise on (and during the debounce of every later query), a sync one never.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n /** Row height — see {@link CommandPaletteDensity}. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n density = \"compact\",\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n // The latest `search`, callable from the debounced effect without being one of its\n // dependencies — `revision` is what re-runs it (see the prop's docstring).\n const runSearch = useEffectEvent((q: string) => search(q));\n const reqId = useRef(0);\n /** What the provider's last answer was: a promise (`false`), an array (`true`), or\n * not known yet. Only a provider known to be async shows \"Searching…\" while the\n * debounce runs — see `search`. */\n const syncRef = useRef<boolean | undefined>(undefined);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop). Adjusted during render on the closed-to-open transition\n // (starting from \"closed\", so a palette mounted open resets too). `controlled` is\n // read at the open, not watched: a caller switching modes while the palette is open\n // is not a reason to wipe what is typed.\n const [wasOpen, setWasOpen] = useState(false);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n }\n }\n // The ref half of that reset: results from a previous open do not carry a\n // highlight into this one.\n useEffect(() => {\n if (open) shownQuery.current = null;\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n if (syncRef.current === false) setLoading(true);\n const run = async () => {\n try {\n const answer = runSearch(query);\n syncRef.current = !isThenable(answer);\n if (!syncRef.current) setLoading(true);\n const r = await answer;\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat, flatIndex } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n // Optional-called, as in combobox-core: jsdom has no scrollIntoView, and every\n // consumer's tests crashed on the first highlighted row (kastlan stubbed it).\n el?.scrollIntoView?.({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n const comfortable = density === \"comfortable\";\n return createPortal(\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer-only backdrop dismiss; Escape is the keyboard path\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className={cn(\n \"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]\",\n comfortable ? \"min-h-11 py-2.5\" : \"py-2\",\n )}\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n const idx = flatIndex.get(item) ?? -1;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 text-start\",\n comfortable ? \"min-h-11 py-2.5 text-base\" : \"py-2 text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { az as KitLinkComponent } from '../kit-labels-Mz8zZHIF.js';
|
|
5
5
|
import '../components/data-table-labels.js';
|
|
6
6
|
import '../components/mini-calendar.js';
|
|
7
7
|
import '../components/calendar-heatmap.js';
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -478,60 +478,68 @@ function SidebarFlyout({ item, children }) {
|
|
|
478
478
|
},
|
|
479
479
|
[cancelClose, closeNow]
|
|
480
480
|
);
|
|
481
|
-
return
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
481
|
+
return (
|
|
482
|
+
// Not a control: hover and focus-within on the nav link inside. Focus (the
|
|
483
|
+
// keyboard path) opens it exactly as hover does, Escape closes it.
|
|
484
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- hover/focus-within wrapper around a real link
|
|
485
|
+
/* @__PURE__ */ jsxs(
|
|
486
|
+
"div",
|
|
487
|
+
{
|
|
488
|
+
ref: wrapperRef,
|
|
489
|
+
className: "relative",
|
|
490
|
+
onMouseEnter: openMenu,
|
|
491
|
+
onMouseLeave: scheduleClose,
|
|
492
|
+
onFocus: openMenu,
|
|
493
|
+
onBlur: scheduleClose,
|
|
494
|
+
children: [
|
|
495
|
+
children,
|
|
496
|
+
open && pos && createPortal(
|
|
497
|
+
// Keeps the flyout open while the pointer crosses into it; keyboard users
|
|
498
|
+
// reach it by focus, which the wrapper above already handles.
|
|
499
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer-only hover bridge
|
|
500
|
+
/* @__PURE__ */ jsx(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
dir,
|
|
504
|
+
style: { position: "fixed", ...pos },
|
|
505
|
+
className: "z-40 ps-1",
|
|
506
|
+
onMouseEnter: cancelClose,
|
|
507
|
+
onMouseLeave: scheduleClose,
|
|
508
|
+
children: /* @__PURE__ */ jsxs(
|
|
509
|
+
"div",
|
|
510
|
+
{
|
|
511
|
+
role: "menu",
|
|
512
|
+
"aria-label": item.label,
|
|
513
|
+
className: "animate-flyout min-w-52 overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg",
|
|
514
|
+
children: [
|
|
515
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-[var(--border)] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]", children: item.label }),
|
|
516
|
+
/* @__PURE__ */ jsx("ul", { className: "py-1", children: item.items.map((sub) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
517
|
+
NavLink,
|
|
518
|
+
{
|
|
519
|
+
to: sub.to,
|
|
520
|
+
end: sub.end ?? true,
|
|
521
|
+
role: "menuitem",
|
|
522
|
+
onClick: () => setOpen(false),
|
|
523
|
+
className: ({ isActive }) => cn(
|
|
524
|
+
"flex items-center gap-2.5 px-3 py-2 text-sm",
|
|
525
|
+
isActive ? "bg-[var(--bg-active)] font-medium text-[var(--text-primary)]" : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
|
|
526
|
+
),
|
|
527
|
+
children: [
|
|
528
|
+
/* @__PURE__ */ jsx(sub.icon, { className: "size-4 shrink-0 text-[var(--text-placeholder)]" }),
|
|
529
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words leading-snug", children: sub.label })
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
) }, sub.to)) })
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
),
|
|
538
|
+
document.body
|
|
539
|
+
)
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
)
|
|
535
543
|
);
|
|
536
544
|
}
|
|
537
545
|
export {
|