@pipe0/react 0.2.4 → 0.2.6
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/CHANGELOG.md +16 -0
- package/dist/components/defaults/adapters/context-select-input.mjs +161 -0
- package/dist/components/defaults/adapters/context-select-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/field-select-input.mjs +33 -0
- package/dist/components/defaults/adapters/field-select-input.mjs.map +1 -0
- package/dist/components/defaults/adapters/index.d.mts.map +1 -1
- package/dist/components/defaults/adapters/index.mjs +4 -0
- package/dist/components/defaults/adapters/index.mjs.map +1 -1
- package/dist/components/defaults/adapters/pipes-run-if-input.mjs +53 -5
- package/dist/components/defaults/adapters/pipes-run-if-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/tagged-ordered-multi-create-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/typed-fields-select-input.mjs +48 -0
- package/dist/components/defaults/adapters/typed-fields-select-input.mjs.map +1 -0
- package/dist/components/internal/combobox/suggest-combobox.mjs +35 -6
- package/dist/components/internal/combobox/suggest-combobox.mjs.map +1 -1
- package/dist/hooks/use-effect-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-effect-catalog-table.mjs +18 -11
- package/dist/hooks/use-effect-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-form-core.d.mts.map +1 -1
- package/dist/hooks/use-form-core.mjs +48 -8
- package/dist/hooks/use-form-core.mjs.map +1 -1
- package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
- package/dist/hooks/use-pipe-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-pipe-catalog-table.mjs +20 -16
- package/dist/hooks/use-pipe-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-search-catalog-table.d.mts +6 -6
- package/dist/hooks/use-search-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-search-catalog-table.mjs +20 -13
- package/dist/hooks/use-search-catalog-table.mjs.map +1 -1
- package/dist/hooks/use-searches-catalog-table.d.mts.map +1 -1
- package/dist/hooks/use-searches-catalog-table.mjs +20 -13
- package/dist/hooks/use-searches-catalog-table.mjs.map +1 -1
- package/dist/types/field-props.d.mts +37 -3
- package/dist/types/field-props.d.mts.map +1 -1
- package/dist/utils/build-section-handlers.mjs +21 -2
- package/dist/utils/build-section-handlers.mjs.map +1 -1
- package/dist/utils/catalog-helpers.mjs +10 -1
- package/dist/utils/catalog-helpers.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tagged-ordered-multi-create-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-ordered-multi-create-input.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { PipesFieldDefinitionWithName, WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { type CSSProperties, type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { IconGripVertical } from \"../../internal/icons.js\";\nimport { filterOptions, type PayloadOption } from \"../../internal/multi-select-popover-trigger.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\n\n/**\n * Detects whether a raw value is a single, exclusive Liquid field reference\n * (e.g. `\"{{ company_domain }}\"` or `\"{{ field | filter }}\"`). Mixed content\n * such as `\"Head of {{ company }}\"` returns false and is treated as text.\n */\nconst PURE_TAG_RE =\n /^\\s*\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)(?:\\s*\\|\\s*[^}]+)?\\s*\\}\\}\\s*$/;\n\ntype ParsedChip =\n | { kind: \"field\"; fieldName: string; raw: string }\n | { kind: \"text\"; text: string; raw: string };\n\nfunction parseChip(raw: string): ParsedChip {\n const m = raw.match(PURE_TAG_RE);\n if (m) return { kind: \"field\", fieldName: m[1], raw };\n return { kind: \"text\", text: raw, raw };\n}\n\n/**\n * Sky-blue pill matching `TagChipDecoration`'s field chip styling so a\n * field-reference chip outside tiptap reads identically to one inside\n * `LiquidEditor`.\n */\nconst FIELD_CHIP_CLASS =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-sky-100 pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-sky-900\";\n\nconst TEXT_CHIP_CLASS =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\";\n\n/** Drop target for an item carried by the Combobox value channel. */\ntype Item = {\n /** Canonical value to store in the field — text as-is, or `{{ fieldName }}`. */\n value: string;\n /** Display label in the dropdown. */\n label: string;\n kind: \"field\" | \"text\" | \"create\";\n widgets?: WidgetsByKind;\n};\n\nexport function TaggedOrderedMultiCreateInputAdapter(\n field: FieldHandle<\"tagged_ordered_multi_create_input\">,\n) {\n const { items, setItems, options, loadOptions, inputFields, meta } = field;\n const maxItems = meta.maxItems;\n const placeholder = meta.placeholder;\n\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), 300);\n\n // Async suggestions — same SWR pattern as `SuggestCombobox`.\n const instanceId = useId();\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const trimmedInput = input.trim();\n\n const fieldRefItems = useMemo<Item[]>(() => {\n if (!inputFields?.length) return [];\n const q = trimmedInput.toLowerCase();\n return inputFields\n .filter((f) => !q || f.resolvedName.toLowerCase().includes(q))\n .map<Item>((f) => ({\n value: `{{ ${f.resolvedName} }}`,\n label: f.resolvedName,\n kind: \"field\",\n }));\n }, [inputFields, trimmedInput]);\n\n const suggestionItems = useMemo<Item[]>(() => {\n const source: PayloadOption[] = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked: PayloadOption[] = isAsync ? source : filterOptions(source, input);\n return ranked.map<Item>((o) => ({\n value: o.value,\n label: o.label,\n kind: \"text\",\n widgets: o.widgets,\n }));\n }, [isAsync, asyncResults, options, input]);\n\n const matchesExistingSuggestion = useMemo(\n () =>\n suggestionItems.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase()) ||\n fieldRefItems.some((f) => f.value.toLowerCase() === trimmedInput.toLowerCase()),\n [suggestionItems, fieldRefItems, trimmedInput],\n );\n\n const showCreateRow = trimmedInput.length > 0 && !matchesExistingSuggestion;\n\n // Flat items list — fed to base-ui as the universe of pickable items so its\n // keyboard navigation has a single ordering to work with. The visual\n // grouping is done by `ComboboxGroup` in the render below; base-ui ranges\n // over `items` linearly and matches by reference.\n const allItems = useMemo<Item[]>(() => {\n const list: Item[] = [];\n for (const f of fieldRefItems) list.push(f);\n for (const s of suggestionItems) list.push(s);\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n kind: \"create\",\n });\n }\n return list;\n }, [fieldRefItems, suggestionItems, showCreateRow, trimmedInput]);\n\n const atMax = maxItems !== undefined && items.length >= maxItems;\n\n // Stable per-position ids so `@dnd-kit` keys survive even when two chips\n // share the same raw string (uncommon, but allowed — the user could insert\n // the same field reference twice in a waterfall).\n const idsRef = useRef<string[]>([]);\n if (idsRef.current.length !== items.length) {\n if (idsRef.current.length < items.length) {\n idsRef.current = [\n ...idsRef.current,\n ...Array.from(\n { length: items.length - idsRef.current.length },\n () => crypto.randomUUID(),\n ),\n ];\n } else {\n idsRef.current = idsRef.current.slice(0, items.length);\n }\n }\n const chipIds = idsRef.current;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = chipIds.indexOf(String(active.id));\n const to = chipIds.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const nextItems = [...items];\n const [movedValue] = nextItems.splice(from, 1);\n nextItems.splice(to, 0, movedValue);\n const nextIds = [...chipIds];\n const [movedId] = nextIds.splice(from, 1);\n nextIds.splice(to, 0, movedId);\n idsRef.current = nextIds;\n setItems(nextItems);\n };\n\n const removeAt = (index: number) => {\n idsRef.current = idsRef.current.filter((_, i) => i !== index);\n setItems(items.filter((_, i) => i !== index));\n };\n\n const appendValue = (value: string) => {\n if (atMax || !value) return;\n idsRef.current = [...idsRef.current, crypto.randomUUID()];\n setItems([...items, value]);\n };\n\n const clearInput = () => {\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n };\n\n const anchor = useComboboxAnchor();\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <Combobox<Item, true>\n multiple\n items={allItems}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n // Chips are rendered manually so base-ui doesn't manage them — keep\n // `value` empty and capture picks via `onValueChange`. Same trick as\n // `IncludeExcludeCombobox`.\n value={[] as Item[]}\n onValueChange={(next) => {\n if (!Array.isArray(next) || next.length === 0) return;\n const last = next[next.length - 1] as Item;\n appendValue(last.value);\n clearInput();\n }}\n isItemEqualToValue={(a, b) => (a as Item).value === (b as Item).value}\n itemToStringLabel={(item) => (item as Item).label}\n itemToStringValue={(item) => (item as Item).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n >\n <ComboboxChips aria-invalid={!!field.error || undefined} ref={anchor}>\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={chipIds} strategy={horizontalListSortingStrategy}>\n {items.map((raw, index) => {\n const parsed = parseChip(raw);\n return (\n <ChipPill\n key={chipIds[index]}\n id={chipIds[index]}\n parsed={parsed}\n onRemove={() => removeAt(index)}\n />\n );\n })}\n </SortableContext>\n </DndContext>\n <ComboboxChipsInput\n placeholder={items.length === 0 ? placeholder : undefined}\n aria-invalid={!!field.error || undefined}\n onKeyDown={(e) => {\n if (e.key === \"Backspace\" && input.length === 0) {\n if (items.length === 0) return;\n e.preventDefault();\n removeAt(items.length - 1);\n return;\n }\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n // Prefer exact suggestion match, then exact field match, else\n // create a text entry.\n const exactSuggestion = suggestionItems.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const exactField = fieldRefItems.find(\n (f) => f.label.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exactSuggestion?.value ?? exactField?.value ?? trimmedInput;\n appendValue(toAdd);\n clearInput();\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && allItems.length > 0 && (\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && allItems.length === 0 && (\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n allItems.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {fieldRefItems.length > 0 && (\n <ComboboxGroup>\n <ComboboxLabel>Field references</ComboboxLabel>\n {fieldRefItems.map((it) => (\n <ComboboxItem\n key={`field:${it.value}`}\n value={it}\n disabled={atMax}\n >\n <span className=\"pz:inline-flex pz:items-center pz:rounded-sm pz:bg-sky-100 pz:px-1 pz:text-sky-900\">\n {it.label}\n </span>\n </ComboboxItem>\n ))}\n </ComboboxGroup>\n )}\n\n {suggestionItems.length > 0 && (\n <ComboboxGroup>\n <ComboboxLabel>Suggestions</ComboboxLabel>\n {suggestionItems.map((it) => (\n <ComboboxItem\n key={`text:${it.value}`}\n value={it}\n disabled={atMax}\n >\n {it.widgets && (\n <WidgetStrip widgets={pickLeadingWidgets(it.widgets)} size={14} />\n )}\n <span className=\"pz:flex-1 pz:truncate\">{it.label}</span>\n {it.widgets?.field_type && (\n <FieldTypeBadge\n type={it.widgets.field_type.type}\n format={it.widgets.field_type.format}\n />\n )}\n </ComboboxItem>\n ))}\n </ComboboxGroup>\n )}\n\n {showCreateRow && (\n <ComboboxItem\n key={`__create__:${trimmedInput}`}\n value={{ value: trimmedInput, label: `Add \"${trimmedInput}\"`, kind: \"create\" }}\n className=\"pz:italic\"\n disabled={atMax}\n >\n {`Add \"${trimmedInput}\"`}\n </ComboboxItem>\n )}\n\n {!loading && allItems.length === 0 && (\n <ComboboxEmpty>\n {trimmedInput.length > 0 ? \"No matches\" : \"Type to add a role\"}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n </div>\n );\n}\n\nfunction pickLeadingWidgets(widgets: WidgetsByKind): WidgetsByKind {\n const { field_type: _ignored, ...rest } = widgets;\n return rest;\n}\n\nfunction ChipPill({\n id,\n parsed,\n onRemove,\n}: {\n id: string;\n parsed: ParsedChip;\n onRemove: () => void;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n const isField = parsed.kind === \"field\";\n const label = isField ? parsed.fieldName : parsed.text;\n return (\n <ChipShell\n setNodeRef={setNodeRef}\n style={style}\n attributes={attributes}\n listeners={listeners}\n kind={parsed.kind}\n onRemove={onRemove}\n >\n <span className=\"pz:px-1\">{label || <span className=\"pz:text-muted-foreground\">empty</span>}</span>\n </ChipShell>\n );\n}\n\nfunction ChipShell({\n setNodeRef,\n style,\n attributes,\n listeners,\n kind,\n onRemove,\n children,\n}: {\n setNodeRef: (n: HTMLElement | null) => void;\n style: CSSProperties;\n // biome-ignore lint/suspicious/noExplicitAny: dnd-kit attribute/listener shape\n attributes: any;\n // biome-ignore lint/suspicious/noExplicitAny: dnd-kit attribute/listener shape\n listeners: any;\n kind: ParsedChip[\"kind\"];\n onRemove: () => void;\n children: ReactNode;\n}) {\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(kind === \"field\" ? FIELD_CHIP_CLASS : TEXT_CHIP_CLASS)}\n data-chip-kind={kind}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tagged-ordered-multi-create-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-ordered-multi-create-input.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { PipesFieldDefinitionWithName, WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport {\n type CSSProperties,\n type ReactNode,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { IconGripVertical } from \"../../internal/icons.js\";\nimport { filterOptions, type PayloadOption } from \"../../internal/multi-select-popover-trigger.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\n\n/**\n * Detects whether a raw value is a single, exclusive Liquid field reference\n * (e.g. `\"{{ company_domain }}\"` or `\"{{ field | filter }}\"`). Mixed content\n * such as `\"Head of {{ company }}\"` returns false and is treated as text.\n */\nconst PURE_TAG_RE = /^\\s*\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)(?:\\s*\\|\\s*[^}]+)?\\s*\\}\\}\\s*$/;\n\ntype ParsedChip =\n | { kind: \"field\"; fieldName: string; raw: string }\n | { kind: \"text\"; text: string; raw: string };\n\nfunction parseChip(raw: string): ParsedChip {\n const m = raw.match(PURE_TAG_RE);\n if (m) return { kind: \"field\", fieldName: m[1], raw };\n return { kind: \"text\", text: raw, raw };\n}\n\n/**\n * Sky-blue pill matching `TagChipDecoration`'s field chip styling so a\n * field-reference chip outside tiptap reads identically to one inside\n * `LiquidEditor`.\n */\nconst FIELD_CHIP_CLASS =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-sky-100 pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-sky-900\";\n\nconst TEXT_CHIP_CLASS =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\";\n\n/** Drop target for an item carried by the Combobox value channel. */\ntype Item = {\n /** Canonical value to store in the field — text as-is, or `{{ fieldName }}`. */\n value: string;\n /** Display label in the dropdown. */\n label: string;\n kind: \"field\" | \"text\" | \"create\";\n widgets?: WidgetsByKind;\n};\n\nexport function TaggedOrderedMultiCreateInputAdapter(\n field: FieldHandle<\"tagged_ordered_multi_create_input\">,\n) {\n const { items, setItems, options, loadOptions, inputFields, meta } = field;\n const maxItems = meta.maxItems;\n const placeholder = meta.placeholder;\n\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), 300);\n\n // Async suggestions — same SWR pattern as `SuggestCombobox`.\n const instanceId = useId();\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const trimmedInput = input.trim();\n\n const fieldRefItems = useMemo<Item[]>(() => {\n if (!inputFields?.length) return [];\n const q = trimmedInput.toLowerCase();\n return inputFields\n .filter((f) => !q || f.resolvedName.toLowerCase().includes(q))\n .map<Item>((f) => ({\n value: `{{ ${f.resolvedName} }}`,\n label: f.resolvedName,\n kind: \"field\",\n }));\n }, [inputFields, trimmedInput]);\n\n const suggestionItems = useMemo<Item[]>(() => {\n const source: PayloadOption[] = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked: PayloadOption[] = isAsync ? source : filterOptions(source, input);\n return ranked.map<Item>((o) => ({\n value: o.value,\n label: o.label,\n kind: \"text\",\n widgets: o.widgets,\n }));\n }, [isAsync, asyncResults, options, input]);\n\n const matchesExistingSuggestion = useMemo(\n () =>\n suggestionItems.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase()) ||\n fieldRefItems.some((f) => f.value.toLowerCase() === trimmedInput.toLowerCase()),\n [suggestionItems, fieldRefItems, trimmedInput],\n );\n\n const showCreateRow = trimmedInput.length > 0 && !matchesExistingSuggestion;\n\n // Flat items list — fed to base-ui as the universe of pickable items so its\n // keyboard navigation has a single ordering to work with. The visual\n // grouping is done by `ComboboxGroup` in the render below; base-ui ranges\n // over `items` linearly and matches by reference.\n const allItems = useMemo<Item[]>(() => {\n const list: Item[] = [];\n for (const f of fieldRefItems) list.push(f);\n for (const s of suggestionItems) list.push(s);\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n kind: \"create\",\n });\n }\n return list;\n }, [fieldRefItems, suggestionItems, showCreateRow, trimmedInput]);\n\n const atMax = maxItems !== undefined && items.length >= maxItems;\n\n // Stable per-position ids so `@dnd-kit` keys survive even when two chips\n // share the same raw string (uncommon, but allowed — the user could insert\n // the same field reference twice in a waterfall).\n const idsRef = useRef<string[]>([]);\n if (idsRef.current.length !== items.length) {\n if (idsRef.current.length < items.length) {\n idsRef.current = [\n ...idsRef.current,\n ...Array.from({ length: items.length - idsRef.current.length }, () => crypto.randomUUID()),\n ];\n } else {\n idsRef.current = idsRef.current.slice(0, items.length);\n }\n }\n const chipIds = idsRef.current;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = chipIds.indexOf(String(active.id));\n const to = chipIds.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const nextItems = [...items];\n const [movedValue] = nextItems.splice(from, 1);\n nextItems.splice(to, 0, movedValue);\n const nextIds = [...chipIds];\n const [movedId] = nextIds.splice(from, 1);\n nextIds.splice(to, 0, movedId);\n idsRef.current = nextIds;\n setItems(nextItems);\n };\n\n const removeAt = (index: number) => {\n idsRef.current = idsRef.current.filter((_, i) => i !== index);\n setItems(items.filter((_, i) => i !== index));\n };\n\n const appendValue = (value: string) => {\n if (atMax || !value) return;\n idsRef.current = [...idsRef.current, crypto.randomUUID()];\n setItems([...items, value]);\n };\n\n const clearInput = () => {\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n };\n\n const anchor = useComboboxAnchor();\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <Combobox<Item, true>\n multiple\n items={allItems}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n // Chips are rendered manually so base-ui doesn't manage them — keep\n // `value` empty and capture picks via `onValueChange`. Same trick as\n // `IncludeExcludeCombobox`.\n value={[] as Item[]}\n onValueChange={(next) => {\n if (!Array.isArray(next) || next.length === 0) return;\n const last = next[next.length - 1] as Item;\n appendValue(last.value);\n clearInput();\n }}\n isItemEqualToValue={(a, b) => (a as Item).value === (b as Item).value}\n itemToStringLabel={(item) => (item as Item).label}\n itemToStringValue={(item) => (item as Item).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n >\n <ComboboxChips aria-invalid={!!field.error || undefined} ref={anchor}>\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={chipIds} strategy={horizontalListSortingStrategy}>\n {items.map((raw, index) => {\n const parsed = parseChip(raw);\n return (\n <ChipPill\n key={chipIds[index]}\n id={chipIds[index]}\n parsed={parsed}\n onRemove={() => removeAt(index)}\n />\n );\n })}\n </SortableContext>\n </DndContext>\n <ComboboxChipsInput\n placeholder={items.length === 0 ? placeholder : undefined}\n aria-invalid={!!field.error || undefined}\n onKeyDown={(e) => {\n if (e.key === \"Backspace\" && input.length === 0) {\n if (items.length === 0) return;\n e.preventDefault();\n removeAt(items.length - 1);\n return;\n }\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n // Prefer exact suggestion match, then exact field match, else\n // create a text entry.\n const exactSuggestion = suggestionItems.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const exactField = fieldRefItems.find(\n (f) => f.label.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exactSuggestion?.value ?? exactField?.value ?? trimmedInput;\n appendValue(toAdd);\n clearInput();\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && allItems.length > 0 && (\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && allItems.length === 0 && (\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n allItems.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {fieldRefItems.length > 0 && (\n <ComboboxGroup>\n <ComboboxLabel>Field references</ComboboxLabel>\n {fieldRefItems.map((it) => (\n <ComboboxItem key={`field:${it.value}`} value={it} disabled={atMax}>\n <span className=\"pz:inline-flex pz:items-center pz:rounded-sm pz:bg-sky-100 pz:px-1 pz:text-sky-900\">\n {it.label}\n </span>\n </ComboboxItem>\n ))}\n </ComboboxGroup>\n )}\n\n {suggestionItems.length > 0 && (\n <ComboboxGroup>\n <ComboboxLabel>Suggestions</ComboboxLabel>\n {suggestionItems.map((it) => (\n <ComboboxItem key={`text:${it.value}`} value={it} disabled={atMax}>\n {it.widgets && (\n <WidgetStrip widgets={pickLeadingWidgets(it.widgets)} size={14} />\n )}\n <span className=\"pz:flex-1 pz:truncate\">{it.label}</span>\n {it.widgets?.field_type && (\n <FieldTypeBadge\n type={it.widgets.field_type.type}\n format={it.widgets.field_type.format}\n />\n )}\n </ComboboxItem>\n ))}\n </ComboboxGroup>\n )}\n\n {showCreateRow && (\n <ComboboxItem\n key={`__create__:${trimmedInput}`}\n value={{ value: trimmedInput, label: `Add \"${trimmedInput}\"`, kind: \"create\" }}\n className=\"pz:italic\"\n disabled={atMax}\n >\n {`Add \"${trimmedInput}\"`}\n </ComboboxItem>\n )}\n\n {!loading && allItems.length === 0 && (\n <ComboboxEmpty>\n {trimmedInput.length > 0 ? \"No matches\" : \"Type to add a role\"}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n </div>\n );\n}\n\nfunction pickLeadingWidgets(widgets: WidgetsByKind): WidgetsByKind {\n const { field_type: _ignored, ...rest } = widgets;\n return rest;\n}\n\nfunction ChipPill({\n id,\n parsed,\n onRemove,\n}: {\n id: string;\n parsed: ParsedChip;\n onRemove: () => void;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n const isField = parsed.kind === \"field\";\n const label = isField ? parsed.fieldName : parsed.text;\n return (\n <ChipShell\n setNodeRef={setNodeRef}\n style={style}\n attributes={attributes}\n listeners={listeners}\n kind={parsed.kind}\n onRemove={onRemove}\n >\n <span className=\"pz:px-1\">\n {label || <span className=\"pz:text-muted-foreground\">empty</span>}\n </span>\n </ChipShell>\n );\n}\n\nfunction ChipShell({\n setNodeRef,\n style,\n attributes,\n listeners,\n kind,\n onRemove,\n children,\n}: {\n setNodeRef: (n: HTMLElement | null) => void;\n style: CSSProperties;\n // biome-ignore lint/suspicious/noExplicitAny: dnd-kit attribute/listener shape\n attributes: any;\n // biome-ignore lint/suspicious/noExplicitAny: dnd-kit attribute/listener shape\n listeners: any;\n kind: ParsedChip[\"kind\"];\n onRemove: () => void;\n children: ReactNode;\n}) {\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(kind === \"field\" ? FIELD_CHIP_CLASS : TEXT_CHIP_CLASS)}\n data-chip-kind={kind}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SuggestCombobox } from "../../internal/combobox/suggest-combobox.mjs";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/components/defaults/adapters/typed-fields-select-input.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Like `fields-select-input`, but each picked column SNAPSHOTS its
|
|
8
|
+
* type/format (from the option's `data`, fetched via field context) into the
|
|
9
|
+
* payload — the source columns live on another sheet, so their types aren't
|
|
10
|
+
* derivable at validate time. Free-typed names fall back to `"unknown"`.
|
|
11
|
+
* Entries already in the value keep their stored snapshot even when the
|
|
12
|
+
* option list has since changed (e.g. the sibling sheet was switched).
|
|
13
|
+
*/
|
|
14
|
+
function TypedFieldsSelectInputAdapter(field) {
|
|
15
|
+
const entries = useMemo(() => field.value ?? [], [field.value]);
|
|
16
|
+
const selected = useMemo(() => entries.map((f) => f.field_name), [entries]);
|
|
17
|
+
const toEntry = (fieldName) => {
|
|
18
|
+
const existing = entries.find((e) => e.field_name === fieldName);
|
|
19
|
+
if (existing) return existing;
|
|
20
|
+
const data = (field.meta.options ?? []).find((o) => o.value === fieldName)?.data;
|
|
21
|
+
const type = data?.type ?? "unknown";
|
|
22
|
+
const format = data?.format ?? null;
|
|
23
|
+
return {
|
|
24
|
+
field_name: fieldName,
|
|
25
|
+
type,
|
|
26
|
+
...format ? { format } : {}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
return /* @__PURE__ */ jsx("div", {
|
|
30
|
+
"data-p0": "input",
|
|
31
|
+
children: /* @__PURE__ */ jsx(SuggestCombobox, {
|
|
32
|
+
value: selected,
|
|
33
|
+
onChange: (next) => field.setValue(next.map(toEntry)),
|
|
34
|
+
options: field.meta.options ?? [],
|
|
35
|
+
maxItems: field.meta.maxItems,
|
|
36
|
+
disableSearch: field.meta.disableSearch,
|
|
37
|
+
disabled: field.disabled,
|
|
38
|
+
ariaInvalid: !!field.error,
|
|
39
|
+
placeholder: field.meta.placeholder ?? "Select fields…",
|
|
40
|
+
emptyLabel: "No fields available",
|
|
41
|
+
reorderable: true
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { TypedFieldsSelectInputAdapter };
|
|
48
|
+
//# sourceMappingURL=typed-fields-select-input.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typed-fields-select-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/typed-fields-select-input.tsx"],"sourcesContent":["import type { RecordFieldFormat, RecordFieldType } from \"@pipe0/base\";\nimport { useMemo } from \"react\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { SuggestCombobox } from \"../../internal/combobox/suggest-combobox.js\";\n\ntype TypedFieldEntry = {\n field_name: string;\n type: RecordFieldType;\n format?: RecordFieldFormat;\n};\n\n/**\n * Like `fields-select-input`, but each picked column SNAPSHOTS its\n * type/format (from the option's `data`, fetched via field context) into the\n * payload — the source columns live on another sheet, so their types aren't\n * derivable at validate time. Free-typed names fall back to `\"unknown\"`.\n * Entries already in the value keep their stored snapshot even when the\n * option list has since changed (e.g. the sibling sheet was switched).\n */\nexport function TypedFieldsSelectInputAdapter(field: FieldHandle<\"typed_fields_select_input\">) {\n const entries = useMemo<TypedFieldEntry[]>(() => field.value ?? [], [field.value]);\n const selected = useMemo(() => entries.map((f) => f.field_name), [entries]);\n\n const toEntry = (fieldName: string): TypedFieldEntry => {\n const existing = entries.find((e) => e.field_name === fieldName);\n if (existing) return existing;\n const data = (field.meta.options ?? []).find((o) => o.value === fieldName)?.data;\n const type = (data?.type as RecordFieldType | undefined) ?? \"unknown\";\n const format = (data?.format as RecordFieldFormat | undefined) ?? null;\n return { field_name: fieldName, type, ...(format ? { format } : {}) };\n };\n\n return (\n <div data-p0=\"input\">\n <SuggestCombobox\n value={selected}\n onChange={(next) => field.setValue(next.map(toEntry))}\n options={field.meta.options ?? []}\n maxItems={field.meta.maxItems}\n disableSearch={field.meta.disableSearch}\n disabled={field.disabled}\n ariaInvalid={!!field.error}\n placeholder={field.meta.placeholder ?? \"Select fields…\"}\n emptyLabel=\"No fields available\"\n reorderable\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAmBA,SAAgB,8BAA8B,OAAiD;CAC7F,MAAM,UAAU,cAAiC,MAAM,SAAS,EAAE,EAAE,CAAC,MAAM,MAAM,CAAC;CAClF,MAAM,WAAW,cAAc,QAAQ,KAAK,MAAM,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC;CAE3E,MAAM,WAAW,cAAuC;EACtD,MAAM,WAAW,QAAQ,MAAM,MAAM,EAAE,eAAe,UAAU;AAChE,MAAI,SAAU,QAAO;EACrB,MAAM,QAAQ,MAAM,KAAK,WAAW,EAAE,EAAE,MAAM,MAAM,EAAE,UAAU,UAAU,EAAE;EAC5E,MAAM,OAAQ,MAAM,QAAwC;EAC5D,MAAM,SAAU,MAAM,UAA4C;AAClE,SAAO;GAAE,YAAY;GAAW;GAAM,GAAI,SAAS,EAAE,QAAQ,GAAG,EAAE;GAAG;;AAGvE,QACE,oBAAC,OAAD;EAAK,WAAQ;YACX,oBAAC,iBAAD;GACE,OAAO;GACP,WAAW,SAAS,MAAM,SAAS,KAAK,IAAI,QAAQ,CAAC;GACrD,SAAS,MAAM,KAAK,WAAW,EAAE;GACjC,UAAU,MAAM,KAAK;GACrB,eAAe,MAAM,KAAK;GAC1B,UAAU,MAAM;GAChB,aAAa,CAAC,CAAC,MAAM;GACrB,aAAa,MAAM,KAAK,eAAe;GACvC,YAAW;GACX;GACA;EACE"}
|
|
@@ -71,7 +71,7 @@ function SortableChip({ id, onRemove, children }) {
|
|
|
71
71
|
* - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.
|
|
72
72
|
* - Debounced async loader with an in-memory cache keyed by query string.
|
|
73
73
|
*/
|
|
74
|
-
function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate = false, disableSearch = false, maxItems, excludeValues = [], ariaInvalid, disabled, debounceMs = 300, labelFor, iconFor, emptyLabel, placeholder, reorderable = false }) {
|
|
74
|
+
function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate = false, disableSearch = false, maxItems, excludeValues = [], ariaInvalid, disabled, debounceMs = 300, labelFor, iconFor, emptyLabel, placeholder, reorderable = false, single = false }) {
|
|
75
75
|
const [input, setInput] = useState("");
|
|
76
76
|
const [debouncedQuery, setDebouncedQuery] = useState("");
|
|
77
77
|
const [open, setOpen] = useState(false);
|
|
@@ -156,6 +156,14 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
|
|
|
156
156
|
value: valueObjects,
|
|
157
157
|
onValueChange: (next) => {
|
|
158
158
|
if (!Array.isArray(next)) return;
|
|
159
|
+
if (single) {
|
|
160
|
+
const last = [...next].reverse().map((item) => typeof item === "string" ? item : item.value).find((v) => v && !excludeSet.has(v));
|
|
161
|
+
onChange(last ? [last] : []);
|
|
162
|
+
setInput("");
|
|
163
|
+
if (isAsync) debouncedSetQuery("");
|
|
164
|
+
setOpen(false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
159
167
|
const seen = /* @__PURE__ */ new Set();
|
|
160
168
|
const deduped = [];
|
|
161
169
|
for (const item of next) {
|
|
@@ -183,7 +191,20 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
|
|
|
183
191
|
"aria-invalid": ariaInvalid || void 0,
|
|
184
192
|
ref: anchor,
|
|
185
193
|
children: [
|
|
186
|
-
|
|
194
|
+
single ? valueObjects[0] && input === "" && /* @__PURE__ */ jsxs("span", {
|
|
195
|
+
className: "pz:flex pz:min-w-0 pz:items-center pz:gap-1.5 pz:text-sm",
|
|
196
|
+
children: [
|
|
197
|
+
iconFor?.(valueObjects[0].value),
|
|
198
|
+
valueObjects[0].widgets && /* @__PURE__ */ jsx(WidgetStrip, {
|
|
199
|
+
widgets: valueObjects[0].widgets,
|
|
200
|
+
size: 14
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ jsx("span", {
|
|
203
|
+
className: "pz:truncate",
|
|
204
|
+
children: valueObjects[0].label
|
|
205
|
+
})
|
|
206
|
+
]
|
|
207
|
+
}) : reorderable ? /* @__PURE__ */ jsx(DndContext, {
|
|
187
208
|
sensors: sortSensors,
|
|
188
209
|
collisionDetection: closestCenter,
|
|
189
210
|
onDragEnd: handleDragEnd,
|
|
@@ -220,10 +241,18 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
|
|
|
220
241
|
if (trimmedInput.length === 0) return;
|
|
221
242
|
e.preventDefault();
|
|
222
243
|
e.stopPropagation();
|
|
223
|
-
if (atMax) return;
|
|
244
|
+
if (!single && atMax) return;
|
|
224
245
|
const toAdd = filteredSuggestions.find((o) => o.value.toLowerCase() === trimmedInput.toLowerCase())?.value ?? (allowCreate ? trimmedInput : null);
|
|
225
246
|
if (!toAdd) return;
|
|
226
|
-
if (
|
|
247
|
+
if (excludeSet.has(toAdd)) return;
|
|
248
|
+
if (single) {
|
|
249
|
+
onChange([toAdd]);
|
|
250
|
+
setInput("");
|
|
251
|
+
if (isAsync) debouncedSetQuery("");
|
|
252
|
+
setOpen(false);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (selectedSet.has(toAdd)) return;
|
|
227
256
|
onChange([...value, toAdd]);
|
|
228
257
|
setInput("");
|
|
229
258
|
if (isAsync) debouncedSetQuery("");
|
|
@@ -247,7 +276,7 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
|
|
|
247
276
|
/* @__PURE__ */ jsxs(ComboboxList, {
|
|
248
277
|
className: cn(loading && items.length > 0 && "pz:opacity-50 pz:transition-opacity pz:pointer-events-none"),
|
|
249
278
|
children: [
|
|
250
|
-
atMax && /* @__PURE__ */ jsxs("div", {
|
|
279
|
+
!single && atMax && /* @__PURE__ */ jsxs("div", {
|
|
251
280
|
className: "pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground",
|
|
252
281
|
children: [
|
|
253
282
|
"Maximum ",
|
|
@@ -263,7 +292,7 @@ function SuggestCombobox({ value, onChange, options, loadOptions, allowCreate =
|
|
|
263
292
|
return /* @__PURE__ */ jsxs(ComboboxItem, {
|
|
264
293
|
value: item,
|
|
265
294
|
className: item.__create ? "pz:italic" : void 0,
|
|
266
|
-
disabled: !selectedSet.has(item.value) && atMax && !item.__create,
|
|
295
|
+
disabled: !single && !selectedSet.has(item.value) && atMax && !item.__create,
|
|
267
296
|
children: [
|
|
268
297
|
!item.__create && iconFor?.(item.value),
|
|
269
298
|
leadingWidgets && /* @__PURE__ */ jsx(WidgetStrip, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suggest-combobox.mjs","names":[],"sources":["../../../../src/components/internal/combobox/suggest-combobox.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconGripVertical } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\n/** Internal item type — the base-ui Combobox handles `{value, label}` natively. */\ntype ComboboxOption = PayloadOption & { __create?: boolean };\n\n/** Split widgets so `field_type` can render on the trailing edge of dropdown\n * rows while every other widget stays in the leading strip. */\nfunction splitWidgets(widgets: WidgetsByKind): {\n leading: WidgetsByKind | undefined;\n} {\n const { field_type: _ignored, ...rest } = widgets;\n const hasLeading = Object.values(rest).some((v) => v != null);\n return { leading: hasLeading ? rest : undefined };\n}\n\n/** Reorderable chip matching `ComboboxChip` visually, with a grip handle. */\nfunction SortableChip({\n id,\n onRemove,\n children,\n}: {\n id: string;\n onRemove: () => void;\n children: ReactNode;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: React.CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\",\n )}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n\nexport interface SuggestComboboxProps {\n value: string[];\n onChange: (value: string[]) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n excludeValues?: string[];\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n labelFor?: (value: string) => string;\n /** Optional leading icon (e.g. provider logo) rendered inside chips and list items. */\n iconFor?: (value: string) => ReactNode;\n emptyLabel?: string;\n placeholder?: string;\n /** When true, chips become drag-sortable and reorder the `value` array. */\n reorderable?: boolean;\n}\n\n/**\n * Unified combobox built on `@base-ui/react`'s Combobox primitive.\n *\n * - Chips + inline search input via `ComboboxChips` / `ComboboxChipsInput`.\n * - Static (`options`) or async (`loadOptions`) item sources. External\n * filtering in both cases so match-sorter ranking / server relevance is\n * honored and base-ui's built-in filter doesn't double-filter.\n * - Free-text creation (`allowCreate`) renders an inline \"Add \\\"X\\\"\" item.\n * - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.\n * - Debounced async loader with an in-memory cache keyed by query string.\n */\nexport function SuggestCombobox({\n value,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n excludeValues = [],\n ariaInvalid,\n disabled,\n debounceMs = 300,\n labelFor,\n iconFor,\n emptyLabel,\n placeholder,\n reorderable = false,\n}: SuggestComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n // Per-instance cache key prefix so two comboboxes with the same query but\n // different `loadOptions` functions don't share results.\n const instanceId = useId();\n\n // SWR handles dedupe + caching by-query + race-safety. We defer the\n // empty-query fetch until the user opens the dropdown — some backends\n // (e.g. Crustdata persondb) don't accept empty queries, and on-mount\n // fetches are wasted work if the field is never opened. `keepPreviousData`\n // preserves the prior result set during a refetch so the UI dims rather\n // than empties.\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n // `swrKey` was `null` when `loadOptions` is undefined, so the fetcher\n // never runs without one — the runtime check is just to satisfy TS.\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const excludeSet = useMemo(() => new Set(excludeValues), [excludeValues]);\n const selectedSet = useMemo(() => new Set(value), [value]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked = isAsync ? source : filterOptions(source, input);\n return ranked.filter((o) => !excludeSet.has(o.value));\n }, [isAsync, asyncResults, options, input, excludeSet]);\n\n const trimmedInput = input.trim();\n const showCreateRow =\n allowCreate &&\n trimmedInput.length > 0 &&\n !selectedSet.has(trimmedInput) &&\n !excludeSet.has(trimmedInput) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n if (labelFor) return labelFor(v);\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n // Persist widgets per value across loadOptions cycles. Async results are\n // replaced with each query, so without this cache an icon shown when\n // picking a value would disappear on the next refetch. Updates run in an\n // effect (not during render) so the cache stays consistent under strict\n // mode and concurrent rendering.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n useEffect(() => {\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n }, [options, asyncResults]);\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets ??\n widgetCacheRef.current.get(v);\n\n const valueObjects = useMemo<ComboboxOption[]>(\n () =>\n value.map((v) => ({\n value: v,\n label: resolveLabel(v),\n widgets: resolveWidgets(v),\n })),\n // resolveLabel/resolveWidgets close over options/asyncResults/labelFor\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [value, options, asyncResults, labelFor],\n );\n\n const items = useMemo<ComboboxOption[]>(() => {\n const list: ComboboxOption[] = [];\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, trimmedInput, filteredSuggestions]);\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = value.indexOf(String(active.id));\n const to = value.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const next = [...value];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n onChange(next);\n };\n\n return (\n <Combobox<ComboboxOption, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n value={valueObjects}\n onValueChange={(next) => {\n if (!Array.isArray(next)) return;\n // Unique by value, rewrite __create items to clean objects.\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const item of next) {\n const v = typeof item === \"string\" ? item : (item as ComboboxOption).value;\n if (!v || seen.has(v)) continue;\n if (excludeSet.has(v)) continue;\n if (maxItems !== undefined && deduped.length >= maxItems) break;\n seen.add(v);\n deduped.push(v);\n }\n onChange(deduped);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n isItemEqualToValue={(a, b) => (a as ComboboxOption).value === (b as ComboboxOption).value}\n itemToStringLabel={(item) => (item as ComboboxOption).label}\n itemToStringValue={(item) => (item as ComboboxOption).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {reorderable ? (\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={value} strategy={horizontalListSortingStrategy}>\n {valueObjects.map((v) => (\n <SortableChip\n key={v.value}\n id={v.value}\n onRemove={() => onChange(value.filter((x) => x !== v.value))}\n >\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </SortableChip>\n ))}\n </SortableContext>\n </DndContext>\n ) : (\n valueObjects.map((v) => (\n <ComboboxChip key={v.value}>\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </ComboboxChip>\n ))\n )}\n <ComboboxChipsInput\n placeholder={value.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n // Prefer an exact match against the visible suggestions, otherwise\n // fall back to creating a new value when allowed.\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? trimmedInput : null);\n if (!toAdd) return;\n if (selectedSet.has(toAdd) || excludeSet.has(toAdd)) return;\n onChange([...value, toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n // Field-type widget renders on the trailing edge of the row so\n // the field name reads first; everything else stays in the\n // leading widget strip.\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const leadingWidgets =\n !item.__create && item.widgets ? splitWidgets(item.widgets).leading : undefined;\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={item}\n className={item.__create ? \"pz:italic\" : undefined}\n disabled={!selectedSet.has(item.value) && atMax && !item.__create}\n >\n {!item.__create && iconFor?.(item.value)}\n {leadingWidgets && <WidgetStrip widgets={leadingWidgets} size={14} />}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"suggest-combobox.mjs","names":[],"sources":["../../../../src/components/internal/combobox/suggest-combobox.tsx"],"sourcesContent":["import {\n closestCenter,\n DndContext,\n type DragEndEvent,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport { horizontalListSortingStrategy, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport type { WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconGripVertical } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\n/** Internal item type — the base-ui Combobox handles `{value, label}` natively. */\ntype ComboboxOption = PayloadOption & { __create?: boolean };\n\n/** Split widgets so `field_type` can render on the trailing edge of dropdown\n * rows while every other widget stays in the leading strip. */\nfunction splitWidgets(widgets: WidgetsByKind): {\n leading: WidgetsByKind | undefined;\n} {\n const { field_type: _ignored, ...rest } = widgets;\n const hasLeading = Object.values(rest).some((v) => v != null);\n return { leading: hasLeading ? rest : undefined };\n}\n\n/** Reorderable chip matching `ComboboxChip` visually, with a grip handle. */\nfunction SortableChip({\n id,\n onRemove,\n children,\n}: {\n id: string;\n onRemove: () => void;\n children: ReactNode;\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id,\n });\n const style: React.CSSProperties = {\n transform: transform\n ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`\n : undefined,\n transition,\n opacity: isDragging ? 0.6 : 1,\n };\n return (\n <span\n ref={setNodeRef}\n style={style}\n className={cn(\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-1 pz:rounded-sm pz:bg-muted pz:pl-0.5 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground\",\n )}\n {...attributes}\n >\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Drag to reorder\"\n className=\"pz:flex pz:items-center pz:justify-center pz:cursor-grab pz:text-muted-foreground pz:hover:text-foreground pz:touch-none\"\n {...listeners}\n >\n <IconGripVertical width={12} height={12} />\n </span>\n {children}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n className=\"pz:opacity-50 pz:hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n aria-label=\"Remove\"\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n}\n\nexport interface SuggestComboboxProps {\n value: string[];\n onChange: (value: string[]) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n excludeValues?: string[];\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n labelFor?: (value: string) => string;\n /** Optional leading icon (e.g. provider logo) rendered inside chips and list items. */\n iconFor?: (value: string) => ReactNode;\n emptyLabel?: string;\n placeholder?: string;\n /** When true, chips become drag-sortable and reorder the `value` array. */\n reorderable?: boolean;\n /**\n * Single-select mode: the selection renders as a plain value (not a\n * removable chip), picking an option REPLACES it, and the popover closes on\n * pick — a searchable analog of a native select. Clicking the selected\n * option again clears it. `value` stays an array (length ≤ 1) so callers\n * keep one contract across both modes; `maxItems`/`reorderable` are ignored.\n */\n single?: boolean;\n}\n\n/**\n * Unified combobox built on `@base-ui/react`'s Combobox primitive.\n *\n * - Chips + inline search input via `ComboboxChips` / `ComboboxChipsInput`.\n * - Static (`options`) or async (`loadOptions`) item sources. External\n * filtering in both cases so match-sorter ranking / server relevance is\n * honored and base-ui's built-in filter doesn't double-filter.\n * - Free-text creation (`allowCreate`) renders an inline \"Add \\\"X\\\"\" item.\n * - `maxItems`, `excludeValues`, `disableSearch`, `ariaInvalid`, `disabled`.\n * - Debounced async loader with an in-memory cache keyed by query string.\n */\nexport function SuggestCombobox({\n value,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n excludeValues = [],\n ariaInvalid,\n disabled,\n debounceMs = 300,\n labelFor,\n iconFor,\n emptyLabel,\n placeholder,\n reorderable = false,\n single = false,\n}: SuggestComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n // Per-instance cache key prefix so two comboboxes with the same query but\n // different `loadOptions` functions don't share results.\n const instanceId = useId();\n\n // SWR handles dedupe + caching by-query + race-safety. We defer the\n // empty-query fetch until the user opens the dropdown — some backends\n // (e.g. Crustdata persondb) don't accept empty queries, and on-mount\n // fetches are wasted work if the field is never opened. `keepPreviousData`\n // preserves the prior result set during a refetch so the UI dims rather\n // than empties.\n const swrKey: readonly [string, string] | null =\n loadOptions && (open || debouncedQuery !== \"\") ? [instanceId, debouncedQuery] : null;\n const {\n data: asyncResults = null,\n isLoading: swrIsLoading,\n isValidating: swrIsValidating,\n } = useSWR<PayloadOption[], Error, readonly [string, string] | null>(\n swrKey,\n async ([, q]) => {\n // `swrKey` was `null` when `loadOptions` is undefined, so the fetcher\n // never runs without one — the runtime check is just to satisfy TS.\n if (!loadOptions) return [];\n const opts = await loadOptions(q);\n return opts.filter((o) => !!o.value);\n },\n { keepPreviousData: true },\n );\n const loading = swrIsLoading || swrIsValidating;\n\n const excludeSet = useMemo(() => new Set(excludeValues), [excludeValues]);\n const selectedSet = useMemo(() => new Set(value), [value]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n const ranked = isAsync ? source : filterOptions(source, input);\n return ranked.filter((o) => !excludeSet.has(o.value));\n }, [isAsync, asyncResults, options, input, excludeSet]);\n\n const trimmedInput = input.trim();\n const showCreateRow =\n allowCreate &&\n trimmedInput.length > 0 &&\n !selectedSet.has(trimmedInput) &&\n !excludeSet.has(trimmedInput) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === trimmedInput.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n if (labelFor) return labelFor(v);\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n // Persist widgets per value across loadOptions cycles. Async results are\n // replaced with each query, so without this cache an icon shown when\n // picking a value would disappear on the next refetch. Updates run in an\n // effect (not during render) so the cache stays consistent under strict\n // mode and concurrent rendering.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n useEffect(() => {\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n }, [options, asyncResults]);\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets ??\n widgetCacheRef.current.get(v);\n\n const valueObjects = useMemo<ComboboxOption[]>(\n () =>\n value.map((v) => ({\n value: v,\n label: resolveLabel(v),\n widgets: resolveWidgets(v),\n })),\n // resolveLabel/resolveWidgets close over options/asyncResults/labelFor\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [value, options, asyncResults, labelFor],\n );\n\n const items = useMemo<ComboboxOption[]>(() => {\n const list: ComboboxOption[] = [];\n if (showCreateRow) {\n list.push({\n value: trimmedInput,\n label: `Add \"${trimmedInput}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, trimmedInput, filteredSuggestions]);\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const sortSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor),\n );\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const from = value.indexOf(String(active.id));\n const to = value.indexOf(String(over.id));\n if (from === -1 || to === -1) return;\n const next = [...value];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n onChange(next);\n };\n\n return (\n <Combobox<ComboboxOption, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n value={valueObjects}\n onValueChange={(next) => {\n if (!Array.isArray(next)) return;\n if (single) {\n // Replace-on-pick: the base-ui multiple model APPENDS the picked\n // item, so the last valid entry is the user's choice; toggling the\n // selected item off yields an empty array (= clear).\n const last = [...next]\n .reverse()\n .map((item) => (typeof item === \"string\" ? item : (item as ComboboxOption).value))\n .find((v) => v && !excludeSet.has(v));\n onChange(last ? [last] : []);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n setOpen(false);\n return;\n }\n // Unique by value, rewrite __create items to clean objects.\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const item of next) {\n const v = typeof item === \"string\" ? item : (item as ComboboxOption).value;\n if (!v || seen.has(v)) continue;\n if (excludeSet.has(v)) continue;\n if (maxItems !== undefined && deduped.length >= maxItems) break;\n seen.add(v);\n deduped.push(v);\n }\n onChange(deduped);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n isItemEqualToValue={(a, b) => (a as ComboboxOption).value === (b as ComboboxOption).value}\n itemToStringLabel={(item) => (item as ComboboxOption).label}\n itemToStringValue={(item) => (item as ComboboxOption).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {single ? (\n // Select-like value display: a plain label, hidden while the user\n // types so the input reads like a search box over the selection.\n valueObjects[0] &&\n input === \"\" && (\n <span className=\"pz:flex pz:min-w-0 pz:items-center pz:gap-1.5 pz:text-sm\">\n {iconFor?.(valueObjects[0].value)}\n {valueObjects[0].widgets && (\n <WidgetStrip widgets={valueObjects[0].widgets} size={14} />\n )}\n <span className=\"pz:truncate\">{valueObjects[0].label}</span>\n </span>\n )\n ) : reorderable ? (\n <DndContext\n sensors={sortSensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={value} strategy={horizontalListSortingStrategy}>\n {valueObjects.map((v) => (\n <SortableChip\n key={v.value}\n id={v.value}\n onRemove={() => onChange(value.filter((x) => x !== v.value))}\n >\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </SortableChip>\n ))}\n </SortableContext>\n </DndContext>\n ) : (\n valueObjects.map((v) => (\n <ComboboxChip key={v.value}>\n {iconFor?.(v.value)}\n {v.widgets && <WidgetStrip widgets={v.widgets} size={14} />}\n {v.label}\n </ComboboxChip>\n ))\n )}\n <ComboboxChipsInput\n placeholder={value.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (trimmedInput.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (!single && atMax) return;\n // Prefer an exact match against the visible suggestions, otherwise\n // fall back to creating a new value when allowed.\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === trimmedInput.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? trimmedInput : null);\n if (!toAdd) return;\n if (excludeSet.has(toAdd)) return;\n if (single) {\n onChange([toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n setOpen(false);\n return;\n }\n if (selectedSet.has(toAdd)) return;\n onChange([...value, toAdd]);\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {!single && atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n // Field-type widget renders on the trailing edge of the row so\n // the field name reads first; everything else stays in the\n // leading widget strip.\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const leadingWidgets =\n !item.__create && item.widgets ? splitWidgets(item.widgets).leading : undefined;\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={item}\n className={item.__create ? \"pz:italic\" : undefined}\n disabled={!single && !selectedSet.has(item.value) && atMax && !item.__create}\n >\n {!item.__create && iconFor?.(item.value)}\n {leadingWidgets && <WidgetStrip widgets={leadingWidgets} size={14} />}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-effect-catalog-table.d.mts","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"use-effect-catalog-table.d.mts","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"mappings":";;;;;;;iBA+CgB,qBAAA,CACd,MAAA;EACE,oBAAA,GAAuB,YAAA;;;AAF3B;;EAOI,MAAA,IAAU,KAAA,EAAO,2BAAA;AAAA;;;;cAgFL,mBAAA;WAA4B,cAAA;EAAA;;;;;sBAhCjC,mBAAA;;yCAAmB,OAAA,CAAA,cAAA;AAAA;AAAA,KAmFlB,2BAAA,GAA8B,UAAA,QAAkB,qBAAA"}
|
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { catalogSearchFilter, catalogSearchSort } from "../utils/catalog-helpers.mjs";
|
|
2
2
|
import { useDebounce } from "./use-debounce.mjs";
|
|
3
3
|
import { useCallback, useMemo, useState } from "react";
|
|
4
|
-
import { getInitialSheetEffectTableData, getLowestSheetEffectCreditAmount, isTokenBilledOperations } from "@pipe0/base";
|
|
5
|
-
import { createColumnHelper, getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table";
|
|
4
|
+
import { getInitialSheetEffectTableData, getLowestSheetEffectCreditAmount, getSheetEffectSearchTarget, isTokenBilledOperations } from "@pipe0/base";
|
|
5
|
+
import { createColumnHelper, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
6
6
|
|
|
7
7
|
//#region src/hooks/use-effect-catalog-table.ts
|
|
8
8
|
const columnHelper = createColumnHelper();
|
|
9
9
|
const columns = [
|
|
10
10
|
columnHelper.accessor("effectId", {
|
|
11
11
|
filterFn: "includesString",
|
|
12
|
-
enableGlobalFilter:
|
|
13
|
-
}),
|
|
14
|
-
columnHelper.accessor("description", {
|
|
15
|
-
filterFn: "fuzzy",
|
|
16
|
-
enableGlobalFilter: true
|
|
12
|
+
enableGlobalFilter: false
|
|
17
13
|
}),
|
|
14
|
+
columnHelper.accessor("description", { enableGlobalFilter: false }),
|
|
18
15
|
columnHelper.accessor("label", {
|
|
19
16
|
filterFn: "includesString",
|
|
20
|
-
enableGlobalFilter:
|
|
17
|
+
enableGlobalFilter: false
|
|
18
|
+
}),
|
|
19
|
+
columnHelper.accessor((row) => getSheetEffectSearchTarget(row), {
|
|
20
|
+
id: "search",
|
|
21
|
+
enableGlobalFilter: true,
|
|
22
|
+
enableSorting: true,
|
|
23
|
+
sortingFn: catalogSearchSort
|
|
21
24
|
})
|
|
22
25
|
];
|
|
23
26
|
function useEffectCatalogTable(config = {}) {
|
|
@@ -29,8 +32,8 @@ function useEffectCatalogTable(config = {}) {
|
|
|
29
32
|
data: useMemo(() => category ? baselineTableData.filter((e) => (e.categories ?? []).includes(category)) : baselineTableData, [baselineTableData, category]),
|
|
30
33
|
getCoreRowModel: getCoreRowModel(),
|
|
31
34
|
getFilteredRowModel: getFilteredRowModel(),
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
getSortedRowModel: getSortedRowModel(),
|
|
36
|
+
globalFilterFn: catalogSearchFilter,
|
|
34
37
|
getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter !== false,
|
|
35
38
|
initialState: {
|
|
36
39
|
columnFilters: initialColumnFilters,
|
|
@@ -40,6 +43,10 @@ function useEffectCatalogTable(config = {}) {
|
|
|
40
43
|
const [globalFilterInput, setGlobalFilterInput, setGlobalFilterImmediately] = useDebounce((v) => {
|
|
41
44
|
if (v === table.getState().globalFilter) return;
|
|
42
45
|
table.setGlobalFilter(v);
|
|
46
|
+
table.setSorting(v ? [{
|
|
47
|
+
id: "search",
|
|
48
|
+
desc: true
|
|
49
|
+
}] : []);
|
|
43
50
|
if (v) setCategory(null);
|
|
44
51
|
});
|
|
45
52
|
const handleCategoryChange = useCallback((next) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-effect-catalog-table.mjs","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"sourcesContent":["import {\n getInitialSheetEffectTableData,\n getLowestSheetEffectCreditAmount,\n isTokenBilledOperations,\n type ProviderName,\n type SheetEffectCatalogTableData,\n type SheetEffectCategory,\n} from \"@pipe0/base\";\nimport {\n type ColumnFilter,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { EffectCardData } from \"../types/catalog-adapters.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"use-effect-catalog-table.mjs","names":[],"sources":["../../src/hooks/use-effect-catalog-table.ts"],"sourcesContent":["import {\n getInitialSheetEffectTableData,\n getLowestSheetEffectCreditAmount,\n getSheetEffectSearchTarget,\n isTokenBilledOperations,\n type ProviderName,\n type SheetEffectCatalogTableData,\n type SheetEffectCategory,\n} from \"@pipe0/base\";\nimport {\n type ColumnFilter,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { EffectCardData } from \"../types/catalog-adapters.js\";\nimport { catalogSearchFilter, catalogSearchSort } from \"../utils/catalog-helpers.js\";\nimport { useDebounce } from \"./use-debounce.js\";\n\nconst columnHelper = createColumnHelper<SheetEffectCatalogTableData>();\n\nconst columns = [\n columnHelper.accessor(\"effectId\", {\n filterFn: \"includesString\",\n enableGlobalFilter: false,\n }),\n columnHelper.accessor(\"description\", {\n enableGlobalFilter: false,\n }),\n columnHelper.accessor(\"label\", {\n filterFn: \"includesString\",\n enableGlobalFilter: false,\n }),\n // Hidden relevance column — the ONLY globally-filterable one, so the global\n // filter scores each row exactly once and catalogSearchSort can read the\n // score back from this column's filter meta.\n columnHelper.accessor((row) => getSheetEffectSearchTarget(row), {\n id: \"search\",\n enableGlobalFilter: true,\n enableSorting: true,\n sortingFn: catalogSearchSort,\n }),\n];\n\nexport function useEffectCatalogTable(\n config: {\n initialColumnFilters?: ColumnFilter[];\n /**\n * Optional predicate to pre-filter the catalog (e.g. by `selectionMode`\n * for the current row selection). Memoize it to keep the table stable.\n */\n filter?: (entry: SheetEffectCatalogTableData) => boolean;\n } = {},\n) {\n const [category, setCategory] = useState<SheetEffectCategory | null>(null);\n const { initialColumnFilters = [] as ColumnFilter[], filter } = config;\n\n // Baseline (config.filter applied, but no category / global filter). Drives\n // the stable category-button counts so the row doesn't jump.\n const baselineTableData = useMemo(\n () =>\n filter ? getInitialSheetEffectTableData().filter(filter) : getInitialSheetEffectTableData(),\n [filter],\n );\n\n const initialEffectTableData = useMemo(\n () =>\n category\n ? baselineTableData.filter((e) => (e.categories ?? []).includes(category))\n : baselineTableData,\n [baselineTableData, category],\n );\n\n const table = useReactTable({\n columns,\n data: initialEffectTableData,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getSortedRowModel: getSortedRowModel(),\n globalFilterFn: catalogSearchFilter,\n getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter !== false,\n initialState: {\n columnFilters: initialColumnFilters,\n pagination: {\n pageSize: 10,\n },\n },\n });\n\n const [globalFilterInput, setGlobalFilterInput, setGlobalFilterImmediately] = useDebounce((v) => {\n if (v === table.getState().globalFilter) return;\n table.setGlobalFilter(v);\n // Relevance order only while a query is active; empty query restores\n // catalog order.\n table.setSorting(v ? [{ id: \"search\", desc: true }] : []);\n if (v) setCategory(null);\n });\n\n const handleCategoryChange = useCallback(\n (next: SheetEffectCategory | null) => {\n setCategory(next);\n setGlobalFilterImmediately(\"\");\n },\n [setGlobalFilterImmediately],\n );\n\n const rows = table.getRowModel().rows;\n const cards = useMemo<EffectCardData[]>(\n () =>\n rows.map((row) => {\n const entry = row.original;\n const provider = (entry.managedProviders[0] ?? \"pipe0\") as ProviderName;\n return {\n effectId: entry.effectId,\n baseEffect: entry.baseEffect,\n label: entry.label,\n description: entry.description,\n provider,\n providers: entry.managedProviders.length ? entry.managedProviders : [provider],\n startingCreditAmount: getLowestSheetEffectCreditAmount(entry),\n costMode: isTokenBilledOperations(entry.billableOperations)\n ? (\"usage\" as const)\n : (\"per_result\" as const),\n selectionMode: entry.selectionMode,\n entry,\n };\n }),\n [rows],\n );\n\n const cardsByCategory = useMemo<\n { category: SheetEffectCategory; cards: EffectCardData[] }[]\n >(() => {\n const groups = new Map<SheetEffectCategory, EffectCardData[]>();\n for (const card of cards) {\n const cats = (card.entry.categories ?? []) as SheetEffectCategory[];\n for (const cat of cats) {\n const arr = groups.get(cat);\n if (arr) arr.push(card);\n else groups.set(cat, [card]);\n }\n }\n return Array.from(groups, ([category, group]) => ({ category, cards: group }));\n }, [cards]);\n\n const categoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const card of cards) {\n for (const cat of (card.entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [cards]);\n\n // Stable counts based on the baseline (no category / global filter applied).\n const baselineCardCount = baselineTableData.length;\n const baselineCategoryCounts = useMemo<Partial<Record<SheetEffectCategory, number>>>(() => {\n const counts: Partial<Record<SheetEffectCategory, number>> = {};\n for (const entry of baselineTableData) {\n for (const cat of (entry.categories ?? []) as SheetEffectCategory[]) {\n counts[cat] = (counts[cat] ?? 0) + 1;\n }\n }\n return counts;\n }, [baselineTableData]);\n\n return {\n table,\n cards,\n cardsByCategory,\n categoryCounts,\n baselineCardCount,\n baselineCategoryCounts,\n\n category,\n setCategory: handleCategoryChange,\n globalFilterInput,\n setGlobalFilterInput,\n };\n}\n\nexport type UseEffectCatalogTableReturn = ReturnType<typeof useEffectCatalogTable>;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-form-core.d.mts","names":[],"sources":["../../src/hooks/use-form-core.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"use-form-core.d.mts","names":[],"sources":["../../src/hooks/use-form-core.ts"],"mappings":";;KAgCY,cAAA;AAAA,KASA,cAAA;EACV,MAAA,EAAQ,cAAA,EADgB;EAGxB,QAAA,WAFsB;EAItB,cAAA,WAJQ;EAMR,mBAAA,WAFA;EAIA,yBAAA;AAAA"}
|