@open-mercato/ui 0.7.1-develop.7122.1.421cefe668 → 0.7.1-develop.7130.1.fef2396fd8
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/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiAssistantLauncher.js +7 -1
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChatSessions.js +4 -1
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiAssistantAvailable.js +32 -0
- package/dist/ai/useAiAssistantAvailable.js.map +7 -0
- package/dist/backend/detail/addressFormat.js +18 -2
- package/dist/backend/detail/addressFormat.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +108 -92
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/AiAssistantLauncher.tsx +26 -6
- package/src/ai/AiChatSessions.tsx +9 -1
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +67 -0
- package/src/ai/__tests__/AiChatSessions.test.tsx +77 -0
- package/src/ai/__tests__/useAiAssistantAvailable.test.tsx +96 -0
- package/src/ai/index.ts +5 -0
- package/src/ai/useAiAssistantAvailable.ts +50 -0
- package/src/backend/detail/__tests__/addressFormat.taxId.test.ts +33 -0
- package/src/backend/detail/addressFormat.tsx +59 -1
- package/src/backend/inputs/ComboboxInput.tsx +112 -79
- package/src/backend/inputs/__tests__/ComboboxInput.dialog.test.tsx +134 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +4 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ai/useAiAssistantAvailable.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n getEnabledModuleIds,\n subscribeToInjectionRegistryChanges,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useBackendChrome } from '../backend/BackendChromeProvider'\n\nexport const AI_ASSISTANT_MODULE_ID = 'ai_assistant'\nexport const AI_ASSISTANT_VIEW_FEATURE = 'ai_assistant.view'\n\nfunction readEnabledModuleIds(): ReadonlySet<string> | null {\n return getEnabledModuleIds()\n}\n\nfunction readEnabledModuleIdsOnServer(): ReadonlySet<string> | null {\n return null\n}\n\n/**\n * Whether the AI assistant surfaces should mount at all.\n *\n * Every `/api/ai_assistant/*` route is gated on `ai_assistant.view`, and an\n * installation that does not enable the module has no such routes to answer.\n * Mounting the launcher or the conversation sync in either case only produces\n * 404s / 403s, so both call sites consult this first instead of probing.\n *\n * Two independent signals have to agree:\n * - the client enabled-module registry, populated from the generated\n * `enabled-module-ids.generated.ts` during client bootstrap. It registers\n * asynchronously, so `null` means \"not known yet\", not \"module absent\".\n * - `ai_assistant.view` in the backend chrome payload. The server already\n * drops grants owned by disabled modules (and expands a superadmin `*`\n * into enabled modules only), and the payload is fetched once and cached\n * by `BackendChromeProvider`, so this costs no extra request. It stays\n * fail-closed until the payload arrives, which is what keeps the cold-load\n * window quiet.\n */\nexport function useAiAssistantAvailable(): boolean {\n const { payload } = useBackendChrome()\n const enabledModuleIds = React.useSyncExternalStore(\n subscribeToInjectionRegistryChanges,\n readEnabledModuleIds,\n readEnabledModuleIdsOnServer,\n )\n const moduleEnabled = enabledModuleIds === null || enabledModuleIds.has(AI_ASSISTANT_MODULE_ID)\n return moduleEnabled && hasFeature(payload?.grantedFeatures, AI_ASSISTANT_VIEW_FEATURE)\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAE1B,MAAM,yBAAyB;AAC/B,MAAM,4BAA4B;AAEzC,SAAS,uBAAmD;AAC1D,SAAO,oBAAoB;AAC7B;AAEA,SAAS,+BAA2D;AAClE,SAAO;AACT;AAqBO,SAAS,0BAAmC;AACjD,QAAM,EAAE,QAAQ,IAAI,iBAAiB;AACrC,QAAM,mBAAmB,MAAM;AAAA,IAC7B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,gBAAgB,qBAAqB,QAAQ,iBAAiB,IAAI,sBAAsB;AAC9F,SAAO,iBAAiB,WAAW,SAAS,iBAAiB,yBAAyB;AACxF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -63,7 +63,22 @@ function formatAddressLines(address, format) {
|
|
|
63
63
|
function formatAddressString(address, format, separator = ", ") {
|
|
64
64
|
return formatAddressLines(address, format).filter(Boolean).join(separator);
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
const TAX_ID_LABEL_KEY_BY_TYPE = {
|
|
67
|
+
pl_nip: "plNip",
|
|
68
|
+
eu_vat: "euVat"
|
|
69
|
+
};
|
|
70
|
+
function resolveTaxIdLabel(label, taxIdType) {
|
|
71
|
+
if (!label) return void 0;
|
|
72
|
+
if (typeof label === "string") return label;
|
|
73
|
+
const key = TAX_ID_LABEL_KEY_BY_TYPE[typeof taxIdType === "string" ? taxIdType : ""] ?? "other";
|
|
74
|
+
return label[key];
|
|
75
|
+
}
|
|
76
|
+
function AddressView({
|
|
77
|
+
address,
|
|
78
|
+
format,
|
|
79
|
+
className,
|
|
80
|
+
lineClassName
|
|
81
|
+
}) {
|
|
67
82
|
const lines = formatAddressLines(address, format);
|
|
68
83
|
if (!lines.length) return null;
|
|
69
84
|
return /* @__PURE__ */ jsx("div", { className, children: lines.map((line, index) => /* @__PURE__ */ jsx("div", { className: lineClassName, children: line }, `${index}-${line}`)) });
|
|
@@ -72,6 +87,7 @@ export {
|
|
|
72
87
|
AddressView,
|
|
73
88
|
formatAddressJson,
|
|
74
89
|
formatAddressLines,
|
|
75
|
-
formatAddressString
|
|
90
|
+
formatAddressString,
|
|
91
|
+
resolveTaxIdLabel
|
|
76
92
|
};
|
|
77
93
|
//# sourceMappingURL=addressFormat.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/detail/addressFormat.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\n\nexport type AddressFormatStrategy = 'line_first' | 'street_first'\n\nexport type AddressValue = {\n addressLine1: string | null | undefined\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n companyName?: string | null\n}\n\nexport type AddressJsonShape = {\n format: AddressFormatStrategy\n companyName: string | null\n addressLine1: string | null\n addressLine2: string | null\n buildingNumber: string | null\n flatNumber: string | null\n postalCode: string | null\n city: string | null\n region: string | null\n country: string | null\n}\n\nfunction normalize(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction mergeStreetLine(address: AddressValue): string | null {\n const street = normalize(address.addressLine1)\n const building = normalize(address.buildingNumber)\n const flat = normalize(address.flatNumber)\n if (!street && !building && !flat) return null\n let line = street ?? ''\n if (building) line = line ? `${line} ${building}` : building\n if (flat) line = line ? `${line}/${flat}` : flat\n return line.length ? line : null\n}\n\nexport function formatAddressJson(address: AddressValue, format: AddressFormatStrategy): AddressJsonShape {\n return {\n format,\n companyName: normalize(address.companyName),\n addressLine1: normalize(address.addressLine1),\n addressLine2: normalize(address.addressLine2),\n buildingNumber: normalize(address.buildingNumber),\n flatNumber: normalize(address.flatNumber),\n postalCode: normalize(address.postalCode),\n city: normalize(address.city),\n region: normalize(address.region),\n country: normalize(address.country),\n }\n}\n\nexport function formatAddressLines(address: AddressValue, format: AddressFormatStrategy): string[] {\n const json = formatAddressJson(address, format)\n const lines: string[] = []\n\n if (json.companyName) lines.push(json.companyName)\n\n if (format === 'street_first') {\n const streetLine = mergeStreetLine(address)\n if (streetLine) lines.push(streetLine)\n const supplemental = normalize(address.addressLine2)\n if (supplemental) lines.push(supplemental)\n const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')\n if (postalCity.length) lines.push(postalCity)\n if (json.region) lines.push(json.region)\n if (json.country) lines.push(json.country)\n } else {\n if (json.addressLine1) {\n const baseLine1 = json.addressLine1\n const appended = mergeStreetLine(address)\n if (!json.buildingNumber && !json.flatNumber) {\n lines.push(baseLine1)\n } else {\n const composite = appended ?? baseLine1\n lines.push(composite)\n }\n }\n if (json.addressLine2) lines.push(json.addressLine2)\n const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')\n if (postalCity.length) lines.push(postalCity)\n if (json.region) lines.push(json.region)\n if (json.country) lines.push(json.country)\n }\n\n return lines\n}\n\nexport function formatAddressString(address: AddressValue, format: AddressFormatStrategy, separator = ', '): string {\n return formatAddressLines(address, format).filter(Boolean).join(separator)\n}\n\ntype AddressViewProps = {\n address: AddressValue\n format: AddressFormatStrategy\n className?: string\n lineClassName?: string\n}\n\nexport function AddressView({
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\n\nexport type AddressFormatStrategy = 'line_first' | 'street_first'\n\nexport type AddressValue = {\n addressLine1: string | null | undefined\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n companyName?: string | null\n /**\n * Contact details that belong to the ADDRESS rather than to the customer: who to call about this\n * delivery, and the tax identifier this invoice address was billed under. They remain available to\n * address editors and snapshot payloads, but are deliberately excluded from `formatAddressLines`\n * and `AddressView`, whose existing contract remains postal-only.\n *\n * `taxIdType` interprets the value in Stripe's `{country}_{kind}` vocabulary (`pl_nip`, `eu_vat`,\n * `other`, widened additively): `1234567890` and `PL1234567890` are the same business, and only the\n * type tells a domestic identifier from an EU VAT number. It is metadata about `taxId`, never a\n * displayed field of its own.\n */\n phone?: string | null\n taxId?: string | null\n taxIdType?: string | null\n}\n\n/**\n * Tax-id labels keyed by `taxIdType`, for a caller that wants the identifier named correctly rather\n * than generically.\n *\n * The stored scheme is chosen explicitly rather than inferred from the identifier. `other` also\n * covers an address written before `taxIdType` existed. An unrecognised type takes the `other` route\n * instead of guessing a domestic scheme.\n */\nexport type TaxIdLabelByType = {\n plNip: string\n euVat: string\n other: string\n}\n\nexport type AddressJsonShape = {\n format: AddressFormatStrategy\n companyName: string | null\n addressLine1: string | null\n addressLine2: string | null\n buildingNumber: string | null\n flatNumber: string | null\n postalCode: string | null\n city: string | null\n region: string | null\n country: string | null\n}\n\nfunction normalize(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction mergeStreetLine(address: AddressValue): string | null {\n const street = normalize(address.addressLine1)\n const building = normalize(address.buildingNumber)\n const flat = normalize(address.flatNumber)\n if (!street && !building && !flat) return null\n let line = street ?? ''\n if (building) line = line ? `${line} ${building}` : building\n if (flat) line = line ? `${line}/${flat}` : flat\n return line.length ? line : null\n}\n\nexport function formatAddressJson(address: AddressValue, format: AddressFormatStrategy): AddressJsonShape {\n return {\n format,\n companyName: normalize(address.companyName),\n addressLine1: normalize(address.addressLine1),\n addressLine2: normalize(address.addressLine2),\n buildingNumber: normalize(address.buildingNumber),\n flatNumber: normalize(address.flatNumber),\n postalCode: normalize(address.postalCode),\n city: normalize(address.city),\n region: normalize(address.region),\n country: normalize(address.country),\n }\n}\n\nexport function formatAddressLines(address: AddressValue, format: AddressFormatStrategy): string[] {\n const json = formatAddressJson(address, format)\n const lines: string[] = []\n\n if (json.companyName) lines.push(json.companyName)\n\n if (format === 'street_first') {\n const streetLine = mergeStreetLine(address)\n if (streetLine) lines.push(streetLine)\n const supplemental = normalize(address.addressLine2)\n if (supplemental) lines.push(supplemental)\n const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')\n if (postalCity.length) lines.push(postalCity)\n if (json.region) lines.push(json.region)\n if (json.country) lines.push(json.country)\n } else {\n if (json.addressLine1) {\n const baseLine1 = json.addressLine1\n const appended = mergeStreetLine(address)\n if (!json.buildingNumber && !json.flatNumber) {\n lines.push(baseLine1)\n } else {\n const composite = appended ?? baseLine1\n lines.push(composite)\n }\n }\n if (json.addressLine2) lines.push(json.addressLine2)\n const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')\n if (postalCity.length) lines.push(postalCity)\n if (json.region) lines.push(json.region)\n if (json.country) lines.push(json.country)\n }\n\n return lines\n}\n\nexport function formatAddressString(address: AddressValue, format: AddressFormatStrategy, separator = ', '): string {\n return formatAddressLines(address, format).filter(Boolean).join(separator)\n}\n\n/**\n * Which member of a label map names which scheme. Private, and deliberately not exhaustive: an\n * unrecognised type resolves to `other`, so the vocabulary can widen without every caller being\n * updated in the same release.\n */\nconst TAX_ID_LABEL_KEY_BY_TYPE: Record<string, keyof TaxIdLabelByType> = {\n pl_nip: 'plNip',\n eu_vat: 'euVat',\n}\n\n/**\n * The label a tax identifier should carry, given its type. Exported because the editor renders the\n * same identifier as an input and must name it the same way this formatter does \u2014 two copies of the\n * mapping is exactly how a foreign number ends up under a domestic scheme's name.\n */\nexport function resolveTaxIdLabel(\n label: string | TaxIdLabelByType | undefined,\n taxIdType: string | null | undefined,\n): string | undefined {\n if (!label) return undefined\n if (typeof label === 'string') return label\n const key = TAX_ID_LABEL_KEY_BY_TYPE[typeof taxIdType === 'string' ? taxIdType : ''] ?? 'other'\n return label[key]\n}\n\ntype AddressViewProps = {\n address: AddressValue\n format: AddressFormatStrategy\n className?: string\n lineClassName?: string\n}\n\nexport function AddressView({\n address,\n format,\n className,\n lineClassName,\n}: AddressViewProps): React.ReactElement | null {\n const lines = formatAddressLines(address, format)\n if (!lines.length) return null\n return (\n <div className={className}>\n {lines.map((line, index) => (\n <div key={`${index}-${line}`} className={lineClassName}>\n {line}\n </div>\n ))}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { X } from "lucide-react";
|
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { Button } from "../../primitives/button.js";
|
|
7
7
|
import { IconButton } from "../../primitives/icon-button.js";
|
|
8
|
+
import { Popover, PopoverAnchor, PopoverContent } from "../../primitives/popover.js";
|
|
8
9
|
function normalizeOptions(input) {
|
|
9
10
|
if (!Array.isArray(input)) return [];
|
|
10
11
|
return input.map((option) => {
|
|
@@ -316,104 +317,119 @@ function ComboboxInput({
|
|
|
316
317
|
}, [optionDomId, selectedIndex, showSuggestions]);
|
|
317
318
|
const showClearButton = clearable && !disabled && (value !== "" || input !== "");
|
|
318
319
|
const listboxVisible = showSuggestions && !disabled && (loading || filteredSuggestions.length > 0 || touched && input.trim().length > 0);
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
320
|
+
return (
|
|
321
|
+
// The suggestion list goes through the DS Popover so it is portaled out of any
|
|
322
|
+
// scrolling ancestor. Rendered in place it was clipped by a Dialog's
|
|
323
|
+
// `overflow-y-auto`, where z-index cannot help. `open` stays fully controlled
|
|
324
|
+
// (no `onOpenChange`) so the blur timer, Escape and selection keep owning
|
|
325
|
+
// dismissal exactly as before.
|
|
326
|
+
/* @__PURE__ */ jsxs(Popover, { open: listboxVisible, children: [
|
|
327
|
+
/* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
|
|
328
|
+
/* @__PURE__ */ jsx(
|
|
329
|
+
"input",
|
|
330
|
+
{
|
|
331
|
+
ref: inputRef,
|
|
332
|
+
type: "text",
|
|
333
|
+
className: [
|
|
334
|
+
"w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed",
|
|
335
|
+
showClearButton ? "pr-9" : ""
|
|
336
|
+
].filter(Boolean).join(" "),
|
|
337
|
+
value: input,
|
|
338
|
+
placeholder: resolvedPlaceholder,
|
|
339
|
+
autoFocus,
|
|
340
|
+
"data-crud-focus-target": "",
|
|
341
|
+
disabled,
|
|
342
|
+
role: "combobox",
|
|
343
|
+
"aria-expanded": listboxVisible,
|
|
344
|
+
"aria-controls": listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : void 0,
|
|
345
|
+
"aria-owns": listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : void 0,
|
|
346
|
+
"aria-autocomplete": "list",
|
|
347
|
+
"aria-activedescendant": listboxVisible && selectedIndex >= 0 ? optionDomId(selectedIndex) : void 0,
|
|
348
|
+
onFocus: () => {
|
|
349
|
+
setTouched(true);
|
|
350
|
+
if (suppressOpenOnFocusRef.current) {
|
|
351
|
+
suppressOpenOnFocusRef.current = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
resetBlurCloseState();
|
|
355
|
+
if (loadSuggestions && availableOptions.length === 0) {
|
|
356
|
+
setLoading(true);
|
|
357
|
+
}
|
|
358
|
+
setShowSuggestions(true);
|
|
359
|
+
},
|
|
360
|
+
onChange: (event) => {
|
|
361
|
+
setTouched(true);
|
|
362
|
+
userTypedRef.current = true;
|
|
363
|
+
setInput(event.target.value);
|
|
364
|
+
setShowSuggestions(true);
|
|
365
|
+
setSelectedIndex(-1);
|
|
366
|
+
},
|
|
367
|
+
onKeyDown: handleKeyDown,
|
|
368
|
+
onBlur: () => {
|
|
369
|
+
userTypedRef.current = false;
|
|
370
|
+
blurClosePendingRef.current = true;
|
|
371
|
+
clearBlurCloseTimer();
|
|
372
|
+
if (loadingRef.current) {
|
|
373
|
+
blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs);
|
|
377
|
+
}
|
|
366
378
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
),
|
|
371
|
-
showClearButton ? /* @__PURE__ */ jsx(
|
|
372
|
-
IconButton,
|
|
373
|
-
{
|
|
374
|
-
type: "button",
|
|
375
|
-
variant: "ghost",
|
|
376
|
-
size: "xs",
|
|
377
|
-
"aria-label": resolvedClearLabel,
|
|
378
|
-
className: "absolute right-1 top-1/2 -translate-y-1/2",
|
|
379
|
-
onMouseDown: (event) => event.preventDefault(),
|
|
380
|
-
onClick: handleClear,
|
|
381
|
-
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
382
|
-
}
|
|
383
|
-
) : null,
|
|
384
|
-
listboxVisible && /* @__PURE__ */ jsx(
|
|
385
|
-
"div",
|
|
386
|
-
{
|
|
387
|
-
className: "absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto",
|
|
388
|
-
children: loading && touched ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", role: "status", children: loadingLabel }) : touched && !filteredSuggestions.length ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", role: "status", children: noMatchesLabel }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", className: "flex flex-col gap-1", children: filteredSuggestions.map((option, index) => /* @__PURE__ */ jsxs(
|
|
389
|
-
Button,
|
|
379
|
+
),
|
|
380
|
+
showClearButton ? /* @__PURE__ */ jsx(
|
|
381
|
+
IconButton,
|
|
390
382
|
{
|
|
391
|
-
id: optionDomId(index),
|
|
392
383
|
type: "button",
|
|
393
384
|
variant: "ghost",
|
|
394
|
-
size: "
|
|
395
|
-
|
|
396
|
-
"
|
|
397
|
-
className: [
|
|
398
|
-
"w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2",
|
|
399
|
-
index === selectedIndex ? "bg-muted" : ""
|
|
400
|
-
].filter(Boolean).join(" "),
|
|
385
|
+
size: "xs",
|
|
386
|
+
"aria-label": resolvedClearLabel,
|
|
387
|
+
className: "absolute right-1 top-1/2 -translate-y-1/2",
|
|
401
388
|
onMouseDown: (event) => event.preventDefault(),
|
|
402
|
-
onClick:
|
|
403
|
-
|
|
404
|
-
|
|
389
|
+
onClick: handleClear,
|
|
390
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
391
|
+
}
|
|
392
|
+
) : null
|
|
393
|
+
] }) }),
|
|
394
|
+
listboxVisible ? /* @__PURE__ */ jsx(
|
|
395
|
+
PopoverContent,
|
|
396
|
+
{
|
|
397
|
+
role: "presentation",
|
|
398
|
+
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
399
|
+
onCloseAutoFocus: (event) => event.preventDefault(),
|
|
400
|
+
onWheel: (event) => event.stopPropagation(),
|
|
401
|
+
onTouchMove: (event) => event.stopPropagation(),
|
|
402
|
+
className: "w-[var(--radix-popover-trigger-width)] min-w-0 max-h-48 sm:max-h-60 overflow-auto overscroll-contain border-input p-2",
|
|
403
|
+
children: loading && touched ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", role: "status", children: loadingLabel }) : touched && !filteredSuggestions.length ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", role: "status", children: noMatchesLabel }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", className: "flex flex-col gap-1", children: filteredSuggestions.map((option, index) => /* @__PURE__ */ jsxs(
|
|
404
|
+
Button,
|
|
405
|
+
{
|
|
406
|
+
id: optionDomId(index),
|
|
407
|
+
type: "button",
|
|
408
|
+
variant: "ghost",
|
|
409
|
+
size: "sm",
|
|
410
|
+
role: "option",
|
|
411
|
+
"aria-selected": index === selectedIndex,
|
|
412
|
+
className: [
|
|
413
|
+
"w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2",
|
|
414
|
+
index === selectedIndex ? "bg-muted" : ""
|
|
415
|
+
].filter(Boolean).join(" "),
|
|
416
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
417
|
+
onClick: () => {
|
|
418
|
+
resetBlurCloseState();
|
|
419
|
+
selectValue(option.value);
|
|
420
|
+
},
|
|
421
|
+
onMouseEnter: () => setSelectedIndex(index),
|
|
422
|
+
children: [
|
|
423
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: option.label }),
|
|
424
|
+
option.description ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: option.description }) : null
|
|
425
|
+
]
|
|
405
426
|
},
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
option.value
|
|
413
|
-
)) })
|
|
414
|
-
}
|
|
415
|
-
)
|
|
416
|
-
] });
|
|
427
|
+
option.value
|
|
428
|
+
)) })
|
|
429
|
+
}
|
|
430
|
+
) : null
|
|
431
|
+
] })
|
|
432
|
+
);
|
|
417
433
|
}
|
|
418
434
|
export {
|
|
419
435
|
ComboboxInput
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/ComboboxInput.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const noMatchesLabel = t('ui.inputs.comboboxInput.noMatches', 'No matches found')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const listboxId = React.useId()\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n // Tracks whether the user actually typed into the field during the current focus\n // session. `touched` cannot serve this purpose because it is set by `onFocus`,\n // which `autoFocus` triggers before the user does anything at all.\n const userTypedRef = React.useRef(false)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with a value that changed outside the component. A focused field is\n // synced too, because `autoFocus` can focus the control before an async default value\n // arrives and a focus-only guard then leaves the control rendering an empty label for\n // a value the form has already committed. Two conditions still block the sync:\n // - the user is typing, so their query is never clobbered mid-keystroke;\n // - `optionMap` only holds the self-mapping placeholder it synthesises for an\n // uncovered value, which would paint the raw record id over a label the user just\n // picked (`asyncOptions` is replaced on every load, so any follow-up load that\n // misses the picked entry \u2014 a failed request, a debounce race, a composite label\n // the route's `?search=` cannot match \u2014 drops it back to the placeholder).\n React.useEffect(() => {\n const option = optionMap.get(value)\n const hasRealLabel = Boolean(option && option.label !== option.value)\n if (document.activeElement === inputRef.current && (userTypedRef.current || !hasRealLabel)) return\n setInput(option?.label ?? value ?? '')\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n userTypedRef.current = false\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n if (!showSuggestions) return\n event.preventDefault()\n event.stopPropagation()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const optionDomId = React.useCallback(\n (index: number) => `${listboxId}-option-${index}`,\n [listboxId],\n )\n\n React.useEffect(() => {\n if (selectedIndex < 0 || !showSuggestions) return\n const activeElement = typeof document !== 'undefined'\n ? document.getElementById(optionDomId(selectedIndex))\n : null\n if (typeof activeElement?.scrollIntoView === 'function') {\n activeElement.scrollIntoView({ block: 'nearest' })\n }\n }, [optionDomId, selectedIndex, showSuggestions])\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n const listboxVisible = showSuggestions\n && !disabled\n && (loading || filteredSuggestions.length > 0 || (touched && input.trim().length > 0))\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={listboxVisible}\n aria-controls={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}\n aria-autocomplete=\"list\"\n aria-activedescendant={listboxVisible && selectedIndex >= 0 ? optionDomId(selectedIndex) : undefined}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n userTypedRef.current = true\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n userTypedRef.current = false\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {listboxVisible && (\n <div\n className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\"\n >\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\" role=\"status\">{loadingLabel}</div>\n ) : touched && !filteredSuggestions.length ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\" role=\"status\">{noMatchesLabel}</div>\n ) : (\n <div id={listboxId} role=\"listbox\" className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n id={optionDomId(index)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"option\"\n aria-selected={index === selectedIndex}\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Popover, PopoverAnchor, PopoverContent } from '../../primitives/popover'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const noMatchesLabel = t('ui.inputs.comboboxInput.noMatches', 'No matches found')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const listboxId = React.useId()\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n // Tracks whether the user actually typed into the field during the current focus\n // session. `touched` cannot serve this purpose because it is set by `onFocus`,\n // which `autoFocus` triggers before the user does anything at all.\n const userTypedRef = React.useRef(false)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with a value that changed outside the component. A focused field is\n // synced too, because `autoFocus` can focus the control before an async default value\n // arrives and a focus-only guard then leaves the control rendering an empty label for\n // a value the form has already committed. Two conditions still block the sync:\n // - the user is typing, so their query is never clobbered mid-keystroke;\n // - `optionMap` only holds the self-mapping placeholder it synthesises for an\n // uncovered value, which would paint the raw record id over a label the user just\n // picked (`asyncOptions` is replaced on every load, so any follow-up load that\n // misses the picked entry \u2014 a failed request, a debounce race, a composite label\n // the route's `?search=` cannot match \u2014 drops it back to the placeholder).\n React.useEffect(() => {\n const option = optionMap.get(value)\n const hasRealLabel = Boolean(option && option.label !== option.value)\n if (document.activeElement === inputRef.current && (userTypedRef.current || !hasRealLabel)) return\n setInput(option?.label ?? value ?? '')\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n userTypedRef.current = false\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n if (!showSuggestions) return\n event.preventDefault()\n event.stopPropagation()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const optionDomId = React.useCallback(\n (index: number) => `${listboxId}-option-${index}`,\n [listboxId],\n )\n\n React.useEffect(() => {\n if (selectedIndex < 0 || !showSuggestions) return\n const activeElement = typeof document !== 'undefined'\n ? document.getElementById(optionDomId(selectedIndex))\n : null\n if (typeof activeElement?.scrollIntoView === 'function') {\n activeElement.scrollIntoView({ block: 'nearest' })\n }\n }, [optionDomId, selectedIndex, showSuggestions])\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n const listboxVisible = showSuggestions\n && !disabled\n && (loading || filteredSuggestions.length > 0 || (touched && input.trim().length > 0))\n\n return (\n // The suggestion list goes through the DS Popover so it is portaled out of any\n // scrolling ancestor. Rendered in place it was clipped by a Dialog's\n // `overflow-y-auto`, where z-index cannot help. `open` stays fully controlled\n // (no `onOpenChange`) so the blur timer, Escape and selection keep owning\n // dismissal exactly as before.\n <Popover open={listboxVisible}>\n <PopoverAnchor asChild>\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={listboxVisible}\n aria-controls={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}\n // The listbox is portaled out of the input's subtree, so `aria-owns` is what\n // makes it a logical descendant and keeps `aria-activedescendant` below valid.\n aria-owns={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}\n aria-autocomplete=\"list\"\n aria-activedescendant={listboxVisible && selectedIndex >= 0 ? optionDomId(selectedIndex) : undefined}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n userTypedRef.current = true\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n userTypedRef.current = false\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n </div>\n </PopoverAnchor>\n\n {/* Unmount the content outright rather than leaning on Radix's exit animation:\n a closing-but-still-mounted layer keeps swallowing Escape, which would\n strand the popup's host (see AdvancedFilterPanel for that failure mode). */}\n {listboxVisible ? (\n <PopoverContent\n // Radix hardcodes `role=\"dialog\"` on popover content. This popup is a\n // positioning shell around the listbox below, and a second dialog node\n // would both misdescribe it and break `getByRole('dialog')` for anything\n // that queries while suggestions happen to be open.\n role=\"presentation\"\n // Focus must stay in the input: the blur-close timer and\n // aria-activedescendant model depend on it, and Radix would otherwise\n // move focus into the list on open and back to the trigger on close.\n onOpenAutoFocus={(event) => event.preventDefault()}\n onCloseAutoFocus={(event) => event.preventDefault()}\n // A modal Dialog locks scrolling through `react-remove-scroll`, which\n // cancels document-level `wheel` and `touchmove` outside its own content\n // node. The portaled list is not part of that exemption, so without these\n // a long list inside a dialog cannot be scrolled by wheel or by touch.\n // `overscroll-contain` stops the scroll chaining to the page at the ends.\n onWheel={(event) => event.stopPropagation()}\n onTouchMove={(event) => event.stopPropagation()}\n className=\"w-[var(--radix-popover-trigger-width)] min-w-0 max-h-48 sm:max-h-60 overflow-auto overscroll-contain border-input p-2\"\n >\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\" role=\"status\">{loadingLabel}</div>\n ) : touched && !filteredSuggestions.length ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\" role=\"status\">{noMatchesLabel}</div>\n ) : (\n <div id={listboxId} role=\"listbox\" className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n id={optionDomId(index)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"option\"\n aria-selected={index === selectedIndex}\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </PopoverContent>\n ) : null}\n </Popover>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsaQ,SAKE,KALF;AApaR,YAAY,WAAW;AACvB,SAAS,SAAS;AAClB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,SAAS,eAAe,sBAAsB;AA8BvD,SAAS,iBAAiB,OAA0D;AAClF,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,SAAO,MACJ,IAAI,CAAC,WAAW;AACf,QAAI,OAAO,WAAW,UAAU;AAC9B,YAAM,UAAU,OAAO,KAAK;AAC5B,UAAI,CAAC,QAAS,QAAO;AACrB,aAAO,EAAE,OAAO,SAAS,OAAO,QAAQ;AAAA,IAC1C;AACA,UAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,OAAO,MAAM,KAAK,IAAI;AACvE,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO,OAAO,KAAK,KAAK;AAAA,MAC/B,aAAa,OAAO,eAAe;AAAA,IACrC;AAAA,EACF,CAAC,EACA,OAAO,CAAC,WAAqC,CAAC,CAAC,MAAM;AAC1D;AAEA,SAAS,gBAAgB,GAAqB,GAA8B;AAC1E,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,EAAE,MAAM,CAAC,QAAQ,UAAU;AAChC,UAAM,OAAO,EAAE,KAAK;AACpB,WAAO,OAAO,UAAU,KAAK,SACxB,OAAO,UAAU,KAAK,UACrB,OAAO,eAAe,WAAW,KAAK,eAAe;AAAA,EAC7D,CAAC;AACH;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ;AACF,GAAuB;AACrB,QAAM,IAAI,KAAK;AACf,QAAM,sBAAsB,eAAe,EAAE,uCAAuC,mBAAmB;AACvG,QAAM,eAAe,EAAE,mCAAmC,2BAAsB;AAChF,QAAM,iBAAiB,EAAE,qCAAqC,kBAAkB;AAChF,QAAM,qBAAqB,cAAc,EAAE,iCAAiC,aAAa;AACzF,QAAM,mBAAmB;AACzB,QAAM,sBAAsB;AAC5B,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAA2B,CAAC,CAAC;AAC3E,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAA2B,CAAC,CAAC;AACjF,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,KAAK;AAClE,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,EAAE;AAC3D,QAAM,YAAY,MAAM,MAAM;AAC9B,QAAM,WAAW,MAAM,OAAyB,IAAI;AACpD,QAAM,aAAa,MAAM,OAAO,KAAK;AACrC,QAAM,oBAAoB,MAAM,OAAsB,IAAI;AAC1D,QAAM,sBAAsB,MAAM,OAAO,KAAK;AAC9C,QAAM,yBAAyB,MAAM,OAAO,QAAQ,aAAa,CAAC,QAAQ,CAAC;AAC3E,QAAM,8BAA8B,MAAM,OAAsB,IAAI;AAIpE,QAAM,eAAe,MAAM,OAAO,KAAK;AAEvC,QAAM,gBAAgB,MAAM;AAAA,IAC1B,MAAM,iBAAiB,CAAC,GAAI,eAAe,CAAC,GAAI,GAAI,eAAe,CAAC,CAAE,CAAC;AAAA,IACvE,CAAC,aAAa,WAAW;AAAA,EAC3B;AAMA,QAAM,EAAE,kBAAkB,oBAAoB,IAAI,MAAM,QAAQ,MAAM;AACpE,UAAM,QAAQ,oBAAI,IAAY;AAC9B,UAAM,UAAU,oBAAI,IAAY;AAChC,eAAW,OAAO,CAAC,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG;AACzE,cAAQ,IAAI,IAAI,KAAK;AACrB,UAAI,IAAI,SAAS,IAAI,UAAU,IAAI,MAAO,OAAM,IAAI,IAAI,KAAK;AAAA,IAC/D;AACA,WAAO,EAAE,kBAAkB,OAAO,qBAAqB,QAAQ;AAAA,EACjE,GAAG,CAAC,eAAe,cAAc,eAAe,CAAC;AAEjD,QAAM,YAAY,MAAM,QAAQ,MAAM;AACpC,UAAM,MAAM,oBAAI,IAA4B;AAC5C,UAAM,WAAW,CAAC,WAA2B;AAC3C,YAAM,WAAW,IAAI,IAAI,OAAO,KAAK;AAErC,UAAI,CAAC,YAAa,SAAS,UAAU,SAAS,SAAS,OAAO,UAAU,OAAO,OAAQ;AACrF,YAAI,IAAI,OAAO,OAAO,MAAM;AAAA,MAC9B;AAAA,IACF;AACA,kBAAc,QAAQ,QAAQ;AAC9B,iBAAa,QAAQ,QAAQ;AAC7B,oBAAgB,QAAQ,QAAQ;AAChC,QAAI,OAAO;AACT,YAAM,WAAW,IAAI,IAAI,KAAK;AAC9B,UAAI,CAAC,UAAU;AACb,YAAI,IAAI,OAAO;AAAA,UACb;AAAA,UACA,OAAO;AAAA,UACP,aAAa,qBAAqB,KAAK,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,cAAc,iBAAiB,oBAAoB,eAAe,KAAK,CAAC;AAE5E,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,WAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,EACtC,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,UAAU,MAAM;AACpB,eAAW,UAAU;AAAA,EACvB,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,sBAAsB,MAAM,YAAY,MAAM;AAClD,QAAI,kBAAkB,YAAY,KAAM;AACxC,WAAO,aAAa,kBAAkB,OAAO;AAC7C,sBAAkB,UAAU;AAAA,EAC9B,GAAG,CAAC,CAAC;AAEL,QAAM,sBAAsB,MAAM,YAAY,MAAM;AAClD,wBAAoB,UAAU;AAC9B,wBAAoB;AAAA,EACtB,GAAG,CAAC,mBAAmB,CAAC;AAExB,QAAM,UAAU,MAAM,qBAAqB,CAAC,mBAAmB,CAAC;AAEhE,QAAM,sBAAsB,MAAM,QAAQ,MAAM;AAC9C,UAAM,QAAQ,MAAM,YAAY,EAAE,KAAK;AACvC,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO,iBAAiB,OAAO,CAAC,WAAW;AACzC,YAAM,aAAa,OAAO,MAAM,YAAY,EAAE,SAAS,KAAK;AAC5D,YAAM,YAAY,OAAO,aAAa,YAAY,EAAE,SAAS,KAAK;AAClE,aAAO,cAAc,QAAQ,SAAS;AAAA,IACxC,CAAC;AAAA,EACH,GAAG,CAAC,kBAAkB,KAAK,CAAC;AAE5B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,mBAAmB,CAAC,WAAW,SAAU;AAC9C,UAAM,QAAQ,MAAM,KAAK;AACzB,QAAI,YAAY;AAChB,UAAM,SAAS,OAAO,WAAW,MAAM;AACrC,iBAAW,IAAI;AACf,cAAQ,QAAQ,EACb,KAAK,MAAM,gBAAgB,KAAK,CAAC,EACjC,KAAK,CAAC,UAAU;AACf,YAAI,CAAC,WAAW;AACd,gBAAM,aAAa,iBAAiB,KAAK;AACzC,0BAAgB,CAAC,SAAS,gBAAgB,MAAM,UAAU,IAAI,OAAO,UAAU;AAAA,QACjF;AAAA,MACF,CAAC,EACA,MAAM,MAAM;AAAA,MAAC,CAAC,EACd,QAAQ,MAAM;AACb,YAAI,CAAC,UAAW,YAAW,KAAK;AAAA,MAClC,CAAC;AAAA,IACL,GAAG,GAAG;AACN,WAAO,MAAM;AACX,kBAAY;AACZ,aAAO,aAAa,MAAM;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,UAAU,OAAO,iBAAiB,OAAO,CAAC;AAI9C,QAAM,oBAAoB,OAAO,iBAAiB,aAAa,eAAe;AAC9E,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,SAAS,SAAU;AACxB,QAAI,YAAY;AAChB,UAAM,QAAQ,CAAC,OAAuB,gBAAgC;AACpE,YAAM,QAAQ,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACzD,UAAI,aAAa,CAAC,SAAS,UAAU,MAAO;AAC5C,yBAAmB,CAAC,SAAS;AAC3B,YAAI,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,SAAS,OAAO,UAAU,KAAK,EAAG,QAAO;AACpF,eAAO,CAAC,GAAG,KAAK,OAAO,CAAC,WAAW,OAAO,UAAU,KAAK,GAAG,EAAE,OAAO,OAAO,OAAO,aAAa,eAAe,KAAK,CAAC;AAAA,MACvH,CAAC;AAAA,IACH;AACA,QAAI,mBAAmB;AACrB,UAAI,iBAAiB,IAAI,KAAK,EAAG;AACjC,cAAQ,QAAQ,EACb,KAAK,MAAM,kBAAkB,KAAK,CAAC,EACnC,KAAK,CAAC,UAAU,MAAM,OAAO,qBAAqB,KAAK,CAAC,CAAC,EACzD,MAAM,MAAM;AAAA,MAAC,CAAC;AACjB,aAAO,MAAM;AAAE,oBAAY;AAAA,MAAK;AAAA,IAClC;AACA,QAAI,oBAAoB,IAAI,KAAK,EAAG;AACpC,QAAI,4BAA4B,YAAY,MAAO;AACnD,gCAA4B,UAAU;AAKtC,QAAI,iBAAiB;AACnB,iBAAW,IAAI;AACf,cAAQ,QAAQ,EACb,KAAK,MAAM,gBAAgB,CAAC,EAC5B,KAAK,CAAC,UAAU;AACf,YAAI,UAAW;AACf,cAAM,aAAa,iBAAiB,KAAK;AACzC,wBAAgB,CAAC,SAAS,gBAAgB,MAAM,UAAU,IAAI,OAAO,UAAU;AAAA,MACjF,CAAC,EACA,MAAM,MAAM;AAAA,MAAC,CAAC,EACd,QAAQ,MAAM;AAAE,YAAI,CAAC,UAAW,YAAW,KAAK;AAAA,MAAE,CAAC;AAAA,IACxD;AACA,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAGlC,GAAG,CAAC,OAAO,UAAU,kBAAkB,qBAAqB,mBAAmB,eAAe,CAAC;AAY/F,QAAM,UAAU,MAAM;AACpB,UAAM,SAAS,UAAU,IAAI,KAAK;AAClC,UAAM,eAAe,QAAQ,UAAU,OAAO,UAAU,OAAO,KAAK;AACpE,QAAI,SAAS,kBAAkB,SAAS,YAAY,aAAa,WAAW,CAAC,cAAe;AAC5F,aAAS,QAAQ,SAAS,SAAS,EAAE;AAAA,EACvC,GAAG,CAAC,OAAO,SAAS,CAAC;AAErB,QAAM,cAAc,MAAM;AAAA,IACxB,CAAC,cAAsB;AACrB,UAAI,SAAU;AACd,0BAAoB;AACpB,YAAM,UAAU,UAAU,KAAK;AAC/B,eAAS,OAAO;AAChB,YAAM,SAAS,UAAU,IAAI,OAAO;AACpC,eAAS,QAAQ,SAAS,OAAO;AACjC,yBAAmB,KAAK;AACxB,uBAAiB,EAAE;AACnB,mBAAa,UAAU;AAAA,IACzB;AAAA,IACA,CAAC,UAAU,UAAU,WAAW,mBAAmB;AAAA,EACrD;AAEA,QAAM,qBAAqB,MAAM;AAAA,IAC/B,CAAC,QAAuC;AACtC,YAAM,QAAQ,IAAI,KAAK,EAAE,YAAY;AACrC,UAAI,CAAC,MAAO,QAAO;AACnB,iBAAW,UAAU,UAAU,OAAO,GAAG;AACvC,YAAI,OAAO,UAAU,IAAI,KAAK,EAAG,QAAO;AACxC,YAAI,OAAO,MAAM,YAAY,MAAM,MAAO,QAAO;AAAA,MACnD;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,mBAAmB,MAAM;AAAA,IAC7B,CAAC,QAAgB;AACf,UAAI,SAAU;AACd,UAAI,aAAa,IAAI,KAAK,MAAM,IAAI;AAClC,oBAAY,EAAE;AACd;AAAA,MACF;AACA,YAAM,SAAS,mBAAmB,GAAG;AACrC,UAAI,QAAQ;AACV,oBAAY,OAAO,KAAK;AACxB;AAAA,MACF;AACA,UAAI,CAAC,mBAAmB;AAItB,2BAAmB,KAAK;AACxB,cAAM,gBAAgB,UAAU,IAAI,KAAK;AACzC,YAAI,iBAAiB,cAAc,UAAU,cAAc,OAAO;AAChE,mBAAS,cAAc,KAAK;AAAA,QAC9B,WAAW,CAAC,OAAO;AACjB,mBAAS,EAAE;AAAA,QACb;AACA;AAAA,MACF;AACA,kBAAY,GAAG;AAAA,IACjB;AAAA,IACA,CAAC,mBAAmB,WAAW,UAAU,oBAAoB,WAAW,aAAa,KAAK;AAAA,EAC5F;AAEA,QAAM,cAAc,MAAM,YAAY,MAAM;AAC1C,QAAI,SAAU;AACd,gBAAY,EAAE;AACd,aAAS,SAAS,MAAM;AAAA,EAC1B,GAAG,CAAC,UAAU,WAAW,CAAC;AAE1B,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,sBAAkB,UAAU;AAC5B,QAAI,SAAU;AACd,wBAAoB,UAAU;AAC9B,qBAAiB,KAAK;AACtB,uBAAmB,KAAK;AACxB,qBAAiB,EAAE;AAAA,EACrB,GAAG,CAAC,kBAAkB,UAAU,KAAK,CAAC;AAEtC,QAAM,mBAAmB,MAAM,YAAY,MAAM;AAC/C,sBAAkB,UAAU;AAC5B,QAAI,SAAU;AACd,QAAI,WAAW,SAAS;AACtB,wBAAkB,UAAU,OAAO,WAAW,gBAAgB,mBAAmB;AACjF;AAAA,IACF;AACA,mBAAe;AAAA,EACjB,GAAG,CAAC,qBAAqB,gBAAgB,QAAQ,CAAC;AAElD,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,oBAAoB,QAAS;AAClC,QAAI,QAAS;AACb,wBAAoB;AACpB,mBAAe;AAAA,EACjB,GAAG,CAAC,qBAAqB,gBAAgB,OAAO,CAAC;AAEjD,QAAM,gBAAgB,MAAM;AAAA,IAC1B,CAAC,UAAiD;AAChD,UAAI,SAAU;AAEd,UAAI,MAAM,QAAQ,aAAa;AAC7B,cAAM,eAAe;AACrB,YAAI,CAAC,iBAAiB;AACpB,6BAAmB,IAAI;AACvB,2BAAiB,CAAC;AAAA,QACpB,OAAO;AACL,2BAAiB,CAAC,SAAS,KAAK,IAAI,OAAO,GAAG,oBAAoB,SAAS,CAAC,CAAC;AAAA,QAC/E;AAAA,MACF,WAAW,MAAM,QAAQ,WAAW;AAClC,cAAM,eAAe;AACrB,yBAAiB,CAAC,SAAS,KAAK,IAAI,OAAO,GAAG,EAAE,CAAC;AAAA,MACnD,WAAW,MAAM,QAAQ,SAAS;AAChC,cAAM,eAAe;AACrB,YAAI,iBAAiB,KAAK,oBAAoB,aAAa,GAAG;AAC5D,sBAAY,oBAAoB,aAAa,EAAE,KAAK;AAAA,QACtD,OAAO;AACL,2BAAiB,KAAK;AAAA,QACxB;AAAA,MACF,WAAW,MAAM,QAAQ,UAAU;AACjC,YAAI,CAAC,gBAAiB;AACtB,cAAM,eAAe;AACrB,cAAM,gBAAgB;AACtB,2BAAmB,KAAK;AACxB,yBAAiB,EAAE;AAAA,MACrB;AAAA,IACF;AAAA,IACA,CAAC,kBAAkB,UAAU,qBAAqB,OAAO,aAAa,eAAe,eAAe;AAAA,EACtG;AAEA,QAAM,cAAc,MAAM;AAAA,IACxB,CAAC,UAAkB,GAAG,SAAS,WAAW,KAAK;AAAA,IAC/C,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,gBAAgB,KAAK,CAAC,gBAAiB;AAC3C,UAAM,gBAAgB,OAAO,aAAa,cACtC,SAAS,eAAe,YAAY,aAAa,CAAC,IAClD;AACJ,QAAI,OAAO,eAAe,mBAAmB,YAAY;AACvD,oBAAc,eAAe,EAAE,OAAO,UAAU,CAAC;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,aAAa,eAAe,eAAe,CAAC;AAEhD,QAAM,kBAAkB,aAAa,CAAC,aAAa,UAAU,MAAM,UAAU;AAC7E,QAAM,iBAAiB,mBAClB,CAAC,aACA,WAAW,oBAAoB,SAAS,KAAM,WAAW,MAAM,KAAK,EAAE,SAAS;AAErF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAME,qBAAC,WAAQ,MAAM,gBACb;AAAA,0BAAC,iBAAc,SAAO,MACpB,+BAAC,SAAI,WAAU,mBAKb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,WAAW;AAAA,cACT;AAAA,cACA,kBAAkB,SAAS;AAAA,YAC7B,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,YACX,OAAO;AAAA,YACP,aAAa;AAAA,YACb;AAAA,YACA,0BAAuB;AAAA,YACvB;AAAA,YACA,MAAK;AAAA,YACL,iBAAe;AAAA,YACf,iBAAe,kBAAkB,CAAC,WAAW,oBAAoB,SAAS,IAAI,YAAY;AAAA,YAG1F,aAAW,kBAAkB,CAAC,WAAW,oBAAoB,SAAS,IAAI,YAAY;AAAA,YACtF,qBAAkB;AAAA,YAClB,yBAAuB,kBAAkB,iBAAiB,IAAI,YAAY,aAAa,IAAI;AAAA,YAC3F,SAAS,MAAM;AACb,yBAAW,IAAI;AACf,kBAAI,uBAAuB,SAAS;AAClC,uCAAuB,UAAU;AACjC;AAAA,cACF;AACA,kCAAoB;AACpB,kBAAI,mBAAmB,iBAAiB,WAAW,GAAG;AACpD,2BAAW,IAAI;AAAA,cACjB;AACA,iCAAmB,IAAI;AAAA,YACzB;AAAA,YACA,UAAU,CAAC,UAAU;AACnB,yBAAW,IAAI;AACf,2BAAa,UAAU;AACvB,uBAAS,MAAM,OAAO,KAAK;AAC3B,iCAAmB,IAAI;AACvB,+BAAiB,EAAE;AAAA,YACrB;AAAA,YACA,WAAW;AAAA,YACX,QAAQ,MAAM;AAIZ,2BAAa,UAAU;AACvB,kCAAoB,UAAU;AAC9B,kCAAoB;AACpB,kBAAI,WAAW,SAAS;AACtB,kCAAkB,UAAU,OAAO,WAAW,gBAAgB,mBAAmB;AACjF;AAAA,cACF;AACA,gCAAkB,UAAU,OAAO,WAAW,kBAAkB,gBAAgB;AAAA,YAClF;AAAA;AAAA,QACF;AAAA,QAEC,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,WAAU;AAAA,YACV,aAAa,CAAC,UAAU,MAAM,eAAe;AAAA,YAC7C,SAAS;AAAA,YAET,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,QACxB,IACE;AAAA,SACN,GACF;AAAA,MAKC,iBACC;AAAA,QAAC;AAAA;AAAA,UAKC,MAAK;AAAA,UAIL,iBAAiB,CAAC,UAAU,MAAM,eAAe;AAAA,UACjD,kBAAkB,CAAC,UAAU,MAAM,eAAe;AAAA,UAMlD,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,UAC1C,aAAa,CAAC,UAAU,MAAM,gBAAgB;AAAA,UAC9C,WAAU;AAAA,UAET,qBAAW,UACV,oBAAC,SAAI,WAAU,6CAA4C,MAAK,UAAU,wBAAa,IACrF,WAAW,CAAC,oBAAoB,SAClC,oBAAC,SAAI,WAAU,6CAA4C,MAAK,UAAU,0BAAe,IAEzF,oBAAC,SAAI,IAAI,WAAW,MAAK,WAAU,WAAU,uBAC1C,8BAAoB,IAAI,CAAC,QAAQ,UAChC;AAAA,YAAC;AAAA;AAAA,cAEC,IAAI,YAAY,KAAK;AAAA,cACrB,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,MAAK;AAAA,cACL,iBAAe,UAAU;AAAA,cACzB,WAAW;AAAA,gBACT;AAAA,gBACA,UAAU,gBAAgB,aAAa;AAAA,cACzC,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,cACX,aAAa,CAAC,UAAU,MAAM,eAAe;AAAA,cAC7C,SAAS,MAAM;AACb,oCAAoB;AACpB,4BAAY,OAAO,KAAK;AAAA,cAC1B;AAAA,cACA,cAAc,MAAM,iBAAiB,KAAK;AAAA,cAE1C;AAAA,oCAAC,UAAK,WAAU,+BAA+B,iBAAO,OAAM;AAAA,gBAC3D,OAAO,cACN,oBAAC,UAAK,WAAU,iCAAiC,iBAAO,aAAY,IAClE;AAAA;AAAA;AAAA,YAvBC,OAAO;AAAA,UAwBd,CACD,GACH;AAAA;AAAA,MAEJ,IACE;AAAA,OACN;AAAA;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7130.1.fef2396fd8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.7.1-develop.7130.1.fef2396fd8",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.7.1-develop.7130.1.fef2396fd8",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
* Global AI assistant launcher.
|
|
5
5
|
*
|
|
6
6
|
* Reusable component that:
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* - Renders nothing unless the `ai_assistant` module is enabled AND the
|
|
8
|
+
* caller holds `ai_assistant.view` (`useAiAssistantAvailable`). Both are
|
|
9
|
+
* resolved from data the backend shell already holds, so an installation
|
|
10
|
+
* without the module never reaches the endpoints below.
|
|
11
|
+
* - Past that gate, hides itself when `/api/ai_assistant/ai/agents` returns
|
|
12
|
+
* zero accessible agents for the caller — that endpoint is the only
|
|
13
|
+
* authoritative visibility signal. `/api/ai_assistant/health` is advisory:
|
|
14
|
+
* a non-OK response or a network error is deliberately treated as
|
|
15
|
+
* "probably healthy" so a flaky endpoint cannot hide the launcher, and
|
|
16
|
+
* only an explicit `{ healthy: false }` body vetoes rendering.
|
|
17
|
+
* - Stays visible with a setup prompt when agents exist but no provider key
|
|
18
|
+
* is configured (`aiConfigured: false`), rather than vanishing.
|
|
10
19
|
* - Exposes a compact icon trigger styled for the topbar.
|
|
11
20
|
* - Opens a Cmd-K-style searchable dialog listing every typed agent the
|
|
12
21
|
* caller is allowed to launch — searchable by label, description, or id —
|
|
@@ -49,6 +58,7 @@ import { IconButton } from '../primitives/icon-button'
|
|
|
49
58
|
import { Kbd, KbdShortcut } from '../primitives/kbd'
|
|
50
59
|
import { useAiDock } from './AiDock'
|
|
51
60
|
import { useAiChatSessions } from './AiChatSessions'
|
|
61
|
+
import { useAiAssistantAvailable } from './useAiAssistantAvailable'
|
|
52
62
|
import { ChatPaneTabs } from './ChatPaneTabs'
|
|
53
63
|
import { ConversationShareButton } from './ConversationShareButton'
|
|
54
64
|
import { AiIcon } from './AiIcon'
|
|
@@ -91,8 +101,9 @@ export interface AiAssistantLauncherProps {
|
|
|
91
101
|
agentsEndpoint?: string
|
|
92
102
|
/**
|
|
93
103
|
* Optional override of the health endpoint. Defaults to
|
|
94
|
-
* `/api/ai_assistant/health`.
|
|
95
|
-
*
|
|
104
|
+
* `/api/ai_assistant/health`. Advisory only: non-2xx responses and network
|
|
105
|
+
* errors count as "probably healthy" so a flaky endpoint cannot hide the
|
|
106
|
+
* launcher. Only an explicit `{ healthy: false }` body hides it.
|
|
96
107
|
*/
|
|
97
108
|
healthEndpoint?: string
|
|
98
109
|
/**
|
|
@@ -193,7 +204,16 @@ function isTextEntryTarget(target: EventTarget | null): boolean {
|
|
|
193
204
|
return false
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
export function AiAssistantLauncher({
|
|
207
|
+
export function AiAssistantLauncher(props: AiAssistantLauncherProps) {
|
|
208
|
+
// The AI routes only exist when the `ai_assistant` module is enabled, and
|
|
209
|
+
// they are gated on `ai_assistant.view`. Deciding this before mounting the
|
|
210
|
+
// content keeps the health and agents effects from firing doomed reads.
|
|
211
|
+
const aiAvailable = useAiAssistantAvailable()
|
|
212
|
+
if (!aiAvailable) return null
|
|
213
|
+
return <AiAssistantLauncherContent {...props} />
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function AiAssistantLauncherContent({
|
|
197
217
|
variant: _variant = 'topbar',
|
|
198
218
|
agentsEndpoint = DEFAULT_AGENTS_ENDPOINT,
|
|
199
219
|
healthEndpoint = DEFAULT_HEALTH_ENDPOINT,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
updateAiServerConversation,
|
|
33
33
|
type AiServerConversation,
|
|
34
34
|
} from './conversation-store'
|
|
35
|
+
import { useAiAssistantAvailable } from './useAiAssistantAvailable'
|
|
35
36
|
|
|
36
37
|
const logger = createLogger('ui').child({ component: 'AiChatSessions' })
|
|
37
38
|
|
|
@@ -214,6 +215,7 @@ function mergeServerConversations(
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
export function AiChatSessionsProvider({ children }: { children: React.ReactNode }) {
|
|
218
|
+
const aiAvailable = useAiAssistantAvailable()
|
|
217
219
|
// Hydrate synchronously via a lazy initializer. The previous "empty
|
|
218
220
|
// state + post-mount load effect" pattern had a window where the
|
|
219
221
|
// persistence effect ran with the empty closure value (because the
|
|
@@ -254,7 +256,13 @@ export function AiChatSessionsProvider({ children }: { children: React.ReactNode
|
|
|
254
256
|
writePersisted(storageKey, state)
|
|
255
257
|
}, [storageKey, state])
|
|
256
258
|
|
|
259
|
+
// The provider wraps the whole backend shell, so it also mounts on
|
|
260
|
+
// installations without the `ai_assistant` module and for users without
|
|
261
|
+
// `ai_assistant.view`. Syncing there only produces a 404 / 403 and a warning
|
|
262
|
+
// on every page load, so skip it — `aiAvailable` is in the dependency list
|
|
263
|
+
// so the sync still runs once the backend chrome payload arrives.
|
|
257
264
|
React.useEffect(() => {
|
|
265
|
+
if (!aiAvailable) return
|
|
258
266
|
let cancelled = false
|
|
259
267
|
listAiServerConversations({ limit: 100 })
|
|
260
268
|
.then((conversations) => {
|
|
@@ -272,7 +280,7 @@ export function AiChatSessionsProvider({ children }: { children: React.ReactNode
|
|
|
272
280
|
return () => {
|
|
273
281
|
cancelled = true
|
|
274
282
|
}
|
|
275
|
-
}, [storageKey])
|
|
283
|
+
}, [aiAvailable, storageKey])
|
|
276
284
|
|
|
277
285
|
const update = React.useCallback(
|
|
278
286
|
(mutator: (prev: AiChatSessionsState) => AiChatSessionsState) => {
|