@igstack/app-catalog-frontend-core 5.0.2-alpha-20260923221955 → 5.0.2

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.
@@ -260,6 +260,9 @@ function SearchResultsList({
260
260
  e.preventDefault();
261
261
  setFocusedIndex((i) => Math.max(i - 1, 0));
262
262
  } else if (e.key === "Enter") {
263
+ if (e.defaultPrevented) return;
264
+ if (e.target instanceof HTMLElement && e.target.closest('button, a, [role="option"]'))
265
+ return;
263
266
  const row = focusedIndex >= 0 ? rows[focusedIndex] : void 0;
264
267
  if (row) activateRow(row);
265
268
  } else if (e.key === "Escape") {
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowUpRight, Search, X } from 'lucide-react'\nimport { use, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { pickNewThisWeek } from '../../utils/launcherHelpers'\nimport { AttributionFooter } from './AttributionFooter'\nimport { ResourceIcon } from './ResourceIcon'\nimport { searchResources } from '@igstack/app-catalog-shared-core'\nimport { Highlight } from '../components/Highlight'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport type { AreaIcon } from '~/types/uiSettings'\nimport { AppCatalogContext } from '../../context/AppCatalogContext'\nimport { areaLabel, groupByArea } from '../../utils/areaGrouping'\n\n/**\n * Adaptive-home discovery spine (issue #38, increment 1) — matches the\n * option-a-launcher prototype:\n * Your apps (frequent) → New this week → all areas, grouped\n *\n * This is the PERSISTENT shell for the catalog: idle browse, active search and\n * an open detail overlay all render inside it. Only the area below the hero\n * swaps (discovery spine ↔ results list), so the hero copy, the search box and\n * the page container never move while the user types. Do not switch the page to\n * a different container on search — that was the layout-jitter bug.\n */\n\n// Type pill text. Plain \"application\" resources are the common case and don't\n// need a badge (the user asked not to show a bare \"App\" badge); only surface a\n// pill for distinctive types (database, cloud, service, …).\nconst typeLabel = (t?: string): string | null => {\n if (!t || t === 'application') return null\n return t.charAt(0).toUpperCase() + t.slice(1)\n}\n\nexport interface AppCatalogGridProps {\n /** All root (non-child) resources, already filtered for deprecated per settings. */\n apps: Resource[]\n /**\n * All resources INCLUDING children/sub-resources. Passed to search so a query\n * matching a sub-resource (e.g. an AWS account, an alias/account ID) surfaces\n * its parent — preserving the existing cross-sub-resource search behavior.\n * Falls back to `apps` when omitted.\n */\n allResources?: Resource[]\n searchValue: string\n onSearchChange: (v: string) => void\n onAppClick: (app: Resource) => void\n /** Opens a matched sub-resource: its parent's detail, singled out to it. */\n onSubClick: (parentSlug: string, subSlug: string) => void\n onLaunch: (app: Resource) => void\n /**\n * @deprecated Unused — it labelled the removed \"Browse all\" header. Still\n * accepted so existing callers keep compiling; drop it on the next major.\n */\n totalCount?: number\n /**\n * True while an app/sub-resource detail overlay is open above the launcher.\n * The results list then stops binding ↑↓/↵/Esc so the overlay owns the\n * keyboard (otherwise Esc would ALSO wipe the search behind the overlay).\n */\n detailOpen?: boolean\n /** Currently open sub-resource slug, for highlighting its row. */\n selectedSubSlug?: string\n}\n\nfunction LaunchButton({\n app,\n onLaunch,\n className,\n}: {\n app: Resource\n onLaunch: (app: Resource) => void\n className?: string\n}) {\n if (!app.appUrl) return null\n // Show the destination URL on hover (user ask) — the native title tooltip\n // reveals where the ↗ jumps to, e.g. \"Open → console.aws.amazon.com\".\n const prettyUrl = displayUrl(app.appUrl)\n return (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={`Open → ${prettyUrl}`}\n aria-label={`Open ${app.displayName} in a new tab (${prettyUrl})`}\n onClick={(e) => {\n e.stopPropagation()\n onLaunch(app)\n }}\n className={cn(\n 'grid place-items-center rounded-full border border-border bg-card text-muted-foreground',\n 'hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors',\n className,\n )}\n >\n <ArrowUpRight className=\"size-3.5\" />\n </a>\n )\n}\n\nfunction SectionHead({\n title,\n count,\n action,\n}: {\n title: string\n count?: string\n action?: React.ReactNode\n}) {\n return (\n <div className=\"flex items-center gap-2.5 mb-4\">\n <h2 className=\"font-serif font-semibold text-[15px] tracking-tight m-0\">\n {title}\n </h2>\n {count && (\n <span className=\"text-[13px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5\">\n {count}\n </span>\n )}\n {action && <div className=\"ml-auto\">{action}</div>}\n </div>\n )\n}\n\n/** Small row used by \"New this week\" and the search results. */\nfunction ResourceRow({\n app,\n onAppClick,\n onLaunch,\n showNew = false,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n showNew?: boolean\n}) {\n return (\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border border-border rounded-[var(--radius)] px-3.5 py-3',\n 'hover:border-ring hover:shadow-sm transition-all',\n )}\n >\n <ResourceIcon app={app} size={38} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14.5px] font-semibold truncate\">\n {app.displayName}\n </span>\n {app.description && (\n <span className=\"block text-[13px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {showNew && (\n <span className=\"text-[11px] font-bold text-[#65a443] bg-[#65a443]/12 rounded-full px-2.5 py-0.5\">\n New\n </span>\n )}\n {typeLabel(app.type) && (\n <span className=\"text-[12px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap\">\n {typeLabel(app.type)}\n </span>\n )}\n <LaunchButton app={app} onLaunch={onLaunch} className=\"size-8\" />\n </button>\n )\n}\n\n/**\n * Wide area card: icon on the left, name and description beside it.\n *\n * The grid cell keeps a fixed footprint and the visible panel is absolute inside\n * it, so a card growing on hover OVERLAYS the row below instead of reflowing the\n * whole grid. Hover reveals the rest of the description and nothing else — an\n * earlier version staggered a delayed tag block in behind the growth, which read\n * as jitter. Keyboard focus expands the same way, and `motion-reduce` drops the\n * animation.\n *\n * The footprint is a fixed height rather than an aspect ratio: at two or three\n * columns a ratio makes the card grow taller as the window widens, and the\n * description — three lines at most — leaves the bottom half empty.\n */\nfunction AppCard({\n app,\n onAppClick,\n onLaunch,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <div className=\"relative h-[92px]\">\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group absolute inset-x-0 top-0 min-h-full max-h-full overflow-hidden',\n 'flex gap-3.5 text-left bg-card border border-border',\n 'rounded-[var(--radius)] p-[15px] pr-11 outline-none',\n 'transition-[max-height,box-shadow,border-color] duration-200',\n 'hover:max-h-96 hover:border-ring hover:shadow-lg hover:z-10',\n 'focus-visible:max-h-96 focus-visible:border-ring focus-visible:shadow-lg focus-visible:z-10',\n 'motion-reduce:transition-none',\n )}\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-3 right-3 size-[26px] opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100\"\n />\n <div className=\"shrink-0\">\n <ResourceIcon app={app} size={40} />\n </div>\n {/* min-w-0 so a long unbroken word truncates instead of pushing the\n column wider than the card. */}\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n {/* shrink-0: while max-height animates, the un-clamped description\n overflows the panel for a few frames. Without it flexbox squeezes\n this line to 0 and back, which reads as the title flickering. */}\n <span className=\"block shrink-0 text-[14.5px] font-bold leading-tight line-clamp-1 group-hover:line-clamp-2 group-focus-visible:line-clamp-2\">\n {app.displayName}\n </span>\n {app.description && (\n <span\n className={cn(\n 'block shrink-0 text-[12.5px] leading-snug text-muted-foreground',\n 'line-clamp-2 group-hover:line-clamp-none group-focus-visible:line-clamp-none',\n )}\n >\n {markdownToPlainText(app.description)}\n </span>\n )}\n </div>\n </button>\n </div>\n )\n}\n\n/**\n * One area: icon, big title, count, then its cards. The icon stands in for the\n * accent bar the header used to carry — it does the same colour job and also\n * says which area you are in. It is decorative: the `h3` carries the name.\n */\nfunction AreaSection({\n label,\n Icon,\n apps,\n onAppClick,\n onLaunch,\n}: {\n label: string\n Icon?: AreaIcon\n apps: Resource[]\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <section className=\"mt-11\">\n <div className=\"flex items-center gap-2.5 mb-4\">\n {Icon ? (\n <Icon\n size={26}\n strokeWidth={1.75}\n className=\"text-primary shrink-0\"\n />\n ) : (\n <div className=\"h-[3px] w-11 rounded-full bg-primary\" />\n )}\n <h3 className=\"font-serif font-semibold text-[27px] leading-tight tracking-tight m-0\">\n {label}\n </h3>\n <span className=\"text-[13px] text-muted-foreground\">\n {apps.length} {apps.length === 1 ? 'tool' : 'tools'}\n </span>\n </div>\n <div className=\"grid gap-3 grid-cols-1 md:grid-cols-2 xl:grid-cols-3\">\n {apps.map((app) => (\n <AppCard\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n </section>\n )\n}\n\n/** Matched sub-resources listed under a parent before the \"… N more\" row. */\nconst COLLAPSED_SUB_LIMIT = 5\n\n/** One line in the flat, keyboard-navigable results list. */\ntype ResultRow =\n | { kind: 'app'; key: string; app: Resource }\n | { kind: 'sub'; key: string; sub: Resource; parent: Resource }\n | { kind: 'more'; key: string; parent: Resource; hidden: number }\n\n/**\n * Keyboard-navigable search results list — the launcher's only search surface.\n *\n * It renders directly BELOW the hero search box, inside the same persistent\n * launcher shell: the hero copy, the search input and the page container never\n * move when the user types, and no filter chrome (tabs / category / deprecated\n * toggle) appears. Those filters live in the browse view only — clearing the\n * search brings them back.\n *\n * Rows are flat and individually clickable. A matched sub-resource gets its own\n * indented row under its parent, so a query that only hits a sub-resource\n * (e.g. a cloud account) can be opened directly instead of forcing the user to\n * dig through the parent's detail. ↑↓ moves over every row, ↵ activates the\n * focused one (the first row until something else is focused), Esc clears the\n * search.\n */\nfunction SearchResultsList({\n apps,\n searchValue,\n onAppClick,\n onSubClick,\n onLaunch,\n onClear,\n keyboardEnabled = true,\n selectedSubSlug,\n}: {\n apps: Resource[]\n searchValue: string\n onAppClick: (app: Resource) => void\n onSubClick: (parentSlug: string, subSlug: string) => void\n onLaunch: (app: Resource) => void\n onClear: () => void\n keyboardEnabled?: boolean\n selectedSubSlug?: string\n}) {\n // The first row starts focused: Enter straight after typing opens it, and the\n // focus style shows which row that is. Nothing-focused (-1) left Enter dead\n // until the user pressed ↓ first (#167).\n const [focusedIndex, setFocusedIndex] = useState(0)\n const [expandedParents, setExpandedParents] = useState<Set<string>>(\n () => new Set(),\n )\n\n const results = useMemo(\n () => searchResources(apps, searchValue),\n [apps, searchValue],\n )\n\n // For each result, the child/sub-resources of it that themselves match the\n // query — so a parent surfaced only via its children (e.g. AWS Console via a\n // \"biom\" account) tells the user WHICH sub-resources matched, instead of an\n // unexplained parent. Keyed by parent slug.\n const matchedChildrenByParent = useMemo(() => {\n const q = searchValue.trim().toLowerCase()\n const map = new Map<string, Resource[]>()\n if (!q) return map\n const resultSlugs = new Set(results.map((r) => r.slug))\n for (const r of apps) {\n if (!r.parentSlug || !resultSlugs.has(r.parentSlug)) continue\n const hay = `${r.displayName} ${r.abbreviation ?? ''} ${(\n r.aliases ?? []\n ).join(' ')} ${r.description ?? ''}`.toLowerCase()\n if (!hay.includes(q)) continue\n const list = map.get(r.parentSlug) ?? []\n list.push(r)\n map.set(r.parentSlug, list)\n }\n return map\n }, [apps, results, searchValue])\n\n // #11 deprecated fallback: when EVERY match is a deprecated app (i.e. there\n // are no active matches for the query, only deprecated ones), surface a\n // notice so the user understands why they're seeing archived tools. The\n // deprecated apps are already in `results` because the launcher feeds search\n // the full resource set; this just labels the situation.\n const didDeprecatedFallback = useMemo(\n () => results.length > 0 && results.every((r) => Boolean(r.deprecated)),\n [results],\n )\n\n // Flatten parents + their matched sub-resources into one navigable list, so a\n // sub-resource row is a first-class result (clickable, focusable) rather than\n // a footnote on its parent.\n const rows = useMemo(() => {\n const out: ResultRow[] = []\n for (const app of results) {\n out.push({ kind: 'app', key: `app:${app.slug}`, app })\n const kids = matchedChildrenByParent.get(app.slug) ?? []\n const expanded = expandedParents.has(app.slug)\n const visible = expanded ? kids : kids.slice(0, COLLAPSED_SUB_LIMIT)\n for (const sub of visible) {\n out.push({\n kind: 'sub',\n key: `sub:${app.slug}:${sub.slug}`,\n sub,\n parent: app,\n })\n }\n const hidden = kids.length - visible.length\n if (hidden > 0) {\n out.push({ kind: 'more', key: `more:${app.slug}`, parent: app, hidden })\n }\n }\n return out\n }, [results, matchedChildrenByParent, expandedParents])\n\n const matchCount = useMemo(() => {\n let n = results.length\n for (const kids of matchedChildrenByParent.values()) n += kids.length\n return n\n }, [results, matchedChildrenByParent])\n\n const activateRow = useCallback(\n (row: ResultRow) => {\n if (row.kind === 'app') {\n onAppClick(row.app)\n } else if (row.kind === 'sub') {\n // Open the parent's detail singled out to THIS sub-resource. Filtering\n // the parent's sub-resource table by the query instead would leave the\n // clicked row buried among every other match (\"project\" matches 47).\n onSubClick(row.parent.slug, row.sub.slug)\n } else {\n setExpandedParents((prev) => new Set(prev).add(row.parent.slug))\n }\n },\n [onAppClick, onSubClick],\n )\n\n // Back to the first row and collapse expanded sub-resource lists when the query changes\n useEffect(() => {\n setFocusedIndex(0)\n setExpandedParents(new Set())\n }, [searchValue])\n\n useEffect(() => {\n // While a detail overlay is open it owns the keyboard: without this guard\n // Esc would clear the search behind the overlay and ↑↓/↵ would navigate to\n // a different app from a list the user cannot see.\n if (!keyboardEnabled) return\n const onKey = (e: KeyboardEvent) => {\n if (rows.length === 0) return\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n setFocusedIndex((i) => Math.min(i + 1, rows.length - 1))\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n setFocusedIndex((i) => Math.max(i - 1, 0))\n } else if (e.key === 'Enter') {\n const row = focusedIndex >= 0 ? rows[focusedIndex] : undefined\n if (row) activateRow(row)\n } else if (e.key === 'Escape') {\n onClear()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [rows, focusedIndex, activateRow, onClear, keyboardEnabled])\n\n return (\n <div className=\"max-w-[620px] mx-auto mt-3\">\n {/* Meta line. Counts matched sub-resources too — they are results in\n their own right, and a parent surfaced only via 47 matching accounts\n used to be reported as \"1 result\". Collapsed ones still count, so the\n number doesn't jump when the \"... N more\" row is expanded. */}\n <div className=\"text-[13px] text-muted-foreground mb-2 px-1\">\n {matchCount === 0\n ? `No results for \"${searchValue}\"`\n : `${matchCount} result${matchCount === 1 ? '' : 's'}`}\n </div>\n\n {/* #11: deprecated-only fallback notice */}\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"text-[13px] text-muted-foreground mb-2 px-1\"\n >\n No active apps for &quot;{searchValue}&quot; — showing deprecated\n matches.\n </div>\n )}\n\n {/* Results — parents and their matched sub-resources as one flat list */}\n <div\n className=\"flex flex-col gap-1.5\"\n role=\"listbox\"\n aria-label=\"Search results\"\n >\n {rows.map((row, i) => {\n const focused = focusedIndex === i\n\n if (row.kind === 'sub') {\n const { sub, parent } = row\n const isOpen = selectedSubSlug === sub.slug\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n data-kind=\"sub\"\n aria-selected={focused}\n // \"Which one am I looking at\" — set while this sub-resource is\n // the one singled out in its parent's detail behind the overlay.\n aria-current={isOpen ? 'true' : undefined}\n aria-label={`${sub.displayName} — sub-resource of ${parent.displayName}`}\n title={sub.displayName}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n // Indented + left rule so the parent/child relation reads at\n // a glance without an extra text prefix.\n 'flex items-center ml-8 border-l-2 pl-3 pr-3 py-1.5 text-left',\n 'text-[13px] rounded-r-[var(--radius)] transition-colors',\n focused || isOpen\n ? 'border-primary bg-muted/40 text-foreground'\n : 'border-border text-muted-foreground hover:border-primary hover:bg-muted/30 hover:text-foreground',\n )}\n >\n <Highlight text={sub.displayName} query={searchValue} />\n </button>\n )\n }\n\n if (row.kind === 'more') {\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n data-kind=\"more\"\n aria-selected={focused}\n aria-label={`Show ${row.hidden} more matching sub-resources of ${row.parent.displayName}`}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n 'flex items-center ml-8 border-l-2 pl-3 pr-3 py-1.5 text-left',\n 'text-[13px] rounded-r-[var(--radius)] transition-colors',\n focused\n ? 'border-primary bg-muted/40 text-foreground'\n : 'border-border text-muted-foreground hover:border-primary hover:text-foreground',\n )}\n >\n ... {row.hidden} more\n </button>\n )\n }\n\n const app = row.app\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n title={`View ${app.displayName}`}\n aria-selected={focused}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border rounded-[var(--radius)] px-3.5 py-2.5',\n 'transition-all',\n focused\n ? 'border-ring shadow-sm bg-muted/30'\n : 'border-border hover:border-ring hover:shadow-sm',\n )}\n >\n <ResourceIcon app={app} size={40} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14px] font-semibold truncate\">\n <Highlight text={app.displayName} query={searchValue} />\n {app.abbreviation && app.abbreviation !== app.displayName && (\n <span className=\"ml-1.5 text-[12px] font-normal text-muted-foreground\">\n (<Highlight text={app.abbreviation} query={searchValue} />\n )\n </span>\n )}\n </span>\n {app.description && (\n <span className=\"block text-[12.5px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {typeLabel(app.type) && (\n <span className=\"text-[11.5px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap shrink-0\">\n {typeLabel(app.type)}\n </span>\n )}\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"size-7 shrink-0 opacity-0 group-hover:opacity-100\"\n />\n </button>\n )\n })}\n </div>\n\n {/* Keyboard hint */}\n {rows.length > 0 && (\n <p className=\"text-[11.5px] text-muted-foreground/60 text-center mt-3\">\n ↑↓ navigate · ↵ view access · esc clear\n </p>\n )}\n </div>\n )\n}\n\nexport function AppCatalogGrid({\n apps,\n allResources,\n searchValue,\n onSearchChange,\n onAppClick,\n onSubClick,\n onLaunch,\n detailOpen = false,\n selectedSubSlug,\n}: AppCatalogGridProps) {\n const { getTopApps } = useAppClickHistory()\n const [topSlugs, setTopSlugs] = useState<string[]>([])\n // Area labels are data (the catalog's own `category` tag definition); only the\n // icons and the day-to-day fold are deployment config.\n const { areas } = useUiSettings()\n const tagsDefinitions = use(AppCatalogContext)?.tagsDefinitions\n\n useEffect(() => {\n void getTopApps(5).then(setTopSlugs)\n }, [getTopApps])\n\n const bySlug = useMemo(() => {\n const m = new Map<string, Resource>()\n for (const a of apps) m.set(a.slug, a)\n return m\n }, [apps])\n\n // Your apps: frequently-clicked (personalized). Empty on a cold new machine.\n const frequent = useMemo(\n () =>\n topSlugs\n .map((s) => bySlug.get(s))\n .filter((a): a is Resource => Boolean(a))\n .slice(0, 5),\n [topSlugs, bySlug],\n )\n\n // New this week: apps ADDED in the last 7 days — see pickNewThisWeek (#96).\n const fresh = useMemo(() => pickNewThisWeek(apps), [apps])\n\n // Everything not already promoted to \"Your apps\", alpha by name.\n const browse = useMemo(() => {\n const promoted = new Set(frequent.map((a) => a.slug))\n return [...apps]\n .filter((a) => !promoted.has(a.slug))\n .sort((a, b) => a.displayName.localeCompare(b.displayName))\n }, [apps, frequent])\n\n // Grouped: day-to-day tools first, then areas biggest-first so the page opens\n // on the areas most people need. Within an area, alpha (from browse).\n const browseGroups = useMemo(() => groupByArea(browse), [browse])\n\n const isSearching = searchValue.trim() !== ''\n const searchRef = useRef<HTMLInputElement>(null)\n\n // ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault()\n searchRef.current?.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [])\n\n return (\n <div className=\"mx-auto w-full max-w-[1000px] pb-24\">\n {/* hero */}\n <div className=\"text-center pt-10 pb-1.5\">\n <h1 className=\"font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5\">\n What do you need to get into?\n </h1>\n <p className=\"text-muted-foreground m-0 mb-6 text-[15px]\">\n Find any tool, see how to request access, or jump straight in.\n </p>\n <div className=\"max-w-[620px] mx-auto\">\n <div className=\"flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors\">\n <Search className=\"size-5 shrink-0 text-muted-foreground\" />\n <input\n type=\"text\"\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n placeholder=\"Search apps, cloud accounts, or keywords\"\n autoComplete=\"off\"\n autoFocus\n ref={searchRef}\n aria-label=\"Search apps\"\n className=\"flex-1 bg-transparent border-0 outline-none text-[16px] text-foreground placeholder:text-muted-foreground\"\n />\n {isSearching ? (\n <button\n type=\"button\"\n aria-label=\"Clear search\"\n onClick={() => {\n onSearchChange('')\n searchRef.current?.focus()\n }}\n className=\"shrink-0 rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-[11px] text-muted-foreground/60 border border-muted-foreground/20 rounded px-1.5 py-0.5 font-mono shrink-0\">\n {typeof navigator !== 'undefined' &&\n navigator.platform.includes('Mac')\n ? '⌘K'\n : 'Ctrl+K'}\n </kbd>\n )}\n </div>\n </div>\n </div>\n\n {/* Search results: rendered in place, directly under the (unmoved) hero\n search box. The discovery spine below is what gets replaced — the\n shell, hero and search box stay put, so typing never shifts layout. */}\n {isSearching && (\n <SearchResultsList\n apps={allResources ?? apps}\n searchValue={searchValue}\n onAppClick={onAppClick}\n onSubClick={onSubClick}\n onLaunch={onLaunch}\n onClear={() => onSearchChange('')}\n keyboardEnabled={!detailOpen}\n selectedSubSlug={selectedSubSlug}\n />\n )}\n\n {/* Discovery spine: only shown when not searching */}\n {!isSearching && (\n <>\n {/* Your apps */}\n {frequent.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead title=\"Your apps\" count=\"frequent\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3.5\">\n {frequent.map((app) => (\n <button\n type=\"button\"\n key={app.slug}\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className=\"group relative bg-card border border-border rounded-[var(--radius)] px-3.5 pt-[18px] pb-4 text-center shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all\"\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-2.5 right-2.5 size-[26px] opacity-0 group-hover:opacity-100\"\n />\n <ResourceIcon\n app={app}\n size={46}\n className=\"mx-auto mb-3\"\n />\n <span className=\"block text-[13.5px] font-bold truncate\">\n {app.displayName}\n </span>\n </button>\n ))}\n </div>\n </section>\n )}\n\n {/* New this week */}\n {fresh.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead\n title=\"New this week\"\n count={`${fresh.length} added`}\n />\n <div className=\"flex flex-col gap-2\">\n {fresh.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n showNew\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Everything else, grouped by area — the area titles carry it, so\n there is no \"Browse all\" header above them. */}\n <section className=\"mt-9\">\n {browseGroups.map(([key, list]) => (\n <AreaSection\n key={key}\n label={areaLabel(key, tagsDefinitions, areas?.dayToDayLabel)}\n Icon={areas?.icons?.[key]}\n apps={list}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </section>\n\n <AttributionFooter />\n </>\n )}\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowUpRight, Search, X } from 'lucide-react'\nimport { use, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { pickNewThisWeek } from '../../utils/launcherHelpers'\nimport { AttributionFooter } from './AttributionFooter'\nimport { ResourceIcon } from './ResourceIcon'\nimport { searchResources } from '@igstack/app-catalog-shared-core'\nimport { Highlight } from '../components/Highlight'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport type { AreaIcon } from '~/types/uiSettings'\nimport { AppCatalogContext } from '../../context/AppCatalogContext'\nimport { areaLabel, groupByArea } from '../../utils/areaGrouping'\n\n/**\n * Adaptive-home discovery spine (issue #38, increment 1) — matches the\n * option-a-launcher prototype:\n * Your apps (frequent) → New this week → all areas, grouped\n *\n * This is the PERSISTENT shell for the catalog: idle browse, active search and\n * an open detail overlay all render inside it. Only the area below the hero\n * swaps (discovery spine ↔ results list), so the hero copy, the search box and\n * the page container never move while the user types. Do not switch the page to\n * a different container on search — that was the layout-jitter bug.\n */\n\n// Type pill text. Plain \"application\" resources are the common case and don't\n// need a badge (the user asked not to show a bare \"App\" badge); only surface a\n// pill for distinctive types (database, cloud, service, …).\nconst typeLabel = (t?: string): string | null => {\n if (!t || t === 'application') return null\n return t.charAt(0).toUpperCase() + t.slice(1)\n}\n\nexport interface AppCatalogGridProps {\n /** All root (non-child) resources, already filtered for deprecated per settings. */\n apps: Resource[]\n /**\n * All resources INCLUDING children/sub-resources. Passed to search so a query\n * matching a sub-resource (e.g. an AWS account, an alias/account ID) surfaces\n * its parent — preserving the existing cross-sub-resource search behavior.\n * Falls back to `apps` when omitted.\n */\n allResources?: Resource[]\n searchValue: string\n onSearchChange: (v: string) => void\n onAppClick: (app: Resource) => void\n /** Opens a matched sub-resource: its parent's detail, singled out to it. */\n onSubClick: (parentSlug: string, subSlug: string) => void\n onLaunch: (app: Resource) => void\n /**\n * @deprecated Unused — it labelled the removed \"Browse all\" header. Still\n * accepted so existing callers keep compiling; drop it on the next major.\n */\n totalCount?: number\n /**\n * True while an app/sub-resource detail overlay is open above the launcher.\n * The results list then stops binding ↑↓/↵/Esc so the overlay owns the\n * keyboard (otherwise Esc would ALSO wipe the search behind the overlay).\n */\n detailOpen?: boolean\n /** Currently open sub-resource slug, for highlighting its row. */\n selectedSubSlug?: string\n}\n\nfunction LaunchButton({\n app,\n onLaunch,\n className,\n}: {\n app: Resource\n onLaunch: (app: Resource) => void\n className?: string\n}) {\n if (!app.appUrl) return null\n // Show the destination URL on hover (user ask) — the native title tooltip\n // reveals where the ↗ jumps to, e.g. \"Open → console.aws.amazon.com\".\n const prettyUrl = displayUrl(app.appUrl)\n return (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={`Open → ${prettyUrl}`}\n aria-label={`Open ${app.displayName} in a new tab (${prettyUrl})`}\n onClick={(e) => {\n e.stopPropagation()\n onLaunch(app)\n }}\n className={cn(\n 'grid place-items-center rounded-full border border-border bg-card text-muted-foreground',\n 'hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors',\n className,\n )}\n >\n <ArrowUpRight className=\"size-3.5\" />\n </a>\n )\n}\n\nfunction SectionHead({\n title,\n count,\n action,\n}: {\n title: string\n count?: string\n action?: React.ReactNode\n}) {\n return (\n <div className=\"flex items-center gap-2.5 mb-4\">\n <h2 className=\"font-serif font-semibold text-[15px] tracking-tight m-0\">\n {title}\n </h2>\n {count && (\n <span className=\"text-[13px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5\">\n {count}\n </span>\n )}\n {action && <div className=\"ml-auto\">{action}</div>}\n </div>\n )\n}\n\n/** Small row used by \"New this week\" and the search results. */\nfunction ResourceRow({\n app,\n onAppClick,\n onLaunch,\n showNew = false,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n showNew?: boolean\n}) {\n return (\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border border-border rounded-[var(--radius)] px-3.5 py-3',\n 'hover:border-ring hover:shadow-sm transition-all',\n )}\n >\n <ResourceIcon app={app} size={38} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14.5px] font-semibold truncate\">\n {app.displayName}\n </span>\n {app.description && (\n <span className=\"block text-[13px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {showNew && (\n <span className=\"text-[11px] font-bold text-[#65a443] bg-[#65a443]/12 rounded-full px-2.5 py-0.5\">\n New\n </span>\n )}\n {typeLabel(app.type) && (\n <span className=\"text-[12px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap\">\n {typeLabel(app.type)}\n </span>\n )}\n <LaunchButton app={app} onLaunch={onLaunch} className=\"size-8\" />\n </button>\n )\n}\n\n/**\n * Wide area card: icon on the left, name and description beside it.\n *\n * The grid cell keeps a fixed footprint and the visible panel is absolute inside\n * it, so a card growing on hover OVERLAYS the row below instead of reflowing the\n * whole grid. Hover reveals the rest of the description and nothing else — an\n * earlier version staggered a delayed tag block in behind the growth, which read\n * as jitter. Keyboard focus expands the same way, and `motion-reduce` drops the\n * animation.\n *\n * The footprint is a fixed height rather than an aspect ratio: at two or three\n * columns a ratio makes the card grow taller as the window widens, and the\n * description — three lines at most — leaves the bottom half empty.\n */\nfunction AppCard({\n app,\n onAppClick,\n onLaunch,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <div className=\"relative h-[92px]\">\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group absolute inset-x-0 top-0 min-h-full max-h-full overflow-hidden',\n 'flex gap-3.5 text-left bg-card border border-border',\n 'rounded-[var(--radius)] p-[15px] pr-11 outline-none',\n 'transition-[max-height,box-shadow,border-color] duration-200',\n 'hover:max-h-96 hover:border-ring hover:shadow-lg hover:z-10',\n 'focus-visible:max-h-96 focus-visible:border-ring focus-visible:shadow-lg focus-visible:z-10',\n 'motion-reduce:transition-none',\n )}\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-3 right-3 size-[26px] opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100\"\n />\n <div className=\"shrink-0\">\n <ResourceIcon app={app} size={40} />\n </div>\n {/* min-w-0 so a long unbroken word truncates instead of pushing the\n column wider than the card. */}\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n {/* shrink-0: while max-height animates, the un-clamped description\n overflows the panel for a few frames. Without it flexbox squeezes\n this line to 0 and back, which reads as the title flickering. */}\n <span className=\"block shrink-0 text-[14.5px] font-bold leading-tight line-clamp-1 group-hover:line-clamp-2 group-focus-visible:line-clamp-2\">\n {app.displayName}\n </span>\n {app.description && (\n <span\n className={cn(\n 'block shrink-0 text-[12.5px] leading-snug text-muted-foreground',\n 'line-clamp-2 group-hover:line-clamp-none group-focus-visible:line-clamp-none',\n )}\n >\n {markdownToPlainText(app.description)}\n </span>\n )}\n </div>\n </button>\n </div>\n )\n}\n\n/**\n * One area: icon, big title, count, then its cards. The icon stands in for the\n * accent bar the header used to carry — it does the same colour job and also\n * says which area you are in. It is decorative: the `h3` carries the name.\n */\nfunction AreaSection({\n label,\n Icon,\n apps,\n onAppClick,\n onLaunch,\n}: {\n label: string\n Icon?: AreaIcon\n apps: Resource[]\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <section className=\"mt-11\">\n <div className=\"flex items-center gap-2.5 mb-4\">\n {Icon ? (\n <Icon\n size={26}\n strokeWidth={1.75}\n className=\"text-primary shrink-0\"\n />\n ) : (\n <div className=\"h-[3px] w-11 rounded-full bg-primary\" />\n )}\n <h3 className=\"font-serif font-semibold text-[27px] leading-tight tracking-tight m-0\">\n {label}\n </h3>\n <span className=\"text-[13px] text-muted-foreground\">\n {apps.length} {apps.length === 1 ? 'tool' : 'tools'}\n </span>\n </div>\n <div className=\"grid gap-3 grid-cols-1 md:grid-cols-2 xl:grid-cols-3\">\n {apps.map((app) => (\n <AppCard\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n </section>\n )\n}\n\n/** Matched sub-resources listed under a parent before the \"… N more\" row. */\nconst COLLAPSED_SUB_LIMIT = 5\n\n/** One line in the flat, keyboard-navigable results list. */\ntype ResultRow =\n | { kind: 'app'; key: string; app: Resource }\n | { kind: 'sub'; key: string; sub: Resource; parent: Resource }\n | { kind: 'more'; key: string; parent: Resource; hidden: number }\n\n/**\n * Keyboard-navigable search results list — the launcher's only search surface.\n *\n * It renders directly BELOW the hero search box, inside the same persistent\n * launcher shell: the hero copy, the search input and the page container never\n * move when the user types, and no filter chrome (tabs / category / deprecated\n * toggle) appears. Those filters live in the browse view only — clearing the\n * search brings them back.\n *\n * Rows are flat and individually clickable. A matched sub-resource gets its own\n * indented row under its parent, so a query that only hits a sub-resource\n * (e.g. a cloud account) can be opened directly instead of forcing the user to\n * dig through the parent's detail. ↑↓ moves over every row, ↵ activates the\n * focused one (the first row until something else is focused), Esc clears the\n * search.\n */\nfunction SearchResultsList({\n apps,\n searchValue,\n onAppClick,\n onSubClick,\n onLaunch,\n onClear,\n keyboardEnabled = true,\n selectedSubSlug,\n}: {\n apps: Resource[]\n searchValue: string\n onAppClick: (app: Resource) => void\n onSubClick: (parentSlug: string, subSlug: string) => void\n onLaunch: (app: Resource) => void\n onClear: () => void\n keyboardEnabled?: boolean\n selectedSubSlug?: string\n}) {\n // The first row starts focused: Enter straight after typing opens it, and the\n // focus style shows which row that is. Nothing-focused (-1) left Enter dead\n // until the user pressed ↓ first (#167).\n const [focusedIndex, setFocusedIndex] = useState(0)\n const [expandedParents, setExpandedParents] = useState<Set<string>>(\n () => new Set(),\n )\n\n const results = useMemo(\n () => searchResources(apps, searchValue),\n [apps, searchValue],\n )\n\n // For each result, the child/sub-resources of it that themselves match the\n // query — so a parent surfaced only via its children (e.g. AWS Console via a\n // \"biom\" account) tells the user WHICH sub-resources matched, instead of an\n // unexplained parent. Keyed by parent slug.\n const matchedChildrenByParent = useMemo(() => {\n const q = searchValue.trim().toLowerCase()\n const map = new Map<string, Resource[]>()\n if (!q) return map\n const resultSlugs = new Set(results.map((r) => r.slug))\n for (const r of apps) {\n if (!r.parentSlug || !resultSlugs.has(r.parentSlug)) continue\n const hay = `${r.displayName} ${r.abbreviation ?? ''} ${(\n r.aliases ?? []\n ).join(' ')} ${r.description ?? ''}`.toLowerCase()\n if (!hay.includes(q)) continue\n const list = map.get(r.parentSlug) ?? []\n list.push(r)\n map.set(r.parentSlug, list)\n }\n return map\n }, [apps, results, searchValue])\n\n // #11 deprecated fallback: when EVERY match is a deprecated app (i.e. there\n // are no active matches for the query, only deprecated ones), surface a\n // notice so the user understands why they're seeing archived tools. The\n // deprecated apps are already in `results` because the launcher feeds search\n // the full resource set; this just labels the situation.\n const didDeprecatedFallback = useMemo(\n () => results.length > 0 && results.every((r) => Boolean(r.deprecated)),\n [results],\n )\n\n // Flatten parents + their matched sub-resources into one navigable list, so a\n // sub-resource row is a first-class result (clickable, focusable) rather than\n // a footnote on its parent.\n const rows = useMemo(() => {\n const out: ResultRow[] = []\n for (const app of results) {\n out.push({ kind: 'app', key: `app:${app.slug}`, app })\n const kids = matchedChildrenByParent.get(app.slug) ?? []\n const expanded = expandedParents.has(app.slug)\n const visible = expanded ? kids : kids.slice(0, COLLAPSED_SUB_LIMIT)\n for (const sub of visible) {\n out.push({\n kind: 'sub',\n key: `sub:${app.slug}:${sub.slug}`,\n sub,\n parent: app,\n })\n }\n const hidden = kids.length - visible.length\n if (hidden > 0) {\n out.push({ kind: 'more', key: `more:${app.slug}`, parent: app, hidden })\n }\n }\n return out\n }, [results, matchedChildrenByParent, expandedParents])\n\n const matchCount = useMemo(() => {\n let n = results.length\n for (const kids of matchedChildrenByParent.values()) n += kids.length\n return n\n }, [results, matchedChildrenByParent])\n\n const activateRow = useCallback(\n (row: ResultRow) => {\n if (row.kind === 'app') {\n onAppClick(row.app)\n } else if (row.kind === 'sub') {\n // Open the parent's detail singled out to THIS sub-resource. Filtering\n // the parent's sub-resource table by the query instead would leave the\n // clicked row buried among every other match (\"project\" matches 47).\n onSubClick(row.parent.slug, row.sub.slug)\n } else {\n setExpandedParents((prev) => new Set(prev).add(row.parent.slug))\n }\n },\n [onAppClick, onSubClick],\n )\n\n // Back to the first row and collapse expanded sub-resource lists when the query changes\n useEffect(() => {\n setFocusedIndex(0)\n setExpandedParents(new Set())\n }, [searchValue])\n\n useEffect(() => {\n // While a detail overlay is open it owns the keyboard: without this guard\n // Esc would clear the search behind the overlay and ↑↓/↵ would navigate to\n // a different app from a list the user cannot see.\n if (!keyboardEnabled) return\n const onKey = (e: KeyboardEvent) => {\n if (rows.length === 0) return\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n setFocusedIndex((i) => Math.min(i + 1, rows.length - 1))\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n setFocusedIndex((i) => Math.max(i - 1, 0))\n } else if (e.key === 'Enter') {\n // A focused row or button activates itself through its native click;\n // acting here as well would open the first result on top of it.\n if (e.defaultPrevented) return\n if (\n e.target instanceof HTMLElement &&\n e.target.closest('button, a, [role=\"option\"]')\n )\n return\n const row = focusedIndex >= 0 ? rows[focusedIndex] : undefined\n if (row) activateRow(row)\n } else if (e.key === 'Escape') {\n onClear()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [rows, focusedIndex, activateRow, onClear, keyboardEnabled])\n\n return (\n <div className=\"max-w-[620px] mx-auto mt-3\">\n {/* Meta line. Counts matched sub-resources too — they are results in\n their own right, and a parent surfaced only via 47 matching accounts\n used to be reported as \"1 result\". Collapsed ones still count, so the\n number doesn't jump when the \"... N more\" row is expanded. */}\n <div className=\"text-[13px] text-muted-foreground mb-2 px-1\">\n {matchCount === 0\n ? `No results for \"${searchValue}\"`\n : `${matchCount} result${matchCount === 1 ? '' : 's'}`}\n </div>\n\n {/* #11: deprecated-only fallback notice */}\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"text-[13px] text-muted-foreground mb-2 px-1\"\n >\n No active apps for &quot;{searchValue}&quot; — showing deprecated\n matches.\n </div>\n )}\n\n {/* Results — parents and their matched sub-resources as one flat list */}\n <div\n className=\"flex flex-col gap-1.5\"\n role=\"listbox\"\n aria-label=\"Search results\"\n >\n {rows.map((row, i) => {\n const focused = focusedIndex === i\n\n if (row.kind === 'sub') {\n const { sub, parent } = row\n const isOpen = selectedSubSlug === sub.slug\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n data-kind=\"sub\"\n aria-selected={focused}\n // \"Which one am I looking at\" — set while this sub-resource is\n // the one singled out in its parent's detail behind the overlay.\n aria-current={isOpen ? 'true' : undefined}\n aria-label={`${sub.displayName} — sub-resource of ${parent.displayName}`}\n title={sub.displayName}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n // Indented + left rule so the parent/child relation reads at\n // a glance without an extra text prefix.\n 'flex items-center ml-8 border-l-2 pl-3 pr-3 py-1.5 text-left',\n 'text-[13px] rounded-r-[var(--radius)] transition-colors',\n focused || isOpen\n ? 'border-primary bg-muted/40 text-foreground'\n : 'border-border text-muted-foreground hover:border-primary hover:bg-muted/30 hover:text-foreground',\n )}\n >\n <Highlight text={sub.displayName} query={searchValue} />\n </button>\n )\n }\n\n if (row.kind === 'more') {\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n data-kind=\"more\"\n aria-selected={focused}\n aria-label={`Show ${row.hidden} more matching sub-resources of ${row.parent.displayName}`}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n 'flex items-center ml-8 border-l-2 pl-3 pr-3 py-1.5 text-left',\n 'text-[13px] rounded-r-[var(--radius)] transition-colors',\n focused\n ? 'border-primary bg-muted/40 text-foreground'\n : 'border-border text-muted-foreground hover:border-primary hover:text-foreground',\n )}\n >\n ... {row.hidden} more\n </button>\n )\n }\n\n const app = row.app\n return (\n <button\n key={row.key}\n type=\"button\"\n role=\"option\"\n title={`View ${app.displayName}`}\n aria-selected={focused}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => activateRow(row)}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border rounded-[var(--radius)] px-3.5 py-2.5',\n 'transition-all',\n focused\n ? 'border-ring shadow-sm bg-muted/30'\n : 'border-border hover:border-ring hover:shadow-sm',\n )}\n >\n <ResourceIcon app={app} size={40} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14px] font-semibold truncate\">\n <Highlight text={app.displayName} query={searchValue} />\n {app.abbreviation && app.abbreviation !== app.displayName && (\n <span className=\"ml-1.5 text-[12px] font-normal text-muted-foreground\">\n (<Highlight text={app.abbreviation} query={searchValue} />\n )\n </span>\n )}\n </span>\n {app.description && (\n <span className=\"block text-[12.5px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {typeLabel(app.type) && (\n <span className=\"text-[11.5px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap shrink-0\">\n {typeLabel(app.type)}\n </span>\n )}\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"size-7 shrink-0 opacity-0 group-hover:opacity-100\"\n />\n </button>\n )\n })}\n </div>\n\n {/* Keyboard hint */}\n {rows.length > 0 && (\n <p className=\"text-[11.5px] text-muted-foreground/60 text-center mt-3\">\n ↑↓ navigate · ↵ view access · esc clear\n </p>\n )}\n </div>\n )\n}\n\nexport function AppCatalogGrid({\n apps,\n allResources,\n searchValue,\n onSearchChange,\n onAppClick,\n onSubClick,\n onLaunch,\n detailOpen = false,\n selectedSubSlug,\n}: AppCatalogGridProps) {\n const { getTopApps } = useAppClickHistory()\n const [topSlugs, setTopSlugs] = useState<string[]>([])\n // Area labels are data (the catalog's own `category` tag definition); only the\n // icons and the day-to-day fold are deployment config.\n const { areas } = useUiSettings()\n const tagsDefinitions = use(AppCatalogContext)?.tagsDefinitions\n\n useEffect(() => {\n void getTopApps(5).then(setTopSlugs)\n }, [getTopApps])\n\n const bySlug = useMemo(() => {\n const m = new Map<string, Resource>()\n for (const a of apps) m.set(a.slug, a)\n return m\n }, [apps])\n\n // Your apps: frequently-clicked (personalized). Empty on a cold new machine.\n const frequent = useMemo(\n () =>\n topSlugs\n .map((s) => bySlug.get(s))\n .filter((a): a is Resource => Boolean(a))\n .slice(0, 5),\n [topSlugs, bySlug],\n )\n\n // New this week: apps ADDED in the last 7 days — see pickNewThisWeek (#96).\n const fresh = useMemo(() => pickNewThisWeek(apps), [apps])\n\n // Everything not already promoted to \"Your apps\", alpha by name.\n const browse = useMemo(() => {\n const promoted = new Set(frequent.map((a) => a.slug))\n return [...apps]\n .filter((a) => !promoted.has(a.slug))\n .sort((a, b) => a.displayName.localeCompare(b.displayName))\n }, [apps, frequent])\n\n // Grouped: day-to-day tools first, then areas biggest-first so the page opens\n // on the areas most people need. Within an area, alpha (from browse).\n const browseGroups = useMemo(() => groupByArea(browse), [browse])\n\n const isSearching = searchValue.trim() !== ''\n const searchRef = useRef<HTMLInputElement>(null)\n\n // ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault()\n searchRef.current?.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [])\n\n return (\n <div className=\"mx-auto w-full max-w-[1000px] pb-24\">\n {/* hero */}\n <div className=\"text-center pt-10 pb-1.5\">\n <h1 className=\"font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5\">\n What do you need to get into?\n </h1>\n <p className=\"text-muted-foreground m-0 mb-6 text-[15px]\">\n Find any tool, see how to request access, or jump straight in.\n </p>\n <div className=\"max-w-[620px] mx-auto\">\n <div className=\"flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors\">\n <Search className=\"size-5 shrink-0 text-muted-foreground\" />\n <input\n type=\"text\"\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n placeholder=\"Search apps, cloud accounts, or keywords\"\n autoComplete=\"off\"\n autoFocus\n ref={searchRef}\n aria-label=\"Search apps\"\n className=\"flex-1 bg-transparent border-0 outline-none text-[16px] text-foreground placeholder:text-muted-foreground\"\n />\n {isSearching ? (\n <button\n type=\"button\"\n aria-label=\"Clear search\"\n onClick={() => {\n onSearchChange('')\n searchRef.current?.focus()\n }}\n className=\"shrink-0 rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-[11px] text-muted-foreground/60 border border-muted-foreground/20 rounded px-1.5 py-0.5 font-mono shrink-0\">\n {typeof navigator !== 'undefined' &&\n navigator.platform.includes('Mac')\n ? '⌘K'\n : 'Ctrl+K'}\n </kbd>\n )}\n </div>\n </div>\n </div>\n\n {/* Search results: rendered in place, directly under the (unmoved) hero\n search box. The discovery spine below is what gets replaced — the\n shell, hero and search box stay put, so typing never shifts layout. */}\n {isSearching && (\n <SearchResultsList\n apps={allResources ?? apps}\n searchValue={searchValue}\n onAppClick={onAppClick}\n onSubClick={onSubClick}\n onLaunch={onLaunch}\n onClear={() => onSearchChange('')}\n keyboardEnabled={!detailOpen}\n selectedSubSlug={selectedSubSlug}\n />\n )}\n\n {/* Discovery spine: only shown when not searching */}\n {!isSearching && (\n <>\n {/* Your apps */}\n {frequent.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead title=\"Your apps\" count=\"frequent\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3.5\">\n {frequent.map((app) => (\n <button\n type=\"button\"\n key={app.slug}\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className=\"group relative bg-card border border-border rounded-[var(--radius)] px-3.5 pt-[18px] pb-4 text-center shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all\"\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-2.5 right-2.5 size-[26px] opacity-0 group-hover:opacity-100\"\n />\n <ResourceIcon\n app={app}\n size={46}\n className=\"mx-auto mb-3\"\n />\n <span className=\"block text-[13.5px] font-bold truncate\">\n {app.displayName}\n </span>\n </button>\n ))}\n </div>\n </section>\n )}\n\n {/* New this week */}\n {fresh.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead\n title=\"New this week\"\n count={`${fresh.length} added`}\n />\n <div className=\"flex flex-col gap-2\">\n {fresh.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n showNew\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Everything else, grouped by area — the area titles carry it, so\n there is no \"Browse all\" header above them. */}\n <section className=\"mt-9\">\n {browseGroups.map(([key, list]) => (\n <AreaSection\n key={key}\n label={areaLabel(key, tagsDefinitions, areas?.dayToDayLabel)}\n Icon={areas?.icons?.[key]}\n apps={list}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </section>\n\n <AttributionFooter />\n </>\n )}\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "5.0.2-alpha-20260923221955",
3
+ "version": "5.0.2",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -113,7 +113,7 @@
113
113
  "tailwindcss": "^4.1.7",
114
114
  "tiptap-markdown": "^0.8.0",
115
115
  "tslib": "^2.3.0",
116
- "@igstack/app-catalog-shared-core": "5.0.2-alpha-20260923221955"
116
+ "@igstack/app-catalog-shared-core": "5.0.2"
117
117
  },
118
118
  "devDependencies": {
119
119
  "@tailwindcss/vite": "^4.0.5",
@@ -140,7 +140,7 @@
140
140
  "vite-plugin-static-copy": "^3.1.4",
141
141
  "vite-plugin-svgr": "^4.2.0",
142
142
  "vitest": "^4.1.2",
143
- "@igstack/app-catalog-backend-core": "5.0.2-alpha-20260923221955"
143
+ "@igstack/app-catalog-backend-core": "5.0.2"
144
144
  },
145
145
  "peerDependencies": {
146
146
  "react": "19.1.2",
@@ -149,7 +149,7 @@
149
149
  "vite": "^6.3.5",
150
150
  "vite-plugin-svgr": "^4.2.0"
151
151
  },
152
- "gitHead": "a1f1b238c79758447c81cdcdb569a6af7790062f",
152
+ "gitHead": "d9fecee127ebca122fa2d45e53819680aebab5e4",
153
153
  "scripts": {
154
154
  "build": "vite build",
155
155
  "build:lenient": "vite build --mode lenient",
@@ -81,6 +81,20 @@ describe('AppCatalogGrid — Enter on a fresh query', () => {
81
81
  expect(onAppClick).not.toHaveBeenCalled()
82
82
  })
83
83
 
84
+ it('lets a focused row activate itself once instead of also opening the first result', () => {
85
+ const onAppClick = renderGrid('alpha')
86
+ const second = screen.getAllByRole('option')[1]!
87
+
88
+ // Enter on a focused row: the browser fires the row's own click; the
89
+ // document listener must stay out of it or rows[0] opens on top.
90
+ fireEvent.keyDown(second, { key: 'Enter' })
91
+ expect(onAppClick).not.toHaveBeenCalled()
92
+
93
+ fireEvent.click(second)
94
+ expect(onAppClick).toHaveBeenCalledTimes(1)
95
+ expect(onAppClick.mock.calls[0]![0].displayName).toBe(nthResultName(1))
96
+ })
97
+
84
98
  it('leaves the keyboard to the detail overlay while one is open', () => {
85
99
  const onAppClick = renderGrid('alpha', true)
86
100
 
@@ -454,6 +454,14 @@ function SearchResultsList({
454
454
  e.preventDefault()
455
455
  setFocusedIndex((i) => Math.max(i - 1, 0))
456
456
  } else if (e.key === 'Enter') {
457
+ // A focused row or button activates itself through its native click;
458
+ // acting here as well would open the first result on top of it.
459
+ if (e.defaultPrevented) return
460
+ if (
461
+ e.target instanceof HTMLElement &&
462
+ e.target.closest('button, a, [role="option"]')
463
+ )
464
+ return
457
465
  const row = focusedIndex >= 0 ? rows[focusedIndex] : undefined
458
466
  if (row) activateRow(row)
459
467
  } else if (e.key === 'Escape') {