@igstack/app-catalog-frontend-core 0.6.5 → 0.7.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 (36) hide show
  1. package/dist/esm/__tests__/integration/serviceDesks.integration.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/gallery/Gallery.zoom-toggle.test.d.ts +0 -0
  3. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +4 -3
  4. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  5. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +4 -3
  6. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
  7. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +4 -3
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +4 -3
  10. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +6 -0
  12. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +43 -0
  13. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js +2 -1
  15. package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.d.ts +6 -0
  17. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +63 -0
  18. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -0
  19. package/dist/esm/modules/gallery/Gallery.js +5 -2
  20. package/dist/esm/modules/gallery/Gallery.js.map +1 -1
  21. package/dist/esm/routeTree.gen.d.ts +20 -3
  22. package/dist/esm/routeTree.gen.js +9 -2
  23. package/dist/esm/routeTree.gen.js.map +1 -1
  24. package/dist/esm/routes/_layout/service-desks.d.ts +6 -0
  25. package/dist/esm/routes/_layout/service-desks.js +25 -0
  26. package/dist/esm/routes/_layout/service-desks.js.map +1 -0
  27. package/package.json +4 -4
  28. package/src/__tests__/integration/mock-backend/magazines.ts +6 -0
  29. package/src/__tests__/integration/serviceDesks.integration.test.ts +85 -0
  30. package/src/__tests__/modules/gallery/Gallery.zoom-toggle.test.tsx +53 -0
  31. package/src/modules/appCatalog/ui/components/ViewToggle.tsx +40 -0
  32. package/src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx +2 -1
  33. package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +91 -0
  34. package/src/modules/gallery/Gallery.tsx +4 -1
  35. package/src/routeTree.gen.ts +33 -2
  36. package/src/routes/_layout/service-desks.tsx +28 -0
@@ -4,17 +4,18 @@ import { createContext, useMemo, useEffect, use } from "react";
4
4
  import { ApiQueryMagazineAppCatalog } from "../api/ApiQueryMagazineAppCatalog.js";
5
5
  import { useUiSettings } from "../../../context/UiSettingsContext.js";
6
6
  import "../ui/context/AppCatalogFiltersContext.js";
7
+ import "@tanstack/react-router";
8
+ import "clsx";
9
+ import "tailwind-merge";
7
10
  import "@tanstack/react-query-devtools";
8
11
  import "next-themes";
12
+ import "../../auth/AuthModalContext.js";
9
13
  import "@radix-ui/react-dialog";
10
14
  import "lucide-react";
11
- import "clsx";
12
- import "tailwind-merge";
13
15
  import "../../config/GlobalConfigContext.js";
14
16
  import "../../pluginCore/PluginManagerContext.js";
15
17
  import "@radix-ui/react-tooltip";
16
18
  import "../../../ui/empty.js";
17
- import "@tanstack/react-router";
18
19
  import "../../../ui/button.js";
19
20
  import { mergeFrontendBuildId } from "./mergeFrontendBuildId.js";
20
21
  const AppCatalogContext = createContext(void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;;;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;"}
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;"}
@@ -4,16 +4,17 @@ import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { Badge } from "../../../../ui/badge.js";
5
5
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
6
6
  import "../context/AppCatalogFiltersContext.js";
7
+ import "@tanstack/react-router";
8
+ import "clsx";
9
+ import "tailwind-merge";
7
10
  import "@tanstack/react-query-devtools";
8
11
  import "next-themes";
12
+ import "../../../auth/AuthModalContext.js";
9
13
  import "@radix-ui/react-dialog";
10
- import "clsx";
11
- import "tailwind-merge";
12
14
  import "../../../config/GlobalConfigContext.js";
13
15
  import "../../../pluginCore/PluginManagerContext.js";
14
16
  import "@radix-ui/react-tooltip";
15
17
  import "../../../../ui/empty.js";
16
- import "@tanstack/react-router";
17
18
  import "../../../../ui/button.js";
18
19
  import { getPersonBySlug } from "../../utils/resolveHelpers.js";
19
20
  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;"}
@@ -8,16 +8,17 @@ import { PersonBadge } from "./PersonBadge.js";
8
8
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../../../../ui/select.js";
9
9
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
10
10
  import "../context/AppCatalogFiltersContext.js";
11
+ import "@tanstack/react-router";
12
+ import "clsx";
13
+ import "tailwind-merge";
11
14
  import "@tanstack/react-query-devtools";
12
15
  import "next-themes";
16
+ import "../../../auth/AuthModalContext.js";
13
17
  import "@radix-ui/react-dialog";
14
- import "clsx";
15
- import "tailwind-merge";
16
18
  import "../../../config/GlobalConfigContext.js";
17
19
  import "../../../pluginCore/PluginManagerContext.js";
18
20
  import "@radix-ui/react-tooltip";
19
21
  import "../../../../ui/empty.js";
20
- import "@tanstack/react-router";
21
22
  import "../../../../ui/button.js";
22
23
  import { getGroupBySlug } from "../../utils/resolveHelpers.js";
23
24
  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":"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"}
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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"}
@@ -6,16 +6,17 @@ import { Badge } from "../../../../ui/badge.js";
6
6
  import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
7
7
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
8
8
  import "../context/AppCatalogFiltersContext.js";
9
+ import "@tanstack/react-router";
10
+ import "clsx";
11
+ import "tailwind-merge";
9
12
  import "@tanstack/react-query-devtools";
10
13
  import "next-themes";
14
+ import "../../../auth/AuthModalContext.js";
11
15
  import "@radix-ui/react-dialog";
12
- import "clsx";
13
- import "tailwind-merge";
14
16
  import "../../../config/GlobalConfigContext.js";
15
17
  import "../../../pluginCore/PluginManagerContext.js";
16
18
  import "@radix-ui/react-tooltip";
17
19
  import "../../../../ui/empty.js";
18
- import "@tanstack/react-router";
19
20
  import "../../../../ui/button.js";
20
21
  import { PersonBadge } from "./PersonBadge.js";
21
22
  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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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Compact segmented toggle for the header (Apps | Service Desks). Rendered in
3
+ * the header's `middle` slot so it adds no header height. Uses router Links so
4
+ * the active segment reflects the current route and each is deep-linkable.
5
+ */
6
+ export declare function ViewToggle(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { Link } from "@tanstack/react-router";
3
+ import { cn } from "../../../../lib/utils.js";
4
+ function ViewToggle() {
5
+ const segment = "inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors";
6
+ const active = "bg-background text-foreground shadow-sm";
7
+ const inactive = "text-muted-foreground hover:text-foreground";
8
+ return /* @__PURE__ */ jsxs(
9
+ "div",
10
+ {
11
+ role: "tablist",
12
+ "aria-label": "View",
13
+ className: "inline-flex items-center gap-1 rounded-lg bg-muted p-[3px]",
14
+ children: [
15
+ /* @__PURE__ */ jsx(
16
+ Link,
17
+ {
18
+ to: "/",
19
+ "aria-label": "Apps",
20
+ className: cn(segment, inactive),
21
+ activeOptions: { exact: true },
22
+ activeProps: { className: cn(segment, active) },
23
+ children: "Apps"
24
+ }
25
+ ),
26
+ /* @__PURE__ */ jsx(
27
+ Link,
28
+ {
29
+ to: "/service-desks",
30
+ "aria-label": "Service Desks",
31
+ className: cn(segment, inactive),
32
+ activeProps: { className: cn(segment, active) },
33
+ children: "Service Desks"
34
+ }
35
+ )
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ export {
41
+ ViewToggle
42
+ };
43
+ //# sourceMappingURL=ViewToggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewToggle.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ViewToggle.tsx"],"sourcesContent":["import { Link } from '@tanstack/react-router'\nimport { cn } from '~/lib/utils'\n\n/**\n * Compact segmented toggle for the header (Apps | Service Desks). Rendered in\n * the header's `middle` slot so it adds no header height. Uses router Links so\n * the active segment reflects the current route and each is deep-linkable.\n */\nexport function ViewToggle() {\n const segment =\n 'inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors'\n const active = 'bg-background text-foreground shadow-sm'\n const inactive = 'text-muted-foreground hover:text-foreground'\n\n return (\n <div\n role=\"tablist\"\n aria-label=\"View\"\n className=\"inline-flex items-center gap-1 rounded-lg bg-muted p-[3px]\"\n >\n <Link\n to=\"/\"\n aria-label=\"Apps\"\n className={cn(segment, inactive)}\n activeOptions={{ exact: true }}\n activeProps={{ className: cn(segment, active) }}\n >\n Apps\n </Link>\n <Link\n to=\"/service-desks\"\n aria-label=\"Service Desks\"\n className={cn(segment, inactive)}\n activeProps={{ className: cn(segment, active) }}\n >\n Service Desks\n </Link>\n </div>\n )\n}\n"],"names":[],"mappings":";;;AAQO,SAAS,aAAa;AAC3B,QAAM,UACJ;AACF,QAAM,SAAS;AACf,QAAM,WAAW;AAEjB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,WAAU;AAAA,MAEV,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,IAAG;AAAA,YACH,cAAW;AAAA,YACX,WAAW,GAAG,SAAS,QAAQ;AAAA,YAC/B,eAAe,EAAE,OAAO,KAAA;AAAA,YACxB,aAAa,EAAE,WAAW,GAAG,SAAS,MAAM,EAAA;AAAA,YAC7C,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAGD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,IAAG;AAAA,YACH,cAAW;AAAA,YACX,WAAW,GAAG,SAAS,QAAQ;AAAA,YAC/B,aAAa,EAAE,WAAW,GAAG,SAAS,MAAM,EAAA;AAAA,YAC7C,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MAED;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useUiSettings } from "../../../../context/UiSettingsContext.js";
3
3
  import { AppCatalogProvider } from "../../context/AppCatalogContext.js";
4
4
  import { AppCatalogFiltersProvider } from "../context/AppCatalogFiltersContext.js";
5
+ import { ViewToggle } from "../components/ViewToggle.js";
5
6
  import { MainLayout } from "../../../../ui/layout/MainLayout.js";
6
7
  import { TopLevelProviders } from "../../../../ui/layout/TopLevelProviders.js";
7
8
  function AppCatalogLayout({
@@ -17,7 +18,7 @@ function AppCatalogLayout({
17
18
  AppCatalogFiltersProvider,
18
19
  {
19
20
  filterableTagPrefixes,
20
- children: /* @__PURE__ */ jsx(MainLayout, { headerMiddle, children: /* @__PURE__ */ jsx("div", { className: "flex flex-1 w-full justify-center min-h-0", children }) })
21
+ children: /* @__PURE__ */ jsx(MainLayout, { headerMiddle: headerMiddle ?? /* @__PURE__ */ jsx(ViewToggle, {}), children: /* @__PURE__ */ jsx("div", { className: "flex flex-1 w-full justify-center min-h-0", children }) })
21
22
  }
22
23
  ) }) });
23
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogLayout.js","sources":["../../../../../../src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx"],"sourcesContent":["import type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { QueryClient } from '@tanstack/react-query'\nimport type { TRPCClient } from '@trpc/client'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { AppCatalogProvider } from '~/modules/appCatalog/context/AppCatalogContext'\nimport { AppCatalogFiltersProvider } from '~/modules/appCatalog/ui/context/AppCatalogFiltersContext'\nimport { MainLayout } from '~/ui/layout/MainLayout'\nimport { TopLevelProviders } from '~/ui/layout/TopLevelProviders'\n\nexport interface AppCatalogLayoutProps {\n children: React.ReactNode\n queryClient: QueryClient\n trpcClient: TRPCClient<TRPCRouter>\n headerMiddle?: React.ReactNode\n}\n\nexport function AppCatalogLayout({\n children,\n queryClient,\n trpcClient,\n headerMiddle,\n}: AppCatalogLayoutProps) {\n const uiSettings = useUiSettings()\n const filterableTagPrefixes = uiSettings.filterPane?.filterByTagPrefixes ?? []\n\n return (\n <TopLevelProviders queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogProvider>\n <AppCatalogFiltersProvider\n filterableTagPrefixes={filterableTagPrefixes}\n >\n <MainLayout headerMiddle={headerMiddle}>\n {/* <Breadcrumb className=\"pb-4\">*/}\n {/* <BreadcrumbList>*/}\n {/* <BreadcrumbItem>*/}\n {/* <BreadcrumbLink asChild>*/}\n {/* <Link to={'/'} className=\"flex items-center gap-1\">*/}\n {/* <HomeIcon className=\"size-4\" />*/}\n {/* <span className=\"sr-only\">Home</span>*/}\n {/* </Link>*/}\n {/* </BreadcrumbLink>*/}\n {/* </BreadcrumbItem>*/}\n {/* <BreadcrumbSeparator />*/}\n {/* <BreadcrumbItem>*/}\n {/* <BreadcrumbLink asChild>*/}\n {/* <Link to=\"/catalog/apps\">App Catalog</Link>*/}\n {/* </BreadcrumbLink>*/}\n {/* </BreadcrumbItem>*/}\n {/* </BreadcrumbList>*/}\n {/* </Breadcrumb>*/}\n <div className=\"flex flex-1 w-full justify-center min-h-0\">\n {children}\n </div>\n </MainLayout>\n </AppCatalogFiltersProvider>\n </AppCatalogProvider>\n </TopLevelProviders>\n )\n}\n"],"names":[],"mappings":";;;;;;AAgBO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;;AACxB,QAAM,aAAa,cAAA;AACnB,QAAM,0BAAwB,gBAAW,eAAX,mBAAuB,wBAAuB,CAAA;AAE5E,SACE,oBAAC,mBAAA,EAAkB,aAA0B,YAC3C,8BAAC,oBAAA,EACC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MAEA,UAAA,oBAAC,cAAW,cAmBV,UAAA,oBAAC,SAAI,WAAU,6CACZ,UACH,EAAA,CACF;AAAA,IAAA;AAAA,EAAA,GAEJ,EAAA,CACF;AAEJ;"}
1
+ {"version":3,"file":"AppCatalogLayout.js","sources":["../../../../../../src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx"],"sourcesContent":["import type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { QueryClient } from '@tanstack/react-query'\nimport type { TRPCClient } from '@trpc/client'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { AppCatalogProvider } from '~/modules/appCatalog/context/AppCatalogContext'\nimport { AppCatalogFiltersProvider } from '~/modules/appCatalog/ui/context/AppCatalogFiltersContext'\nimport { ViewToggle } from '~/modules/appCatalog/ui/components/ViewToggle'\nimport { MainLayout } from '~/ui/layout/MainLayout'\nimport { TopLevelProviders } from '~/ui/layout/TopLevelProviders'\n\nexport interface AppCatalogLayoutProps {\n children: React.ReactNode\n queryClient: QueryClient\n trpcClient: TRPCClient<TRPCRouter>\n headerMiddle?: React.ReactNode\n}\n\nexport function AppCatalogLayout({\n children,\n queryClient,\n trpcClient,\n headerMiddle,\n}: AppCatalogLayoutProps) {\n const uiSettings = useUiSettings()\n const filterableTagPrefixes = uiSettings.filterPane?.filterByTagPrefixes ?? []\n\n return (\n <TopLevelProviders queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogProvider>\n <AppCatalogFiltersProvider\n filterableTagPrefixes={filterableTagPrefixes}\n >\n <MainLayout headerMiddle={headerMiddle ?? <ViewToggle />}>\n {/* <Breadcrumb className=\"pb-4\">*/}\n {/* <BreadcrumbList>*/}\n {/* <BreadcrumbItem>*/}\n {/* <BreadcrumbLink asChild>*/}\n {/* <Link to={'/'} className=\"flex items-center gap-1\">*/}\n {/* <HomeIcon className=\"size-4\" />*/}\n {/* <span className=\"sr-only\">Home</span>*/}\n {/* </Link>*/}\n {/* </BreadcrumbLink>*/}\n {/* </BreadcrumbItem>*/}\n {/* <BreadcrumbSeparator />*/}\n {/* <BreadcrumbItem>*/}\n {/* <BreadcrumbLink asChild>*/}\n {/* <Link to=\"/catalog/apps\">App Catalog</Link>*/}\n {/* </BreadcrumbLink>*/}\n {/* </BreadcrumbItem>*/}\n {/* </BreadcrumbList>*/}\n {/* </Breadcrumb>*/}\n <div className=\"flex flex-1 w-full justify-center min-h-0\">\n {children}\n </div>\n </MainLayout>\n </AppCatalogFiltersProvider>\n </AppCatalogProvider>\n </TopLevelProviders>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAiBO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;;AACxB,QAAM,aAAa,cAAA;AACnB,QAAM,0BAAwB,gBAAW,eAAX,mBAAuB,wBAAuB,CAAA;AAE5E,SACE,oBAAC,mBAAA,EAAkB,aAA0B,YAC3C,8BAAC,oBAAA,EACC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MAEA,UAAA,oBAAC,YAAA,EAAW,cAAc,gBAAgB,oBAAC,YAAA,CAAA,CAAW,GAmBpD,UAAA,oBAAC,OAAA,EAAI,WAAU,6CACZ,SAAA,CACH,EAAA,CACF;AAAA,IAAA;AAAA,EAAA,GAEJ,EAAA,CACF;AAEJ;"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Service Desks view (#9): a searchable table of all service-desk approval
3
+ * methods (type === 'service'), each with a link that opens its portal in a new
4
+ * tab. Data rides in on the existing app-catalog query (context.approvalMethods).
5
+ */
6
+ export declare function ServiceDesksPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,63 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { ExternalLink } from "lucide-react";
3
+ import { useState, useMemo } from "react";
4
+ import { InputGroup, InputGroupInput } from "../../../../ui/input-group.js";
5
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
6
+ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
7
+ function ServiceDesksPage() {
8
+ const { approvalMethods } = useAppCatalogContext();
9
+ const [search, setSearch] = useState("");
10
+ const desks = useMemo(() => {
11
+ const services = approvalMethods.filter((m) => m.type === "service");
12
+ const q = search.trim().toLowerCase();
13
+ const filtered = q ? services.filter((m) => m.displayName.toLowerCase().includes(q)) : services;
14
+ return [...filtered].sort(
15
+ (a, b) => a.displayName.localeCompare(b.displayName)
16
+ );
17
+ }, [approvalMethods, search]);
18
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0 gap-4", children: [
19
+ /* @__PURE__ */ jsx(InputGroup, { className: "max-w-sm", children: /* @__PURE__ */ jsx(
20
+ InputGroupInput,
21
+ {
22
+ value: search,
23
+ onChange: (e) => setSearch(e.target.value),
24
+ placeholder: "Search service desks by name…",
25
+ "aria-label": "Search service desks"
26
+ }
27
+ ) }),
28
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: desks.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
29
+ "No service desks found",
30
+ search && ` for "${search}"`,
31
+ "."
32
+ ] }) : /* @__PURE__ */ jsxs(Table, { children: [
33
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
34
+ /* @__PURE__ */ jsx(TableHead, { children: "Service Desk" }),
35
+ /* @__PURE__ */ jsx(TableHead, { children: "Link" })
36
+ ] }) }),
37
+ /* @__PURE__ */ jsx(TableBody, { children: desks.map((desk) => {
38
+ const url = desk.config.url;
39
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
40
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: desk.displayName }),
41
+ /* @__PURE__ */ jsx(TableCell, { children: url ? /* @__PURE__ */ jsxs(
42
+ "a",
43
+ {
44
+ href: url,
45
+ target: "_blank",
46
+ rel: "noopener noreferrer",
47
+ className: "inline-flex items-center gap-1 text-primary hover:underline",
48
+ title: url,
49
+ children: [
50
+ "Open",
51
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3" })
52
+ ]
53
+ }
54
+ ) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }) })
55
+ ] }, desk.slug);
56
+ }) })
57
+ ] }) })
58
+ ] });
59
+ }
60
+ export {
61
+ ServiceDesksPage
62
+ };
63
+ //# sourceMappingURL=ServiceDesksPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceDesksPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx"],"sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { InputGroup, InputGroupInput } from '~/ui/input-group'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\n\n/**\n * Service Desks view (#9): a searchable table of all service-desk approval\n * methods (type === 'service'), each with a link that opens its portal in a new\n * tab. Data rides in on the existing app-catalog query (context.approvalMethods).\n */\nexport function ServiceDesksPage() {\n const { approvalMethods } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n\n const desks = useMemo(() => {\n const services = approvalMethods.filter((m) => m.type === 'service')\n const q = search.trim().toLowerCase()\n const filtered = q\n ? services.filter((m) => m.displayName.toLowerCase().includes(q))\n : services\n return [...filtered].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n }, [approvalMethods, search])\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0 gap-4\">\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search service desks by name…\"\n aria-label=\"Search service desks\"\n />\n </InputGroup>\n\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {desks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No service desks found{search && ` for \"${search}\"`}.\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Service Desk</TableHead>\n <TableHead>Link</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {desks.map((desk) => {\n const url = desk.config.url\n return (\n <TableRow key={desk.slug}>\n <TableCell className=\"font-medium\">\n {desk.displayName}\n </TableCell>\n <TableCell>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-primary hover:underline\"\n title={url}\n >\n Open\n <ExternalLink className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAkBO,SAAS,mBAAmB;AACjC,QAAM,EAAE,gBAAA,IAAoB,qBAAA;AAC5B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AAEvC,QAAM,QAAQ,QAAQ,MAAM;AAC1B,UAAM,WAAW,gBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,SAAS;AACnE,UAAM,IAAI,OAAO,KAAA,EAAO,YAAA;AACxB,UAAM,WAAW,IACb,SAAS,OAAO,CAAC,MAAM,EAAE,YAAY,YAAA,EAAc,SAAS,CAAC,CAAC,IAC9D;AACJ,WAAO,CAAC,GAAG,QAAQ,EAAE;AAAA,MAAK,CAAC,GAAG,MAC5B,EAAE,YAAY,cAAc,EAAE,WAAW;AAAA,IAAA;AAAA,EAE7C,GAAG,CAAC,iBAAiB,MAAM,CAAC;AAE5B,SACE,qBAAC,OAAA,EAAI,WAAU,sCACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW,WAAU,YACpB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK;AAAA,QACzC,aAAY;AAAA,QACZ,cAAW;AAAA,MAAA;AAAA,IAAA,GAEf;AAAA,IAEA,oBAAC,OAAA,EAAI,WAAU,kCACZ,UAAA,MAAM,WAAW,IAChB,qBAAC,KAAA,EAAE,WAAU,iCAAgC,UAAA;AAAA,MAAA;AAAA,MACpB,UAAU,SAAS,MAAM;AAAA,MAAI;AAAA,IAAA,EAAA,CACtD,yBAEC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAU,UAAA,eAAA,CAAY;AAAA,QACvB,oBAAC,aAAU,UAAA,OAAA,CAAI;AAAA,MAAA,EAAA,CACjB,EAAA,CACF;AAAA,MACA,oBAAC,WAAA,EACE,UAAA,MAAM,IAAI,CAAC,SAAS;AACnB,cAAM,MAAM,KAAK,OAAO;AACxB,oCACG,UAAA,EACC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,eAClB,UAAA,KAAK,aACR;AAAA,UACA,oBAAC,aACE,UAAA,MACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM;AAAA,cACN,QAAO;AAAA,cACP,KAAI;AAAA,cACJ,WAAU;AAAA,cACV,OAAO;AAAA,cACR,UAAA;AAAA,gBAAA;AAAA,gBAEC,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA,IAGnC,oBAAC,QAAA,EAAK,WAAU,yBAAwB,eAAC,EAAA,CAE7C;AAAA,QAAA,EAAA,GAnBa,KAAK,IAoBpB;AAAA,MAEJ,CAAC,EAAA,CACH;AAAA,IAAA,EAAA,CACF,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
@@ -160,10 +160,13 @@ function Gallery({
160
160
  {
161
161
  src: currentImage.url,
162
162
  alt: currentImage.alt,
163
- className: "max-w-full h-auto",
163
+ className: "max-w-full h-auto cursor-zoom-out",
164
+ onClick: () => setFullscreen(false),
164
165
  onLoad: () => handleImageLoad(currentIndex),
165
166
  onError: () => handleImageError(currentIndex),
166
- style: { opacity: imageState === "loaded" ? 1 : 0 }
167
+ style: { opacity: imageState === "loaded" ? 1 : 0 },
168
+ role: "button",
169
+ "aria-label": "Zoom out"
167
170
  }
168
171
  )
169
172
  ] }),
@@ -1 +1 @@
1
- {"version":3,"file":"Gallery.js","sources":["../../../../src/modules/gallery/Gallery.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport useEmblaCarousel from 'embla-carousel-react'\nimport { ChevronLeft, ChevronRight, ImageOff, X } from 'lucide-react'\nimport { clamp } from 'radashi'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { Button } from '~/ui/button'\nimport { Spinner } from '~/ui/spinner'\nimport { cn } from '~/lib/utils'\n\nexport interface GalleryImage {\n url: string\n alt: string\n}\n\nexport interface GalleryProps {\n images: GalleryImage[]\n initialIndex?: number\n onIndexChange?: (index: number) => void\n onFullscreenChange?: (isFullscreen: boolean) => void\n className?: string\n title?: string\n}\n\nexport function Gallery({\n images,\n initialIndex = 0,\n onIndexChange,\n onFullscreenChange,\n className,\n title,\n}: GalleryProps) {\n const [emblaRef, emblaApi] = useEmblaCarousel({\n loop: false,\n align: 'center',\n })\n // Use lazy initialization for clamp() call\n const [currentIndex, setCurrentIndex] = useState(() =>\n clamp(initialIndex, 0, images.length - 1),\n )\n const [isFullscreen, setIsFullscreen] = useState(false)\n const [imageStates, setImageStates] = useState<\n Record<number, 'loading' | 'loaded' | 'error'>\n >(() => Object.fromEntries(images.map((_, i) => [i, 'loading'])))\n\n // Reset image states when images array changes\n useEffect(() => {\n setImageStates(Object.fromEntries(images.map((_, i) => [i, 'loading'])))\n }, [images])\n\n // Stabilize onFullscreenChange callback with ref\n const onFullscreenChangeRef = useRef(onFullscreenChange)\n useEffect(() => {\n onFullscreenChangeRef.current = onFullscreenChange\n }, [onFullscreenChange])\n\n const setFullscreen = useCallback((value: boolean) => {\n setIsFullscreen(value)\n onFullscreenChangeRef.current?.(value)\n }, [])\n\n // Stabilize onIndexChange callback with ref\n const onIndexChangeRef = useRef(onIndexChange)\n useEffect(() => {\n onIndexChangeRef.current = onIndexChange\n }, [onIndexChange])\n\n const scrollPrev = useCallback(() => {\n if (emblaApi) emblaApi.scrollPrev()\n }, [emblaApi])\n\n const scrollNext = useCallback(() => {\n if (emblaApi) emblaApi.scrollNext()\n }, [emblaApi])\n\n // Sync current index with embla using event handler\n const onSelect = useCallback(() => {\n if (!emblaApi) return\n const index = emblaApi.selectedScrollSnap()\n setCurrentIndex(index)\n onIndexChangeRef.current?.(index)\n }, [emblaApi])\n\n // Subscribe to embla select events and initialize\n useEffect(() => {\n if (!emblaApi) return\n\n // Initialize current index\n onSelect()\n\n // Subscribe to future changes\n emblaApi.on('select', onSelect)\n return () => {\n emblaApi.off('select', onSelect)\n }\n }, [emblaApi, onSelect])\n\n // Scroll to initial index on mount only\n useEffect(() => {\n if (emblaApi && typeof initialIndex === 'number') {\n emblaApi.scrollTo(initialIndex)\n }\n }, [emblaApi, initialIndex])\n\n // Keyboard navigation\n useHotkeys(\n 'left',\n (e) => {\n e.preventDefault()\n scrollPrev()\n },\n {\n enabled: images.length > 1,\n enableOnFormTags: false,\n preventDefault: true,\n },\n [scrollPrev, images.length],\n )\n useHotkeys(\n 'right',\n (e) => {\n e.preventDefault()\n scrollNext()\n },\n {\n enabled: images.length > 1,\n enableOnFormTags: false,\n preventDefault: true,\n },\n [scrollNext, images.length],\n )\n // Intercept Escape in fullscreen — must stopPropagation before Radix Dialog sees it\n useEffect(() => {\n if (!isFullscreen) return\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.stopPropagation()\n e.preventDefault()\n setFullscreen(false)\n }\n }\n\n document.addEventListener('keydown', handleEscape, true)\n return () => {\n document.removeEventListener('keydown', handleEscape, true)\n }\n }, [isFullscreen, setFullscreen])\n\n // Mouse wheel navigation with debouncing\n useEffect(() => {\n if (!emblaApi || images.length <= 1) return\n\n const emblaNode = emblaApi.rootNode()\n\n let isScrolling = false\n let scrollTimeout: NodeJS.Timeout\n\n const handleWheel = (event: WheelEvent) => {\n // Prevent page scroll when wheeling over gallery\n event.preventDefault()\n\n // Skip if already scrolling (debounce)\n if (isScrolling) return\n\n // Determine scroll direction\n const deltaY = event.deltaY\n const deltaX = event.deltaX\n\n // Use vertical scroll primarily, fall back to horizontal\n const delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY : deltaX\n\n if (delta > 0) {\n // Scroll down/right -> next slide\n scrollNext()\n } else if (delta < 0) {\n // Scroll up/left -> previous slide\n scrollPrev()\n }\n\n // Set cooldown period\n isScrolling = true\n clearTimeout(scrollTimeout)\n scrollTimeout = setTimeout(() => {\n isScrolling = false\n }, 300) // 300ms cooldown\n }\n\n emblaNode.addEventListener('wheel', handleWheel, { passive: false })\n\n return () => {\n emblaNode.removeEventListener('wheel', handleWheel)\n clearTimeout(scrollTimeout)\n }\n }, [emblaApi, scrollNext, scrollPrev, images.length])\n\n const currentImage = images[currentIndex]\n\n const handleImageLoad = (index: number) => {\n setImageStates((prev) => ({ ...prev, [index]: 'loaded' }))\n }\n\n const handleImageError = (index: number) => {\n setImageStates((prev) => ({ ...prev, [index]: 'error' }))\n }\n\n // Fullscreen mode\n if (isFullscreen && currentImage) {\n const imageState = imageStates[currentIndex]\n return (\n <div\n className=\"fixed inset-0 z-50 bg-background\"\n role=\"dialog\"\n aria-label=\"Fullscreen view\"\n >\n {imageState === 'error' ? (\n <div className=\"flex h-screen w-screen items-center justify-center\">\n <div className=\"flex flex-col items-center gap-2 text-muted-foreground\">\n <ImageOff className=\"size-12\" />\n <p className=\"text-sm\">Failed to load image</p>\n </div>\n </div>\n ) : (\n <div className=\"h-full w-full overflow-auto p-2\">\n {imageState !== 'loaded' && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n )}\n <img\n src={currentImage.url}\n alt={currentImage.alt}\n className=\"max-w-full h-auto\"\n onLoad={() => handleImageLoad(currentIndex)}\n onError={() => handleImageError(currentIndex)}\n style={{ opacity: imageState === 'loaded' ? 1 : 0 }}\n />\n </div>\n )}\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setFullscreen(false)}\n className=\"absolute top-4 right-4 z-10\"\n aria-label=\"Exit fullscreen\"\n >\n <X className=\"size-4\" />\n </Button>\n </div>\n )\n }\n\n return (\n <div className={cn('flex h-full flex-col min-h-0', className)}>\n {/* Title Header */}\n {title && (\n <div className=\"px-6 py-4\">\n <h2 className=\"text-lg font-semibold\">{title}</h2>\n </div>\n )}\n\n {/* Carousel Viewport with Peek Effect */}\n <div className=\"relative flex-1 min-h-0 overflow-hidden px-4\">\n <div ref={emblaRef} className=\"h-full overflow-hidden\">\n <div className=\"flex h-full gap-4\">\n {images.map((image, index) => {\n const imageState = imageStates[index]\n // Use 100% width for single image, 85% for multiple (peek effect)\n const slideWidth = images.length === 1 ? '100%' : '85%'\n return (\n <div\n key={image.url}\n className={cn(\n 'relative flex min-w-0 items-center justify-center transition-opacity duration-300 h-full',\n index !== currentIndex && 'opacity-40',\n )}\n style={{ flexBasis: slideWidth, flexShrink: 0, flexGrow: 0 }}\n >\n {imageState === 'error' ? (\n <div className=\"flex flex-col items-center gap-2 text-muted-foreground\">\n <ImageOff className=\"size-12\" />\n <p className=\"text-xs\">Failed to load</p>\n </div>\n ) : (\n <>\n {imageState !== 'loaded' && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n )}\n <img\n src={image.url}\n alt={image.alt}\n className=\"max-w-full max-h-full w-auto h-auto object-contain cursor-pointer border border-border/30 rounded\"\n onClick={() => {\n if (index !== currentIndex) {\n emblaApi?.scrollTo(index)\n } else {\n setFullscreen(true)\n }\n }}\n onLoad={() => handleImageLoad(index)}\n onError={() => handleImageError(index)}\n style={{ opacity: imageState === 'loaded' ? 1 : 0 }}\n role=\"button\"\n aria-label={`Navigate to ${image.alt}`}\n />\n </>\n )}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div className=\"flex items-center justify-center gap-4 py-4\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={scrollPrev}\n disabled={images.length <= 1}\n aria-label=\"Previous screenshot\"\n className=\"h-12 w-12\"\n >\n <ChevronLeft className=\"h-12 w-12\" />\n </Button>\n\n <div className=\"text-sm text-muted-foreground\" aria-live=\"polite\">\n {currentIndex + 1} / {images.length}\n </div>\n\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={scrollNext}\n disabled={images.length <= 1}\n aria-label=\"Next screenshot\"\n className=\"h-12 w-12\"\n >\n <ChevronRight className=\"h-12 w-12\" />\n </Button>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Gallery.js","sources":["../../../../src/modules/gallery/Gallery.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport useEmblaCarousel from 'embla-carousel-react'\nimport { ChevronLeft, ChevronRight, ImageOff, X } from 'lucide-react'\nimport { clamp } from 'radashi'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { Button } from '~/ui/button'\nimport { Spinner } from '~/ui/spinner'\nimport { cn } from '~/lib/utils'\n\nexport interface GalleryImage {\n url: string\n alt: string\n}\n\nexport interface GalleryProps {\n images: GalleryImage[]\n initialIndex?: number\n onIndexChange?: (index: number) => void\n onFullscreenChange?: (isFullscreen: boolean) => void\n className?: string\n title?: string\n}\n\nexport function Gallery({\n images,\n initialIndex = 0,\n onIndexChange,\n onFullscreenChange,\n className,\n title,\n}: GalleryProps) {\n const [emblaRef, emblaApi] = useEmblaCarousel({\n loop: false,\n align: 'center',\n })\n // Use lazy initialization for clamp() call\n const [currentIndex, setCurrentIndex] = useState(() =>\n clamp(initialIndex, 0, images.length - 1),\n )\n const [isFullscreen, setIsFullscreen] = useState(false)\n const [imageStates, setImageStates] = useState<\n Record<number, 'loading' | 'loaded' | 'error'>\n >(() => Object.fromEntries(images.map((_, i) => [i, 'loading'])))\n\n // Reset image states when images array changes\n useEffect(() => {\n setImageStates(Object.fromEntries(images.map((_, i) => [i, 'loading'])))\n }, [images])\n\n // Stabilize onFullscreenChange callback with ref\n const onFullscreenChangeRef = useRef(onFullscreenChange)\n useEffect(() => {\n onFullscreenChangeRef.current = onFullscreenChange\n }, [onFullscreenChange])\n\n const setFullscreen = useCallback((value: boolean) => {\n setIsFullscreen(value)\n onFullscreenChangeRef.current?.(value)\n }, [])\n\n // Stabilize onIndexChange callback with ref\n const onIndexChangeRef = useRef(onIndexChange)\n useEffect(() => {\n onIndexChangeRef.current = onIndexChange\n }, [onIndexChange])\n\n const scrollPrev = useCallback(() => {\n if (emblaApi) emblaApi.scrollPrev()\n }, [emblaApi])\n\n const scrollNext = useCallback(() => {\n if (emblaApi) emblaApi.scrollNext()\n }, [emblaApi])\n\n // Sync current index with embla using event handler\n const onSelect = useCallback(() => {\n if (!emblaApi) return\n const index = emblaApi.selectedScrollSnap()\n setCurrentIndex(index)\n onIndexChangeRef.current?.(index)\n }, [emblaApi])\n\n // Subscribe to embla select events and initialize\n useEffect(() => {\n if (!emblaApi) return\n\n // Initialize current index\n onSelect()\n\n // Subscribe to future changes\n emblaApi.on('select', onSelect)\n return () => {\n emblaApi.off('select', onSelect)\n }\n }, [emblaApi, onSelect])\n\n // Scroll to initial index on mount only\n useEffect(() => {\n if (emblaApi && typeof initialIndex === 'number') {\n emblaApi.scrollTo(initialIndex)\n }\n }, [emblaApi, initialIndex])\n\n // Keyboard navigation\n useHotkeys(\n 'left',\n (e) => {\n e.preventDefault()\n scrollPrev()\n },\n {\n enabled: images.length > 1,\n enableOnFormTags: false,\n preventDefault: true,\n },\n [scrollPrev, images.length],\n )\n useHotkeys(\n 'right',\n (e) => {\n e.preventDefault()\n scrollNext()\n },\n {\n enabled: images.length > 1,\n enableOnFormTags: false,\n preventDefault: true,\n },\n [scrollNext, images.length],\n )\n // Intercept Escape in fullscreen — must stopPropagation before Radix Dialog sees it\n useEffect(() => {\n if (!isFullscreen) return\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.stopPropagation()\n e.preventDefault()\n setFullscreen(false)\n }\n }\n\n document.addEventListener('keydown', handleEscape, true)\n return () => {\n document.removeEventListener('keydown', handleEscape, true)\n }\n }, [isFullscreen, setFullscreen])\n\n // Mouse wheel navigation with debouncing\n useEffect(() => {\n if (!emblaApi || images.length <= 1) return\n\n const emblaNode = emblaApi.rootNode()\n\n let isScrolling = false\n let scrollTimeout: NodeJS.Timeout\n\n const handleWheel = (event: WheelEvent) => {\n // Prevent page scroll when wheeling over gallery\n event.preventDefault()\n\n // Skip if already scrolling (debounce)\n if (isScrolling) return\n\n // Determine scroll direction\n const deltaY = event.deltaY\n const deltaX = event.deltaX\n\n // Use vertical scroll primarily, fall back to horizontal\n const delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY : deltaX\n\n if (delta > 0) {\n // Scroll down/right -> next slide\n scrollNext()\n } else if (delta < 0) {\n // Scroll up/left -> previous slide\n scrollPrev()\n }\n\n // Set cooldown period\n isScrolling = true\n clearTimeout(scrollTimeout)\n scrollTimeout = setTimeout(() => {\n isScrolling = false\n }, 300) // 300ms cooldown\n }\n\n emblaNode.addEventListener('wheel', handleWheel, { passive: false })\n\n return () => {\n emblaNode.removeEventListener('wheel', handleWheel)\n clearTimeout(scrollTimeout)\n }\n }, [emblaApi, scrollNext, scrollPrev, images.length])\n\n const currentImage = images[currentIndex]\n\n const handleImageLoad = (index: number) => {\n setImageStates((prev) => ({ ...prev, [index]: 'loaded' }))\n }\n\n const handleImageError = (index: number) => {\n setImageStates((prev) => ({ ...prev, [index]: 'error' }))\n }\n\n // Fullscreen mode\n if (isFullscreen && currentImage) {\n const imageState = imageStates[currentIndex]\n return (\n <div\n className=\"fixed inset-0 z-50 bg-background\"\n role=\"dialog\"\n aria-label=\"Fullscreen view\"\n >\n {imageState === 'error' ? (\n <div className=\"flex h-screen w-screen items-center justify-center\">\n <div className=\"flex flex-col items-center gap-2 text-muted-foreground\">\n <ImageOff className=\"size-12\" />\n <p className=\"text-sm\">Failed to load image</p>\n </div>\n </div>\n ) : (\n <div className=\"h-full w-full overflow-auto p-2\">\n {imageState !== 'loaded' && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n )}\n <img\n src={currentImage.url}\n alt={currentImage.alt}\n className=\"max-w-full h-auto cursor-zoom-out\"\n onClick={() => setFullscreen(false)}\n onLoad={() => handleImageLoad(currentIndex)}\n onError={() => handleImageError(currentIndex)}\n style={{ opacity: imageState === 'loaded' ? 1 : 0 }}\n role=\"button\"\n aria-label=\"Zoom out\"\n />\n </div>\n )}\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setFullscreen(false)}\n className=\"absolute top-4 right-4 z-10\"\n aria-label=\"Exit fullscreen\"\n >\n <X className=\"size-4\" />\n </Button>\n </div>\n )\n }\n\n return (\n <div className={cn('flex h-full flex-col min-h-0', className)}>\n {/* Title Header */}\n {title && (\n <div className=\"px-6 py-4\">\n <h2 className=\"text-lg font-semibold\">{title}</h2>\n </div>\n )}\n\n {/* Carousel Viewport with Peek Effect */}\n <div className=\"relative flex-1 min-h-0 overflow-hidden px-4\">\n <div ref={emblaRef} className=\"h-full overflow-hidden\">\n <div className=\"flex h-full gap-4\">\n {images.map((image, index) => {\n const imageState = imageStates[index]\n // Use 100% width for single image, 85% for multiple (peek effect)\n const slideWidth = images.length === 1 ? '100%' : '85%'\n return (\n <div\n key={image.url}\n className={cn(\n 'relative flex min-w-0 items-center justify-center transition-opacity duration-300 h-full',\n index !== currentIndex && 'opacity-40',\n )}\n style={{ flexBasis: slideWidth, flexShrink: 0, flexGrow: 0 }}\n >\n {imageState === 'error' ? (\n <div className=\"flex flex-col items-center gap-2 text-muted-foreground\">\n <ImageOff className=\"size-12\" />\n <p className=\"text-xs\">Failed to load</p>\n </div>\n ) : (\n <>\n {imageState !== 'loaded' && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n )}\n <img\n src={image.url}\n alt={image.alt}\n className=\"max-w-full max-h-full w-auto h-auto object-contain cursor-pointer border border-border/30 rounded\"\n onClick={() => {\n if (index !== currentIndex) {\n emblaApi?.scrollTo(index)\n } else {\n setFullscreen(true)\n }\n }}\n onLoad={() => handleImageLoad(index)}\n onError={() => handleImageError(index)}\n style={{ opacity: imageState === 'loaded' ? 1 : 0 }}\n role=\"button\"\n aria-label={`Navigate to ${image.alt}`}\n />\n </>\n )}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div className=\"flex items-center justify-center gap-4 py-4\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={scrollPrev}\n disabled={images.length <= 1}\n aria-label=\"Previous screenshot\"\n className=\"h-12 w-12\"\n >\n <ChevronLeft className=\"h-12 w-12\" />\n </Button>\n\n <div className=\"text-sm text-muted-foreground\" aria-live=\"polite\">\n {currentIndex + 1} / {images.length}\n </div>\n\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={scrollNext}\n disabled={images.length <= 1}\n aria-label=\"Next screenshot\"\n className=\"h-12 w-12\"\n >\n <ChevronRight className=\"h-12 w-12\" />\n </Button>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}