@igstack/app-catalog-frontend-core 6.3.1-alpha-20261003051755 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/utils/accessTabLabel.test.d.ts +1 -0
- package/dist/esm/api/infra/trpc.d.ts +42 -21
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +19 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +17 -2
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.d.ts +14 -0
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js +115 -0
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +11 -4
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js +0 -4
- package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +4 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +377 -385
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.d.ts +20 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js +59 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +64 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +90 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +13 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +433 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +18 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.d.ts +30 -0
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.js +34 -0
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.js.map +1 -0
- package/dist/index.css +269 -5
- package/package.json +4 -4
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +17 -3
- package/src/__tests__/modules/appCatalog/ui/catalog/RequestAppDialog.test.tsx +113 -0
- package/src/__tests__/modules/appCatalog/ui/components/AccessRequestSection.test.tsx +6 -4
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +25 -10
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +193 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +2 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +32 -9
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +125 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +2 -2
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +181 -0
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +131 -0
- package/src/__tests__/modules/appCatalog/utils/accessTabLabel.test.ts +206 -0
- package/src/__tests__/ui/components/header/Header.loginHidden.test.tsx +1 -1
- package/src/index.css +269 -5
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +24 -0
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +46 -2
- package/src/modules/appCatalog/ui/catalog/RequestAppDialog.tsx +169 -0
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +3 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +9 -4
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +4 -1
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +574 -554
- package/src/modules/appCatalog/ui/detail/AppDoor.tsx +97 -0
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +195 -0
- package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +605 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +20 -1
- package/src/modules/appCatalog/utils/accessTabLabel.ts +95 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.d.ts +0 -3
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js +0 -228
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js.map +0 -1
- package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.emptyState.test.tsx +0 -64
- package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.reviewStatus.test.tsx +0 -93
- package/src/modules/appCatalog/ui/detail/CommentsSection.tsx +0 -296
- /package/dist/esm/__tests__/modules/appCatalog/ui/{detail/CommentsSection.emptyState.test.d.ts → catalog/RequestAppDialog.test.d.ts} +0 -0
- /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{CommentsSection.reviewStatus.test.d.ts → AppDetails.doorAndTabs.test.d.ts} +0 -0
|
File without changes
|
|
File without changes
|
package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.d.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -43,7 +43,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
43
43
|
meta: object;
|
|
44
44
|
}>;
|
|
45
45
|
}>>;
|
|
46
|
-
|
|
46
|
+
feedback: import('@trpc/server').TRPCBuiltRouter<{
|
|
47
47
|
ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
|
|
48
48
|
meta: object;
|
|
49
49
|
errorShape: import('@trpc/server').TRPCDefaultErrorShape;
|
|
@@ -53,15 +53,22 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
53
53
|
input: {
|
|
54
54
|
resourceSlug: string;
|
|
55
55
|
};
|
|
56
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
56
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackList;
|
|
57
|
+
meta: object;
|
|
58
|
+
}>;
|
|
59
|
+
mine: import('@trpc/server').TRPCQueryProcedure<{
|
|
60
|
+
input: void;
|
|
61
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView[];
|
|
57
62
|
meta: object;
|
|
58
63
|
}>;
|
|
59
64
|
add: import('@trpc/server').TRPCMutationProcedure<{
|
|
60
65
|
input: {
|
|
61
|
-
resourceSlug
|
|
62
|
-
|
|
66
|
+
resourceSlug?: string | undefined;
|
|
67
|
+
subject?: string | undefined;
|
|
68
|
+
body?: string | undefined;
|
|
69
|
+
attachmentIds?: string[] | undefined;
|
|
63
70
|
};
|
|
64
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
71
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
65
72
|
meta: object;
|
|
66
73
|
}>;
|
|
67
74
|
edit: import('@trpc/server').TRPCMutationProcedure<{
|
|
@@ -69,10 +76,10 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
69
76
|
id: string;
|
|
70
77
|
body: string;
|
|
71
78
|
};
|
|
72
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
79
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
73
80
|
meta: object;
|
|
74
81
|
}>;
|
|
75
|
-
|
|
82
|
+
dismiss: import('@trpc/server').TRPCMutationProcedure<{
|
|
76
83
|
input: {
|
|
77
84
|
id: string;
|
|
78
85
|
};
|
|
@@ -159,7 +166,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
159
166
|
meta: object;
|
|
160
167
|
}>;
|
|
161
168
|
}>>;
|
|
162
|
-
|
|
169
|
+
feedback: import('@trpc/server').TRPCBuiltRouter<{
|
|
163
170
|
ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
|
|
164
171
|
meta: object;
|
|
165
172
|
errorShape: import('@trpc/server').TRPCDefaultErrorShape;
|
|
@@ -169,15 +176,22 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
169
176
|
input: {
|
|
170
177
|
resourceSlug: string;
|
|
171
178
|
};
|
|
172
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
179
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackList;
|
|
180
|
+
meta: object;
|
|
181
|
+
}>;
|
|
182
|
+
mine: import('@trpc/server').TRPCQueryProcedure<{
|
|
183
|
+
input: void;
|
|
184
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView[];
|
|
173
185
|
meta: object;
|
|
174
186
|
}>;
|
|
175
187
|
add: import('@trpc/server').TRPCMutationProcedure<{
|
|
176
188
|
input: {
|
|
177
|
-
resourceSlug
|
|
178
|
-
|
|
189
|
+
resourceSlug?: string | undefined;
|
|
190
|
+
subject?: string | undefined;
|
|
191
|
+
body?: string | undefined;
|
|
192
|
+
attachmentIds?: string[] | undefined;
|
|
179
193
|
};
|
|
180
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
194
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
181
195
|
meta: object;
|
|
182
196
|
}>;
|
|
183
197
|
edit: import('@trpc/server').TRPCMutationProcedure<{
|
|
@@ -185,10 +199,10 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
185
199
|
id: string;
|
|
186
200
|
body: string;
|
|
187
201
|
};
|
|
188
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
202
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
189
203
|
meta: object;
|
|
190
204
|
}>;
|
|
191
|
-
|
|
205
|
+
dismiss: import('@trpc/server').TRPCMutationProcedure<{
|
|
192
206
|
input: {
|
|
193
207
|
id: string;
|
|
194
208
|
};
|
|
@@ -274,7 +288,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
274
288
|
meta: object;
|
|
275
289
|
}>;
|
|
276
290
|
}>>;
|
|
277
|
-
|
|
291
|
+
feedback: import('@trpc/server').TRPCBuiltRouter<{
|
|
278
292
|
ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
|
|
279
293
|
meta: object;
|
|
280
294
|
errorShape: import('@trpc/server').TRPCDefaultErrorShape;
|
|
@@ -284,15 +298,22 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
284
298
|
input: {
|
|
285
299
|
resourceSlug: string;
|
|
286
300
|
};
|
|
287
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
301
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackList;
|
|
302
|
+
meta: object;
|
|
303
|
+
}>;
|
|
304
|
+
mine: import('@trpc/server').TRPCQueryProcedure<{
|
|
305
|
+
input: void;
|
|
306
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView[];
|
|
288
307
|
meta: object;
|
|
289
308
|
}>;
|
|
290
309
|
add: import('@trpc/server').TRPCMutationProcedure<{
|
|
291
310
|
input: {
|
|
292
|
-
resourceSlug
|
|
293
|
-
|
|
311
|
+
resourceSlug?: string | undefined;
|
|
312
|
+
subject?: string | undefined;
|
|
313
|
+
body?: string | undefined;
|
|
314
|
+
attachmentIds?: string[] | undefined;
|
|
294
315
|
};
|
|
295
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
316
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
296
317
|
meta: object;
|
|
297
318
|
}>;
|
|
298
319
|
edit: import('@trpc/server').TRPCMutationProcedure<{
|
|
@@ -300,10 +321,10 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
300
321
|
id: string;
|
|
301
322
|
body: string;
|
|
302
323
|
};
|
|
303
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
324
|
+
output: import('@igstack/app-catalog-backend-core').FeedbackView;
|
|
304
325
|
meta: object;
|
|
305
326
|
}>;
|
|
306
|
-
|
|
327
|
+
dismiss: import('@trpc/server').TRPCMutationProcedure<{
|
|
307
328
|
input: {
|
|
308
329
|
id: string;
|
|
309
330
|
};
|
|
@@ -83,6 +83,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
83
83
|
title: string;
|
|
84
84
|
identity: string;
|
|
85
85
|
}[] | undefined;
|
|
86
|
+
childrenLabel?: string | undefined;
|
|
86
87
|
parentSlug?: string | undefined;
|
|
87
88
|
tier?: string | undefined;
|
|
88
89
|
aliases?: string[] | undefined;
|
|
@@ -12,6 +12,7 @@ import { displayUrl } from "../../utils/displayUrl.js";
|
|
|
12
12
|
import { useUiSettings } from "../../../../context/UiSettingsContext.js";
|
|
13
13
|
import { AppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
14
14
|
import { groupByArea, areaLabel } from "../../utils/areaGrouping.js";
|
|
15
|
+
import { RequestAppDialog } from "./RequestAppDialog.js";
|
|
15
16
|
const typeLabel = (t) => {
|
|
16
17
|
if (!t || t === "application") return null;
|
|
17
18
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
@@ -182,6 +183,7 @@ function SearchResultsList({
|
|
|
182
183
|
const [expandedParents, setExpandedParents] = useState(
|
|
183
184
|
() => /* @__PURE__ */ new Set()
|
|
184
185
|
);
|
|
186
|
+
const [requestOpen, setRequestOpen] = useState(false);
|
|
185
187
|
const results = useMemo(
|
|
186
188
|
() => searchResources(apps, searchValue),
|
|
187
189
|
[apps, searchValue]
|
|
@@ -273,6 +275,23 @@ function SearchResultsList({
|
|
|
273
275
|
}, [rows, focusedIndex, activateRow, onClear, keyboardEnabled]);
|
|
274
276
|
return /* @__PURE__ */ jsxs("div", { className: "max-w-[620px] mx-auto mt-3", children: [
|
|
275
277
|
/* @__PURE__ */ jsx("div", { className: "text-[13px] text-muted-foreground mb-2 px-1", children: matchCount === 0 ? `No results for "${searchValue}"` : `${matchCount} result${matchCount === 1 ? "" : "s"}` }),
|
|
278
|
+
matchCount === 0 && !didDeprecatedFallback && /* @__PURE__ */ jsx("div", { className: "mb-3 px-1", children: /* @__PURE__ */ jsx(
|
|
279
|
+
"button",
|
|
280
|
+
{
|
|
281
|
+
type: "button",
|
|
282
|
+
onClick: () => setRequestOpen(true),
|
|
283
|
+
className: "text-sm underline decoration-dotted underline-offset-2 hover:text-foreground text-muted-foreground",
|
|
284
|
+
children: "Not in the catalog? Ask for it to be added"
|
|
285
|
+
}
|
|
286
|
+
) }),
|
|
287
|
+
/* @__PURE__ */ jsx(
|
|
288
|
+
RequestAppDialog,
|
|
289
|
+
{
|
|
290
|
+
open: requestOpen,
|
|
291
|
+
onOpenChange: setRequestOpen,
|
|
292
|
+
initialSubject: searchValue
|
|
293
|
+
}
|
|
294
|
+
),
|
|
276
295
|
didDeprecatedFallback && /* @__PURE__ */ jsxs(
|
|
277
296
|
"div",
|
|
278
297
|
{
|
|
@@ -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 { 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 "{searchValue}" — 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-8\">\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 )}\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 { 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'\nimport { RequestAppDialog } from './RequestAppDialog'\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 // Asking for something absent. Local state rather than a url param: a half-typed\n // request has no business in a shareable link.\n const [requestOpen, setRequestOpen] = useState(false)\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 {/* A zero-result search is the most informative moment in the catalog: someone\n has said exactly what they expected to find. Showing only \"No results\"\n throws that away and leaves them with nowhere to go. */}\n {matchCount === 0 && !didDeprecatedFallback && (\n <div className=\"mb-3 px-1\">\n <button\n type=\"button\"\n onClick={() => setRequestOpen(true)}\n className=\"text-sm underline decoration-dotted underline-offset-2 hover:text-foreground text-muted-foreground\"\n >\n Not in the catalog? Ask for it to be added\n </button>\n </div>\n )}\n <RequestAppDialog\n open={requestOpen}\n onOpenChange={setRequestOpen}\n initialSubject={searchValue}\n />\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 "{searchValue}" — 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-8\">\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 )}\n </div>\n )\n}\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;AAgCA,MAAM,YAAY,CAAC,MAA8B;AAC/C,MAAI,CAAC,KAAK,MAAM,cAAe,QAAO;AACtC,SAAO,EAAE,OAAO,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC;AAC9C;AAiCA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,CAAC,IAAI,OAAQ,QAAO;AAGxB,QAAM,YAAY,WAAW,IAAI,MAAM;AACvC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,IAAI;AAAA,MACV,QAAO;AAAA,MACP,KAAI;AAAA,MACJ,OAAO,UAAU,SAAS;AAAA,MAC1B,cAAY,QAAQ,IAAI,WAAW,kBAAkB,SAAS;AAAA,MAC9D,SAAS,CAAC,MAAM;AACd,UAAE,gBAAA;AACF,iBAAS,GAAG;AAAA,MACd;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA,oBAAC,cAAA,EAAa,WAAU,WAAA,CAAW;AAAA,IAAA;AAAA,EAAA;AAGzC;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,2DACX,UAAA,OACH;AAAA,IACC,SACC,oBAAC,QAAA,EAAK,WAAU,yEACb,UAAA,OACH;AAAA,IAED,UAAU,oBAAC,OAAA,EAAI,WAAU,WAAW,UAAA,OAAA,CAAO;AAAA,EAAA,GAC9C;AAEJ;AAGA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAKG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,MAAM,WAAW,GAAG;AAAA,MAC7B,OAAO,QAAQ,IAAI,WAAW;AAAA,MAC9B,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,oBAAC,cAAA,EAAa,KAAU,MAAM,GAAA,CAAI;AAAA,QAClC,qBAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAU,8CACb,UAAA,IAAI,aACP;AAAA,UACC,IAAI,eACH,oBAAC,QAAA,EAAK,WAAU,oDACb,UAAA,oBAAoB,IAAI,WAAW,EAAA,CACtC;AAAA,QAAA,GAEJ;AAAA,QACC,WACC,oBAAC,QAAA,EAAK,WAAU,mFAAkF,UAAA,OAElG;AAAA,QAED,UAAU,IAAI,IAAI,KACjB,oBAAC,QAAA,EAAK,WAAU,2FACb,UAAA,UAAU,IAAI,IAAI,EAAA,CACrB;AAAA,QAEF,oBAAC,cAAA,EAAa,KAAU,UAAoB,WAAU,SAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGrE;AAgBA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,oBAAC,OAAA,EAAI,WAAU,qBACb,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,MAAM,WAAW,GAAG;AAAA,MAC7B,OAAO,QAAQ,IAAI,WAAW;AAAA,MAC9B,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,QAEZ,oBAAC,SAAI,WAAU,YACb,8BAAC,cAAA,EAAa,KAAU,MAAM,GAAA,CAAI,EAAA,CACpC;AAAA,QAGA,qBAAC,OAAA,EAAI,WAAU,iCAIb,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAU,+HACb,UAAA,IAAI,aACP;AAAA,UACC,IAAI,eACH;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cAAA;AAAA,cAGD,UAAA,oBAAoB,IAAI,WAAW;AAAA,YAAA;AAAA,UAAA;AAAA,QACtC,EAAA,CAEJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;AAOA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE,qBAAC,WAAA,EAAQ,WAAU,SACjB,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,kCACZ,UAAA;AAAA,MAAA,OACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM;AAAA,UACN,aAAa;AAAA,UACb,WAAU;AAAA,QAAA;AAAA,MAAA,IAGZ,oBAAC,OAAA,EAAI,WAAU,uCAAA,CAAuC;AAAA,MAExD,oBAAC,MAAA,EAAG,WAAU,yEACX,UAAA,OACH;AAAA,MACA,qBAAC,QAAA,EAAK,WAAU,qCACb,UAAA;AAAA,QAAA,KAAK;AAAA,QAAO;AAAA,QAAE,KAAK,WAAW,IAAI,SAAS;AAAA,MAAA,EAAA,CAC9C;AAAA,IAAA,GACF;AAAA,wBACC,OAAA,EAAI,WAAU,wDACZ,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAHK,IAAI;AAAA,IAAA,CAKZ,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAGA,MAAM,sBAAsB;AAwB5B,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AACF,GASG;AAID,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI;AAAA,IAC5C,0BAAU,IAAA;AAAA,EAAI;AAIhB,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,UAAU;AAAA,IACd,MAAM,gBAAgB,MAAM,WAAW;AAAA,IACvC,CAAC,MAAM,WAAW;AAAA,EAAA;AAOpB,QAAM,0BAA0B,QAAQ,MAAM;AAC5C,UAAM,IAAI,YAAY,KAAA,EAAO,YAAA;AAC7B,UAAM,0BAAU,IAAA;AAChB,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,cAAc,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AACtD,eAAW,KAAK,MAAM;AACpB,UAAI,CAAC,EAAE,cAAc,CAAC,YAAY,IAAI,EAAE,UAAU,EAAG;AACrD,YAAM,MAAM,GAAG,EAAE,WAAW,IAAI,EAAE,gBAAgB,EAAE,KAClD,EAAE,WAAW,CAAA,GACb,KAAK,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,YAAA;AACrC,UAAI,CAAC,IAAI,SAAS,CAAC,EAAG;AACtB,YAAM,OAAO,IAAI,IAAI,EAAE,UAAU,KAAK,CAAA;AACtC,WAAK,KAAK,CAAC;AACX,UAAI,IAAI,EAAE,YAAY,IAAI;AAAA,IAC5B;AACA,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,SAAS,WAAW,CAAC;AAO/B,QAAM,wBAAwB;AAAA,IAC5B,MAAM,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAC,MAAM,QAAQ,EAAE,UAAU,CAAC;AAAA,IACtE,CAAC,OAAO;AAAA,EAAA;AAMV,QAAM,OAAO,QAAQ,MAAM;AACzB,UAAM,MAAmB,CAAA;AACzB,eAAW,OAAO,SAAS;AACzB,UAAI,KAAK,EAAE,MAAM,OAAO,KAAK,OAAO,IAAI,IAAI,IAAI,IAAA,CAAK;AACrD,YAAM,OAAO,wBAAwB,IAAI,IAAI,IAAI,KAAK,CAAA;AACtD,YAAM,WAAW,gBAAgB,IAAI,IAAI,IAAI;AAC7C,YAAM,UAAU,WAAW,OAAO,KAAK,MAAM,GAAG,mBAAmB;AACnE,iBAAW,OAAO,SAAS;AACzB,YAAI,KAAK;AAAA,UACP,MAAM;AAAA,UACN,KAAK,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI;AAAA,UAChC;AAAA,UACA,QAAQ;AAAA,QAAA,CACT;AAAA,MACH;AACA,YAAM,SAAS,KAAK,SAAS,QAAQ;AACrC,UAAI,SAAS,GAAG;AACd,YAAI,KAAK,EAAE,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI,IAAI,QAAQ,KAAK,OAAA,CAAQ;AAAA,MACzE;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,yBAAyB,eAAe,CAAC;AAEtD,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,IAAI,QAAQ;AAChB,eAAW,QAAQ,wBAAwB,OAAA,QAAe,KAAK;AAC/D,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,uBAAuB,CAAC;AAErC,QAAM,cAAc;AAAA,IAClB,CAAC,QAAmB;AAClB,UAAI,IAAI,SAAS,OAAO;AACtB,mBAAW,IAAI,GAAG;AAAA,MACpB,WAAW,IAAI,SAAS,OAAO;AAI7B,mBAAW,IAAI,OAAO,MAAM,IAAI,IAAI,IAAI;AAAA,MAC1C,OAAO;AACL,2BAAmB,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,OAAO,IAAI,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,IACA,CAAC,YAAY,UAAU;AAAA,EAAA;AAIzB,YAAU,MAAM;AACd,oBAAgB,CAAC;AACjB,uBAAmB,oBAAI,KAAK;AAAA,EAC9B,GAAG,CAAC,WAAW,CAAC;AAEhB,YAAU,MAAM;AAId,QAAI,CAAC,gBAAiB;AACtB,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,KAAK,WAAW,EAAG;AACvB,UAAI,EAAE,QAAQ,aAAa;AACzB,UAAE,eAAA;AACF,wBAAgB,CAAC,MAAM,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAC,CAAC;AAAA,MACzD,WAAW,EAAE,QAAQ,WAAW;AAC9B,UAAE,eAAA;AACF,wBAAgB,CAAC,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC;AAAA,MAC3C,WAAW,EAAE,QAAQ,SAAS;AAG5B,YAAI,EAAE,iBAAkB;AACxB,YACE,EAAE,kBAAkB,eACpB,EAAE,OAAO,QAAQ,4BAA4B;AAE7C;AACF,cAAM,MAAM,gBAAgB,IAAI,KAAK,YAAY,IAAI;AACrD,YAAI,iBAAiB,GAAG;AAAA,MAC1B,WAAW,EAAE,QAAQ,UAAU;AAC7B,gBAAA;AAAA,MACF;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,MAAM,cAAc,aAAa,SAAS,eAAe,CAAC;AAE9D,SACE,qBAAC,OAAA,EAAI,WAAU,8BAKb,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,+CACZ,UAAA,eAAe,IACZ,mBAAmB,WAAW,MAC9B,GAAG,UAAU,UAAU,eAAe,IAAI,KAAK,GAAG,IACxD;AAAA,IAKC,eAAe,KAAK,CAAC,yBACpB,oBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,eAAe,IAAI;AAAA,QAClC,WAAU;AAAA,QACX,UAAA;AAAA,MAAA;AAAA,IAAA,GAGH;AAAA,IAEF;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,gBAAgB;AAAA,MAAA;AAAA,IAAA;AAAA,IAIjB,yBACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACX,UAAA;AAAA,UAAA;AAAA,UAC2B;AAAA,UAAY;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAM1C;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QAEV,UAAA,KAAK,IAAI,CAAC,KAAK,MAAM;AACpB,gBAAM,UAAU,iBAAiB;AAEjC,cAAI,IAAI,SAAS,OAAO;AACtB,kBAAM,EAAE,KAAK,OAAA,IAAW;AACxB,kBAAM,SAAS,oBAAoB,IAAI;AACvC,mBACE;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,aAAU;AAAA,gBACV,iBAAe;AAAA,gBAGf,gBAAc,SAAS,SAAS;AAAA,gBAChC,cAAY,GAAG,IAAI,WAAW,sBAAsB,OAAO,WAAW;AAAA,gBACtE,OAAO,IAAI;AAAA,gBACX,cAAc,MAAM,gBAAgB,CAAC;AAAA,gBACrC,SAAS,MAAM,YAAY,GAAG;AAAA,gBAC9B,WAAW;AAAA;AAAA;AAAA,kBAGT;AAAA,kBACA;AAAA,kBACA,WAAW,SACP,+CACA;AAAA,gBAAA;AAAA,gBAGN,8BAAC,WAAA,EAAU,MAAM,IAAI,aAAa,OAAO,YAAA,CAAa;AAAA,cAAA;AAAA,cAtBjD,IAAI;AAAA,YAAA;AAAA,UAyBf;AAEA,cAAI,IAAI,SAAS,QAAQ;AACvB,mBACE;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,aAAU;AAAA,gBACV,iBAAe;AAAA,gBACf,cAAY,QAAQ,IAAI,MAAM,mCAAmC,IAAI,OAAO,WAAW;AAAA,gBACvF,cAAc,MAAM,gBAAgB,CAAC;AAAA,gBACrC,SAAS,MAAM,YAAY,GAAG;AAAA,gBAC9B,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,kBACA,UACI,+CACA;AAAA,gBAAA;AAAA,gBAEP,UAAA;AAAA,kBAAA;AAAA,kBACM,IAAI;AAAA,kBAAO;AAAA,gBAAA;AAAA,cAAA;AAAA,cAhBX,IAAI;AAAA,YAAA;AAAA,UAmBf;AAEA,gBAAM,MAAM,IAAI;AAChB,iBACE;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,OAAO,QAAQ,IAAI,WAAW;AAAA,cAC9B,iBAAe;AAAA,cACf,cAAc,MAAM,gBAAgB,CAAC;AAAA,cACrC,SAAS,MAAM,YAAY,GAAG;AAAA,cAC9B,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,UACI,sCACA;AAAA,cAAA;AAAA,cAGN,UAAA;AAAA,gBAAA,oBAAC,cAAA,EAAa,KAAU,MAAM,GAAA,CAAI;AAAA,gBAClC,qBAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,kBAAA,qBAAC,QAAA,EAAK,WAAU,4CACd,UAAA;AAAA,oBAAA,oBAAC,WAAA,EAAU,MAAM,IAAI,aAAa,OAAO,aAAa;AAAA,oBACrD,IAAI,gBAAgB,IAAI,iBAAiB,IAAI,eAC5C,qBAAC,QAAA,EAAK,WAAU,wDAAuD,UAAA;AAAA,sBAAA;AAAA,0CACnE,WAAA,EAAU,MAAM,IAAI,cAAc,OAAO,aAAa;AAAA,sBAAE;AAAA,oBAAA,EAAA,CAE5D;AAAA,kBAAA,GAEJ;AAAA,kBACC,IAAI,eACH,oBAAC,QAAA,EAAK,WAAU,sDACb,UAAA,oBAAoB,IAAI,WAAW,EAAA,CACtC;AAAA,gBAAA,GAEJ;AAAA,gBACC,UAAU,IAAI,IAAI,KACjB,oBAAC,QAAA,EAAK,WAAU,sGACb,UAAA,UAAU,IAAI,IAAI,EAAA,CACrB;AAAA,gBAEF;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC;AAAA,oBACA;AAAA,oBACA,WAAU;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACZ;AAAA,YAAA;AAAA,YA1CK,IAAI;AAAA,UAAA;AAAA,QA6Cf,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,IAIF,KAAK,SAAS,yBACZ,KAAA,EAAE,WAAU,2DAA0D,UAAA,0CAAA,CAEvE;AAAA,EAAA,GAEJ;AAEJ;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AACF,GAAwB;;AACtB,QAAM,EAAE,WAAA,IAAe,mBAAA;AACvB,QAAM,CAAC,UAAU,WAAW,IAAI,SAAmB,CAAA,CAAE;AAGrD,QAAM,EAAE,MAAA,IAAU,cAAA;AAClB,QAAM,mBAAkB,SAAI,iBAAiB,MAArB,mBAAwB;AAEhD,YAAU,MAAM;AACd,SAAK,WAAW,CAAC,EAAE,KAAK,WAAW;AAAA,EACrC,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,wBAAQ,IAAA;AACd,eAAW,KAAK,KAAM,GAAE,IAAI,EAAE,MAAM,CAAC;AACrC,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAGT,QAAM,WAAW;AAAA,IACf,MACE,SACG,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,CAAC,EACxB,OAAO,CAAC,MAAqB,QAAQ,CAAC,CAAC,EACvC,MAAM,GAAG,CAAC;AAAA,IACf,CAAC,UAAU,MAAM;AAAA,EAAA;AAInB,QAAM,QAAQ,QAAQ,MAAM,gBAAgB,IAAI,GAAG,CAAC,IAAI,CAAC;AAGzD,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,WAAW,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AACpD,WAAO,CAAC,GAAG,IAAI,EACZ,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,IAAI,CAAC,EACnC,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,cAAc,EAAE,WAAW,CAAC;AAAA,EAC9D,GAAG,CAAC,MAAM,QAAQ,CAAC;AAInB,QAAM,eAAe,QAAQ,MAAM,YAAY,MAAM,GAAG,CAAC,MAAM,CAAC;AAEhE,QAAM,cAAc,YAAY,KAAA,MAAW;AAC3C,QAAM,YAAY,OAAyB,IAAI;AAG/C,YAAU,MAAM;AACd,UAAM,QAAQ,CAAC,MAAqB;;AAClC,WAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,KAAK;AAC7C,UAAE,eAAA;AACF,SAAAA,MAAA,UAAU,YAAV,gBAAAA,IAAmB;AAAA,MACrB;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAA,CAAE;AAEL,SACE,qBAAC,OAAA,EAAI,WAAU,sCAEb,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,4BACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,kEAAiE,UAAA,iCAE/E;AAAA,MACA,oBAAC,KAAA,EAAE,WAAU,8CAA6C,UAAA,kEAE1D;AAAA,0BACC,OAAA,EAAI,WAAU,yBACb,UAAA,qBAAC,OAAA,EAAI,WAAU,+LACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAO,WAAU,wCAAA,CAAwC;AAAA,QAC1D;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,eAAe,EAAE,OAAO,KAAK;AAAA,YAC9C,aAAY;AAAA,YACZ,cAAa;AAAA,YACb,WAAS;AAAA,YACT,KAAK;AAAA,YACL,cAAW;AAAA,YACX,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,QAEX,cACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS,MAAM;;AACb,6BAAe,EAAE;AACjB,eAAAA,MAAA,UAAU,YAAV,gBAAAA,IAAmB;AAAA,YACrB;AAAA,YACA,WAAU;AAAA,YAEV,UAAA,oBAAC,GAAA,EAAE,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA,IAGxB,oBAAC,OAAA,EAAI,WAAU,8JACZ,UAAA,OAAO,cAAc,eACtB,UAAU,SAAS,SAAS,KAAK,IAC7B,OACA,SAAA,CACN;AAAA,MAAA,EAAA,CAEJ,EAAA,CACF;AAAA,IAAA,GACF;AAAA,IAKC,eACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM,gBAAgB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,MAAM,eAAe,EAAE;AAAA,QAChC,iBAAiB,CAAC;AAAA,QAClB;AAAA,MAAA;AAAA,IAAA;AAAA,IAKH,CAAC,eACA,qBAAA,UAAA,EAEG,UAAA;AAAA,MAAA,SAAS,SAAS,KACjB,qBAAC,WAAA,EAAQ,WAAU,QACjB,UAAA;AAAA,QAAA,oBAAC,aAAA,EAAY,OAAM,aAAY,OAAM,YAAW;AAAA,4BAC/C,OAAA,EAAI,WAAU,0DACZ,UAAA,SAAS,IAAI,CAAC,QACb;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YAEL,SAAS,MAAM,WAAW,GAAG;AAAA,YAC7B,OAAO,QAAQ,IAAI,WAAW;AAAA,YAC9B,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA;AAAA,kBACA,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEZ;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA,MAAM;AAAA,kBACN,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEZ,oBAAC,QAAA,EAAK,WAAU,0CACb,cAAI,YAAA,CACP;AAAA,YAAA;AAAA,UAAA;AAAA,UAjBK,IAAI;AAAA,QAAA,CAmBZ,EAAA,CACH;AAAA,MAAA,GACF;AAAA,MAID,MAAM,SAAS,KACd,qBAAC,WAAA,EAAQ,WAAU,QACjB,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO,GAAG,MAAM,MAAM;AAAA,UAAA;AAAA,QAAA;AAAA,4BAEvB,OAAA,EAAI,WAAU,uBACZ,UAAA,MAAM,IAAI,CAAC,QACV;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC;AAAA,YACA;AAAA,YACA;AAAA,YACA,SAAO;AAAA,UAAA;AAAA,UAJF,IAAI;AAAA,QAAA,CAMZ,EAAA,CACH;AAAA,MAAA,GACF;AAAA,MAKF,oBAAC,WAAA,EAAQ,WAAU,QAChB,UAAA,aAAa,IAAI,CAAC,CAAC,KAAK,IAAI,MAAA;;AAC3B;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,OAAO,UAAU,KAAK,iBAAiB,+BAAO,aAAa;AAAA,YAC3D,OAAMA,MAAA,+BAAO,UAAP,gBAAAA,IAAe;AAAA,YACrB,MAAM;AAAA,YACN;AAAA,YACA;AAAA,UAAA;AAAA,UALK;AAAA,QAAA;AAAA,OAOR,EAAA,CACH;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
|
-
import { useRef, useEffect } from "react";
|
|
3
|
+
import { useRef, useLayoutEffect, useEffect } from "react";
|
|
4
4
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
5
5
|
import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
|
|
6
6
|
import { AppDetails } from "../detail/AppDetails.js";
|
|
@@ -13,6 +13,21 @@ function AppDetailPanel({
|
|
|
13
13
|
}) {
|
|
14
14
|
const { approvalMethods } = useAppCatalogContext();
|
|
15
15
|
const dialogRef = useRef(null);
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const card = dialogRef.current;
|
|
18
|
+
const scroller = card == null ? void 0 : card.parentElement;
|
|
19
|
+
if (!card || !scroller) return;
|
|
20
|
+
const pinCentre = () => {
|
|
21
|
+
card.style.marginTop = "0px";
|
|
22
|
+
const style = getComputedStyle(scroller);
|
|
23
|
+
const usable = scroller.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
24
|
+
const slack = usable - card.offsetHeight;
|
|
25
|
+
card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`;
|
|
26
|
+
};
|
|
27
|
+
pinCentre();
|
|
28
|
+
window.addEventListener("resize", pinCentre);
|
|
29
|
+
return () => window.removeEventListener("resize", pinCentre);
|
|
30
|
+
}, [app.slug, subResource == null ? void 0 : subResource.slug]);
|
|
16
31
|
const shownSlug = (subResource == null ? void 0 : subResource.slug) ?? app.slug;
|
|
17
32
|
useEffect(() => {
|
|
18
33
|
const dialog = dialogRef.current;
|
|
@@ -36,7 +51,7 @@ function AppDetailPanel({
|
|
|
36
51
|
"aria-modal": "true",
|
|
37
52
|
"aria-label": `${(subResource == null ? void 0 : subResource.displayName) ?? app.displayName} details`,
|
|
38
53
|
tabIndex: -1,
|
|
39
|
-
className: "relative w-full max-w-[min(1120px,94vw)]
|
|
54
|
+
className: "relative w-full max-w-[min(1120px,94vw)] rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none",
|
|
40
55
|
children: [
|
|
41
56
|
/* @__PURE__ */ jsx(
|
|
42
57
|
"button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useEffect, useRef } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods } = useAppCatalogContext()\n const dialogRef = useRef<HTMLDivElement>(null)\n\n // Focus the dialog on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n useEffect(() => {\n const dialog = dialogRef.current\n if (dialog && !dialog.contains(document.activeElement)) dialog.focus()\n }, [shownSlug])\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n className=\"relative w-full max-w-[min(1120px,94vw)]
|
|
1
|
+
{"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods } = useAppCatalogContext()\n const dialogRef = useRef<HTMLDivElement>(null)\n\n /**\n * Centre the card ONCE when it opens, then hold that position.\n *\n * `my-auto` centred it on every render, so each panel height change pushed both\n * edges and half the delta became upward movement of everything above it —\n * including the tab strip, which moved 113px and took the control the user had\n * just clicked with it (CLS 0.19, \"needs improvement\").\n *\n * Top-anchoring fixed the shift but looked wrong: a short card sat high on the\n * page. So pin the centred offset instead. The card still opens centred, and\n * anything that grows it afterwards extends downward only — which is what a\n * reader expects when they open a tab with more in it.\n *\n * Only on open and on resize. Deliberately NOT on content change: reacting to\n * content is precisely the bug.\n */\n useLayoutEffect(() => {\n const card = dialogRef.current\n const scroller = card?.parentElement\n if (!card || !scroller) return\n\n const pinCentre = () => {\n card.style.marginTop = '0px'\n const style = getComputedStyle(scroller)\n const usable =\n scroller.clientHeight -\n parseFloat(style.paddingTop) -\n parseFloat(style.paddingBottom)\n const slack = usable - card.offsetHeight\n // Negative slack means the card is already taller than the viewport; leave it\n // at the top and let the overlay scroll, rather than pushing its head off.\n card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`\n }\n\n pinCentre()\n window.addEventListener('resize', pinCentre)\n return () => window.removeEventListener('resize', pinCentre)\n // Keyed on the opened entry: a different card is a new open, and should\n // re-centre for its own height.\n }, [app.slug, subResource?.slug])\n\n // Focus the dialog on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n useEffect(() => {\n const dialog = dialogRef.current\n if (dialog && !dialog.contains(document.activeElement)) dialog.focus()\n }, [shownSlug])\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n // Centred on open, then held — see the pin effect above. `my-auto` is\n // deliberately absent: it re-centres on EVERY height change, which is the\n // layout-shift bug.\n className=\"relative w-full max-w-[min(1120px,94vw)] rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none\"\n >\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"absolute top-3 right-3 z-10 rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n {/* Keyed by the resource on screen, so the body remounts when the card\n navigates from one app to another (\"View replacement\", an access\n prerequisite's parent) instead of updating in place. Everything\n per-app inside it is component state with no reset of its own — the\n roles table's expansion, the icon/screenshot error flags, a draft\n source edit — and it used to arrive at the next app still carrying\n the previous one's, plus the previous scroll offset.\n\n The key is here and not on the outer dialog on purpose: that one\n would restart the open animation and re-run the mount-focus effect\n on every in-card navigation. */}\n <div\n key={shownSlug}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n />\n ) : (\n <AppDetails\n app={app}\n onAppClick={onAppClick}\n onClosePanel={onClose}\n />\n )}\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAsBO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,gBAAA,IAAoB,qBAAA;AAC5B,QAAM,YAAY,OAAuB,IAAI;AAkB7C,kBAAgB,MAAM;AACpB,UAAM,OAAO,UAAU;AACvB,UAAM,WAAW,6BAAM;AACvB,QAAI,CAAC,QAAQ,CAAC,SAAU;AAExB,UAAM,YAAY,MAAM;AACtB,WAAK,MAAM,YAAY;AACvB,YAAM,QAAQ,iBAAiB,QAAQ;AACvC,YAAM,SACJ,SAAS,eACT,WAAW,MAAM,UAAU,IAC3B,WAAW,MAAM,aAAa;AAChC,YAAM,QAAQ,SAAS,KAAK;AAG5B,WAAK,MAAM,YAAY,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC9D;AAEA,cAAA;AACA,WAAO,iBAAiB,UAAU,SAAS;AAC3C,WAAO,MAAM,OAAO,oBAAoB,UAAU,SAAS;AAAA,EAG7D,GAAG,CAAC,IAAI,MAAM,2CAAa,IAAI,CAAC;AAchC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAC3C,YAAU,MAAM;AACd,UAAM,SAAS,UAAU;AACzB,QAAI,UAAU,CAAC,OAAO,SAAS,SAAS,aAAa,UAAU,MAAA;AAAA,EACjE,GAAG,CAAC,SAAS,CAAC;AAEd,SACE,qBAAC,OAAA,EAAI,WAAU,yFAEb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,SAAS;AAAA,QACT,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAMZ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,QAC1D,UAAU;AAAA,QAIV,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAW;AAAA,cACX,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA,oBAAC,GAAA,EAAE,WAAU,SAAA,CAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAaxB;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cAET,UAAA,cACC;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA,QAAQ;AAAA,kBACR;AAAA,kBACA,QAAQ,kBAAkB;AAAA,gBAAA;AAAA,cAAA,IAG5B;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA;AAAA,kBACA,cAAc;AAAA,gBAAA;AAAA,cAAA;AAAA,YAChB;AAAA,YAfG;AAAA,UAAA;AAAA,QAiBP;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface RequestAppDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
/** What they searched for. Pre-fills the name, because they just typed it. */
|
|
5
|
+
initialSubject?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Mounts nothing until it is opened, which is what keeps `useTRPC()` out of the
|
|
9
|
+
* closed path. The catalog grid is rendered bare in several tests with no
|
|
10
|
+
* TRPCProvider, and a hook cannot be called conditionally — so the gate has to be a
|
|
11
|
+
* component boundary rather than an `if` inside one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function RequestAppDialog(props: RequestAppDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
export {};
|