@open-mercato/search 0.6.6-develop.6396.1.fe832b428c → 0.6.6-develop.6398.1.013efe913e

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.
@@ -1,2 +1,2 @@
1
- [build:search] found 93 entry points
1
+ [build:search] found 94 entry points
2
2
  [build:search] built successfully
@@ -0,0 +1,51 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
3
+ test.describe("TC-SEARCH-012: global search results escape the sticky header stacking context", () => {
4
+ test("the results panel is portaled to <body> and stays above high z-index page content", async ({ page }) => {
5
+ await login(page, "admin");
6
+ await page.goto("/backend");
7
+ await page.waitForLoadState("domcontentloaded");
8
+ await page.locator("header").getByRole("button", { name: "Open global search" }).click();
9
+ const input = page.locator('input[aria-controls="topbar-search-results"]');
10
+ await expect(input).toBeVisible();
11
+ await input.fill("au");
12
+ const panel = page.locator("#topbar-search-results");
13
+ await expect(panel).toBeVisible();
14
+ const structure = await panel.evaluate((el) => ({
15
+ parentIsBody: el.parentElement === document.body,
16
+ insideHeader: Boolean(el.closest("header")),
17
+ position: getComputedStyle(el).position
18
+ }));
19
+ expect(structure.parentIsBody, "results panel should be portaled directly under <body>").toBe(true);
20
+ expect(structure.insideHeader, "results panel must not live inside the sticky header").toBe(false);
21
+ expect(structure.position, "portaled panel is positioned relative to the viewport").toBe("fixed");
22
+ const stacking = await panel.evaluate((el) => {
23
+ const rect = el.getBoundingClientRect();
24
+ const x = Math.round(rect.left + rect.width / 2);
25
+ const y = Math.round(rect.top + rect.height / 2);
26
+ const probe = document.createElement("div");
27
+ probe.id = "__zorder_probe__";
28
+ Object.assign(probe.style, {
29
+ position: "fixed",
30
+ top: `${y - 25}px`,
31
+ left: `${x - 25}px`,
32
+ width: "50px",
33
+ height: "50px",
34
+ zIndex: "30",
35
+ background: "rgba(255,0,0,0.5)"
36
+ });
37
+ document.body.appendChild(probe);
38
+ const stack = document.elementsFromPoint(x, y);
39
+ const panelIdx = stack.findIndex((node) => node === el || el.contains(node));
40
+ const probeIdx = stack.findIndex((node) => node.id === "__zorder_probe__");
41
+ probe.remove();
42
+ return { panelIdx, probeIdx };
43
+ });
44
+ expect(stacking.panelIdx, "the results panel should be hit at its own centre").toBeGreaterThanOrEqual(0);
45
+ expect(
46
+ stacking.probeIdx === -1 || stacking.panelIdx < stacking.probeIdx,
47
+ "a z-index:30 page element must not paint over the results panel"
48
+ ).toBe(true);
49
+ });
50
+ });
51
+ //# sourceMappingURL=TC-SEARCH-012.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/search/__integration__/TC-SEARCH-012.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'\n\n/**\n * TC-SEARCH-012: the global (top-bar) search results panel escapes the sticky\n * header's stacking context so page content can never paint over it (#3097).\n *\n * The backend top bar is `position: sticky; z-index: 10`, which establishes a\n * stacking context. Rendered inline, the results panel's `z-index` (z-popover)\n * only ranked *within* that context, so relative to the page it was capped at the\n * header's z-index \u2014 any page element with its own stacking context above 10\n * (sticky table cells, cards, positioned widgets) bled over the results. The fix\n * portals the panel to <body> with fixed positioning.\n *\n * Self-contained: needs no indexed data. A below-minimum query (< 3 chars) opens\n * the panel in its \"type more\" hint state without issuing a search request, which\n * is enough to assert both structure and stacking. A synthetic z-index:30 overlay\n * proves the panel is painted on top \u2014 on the pre-fix inline markup that overlay\n * painted over the trapped panel instead.\n */\ntest.describe('TC-SEARCH-012: global search results escape the sticky header stacking context', () => {\n test('the results panel is portaled to <body> and stays above high z-index page content', async ({ page }) => {\n await login(page, 'admin')\n await page.goto('/backend')\n await page.waitForLoadState('domcontentloaded')\n\n // Open the collapsed top-bar search (icon button) and focus its input.\n await page.locator('header').getByRole('button', { name: 'Open global search' }).click()\n const input = page.locator('input[aria-controls=\"topbar-search-results\"]')\n await expect(input).toBeVisible()\n\n // A 2-char query (below the 3-char minimum) opens the panel in its hint state\n // \u2014 no search API call, so the assertion never depends on indexed records.\n await input.fill('au')\n const panel = page.locator('#topbar-search-results')\n await expect(panel).toBeVisible()\n\n // -- Structural guard: portaled to <body>, not nested inside the sticky header.\n const structure = await panel.evaluate((el) => ({\n parentIsBody: el.parentElement === document.body,\n insideHeader: Boolean(el.closest('header')),\n position: getComputedStyle(el).position,\n }))\n expect(structure.parentIsBody, 'results panel should be portaled directly under <body>').toBe(true)\n expect(structure.insideHeader, 'results panel must not live inside the sticky header').toBe(false)\n expect(structure.position, 'portaled panel is positioned relative to the viewport').toBe('fixed')\n\n // -- Stacking guard: page content at z-index 30 overlapping the panel stays\n // BELOW it. Pre-fix, the panel was trapped at the header's z-index (10) and\n // this overlay painted over it.\n const stacking = await panel.evaluate((el) => {\n const rect = el.getBoundingClientRect()\n const x = Math.round(rect.left + rect.width / 2)\n const y = Math.round(rect.top + rect.height / 2)\n const probe = document.createElement('div')\n probe.id = '__zorder_probe__'\n Object.assign(probe.style, {\n position: 'fixed',\n top: `${y - 25}px`,\n left: `${x - 25}px`,\n width: '50px',\n height: '50px',\n zIndex: '30',\n background: 'rgba(255,0,0,0.5)',\n })\n document.body.appendChild(probe)\n const stack = document.elementsFromPoint(x, y)\n const panelIdx = stack.findIndex((node) => node === el || el.contains(node))\n const probeIdx = stack.findIndex((node) => (node as HTMLElement).id === '__zorder_probe__')\n probe.remove()\n return { panelIdx, probeIdx }\n })\n // The panel must be present at the point and painted ABOVE the z-index:30\n // overlay. Asserting the relative order (rather than \"topmost\") keeps the\n // guard precise if some unrelated top-layer element ever overlaps the centre.\n expect(stacking.panelIdx, 'the results panel should be hit at its own centre').toBeGreaterThanOrEqual(0)\n expect(\n stacking.probeIdx === -1 || stacking.panelIdx < stacking.probeIdx,\n 'a z-index:30 page element must not paint over the results panel',\n ).toBe(true)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,aAAa;AAmBtB,KAAK,SAAS,kFAAkF,MAAM;AACpG,OAAK,qFAAqF,OAAO,EAAE,KAAK,MAAM;AAC5G,UAAM,MAAM,MAAM,OAAO;AACzB,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,KAAK,iBAAiB,kBAAkB;AAG9C,UAAM,KAAK,QAAQ,QAAQ,EAAE,UAAU,UAAU,EAAE,MAAM,qBAAqB,CAAC,EAAE,MAAM;AACvF,UAAM,QAAQ,KAAK,QAAQ,8CAA8C;AACzE,UAAM,OAAO,KAAK,EAAE,YAAY;AAIhC,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,QAAQ,KAAK,QAAQ,wBAAwB;AACnD,UAAM,OAAO,KAAK,EAAE,YAAY;AAGhC,UAAM,YAAY,MAAM,MAAM,SAAS,CAAC,QAAQ;AAAA,MAC9C,cAAc,GAAG,kBAAkB,SAAS;AAAA,MAC5C,cAAc,QAAQ,GAAG,QAAQ,QAAQ,CAAC;AAAA,MAC1C,UAAU,iBAAiB,EAAE,EAAE;AAAA,IACjC,EAAE;AACF,WAAO,UAAU,cAAc,wDAAwD,EAAE,KAAK,IAAI;AAClG,WAAO,UAAU,cAAc,sDAAsD,EAAE,KAAK,KAAK;AACjG,WAAO,UAAU,UAAU,uDAAuD,EAAE,KAAK,OAAO;AAKhG,UAAM,WAAW,MAAM,MAAM,SAAS,CAAC,OAAO;AAC5C,YAAM,OAAO,GAAG,sBAAsB;AACtC,YAAM,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,CAAC;AAC/C,YAAM,IAAI,KAAK,MAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AAC/C,YAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,YAAM,KAAK;AACX,aAAO,OAAO,MAAM,OAAO;AAAA,QACzB,UAAU;AAAA,QACV,KAAK,GAAG,IAAI,EAAE;AAAA,QACd,MAAM,GAAG,IAAI,EAAE;AAAA,QACf,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,YAAY;AAAA,MACd,CAAC;AACD,eAAS,KAAK,YAAY,KAAK;AAC/B,YAAM,QAAQ,SAAS,kBAAkB,GAAG,CAAC;AAC7C,YAAM,WAAW,MAAM,UAAU,CAAC,SAAS,SAAS,MAAM,GAAG,SAAS,IAAI,CAAC;AAC3E,YAAM,WAAW,MAAM,UAAU,CAAC,SAAU,KAAqB,OAAO,kBAAkB;AAC1F,YAAM,OAAO;AACb,aAAO,EAAE,UAAU,SAAS;AAAA,IAC9B,CAAC;AAID,WAAO,SAAS,UAAU,mDAAmD,EAAE,uBAAuB,CAAC;AACvG;AAAA,MACE,SAAS,aAAa,MAAM,SAAS,WAAW,SAAS;AAAA,MACzD;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,EACb,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import NextLink from "next/link";
5
6
  import { useRouter } from "next/navigation";
6
7
  import {
@@ -141,6 +142,7 @@ function TopbarSearchInline({
141
142
  const [expanded, setExpanded] = React.useState(false);
142
143
  const [selectedIndex, setSelectedIndex] = React.useState(0);
143
144
  const [showScopeHint, setShowScopeHint] = React.useState(() => hasActiveOrganizationSelection());
145
+ const [anchor, setAnchor] = React.useState(null);
144
146
  const inputRef = React.useRef(null);
145
147
  const popoverRef = React.useRef(null);
146
148
  const containerRef = React.useRef(null);
@@ -290,6 +292,24 @@ function TopbarSearchInline({
290
292
  const tooShort = trimmed.length > 0 && trimmed.length < MIN_QUERY_LENGTH;
291
293
  const showPopover = open && (loading || results.length > 0 || error !== null || tooShort || showVectorWarning);
292
294
  const showClear = query.length > 0;
295
+ React.useEffect(() => {
296
+ if (!showPopover) return;
297
+ const update = () => {
298
+ const el = containerRef.current;
299
+ if (!el) return;
300
+ const rect = el.getBoundingClientRect();
301
+ const width = Math.max(rect.width, 320);
302
+ const left = Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8));
303
+ setAnchor({ top: rect.bottom + 4, left, width });
304
+ };
305
+ update();
306
+ window.addEventListener("resize", update);
307
+ window.addEventListener("scroll", update, true);
308
+ return () => {
309
+ window.removeEventListener("resize", update);
310
+ window.removeEventListener("scroll", update, true);
311
+ };
312
+ }, [showPopover]);
293
313
  if (!expanded) {
294
314
  return /* @__PURE__ */ jsx(
295
315
  "span",
@@ -371,77 +391,81 @@ function TopbarSearchInline({
371
391
  ]
372
392
  }
373
393
  ),
374
- showPopover ? /* @__PURE__ */ jsxs(
375
- "div",
376
- {
377
- ref: popoverRef,
378
- id: "topbar-search-results",
379
- role: "listbox",
380
- className: "absolute left-0 right-0 top-[calc(100%+4px)] z-popover min-w-[320px] rounded-md border bg-popover text-popover-foreground shadow-md",
381
- children: [
382
- error ? /* @__PURE__ */ jsx("p", { className: "border-b px-3 py-2 text-sm text-destructive", children: error }) : null,
383
- showVectorWarning && !error ? /* @__PURE__ */ jsxs("div", { className: "border-b bg-status-warning-bg px-3 py-2 text-xs text-status-warning-text", children: [
384
- /* @__PURE__ */ jsx("p", { children: missingConfigMessage }),
385
- /* @__PURE__ */ jsx(
386
- NextLink,
387
- {
388
- href: "/backend/config/search",
389
- onClick: () => collapseAndReset(),
390
- className: "mt-1 inline-flex items-center font-medium underline underline-offset-2 hover:no-underline",
391
- children: t("search.dialog.warnings.configureLink", "Configure search settings")
392
- }
393
- )
394
- ] }) : null,
395
- showScopeHint ? /* @__PURE__ */ jsx("p", { className: "border-b px-3 py-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground", children: t("search.scopeHint.currentOrg", "Scoped to current organization") }) : null,
396
- tooShort && !error ? /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: t("search.dialog.empty.hint", { count: MIN_QUERY_LENGTH }) }) : null,
397
- !tooShort && results.length === 0 && !loading && !error ? /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: t("search.dialog.empty.none", "No results") }) : null,
398
- results.length > 0 ? /* @__PURE__ */ jsx("div", { ref: listRef, className: "max-h-[380px] overflow-y-auto p-1", children: results.map((result, index) => {
399
- const presenter = result.presenter;
400
- const isActive = index === selectedIndex;
401
- const hasLink = pickPrimaryLink(result) !== null;
402
- const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null;
403
- return /* @__PURE__ */ jsxs(
404
- "button",
405
- {
406
- type: "button",
407
- "data-active": isActive,
408
- onClick: () => openResult(result),
409
- onMouseEnter: () => setSelectedIndex(index),
410
- role: "option",
411
- "aria-selected": isActive,
412
- className: cn(
413
- "flex w-full items-start gap-3 rounded-sm px-2 py-2 text-left transition-colors",
414
- isActive ? "bg-muted" : "hover:bg-muted/60",
415
- !hasLink && "opacity-60"
416
- ),
417
- children: [
418
- Icon ? /* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background", children: /* @__PURE__ */ jsx(Icon, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }) }) : /* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background", children: /* @__PURE__ */ jsx(FileText, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }) }),
419
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
420
- /* @__PURE__ */ jsx("span", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
421
- "span",
422
- {
423
- className: cn(
424
- "truncate text-sm font-medium",
425
- !hasLink ? "text-muted-foreground" : "text-foreground"
426
- ),
427
- children: presenter?.title ?? result.recordId
428
- }
429
- ) }),
430
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-overline text-muted-foreground", children: [
431
- /* @__PURE__ */ jsx("span", { className: "truncate", children: formatEntityId(result.entityId) }),
432
- presenter?.subtitle ? /* @__PURE__ */ jsxs(Fragment, { children: [
433
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
434
- /* @__PURE__ */ jsx("span", { className: "truncate", children: presenter.subtitle })
435
- ] }) : null
394
+ showPopover && anchor ? createPortal(
395
+ /* @__PURE__ */ jsxs(
396
+ "div",
397
+ {
398
+ ref: popoverRef,
399
+ id: "topbar-search-results",
400
+ role: "listbox",
401
+ style: { position: "fixed", top: anchor.top, left: anchor.left, width: anchor.width },
402
+ className: "z-popover rounded-md border bg-popover text-popover-foreground shadow-md",
403
+ children: [
404
+ error ? /* @__PURE__ */ jsx("p", { className: "border-b px-3 py-2 text-sm text-destructive", children: error }) : null,
405
+ showVectorWarning && !error ? /* @__PURE__ */ jsxs("div", { className: "border-b bg-status-warning-bg px-3 py-2 text-xs text-status-warning-text", children: [
406
+ /* @__PURE__ */ jsx("p", { children: missingConfigMessage }),
407
+ /* @__PURE__ */ jsx(
408
+ NextLink,
409
+ {
410
+ href: "/backend/config/search",
411
+ onClick: () => collapseAndReset(),
412
+ className: "mt-1 inline-flex items-center font-medium underline underline-offset-2 hover:no-underline",
413
+ children: t("search.dialog.warnings.configureLink", "Configure search settings")
414
+ }
415
+ )
416
+ ] }) : null,
417
+ showScopeHint ? /* @__PURE__ */ jsx("p", { className: "border-b px-3 py-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground", children: t("search.scopeHint.currentOrg", "Scoped to current organization") }) : null,
418
+ tooShort && !error ? /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: t("search.dialog.empty.hint", { count: MIN_QUERY_LENGTH }) }) : null,
419
+ !tooShort && results.length === 0 && !loading && !error ? /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: t("search.dialog.empty.none", "No results") }) : null,
420
+ results.length > 0 ? /* @__PURE__ */ jsx("div", { ref: listRef, className: "max-h-[380px] overflow-y-auto p-1", children: results.map((result, index) => {
421
+ const presenter = result.presenter;
422
+ const isActive = index === selectedIndex;
423
+ const hasLink = pickPrimaryLink(result) !== null;
424
+ const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null;
425
+ return /* @__PURE__ */ jsxs(
426
+ "button",
427
+ {
428
+ type: "button",
429
+ "data-active": isActive,
430
+ onClick: () => openResult(result),
431
+ onMouseEnter: () => setSelectedIndex(index),
432
+ role: "option",
433
+ "aria-selected": isActive,
434
+ className: cn(
435
+ "flex w-full items-start gap-3 rounded-sm px-2 py-2 text-left transition-colors",
436
+ isActive ? "bg-muted" : "hover:bg-muted/60",
437
+ !hasLink && "opacity-60"
438
+ ),
439
+ children: [
440
+ Icon ? /* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background", children: /* @__PURE__ */ jsx(Icon, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }) }) : /* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background", children: /* @__PURE__ */ jsx(FileText, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }) }),
441
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
442
+ /* @__PURE__ */ jsx("span", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
443
+ "span",
444
+ {
445
+ className: cn(
446
+ "truncate text-sm font-medium",
447
+ !hasLink ? "text-muted-foreground" : "text-foreground"
448
+ ),
449
+ children: presenter?.title ?? result.recordId
450
+ }
451
+ ) }),
452
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-overline text-muted-foreground", children: [
453
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: formatEntityId(result.entityId) }),
454
+ presenter?.subtitle ? /* @__PURE__ */ jsxs(Fragment, { children: [
455
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
456
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: presenter.subtitle })
457
+ ] }) : null
458
+ ] })
436
459
  ] })
437
- ] })
438
- ]
439
- },
440
- `${result.entityId}:${result.recordId}`
441
- );
442
- }) }) : null
443
- ]
444
- }
460
+ ]
461
+ },
462
+ `${result.entityId}:${result.recordId}`
463
+ );
464
+ }) }) : null
465
+ ]
466
+ }
467
+ ),
468
+ document.body
445
469
  ) : null
446
470
  ]
447
471
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/search/frontend/components/TopbarSearchInline.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport NextLink from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport {\n Search,\n Loader2,\n Zap,\n User,\n Users,\n Building,\n StickyNote,\n Briefcase,\n CheckSquare,\n FileText,\n Mail,\n Phone,\n Calendar,\n Clock,\n Star,\n Tag,\n Flag,\n Heart,\n Bookmark,\n Package,\n Truck,\n ShoppingCart,\n CreditCard,\n DollarSign,\n Target,\n Award,\n Trophy,\n Rocket,\n Lightbulb,\n MessageSquare,\n Bell,\n Settings,\n Globe,\n MapPin,\n Link as LinkIcon,\n Folder,\n Database,\n Activity,\n X,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { SearchResult, SearchResultLink } from '@open-mercato/shared/modules/search'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { ForbiddenError } from '@open-mercato/ui/backend/utils/api'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { resolveSearchMinTokenLength } from '@open-mercato/shared/lib/search/config'\nimport { fetchGlobalSearchResults } from '../utils'\n\nconst MIN_QUERY_LENGTH = resolveSearchMinTokenLength()\n\nfunction normalizeLinks(links?: SearchResultLink[] | null): SearchResultLink[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string')\n}\n\nfunction pickPrimaryLink(result: SearchResult): string | null {\n if (result.url) return result.url\n const links = normalizeLinks(result.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue)\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nconst ICON_MAP: Record<string, LucideIcon> = {\n bolt: Zap,\n zap: Zap,\n user: User,\n users: Users,\n building: Building,\n 'sticky-note': StickyNote,\n briefcase: Briefcase,\n 'check-square': CheckSquare,\n 'file-text': FileText,\n mail: Mail,\n phone: Phone,\n calendar: Calendar,\n clock: Clock,\n star: Star,\n tag: Tag,\n flag: Flag,\n heart: Heart,\n bookmark: Bookmark,\n package: Package,\n truck: Truck,\n 'shopping-cart': ShoppingCart,\n 'credit-card': CreditCard,\n 'dollar-sign': DollarSign,\n target: Target,\n award: Award,\n trophy: Trophy,\n rocket: Rocket,\n lightbulb: Lightbulb,\n 'message-square': MessageSquare,\n bell: Bell,\n settings: Settings,\n globe: Globe,\n 'map-pin': MapPin,\n link: LinkIcon,\n folder: Folder,\n database: Database,\n activity: Activity,\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n return ICON_MAP[name.toLowerCase()] ?? null\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n return `${humanizeSegment(module)} \u00B7 ${humanizeSegment(entity)}`\n}\n\nexport type TopbarSearchInlineProps = {\n /** Whether embedding provider is configured for vector search */\n embeddingConfigured: boolean\n /** Warning text to show when vector search is enabled but not configured */\n missingConfigMessage: string\n}\n\nexport function TopbarSearchInline({\n embeddingConfigured,\n missingConfigMessage,\n}: TopbarSearchInlineProps) {\n const router = useRouter()\n const t = useT()\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<SearchResult[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [open, setOpen] = React.useState(false)\n const [expanded, setExpanded] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(0)\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n const popoverRef = React.useRef<HTMLDivElement | null>(null)\n const containerRef = React.useRef<HTMLElement | null>(null)\n const listRef = React.useRef<HTMLDivElement | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n\n const expandAndFocus = React.useCallback(() => {\n setExpanded(true)\n // Wait one tick so the input is mounted/transitioned before focusing\n requestAnimationFrame(() => {\n inputRef.current?.focus()\n inputRef.current?.select()\n })\n }, [])\n\n const collapseAndReset = React.useCallback(() => {\n setOpen(false)\n setExpanded(false)\n setQuery('')\n inputRef.current?.blur()\n }, [])\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n // Cmd/Ctrl+K expands + focuses the input\n React.useEffect(() => {\n const handler = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n expandAndFocus()\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n }, [expandAndFocus])\n\n // Click outside closes the popover AND collapses input (unless user has typed something)\n React.useEffect(() => {\n if (!expanded) return\n const handler = (event: MouseEvent) => {\n const target = event.target as Node | null\n if (!target) return\n if (containerRef.current?.contains(target)) return\n if (popoverRef.current?.contains(target)) return\n collapseAndReset()\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [expanded, collapseAndReset])\n\n // Fetch results on query change (debounced)\n React.useEffect(() => {\n abortRef.current?.abort()\n const trimmed = query.trim()\n if (trimmed.length < MIN_QUERY_LENGTH) {\n setResults([])\n setError(null)\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n const handle = setTimeout(async () => {\n try {\n const data = await fetchGlobalSearchResults(query, {\n limit: 10,\n signal: controller.signal,\n })\n setResults(data.results)\n setError(data.error ?? null)\n setSelectedIndex(0)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n const abortError = err as { name?: string }\n if (abortError?.name === 'AbortError') return\n if (err instanceof ForbiddenError) {\n setError(t('search.dialog.errors.noPermission'))\n } else {\n setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))\n }\n setResults([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 220)\n\n return () => {\n clearTimeout(handle)\n controller.abort()\n }\n }, [query, t])\n\n // Auto-scroll active item into view\n React.useEffect(() => {\n const container = listRef.current\n const active = container?.querySelector<HTMLElement>('[data-active=\"true\"]')\n if (!container || !active) return\n const { top: containerTop, bottom: containerBottom } = container.getBoundingClientRect()\n const { top: activeTop, bottom: activeBottom } = active.getBoundingClientRect()\n if (activeTop < containerTop) {\n container.scrollTop -= containerTop - activeTop\n } else if (activeBottom > containerBottom) {\n container.scrollTop += activeBottom - containerBottom\n }\n }, [selectedIndex])\n\n const openResult = React.useCallback(\n (result: SearchResult | undefined) => {\n if (!result) return\n const href = pickPrimaryLink(result)\n if (!href) return\n router.push(href)\n collapseAndReset()\n },\n [router, collapseAndReset],\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!open) setOpen(true)\n setSelectedIndex((prev) => (results.length ? (prev + 1) % results.length : 0))\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => {\n if (!results.length) return 0\n return prev <= 0 ? results.length - 1 : prev - 1\n })\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n if (open) {\n setOpen(false)\n } else if (query) {\n setQuery('')\n } else {\n collapseAndReset()\n }\n return\n }\n if (event.key === 'Enter') {\n event.preventDefault()\n const target = results[selectedIndex]\n openResult(target)\n return\n }\n },\n [open, query, results, selectedIndex, openResult, collapseAndReset],\n )\n\n const showVectorWarning = !embeddingConfigured && !error\n const trimmed = query.trim()\n const tooShort = trimmed.length > 0 && trimmed.length < MIN_QUERY_LENGTH\n const showPopover =\n open && (loading || results.length > 0 || error !== null || tooShort || showVectorWarning)\n const showClear = query.length > 0\n\n if (!expanded) {\n return (\n <span\n ref={(el) => {\n containerRef.current = el\n }}\n className=\"inline-flex\"\n >\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"lg\"\n onClick={expandAndFocus}\n aria-label={t('search.dialog.actions.openGlobalSearch', 'Open global search')}\n title={t('search.dialog.actions.openGlobalSearch', 'Open global search')}\n >\n <Search className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n </span>\n )\n }\n\n return (\n <div\n ref={(el) => {\n containerRef.current = el\n }}\n data-search-expanded=\"true\"\n className=\"relative min-w-0 sm:w-[260px] md:w-[320px] max-sm:absolute max-sm:inset-x-3 max-sm:top-1/2 max-sm:-translate-y-1/2 max-sm:z-popover\"\n >\n <div\n className={cn(\n 'flex h-9 items-center gap-2 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors hover:bg-muted/40',\n open ? 'border-foreground bg-background shadow-focus' : '',\n )}\n onClick={() => inputRef.current?.focus()}\n >\n <Search className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <input\n ref={inputRef}\n type=\"text\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n if (!open) setOpen(true)\n }}\n onFocus={() => setOpen(true)}\n onKeyDown={handleKeyDown}\n placeholder={t('search.dialog.actions.search', 'Search')}\n aria-label={t('search.dialog.actions.openGlobalSearch', 'Search')}\n aria-autocomplete=\"list\"\n aria-expanded={open}\n aria-controls=\"topbar-search-results\"\n className=\"flex-1 min-w-0 bg-transparent placeholder:text-muted-foreground/70 focus:outline-none\"\n // Inline search; no need for native browser autocomplete or autocapitalize\n autoComplete=\"off\"\n spellCheck={false}\n />\n {loading ? (\n <Loader2 className=\"size-4 shrink-0 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n ) : showClear ? (\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation()\n setQuery('')\n inputRef.current?.focus()\n }}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n aria-label={t('search.dialog.actions.clear', 'Clear search')}\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n ) : (\n <kbd className=\"hidden md:inline-flex shrink-0 items-center rounded border bg-muted/50 px-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n \u2318K\n </kbd>\n )}\n </div>\n\n {showPopover ? (\n <div\n ref={popoverRef}\n id=\"topbar-search-results\"\n role=\"listbox\"\n className=\"absolute left-0 right-0 top-[calc(100%+4px)] z-popover min-w-[320px] rounded-md border bg-popover text-popover-foreground shadow-md\"\n >\n {error ? (\n <p className=\"border-b px-3 py-2 text-sm text-destructive\">{error}</p>\n ) : null}\n {showVectorWarning && !error ? (\n <div className=\"border-b bg-status-warning-bg px-3 py-2 text-xs text-status-warning-text\">\n <p>{missingConfigMessage}</p>\n <NextLink\n href=\"/backend/config/search\"\n onClick={() => collapseAndReset()}\n className=\"mt-1 inline-flex items-center font-medium underline underline-offset-2 hover:no-underline\"\n >\n {t('search.dialog.warnings.configureLink', 'Configure search settings')}\n </NextLink>\n </div>\n ) : null}\n {showScopeHint ? (\n <p className=\"border-b px-3 py-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </p>\n ) : null}\n\n {tooShort && !error ? (\n <div className=\"px-3 py-3 text-sm text-muted-foreground\">\n {t('search.dialog.empty.hint', { count: MIN_QUERY_LENGTH })}\n </div>\n ) : null}\n\n {!tooShort && results.length === 0 && !loading && !error ? (\n <div className=\"px-3 py-3 text-sm text-muted-foreground\">\n {t('search.dialog.empty.none', 'No results')}\n </div>\n ) : null}\n\n {results.length > 0 ? (\n <div ref={listRef} className=\"max-h-[380px] overflow-y-auto p-1\">\n {results.map((result, index) => {\n const presenter = result.presenter\n const isActive = index === selectedIndex\n const hasLink = pickPrimaryLink(result) !== null\n const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null\n return (\n <button\n key={`${result.entityId}:${result.recordId}`}\n type=\"button\"\n data-active={isActive}\n onClick={() => openResult(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n role=\"option\"\n aria-selected={isActive}\n className={cn(\n 'flex w-full items-start gap-3 rounded-sm px-2 py-2 text-left transition-colors',\n isActive ? 'bg-muted' : 'hover:bg-muted/60',\n !hasLink && 'opacity-60',\n )}\n >\n {Icon ? (\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background\">\n <Icon className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </span>\n ) : (\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background\">\n <FileText className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </span>\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"flex items-center gap-2\">\n <span\n className={cn(\n 'truncate text-sm font-medium',\n !hasLink ? 'text-muted-foreground' : 'text-foreground',\n )}\n >\n {presenter?.title ?? result.recordId}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-overline text-muted-foreground\">\n <span className=\"truncate\">{formatEntityId(result.entityId)}</span>\n {presenter?.subtitle ? (\n <>\n <span aria-hidden=\"true\">\u00B7</span>\n <span className=\"truncate\">{presenter.subtitle}</span>\n </>\n ) : null}\n </span>\n </span>\n </button>\n )\n })}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport default TopbarSearchInline\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport NextLink from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport {\n Search,\n Loader2,\n Zap,\n User,\n Users,\n Building,\n StickyNote,\n Briefcase,\n CheckSquare,\n FileText,\n Mail,\n Phone,\n Calendar,\n Clock,\n Star,\n Tag,\n Flag,\n Heart,\n Bookmark,\n Package,\n Truck,\n ShoppingCart,\n CreditCard,\n DollarSign,\n Target,\n Award,\n Trophy,\n Rocket,\n Lightbulb,\n MessageSquare,\n Bell,\n Settings,\n Globe,\n MapPin,\n Link as LinkIcon,\n Folder,\n Database,\n Activity,\n X,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { SearchResult, SearchResultLink } from '@open-mercato/shared/modules/search'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { ForbiddenError } from '@open-mercato/ui/backend/utils/api'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { resolveSearchMinTokenLength } from '@open-mercato/shared/lib/search/config'\nimport { fetchGlobalSearchResults } from '../utils'\n\nconst MIN_QUERY_LENGTH = resolveSearchMinTokenLength()\n\nfunction normalizeLinks(links?: SearchResultLink[] | null): SearchResultLink[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string')\n}\n\nfunction pickPrimaryLink(result: SearchResult): string | null {\n if (result.url) return result.url\n const links = normalizeLinks(result.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue)\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nconst ICON_MAP: Record<string, LucideIcon> = {\n bolt: Zap,\n zap: Zap,\n user: User,\n users: Users,\n building: Building,\n 'sticky-note': StickyNote,\n briefcase: Briefcase,\n 'check-square': CheckSquare,\n 'file-text': FileText,\n mail: Mail,\n phone: Phone,\n calendar: Calendar,\n clock: Clock,\n star: Star,\n tag: Tag,\n flag: Flag,\n heart: Heart,\n bookmark: Bookmark,\n package: Package,\n truck: Truck,\n 'shopping-cart': ShoppingCart,\n 'credit-card': CreditCard,\n 'dollar-sign': DollarSign,\n target: Target,\n award: Award,\n trophy: Trophy,\n rocket: Rocket,\n lightbulb: Lightbulb,\n 'message-square': MessageSquare,\n bell: Bell,\n settings: Settings,\n globe: Globe,\n 'map-pin': MapPin,\n link: LinkIcon,\n folder: Folder,\n database: Database,\n activity: Activity,\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n return ICON_MAP[name.toLowerCase()] ?? null\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n return `${humanizeSegment(module)} \u00B7 ${humanizeSegment(entity)}`\n}\n\nexport type TopbarSearchInlineProps = {\n /** Whether embedding provider is configured for vector search */\n embeddingConfigured: boolean\n /** Warning text to show when vector search is enabled but not configured */\n missingConfigMessage: string\n}\n\nexport function TopbarSearchInline({\n embeddingConfigured,\n missingConfigMessage,\n}: TopbarSearchInlineProps) {\n const router = useRouter()\n const t = useT()\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<SearchResult[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [open, setOpen] = React.useState(false)\n const [expanded, setExpanded] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(0)\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n const [anchor, setAnchor] = React.useState<{ top: number; left: number; width: number } | null>(null)\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n const popoverRef = React.useRef<HTMLDivElement | null>(null)\n const containerRef = React.useRef<HTMLElement | null>(null)\n const listRef = React.useRef<HTMLDivElement | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n\n const expandAndFocus = React.useCallback(() => {\n setExpanded(true)\n // Wait one tick so the input is mounted/transitioned before focusing\n requestAnimationFrame(() => {\n inputRef.current?.focus()\n inputRef.current?.select()\n })\n }, [])\n\n const collapseAndReset = React.useCallback(() => {\n setOpen(false)\n setExpanded(false)\n setQuery('')\n inputRef.current?.blur()\n }, [])\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n // Cmd/Ctrl+K expands + focuses the input\n React.useEffect(() => {\n const handler = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n expandAndFocus()\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n }, [expandAndFocus])\n\n // Click outside closes the popover AND collapses input (unless user has typed something)\n React.useEffect(() => {\n if (!expanded) return\n const handler = (event: MouseEvent) => {\n const target = event.target as Node | null\n if (!target) return\n if (containerRef.current?.contains(target)) return\n if (popoverRef.current?.contains(target)) return\n collapseAndReset()\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [expanded, collapseAndReset])\n\n // Fetch results on query change (debounced)\n React.useEffect(() => {\n abortRef.current?.abort()\n const trimmed = query.trim()\n if (trimmed.length < MIN_QUERY_LENGTH) {\n setResults([])\n setError(null)\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n const handle = setTimeout(async () => {\n try {\n const data = await fetchGlobalSearchResults(query, {\n limit: 10,\n signal: controller.signal,\n })\n setResults(data.results)\n setError(data.error ?? null)\n setSelectedIndex(0)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n const abortError = err as { name?: string }\n if (abortError?.name === 'AbortError') return\n if (err instanceof ForbiddenError) {\n setError(t('search.dialog.errors.noPermission'))\n } else {\n setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))\n }\n setResults([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 220)\n\n return () => {\n clearTimeout(handle)\n controller.abort()\n }\n }, [query, t])\n\n // Auto-scroll active item into view\n React.useEffect(() => {\n const container = listRef.current\n const active = container?.querySelector<HTMLElement>('[data-active=\"true\"]')\n if (!container || !active) return\n const { top: containerTop, bottom: containerBottom } = container.getBoundingClientRect()\n const { top: activeTop, bottom: activeBottom } = active.getBoundingClientRect()\n if (activeTop < containerTop) {\n container.scrollTop -= containerTop - activeTop\n } else if (activeBottom > containerBottom) {\n container.scrollTop += activeBottom - containerBottom\n }\n }, [selectedIndex])\n\n const openResult = React.useCallback(\n (result: SearchResult | undefined) => {\n if (!result) return\n const href = pickPrimaryLink(result)\n if (!href) return\n router.push(href)\n collapseAndReset()\n },\n [router, collapseAndReset],\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!open) setOpen(true)\n setSelectedIndex((prev) => (results.length ? (prev + 1) % results.length : 0))\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => {\n if (!results.length) return 0\n return prev <= 0 ? results.length - 1 : prev - 1\n })\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n if (open) {\n setOpen(false)\n } else if (query) {\n setQuery('')\n } else {\n collapseAndReset()\n }\n return\n }\n if (event.key === 'Enter') {\n event.preventDefault()\n const target = results[selectedIndex]\n openResult(target)\n return\n }\n },\n [open, query, results, selectedIndex, openResult, collapseAndReset],\n )\n\n const showVectorWarning = !embeddingConfigured && !error\n const trimmed = query.trim()\n const tooShort = trimmed.length > 0 && trimmed.length < MIN_QUERY_LENGTH\n const showPopover =\n open && (loading || results.length > 0 || error !== null || tooShort || showVectorWarning)\n const showClear = query.length > 0\n\n // The results panel is portaled to <body> so it escapes the sticky header's\n // stacking context (z-sticky) \u2014 rendered inline it stays trapped at the\n // header's z-index and page content with its own z-index paints over it\n // (#3097). Anchor it under the input and keep it aligned on scroll/resize.\n React.useEffect(() => {\n if (!showPopover) return\n const update = () => {\n const el = containerRef.current\n if (!el) return\n const rect = el.getBoundingClientRect()\n const width = Math.max(rect.width, 320)\n const left = Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8))\n setAnchor({ top: rect.bottom + 4, left, width })\n }\n update()\n window.addEventListener('resize', update)\n window.addEventListener('scroll', update, true)\n return () => {\n window.removeEventListener('resize', update)\n window.removeEventListener('scroll', update, true)\n }\n }, [showPopover])\n\n if (!expanded) {\n return (\n <span\n ref={(el) => {\n containerRef.current = el\n }}\n className=\"inline-flex\"\n >\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"lg\"\n onClick={expandAndFocus}\n aria-label={t('search.dialog.actions.openGlobalSearch', 'Open global search')}\n title={t('search.dialog.actions.openGlobalSearch', 'Open global search')}\n >\n <Search className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n </span>\n )\n }\n\n return (\n <div\n ref={(el) => {\n containerRef.current = el\n }}\n data-search-expanded=\"true\"\n className=\"relative min-w-0 sm:w-[260px] md:w-[320px] max-sm:absolute max-sm:inset-x-3 max-sm:top-1/2 max-sm:-translate-y-1/2 max-sm:z-popover\"\n >\n <div\n className={cn(\n 'flex h-9 items-center gap-2 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors hover:bg-muted/40',\n open ? 'border-foreground bg-background shadow-focus' : '',\n )}\n onClick={() => inputRef.current?.focus()}\n >\n <Search className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <input\n ref={inputRef}\n type=\"text\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n if (!open) setOpen(true)\n }}\n onFocus={() => setOpen(true)}\n onKeyDown={handleKeyDown}\n placeholder={t('search.dialog.actions.search', 'Search')}\n aria-label={t('search.dialog.actions.openGlobalSearch', 'Search')}\n aria-autocomplete=\"list\"\n aria-expanded={open}\n aria-controls=\"topbar-search-results\"\n className=\"flex-1 min-w-0 bg-transparent placeholder:text-muted-foreground/70 focus:outline-none\"\n // Inline search; no need for native browser autocomplete or autocapitalize\n autoComplete=\"off\"\n spellCheck={false}\n />\n {loading ? (\n <Loader2 className=\"size-4 shrink-0 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n ) : showClear ? (\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation()\n setQuery('')\n inputRef.current?.focus()\n }}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n aria-label={t('search.dialog.actions.clear', 'Clear search')}\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n ) : (\n <kbd className=\"hidden md:inline-flex shrink-0 items-center rounded border bg-muted/50 px-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n \u2318K\n </kbd>\n )}\n </div>\n\n {showPopover && anchor ? createPortal(\n <div\n ref={popoverRef}\n id=\"topbar-search-results\"\n role=\"listbox\"\n style={{ position: 'fixed', top: anchor.top, left: anchor.left, width: anchor.width }}\n className=\"z-popover rounded-md border bg-popover text-popover-foreground shadow-md\"\n >\n {error ? (\n <p className=\"border-b px-3 py-2 text-sm text-destructive\">{error}</p>\n ) : null}\n {showVectorWarning && !error ? (\n <div className=\"border-b bg-status-warning-bg px-3 py-2 text-xs text-status-warning-text\">\n <p>{missingConfigMessage}</p>\n <NextLink\n href=\"/backend/config/search\"\n onClick={() => collapseAndReset()}\n className=\"mt-1 inline-flex items-center font-medium underline underline-offset-2 hover:no-underline\"\n >\n {t('search.dialog.warnings.configureLink', 'Configure search settings')}\n </NextLink>\n </div>\n ) : null}\n {showScopeHint ? (\n <p className=\"border-b px-3 py-1.5 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </p>\n ) : null}\n\n {tooShort && !error ? (\n <div className=\"px-3 py-3 text-sm text-muted-foreground\">\n {t('search.dialog.empty.hint', { count: MIN_QUERY_LENGTH })}\n </div>\n ) : null}\n\n {!tooShort && results.length === 0 && !loading && !error ? (\n <div className=\"px-3 py-3 text-sm text-muted-foreground\">\n {t('search.dialog.empty.none', 'No results')}\n </div>\n ) : null}\n\n {results.length > 0 ? (\n <div ref={listRef} className=\"max-h-[380px] overflow-y-auto p-1\">\n {results.map((result, index) => {\n const presenter = result.presenter\n const isActive = index === selectedIndex\n const hasLink = pickPrimaryLink(result) !== null\n const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null\n return (\n <button\n key={`${result.entityId}:${result.recordId}`}\n type=\"button\"\n data-active={isActive}\n onClick={() => openResult(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n role=\"option\"\n aria-selected={isActive}\n className={cn(\n 'flex w-full items-start gap-3 rounded-sm px-2 py-2 text-left transition-colors',\n isActive ? 'bg-muted' : 'hover:bg-muted/60',\n !hasLink && 'opacity-60',\n )}\n >\n {Icon ? (\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background\">\n <Icon className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </span>\n ) : (\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md border bg-background\">\n <FileText className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </span>\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"flex items-center gap-2\">\n <span\n className={cn(\n 'truncate text-sm font-medium',\n !hasLink ? 'text-muted-foreground' : 'text-foreground',\n )}\n >\n {presenter?.title ?? result.recordId}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-overline text-muted-foreground\">\n <span className=\"truncate\">{formatEntityId(result.entityId)}</span>\n {presenter?.subtitle ? (\n <>\n <span aria-hidden=\"true\">\u00B7</span>\n <span className=\"truncate\">{presenter.subtitle}</span>\n </>\n ) : null}\n </span>\n </span>\n </button>\n )\n })}\n </div>\n ) : null}\n </div>,\n document.body,\n ) : null}\n </div>\n )\n}\n\nexport default TopbarSearchInline\n"],
5
+ "mappings": "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",
6
6
  "names": ["trimmed"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/search",
3
- "version": "0.6.6-develop.6396.1.fe832b428c",
3
+ "version": "0.6.6-develop.6398.1.013efe913e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -127,9 +127,9 @@
127
127
  "zod": "^4.4.3"
128
128
  },
129
129
  "peerDependencies": {
130
- "@open-mercato/core": "0.6.6-develop.6396.1.fe832b428c",
131
- "@open-mercato/queue": "0.6.6-develop.6396.1.fe832b428c",
132
- "@open-mercato/shared": "0.6.6-develop.6396.1.fe832b428c"
130
+ "@open-mercato/core": "0.6.6-develop.6398.1.013efe913e",
131
+ "@open-mercato/queue": "0.6.6-develop.6398.1.013efe913e",
132
+ "@open-mercato/shared": "0.6.6-develop.6398.1.013efe913e"
133
133
  },
134
134
  "devDependencies": {
135
135
  "@types/jest": "^30.0.0",
@@ -0,0 +1,82 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'
3
+
4
+ /**
5
+ * TC-SEARCH-012: the global (top-bar) search results panel escapes the sticky
6
+ * header's stacking context so page content can never paint over it (#3097).
7
+ *
8
+ * The backend top bar is `position: sticky; z-index: 10`, which establishes a
9
+ * stacking context. Rendered inline, the results panel's `z-index` (z-popover)
10
+ * only ranked *within* that context, so relative to the page it was capped at the
11
+ * header's z-index — any page element with its own stacking context above 10
12
+ * (sticky table cells, cards, positioned widgets) bled over the results. The fix
13
+ * portals the panel to <body> with fixed positioning.
14
+ *
15
+ * Self-contained: needs no indexed data. A below-minimum query (< 3 chars) opens
16
+ * the panel in its "type more" hint state without issuing a search request, which
17
+ * is enough to assert both structure and stacking. A synthetic z-index:30 overlay
18
+ * proves the panel is painted on top — on the pre-fix inline markup that overlay
19
+ * painted over the trapped panel instead.
20
+ */
21
+ test.describe('TC-SEARCH-012: global search results escape the sticky header stacking context', () => {
22
+ test('the results panel is portaled to <body> and stays above high z-index page content', async ({ page }) => {
23
+ await login(page, 'admin')
24
+ await page.goto('/backend')
25
+ await page.waitForLoadState('domcontentloaded')
26
+
27
+ // Open the collapsed top-bar search (icon button) and focus its input.
28
+ await page.locator('header').getByRole('button', { name: 'Open global search' }).click()
29
+ const input = page.locator('input[aria-controls="topbar-search-results"]')
30
+ await expect(input).toBeVisible()
31
+
32
+ // A 2-char query (below the 3-char minimum) opens the panel in its hint state
33
+ // — no search API call, so the assertion never depends on indexed records.
34
+ await input.fill('au')
35
+ const panel = page.locator('#topbar-search-results')
36
+ await expect(panel).toBeVisible()
37
+
38
+ // -- Structural guard: portaled to <body>, not nested inside the sticky header.
39
+ const structure = await panel.evaluate((el) => ({
40
+ parentIsBody: el.parentElement === document.body,
41
+ insideHeader: Boolean(el.closest('header')),
42
+ position: getComputedStyle(el).position,
43
+ }))
44
+ expect(structure.parentIsBody, 'results panel should be portaled directly under <body>').toBe(true)
45
+ expect(structure.insideHeader, 'results panel must not live inside the sticky header').toBe(false)
46
+ expect(structure.position, 'portaled panel is positioned relative to the viewport').toBe('fixed')
47
+
48
+ // -- Stacking guard: page content at z-index 30 overlapping the panel stays
49
+ // BELOW it. Pre-fix, the panel was trapped at the header's z-index (10) and
50
+ // this overlay painted over it.
51
+ const stacking = await panel.evaluate((el) => {
52
+ const rect = el.getBoundingClientRect()
53
+ const x = Math.round(rect.left + rect.width / 2)
54
+ const y = Math.round(rect.top + rect.height / 2)
55
+ const probe = document.createElement('div')
56
+ probe.id = '__zorder_probe__'
57
+ Object.assign(probe.style, {
58
+ position: 'fixed',
59
+ top: `${y - 25}px`,
60
+ left: `${x - 25}px`,
61
+ width: '50px',
62
+ height: '50px',
63
+ zIndex: '30',
64
+ background: 'rgba(255,0,0,0.5)',
65
+ })
66
+ document.body.appendChild(probe)
67
+ const stack = document.elementsFromPoint(x, y)
68
+ const panelIdx = stack.findIndex((node) => node === el || el.contains(node))
69
+ const probeIdx = stack.findIndex((node) => (node as HTMLElement).id === '__zorder_probe__')
70
+ probe.remove()
71
+ return { panelIdx, probeIdx }
72
+ })
73
+ // The panel must be present at the point and painted ABOVE the z-index:30
74
+ // overlay. Asserting the relative order (rather than "topmost") keeps the
75
+ // guard precise if some unrelated top-layer element ever overlaps the centre.
76
+ expect(stacking.panelIdx, 'the results panel should be hit at its own centre').toBeGreaterThanOrEqual(0)
77
+ expect(
78
+ stacking.probeIdx === -1 || stacking.panelIdx < stacking.probeIdx,
79
+ 'a z-index:30 page element must not paint over the results panel',
80
+ ).toBe(true)
81
+ })
82
+ })
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
+ import { createPortal } from 'react-dom'
4
5
  import NextLink from 'next/link'
5
6
  import { useRouter } from 'next/navigation'
6
7
  import {
@@ -163,6 +164,7 @@ export function TopbarSearchInline({
163
164
  const [expanded, setExpanded] = React.useState(false)
164
165
  const [selectedIndex, setSelectedIndex] = React.useState(0)
165
166
  const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())
167
+ const [anchor, setAnchor] = React.useState<{ top: number; left: number; width: number } | null>(null)
166
168
  const inputRef = React.useRef<HTMLInputElement | null>(null)
167
169
  const popoverRef = React.useRef<HTMLDivElement | null>(null)
168
170
  const containerRef = React.useRef<HTMLElement | null>(null)
@@ -332,6 +334,29 @@ export function TopbarSearchInline({
332
334
  open && (loading || results.length > 0 || error !== null || tooShort || showVectorWarning)
333
335
  const showClear = query.length > 0
334
336
 
337
+ // The results panel is portaled to <body> so it escapes the sticky header's
338
+ // stacking context (z-sticky) — rendered inline it stays trapped at the
339
+ // header's z-index and page content with its own z-index paints over it
340
+ // (#3097). Anchor it under the input and keep it aligned on scroll/resize.
341
+ React.useEffect(() => {
342
+ if (!showPopover) return
343
+ const update = () => {
344
+ const el = containerRef.current
345
+ if (!el) return
346
+ const rect = el.getBoundingClientRect()
347
+ const width = Math.max(rect.width, 320)
348
+ const left = Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8))
349
+ setAnchor({ top: rect.bottom + 4, left, width })
350
+ }
351
+ update()
352
+ window.addEventListener('resize', update)
353
+ window.addEventListener('scroll', update, true)
354
+ return () => {
355
+ window.removeEventListener('resize', update)
356
+ window.removeEventListener('scroll', update, true)
357
+ }
358
+ }, [showPopover])
359
+
335
360
  if (!expanded) {
336
361
  return (
337
362
  <span
@@ -412,12 +437,13 @@ export function TopbarSearchInline({
412
437
  )}
413
438
  </div>
414
439
 
415
- {showPopover ? (
440
+ {showPopover && anchor ? createPortal(
416
441
  <div
417
442
  ref={popoverRef}
418
443
  id="topbar-search-results"
419
444
  role="listbox"
420
- className="absolute left-0 right-0 top-[calc(100%+4px)] z-popover min-w-[320px] rounded-md border bg-popover text-popover-foreground shadow-md"
445
+ style={{ position: 'fixed', top: anchor.top, left: anchor.left, width: anchor.width }}
446
+ className="z-popover rounded-md border bg-popover text-popover-foreground shadow-md"
421
447
  >
422
448
  {error ? (
423
449
  <p className="border-b px-3 py-2 text-sm text-destructive">{error}</p>
@@ -509,7 +535,8 @@ export function TopbarSearchInline({
509
535
  })}
510
536
  </div>
511
537
  ) : null}
512
- </div>
538
+ </div>,
539
+ document.body,
513
540
  ) : null}
514
541
  </div>
515
542
  )