@igstack/app-catalog-frontend-core 0.5.0 → 0.6.1

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.
Files changed (46) hide show
  1. package/dist/esm/__tests__/integration/appDeepLink.integration.test.d.ts +0 -0
  2. package/dist/esm/__tests__/integration/harness/given.d.ts +5 -1
  3. package/dist/esm/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.d.ts +1 -0
  4. package/dist/esm/__tests__/ui/components/header/VersionDisplay.test.d.ts +0 -0
  5. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +6 -8
  6. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  7. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.d.ts +15 -0
  8. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js +15 -0
  9. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js.map +1 -0
  10. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
  11. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
  12. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -0
  13. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
  15. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -1
  17. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +3 -1
  18. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  19. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +3 -1
  20. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +17 -12
  21. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  22. package/dist/esm/routeTree.gen.d.ts +18 -4
  23. package/dist/esm/routeTree.gen.js +11 -4
  24. package/dist/esm/routeTree.gen.js.map +1 -1
  25. package/dist/esm/routes/_layout/app.$slug.d.ts +3 -0
  26. package/dist/esm/routes/_layout/app._slug.js +21 -0
  27. package/dist/esm/routes/_layout/app._slug.js.map +1 -0
  28. package/dist/esm/routes/_layout/index.d.ts +0 -1
  29. package/dist/esm/routes/_layout/index.js +0 -1
  30. package/dist/esm/routes/_layout/index.js.map +1 -1
  31. package/dist/esm/ui/components/header/Header.d.ts +4 -0
  32. package/dist/esm/ui/components/header/Header.js +40 -17
  33. package/dist/esm/ui/components/header/Header.js.map +1 -1
  34. package/package.json +4 -3
  35. package/src/__tests__/integration/appDeepLink.integration.test.ts +55 -0
  36. package/src/__tests__/integration/harness/given.tsx +7 -3
  37. package/src/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.ts +42 -0
  38. package/src/__tests__/ui/components/header/VersionDisplay.test.tsx +38 -0
  39. package/src/modules/appCatalog/context/AppCatalogContext.tsx +5 -11
  40. package/src/modules/appCatalog/context/mergeFrontendBuildId.ts +31 -0
  41. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +4 -0
  42. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +18 -15
  43. package/src/routeTree.gen.ts +21 -2
  44. package/src/routes/_layout/app.$slug.tsx +23 -0
  45. package/src/routes/_layout/index.tsx +0 -1
  46. package/src/ui/components/header/Header.tsx +39 -16
@@ -1,3 +1,4 @@
1
+ import { createAcRouter } from '~/util/createAcRouter';
1
2
  import { Magazine } from '../mock-backend/magazines.js';
2
3
  import { MockBackendVerifier } from './MockBackendVerifier.js';
3
4
  import { GlobalError } from '../tools/ErrorTools.js';
@@ -7,6 +8,7 @@ import { GalleryTools } from '../tools/GalleryTools.js';
7
8
  export interface GivenResult {
8
9
  ui: UiTools;
9
10
  backend: MockBackendVerifier;
11
+ router: ReturnType<typeof createAcRouter>;
10
12
  }
11
13
  export interface UiTools {
12
14
  catalog: CatalogTools;
@@ -15,4 +17,6 @@ export interface UiTools {
15
17
  globalError: () => GlobalError;
16
18
  }
17
19
  export declare function cleanupTestResources(): Promise<void>;
18
- export declare function given(magazine: Magazine): Promise<GivenResult>;
20
+ export declare function given(magazine: Magazine, opts?: {
21
+ initialRoute?: string;
22
+ }): Promise<GivenResult>;
@@ -13,7 +13,9 @@ import "../../config/GlobalConfigContext.js";
13
13
  import "../../pluginCore/PluginManagerContext.js";
14
14
  import "@radix-ui/react-tooltip";
15
15
  import "../../../ui/empty.js";
16
+ import "@tanstack/react-router";
16
17
  import "../../../ui/button.js";
18
+ import { mergeFrontendBuildId } from "./mergeFrontendBuildId.js";
17
19
  const AppCatalogContext = createContext(void 0);
18
20
  function AppCatalogProvider({ children }) {
19
21
  var _a, _b;
@@ -29,14 +31,10 @@ function AppCatalogProvider({ children }) {
29
31
  approvalMethods: (data == null ? void 0 : data.approvalMethods) ?? [],
30
32
  persons: (data == null ? void 0 : data.persons) ?? [],
31
33
  groups: (data == null ? void 0 : data.groups) ?? [],
32
- versions: {
33
- ...data == null ? void 0 : data.versions,
34
- ...uiSettings.frontendBuildId && {
35
- frontend: {
36
- displayName: uiSettings.frontendBuildId === "local" ? "local" : `#${uiSettings.frontendBuildId}`
37
- }
38
- }
39
- }
34
+ versions: mergeFrontendBuildId(
35
+ (data == null ? void 0 : data.versions) ?? {},
36
+ uiSettings.frontendBuildId
37
+ )
40
38
  }),
41
39
  [
42
40
  data == null ? void 0 : data.approvalMethods,
@@ -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'\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: {\n ...data?.versions,\n ...(uiSettings.frontendBuildId && {\n frontend: {\n displayName:\n uiSettings.frontendBuildId === 'local'\n ? 'local'\n : `#${uiSettings.frontendBuildId}`,\n },\n }),\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":";;;;;;;;;;;;;;;;AAwBO,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,QACR,GAAG,6BAAM;AAAA,QACT,GAAI,WAAW,mBAAmB;AAAA,UAChC,UAAU;AAAA,YACR,aACE,WAAW,oBAAoB,UAC3B,UACA,IAAI,WAAW,eAAe;AAAA,UAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;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;"}
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;"}
@@ -0,0 +1,15 @@
1
+ import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
2
+ /**
3
+ * Merge the (natera-agnostic) frontend build id into the backend-provided
4
+ * version info's `frontend` slot.
5
+ *
6
+ * The build id is baked into the deployed JS bundle at build time (e.g. a CI
7
+ * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
8
+ * npm version instead of replacing it, so the footer surfaces both:
9
+ * `#<buildId> · <frontend-core version> (<sha>)`.
10
+ *
11
+ * - No build id → return versions unchanged.
12
+ * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
13
+ * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
14
+ */
15
+ export declare function mergeFrontendBuildId(versions: AppVersionInfo, frontendBuildId: string | undefined): AppVersionInfo;
@@ -0,0 +1,15 @@
1
+ function mergeFrontendBuildId(versions, frontendBuildId) {
2
+ if (!frontendBuildId) return versions;
3
+ if (frontendBuildId === "local") {
4
+ return { ...versions, frontend: { displayName: "local" } };
5
+ }
6
+ const be = versions.frontend;
7
+ return {
8
+ ...versions,
9
+ frontend: be ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` } : { displayName: `#${frontendBuildId}` }
10
+ };
11
+ }
12
+ export {
13
+ mergeFrontendBuildId
14
+ };
15
+ //# sourceMappingURL=mergeFrontendBuildId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeFrontendBuildId.js","sources":["../../../../../src/modules/appCatalog/context/mergeFrontendBuildId.ts"],"sourcesContent":["import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'\n\n/**\n * Merge the (natera-agnostic) frontend build id into the backend-provided\n * version info's `frontend` slot.\n *\n * The build id is baked into the deployed JS bundle at build time (e.g. a CI\n * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core\n * npm version instead of replacing it, so the footer surfaces both:\n * `#<buildId> · <frontend-core version> (<sha>)`.\n *\n * - No build id → return versions unchanged.\n * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).\n * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.\n */\nexport function mergeFrontendBuildId(\n versions: AppVersionInfo,\n frontendBuildId: string | undefined,\n): AppVersionInfo {\n if (!frontendBuildId) return versions\n if (frontendBuildId === 'local') {\n return { ...versions, frontend: { displayName: 'local' } }\n }\n const be = versions.frontend\n return {\n ...versions,\n frontend: be\n ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }\n : { displayName: `#${frontendBuildId}` },\n }\n}\n"],"names":[],"mappings":"AAeO,SAAS,qBACd,UACA,iBACgB;AAChB,MAAI,CAAC,gBAAiB,QAAO;AAC7B,MAAI,oBAAoB,SAAS;AAC/B,WAAO,EAAE,GAAG,UAAU,UAAU,EAAE,aAAa,UAAQ;AAAA,EACzD;AACA,QAAM,KAAK,SAAS;AACpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,KACN,EAAE,GAAG,IAAI,aAAa,IAAI,eAAe,MAAM,GAAG,WAAW,OAC7D,EAAE,aAAa,IAAI,eAAe,GAAA;AAAA,EAAG;AAE7C;"}
@@ -12,6 +12,7 @@ import "../../../config/GlobalConfigContext.js";
12
12
  import "../../../pluginCore/PluginManagerContext.js";
13
13
  import "@radix-ui/react-tooltip";
14
14
  import "../../../../ui/empty.js";
15
+ import "@tanstack/react-router";
15
16
  import "../../../../ui/button.js";
16
17
  import { getPersonBySlug } from "../../utils/resolveHelpers.js";
17
18
  function PersonBadge({ slug }) {
@@ -1 +1 @@
1
- {"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\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\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAUO,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;AAEJ,QAAM,QAAQ,iCAAQ;AAEtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAU;AAAA,MACV,OAAO,QAAQ,GAAG,WAAW,KAAK,KAAK,MAAM;AAAA,MAE7C,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,QACxB;AAAA,QACA,SAAS,oBAAC,iBAAA,EAAgB,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG/C;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,gBAAU,UAAU,UAAU,KAAK;AACnC,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;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAO,SAAS,YAAY,QAAQ,KAAK;AAAA,MACzC,MAAK;AAAA,MAEJ,UAAA,6BACE,OAAA,EAAM,WAAU,yBAAwB,IAEzC,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAIjC;"}
1
+ {"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\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\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAUO,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;AAEJ,QAAM,QAAQ,iCAAQ;AAEtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAU;AAAA,MACV,OAAO,QAAQ,GAAG,WAAW,KAAK,KAAK,MAAM;AAAA,MAE7C,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,QACxB;AAAA,QACA,SAAS,oBAAC,iBAAA,EAAgB,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG/C;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,gBAAU,UAAU,UAAU,KAAK;AACnC,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;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAO,SAAS,YAAY,QAAQ,KAAK;AAAA,MACzC,MAAK;AAAA,MAEJ,UAAA,6BACE,OAAA,EAAM,WAAU,yBAAwB,IAEzC,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAIjC;"}
@@ -16,6 +16,7 @@ import "../../../config/GlobalConfigContext.js";
16
16
  import "../../../pluginCore/PluginManagerContext.js";
17
17
  import "@radix-ui/react-tooltip";
18
18
  import "../../../../ui/empty.js";
19
+ import "@tanstack/react-router";
19
20
  import "../../../../ui/button.js";
20
21
  import { getGroupBySlug } from "../../utils/resolveHelpers.js";
21
22
  function getTierBadgeVariant(tierSlug) {
@@ -1 +1 @@
1
- {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.description}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,oBACP,UACqD;AACrD,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,SAAS,aAAa,UAAW,QAAO;AACzD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA0B;AACvD,MAAI,aAAa;AACf,WAAO;AACT,MAAI,aAAa;AACf,WAAO;AACT,SAAO;AACT;AAEA,SAAS,oBAAoB,UAA0B;AACrD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,MAAO,QAAO;AAC/B,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEO,SAAS,oBAAoB;AAAA,EAClC;AACF,GAA6B;AAC3B,QAAM,EAAE,OAAA,IAAW,qBAAA;AACnB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAiB,KAAK;AAE1D,QAAM,cAAc,QAAQ,MAAM;AAChC,UAAM,4BAAY,IAAA;AAClB,eAAW,MAAM,cAAc;AAC7B,UAAI,GAAG,KAAM,OAAM,IAAI,GAAG,IAAI;AAAA,IAChC;AACA,WAAO,CAAC,GAAG,KAAK,EAAE,KAAA;AAAA,EACpB,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,SAAS;AAEb,QAAI,eAAe,OAAO;AACxB,eAAS,OAAO,OAAO,CAAC,OAAO,GAAG,SAAS,UAAU;AAAA,IACvD;AAEA,QAAI,OAAO,QAAQ;AACjB,YAAM,IAAI,OAAO,KAAA,EAAO,YAAA;AACxB,eAAS,OAAO;AAAA,QACd,CAAC,OAAA;;AACC,oBAAG,YAAY,YAAA,EAAc,SAAS,CAAC,MACtC,GAAG,WAAW,CAAA,GAAI,KAAK,CAAC,MAAM,EAAE,YAAA,EAAc,SAAS,CAAC,CAAC,QACzD,QAAG,gBAAH,mBAAgB,cAAc,SAAS,OAAM;AAAA;AAAA,MAAA;AAAA,IAEpD;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,cAAc,QAAQ,UAAU,CAAC;AAErC,MAAI,aAAa,WAAW,EAAG,QAAO;AAEtC,SACE,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,IAAA,oBAAC,SAAI,WAAU,qCACb,UAAA,qBAAC,OAAA,EAAI,WAAU,uBAAsB,UAAA;AAAA,MAAA;AAAA,MACnB,SAAS;AAAA,MAAO;AAAA,MAAK,aAAa;AAAA,MAAO;AAAA,IAAA,EAAA,CAC3D,EAAA,CACF;AAAA,IAGA,qBAAC,OAAA,EAAI,WAAU,cACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAO,WAAU,yDAAA,CAAyD;AAAA,QAC3E;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK;AAAA,YACzC,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MACC,YAAY,SAAS,KACpB,qBAAC,UAAO,OAAO,YAAY,eAAe,eACxC,UAAA;AAAA,QAAA,oBAAC,iBAAc,WAAU,iBACvB,8BAAC,aAAA,EAAY,aAAY,aAAY,EAAA,CACvC;AAAA,6BACC,eAAA,EACC,UAAA;AAAA,UAAA,oBAAC,YAAA,EAAW,OAAM,OAAM,UAAA,aAAS;AAAA,UAChC,YAAY,IAAI,CAAC,SAChB,oBAAC,cAAsB,OAAO,MAC3B,UAAA,KAAA,GADc,IAEjB,CACD;AAAA,QAAA,EAAA,CACH;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,GAEJ;AAAA,IAGA,oBAAC,OAAA,EAAI,WAAU,iDACb,+BAAC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAU,UAAA,OAAA,CAAI;AAAA,QACf,oBAAC,WAAA,EAAU,WAAU,YAAW,UAAA,QAAI;AAAA,QACpC,oBAAC,aAAU,UAAA,QAAA,CAAK;AAAA,QAChB,oBAAC,aAAU,UAAA,kBAAA,CAAe;AAAA,MAAA,EAAA,CAC5B,EAAA,CACF;AAAA,0BACC,WAAA,EACE,UAAA,SAAS,WAAW,wBAClB,UAAA,EACC,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UACX,UAAA;AAAA,QAAA;AAAA,MAAA,EAED,CACF,IAEA,SAAS,IAAI,CAAC,OAAO;AAEnB,cAAM,qBACJ,GAAG,8BAA8B,CAAA,GACjC,QAAQ,CAAC,cAAc;AACvB,gBAAM,QAAQ,eAAe,QAAQ,SAAS;AAC9C,kBAAO,+BAAO,gBAAe,CAAA;AAAA,QAC/B,CAAC;AAED,cAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,iBAAiB,CAAC;AAExD,oCACG,UAAA,EACC,UAAA;AAAA,UAAA,qBAAC,WAAA,EACC,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,uBACZ,UAAA,GAAG,aACN;AAAA,aACE,GAAG,WAAW,CAAA,GAAI,SAAS,KAC3B,oBAAC,OAAA,EAAI,WAAU,wCACX,cAAG,WAAW,CAAA,GAAI,KAAK,IAAI,GAC/B;AAAA,YAED,GAAG,eACF,oBAAC,SAAI,WAAU,wCACZ,aAAG,YAAA,CACN;AAAA,UAAA,GAEJ;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,GAAG,QACF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS,oBAAoB,GAAG,IAAI;AAAA,cACpC,WAAW,WAAW,sBAAsB,GAAG,IAAI,CAAC;AAAA,cAEnD,UAAA,oBAAoB,GAAG,IAAI;AAAA,YAAA;AAAA,UAAA,GAGlC;AAAA,UACA,oBAAC,aACE,UAAA,GAAG,uCACD,aAAA,EAAY,MAAM,GAAG,gBAAA,CAAiB,EAAA,CAE3C;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,kBAAkB,SAAS,IAC1B,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,kBAAkB,IAAI,CAAC,mCACrB,aAAA,EAA6B,MAAM,WAAA,GAAlB,UAA8B,CACjD,EAAA,CACH,IAEA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC,EAAA,CAE7C;AAAA,QAAA,EAAA,GAzCa,GAAG,IA0ClB;AAAA,MAEJ,CAAC,EAAA,CAEL;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.description}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ import "../../../config/GlobalConfigContext.js";
14
14
  import "../../../pluginCore/PluginManagerContext.js";
15
15
  import "@radix-ui/react-tooltip";
16
16
  import "../../../../ui/empty.js";
17
+ import "@tanstack/react-router";
17
18
  import "../../../../ui/button.js";
18
19
  import { PersonBadge } from "./PersonBadge.js";
19
20
  function getTierBadgeVariant(tierSlug) {
@@ -1 +1 @@
1
- {"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -12,5 +12,7 @@ export interface AppCatalogGridProps {
12
12
  totalAppsCount?: number;
13
13
  /** Callback to clear all filters and search */
14
14
  onClearFilters?: () => void;
15
+ /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
16
+ onClosePanel?: () => void;
15
17
  }
16
- export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, searchQuery, totalAppsCount, onClearFilters, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, searchQuery, totalAppsCount, onClearFilters, onClosePanel, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
@@ -494,7 +494,8 @@ function AppCatalogGrid({
494
494
  hasSearch = false,
495
495
  searchQuery,
496
496
  totalAppsCount,
497
- onClearFilters
497
+ onClearFilters,
498
+ onClosePanel
498
499
  }) {
499
500
  const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) : null;
500
501
  const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
@@ -614,6 +615,7 @@ function AppCatalogGrid({
614
615
  };
615
616
  const handleClosePanel = () => {
616
617
  setHasUserClosed(true);
618
+ onClosePanel == null ? void 0 : onClosePanel();
617
619
  };
618
620
  return /* @__PURE__ */ jsxs(ResizablePanelGroup, { orientation: "horizontal", className: "h-full w-full", children: [
619
621
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, Plus, Trash2, X } from 'lucide-react'\nimport React, { useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\nimport { highlightText } from '../../utils/searchApps'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\n\nexport interface AppCatalogGridProps {\n apps: Resource[]\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: Resource) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Search query for highlighting matches */\n searchQuery?: string\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction HighlightedText({\n text,\n searchQuery,\n}: {\n text: string\n searchQuery?: string\n}) {\n if (!searchQuery) {\n return <>{text}</>\n }\n\n const segments = highlightText(text, searchQuery)\n\n return (\n <>\n {segments.map((segment, index) =>\n segment.highlight ? (\n <mark\n key={index}\n className=\"bg-yellow-300 dark:bg-yellow-600/60 font-semibold text-gray-900 dark:text-gray-100\"\n >\n {segment.text}\n </mark>\n ) : (\n <React.Fragment key={index}>{segment.text}</React.Fragment>\n ),\n )}\n </>\n )\n}\n\nfunction AppIcon({ app, className }: { app: Resource; className?: string }) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.abbreviation || app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction TiersAndSubResourcesPanel({ app }: { app: Resource }) {\n const { resources } = useAppCatalogContext()\n const appSubResources = React.useMemo(\n () => getChildResources(resources, app.slug),\n [resources, app.slug],\n )\n\n return (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <div className=\"mt-6\">\n <TierVariantsSection tiers={app.tiers} />\n </div>\n )}\n {appSubResources.length > 0 && (\n <div className=\"mt-6\">\n <SubResourcesSection subResources={appSubResources} />\n </div>\n )}\n </>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n\n // Enter: open screenshot gallery\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n // Esc: close the details panel (only when gallery is NOT open)\n useHotkeys(\n 'escape',\n () => {\n onClosePanel()\n },\n { enabled: !isGalleryOpen },\n [isGalleryOpen, onClosePanel],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"border-b pb-6\">\n <div className=\"flex items-center gap-3\">\n <AppIcon app={app} className=\"size-16\" />\n <div className=\"-mx-3 flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 px-3\">\n <div className=\"text-2xl font-semibold min-w-0\">\n {app.abbreviation\n ? `${app.displayName} (${app.abbreviation})`\n : app.displayName}\n </div>\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n </div>\n {isAdmin && (\n <div className=\"mt-1 px-3\">\n <span className=\"text-xs text-muted-foreground mr-2\">\n Slug:\n </span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </div>\n )}\n <div className=\"mt-1 px-3\">\n {isAdmin ? (\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n renderView={(url) =>\n url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n ) : app.appUrl ? (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {app.appUrl.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </div>\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Tier Variants and Sub-Resources */}\n <TiersAndSubResourcesPanel app={app} />\n\n {/* Links */}\n {app.links && app.links.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-1 text-xs font-medium text-muted-foreground\">\n Links\n </h3>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || link.url.replace(/https?:\\/\\//g, '')}\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Sources */}\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {val.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n <li key={index} className=\"flex items-center gap-2 text-xs\">\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Resource[]\n}\n\nfunction groupApps(\n apps: Resource[],\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): GroupedApps[] {\n // When search is active, skip grouping and preserve relevance order\n if (hasSearch) {\n return [{ groupName: 'All Apps', apps: [...apps] }]\n }\n\n if (!groupingDef) {\n // No grouping definition - sort alphabetically\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Resource[]>()\n const ungrouped: Resource[] = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: GroupedApps[] = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups by app count descending\n result.sort((a, b) => b.apps.length - a.apps.length)\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n searchQuery,\n totalAppsCount,\n onClearFilters,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Build a map of parentSlug -> matched child resource displayName for search annotation\n const { resources: allResources2 } = useAppCatalogContext()\n const matchedSubResourceMap = React.useMemo(() => {\n const map = new Map<string, string>()\n if (!searchQuery?.trim() || allResources2.length === 0) return map\n const queryTerms = searchQuery\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n const allTermsMatch = (text: string): boolean =>\n queryTerms.every((term) => text.includes(term))\n\n for (const r of allResources2) {\n if (!r.parentSlug) continue\n if (map.has(r.parentSlug)) continue\n const nameMatch = allTermsMatch(r.displayName.toLowerCase())\n const aliasMatch = (r.aliases ?? []).some((a) =>\n allTermsMatch(a.toLowerCase()),\n )\n const descMatch = r.description\n ? allTermsMatch(r.description.toLowerCase())\n : false\n if (nameMatch || aliasMatch || descMatch) {\n map.set(r.parentSlug, r.displayName)\n }\n }\n return map\n }, [searchQuery, allResources2])\n\n // Define columns\n const columns = React.useMemo<ColumnDef<Resource>[]>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex flex-col\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n <HighlightedText\n text={\n row.original.abbreviation ||\n row.original.displayName ||\n 'Unnamed App'\n }\n searchQuery={searchQuery}\n />\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n {row.original.abbreviation && (\n <span className=\"text-xs text-muted-foreground\">\n <HighlightedText\n text={row.original.displayName}\n searchQuery={searchQuery}\n />\n </span>\n )}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <div>\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n <HighlightedText\n text={row.original.description || '—'}\n searchQuery={searchQuery}\n />\n </span>\n {matchedSubResourceMap.get(row.original.slug) && (\n <div className=\"text-xs text-primary mt-0.5\">\n Matched sub-resource:{' '}\n {matchedSubResourceMap.get(row.original.slug)}\n </div>\n )}\n </div>\n ),\n },\n ],\n [searchQuery, matchedSubResourceMap],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - derive from selectedApp and explicit close\n const [hasUserClosed, setHasUserClosed] = useState(false)\n\n // Auto-open when app is selected, unless user explicitly closed\n const isPanelOpen = selectedApp !== null && !hasUserClosed\n\n // Reset close flag when selectedApp changes\n React.useEffect(() => {\n if (selectedApp) {\n setHasUserClosed(false)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: Resource) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setHasUserClosed(true)\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n\n {/* Clear Filters Row */}\n {totalAppsCount &&\n totalAppsCount > apps.length &&\n onClearFilters && (\n <TableRow>\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-8 text-center\"\n >\n <Button\n variant=\"outline\"\n onClick={onClearFilters}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear filters to show all apps ({totalAppsCount})\n </Button>\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-4 right-4 z-10 hover:bg-accent\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n <AppDetails\n app={selectedApp}\n onAppClick={onAppClick}\n onClosePanel={handleClosePanel}\n />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
1
+ {"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, Plus, Trash2, X } from 'lucide-react'\nimport React, { useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\nimport { highlightText } from '../../utils/searchApps'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\n\nexport interface AppCatalogGridProps {\n apps: Resource[]\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: Resource) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Search query for highlighting matches */\n searchQuery?: string\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */\n onClosePanel?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction HighlightedText({\n text,\n searchQuery,\n}: {\n text: string\n searchQuery?: string\n}) {\n if (!searchQuery) {\n return <>{text}</>\n }\n\n const segments = highlightText(text, searchQuery)\n\n return (\n <>\n {segments.map((segment, index) =>\n segment.highlight ? (\n <mark\n key={index}\n className=\"bg-yellow-300 dark:bg-yellow-600/60 font-semibold text-gray-900 dark:text-gray-100\"\n >\n {segment.text}\n </mark>\n ) : (\n <React.Fragment key={index}>{segment.text}</React.Fragment>\n ),\n )}\n </>\n )\n}\n\nfunction AppIcon({ app, className }: { app: Resource; className?: string }) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.abbreviation || app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction TiersAndSubResourcesPanel({ app }: { app: Resource }) {\n const { resources } = useAppCatalogContext()\n const appSubResources = React.useMemo(\n () => getChildResources(resources, app.slug),\n [resources, app.slug],\n )\n\n return (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <div className=\"mt-6\">\n <TierVariantsSection tiers={app.tiers} />\n </div>\n )}\n {appSubResources.length > 0 && (\n <div className=\"mt-6\">\n <SubResourcesSection subResources={appSubResources} />\n </div>\n )}\n </>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n\n // Enter: open screenshot gallery\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n // Esc: close the details panel (only when gallery is NOT open)\n useHotkeys(\n 'escape',\n () => {\n onClosePanel()\n },\n { enabled: !isGalleryOpen },\n [isGalleryOpen, onClosePanel],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"border-b pb-6\">\n <div className=\"flex items-center gap-3\">\n <AppIcon app={app} className=\"size-16\" />\n <div className=\"-mx-3 flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 px-3\">\n <div className=\"text-2xl font-semibold min-w-0\">\n {app.abbreviation\n ? `${app.displayName} (${app.abbreviation})`\n : app.displayName}\n </div>\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n </div>\n {isAdmin && (\n <div className=\"mt-1 px-3\">\n <span className=\"text-xs text-muted-foreground mr-2\">\n Slug:\n </span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </div>\n )}\n <div className=\"mt-1 px-3\">\n {isAdmin ? (\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n renderView={(url) =>\n url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n ) : app.appUrl ? (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {app.appUrl.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </div>\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Tier Variants and Sub-Resources */}\n <TiersAndSubResourcesPanel app={app} />\n\n {/* Links */}\n {app.links && app.links.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-1 text-xs font-medium text-muted-foreground\">\n Links\n </h3>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || link.url.replace(/https?:\\/\\//g, '')}\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Sources */}\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {val.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n <li key={index} className=\"flex items-center gap-2 text-xs\">\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Resource[]\n}\n\nfunction groupApps(\n apps: Resource[],\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): GroupedApps[] {\n // When search is active, skip grouping and preserve relevance order\n if (hasSearch) {\n return [{ groupName: 'All Apps', apps: [...apps] }]\n }\n\n if (!groupingDef) {\n // No grouping definition - sort alphabetically\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Resource[]>()\n const ungrouped: Resource[] = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: GroupedApps[] = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups by app count descending\n result.sort((a, b) => b.apps.length - a.apps.length)\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n searchQuery,\n totalAppsCount,\n onClearFilters,\n onClosePanel,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Build a map of parentSlug -> matched child resource displayName for search annotation\n const { resources: allResources2 } = useAppCatalogContext()\n const matchedSubResourceMap = React.useMemo(() => {\n const map = new Map<string, string>()\n if (!searchQuery?.trim() || allResources2.length === 0) return map\n const queryTerms = searchQuery\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n const allTermsMatch = (text: string): boolean =>\n queryTerms.every((term) => text.includes(term))\n\n for (const r of allResources2) {\n if (!r.parentSlug) continue\n if (map.has(r.parentSlug)) continue\n const nameMatch = allTermsMatch(r.displayName.toLowerCase())\n const aliasMatch = (r.aliases ?? []).some((a) =>\n allTermsMatch(a.toLowerCase()),\n )\n const descMatch = r.description\n ? allTermsMatch(r.description.toLowerCase())\n : false\n if (nameMatch || aliasMatch || descMatch) {\n map.set(r.parentSlug, r.displayName)\n }\n }\n return map\n }, [searchQuery, allResources2])\n\n // Define columns\n const columns = React.useMemo<ColumnDef<Resource>[]>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex flex-col\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n <HighlightedText\n text={\n row.original.abbreviation ||\n row.original.displayName ||\n 'Unnamed App'\n }\n searchQuery={searchQuery}\n />\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n {row.original.abbreviation && (\n <span className=\"text-xs text-muted-foreground\">\n <HighlightedText\n text={row.original.displayName}\n searchQuery={searchQuery}\n />\n </span>\n )}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <div>\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n <HighlightedText\n text={row.original.description || '—'}\n searchQuery={searchQuery}\n />\n </span>\n {matchedSubResourceMap.get(row.original.slug) && (\n <div className=\"text-xs text-primary mt-0.5\">\n Matched sub-resource:{' '}\n {matchedSubResourceMap.get(row.original.slug)}\n </div>\n )}\n </div>\n ),\n },\n ],\n [searchQuery, matchedSubResourceMap],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - derive from selectedApp and explicit close\n const [hasUserClosed, setHasUserClosed] = useState(false)\n\n // Auto-open when app is selected, unless user explicitly closed\n const isPanelOpen = selectedApp !== null && !hasUserClosed\n\n // Reset close flag when selectedApp changes\n React.useEffect(() => {\n if (selectedApp) {\n setHasUserClosed(false)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: Resource) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setHasUserClosed(true)\n onClosePanel?.()\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n\n {/* Clear Filters Row */}\n {totalAppsCount &&\n totalAppsCount > apps.length &&\n onClearFilters && (\n <TableRow>\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-8 text-center\"\n >\n <Button\n variant=\"outline\"\n onClick={onClearFilters}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear filters to show all apps ({totalAppsCount})\n </Button>\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-4 right-4 z-10 hover:bg-accent\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n <AppDetails\n app={selectedApp}\n onAppClick={onAppClick}\n onClosePanel={handleClosePanel}\n />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}