@igstack/app-catalog-frontend-core 6.6.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/esm/App.d.ts +29 -3
  2. package/dist/esm/App.js +4 -2
  3. package/dist/esm/App.js.map +1 -1
  4. package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.d.ts +0 -0
  5. package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.d.ts +0 -0
  6. package/dist/esm/__tests__/modules/dismiss/DismissStack.test.d.ts +0 -0
  7. package/dist/esm/__tests__/modules/extensions/slots.test.d.ts +0 -0
  8. package/dist/esm/__tests__/publicEntryResolves.test.d.ts +1 -0
  9. package/dist/esm/appPropsFactory.d.ts +19 -1
  10. package/dist/esm/appPropsFactory.js +4 -3
  11. package/dist/esm/appPropsFactory.js.map +1 -1
  12. package/dist/esm/index.d.ts +15 -2
  13. package/dist/esm/index.js +5 -0
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/internal.d.ts +7 -1
  16. package/dist/esm/internal.js +0 -2
  17. package/dist/esm/internal.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +17 -0
  19. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +7 -16
  20. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +20 -11
  22. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
  23. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +39 -17
  24. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  25. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +0 -14
  26. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
  27. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js +5 -18
  28. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  29. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +12 -1
  30. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +3 -1
  31. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
  32. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +17 -1
  33. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +212 -79
  34. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  35. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +0 -14
  36. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  37. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.d.ts +1 -2
  38. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +31 -23
  39. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  40. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +5 -0
  41. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
  42. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +5 -3
  43. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  44. package/dist/esm/modules/auth/AuthContext.d.ts +12 -0
  45. package/dist/esm/modules/auth/AuthContext.js +5 -0
  46. package/dist/esm/modules/auth/AuthContext.js.map +1 -1
  47. package/dist/esm/modules/auth/index.d.ts +1 -1
  48. package/dist/esm/modules/dismiss/DismissStack.d.ts +102 -0
  49. package/dist/esm/modules/dismiss/DismissStack.js +82 -0
  50. package/dist/esm/modules/dismiss/DismissStack.js.map +1 -0
  51. package/dist/esm/modules/extensions/ExtensionsContext.d.ts +17 -0
  52. package/dist/esm/modules/extensions/ExtensionsContext.js +15 -0
  53. package/dist/esm/modules/extensions/ExtensionsContext.js.map +1 -0
  54. package/dist/esm/modules/extensions/factories.d.ts +18 -0
  55. package/dist/esm/modules/extensions/factories.js +12 -0
  56. package/dist/esm/modules/extensions/factories.js.map +1 -0
  57. package/dist/esm/modules/extensions/features/resourceDetail.d.ts +92 -0
  58. package/dist/esm/modules/extensions/features/resourceDetail.js +14 -0
  59. package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -0
  60. package/dist/esm/modules/extensions/index.d.ts +79 -0
  61. package/dist/esm/modules/extensions/index.js +26 -0
  62. package/dist/esm/modules/extensions/index.js.map +1 -0
  63. package/dist/esm/modules/extensions/makeSlot.d.ts +46 -0
  64. package/dist/esm/modules/extensions/makeSlot.js +62 -0
  65. package/dist/esm/modules/extensions/makeSlot.js.map +1 -0
  66. package/dist/esm/modules/extensions/types.d.ts +46 -0
  67. package/dist/esm/modules/gallery/Gallery.js +6 -14
  68. package/dist/esm/modules/gallery/Gallery.js.map +1 -1
  69. package/dist/esm/routeTree.gen.d.ts +34 -34
  70. package/dist/esm/routeTree.gen.js +14 -14
  71. package/dist/esm/routeTree.gen.js.map +1 -1
  72. package/dist/esm/router.d.ts +23 -0
  73. package/dist/esm/router.js +5 -0
  74. package/dist/esm/router.js.map +1 -0
  75. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
  76. package/dist/esm/routes/_layout/app.$slug_.sub.$subSlug.d.ts +1 -1
  77. package/dist/esm/routes/_layout/catalog.apps.index.d.ts +2 -2
  78. package/dist/esm/routes/_layout/index.d.ts +1 -1
  79. package/dist/esm/routes/_layout/login.d.ts +1 -1
  80. package/dist/esm/routes/_layout/mcp.d.ts +1 -1
  81. package/dist/esm/routes/_layout/service-desks.d.ts +1 -1
  82. package/dist/esm/routes/_layout.d.ts +1 -1
  83. package/dist/esm/routes/_layout.js +13 -2
  84. package/dist/esm/routes/_layout.js.map +1 -1
  85. package/dist/esm/routes/auth.callback.d.ts +1 -1
  86. package/dist/esm/ui/components/error/DefaultErrorComponent.js +9 -2
  87. package/dist/esm/ui/components/error/DefaultErrorComponent.js.map +1 -1
  88. package/dist/esm/ui/components/header/Header.js +0 -1
  89. package/dist/esm/ui/components/header/Header.js.map +1 -1
  90. package/dist/esm/ui/dialog.d.ts +1 -1
  91. package/dist/esm/ui/dialog.js +9 -0
  92. package/dist/esm/ui/dialog.js.map +1 -1
  93. package/dist/esm/ui/table.d.ts +25 -1
  94. package/dist/esm/ui/table.js +20 -3
  95. package/dist/esm/ui/table.js.map +1 -1
  96. package/dist/esm/util/createAcRouter.d.ts +1 -1
  97. package/package.json +19 -11
  98. package/src/App.tsx +34 -3
  99. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +4 -1
  100. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.focus.test.tsx +1 -1
  101. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +115 -0
  102. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.tsx +120 -0
  103. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -5
  104. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +9 -9
  105. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +31 -5
  106. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -3
  107. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +48 -6
  108. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +1 -7
  109. package/src/__tests__/modules/dismiss/DismissStack.test.tsx +330 -0
  110. package/src/__tests__/modules/extensions/slots.test.tsx +252 -0
  111. package/src/__tests__/publicEntryResolves.test.ts +63 -0
  112. package/src/appPropsFactory.ts +23 -3
  113. package/src/index.tsx +32 -2
  114. package/src/internal.ts +7 -1
  115. package/src/modules/appCatalog/context/AppCatalogContext.tsx +21 -0
  116. package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +52 -12
  117. package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +59 -18
  118. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +12 -20
  119. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +19 -0
  120. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +396 -111
  121. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +46 -26
  122. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +20 -0
  123. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +17 -3
  124. package/src/modules/auth/AuthContext.tsx +15 -0
  125. package/src/modules/auth/index.ts +1 -0
  126. package/src/modules/dismiss/DismissStack.tsx +224 -0
  127. package/src/modules/extensions/ExtensionsContext.tsx +36 -0
  128. package/src/modules/extensions/factories.ts +19 -0
  129. package/src/modules/extensions/features/resourceDetail.ts +74 -0
  130. package/src/modules/extensions/index.ts +121 -0
  131. package/src/modules/extensions/makeSlot.tsx +123 -0
  132. package/src/modules/extensions/types.ts +51 -0
  133. package/src/modules/gallery/Gallery.tsx +13 -17
  134. package/src/routeTree.gen.ts +31 -31
  135. package/src/router.ts +23 -0
  136. package/src/routes/_layout.tsx +51 -1
  137. package/src/ui/components/error/DefaultErrorComponent.tsx +22 -4
  138. package/src/ui/dialog.tsx +26 -0
  139. package/src/ui/table.tsx +46 -3
@@ -1,9 +1,15 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { X } from "lucide-react";
3
- import { useRef, useLayoutEffect, useEffect } from "react";
3
+ import { useMemo, useRef, useLayoutEffect, useEffect } from "react";
4
4
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
5
+ import { useOptionalUser } from "../../../auth/AuthContext.js";
6
+ import "@tanstack/react-query";
7
+ import "../../../../ui/button.js";
8
+ import { useDismissStack, useDismissable } from "../../../dismiss/DismissStack.js";
9
+ import { getChildResources } from "../../utils/resolveHelpers.js";
5
10
  import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
6
11
  import { AppDetails } from "../detail/AppDetails.js";
12
+ import { ResourceDetailProvider, ResourceDetailAccessActions } from "../../../extensions/features/resourceDetail.js";
7
13
  function AppDetailPanel({
8
14
  app,
9
15
  subResource,
@@ -11,7 +17,17 @@ function AppDetailPanel({
11
17
  onAppClick,
12
18
  onBackToParent
13
19
  }) {
14
- const { approvalMethods } = useAppCatalogContext();
20
+ const { approvalMethods, resources: allResources } = useAppCatalogContext();
21
+ const user = useOptionalUser();
22
+ const subResourceActionsCtx = useMemo(
23
+ () => ({
24
+ resource: subResource ?? app,
25
+ parent: subResource ? app : void 0,
26
+ subResources: getChildResources(allResources, app.slug),
27
+ user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null
28
+ }),
29
+ [subResource, app, allResources, user]
30
+ );
15
31
  const dialogRef = useRef(null);
16
32
  useLayoutEffect(() => {
17
33
  const card = dialogRef.current;
@@ -29,10 +45,15 @@ function AppDetailPanel({
29
45
  return () => window.removeEventListener("resize", pinCentre);
30
46
  }, [app.slug, subResource == null ? void 0 : subResource.slug]);
31
47
  const shownSlug = (subResource == null ? void 0 : subResource.slug) ?? app.slug;
48
+ const stack = useDismissStack();
32
49
  useEffect(() => {
33
50
  const dialog = dialogRef.current;
34
- if (dialog && !dialog.contains(document.activeElement)) dialog.focus();
35
- }, [shownSlug]);
51
+ if (!dialog || dialog.contains(document.activeElement)) return;
52
+ const top = stack.topElement();
53
+ if (top && top !== dialog) return;
54
+ dialog.focus();
55
+ }, [shownSlug, stack]);
56
+ useDismissable(dialogRef, onClose);
36
57
  return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10", children: [
37
58
  /* @__PURE__ */ jsx(
38
59
  "button",
@@ -67,20 +88,21 @@ function AppDetailPanel({
67
88
  "div",
68
89
  {
69
90
  className: "max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7",
70
- children: subResource ? /* @__PURE__ */ jsx(
71
- SubResourceDetailPanel,
91
+ children: /* @__PURE__ */ jsx(
92
+ ResourceDetailProvider,
72
93
  {
73
- subResource,
74
- parent: app,
75
- approvalMethods,
76
- onBack: onBackToParent ?? onClose
77
- }
78
- ) : /* @__PURE__ */ jsx(
79
- AppDetails,
80
- {
81
- app,
82
- onAppClick,
83
- onClosePanel: onClose
94
+ resource: subResource ?? app,
95
+ parent: subResource ? app : void 0,
96
+ children: subResource ? /* @__PURE__ */ jsx(
97
+ SubResourceDetailPanel,
98
+ {
99
+ subResource,
100
+ parent: app,
101
+ approvalMethods,
102
+ onBack: onBackToParent ?? onClose,
103
+ actions: /* @__PURE__ */ jsx(ResourceDetailAccessActions, { ...subResourceActionsCtx })
104
+ }
105
+ ) : /* @__PURE__ */ jsx(AppDetails, { app, onAppClick })
84
106
  }
85
107
  )
86
108
  },
@@ -1 +1 @@
1
- {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods } = useAppCatalogContext()\n const dialogRef = useRef<HTMLDivElement>(null)\n\n /**\n * Centre the card ONCE when it opens, then hold that position.\n *\n * `my-auto` centred it on every render, so each panel height change pushed both\n * edges and half the delta became upward movement of everything above it —\n * including the tab strip, which moved 113px and took the control the user had\n * just clicked with it (CLS 0.19, \"needs improvement\").\n *\n * Top-anchoring fixed the shift but looked wrong: a short card sat high on the\n * page. So pin the centred offset instead. The card still opens centred, and\n * anything that grows it afterwards extends downward only — which is what a\n * reader expects when they open a tab with more in it.\n *\n * Only on open and on resize. Deliberately NOT on content change: reacting to\n * content is precisely the bug.\n */\n useLayoutEffect(() => {\n const card = dialogRef.current\n const scroller = card?.parentElement\n if (!card || !scroller) return\n\n const pinCentre = () => {\n card.style.marginTop = '0px'\n const style = getComputedStyle(scroller)\n const usable =\n scroller.clientHeight -\n parseFloat(style.paddingTop) -\n parseFloat(style.paddingBottom)\n const slack = usable - card.offsetHeight\n // Negative slack means the card is already taller than the viewport; leave it\n // at the top and let the overlay scroll, rather than pushing its head off.\n card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`\n }\n\n pinCentre()\n window.addEventListener('resize', pinCentre)\n return () => window.removeEventListener('resize', pinCentre)\n // Keyed on the opened entry: a different card is a new open, and should\n // re-centre for its own height.\n }, [app.slug, subResource?.slug])\n\n // Focus the dialog on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n useEffect(() => {\n const dialog = dialogRef.current\n if (dialog && !dialog.contains(document.activeElement)) dialog.focus()\n }, [shownSlug])\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n // Centred on open, then held — see the pin effect above. `my-auto` is\n // deliberately absent: it re-centres on EVERY height change, which is the\n // layout-shift bug.\n // `overflow-hidden` belongs on the rounded box: the scrolling child\n // below carries the band's tint to its own square corners and paints\n // them over this radius, so the card's top corners read as cut off.\n className=\"relative w-full max-w-[min(1120px,94vw)] overflow-hidden rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none\"\n >\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"absolute top-3 right-3 z-10 rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n {/* Keyed by the resource on screen, so the body remounts when the card\n navigates from one app to another (\"View replacement\", an access\n prerequisite's parent) instead of updating in place. Everything\n per-app inside it is component state with no reset of its own — the\n roles table's expansion, the icon/screenshot error flags, a draft\n source edit — and it used to arrive at the next app still carrying\n the previous one's, plus the previous scroll offset.\n\n The key is here and not on the outer dialog on purpose: that one\n would restart the open animation and re-run the mount-focus effect\n on every in-card navigation. */}\n <div\n key={shownSlug}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n />\n ) : (\n <AppDetails\n app={app}\n onAppClick={onAppClick}\n onClosePanel={onClose}\n />\n )}\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAsBO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,gBAAA,IAAoB,qBAAA;AAC5B,QAAM,YAAY,OAAuB,IAAI;AAkB7C,kBAAgB,MAAM;AACpB,UAAM,OAAO,UAAU;AACvB,UAAM,WAAW,6BAAM;AACvB,QAAI,CAAC,QAAQ,CAAC,SAAU;AAExB,UAAM,YAAY,MAAM;AACtB,WAAK,MAAM,YAAY;AACvB,YAAM,QAAQ,iBAAiB,QAAQ;AACvC,YAAM,SACJ,SAAS,eACT,WAAW,MAAM,UAAU,IAC3B,WAAW,MAAM,aAAa;AAChC,YAAM,QAAQ,SAAS,KAAK;AAG5B,WAAK,MAAM,YAAY,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC9D;AAEA,cAAA;AACA,WAAO,iBAAiB,UAAU,SAAS;AAC3C,WAAO,MAAM,OAAO,oBAAoB,UAAU,SAAS;AAAA,EAG7D,GAAG,CAAC,IAAI,MAAM,2CAAa,IAAI,CAAC;AAchC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAC3C,YAAU,MAAM;AACd,UAAM,SAAS,UAAU;AACzB,QAAI,UAAU,CAAC,OAAO,SAAS,SAAS,aAAa,UAAU,MAAA;AAAA,EACjE,GAAG,CAAC,SAAS,CAAC;AAEd,SACE,qBAAC,OAAA,EAAI,WAAU,yFAEb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,SAAS;AAAA,QACT,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAMZ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,QAC1D,UAAU;AAAA,QAOV,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAW;AAAA,cACX,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA,oBAAC,GAAA,EAAE,WAAU,SAAA,CAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAaxB;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cAET,UAAA,cACC;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA,QAAQ;AAAA,kBACR;AAAA,kBACA,QAAQ,kBAAkB;AAAA,gBAAA;AAAA,cAAA,IAG5B;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC;AAAA,kBACA;AAAA,kBACA,cAAc;AAAA,gBAAA;AAAA,cAAA;AAAA,YAChB;AAAA,YAfG;AAAA,UAAA;AAAA,QAiBP;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
1
+ {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useMemo, useRef } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { useDismissStack, useDismissable } from '~/modules/dismiss/DismissStack'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const dialogRef = useRef<HTMLDivElement>(null)\n\n /**\n * Centre the card ONCE when it opens, then hold that position.\n *\n * `my-auto` centred it on every render, so each panel height change pushed both\n * edges and half the delta became upward movement of everything above it —\n * including the tab strip, which moved 113px and took the control the user had\n * just clicked with it (CLS 0.19, \"needs improvement\").\n *\n * Top-anchoring fixed the shift but looked wrong: a short card sat high on the\n * page. So pin the centred offset instead. The card still opens centred, and\n * anything that grows it afterwards extends downward only — which is what a\n * reader expects when they open a tab with more in it.\n *\n * Only on open and on resize. Deliberately NOT on content change: reacting to\n * content is precisely the bug.\n */\n useLayoutEffect(() => {\n const card = dialogRef.current\n const scroller = card?.parentElement\n if (!card || !scroller) return\n\n const pinCentre = () => {\n card.style.marginTop = '0px'\n const style = getComputedStyle(scroller)\n const usable =\n scroller.clientHeight -\n parseFloat(style.paddingTop) -\n parseFloat(style.paddingBottom)\n const slack = usable - card.offsetHeight\n // Negative slack means the card is already taller than the viewport; leave it\n // at the top and let the overlay scroll, rather than pushing its head off.\n card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`\n }\n\n pinCentre()\n window.addEventListener('resize', pinCentre)\n return () => window.removeEventListener('resize', pinCentre)\n // Keyed on the opened entry: a different card is a new open, and should\n // re-centre for its own height.\n }, [app.slug, subResource?.slug])\n\n // Focus the dialog on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n const stack = useDismissStack()\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog || dialog.contains(document.activeElement)) return\n // Only while the card is on top. A layer opened OVER it — a deployment's\n // dialog on a nested route — owns the caret, and on a cold deep link that\n // layer mounts before the card does, so without this check the card took\n // focus back off the dialog's first field the moment the catalog arrived.\n const top = stack.topElement()\n if (top && top !== dialog) return\n dialog.focus()\n }, [shownSlug, stack])\n\n // Escape closes the card. Registered here rather than in AppDetails because\n // this is the layer's element, and because the sub-resource view renders a\n // different body in place of AppDetails — which left that view with no\n // Escape at all while the registration lived in the body.\n useDismissable(dialogRef, onClose)\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n // Centred on open, then held — see the pin effect above. `my-auto` is\n // deliberately absent: it re-centres on EVERY height change, which is the\n // layout-shift bug.\n // `overflow-hidden` belongs on the rounded box: the scrolling child\n // below carries the band's tint to its own square corners and paints\n // them over this radius, so the card's top corners read as cut off.\n className=\"relative w-full max-w-[min(1120px,94vw)] overflow-hidden rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none\"\n >\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"absolute top-3 right-3 z-10 rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n {/* Keyed by the resource on screen, so the body remounts when the card\n navigates from one app to another (\"View replacement\", an access\n prerequisite's parent) instead of updating in place. Everything\n per-app inside it is component state with no reset of its own — the\n roles table's expansion, the icon/screenshot error flags, a draft\n source edit — and it used to arrive at the next app still carrying\n the previous one's, plus the previous scroll offset.\n\n The key is here and not on the outer dialog on purpose: that one\n would restart the open animation and re-run the mount-focus effect\n on every in-card navigation. */}\n <div\n key={shownSlug}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={\n <ResourceDetailAccessActions {...subResourceActionsCtx} />\n }\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -4,20 +4,6 @@ import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { Badge } from "../../../../ui/badge.js";
5
5
  import { Popover, PopoverTrigger, PopoverContent } from "../../../../ui/popover.js";
6
6
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
7
- import "../context/AppCatalogFiltersContext.js";
8
- import "@tanstack/react-router";
9
- import "clsx";
10
- import "tailwind-merge";
11
- import "next-themes";
12
- import "@tanstack/react-query";
13
- import "../../../../ui/button.js";
14
- import "../../../auth/AuthModalContext.js";
15
- import "@radix-ui/react-dialog";
16
- import "../../../config/GlobalConfigContext.js";
17
- import "../../../pluginCore/PluginManagerContext.js";
18
- import "@radix-ui/react-tooltip";
19
- import "../../../../ui/empty.js";
20
- import "../pages/AppCatalogPage.js";
21
7
  import { getPersonBySlug, getGroupBySlug } from "../../utils/resolveHelpers.js";
22
8
  function PersonBadge({ slug }) {
23
9
  const { persons } = useAppCatalogContext();
@@ -1 +1 @@
1
- {"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import type { Group } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Mail, User, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport {\n getGroupBySlug,\n getPersonBySlug,\n} from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * Chip for a slug that may name EITHER a Person or a Group (owner, approvers).\n * Resolves against both collections; falls back to the raw slug so an unknown\n * slug is still visible rather than silently dropped.\n */\nexport function PersonOrGroupBadge({ slug }: PersonBadgeProps) {\n const { persons, groups } = useAppCatalogContext()\n\n if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />\n\n const group = getGroupBySlug(groups, slug)\n if (!group) {\n return (\n <Badge variant=\"outline\" className=\"font-normal\" title={slug}>\n {slug}\n </Badge>\n )\n }\n\n // No group email to copy → show each member as its own clickable/copyable\n // chip instead of one opaque group chip.\n if (!group.email && group.memberSlugs.length > 0) {\n return (\n <span className=\"inline-flex flex-wrap gap-1\">\n {group.memberSlugs.map((memberSlug) => (\n <PersonOrGroupBadge key={memberSlug} slug={memberSlug} />\n ))}\n </span>\n )\n }\n\n return <GroupBadge group={group} />\n}\n\n/** Group chip: name, optional email, and the member names in a popover. */\nfunction GroupBadge({ group }: { group: Group }) {\n const { persons } = useAppCatalogContext()\n const memberNames = group.memberSlugs.map((s) => {\n const p = getPersonBySlug(persons, s)\n return p ? `${p.firstName} ${p.lastName}`.trim() || s : s\n })\n // Many groups (e.g. aws-maintainers-*) have no real displayName set — it's\n // just their slug. Show the member names directly instead of the opaque\n // slug so approvers stay recognizable at a glance, without a popover click.\n const hasRealDisplayName =\n group.displayName && group.displayName !== group.slug\n const displayName = hasRealDisplayName\n ? group.displayName!\n : memberNames.length > 0\n ? memberNames.join(', ')\n : group.slug\n\n if (!group.email && memberNames.length === 0) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <Users className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button type=\"button\" className=\"inline-flex\" title={displayName}>\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <Users className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<Users className=\"size-3.5\" />}\n label=\"Group\"\n value={displayName}\n />\n {group.email && (\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={group.email}\n href={`mailto:${group.email}`}\n />\n )}\n {memberNames.length > 0 && (\n <div className=\"px-2 py-1.5\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n Members\n </span>\n <span className=\"block text-[13px]\">{memberNames.join(', ')}</span>\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import type { Group } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Mail, User, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport {\n getGroupBySlug,\n getPersonBySlug,\n} from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * Chip for a slug that may name EITHER a Person or a Group (owner, approvers).\n * Resolves against both collections; falls back to the raw slug so an unknown\n * slug is still visible rather than silently dropped.\n */\nexport function PersonOrGroupBadge({ slug }: PersonBadgeProps) {\n const { persons, groups } = useAppCatalogContext()\n\n if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />\n\n const group = getGroupBySlug(groups, slug)\n if (!group) {\n return (\n <Badge variant=\"outline\" className=\"font-normal\" title={slug}>\n {slug}\n </Badge>\n )\n }\n\n // No group email to copy → show each member as its own clickable/copyable\n // chip instead of one opaque group chip.\n if (!group.email && group.memberSlugs.length > 0) {\n return (\n <span className=\"inline-flex flex-wrap gap-1\">\n {group.memberSlugs.map((memberSlug) => (\n <PersonOrGroupBadge key={memberSlug} slug={memberSlug} />\n ))}\n </span>\n )\n }\n\n return <GroupBadge group={group} />\n}\n\n/** Group chip: name, optional email, and the member names in a popover. */\nfunction GroupBadge({ group }: { group: Group }) {\n const { persons } = useAppCatalogContext()\n const memberNames = group.memberSlugs.map((s) => {\n const p = getPersonBySlug(persons, s)\n return p ? `${p.firstName} ${p.lastName}`.trim() || s : s\n })\n // Many groups (e.g. aws-maintainers-*) have no real displayName set — it's\n // just their slug. Show the member names directly instead of the opaque\n // slug so approvers stay recognizable at a glance, without a popover click.\n const hasRealDisplayName =\n group.displayName && group.displayName !== group.slug\n const displayName = hasRealDisplayName\n ? group.displayName!\n : memberNames.length > 0\n ? memberNames.join(', ')\n : group.slug\n\n if (!group.email && memberNames.length === 0) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <Users className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button type=\"button\" className=\"inline-flex\" title={displayName}>\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <Users className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<Users className=\"size-3.5\" />}\n label=\"Group\"\n value={displayName}\n />\n {group.email && (\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={group.email}\n href={`mailto:${group.email}`}\n />\n )}\n {memberNames.length > 0 && (\n <div className=\"px-2 py-1.5\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n Members\n </span>\n <span className=\"block text-[13px]\">{memberNames.join(', ')}</span>\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAqBO,SAAS,YAAY,EAAE,QAA0B;AACtD,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,SAAS,gBAAgB,SAAS,IAAI;AAE5C,QAAM,cAAc,SAChB,GAAG,OAAO,SAAS,IAAI,OAAO,QAAQ,GAAG,KAAA,KAAU,OACnD;AACJ,QAAM,QAAQ,iCAAQ;AAGtB,MAAI,CAAC,OAAO;AACV,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QACV,OAAO;AAAA,QAEP,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,UACxB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,8BACG,SAAA,EACC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,GAAG,WAAW,MAAM,KAAK;AAAA,QAEhC,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAQ;AAAA,YACR,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,cACxB;AAAA,cACD,oBAAC,MAAA,EAAK,WAAU,oBAAA,CAAoB;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACtC;AAAA,IAAA,GAEJ;AAAA,IACA,qBAAC,gBAAA,EAAe,OAAM,SAAQ,WAAU,YACtC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO;AAAA,QAAA;AAAA,MAAA;AAAA,MAET;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM,UAAU,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IACvB,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAOO,SAAS,mBAAmB,EAAE,QAA0B;AAC7D,QAAM,EAAE,SAAS,OAAA,IAAW,qBAAA;AAE5B,MAAI,gBAAgB,SAAS,IAAI,EAAG,QAAO,oBAAC,eAAY,MAAY;AAEpE,QAAM,QAAQ,eAAe,QAAQ,IAAI;AACzC,MAAI,CAAC,OAAO;AACV,WACE,oBAAC,SAAM,SAAQ,WAAU,WAAU,eAAc,OAAO,MACrD,UAAA,KAAA,CACH;AAAA,EAEJ;AAIA,MAAI,CAAC,MAAM,SAAS,MAAM,YAAY,SAAS,GAAG;AAChD,WACE,oBAAC,QAAA,EAAK,WAAU,+BACb,gBAAM,YAAY,IAAI,CAAC,mCACrB,oBAAA,EAAoC,MAAM,cAAlB,UAA8B,CACxD,GACH;AAAA,EAEJ;AAEA,SAAO,oBAAC,cAAW,OAAc;AACnC;AAGA,SAAS,WAAW,EAAE,SAA2B;AAC/C,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,cAAc,MAAM,YAAY,IAAI,CAAC,MAAM;AAC/C,UAAM,IAAI,gBAAgB,SAAS,CAAC;AACpC,WAAO,IAAI,GAAG,EAAE,SAAS,IAAI,EAAE,QAAQ,GAAG,KAAA,KAAU,IAAI;AAAA,EAC1D,CAAC;AAID,QAAM,qBACJ,MAAM,eAAe,MAAM,gBAAgB,MAAM;AACnD,QAAM,cAAc,qBAChB,MAAM,cACN,YAAY,SAAS,IACnB,YAAY,KAAK,IAAI,IACrB,MAAM;AAEZ,MAAI,CAAC,MAAM,SAAS,YAAY,WAAW,GAAG;AAC5C,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QACV,OAAO;AAAA,QAEP,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,UACzB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,8BACG,SAAA,EACC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA,oBAAC,UAAA,EAAO,MAAK,UAAS,WAAU,eAAc,OAAO,aACnD,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,UACzB;AAAA,UACD,oBAAC,MAAA,EAAK,WAAU,oBAAA,CAAoB;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAExC,EAAA,CACF;AAAA,IACA,qBAAC,gBAAA,EAAe,OAAM,SAAQ,WAAU,YACtC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,OAAA,EAAM,WAAU,WAAA,CAAW;AAAA,UAClC,OAAM;AAAA,UACN,OAAO;AAAA,QAAA;AAAA,MAAA;AAAA,MAER,MAAM,SACL;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,UACjC,OAAM;AAAA,UACN,OAAO,MAAM;AAAA,UACb,MAAM,UAAU,MAAM,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,MAG9B,YAAY,SAAS,KACpB,qBAAC,OAAA,EAAI,WAAU,eACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,mEAAkE,UAAA,WAElF;AAAA,4BACC,QAAA,EAAK,WAAU,qBAAqB,UAAA,YAAY,KAAK,IAAI,EAAA,CAAE;AAAA,MAAA,EAAA,CAC9D;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;AAGA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD;AAAA,IACE,MAAM,MAAM;AACV,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,WAAK,UAAU,UAAU,UAAU,KAAK;AACxC,gBAAU,IAAI;AACd,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC9D;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,SACE,qBAAC,OAAA,EAAI,WAAU,iEACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,kCAAkC,UAAA,MAAK;AAAA,IACvD,qBAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAU,mEACb,UAAA,OACH;AAAA,MACC,OACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACA,WAAU;AAAA,UAET,UAAA;AAAA,QAAA;AAAA,MAAA,IAGH,oBAAC,QAAA,EAAK,WAAU,8BAA8B,UAAA,MAAA,CAAM;AAAA,IAAA,GAExD;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACV,OAAO,SAAS,YAAY,QAAQ,MAAM,aAAa;AAAA,QACvD,cAAY,QAAQ,MAAM,YAAA,CAAa;AAAA,QAEtC,UAAA,6BACE,OAAA,EAAM,WAAU,2BAA0B,IAE3C,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAE/B,GACF;AAEJ;"}
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useMemo } from "react";
3
+ import { useDismissable } from "../../../dismiss/DismissStack.js";
3
4
  import { Gallery } from "../../../gallery/Gallery.js";
4
5
  import { Dialog, DialogContent, DialogTitle } from "../../../../ui/dialog.js";
5
6
  import { VisuallyHidden } from "../../../../ui/visually-hidden.js";
@@ -11,7 +12,8 @@ function ScreenshotGallery({
11
12
  onOpenChange,
12
13
  title
13
14
  }) {
14
- const isFullscreenRef = useRef(false);
15
+ const contentRef = useRef(null);
16
+ useDismissable(contentRef, () => onOpenChange(false), { enabled: open });
15
17
  const images = useMemo(
16
18
  () => screenshotIds.map((id) => ({
17
19
  url: `/api/screenshots/${id}?size=2000`,
@@ -25,28 +27,13 @@ function ScreenshotGallery({
25
27
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
26
28
  DialogContent,
27
29
  {
30
+ ref: contentRef,
28
31
  "aria-describedby": void 0,
29
32
  className: "h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden",
30
33
  showCloseButton: true,
31
- onEscapeKeyDown: (e) => {
32
- e.stopPropagation();
33
- if (isFullscreenRef.current) {
34
- e.preventDefault();
35
- }
36
- },
37
34
  children: [
38
35
  /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(DialogTitle, { children: title || `${app.abbreviation || app.displayName} screenshots` }) }),
39
- /* @__PURE__ */ jsx(
40
- Gallery,
41
- {
42
- images,
43
- initialIndex,
44
- title,
45
- onFullscreenChange: (fs) => {
46
- isFullscreenRef.current = fs;
47
- }
48
- }
49
- )
36
+ /* @__PURE__ */ jsx(Gallery, { images, initialIndex, title })
50
37
  ]
51
38
  }
52
39
  ) });
@@ -1 +1 @@
1
- {"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n // Track whether Gallery is in fullscreen — if so, block Radix from closing on Escape\n const isFullscreenRef = useRef(false)\n\n // Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen\n // viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads\n // for every slide in the carousel (all slides mount at once, unlazy).\n const images: GalleryImage[] = useMemo(\n () =>\n screenshotIds.map((id) => ({\n url: `/api/screenshots/${id}?size=2000`,\n alt: `${app.abbreviation || app.displayName} screenshot`,\n })),\n [screenshotIds, app.abbreviation, app.displayName],\n )\n\n // Don't render if no screenshots\n if (screenshotIds.length === 0) {\n return null\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-describedby={undefined}\n className=\"h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden\"\n showCloseButton={true}\n onEscapeKeyDown={(e) => {\n // Stop propagation so the Esc event doesn't reach outer document listeners\n // (e.g. SearchResultsList's raw keydown handler, which would clear search).\n e.stopPropagation()\n // If Gallery is in fullscreen, its capture listener already handled Escape.\n // Prevent Radix from also closing the dialog on the same event.\n if (isFullscreenRef.current) {\n e.preventDefault()\n }\n }}\n >\n <VisuallyHidden>\n <DialogTitle>\n {title || `${app.abbreviation || app.displayName} screenshots`}\n </DialogTitle>\n </VisuallyHidden>\n <Gallery\n images={images}\n initialIndex={initialIndex}\n title={title}\n onFullscreenChange={(fs) => {\n isFullscreenRef.current = fs\n }}\n />\n </DialogContent>\n </Dialog>\n )\n}\n"],"names":[],"mappings":";;;;;AAkBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AAEzB,QAAM,kBAAkB,OAAO,KAAK;AAKpC,QAAM,SAAyB;AAAA,IAC7B,MACE,cAAc,IAAI,CAAC,QAAQ;AAAA,MACzB,KAAK,oBAAoB,EAAE;AAAA,MAC3B,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,IAAA,EAC3C;AAAA,IACJ,CAAC,eAAe,IAAI,cAAc,IAAI,WAAW;AAAA,EAAA;AAInD,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,QAAA,EAAO,MAAY,cAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,oBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,iBAAiB;AAAA,MACjB,iBAAiB,CAAC,MAAM;AAGtB,UAAE,gBAAA;AAGF,YAAI,gBAAgB,SAAS;AAC3B,YAAE,eAAA;AAAA,QACJ;AAAA,MACF;AAAA,MAEA,UAAA;AAAA,QAAA,oBAAC,gBAAA,EACC,UAAA,oBAAC,aAAA,EACE,UAAA,SAAS,GAAG,IAAI,gBAAgB,IAAI,WAAW,eAAA,CAClD,GACF;AAAA,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,oBAAoB,CAAC,OAAO;AAC1B,8BAAgB,UAAU;AAAA,YAC5B;AAAA,UAAA;AAAA,QAAA;AAAA,MACF;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\nimport { useDismissable } from '~/modules/dismiss/DismissStack'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n /**\n * The dialog is dismissable through the stack, not through Radix.\n *\n * It no longer needs to know whether the gallery inside it is fullscreen:\n * fullscreen registers above this layer while it is on, so Escape reaches it\n * first and this dialog only hears Escape once fullscreen is off.\n */\n const contentRef = useRef<HTMLDivElement>(null)\n useDismissable(contentRef, () => onOpenChange(false), { enabled: open })\n\n // Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen\n // viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads\n // for every slide in the carousel (all slides mount at once, unlazy).\n const images: GalleryImage[] = useMemo(\n () =>\n screenshotIds.map((id) => ({\n url: `/api/screenshots/${id}?size=2000`,\n alt: `${app.abbreviation || app.displayName} screenshot`,\n })),\n [screenshotIds, app.abbreviation, app.displayName],\n )\n\n // Don't render if no screenshots\n if (screenshotIds.length === 0) {\n return null\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n ref={contentRef}\n aria-describedby={undefined}\n className=\"h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden\"\n showCloseButton={true}\n >\n <VisuallyHidden>\n <DialogTitle>\n {title || `${app.abbreviation || app.displayName} screenshots`}\n </DialogTitle>\n </VisuallyHidden>\n <Gallery images={images} initialIndex={initialIndex} title={title} />\n </DialogContent>\n </Dialog>\n )\n}\n"],"names":[],"mappings":";;;;;;AAmBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AAQzB,QAAM,aAAa,OAAuB,IAAI;AAC9C,iBAAe,YAAY,MAAM,aAAa,KAAK,GAAG,EAAE,SAAS,MAAM;AAKvE,QAAM,SAAyB;AAAA,IAC7B,MACE,cAAc,IAAI,CAAC,QAAQ;AAAA,MACzB,KAAK,oBAAoB,EAAE;AAAA,MAC3B,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,IAAA,EAC3C;AAAA,IACJ,CAAC,eAAe,IAAI,cAAc,IAAI,WAAW;AAAA,EAAA;AAInD,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,QAAA,EAAO,MAAY,cAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,iBAAiB;AAAA,MAEjB,UAAA;AAAA,QAAA,oBAAC,gBAAA,EACC,UAAA,oBAAC,aAAA,EACE,UAAA,SAAS,GAAG,IAAI,gBAAgB,IAAI,WAAW,eAAA,CAClD,GACF;AAAA,QACA,oBAAC,SAAA,EAAQ,QAAgB,cAA4B,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEvE;AAEJ;"}
@@ -1,9 +1,20 @@
1
1
  import { AppApprovalMethod, Resource } from '@igstack/app-catalog-backend-core';
2
+ import { ReactNode } from 'react';
2
3
  interface SubResourceDetailPanelProps {
3
4
  subResource: Resource;
4
5
  parent: Resource;
5
6
  approvalMethods: AppApprovalMethod[];
6
7
  onBack: () => void;
8
+ /**
9
+ * Plugin actions for this sub-resource, rendered under the title.
10
+ *
11
+ * Passed in rather than rendered here on purpose. The slot's payload needs the
12
+ * catalog context and the signed-in user, and calling those hooks in this
13
+ * component would break the tests that render it bare — the same failure mode
14
+ * a `useTRPC()` call has in `AppCatalogGrid`. Optional, so those tests keep
15
+ * passing untouched.
16
+ */
17
+ actions?: ReactNode;
7
18
  }
8
- export declare function SubResourceDetailPanel({ subResource, parent, approvalMethods, onBack, }: SubResourceDetailPanelProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function SubResourceDetailPanel({ subResource, parent, approvalMethods, onBack, actions, }: SubResourceDetailPanelProps): import("react/jsx-runtime").JSX.Element;
9
20
  export {};
@@ -11,7 +11,8 @@ function SubResourceDetailPanel({
11
11
  subResource,
12
12
  parent,
13
13
  approvalMethods,
14
- onBack
14
+ onBack,
15
+ actions
15
16
  }) {
16
17
  const hasAccessInfo = (r) => {
17
18
  var _a;
@@ -64,6 +65,7 @@ function SubResourceDetailPanel({
64
65
  }
65
66
  )
66
67
  ] }),
68
+ actions ? /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-center gap-2", children: actions }) : null,
67
69
  hasAnyAccess && // The whole two-step region carries the anchor: the step labels naming
68
70
  // the parent live out here, not inside the nested AccessRequestSections,
69
71
  // so anchoring only those dropped the parent from the chain.
@@ -1 +1 @@
1
- {"version":3,"file":"SubResourceDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { ArrowLeft, ExternalLink } from 'lucide-react'\nimport { Button } from '~/ui/button'\nimport { Badge } from '~/ui/badge'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { AccessRequestSection } from './AccessRequestSection'\n\ninterface SubResourceDetailPanelProps {\n subResource: Resource\n parent: Resource\n approvalMethods: AppApprovalMethod[]\n onBack: () => void\n}\n\nfunction StepBadge({ step }: { step: number }) {\n return (\n <span className=\"inline-flex items-center justify-center size-6 rounded-full bg-primary text-primary-foreground text-xs font-semibold shrink-0\">\n {step}\n </span>\n )\n}\n\nexport function SubResourceDetailPanel({\n subResource,\n parent,\n approvalMethods,\n onBack,\n}: SubResourceDetailPanelProps) {\n // Mirrors AccessRequestSection's fallback: a sub-resource usually documents\n // access through top-level `approverSlugs`/`accessComments`, not through an\n // `accessRequest`. Only counting the latter hid its step of the chain.\n const hasAccessInfo = (r: Resource) =>\n !!r.accessRequest ||\n (r.approverSlugs?.length ?? 0) > 0 ||\n !!r.ownerPersonSlug ||\n !!r.accessComments\n\n const hasParentAccess = hasAccessInfo(parent)\n const hasSubAccess = hasAccessInfo(subResource)\n const hasTwoStepAccess = hasParentAccess && hasSubAccess\n const hasAnyAccess = hasParentAccess || hasSubAccess\n\n return (\n <div className=\"flex h-full flex-col p-6\">\n {/* Back navigation */}\n <div className=\"mb-4\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onBack}\n className=\"gap-1.5 text-muted-foreground hover:text-foreground -ml-2\"\n aria-label={`Back to ${parent.displayName}`}\n >\n <ArrowLeft className=\"size-4\" />\n <span>{parent.displayName}</span>\n </Button>\n </div>\n\n {/* Sub-resource title */}\n <div className=\"border-b pb-4\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h2 className=\"text-xl font-semibold\">{subResource.displayName}</h2>\n {subResource.tier && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {subResource.tier}\n </Badge>\n )}\n </div>\n {subResource.description && (\n <p className=\"text-sm text-muted-foreground mt-1\">\n {subResource.description}\n </p>\n )}\n {(subResource.aliases ?? []).length > 0 && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n Also known as: {(subResource.aliases ?? []).join(', ')}\n </p>\n )}\n {/* A child's own URL is what the parent's generic one cannot be -- the\n console of THIS account. Keyed on `appUrl`, so every sub-resource\n carrying one is launchable, not only cloud accounts. Deliberately no\n fallback to the parent's URL: landing on the wrong account is the bug\n this exists to fix. */}\n {subResource.appUrl && (\n <a\n href={subResource.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-3 inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors\"\n title={`Open \\u2192 ${displayUrl(subResource.appUrl)}`}\n aria-label={`Open ${subResource.displayName} in a new tab (${displayUrl(subResource.appUrl)})`}\n >\n <ExternalLink className=\"size-3.5 shrink-0\" />\n Open{' '}\n <span className=\"max-w-[240px] truncate text-primary-foreground/70 text-xs font-normal\">\n {displayUrl(subResource.appUrl)}\n </span>\n </a>\n )}\n </div>\n\n {/* Two-step access section */}\n {hasAnyAccess && (\n // The whole two-step region carries the anchor: the step labels naming\n // the parent live out here, not inside the nested AccessRequestSections,\n // so anchoring only those dropped the parent from the chain.\n <div data-access-section=\"\" className=\"mt-6 space-y-6\">\n <h3 className=\"text-sm font-medium\">How to get access</h3>\n\n {hasTwoStepAccess ? (\n <>\n {/* Step 1: Parent access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={1} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {parent.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={parent}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n\n {/* Step 2: Sub-resource access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={2} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {subResource.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={subResource}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n </>\n ) : (\n /* Single-step: only one of them has access info */\n <AccessRequestSection\n app={hasParentAccess ? parent : subResource}\n approvalMethods={approvalMethods}\n />\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SubResourceDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport type { ReactNode } from 'react'\nimport { ArrowLeft, ExternalLink } from 'lucide-react'\nimport { Button } from '~/ui/button'\nimport { Badge } from '~/ui/badge'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { AccessRequestSection } from './AccessRequestSection'\n\ninterface SubResourceDetailPanelProps {\n subResource: Resource\n parent: Resource\n approvalMethods: AppApprovalMethod[]\n onBack: () => void\n /**\n * Plugin actions for this sub-resource, rendered under the title.\n *\n * Passed in rather than rendered here on purpose. The slot's payload needs the\n * catalog context and the signed-in user, and calling those hooks in this\n * component would break the tests that render it bare — the same failure mode\n * a `useTRPC()` call has in `AppCatalogGrid`. Optional, so those tests keep\n * passing untouched.\n */\n actions?: ReactNode\n}\n\nfunction StepBadge({ step }: { step: number }) {\n return (\n <span className=\"inline-flex items-center justify-center size-6 rounded-full bg-primary text-primary-foreground text-xs font-semibold shrink-0\">\n {step}\n </span>\n )\n}\n\nexport function SubResourceDetailPanel({\n subResource,\n parent,\n approvalMethods,\n onBack,\n actions,\n}: SubResourceDetailPanelProps) {\n // Mirrors AccessRequestSection's fallback: a sub-resource usually documents\n // access through top-level `approverSlugs`/`accessComments`, not through an\n // `accessRequest`. Only counting the latter hid its step of the chain.\n const hasAccessInfo = (r: Resource) =>\n !!r.accessRequest ||\n (r.approverSlugs?.length ?? 0) > 0 ||\n !!r.ownerPersonSlug ||\n !!r.accessComments\n\n const hasParentAccess = hasAccessInfo(parent)\n const hasSubAccess = hasAccessInfo(subResource)\n const hasTwoStepAccess = hasParentAccess && hasSubAccess\n const hasAnyAccess = hasParentAccess || hasSubAccess\n\n return (\n <div className=\"flex h-full flex-col p-6\">\n {/* Back navigation */}\n <div className=\"mb-4\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onBack}\n className=\"gap-1.5 text-muted-foreground hover:text-foreground -ml-2\"\n aria-label={`Back to ${parent.displayName}`}\n >\n <ArrowLeft className=\"size-4\" />\n <span>{parent.displayName}</span>\n </Button>\n </div>\n\n {/* Sub-resource title */}\n <div className=\"border-b pb-4\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h2 className=\"text-xl font-semibold\">{subResource.displayName}</h2>\n {subResource.tier && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {subResource.tier}\n </Badge>\n )}\n </div>\n {subResource.description && (\n <p className=\"text-sm text-muted-foreground mt-1\">\n {subResource.description}\n </p>\n )}\n {(subResource.aliases ?? []).length > 0 && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n Also known as: {(subResource.aliases ?? []).join(', ')}\n </p>\n )}\n {/* A child's own URL is what the parent's generic one cannot be -- the\n console of THIS account. Keyed on `appUrl`, so every sub-resource\n carrying one is launchable, not only cloud accounts. Deliberately no\n fallback to the parent's URL: landing on the wrong account is the bug\n this exists to fix. */}\n {subResource.appUrl && (\n <a\n href={subResource.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-3 inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors\"\n title={`Open \\u2192 ${displayUrl(subResource.appUrl)}`}\n aria-label={`Open ${subResource.displayName} in a new tab (${displayUrl(subResource.appUrl)})`}\n >\n <ExternalLink className=\"size-3.5 shrink-0\" />\n Open{' '}\n <span className=\"max-w-[240px] truncate text-primary-foreground/70 text-xs font-normal\">\n {displayUrl(subResource.appUrl)}\n </span>\n </a>\n )}\n </div>\n\n {/* ABOVE the gate below on purpose: an account with no approver, owner\n or access comments renders no access region at all, and that is\n exactly the case where an approver most needs the action. */}\n {actions ? (\n <div className=\"mt-4 flex items-center gap-2\">{actions}</div>\n ) : null}\n\n {/* Two-step access section */}\n {hasAnyAccess && (\n // The whole two-step region carries the anchor: the step labels naming\n // the parent live out here, not inside the nested AccessRequestSections,\n // so anchoring only those dropped the parent from the chain.\n <div data-access-section=\"\" className=\"mt-6 space-y-6\">\n <h3 className=\"text-sm font-medium\">How to get access</h3>\n\n {hasTwoStepAccess ? (\n <>\n {/* Step 1: Parent access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={1} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {parent.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={parent}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n\n {/* Step 2: Sub-resource access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={2} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {subResource.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={subResource}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n </>\n ) : (\n /* Single-step: only one of them has access info */\n <AccessRequestSection\n app={hasParentAccess ? parent : subResource}\n approvalMethods={approvalMethods}\n />\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -3,6 +3,14 @@ interface SubResourcesSectionProps {
3
3
  subResources: Resource[];
4
4
  /** Slug of the parent, for linking to a child's own page. */
5
5
  parentSlug: string;
6
+ /**
7
+ * The parent itself, for the row-actions slot's payload.
8
+ *
9
+ * Optional, and the Admin column is skipped without it: a row action is about
10
+ * a child *of something*, and a plugin deciding whether it applies needs the
11
+ * parent to decide against.
12
+ */
13
+ parent?: Resource;
6
14
  /**
7
15
  * Initial filter text (#38 item C). When the user reached this app by
8
16
  * searching a term that matched a sub-resource, seed the sub-resource filter
@@ -11,6 +19,14 @@ interface SubResourcesSectionProps {
11
19
  * account is pre-filtered). Only applied when it actually matches a child.
12
20
  */
13
21
  initialSearch?: string;
22
+ /**
23
+ * What this app calls its children, from the parent's `childrenLabel`.
24
+ *
25
+ * The tab above already uses it, so without it here the tab and the panel it
26
+ * opens disagree — "AWS Accounts" over a heading that says "Sub-Resources".
27
+ * Defaults to the same generic wording the tab falls back to.
28
+ */
29
+ childrenLabel?: string;
14
30
  }
15
- export declare function SubResourcesSection({ subResources, parentSlug, initialSearch, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
31
+ export declare function SubResourcesSection({ subResources, parentSlug, parent, initialSearch, childrenLabel, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
16
32
  export {};