@igstack/app-catalog-frontend-core 2.0.4 → 3.0.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/utils/areaGrouping.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/pwa/PwaAutoUpdateController.test.d.ts +1 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.d.ts +6 -3
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +112 -32
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +0 -5
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/areaGrouping.d.ts +22 -0
- package/dist/esm/modules/appCatalog/utils/areaGrouping.js +46 -0
- package/dist/esm/modules/appCatalog/utils/areaGrouping.js.map +1 -0
- package/dist/esm/modules/pwa/PwaAutoUpdateController.d.ts +8 -0
- package/dist/esm/modules/pwa/PwaAutoUpdateController.js +15 -1
- package/dist/esm/modules/pwa/PwaAutoUpdateController.js.map +1 -1
- package/dist/esm/types/uiSettings.d.ts +28 -0
- package/dist/esm/ui/components/header/Header.js +1 -0
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/modules/appCatalog/utils/areaGrouping.test.ts +83 -0
- package/src/__tests__/modules/pwa/PwaAutoUpdateController.test.ts +61 -0
- package/src/index.tsx +2 -1
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +159 -34
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +0 -10
- package/src/modules/appCatalog/utils/areaGrouping.ts +85 -0
- package/src/modules/pwa/PwaAutoUpdateController.ts +19 -3
- package/src/types/uiSettings.ts +30 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { appPropsFactory } from './appPropsFactory.js';
|
|
2
2
|
export { App } from './App.js';
|
|
3
3
|
export type { AppProps } from './App.js';
|
|
4
|
-
export type { UiSettings } from './types/uiSettings.js';
|
|
4
|
+
export type { AreaIcon, AreasSettings, UiSettings } from './types/uiSettings.js';
|
|
5
|
+
export { DAY_TO_DAY_AREA_KEY } from './modules/appCatalog/utils/areaGrouping.js';
|
|
5
6
|
export { PwaAutoUpdateProvider, usePwaAutoUpdate } from './modules/pwa.js';
|
|
6
7
|
export type { PwaAutoUpdateOptions, PwaUpdateHandle } from './modules/pwa.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { appPropsFactory } from "./appPropsFactory.js";
|
|
2
2
|
import { App } from "./App.js";
|
|
3
|
+
import { DAY_TO_DAY_AREA_KEY } from "./modules/appCatalog/utils/areaGrouping.js";
|
|
3
4
|
import { PwaAutoUpdateProvider, usePwaAutoUpdate } from "./modules/pwa/PwaAutoUpdateContext.js";
|
|
4
5
|
export {
|
|
5
6
|
App,
|
|
7
|
+
DAY_TO_DAY_AREA_KEY,
|
|
6
8
|
PwaAutoUpdateProvider,
|
|
7
9
|
appPropsFactory,
|
|
8
10
|
usePwaAutoUpdate
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
|
@@ -9,6 +9,7 @@ import "tailwind-merge";
|
|
|
9
9
|
import "@tanstack/react-query-devtools";
|
|
10
10
|
import "next-themes";
|
|
11
11
|
import "../../../ui/button.js";
|
|
12
|
+
import "../../auth/AuthModalContext.js";
|
|
12
13
|
import "@radix-ui/react-dialog";
|
|
13
14
|
import "lucide-react";
|
|
14
15
|
import "../../config/GlobalConfigContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,SACR,6BAAM,aAAY,CAAA;AAAA,QAClB,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
|
@@ -15,8 +15,11 @@ export interface AppCatalogGridProps {
|
|
|
15
15
|
/** Opens a matched sub-resource: its parent's detail, singled out to it. */
|
|
16
16
|
onSubClick: (parentSlug: string, subSlug: string) => void;
|
|
17
17
|
onLaunch: (app: Resource) => void;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Unused — it labelled the removed "Browse all" header. Still
|
|
20
|
+
* accepted so existing callers keep compiling; drop it on the next major.
|
|
21
|
+
*/
|
|
22
|
+
totalCount?: number;
|
|
20
23
|
/**
|
|
21
24
|
* True while an app/sub-resource detail overlay is open above the launcher.
|
|
22
25
|
* The results list then stops binding ↑↓/↵/Esc so the overlay owns the
|
|
@@ -26,4 +29,4 @@ export interface AppCatalogGridProps {
|
|
|
26
29
|
/** Currently open sub-resource slug, for highlighting its row. */
|
|
27
30
|
selectedSubSlug?: string;
|
|
28
31
|
}
|
|
29
|
-
export declare function AppCatalogGrid({ apps, allResources, searchValue, onSearchChange, onAppClick, onSubClick, onLaunch,
|
|
32
|
+
export declare function AppCatalogGrid({ apps, allResources, searchValue, onSearchChange, onAppClick, onSubClick, onLaunch, detailOpen, selectedSubSlug, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Search, X, ArrowUpRight } from "lucide-react";
|
|
3
|
-
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
3
|
+
import { useState, use, useEffect, useMemo, useRef, useCallback } from "react";
|
|
4
4
|
import { cn } from "../../../../lib/utils.js";
|
|
5
5
|
import { useAppClickHistory } from "../../hooks/useAppClickHistory.js";
|
|
6
6
|
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
@@ -10,6 +10,9 @@ import { ResourceIcon } from "./ResourceIcon.js";
|
|
|
10
10
|
import { searchResources } from "@igstack/app-catalog-shared-core";
|
|
11
11
|
import { Highlight } from "../components/Highlight.js";
|
|
12
12
|
import { displayUrl } from "../../utils/displayUrl.js";
|
|
13
|
+
import { useUiSettings } from "../../../../context/UiSettingsContext.js";
|
|
14
|
+
import { AppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
15
|
+
import { groupByArea, areaLabel } from "../../utils/areaGrouping.js";
|
|
13
16
|
const typeLabel = (t) => {
|
|
14
17
|
if (!t || t === "application") return null;
|
|
15
18
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
@@ -83,6 +86,88 @@ function ResourceRow({
|
|
|
83
86
|
}
|
|
84
87
|
);
|
|
85
88
|
}
|
|
89
|
+
function AppCard({
|
|
90
|
+
app,
|
|
91
|
+
onAppClick,
|
|
92
|
+
onLaunch
|
|
93
|
+
}) {
|
|
94
|
+
return /* @__PURE__ */ jsx("div", { className: "relative h-[92px]", children: /* @__PURE__ */ jsxs(
|
|
95
|
+
"button",
|
|
96
|
+
{
|
|
97
|
+
type: "button",
|
|
98
|
+
onClick: () => onAppClick(app),
|
|
99
|
+
title: `View ${app.displayName}`,
|
|
100
|
+
className: cn(
|
|
101
|
+
"group absolute inset-x-0 top-0 min-h-full max-h-full overflow-hidden",
|
|
102
|
+
"flex gap-3.5 text-left bg-card border border-border",
|
|
103
|
+
"rounded-[var(--radius)] p-[15px] pr-11 outline-none",
|
|
104
|
+
"transition-[max-height,box-shadow,border-color] duration-200",
|
|
105
|
+
"hover:max-h-96 hover:border-ring hover:shadow-lg hover:z-10",
|
|
106
|
+
"focus-visible:max-h-96 focus-visible:border-ring focus-visible:shadow-lg focus-visible:z-10",
|
|
107
|
+
"motion-reduce:transition-none"
|
|
108
|
+
),
|
|
109
|
+
children: [
|
|
110
|
+
/* @__PURE__ */ jsx(
|
|
111
|
+
LaunchButton,
|
|
112
|
+
{
|
|
113
|
+
app,
|
|
114
|
+
onLaunch,
|
|
115
|
+
className: "absolute top-3 right-3 size-[26px] opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(ResourceIcon, { app, size: 40 }) }),
|
|
119
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
|
|
120
|
+
/* @__PURE__ */ jsx("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", children: app.displayName }),
|
|
121
|
+
app.description && /* @__PURE__ */ jsx(
|
|
122
|
+
"span",
|
|
123
|
+
{
|
|
124
|
+
className: cn(
|
|
125
|
+
"block shrink-0 text-[12.5px] leading-snug text-muted-foreground",
|
|
126
|
+
"line-clamp-2 group-hover:line-clamp-none group-focus-visible:line-clamp-none"
|
|
127
|
+
),
|
|
128
|
+
children: markdownToPlainText(app.description)
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
] })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
) });
|
|
135
|
+
}
|
|
136
|
+
function AreaSection({
|
|
137
|
+
label,
|
|
138
|
+
Icon,
|
|
139
|
+
apps,
|
|
140
|
+
onAppClick,
|
|
141
|
+
onLaunch
|
|
142
|
+
}) {
|
|
143
|
+
return /* @__PURE__ */ jsxs("section", { className: "mt-11", children: [
|
|
144
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 mb-4", children: [
|
|
145
|
+
Icon ? /* @__PURE__ */ jsx(
|
|
146
|
+
Icon,
|
|
147
|
+
{
|
|
148
|
+
size: 26,
|
|
149
|
+
strokeWidth: 1.75,
|
|
150
|
+
className: "text-primary shrink-0"
|
|
151
|
+
}
|
|
152
|
+
) : /* @__PURE__ */ jsx("div", { className: "h-[3px] w-11 rounded-full bg-primary" }),
|
|
153
|
+
/* @__PURE__ */ jsx("h3", { className: "font-serif font-semibold text-[27px] leading-tight tracking-tight m-0", children: label }),
|
|
154
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[13px] text-muted-foreground", children: [
|
|
155
|
+
apps.length,
|
|
156
|
+
" ",
|
|
157
|
+
apps.length === 1 ? "tool" : "tools"
|
|
158
|
+
] })
|
|
159
|
+
] }),
|
|
160
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-3 grid-cols-1 md:grid-cols-2 xl:grid-cols-3", children: apps.map((app) => /* @__PURE__ */ jsx(
|
|
161
|
+
AppCard,
|
|
162
|
+
{
|
|
163
|
+
app,
|
|
164
|
+
onAppClick,
|
|
165
|
+
onLaunch
|
|
166
|
+
},
|
|
167
|
+
app.slug
|
|
168
|
+
)) })
|
|
169
|
+
] });
|
|
170
|
+
}
|
|
86
171
|
const COLLAPSED_SUB_LIMIT = 5;
|
|
87
172
|
function SearchResultsList({
|
|
88
173
|
apps,
|
|
@@ -314,12 +399,14 @@ function AppCatalogGrid({
|
|
|
314
399
|
onAppClick,
|
|
315
400
|
onSubClick,
|
|
316
401
|
onLaunch,
|
|
317
|
-
totalCount,
|
|
318
402
|
detailOpen = false,
|
|
319
403
|
selectedSubSlug
|
|
320
404
|
}) {
|
|
405
|
+
var _a;
|
|
321
406
|
const { getTopApps } = useAppClickHistory();
|
|
322
407
|
const [topSlugs, setTopSlugs] = useState([]);
|
|
408
|
+
const { areas } = useUiSettings();
|
|
409
|
+
const tagsDefinitions = (_a = use(AppCatalogContext)) == null ? void 0 : _a.tagsDefinitions;
|
|
323
410
|
useEffect(() => {
|
|
324
411
|
void getTopApps(5).then(setTopSlugs);
|
|
325
412
|
}, [getTopApps]);
|
|
@@ -337,16 +424,18 @@ function AppCatalogGrid({
|
|
|
337
424
|
const promoted = new Set(frequent.map((a) => a.slug));
|
|
338
425
|
return [...apps].filter((a) => !promoted.has(a.slug)).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
339
426
|
}, [apps, frequent]);
|
|
340
|
-
const
|
|
341
|
-
|
|
427
|
+
const browseGroups = useMemo(
|
|
428
|
+
() => groupByArea(browse, areas == null ? void 0 : areas.dayToDayCategories),
|
|
429
|
+
[browse, areas == null ? void 0 : areas.dayToDayCategories]
|
|
430
|
+
);
|
|
342
431
|
const isSearching = searchValue.trim() !== "";
|
|
343
432
|
const searchRef = useRef(null);
|
|
344
433
|
useEffect(() => {
|
|
345
434
|
const onKey = (e) => {
|
|
346
|
-
var
|
|
435
|
+
var _a2;
|
|
347
436
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
348
437
|
e.preventDefault();
|
|
349
|
-
(
|
|
438
|
+
(_a2 = searchRef.current) == null ? void 0 : _a2.focus();
|
|
350
439
|
}
|
|
351
440
|
};
|
|
352
441
|
document.addEventListener("keydown", onKey);
|
|
@@ -364,7 +453,7 @@ function AppCatalogGrid({
|
|
|
364
453
|
type: "text",
|
|
365
454
|
value: searchValue,
|
|
366
455
|
onChange: (e) => onSearchChange(e.target.value),
|
|
367
|
-
placeholder: "Search apps,
|
|
456
|
+
placeholder: "Search apps, cloud accounts, or keywords",
|
|
368
457
|
autoComplete: "off",
|
|
369
458
|
autoFocus: true,
|
|
370
459
|
ref: searchRef,
|
|
@@ -378,9 +467,9 @@ function AppCatalogGrid({
|
|
|
378
467
|
type: "button",
|
|
379
468
|
"aria-label": "Clear search",
|
|
380
469
|
onClick: () => {
|
|
381
|
-
var
|
|
470
|
+
var _a2;
|
|
382
471
|
onSearchChange("");
|
|
383
|
-
(
|
|
472
|
+
(_a2 = searchRef.current) == null ? void 0 : _a2.focus();
|
|
384
473
|
},
|
|
385
474
|
className: "shrink-0 rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
386
475
|
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
@@ -453,29 +542,20 @@ function AppCatalogGrid({
|
|
|
453
542
|
app.slug
|
|
454
543
|
)) })
|
|
455
544
|
] }),
|
|
456
|
-
/* @__PURE__ */
|
|
457
|
-
|
|
458
|
-
/* @__PURE__ */
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
{
|
|
471
|
-
app,
|
|
472
|
-
onAppClick,
|
|
473
|
-
onLaunch
|
|
474
|
-
},
|
|
475
|
-
app.slug
|
|
476
|
-
)) })
|
|
477
|
-
] })
|
|
478
|
-
] }),
|
|
545
|
+
/* @__PURE__ */ jsx("section", { className: "mt-9", children: browseGroups.map(([key, list]) => {
|
|
546
|
+
var _a2;
|
|
547
|
+
return /* @__PURE__ */ jsx(
|
|
548
|
+
AreaSection,
|
|
549
|
+
{
|
|
550
|
+
label: areaLabel(key, tagsDefinitions, areas == null ? void 0 : areas.dayToDayLabel),
|
|
551
|
+
Icon: (_a2 = areas == null ? void 0 : areas.icons) == null ? void 0 : _a2[key],
|
|
552
|
+
apps: list,
|
|
553
|
+
onAppClick,
|
|
554
|
+
onLaunch
|
|
555
|
+
},
|
|
556
|
+
key
|
|
557
|
+
);
|
|
558
|
+
}) }),
|
|
479
559
|
/* @__PURE__ */ jsx(AttributionFooter, {})
|
|
480
560
|
] })
|
|
481
561
|
] });
|
|
@@ -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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { pickNewThisWeek } from '../../utils/launcherHelpers'\nimport { AttributionFooter } from './AttributionFooter'\nimport { ResourceIcon } from './ResourceIcon'\nimport { searchResources } from '@igstack/app-catalog-shared-core'\nimport { Highlight } from '../components/Highlight'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\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 → Browse all\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 /** Total resource count for the \"Browse all\" label. */\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 \"Browse all\". */\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/** 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, Esc clears the 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 const [focusedIndex, setFocusedIndex] = useState(-1)\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 // Reset focus and collapse expanded sub-resource lists when the query changes\n useEffect(() => {\n setFocusedIndex(-1)\n setExpandedParents(new Set())\n }, [searchValue])\n\n useEffect(() => {\n // While a detail overlay is open it owns the keyboard: without this guard\n // Esc would clear the search behind the overlay and ↑↓/↵ would navigate to\n // a different app from a list the user cannot see.\n if (!keyboardEnabled) return\n const onKey = (e: KeyboardEvent) => {\n if (rows.length === 0) return\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n setFocusedIndex((i) => Math.min(i + 1, rows.length - 1))\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n setFocusedIndex((i) => Math.max(i - 1, 0))\n } else if (e.key === 'Enter') {\n const row = focusedIndex >= 0 ? rows[focusedIndex] : undefined\n if (row) activateRow(row)\n } else if (e.key === 'Escape') {\n onClear()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [rows, focusedIndex, activateRow, onClear, keyboardEnabled])\n\n return (\n <div className=\"max-w-[620px] mx-auto mt-3\">\n {/* Meta line. Counts matched sub-resources too — they are results in\n their own right, and a parent surfaced only via 47 matching accounts\n used to be reported as \"1 result\". Collapsed ones still count, so the\n number doesn't jump when the \"... N more\" row is expanded. */}\n <div className=\"text-[13px] text-muted-foreground mb-2 px-1\">\n {matchCount === 0\n ? `No results for \"${searchValue}\"`\n : `${matchCount} result${matchCount === 1 ? '' : 's'}`}\n </div>\n\n {/* #11: deprecated-only fallback notice */}\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"text-[13px] text-muted-foreground mb-2 px-1\"\n >\n No active apps for "{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 totalCount,\n detailOpen = false,\n selectedSubSlug,\n}: AppCatalogGridProps) {\n const { getTopApps } = useAppClickHistory()\n const [topSlugs, setTopSlugs] = useState<string[]>([])\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 // Browse all: 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 const browseLeft = browse.filter((_, i) => i % 2 === 0)\n const browseRight = browse.filter((_, i) => i % 2 === 1)\n\n const isSearching = searchValue.trim() !== ''\n const searchRef = useRef<HTMLInputElement>(null)\n\n // ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault()\n searchRef.current?.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [])\n\n return (\n <div className=\"mx-auto w-full max-w-[1000px] pb-24\">\n {/* hero */}\n <div className=\"text-center pt-10 pb-1.5\">\n <h1 className=\"font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5\">\n What do you need to get into?\n </h1>\n <p className=\"text-muted-foreground m-0 mb-6 text-[15px]\">\n Find any tool, see how to request access, or jump straight in.\n </p>\n <div className=\"max-w-[620px] mx-auto\">\n <div className=\"flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors\">\n <Search className=\"size-5 shrink-0 text-muted-foreground\" />\n <input\n type=\"text\"\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n placeholder=\"Search apps, databases, cloud accounts, account IDs…\"\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 {/* Browse all */}\n <section className=\"mt-9\">\n <SectionHead title=\"Browse all\" count={`${totalCount} resources`} />\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-2\">\n <div className=\"flex flex-col gap-2\">\n {browseLeft.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n <div className=\"flex flex-col gap-2\">\n {browseRight.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n </div>\n </section>\n\n <AttributionFooter />\n </>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowUpRight, Search, X } from 'lucide-react'\nimport { use, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { pickNewThisWeek } from '../../utils/launcherHelpers'\nimport { AttributionFooter } from './AttributionFooter'\nimport { ResourceIcon } from './ResourceIcon'\nimport { searchResources } from '@igstack/app-catalog-shared-core'\nimport { Highlight } from '../components/Highlight'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport type { AreaIcon } from '~/types/uiSettings'\nimport { AppCatalogContext } from '../../context/AppCatalogContext'\nimport { areaLabel, groupByArea } from '../../utils/areaGrouping'\n\n/**\n * Adaptive-home discovery spine (issue #38, increment 1) — matches the\n * option-a-launcher prototype:\n * Your apps (frequent) → New this week → all areas, grouped\n *\n * This is the PERSISTENT shell for the catalog: idle browse, active search and\n * an open detail overlay all render inside it. Only the area below the hero\n * swaps (discovery spine ↔ results list), so the hero copy, the search box and\n * the page container never move while the user types. Do not switch the page to\n * a different container on search — that was the layout-jitter bug.\n */\n\n// Type pill text. Plain \"application\" resources are the common case and don't\n// need a badge (the user asked not to show a bare \"App\" badge); only surface a\n// pill for distinctive types (database, cloud, service, …).\nconst typeLabel = (t?: string): string | null => {\n if (!t || t === 'application') return null\n return t.charAt(0).toUpperCase() + t.slice(1)\n}\n\nexport interface AppCatalogGridProps {\n /** All root (non-child) resources, already filtered for deprecated per settings. */\n apps: Resource[]\n /**\n * All resources INCLUDING children/sub-resources. Passed to search so a query\n * matching a sub-resource (e.g. an AWS account, an alias/account ID) surfaces\n * its parent — preserving the existing cross-sub-resource search behavior.\n * Falls back to `apps` when omitted.\n */\n allResources?: Resource[]\n searchValue: string\n onSearchChange: (v: string) => void\n onAppClick: (app: Resource) => void\n /** Opens a matched sub-resource: its parent's detail, singled out to it. */\n onSubClick: (parentSlug: string, subSlug: string) => void\n onLaunch: (app: Resource) => void\n /**\n * @deprecated Unused — it labelled the removed \"Browse all\" header. Still\n * accepted so existing callers keep compiling; drop it on the next major.\n */\n totalCount?: number\n /**\n * True while an app/sub-resource detail overlay is open above the launcher.\n * The results list then stops binding ↑↓/↵/Esc so the overlay owns the\n * keyboard (otherwise Esc would ALSO wipe the search behind the overlay).\n */\n detailOpen?: boolean\n /** Currently open sub-resource slug, for highlighting its row. */\n selectedSubSlug?: string\n}\n\nfunction LaunchButton({\n app,\n onLaunch,\n className,\n}: {\n app: Resource\n onLaunch: (app: Resource) => void\n className?: string\n}) {\n if (!app.appUrl) return null\n // Show the destination URL on hover (user ask) — the native title tooltip\n // reveals where the ↗ jumps to, e.g. \"Open → console.aws.amazon.com\".\n const prettyUrl = displayUrl(app.appUrl)\n return (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={`Open → ${prettyUrl}`}\n aria-label={`Open ${app.displayName} in a new tab (${prettyUrl})`}\n onClick={(e) => {\n e.stopPropagation()\n onLaunch(app)\n }}\n className={cn(\n 'grid place-items-center rounded-full border border-border bg-card text-muted-foreground',\n 'hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors',\n className,\n )}\n >\n <ArrowUpRight className=\"size-3.5\" />\n </a>\n )\n}\n\nfunction SectionHead({\n title,\n count,\n action,\n}: {\n title: string\n count?: string\n action?: React.ReactNode\n}) {\n return (\n <div className=\"flex items-center gap-2.5 mb-4\">\n <h2 className=\"font-serif font-semibold text-[15px] tracking-tight m-0\">\n {title}\n </h2>\n {count && (\n <span className=\"text-[13px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5\">\n {count}\n </span>\n )}\n {action && <div className=\"ml-auto\">{action}</div>}\n </div>\n )\n}\n\n/** Small row used by \"New this week\" and the search results. */\nfunction ResourceRow({\n app,\n onAppClick,\n onLaunch,\n showNew = false,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n showNew?: boolean\n}) {\n return (\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border border-border rounded-[var(--radius)] px-3.5 py-3',\n 'hover:border-ring hover:shadow-sm transition-all',\n )}\n >\n <ResourceIcon app={app} size={38} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14.5px] font-semibold truncate\">\n {app.displayName}\n </span>\n {app.description && (\n <span className=\"block text-[13px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {showNew && (\n <span className=\"text-[11px] font-bold text-[#65a443] bg-[#65a443]/12 rounded-full px-2.5 py-0.5\">\n New\n </span>\n )}\n {typeLabel(app.type) && (\n <span className=\"text-[12px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap\">\n {typeLabel(app.type)}\n </span>\n )}\n <LaunchButton app={app} onLaunch={onLaunch} className=\"size-8\" />\n </button>\n )\n}\n\n/**\n * Wide area card: icon on the left, name and description beside it.\n *\n * The grid cell keeps a fixed footprint and the visible panel is absolute inside\n * it, so a card growing on hover OVERLAYS the row below instead of reflowing the\n * whole grid. Hover reveals the rest of the description and nothing else — an\n * earlier version staggered a delayed tag block in behind the growth, which read\n * as jitter. Keyboard focus expands the same way, and `motion-reduce` drops the\n * animation.\n *\n * The footprint is a fixed height rather than an aspect ratio: at two or three\n * columns a ratio makes the card grow taller as the window widens, and the\n * description — three lines at most — leaves the bottom half empty.\n */\nfunction AppCard({\n app,\n onAppClick,\n onLaunch,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <div className=\"relative h-[92px]\">\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group absolute inset-x-0 top-0 min-h-full max-h-full overflow-hidden',\n 'flex gap-3.5 text-left bg-card border border-border',\n 'rounded-[var(--radius)] p-[15px] pr-11 outline-none',\n 'transition-[max-height,box-shadow,border-color] duration-200',\n 'hover:max-h-96 hover:border-ring hover:shadow-lg hover:z-10',\n 'focus-visible:max-h-96 focus-visible:border-ring focus-visible:shadow-lg focus-visible:z-10',\n 'motion-reduce:transition-none',\n )}\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-3 right-3 size-[26px] opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100\"\n />\n <div className=\"shrink-0\">\n <ResourceIcon app={app} size={40} />\n </div>\n {/* min-w-0 so a long unbroken word truncates instead of pushing the\n column wider than the card. */}\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n {/* shrink-0: while max-height animates, the un-clamped description\n overflows the panel for a few frames. Without it flexbox squeezes\n this line to 0 and back, which reads as the title flickering. */}\n <span className=\"block shrink-0 text-[14.5px] font-bold leading-tight line-clamp-1 group-hover:line-clamp-2 group-focus-visible:line-clamp-2\">\n {app.displayName}\n </span>\n {app.description && (\n <span\n className={cn(\n 'block shrink-0 text-[12.5px] leading-snug text-muted-foreground',\n 'line-clamp-2 group-hover:line-clamp-none group-focus-visible:line-clamp-none',\n )}\n >\n {markdownToPlainText(app.description)}\n </span>\n )}\n </div>\n </button>\n </div>\n )\n}\n\n/**\n * One area: icon, big title, count, then its cards. The icon stands in for the\n * accent bar the header used to carry — it does the same colour job and also\n * says which area you are in. It is decorative: the `h3` carries the name.\n */\nfunction AreaSection({\n label,\n Icon,\n apps,\n onAppClick,\n onLaunch,\n}: {\n label: string\n Icon?: AreaIcon\n apps: Resource[]\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n}) {\n return (\n <section className=\"mt-11\">\n <div className=\"flex items-center gap-2.5 mb-4\">\n {Icon ? (\n <Icon\n size={26}\n strokeWidth={1.75}\n className=\"text-primary shrink-0\"\n />\n ) : (\n <div className=\"h-[3px] w-11 rounded-full bg-primary\" />\n )}\n <h3 className=\"font-serif font-semibold text-[27px] leading-tight tracking-tight m-0\">\n {label}\n </h3>\n <span className=\"text-[13px] text-muted-foreground\">\n {apps.length} {apps.length === 1 ? 'tool' : 'tools'}\n </span>\n </div>\n <div className=\"grid gap-3 grid-cols-1 md:grid-cols-2 xl:grid-cols-3\">\n {apps.map((app) => (\n <AppCard\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n </section>\n )\n}\n\n/** Matched sub-resources listed under a parent before the \"… N more\" row. */\nconst COLLAPSED_SUB_LIMIT = 5\n\n/** One line in the flat, keyboard-navigable results list. */\ntype ResultRow =\n | { kind: 'app'; key: string; app: Resource }\n | { kind: 'sub'; key: string; sub: Resource; parent: Resource }\n | { kind: 'more'; key: string; parent: Resource; hidden: number }\n\n/**\n * Keyboard-navigable search results list — the launcher's only search surface.\n *\n * It renders directly BELOW the hero search box, inside the same persistent\n * launcher shell: the hero copy, the search input and the page container never\n * move when the user types, and no filter chrome (tabs / category / deprecated\n * toggle) appears. Those filters live in the browse view only — clearing the\n * search brings them back.\n *\n * Rows are flat and individually clickable. A matched sub-resource gets its own\n * indented row under its parent, so a query that only hits a sub-resource\n * (e.g. a cloud account) can be opened directly instead of forcing the user to\n * dig through the parent's detail. ↑↓ moves over every row, ↵ activates the\n * focused one, Esc clears the 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 const [focusedIndex, setFocusedIndex] = useState(-1)\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 // Reset focus and collapse expanded sub-resource lists when the query changes\n useEffect(() => {\n setFocusedIndex(-1)\n setExpandedParents(new Set())\n }, [searchValue])\n\n useEffect(() => {\n // While a detail overlay is open it owns the keyboard: without this guard\n // Esc would clear the search behind the overlay and ↑↓/↵ would navigate to\n // a different app from a list the user cannot see.\n if (!keyboardEnabled) return\n const onKey = (e: KeyboardEvent) => {\n if (rows.length === 0) return\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n setFocusedIndex((i) => Math.min(i + 1, rows.length - 1))\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n setFocusedIndex((i) => Math.max(i - 1, 0))\n } else if (e.key === 'Enter') {\n const row = focusedIndex >= 0 ? rows[focusedIndex] : undefined\n if (row) activateRow(row)\n } else if (e.key === 'Escape') {\n onClear()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [rows, focusedIndex, activateRow, onClear, keyboardEnabled])\n\n return (\n <div className=\"max-w-[620px] mx-auto mt-3\">\n {/* Meta line. Counts matched sub-resources too — they are results in\n their own right, and a parent surfaced only via 47 matching accounts\n used to be reported as \"1 result\". Collapsed ones still count, so the\n number doesn't jump when the \"... N more\" row is expanded. */}\n <div className=\"text-[13px] text-muted-foreground mb-2 px-1\">\n {matchCount === 0\n ? `No results for \"${searchValue}\"`\n : `${matchCount} result${matchCount === 1 ? '' : 's'}`}\n </div>\n\n {/* #11: deprecated-only fallback notice */}\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"text-[13px] text-muted-foreground mb-2 px-1\"\n >\n No active apps for "{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(\n () => groupByArea(browse, areas?.dayToDayCategories),\n [browse, areas?.dayToDayCategories],\n )\n\n const isSearching = searchValue.trim() !== ''\n const searchRef = useRef<HTMLInputElement>(null)\n\n // ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault()\n searchRef.current?.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [])\n\n return (\n <div className=\"mx-auto w-full max-w-[1000px] pb-24\">\n {/* hero */}\n <div className=\"text-center pt-10 pb-1.5\">\n <h1 className=\"font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5\">\n What do you need to get into?\n </h1>\n <p className=\"text-muted-foreground m-0 mb-6 text-[15px]\">\n Find any tool, see how to request access, or jump straight in.\n </p>\n <div className=\"max-w-[620px] mx-auto\">\n <div className=\"flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors\">\n <Search className=\"size-5 shrink-0 text-muted-foreground\" />\n <input\n type=\"text\"\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n placeholder=\"Search apps, cloud accounts, or keywords\"\n autoComplete=\"off\"\n autoFocus\n ref={searchRef}\n aria-label=\"Search apps\"\n className=\"flex-1 bg-transparent border-0 outline-none text-[16px] text-foreground placeholder:text-muted-foreground\"\n />\n {isSearching ? (\n <button\n type=\"button\"\n aria-label=\"Clear search\"\n onClick={() => {\n onSearchChange('')\n searchRef.current?.focus()\n }}\n className=\"shrink-0 rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-[11px] text-muted-foreground/60 border border-muted-foreground/20 rounded px-1.5 py-0.5 font-mono shrink-0\">\n {typeof navigator !== 'undefined' &&\n navigator.platform.includes('Mac')\n ? '⌘K'\n : 'Ctrl+K'}\n </kbd>\n )}\n </div>\n </div>\n </div>\n\n {/* Search results: rendered in place, directly under the (unmoved) hero\n search box. The discovery spine below is what gets replaced — the\n shell, hero and search box stay put, so typing never shifts layout. */}\n {isSearching && (\n <SearchResultsList\n apps={allResources ?? apps}\n searchValue={searchValue}\n onAppClick={onAppClick}\n onSubClick={onSubClick}\n onLaunch={onLaunch}\n onClear={() => onSearchChange('')}\n keyboardEnabled={!detailOpen}\n selectedSubSlug={selectedSubSlug}\n />\n )}\n\n {/* Discovery spine: only shown when not searching */}\n {!isSearching && (\n <>\n {/* Your apps */}\n {frequent.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead title=\"Your apps\" count=\"frequent\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3.5\">\n {frequent.map((app) => (\n <button\n type=\"button\"\n key={app.slug}\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className=\"group relative bg-card border border-border rounded-[var(--radius)] px-3.5 pt-[18px] pb-4 text-center shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all\"\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-2.5 right-2.5 size-[26px] opacity-0 group-hover:opacity-100\"\n />\n <ResourceIcon\n app={app}\n size={46}\n className=\"mx-auto mb-3\"\n />\n <span className=\"block text-[13.5px] font-bold truncate\">\n {app.displayName}\n </span>\n </button>\n ))}\n </div>\n </section>\n )}\n\n {/* New this week */}\n {fresh.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead\n title=\"New this week\"\n count={`${fresh.length} added`}\n />\n <div className=\"flex flex-col gap-2\">\n {fresh.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n showNew\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Everything else, grouped by area — the area titles carry it, so\n there is no \"Browse all\" header above them. */}\n <section className=\"mt-9\">\n {browseGroups.map(([key, list]) => (\n <AreaSection\n key={key}\n label={areaLabel(key, tagsDefinitions, areas?.dayToDayLabel)}\n Icon={areas?.icons?.[key]}\n apps={list}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </section>\n\n <AttributionFooter />\n </>\n )}\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@ import "@tanstack/react-query-devtools";
|
|
|
12
12
|
import "next-themes";
|
|
13
13
|
import "@tanstack/react-query";
|
|
14
14
|
import "../../../../ui/button.js";
|
|
15
|
+
import "../../../auth/AuthModalContext.js";
|
|
15
16
|
import "@radix-ui/react-dialog";
|
|
16
17
|
import "../../../config/GlobalConfigContext.js";
|
|
17
18
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import type { Group } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Mail, User, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport {\n getGroupBySlug,\n getPersonBySlug,\n} from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * Chip for a slug that may name EITHER a Person or a Group (owner, approvers).\n * Resolves against both collections; falls back to the raw slug so an unknown\n * slug is still visible rather than silently dropped.\n */\nexport function PersonOrGroupBadge({ slug }: PersonBadgeProps) {\n const { persons, groups } = useAppCatalogContext()\n\n if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />\n\n const group = getGroupBySlug(groups, slug)\n if (!group) {\n return (\n <Badge variant=\"outline\" className=\"font-normal\" title={slug}>\n {slug}\n </Badge>\n )\n }\n\n // No group email to copy → show each member as its own clickable/copyable\n // chip instead of one opaque group chip.\n if (!group.email && group.memberSlugs.length > 0) {\n return (\n <span className=\"inline-flex flex-wrap gap-1\">\n {group.memberSlugs.map((memberSlug) => (\n <PersonOrGroupBadge key={memberSlug} slug={memberSlug} />\n ))}\n </span>\n )\n }\n\n return <GroupBadge group={group} />\n}\n\n/** Group chip: name, optional email, and the member names in a popover. */\nfunction GroupBadge({ group }: { group: Group }) {\n const { persons } = useAppCatalogContext()\n const memberNames = group.memberSlugs.map((s) => {\n const p = getPersonBySlug(persons, s)\n return p ? `${p.firstName} ${p.lastName}`.trim() || s : s\n })\n // Many groups (e.g. aws-maintainers-*) have no real displayName set — it's\n // just their slug. Show the member names directly instead of the opaque\n // slug so approvers stay recognizable at a glance, without a popover click.\n const hasRealDisplayName =\n group.displayName && group.displayName !== group.slug\n const displayName = hasRealDisplayName\n ? group.displayName!\n : memberNames.length > 0\n ? memberNames.join(', ')\n : group.slug\n\n if (!group.email && memberNames.length === 0) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <Users className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button type=\"button\" className=\"inline-flex\" title={displayName}>\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <Users className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<Users className=\"size-3.5\" />}\n label=\"Group\"\n value={displayName}\n />\n {group.email && (\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={group.email}\n href={`mailto:${group.email}`}\n />\n )}\n {memberNames.length > 0 && (\n <div className=\"px-2 py-1.5\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n Members\n </span>\n <span className=\"block text-[13px]\">{memberNames.join(', ')}</span>\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBO,SAAS,YAAY,EAAE,QAA0B;AACtD,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,SAAS,gBAAgB,SAAS,IAAI;AAE5C,QAAM,cAAc,SAChB,GAAG,OAAO,SAAS,IAAI,OAAO,QAAQ,GAAG,KAAA,KAAU,OACnD;AACJ,QAAM,QAAQ,iCAAQ;AAGtB,MAAI,CAAC,OAAO;AACV,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QACV,OAAO;AAAA,QAEP,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,UACxB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,8BACG,SAAA,EACC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,GAAG,WAAW,MAAM,KAAK;AAAA,QAEhC,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAQ;AAAA,YACR,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,cACxB;AAAA,cACD,oBAAC,MAAA,EAAK,WAAU,oBAAA,CAAoB;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACtC;AAAA,IAAA,GAEJ;AAAA,IACA,qBAAC,gBAAA,EAAe,OAAM,SAAQ,WAAU,YACtC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO;AAAA,QAAA;AAAA,MAAA;AAAA,MAET;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM,UAAU,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IACvB,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAOO,SAAS,mBAAmB,EAAE,QAA0B;AAC7D,QAAM,EAAE,SAAS,OAAA,IAAW,qBAAA;AAE5B,MAAI,gBAAgB,SAAS,IAAI,EAAG,QAAO,oBAAC,eAAY,MAAY;AAEpE,QAAM,QAAQ,eAAe,QAAQ,IAAI;AACzC,MAAI,CAAC,OAAO;AACV,WACE,oBAAC,SAAM,SAAQ,WAAU,WAAU,eAAc,OAAO,MACrD,UAAA,KAAA,CACH;AAAA,EAEJ;AAIA,MAAI,CAAC,MAAM,SAAS,MAAM,YAAY,SAAS,GAAG;AAChD,WACE,oBAAC,QAAA,EAAK,WAAU,+BACb,gBAAM,YAAY,IAAI,CAAC,mCACrB,oBAAA,EAAoC,MAAM,cAAlB,UAA8B,CACxD,GACH;AAAA,EAEJ;AAEA,SAAO,oBAAC,cAAW,OAAc;AACnC;AAGA,SAAS,WAAW,EAAE,SAA2B;AAC/C,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,cAAc,MAAM,YAAY,IAAI,CAAC,MAAM;AAC/C,UAAM,IAAI,gBAAgB,SAAS,CAAC;AACpC,WAAO,IAAI,GAAG,EAAE,SAAS,IAAI,EAAE,QAAQ,GAAG,KAAA,KAAU,IAAI;AAAA,EAC1D,CAAC;AAID,QAAM,qBACJ,MAAM,eAAe,MAAM,gBAAgB,MAAM;AACnD,QAAM,cAAc,qBAChB,MAAM,cACN,YAAY,SAAS,IACnB,YAAY,KAAK,IAAI,IACrB,MAAM;AAEZ,MAAI,CAAC,MAAM,SAAS,YAAY,WAAW,GAAG;AAC5C,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QACV,OAAO;AAAA,QAEP,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,UACzB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,8BACG,SAAA,EACC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA,oBAAC,UAAA,EAAO,MAAK,UAAS,WAAU,eAAc,OAAO,aACnD,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,UACzB;AAAA,UACD,oBAAC,MAAA,EAAK,WAAU,oBAAA,CAAoB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAExC,EAAA,CACF;AAAA,IACA,qBAAC,gBAAA,EAAe,OAAM,SAAQ,WAAU,YACtC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,OAAA,EAAM,WAAU,WAAA,CAAW;AAAA,UAClC,OAAM;AAAA,UACN,OAAO;AAAA,QAAA;AAAA,MAAA;AAAA,MAER,MAAM,SACL;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO,MAAM;AAAA,UACb,MAAM,UAAU,MAAM,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,MAG9B,YAAY,SAAS,KACpB,qBAAC,OAAA,EAAI,WAAU,eACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,mEAAkE,UAAA,WAElF;AAAA,4BACC,QAAA,EAAK,WAAU,qBAAqB,UAAA,YAAY,KAAK,IAAI,EAAA,CAAE;AAAA,MAAA,EAAA,CAC9D;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;AAGA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD;AAAA,IACE,MAAM,MAAM;AACV,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,WAAK,UAAU,UAAU,UAAU,KAAK;AACxC,gBAAU,IAAI;AACd,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC9D;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,SACE,qBAAC,OAAA,EAAI,WAAU,iEACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,kCAAkC,UAAA,MAAK;AAAA,IACvD,qBAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAU,mEACb,UAAA,OACH;AAAA,MACC,OACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACA,WAAU;AAAA,UAET,UAAA;AAAA,QAAA;AAAA,MAAA,IAGH,oBAAC,QAAA,EAAK,WAAU,8BAA8B,UAAA,MAAA,CAAM;AAAA,IAAA,GAExD;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACV,OAAO,SAAS,YAAY,QAAQ,MAAM,aAAa;AAAA,QACvD,cAAY,QAAQ,MAAM,YAAA,CAAa;AAAA,QAEtC,UAAA,6BACE,OAAA,EAAM,WAAU,2BAA0B,IAE3C,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAE/B,GACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import type { Group } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Mail, User, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport {\n getGroupBySlug,\n getPersonBySlug,\n} from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * Chip for a slug that may name EITHER a Person or a Group (owner, approvers).\n * Resolves against both collections; falls back to the raw slug so an unknown\n * slug is still visible rather than silently dropped.\n */\nexport function PersonOrGroupBadge({ slug }: PersonBadgeProps) {\n const { persons, groups } = useAppCatalogContext()\n\n if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />\n\n const group = getGroupBySlug(groups, slug)\n if (!group) {\n return (\n <Badge variant=\"outline\" className=\"font-normal\" title={slug}>\n {slug}\n </Badge>\n )\n }\n\n // No group email to copy → show each member as its own clickable/copyable\n // chip instead of one opaque group chip.\n if (!group.email && group.memberSlugs.length > 0) {\n return (\n <span className=\"inline-flex flex-wrap gap-1\">\n {group.memberSlugs.map((memberSlug) => (\n <PersonOrGroupBadge key={memberSlug} slug={memberSlug} />\n ))}\n </span>\n )\n }\n\n return <GroupBadge group={group} />\n}\n\n/** Group chip: name, optional email, and the member names in a popover. */\nfunction GroupBadge({ group }: { group: Group }) {\n const { persons } = useAppCatalogContext()\n const memberNames = group.memberSlugs.map((s) => {\n const p = getPersonBySlug(persons, s)\n return p ? `${p.firstName} ${p.lastName}`.trim() || s : s\n })\n // Many groups (e.g. aws-maintainers-*) have no real displayName set — it's\n // just their slug. Show the member names directly instead of the opaque\n // slug so approvers stay recognizable at a glance, without a popover click.\n const hasRealDisplayName =\n group.displayName && group.displayName !== group.slug\n const displayName = hasRealDisplayName\n ? group.displayName!\n : memberNames.length > 0\n ? memberNames.join(', ')\n : group.slug\n\n if (!group.email && memberNames.length === 0) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <Users className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button type=\"button\" className=\"inline-flex\" title={displayName}>\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <Users className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<Users className=\"size-3.5\" />}\n label=\"Group\"\n value={displayName}\n />\n {group.email && (\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={group.email}\n href={`mailto:${group.email}`}\n />\n )}\n {memberNames.length > 0 && (\n <div className=\"px-2 py-1.5\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n Members\n </span>\n <span className=\"block text-[13px]\">{memberNames.join(', ')}</span>\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ import "@tanstack/react-query-devtools";
|
|
|
13
13
|
import "next-themes";
|
|
14
14
|
import "@tanstack/react-query";
|
|
15
15
|
import "../../../../ui/button.js";
|
|
16
|
+
import "../../../auth/AuthModalContext.js";
|
|
16
17
|
import "@radix-ui/react-dialog";
|
|
17
18
|
import "../../../config/GlobalConfigContext.js";
|
|
18
19
|
import "../../../pluginCore/PluginManagerContext.js";
|