@igstack/app-catalog-frontend-core 6.10.0 → 6.10.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.
@@ -5,6 +5,7 @@ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
5
5
  import { useOptionalUser } from "../../../auth/AuthContext.js";
6
6
  import "@tanstack/react-query";
7
7
  import "../../../../ui/button.js";
8
+ import "../../../auth/AuthModalContext.js";
8
9
  import { useDismissStack, useDismissable } from "../../../dismiss/DismissStack.js";
9
10
  import { getChildResources } from "../../utils/resolveHelpers.js";
10
11
  import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
@@ -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 { Maximize2, Minimize2, X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } 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 // Not persisted: every open starts at the normal width. The toggle is for the\n // one card being read, and the default stays readable for the next one.\n const [isWide, setIsWide] = useState(false)\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 card 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 // The door link rather than the dialog itself when the app has one: Enter on\n // a search result opens the card, so a second Enter is the browser's own link\n // activation and takes the reader into the app. No door: the dialog.\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 const target = dialog.querySelector<HTMLElement>('.app-door') ?? dialog\n target.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 // Widening drops the cap only. The pinned top margin is left alone, so\n // the card's top edge stays put and restoring gives back the same layout.\n className={`relative w-full ${isWide ? 'max-w-none' : '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 <div className=\"absolute top-3 right-3 z-10 flex gap-1\">\n <button\n type=\"button\"\n aria-label={isWide ? 'Restore width' : 'Widen card'}\n title={isWide ? 'Restore width' : 'Widen card'}\n onClick={() => setIsWide((wide) => !wide)}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n {isWide ? (\n <Minimize2 className=\"size-4\" />\n ) : (\n <Maximize2 className=\"size-4\" />\n )}\n </button>\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"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 </div>\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":";;;;;;;;;;;;AA6BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,OAAO,gBAAA;AAIb,QAAM,wBAAwB;AAAA,IAC5B,OAAO;AAAA,MACL,UAAU,eAAe;AAAA,MACzB,QAAQ,cAAc,MAAM;AAAA,MAC5B,cAAc,kBAAkB,cAAc,IAAI,IAAI;AAAA,MACtD,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU;AAAA,IAAA;AAAA,IAEvE,CAAC,aAAa,KAAK,cAAc,IAAI;AAAA,EAAA;AAEvC,QAAM,YAAY,OAAuB,IAAI;AAG7C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAkB1C,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;AAkBhC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAC3C,QAAM,QAAQ,gBAAA;AACd,YAAU,MAAM;AACd,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,UAAU,OAAO,SAAS,SAAS,aAAa,EAAG;AAKxD,UAAM,MAAM,MAAM,WAAA;AAClB,QAAI,OAAO,QAAQ,OAAQ;AAC3B,UAAM,SAAS,OAAO,cAA2B,WAAW,KAAK;AACjE,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,WAAW,KAAK,CAAC;AAMrB,iBAAe,WAAW,OAAO;AAEjC,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,QASV,WAAW,mBAAmB,SAAS,eAAe,0BAA0B;AAAA,QAEhF,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,SAAS,kBAAkB;AAAA,gBACvC,OAAO,SAAS,kBAAkB;AAAA,gBAClC,SAAS,MAAM,UAAU,CAAC,SAAS,CAAC,IAAI;AAAA,gBACxC,WAAU;AAAA,gBAET,UAAA,6BACE,WAAA,EAAU,WAAU,UAAS,IAE9B,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS;AAAA,cAAA;AAAA,YAAA;AAAA,YAGlC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAW;AAAA,gBACX,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,UAAA,oBAAC,GAAA,EAAE,WAAU,SAAA,CAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACxB,GACF;AAAA,UAYA;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cAKV,UAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,UAAU,eAAe;AAAA,kBACzB,QAAQ,cAAc,MAAM;AAAA,kBAE3B,UAAA,cACC;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC;AAAA,sBACA,QAAQ;AAAA,sBACR;AAAA,sBACA,QAAQ,kBAAkB;AAAA,sBAC1B,SACE,oBAAC,6BAAA,EAA6B,GAAG,sBAAA,CAAuB;AAAA,oBAAA;AAAA,kBAAA,IAI5D,oBAAC,YAAA,EAAW,KAAU,WAAA,CAAwB;AAAA,gBAAA;AAAA,cAAA;AAAA,YAElD;AAAA,YAvBK;AAAA,UAAA;AAAA,QAwBP;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 { Maximize2, Minimize2, X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } 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 // Not persisted: every open starts at the normal width. The toggle is for the\n // one card being read, and the default stays readable for the next one.\n const [isWide, setIsWide] = useState(false)\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 card 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 // The door link rather than the dialog itself when the app has one: Enter on\n // a search result opens the card, so a second Enter is the browser's own link\n // activation and takes the reader into the app. No door: the dialog.\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 const target = dialog.querySelector<HTMLElement>('.app-door') ?? dialog\n target.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 // Widening drops the cap only. The pinned top margin is left alone, so\n // the card's top edge stays put and restoring gives back the same layout.\n className={`relative w-full ${isWide ? 'max-w-none' : '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 <div className=\"absolute top-3 right-3 z-10 flex gap-1\">\n <button\n type=\"button\"\n aria-label={isWide ? 'Restore width' : 'Widen card'}\n title={isWide ? 'Restore width' : 'Widen card'}\n onClick={() => setIsWide((wide) => !wide)}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n {isWide ? (\n <Minimize2 className=\"size-4\" />\n ) : (\n <Maximize2 className=\"size-4\" />\n )}\n </button>\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"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 </div>\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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"}
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import { useSearch } from "@tanstack/react-router";
3
3
  import { ExternalLink, Trash2, Plus } from "lucide-react";
4
4
  import React__default from "react";
@@ -12,6 +12,7 @@ import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
12
12
  import { PersonBadge } from "../components/PersonBadge.js";
13
13
  import { useUser } from "../../../auth/AuthContext.js";
14
14
  import "@tanstack/react-query";
15
+ import "../../../auth/AuthModalContext.js";
15
16
  import { InlineEditableField } from "../components/InlineEditableField.js";
16
17
  import { MarkdownText } from "../components/MarkdownText.js";
17
18
  import { ScreenshotGallery } from "../components/ScreenshotGallery.js";
@@ -281,8 +282,8 @@ function AppDetails({
281
282
  ariaLabel: behindCount ? `Behind this card, ${behindCount} waiting for review` : void 0,
282
283
  render: () => {
283
284
  var _a2;
284
- return /* @__PURE__ */ jsx("div", { className: "behind-panel", children: /* @__PURE__ */ jsxs("div", { className: "behind-layout", children: [
285
- /* @__PURE__ */ jsx("div", { className: "behind-main", children: /* @__PURE__ */ jsx(
285
+ return /* @__PURE__ */ jsxs("div", { className: "behind-panel", children: [
286
+ /* @__PURE__ */ jsx(
286
287
  CommunitySection,
287
288
  {
288
289
  ref: knowMoreRef,
@@ -290,115 +291,12 @@ function AppDetails({
290
291
  appName: app.displayName,
291
292
  claims: app.claims
292
293
  }
293
- ) }),
294
- /* @__PURE__ */ jsxs("div", { className: "behind-aside", children: [
295
- (isAdmin || sourceUrls.length > 0 || app.createdAt || ((_a2 = app.freshness) == null ? void 0 : _a2.lastCheckedAt)) && /* @__PURE__ */ jsxs(Group, { title: "Kept up to date from", children: [
296
- isAdmin ? /* @__PURE__ */ jsxs(Fragment, { children: [
297
- /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: displaySources.map((url, index) => {
298
- const isDraft = draftSource !== null && index === sourceUrls.length;
299
- return /* @__PURE__ */ jsxs(
300
- "li",
301
- {
302
- className: "group flex items-center gap-2 text-xs",
303
- children: [
304
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0 tabular-nums", children: [
305
- index + 1,
306
- "."
307
- ] }),
308
- /* @__PURE__ */ jsx(
309
- InlineEditableField,
310
- {
311
- value: url,
312
- initialEditMode: isDraft,
313
- onCancel: isDraft ? () => setDraftSource(null) : void 0,
314
- onSave: (newUrl) => {
315
- if (isDraft) {
316
- setDraftSource(null);
317
- if (newUrl) {
318
- updateApp.mutate({
319
- id: app.id,
320
- data: {
321
- sources: [...sourceUrls, newUrl]
322
- }
323
- });
324
- }
325
- } else {
326
- const next = [...sourceUrls];
327
- next[index] = newUrl;
328
- updateApp.mutate({
329
- id: app.id,
330
- data: { sources: next.filter(Boolean) }
331
- });
332
- }
333
- },
334
- placeholder: "https://...",
335
- viewClassName: "flex-1 min-w-0",
336
- renderView: (val) => val ? /* @__PURE__ */ jsxs(
337
- "a",
338
- {
339
- href: val,
340
- target: "_blank",
341
- rel: "noopener noreferrer",
342
- className: "hover:text-primary inline-flex items-center gap-1 truncate",
343
- children: [
344
- displayUrl(val),
345
- /* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
346
- ]
347
- }
348
- ) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" })
349
- }
350
- ),
351
- !isDraft && pulseByUrl.has(url) && /* @__PURE__ */ jsx(
352
- SourcePulse,
353
- {
354
- pulse: pulseByUrl.get(url),
355
- className: "ml-auto"
356
- }
357
- ),
358
- !isDraft && /* @__PURE__ */ jsx(
359
- Button,
360
- {
361
- type: "button",
362
- variant: "ghost",
363
- size: "icon-sm",
364
- "aria-label": "Remove source",
365
- className: "shrink-0 text-muted-foreground hover:text-destructive",
366
- onClick: () => {
367
- const next = sourceUrls.filter(
368
- (_, i) => i !== index
369
- );
370
- updateApp.mutate({
371
- id: app.id,
372
- data: { sources: next }
373
- });
374
- },
375
- children: /* @__PURE__ */ jsx(Trash2, { className: "size-3.5" })
376
- }
377
- )
378
- ]
379
- },
380
- isDraft ? "draft" : `${index}-${url}`
381
- );
382
- }) }),
383
- /* @__PURE__ */ jsxs(
384
- Button,
385
- {
386
- type: "button",
387
- variant: "ghost",
388
- size: "sm",
389
- className: "mt-2 gap-1 text-muted-foreground",
390
- onClick: () => setDraftSource(""),
391
- children: [
392
- /* @__PURE__ */ jsx(Plus, { className: "size-3.5" }),
393
- "Add source"
394
- ]
395
- }
396
- )
397
- ] }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: sourceUrls.map((url, index) => (
398
- // `group` so hovering anywhere on the row fades that source's
399
- // track in — the mark sits in a right-hand column, and row
400
- // hover is what ties the two ends of a wide row together.
401
- /* @__PURE__ */ jsxs(
294
+ ),
295
+ (isAdmin || sourceUrls.length > 0 || app.createdAt || ((_a2 = app.freshness) == null ? void 0 : _a2.lastCheckedAt)) && /* @__PURE__ */ jsxs(Group, { title: "Kept up to date from", children: [
296
+ isAdmin ? /* @__PURE__ */ jsxs(Fragment, { children: [
297
+ /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: displaySources.map((url, index) => {
298
+ const isDraft = draftSource !== null && index === sourceUrls.length;
299
+ return /* @__PURE__ */ jsxs(
402
300
  "li",
403
301
  {
404
302
  className: "group flex items-center gap-2 text-xs",
@@ -407,52 +305,151 @@ function AppDetails({
407
305
  index + 1,
408
306
  "."
409
307
  ] }),
410
- url ? /* @__PURE__ */ jsxs(
411
- "a",
308
+ /* @__PURE__ */ jsx(
309
+ InlineEditableField,
412
310
  {
413
- href: url,
414
- target: "_blank",
415
- rel: "noopener noreferrer",
416
- className: "hover:text-primary inline-flex min-w-0 items-center gap-1 truncate",
417
- children: [
418
- displayUrl(url),
419
- /* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
420
- ]
311
+ value: url,
312
+ initialEditMode: isDraft,
313
+ onCancel: isDraft ? () => setDraftSource(null) : void 0,
314
+ onSave: (newUrl) => {
315
+ if (isDraft) {
316
+ setDraftSource(null);
317
+ if (newUrl) {
318
+ updateApp.mutate({
319
+ id: app.id,
320
+ data: { sources: [...sourceUrls, newUrl] }
321
+ });
322
+ }
323
+ } else {
324
+ const next = [...sourceUrls];
325
+ next[index] = newUrl;
326
+ updateApp.mutate({
327
+ id: app.id,
328
+ data: { sources: next.filter(Boolean) }
329
+ });
330
+ }
331
+ },
332
+ placeholder: "https://...",
333
+ viewClassName: "flex-1 min-w-0",
334
+ renderView: (val) => val ? /* @__PURE__ */ jsxs(
335
+ "a",
336
+ {
337
+ href: val,
338
+ target: "_blank",
339
+ rel: "noopener noreferrer",
340
+ className: "hover:text-primary inline-flex items-center gap-1 truncate",
341
+ children: [
342
+ displayUrl(val),
343
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
344
+ ]
345
+ }
346
+ ) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" })
421
347
  }
422
- ) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }),
423
- pulseByUrl.has(url) && /* @__PURE__ */ jsx(
348
+ ),
349
+ !isDraft && pulseByUrl.has(url) && /* @__PURE__ */ jsx(
424
350
  SourcePulse,
425
351
  {
426
352
  pulse: pulseByUrl.get(url),
427
353
  className: "ml-auto"
428
354
  }
355
+ ),
356
+ !isDraft && /* @__PURE__ */ jsx(
357
+ Button,
358
+ {
359
+ type: "button",
360
+ variant: "ghost",
361
+ size: "icon-sm",
362
+ "aria-label": "Remove source",
363
+ className: "shrink-0 text-muted-foreground hover:text-destructive",
364
+ onClick: () => {
365
+ const next = sourceUrls.filter(
366
+ (_, i) => i !== index
367
+ );
368
+ updateApp.mutate({
369
+ id: app.id,
370
+ data: { sources: next }
371
+ });
372
+ },
373
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-3.5" })
374
+ }
429
375
  )
430
376
  ]
431
377
  },
432
- index
433
- )
434
- )) }),
435
- datesLine && /* @__PURE__ */ jsx("p", { className: "behind-dates", title: datesTitle, children: datesLine })
436
- ] }),
437
- (app.aiPrompt || app.aiMemory) && /* @__PURE__ */ jsxs(Group, { title: "How the AI maintains this card", children: [
438
- app.aiPrompt && /* @__PURE__ */ jsxs(Fragment, { children: [
439
- /* @__PURE__ */ jsx("p", { className: "behind-ai__label", children: "Prompt" }),
440
- /* @__PURE__ */ jsx("p", { className: "behind-ai", children: app.aiPrompt })
441
- ] }),
442
- app.aiMemory && /* @__PURE__ */ jsxs(Fragment, { children: [
443
- /* @__PURE__ */ jsx("p", { className: "behind-ai__label", children: "Memory" }),
444
- /* @__PURE__ */ jsx("p", { className: "behind-ai", children: app.aiMemory })
445
- ] })
378
+ isDraft ? "draft" : `${index}-${url}`
379
+ );
380
+ }) }),
381
+ /* @__PURE__ */ jsxs(
382
+ Button,
383
+ {
384
+ type: "button",
385
+ variant: "ghost",
386
+ size: "sm",
387
+ className: "mt-2 gap-1 text-muted-foreground",
388
+ onClick: () => setDraftSource(""),
389
+ children: [
390
+ /* @__PURE__ */ jsx(Plus, { className: "size-3.5" }),
391
+ "Add source"
392
+ ]
393
+ }
394
+ )
395
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: sourceUrls.map((url, index) => (
396
+ // `group` so hovering anywhere on the row fades that source's
397
+ // track in — the mark sits in a right-hand column, and row
398
+ // hover is what ties the two ends of a wide row together.
399
+ /* @__PURE__ */ jsxs(
400
+ "li",
401
+ {
402
+ className: "group flex items-center gap-2 text-xs",
403
+ children: [
404
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0 tabular-nums", children: [
405
+ index + 1,
406
+ "."
407
+ ] }),
408
+ url ? /* @__PURE__ */ jsxs(
409
+ "a",
410
+ {
411
+ href: url,
412
+ target: "_blank",
413
+ rel: "noopener noreferrer",
414
+ className: "hover:text-primary inline-flex min-w-0 items-center gap-1 truncate",
415
+ children: [
416
+ displayUrl(url),
417
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
418
+ ]
419
+ }
420
+ ) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }),
421
+ pulseByUrl.has(url) && /* @__PURE__ */ jsx(
422
+ SourcePulse,
423
+ {
424
+ pulse: pulseByUrl.get(url),
425
+ className: "ml-auto"
426
+ }
427
+ )
428
+ ]
429
+ },
430
+ index
431
+ )
432
+ )) }),
433
+ datesLine && /* @__PURE__ */ jsx("p", { className: "behind-dates", title: datesTitle, children: datesLine })
434
+ ] }),
435
+ (app.aiPrompt || app.aiMemory) && /* @__PURE__ */ jsxs(Group, { title: "How the AI maintains this card", children: [
436
+ app.aiPrompt && /* @__PURE__ */ jsxs(Fragment, { children: [
437
+ /* @__PURE__ */ jsx("p", { className: "behind-ai__label", children: "Prompt" }),
438
+ /* @__PURE__ */ jsx("p", { className: "behind-ai", children: app.aiPrompt })
446
439
  ] }),
447
- /* @__PURE__ */ jsx("div", { className: "behind-card-source", children: /* @__PURE__ */ jsx(
448
- ResourceDetailCardSource,
449
- {
450
- appSlug: app.slug,
451
- appName: app.displayName
452
- }
453
- ) })
454
- ] })
455
- ] }) });
440
+ app.aiMemory && /* @__PURE__ */ jsxs(Fragment, { children: [
441
+ /* @__PURE__ */ jsx("p", { className: "behind-ai__label", children: "Memory" }),
442
+ /* @__PURE__ */ jsx("p", { className: "behind-ai", children: app.aiMemory })
443
+ ] })
444
+ ] }),
445
+ /* @__PURE__ */ jsx("div", { className: "behind-card-source", children: /* @__PURE__ */ jsx(
446
+ ResourceDetailCardSource,
447
+ {
448
+ appSlug: app.slug,
449
+ appName: app.displayName
450
+ }
451
+ ) })
452
+ ] });
456
453
  }
457
454
  }
458
455
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <div className=\"behind-layout\">\n <div className=\"behind-main\">\n <CommunitySection\n ref={knowMoreRef}\n appSlug={app.slug}\n appName={app.displayName}\n claims={app.claims}\n />\n </div>\n\n <div className=\"behind-aside\">\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft\n ? () => setDraftSource(null)\n : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: {\n sources: [...sourceUrls, newUrl],\n },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">\n —\n </span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
1
+ {"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n appSlug={app.slug}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,MAAM;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,eAAe;AAAA,EACf,QAAQ;AACV;AAMA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AACF,GAGG;AACD,SACE,qBAAC,WAAA,EAAQ,WAAU,gBACjB,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,uBAAuB,UAAA,OAAM;AAAA,IAC1C;AAAA,EAAA,GACH;AAEJ;AAGA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE,qBAAC,OAAA,EAAI,WAAW,QAAQ,KAAK,QAC3B,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,wBAAwB,UAAA,OAAM;AAAA,IAC3C,QAAQ,oBAAC,KAAA,EAAE,WAAU,sCAAsC,UAAA,MAAK;AAAA,IAChE;AAAA,EAAA,GACH;AAEJ;AAEA,SAAS,cAAc,EAAE,OAA0B;;AACjD,QAAM,CAAC,YAAY,aAAa,IAAIA,eAAM,SAAS,KAAK;AACxD,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,eAAM,SAAS,IAAI;AAE/D,QAAM,gBAAe,SAAI,kBAAJ,mBAAoB;AACzC,MAAI,CAAC,cAAc;AACjB,WACE,oBAAC,SAAI,WAAU,2FACb,8BAAC,OAAA,EAAI,WAAU,0FAAyF,UAAA,0BAAA,CAExG,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,qBAAqB,oBAAoB,YAAY;AAE3D,6BACG,OAAA,EAAI,WAAU,8BACb,UAAA,qBAAC,OAAA,EAAI,WAAU,+EACZ,UAAA;AAAA,IAAA,CAAC,aACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,QAC3C,WAAU;AAAA,QACV,SAAS,MAAM;AACb,wBAAc,IAAI;AAClB,4BAAkB,KAAK;AAAA,QACzB;AAAA,QACA,QAAQ,MAAM,kBAAkB,KAAK;AAAA,MAAA;AAAA,IAAA,IAErC;AAAA,KACF,cAAc,mBACd,oBAAC,OAAA,EAAI,WAAU,0FACZ,UAAA,iBACG,0BACA,0BAAA,CACN;AAAA,EAAA,EAAA,CAEJ,EAAA,CACF;AAEJ;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAGG;;AACD,QAAM,CAAC,eAAe,gBAAgB,IAAIA,eAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,qBAAqB,sBAAsB,IAAIA,eAAM,SAAS,CAAC;AAEtE,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,EAAE,YAAA,IAAgB,mBAAA;AACxB,QAAM,YAAY,aAAA;AAClB,QAAM,CAAC,aAAa,cAAc,IAAIA,eAAM,SAAwB,IAAI;AACxE,QAAM,OAAO,QAAA;AACb,QAAM,WAAU,6BAAM,YAAW;AACjC,QAAM,EAAE,OAAO,YAAA,IAAgB,qBAAA;AAE/B,QAAM,cAAc,gBAAgB,IAAI,MAAM,IAAI,MAAM;AAGxD,QAAM,eAAc,SAAI,cAAJ,mBAAe;AACnC,QAAM,YAAY,CAAA;AAClB,MAAI,aAAa;AACf,UAAM,WAAS,SAAI,cAAJ,mBAAe,WAAU,0BAA0B;AAClE,cAAU,KAAK,gBAAgB,mBAAmB,WAAW,CAAC,GAAG,MAAM,EAAE;AAAA,EAC3E;AACA,MAAI,IAAI,WAAW;AACjB,UAAM,aAAa,UAAU,SAAS,IAAI,UAAU;AACpD,cAAU,KAAK,GAAG,UAAU,IAAI,mBAAmB,IAAI,SAAS,CAAC,EAAE;AAAA,EACrE;AACA,QAAM,YAAY,UAAU,KAAK,KAAK;AACtC,QAAM,aAAa;AAAA,MACjB,SAAI,cAAJ,mBAAe,wBACb,wBAAwB,IAAI,UAAU,mBAAmB;AAAA,IAC3D,eAAe,gBAAgB,WAAW;AAAA,IAC1C,IAAI,aAAa,SAAS,IAAI,SAAS;AAAA,EAAA,EAEtC,OAAO,OAAO,EACd,KAAK,KAAK;AAMb,QAAM,cAAcA,eAAM,OAA+B,IAAI;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAIA,eAAM,SAAS,CAAC;AAChDA,iBAAM,UAAU,MAAM;;AACpB,QAAI,SAAU,EAAAC,MAAA,YAAY,YAAZ,gBAAAA,IAAqB;AAAA,EACrC,GAAG,CAAC,QAAQ,CAAC;AAKb,QAAM,SAAS,UAAU,EAAE,QAAQ,OAAO;AAC1C,QAAM,UAAU,OAAO;AAEvB,QAAM,eAAe,IAAI,QAAQ,CAAA,GAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,SAAS,GAAG,CAAC;AACvE,QAAM,eACJ,SAAI,YAAJ,mBAAa,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,IAAI,EAAE,SAAS,CAAA;AAClE,QAAM,iBACJ,gBAAgB,OAAO,CAAC,GAAG,YAAY,WAAW,IAAI;AAIxD,QAAM,aAAaD,eAAM,QAAQ,MAAM;AACrC,UAAM,4BAAY,IAAA;AAClB,eAAW,UAAU,IAAI,WAAW,CAAA,GAAI;AACtC,UAAI,OAAO,WAAW,YAAY,OAAO,OAAO;AAC9C,cAAM,IAAI,OAAO,KAAK,OAAO,KAAK;AAAA,MACpC;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,OAAO,CAAC;AAEhB,QAAM,WAAWA,eAAM;AAAA,IACrB,MAAM,kBAAkB,cAAc,IAAI,IAAI;AAAA,IAC9C,CAAC,cAAc,IAAI,IAAI;AAAA,EAAA;AAMzB,QAAM,SAASA,eAAM;AAAA,IACnB,MACE,IAAI,aACA,aAAa,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,UAAU,IAClD;AAAA,IACN,CAAC,cAAc,IAAI,UAAU;AAAA,EAAA;AAK/B,QAAM,mBAAmBA,eAAM;AAAA,IAC7B,OAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,cAAc,SACV,kBAAkB,cAAc,OAAO,IAAI,IAC3C;AAAA,MACJ,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,YAAY;AAAA,IAAA;AAAA,IAEhD,CAAC,KAAK,QAAQ,cAAc,UAAU,MAAM,OAAO;AAAA,EAAA;AAKrD;AAAA,IACE;AAAA,IACA,MAAM;;AACJ,YAAM,OAAMC,MAAA,SAAS,kBAAT,gBAAAA,IAAwB;AACpC,UACE,QAAQ,YACR,QAAQ,OACR,QAAQ,WACR,QAAQ,YACR,QAAQ;AAER;AAEF,UAAI,IAAI,iBAAiB,IAAI,cAAc,SAAS,GAAG;AACrD,+BAAuB,CAAC;AACxB,yBAAiB,IAAI;AAAA,MACvB;AAAA,IACF;AAAA,IACA,EAAE,SAAS,CAAC,cAAA;AAAA,IACZ,CAAC,KAAK,aAAa;AAAA,EAAA;AAGrB,QAAM,wBAAwB,CAAC,UAAkB;AAC/C,2BAAuB,KAAK;AAC5B,qBAAiB,IAAI;AAAA,EACvB;AAGA,QAAM,mBAAiB,SAAI,eAAJ,mBAAgB,mBACnC,aAAa,KAAK,CAAC,MAAA;;AAAM,aAAE,WAASA,MAAA,IAAI,eAAJ,gBAAAA,IAAgB;AAAA,GAAe,IACnE;AAEJ,QAAM,SAAS,eAAe,KAAK,eAAe;AAElD,QAAM,kBAAkB,SAAS,SAAS,QAAM,SAAI,UAAJ,mBAAW,WAAU,KAAK;AAC1E,QAAM,iBAAiB,SAAS,SAC3B,IAAI,iBAAiB,cACtB;AAEJ,QAAM,yBAAuB,SAAI,UAAJ,mBAAW,WAAU,KAAK,KAAK,CAAC,CAAC,IAAI;AAElE,QAAM,OAAoB;AAAA,IACxB;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO;AAAA,MACP,QAAQ,MACN,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,SAAA,EAAQ,OAAM,eAAc,OAAK,MAC/B,UAAA,UACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI,eAAe;AAAA,YAC1B,QAAQ,CAAC,gBACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,YAAA,GAAe;AAAA,YAExD,WAAS;AAAA,YACT,aAAY;AAAA,YACZ,WAAU;AAAA,UAAA;AAAA,QAAA,IAEV,IAAI,cACN,oBAAC,gBAAa,WAAU,qEACrB,UAAA,IAAI,YAAA,CACP,IAEA,oBAAC,KAAA,EAAE,WAAU,iCAAgC,eAAC,GAElD;AAAA,QAEC,IAAI,iBAAiB,IAAI,cAAc,SAAS,KAC/C,oBAAC,SAAA,EAAQ,OAAO,gBAAgB,IAAI,cAAc,MAAM,KAGtD,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,uBAAuB,IAAI,WAAW;AAAA,YAClD,WAAU;AAAA,YACV,SAAS,MAAM,sBAAsB,CAAC;AAAA,YAEtC,UAAA;AAAA,cAAA,oBAAC,iBAAc,KAAU;AAAA,cACxB,IAAI,cAAc,SAAS,KAC1B,qBAAC,KAAA,EAAE,WAAU,kDAAiD,UAAA;AAAA,gBAAA;AAAA,gBACzC,IAAI,cAAc;AAAA,gBAAO;AAAA,cAAA,EAAA,CAC9C;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,GAGN;AAAA,QAKD,IAAI,mBACH,oBAAC,SAAA,EAAQ,OAAM,SAAQ,MAAK,wCAC1B,UAAA,oBAAC,aAAA,EAAY,MAAM,IAAI,iBAAiB,GAC1C;AAAA,QAOD,YAAY,SAAS,KACpB,oBAAC,SAAA,EAAQ,OAAM,QACb,UAAA,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,YAAY,IAAI,CAAC,QAChB,oBAAC,OAAA,EAAgB,SAAQ,aAAY,WAAU,WAC5C,UAAA,IAAA,GADS,GAEZ,CACD,EAAA,CACH,EAAA,CACF;AAAA,QAGD,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,SAAA,EAAQ,OAAM,SACb,UAAA,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,IAAI,MAAM,IAAI,CAAC,SACd,oBAAC,OAAA,EAAiB,SAAQ,WAAU,WAAU,WAC3C,UAAA,KAAA,GADS,IAEZ,CACD,GACH,EAAA,CACF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA;AAAA,IAGJ;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO,YAAY,OAAO,KAAK;AAAA,MAC/B,MAAM,OAAO;AAAA,MACb,QAAQ,MACN,qBAAA,UAAA,EAGE,UAAA;AAAA,QAAA,oBAAC,yBAAA,EAAwB,UAAU,KAAK,cAAc,YAAY;AAAA,QAClE,oBAAC,sBAAA,EAAqB,KAAU,gBAAA,CAAkC;AAAA,MAAA,EAAA,CACpE;AAAA,IAAA;AAAA,IAGJ,GAAI,kBACA;AAAA,MACE;AAAA,QACE,IAAI,IAAI;AAAA,QACR,OAAO;AAAA,QACP,MAAM,SAAS,SAAS,OAAO,SAAS,MAAM,IAAI;AAAA,QAClD,QAAQ,MAAA;;AACN,sCAAA,UAAA,EACG,UAAA;AAAA,YAAA,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,qBAAA,EAAoB,OAAO,IAAI,MAAA,CAAO;AAAA,YAExC,SAAS,SAAS,KACjB,oBAAC,OAAA,EAAI,aAAWA,MAAA,IAAI,UAAJ,gBAAAA,IAAW,UAAS,SAAS,IAI3C,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,cAAc;AAAA,gBACd,YAAY,IAAI;AAAA,gBAChB,QAAQ;AAAA,gBACR,eAAe,YAAY;AAAA,gBAC3B,eAAe,IAAI;AAAA,cAAA;AAAA,YAAA,EACrB,CACF;AAAA,UAAA,EAAA,CAEJ;AAAA;AAAA,MAAA;AAAA,IAEJ,IAEF,CAAA;AAAA,IACJ,GAAI,sBACA;AAAA,MACE;AAAA,QACE,IAAI,IAAI;AAAA,QACR,OAAO;AAAA,QACP,QAAQ,MAAA;;AACN,sCAAA,UAAA,EACG,UAAA;AAAA,YAAA,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,WAAQ,OAAM,iBAAgB,OAAK,MAClC,UAAA,oBAAC,SAAI,WAAU,eACZ,cAAI,MAAM,IAAI,CAAC,SACd;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,MAAM,KAAK;AAAA,gBACX,QAAO;AAAA,gBACP,KAAI;AAAA,gBACJ,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAA,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,kBACzC,KAAK,SAAS,WAAW,KAAK,GAAG;AAAA,gBAAA;AAAA,cAAA;AAAA,cAP7B,KAAK;AAAA,YAAA,CASb,GACH,EAAA,CACF;AAAA,YAKD,IAAI,cACH,oBAAC,SAAA,EAAQ,OAAM,cAAa,OAAO,GAACA,MAAA,IAAI,UAAJ,gBAAAA,IAAW,SAC7C,UAAA,oBAAC,cAAA,EAAa,WAAU,uGACrB,UAAA,IAAI,YACP,EAAA,CACF;AAAA,UAAA,EAAA,CAEJ;AAAA;AAAA,MAAA;AAAA,IAEJ,IAEF,CAAA;AAAA,IACJ;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO;AAAA,MACP,MAAM,cAAc,OAAO,WAAW,IAAI;AAAA,MAC1C,WAAW,cACP,qBAAqB,WAAW,wBAChC;AAAA,MACJ,QAAQ,MAAA;;AACN,oCAAC,OAAA,EAAI,WAAU,gBACb,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,SAAS,IAAI;AAAA,cACb,SAAS,IAAI;AAAA,cACb,QAAQ,IAAI;AAAA,YAAA;AAAA,UAAA;AAAA,WAGZ,WACA,WAAW,SAAS,KACpB,IAAI,eACJA,MAAA,IAAI,cAAJ,gBAAAA,IAAe,mBACf,qBAAC,OAAA,EAAM,OAAM,wBACV,UAAA;AAAA,YAAA,UACC,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,QAAG,WAAU,aACX,yBAAe,IAAI,CAAC,KAAK,UAAU;AAClC,sBAAM,UACJ,gBAAgB,QAAQ,UAAU,WAAW;AAC/C,uBACE;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBAEC,WAAU;AAAA,oBAEV,UAAA;AAAA,sBAAA,qBAAC,QAAA,EAAK,WAAU,+CACb,UAAA;AAAA,wBAAA,QAAQ;AAAA,wBAAE;AAAA,sBAAA,GACb;AAAA,sBACA;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,OAAO;AAAA,0BACP,iBAAiB;AAAA,0BACjB,UACE,UAAU,MAAM,eAAe,IAAI,IAAI;AAAA,0BAEzC,QAAQ,CAAC,WAAW;AAClB,gCAAI,SAAS;AACX,6CAAe,IAAI;AACnB,kCAAI,QAAQ;AACV,0CAAU,OAAO;AAAA,kCACf,IAAI,IAAI;AAAA,kCACR,MAAM,EAAE,SAAS,CAAC,GAAG,YAAY,MAAM,EAAA;AAAA,gCAAE,CAC1C;AAAA,8BACH;AAAA,4BACF,OAAO;AACL,oCAAM,OAAO,CAAC,GAAG,UAAU;AAC3B,mCAAK,KAAK,IAAI;AACd,wCAAU,OAAO;AAAA,gCACf,IAAI,IAAI;AAAA,gCACR,MAAM,EAAE,SAAS,KAAK,OAAO,OAAO,EAAA;AAAA,8BAAE,CACvC;AAAA,4BACH;AAAA,0BACF;AAAA,0BACA,aAAY;AAAA,0BACZ,eAAc;AAAA,0BACd,YAAY,CAAC,QACX,MACE;AAAA,4BAAC;AAAA,4BAAA;AAAA,8BACC,MAAM;AAAA,8BACN,QAAO;AAAA,8BACP,KAAI;AAAA,8BACJ,WAAU;AAAA,8BAET,UAAA;AAAA,gCAAA,WAAW,GAAG;AAAA,gCACf,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,8BAAA;AAAA,4BAAA;AAAA,0BAAA,IAG5C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC;AAAA,wBAAA;AAAA,sBAAA;AAAA,sBAI9C,CAAC,WAAW,WAAW,IAAI,GAAG,KAC7B;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,OAAO,WAAW,IAAI,GAAG;AAAA,0BACzB,WAAU;AAAA,wBAAA;AAAA,sBAAA;AAAA,sBAGb,CAAC,WACA;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,MAAK;AAAA,0BACL,SAAQ;AAAA,0BACR,MAAK;AAAA,0BACL,cAAW;AAAA,0BACX,WAAU;AAAA,0BACV,SAAS,MAAM;AACb,kCAAM,OAAO,WAAW;AAAA,8BACtB,CAAC,GAAG,MAAM,MAAM;AAAA,4BAAA;AAElB,sCAAU,OAAO;AAAA,8BACf,IAAI,IAAI;AAAA,8BACR,MAAM,EAAE,SAAS,KAAA;AAAA,4BAAK,CACvB;AAAA,0BACH;AAAA,0BAEA,UAAA,oBAAC,QAAA,EAAO,WAAU,WAAA,CAAW;AAAA,wBAAA;AAAA,sBAAA;AAAA,oBAC/B;AAAA,kBAAA;AAAA,kBAxEG,UAAU,UAAU,GAAG,KAAK,IAAI,GAAG;AAAA,gBAAA;AAAA,cA4E9C,CAAC,EAAA,CACH;AAAA,cACA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,SAAS,MAAM,eAAe,EAAE;AAAA,kBAEhC,UAAA;AAAA,oBAAA,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,oBAAE;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,YAE/B,EAAA,CACF,wBAEC,MAAA,EAAG,WAAU,aACX,UAAA,WAAW,IAAI,CAAC,KAAK;AAAA;AAAA;AAAA;AAAA,cAIpB;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBAEC,WAAU;AAAA,kBAEV,UAAA;AAAA,oBAAA,qBAAC,QAAA,EAAK,WAAU,+CACb,UAAA;AAAA,sBAAA,QAAQ;AAAA,sBAAE;AAAA,oBAAA,GACb;AAAA,oBACC,MACC;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,MAAM;AAAA,wBACN,QAAO;AAAA,wBACP,KAAI;AAAA,wBACJ,WAAU;AAAA,wBAET,UAAA;AAAA,0BAAA,WAAW,GAAG;AAAA,0BACf,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,wBAAA;AAAA,sBAAA;AAAA,oBAAA,IAG5C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,oBAE1C,WAAW,IAAI,GAAG,KACjB;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,OAAO,WAAW,IAAI,GAAG;AAAA,wBACzB,WAAU;AAAA,sBAAA;AAAA,oBAAA;AAAA,kBACZ;AAAA,gBAAA;AAAA,gBAvBG;AAAA,cAAA;AAAA,aA0BR,GACH;AAAA,YAED,aACC,oBAAC,KAAA,EAAE,WAAU,gBAAe,OAAO,YAChC,UAAA,UAAA,CACH;AAAA,UAAA,GAEJ;AAAA,WAGA,IAAI,YAAY,IAAI,aACpB,qBAAC,OAAA,EAAM,OAAM,kCACV,UAAA;AAAA,YAAA,IAAI,YACH,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,KAAA,EAAE,WAAU,oBAAmB,UAAA,UAAM;AAAA,cACtC,oBAAC,KAAA,EAAE,WAAU,aAAa,cAAI,SAAA,CAAS;AAAA,YAAA,GACzC;AAAA,YAED,IAAI,YACH,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,KAAA,EAAE,WAAU,oBAAmB,UAAA,UAAM;AAAA,cACtC,oBAAC,KAAA,EAAE,WAAU,aAAa,cAAI,SAAA,CAAS;AAAA,YAAA,EAAA,CACzC;AAAA,UAAA,GAEJ;AAAA,UAGF,oBAAC,OAAA,EAAI,WAAU,sBACb,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS,IAAI;AAAA,cACb,SAAS,IAAI;AAAA,YAAA;AAAA,UAAA,EACf,CACF;AAAA,QAAA,EAAA,CACF;AAAA;AAAA,IAAA;AAAA,EAEJ;AAaF,QAAM,OACJ,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,+CACb,UAAA;AAAA,MAAA,oBAAC,WAAQ,KAAU,QAAQ,MAAM,YAAY,IAAI,IAAI,GAAG;AAAA,MACvD,IAAI,cACH;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SACE,IAAI,WAAW,SAAS,gBACpB,cACA;AAAA,UAGL,UAAA,IAAI,WAAW,SAAS,gBACrB,gBACA;AAAA,QAAA;AAAA,MAAA;AAAA,MAOR,oBAAC,SAAI,WAAU,yCACb,8BAAC,6BAAA,EAA6B,GAAG,kBAAkB,EAAA,CACrD;AAAA,IAAA,GACF;AAAA,IAEC,IAAI,aAAa,IAAI,UAAU,SAAS,KACvC,qBAAC,KAAA,EAAE,WAAU,wCAAuC,UAAA;AAAA,MAAA;AAAA,MACnC,IAAI,UAAU,KAAK,IAAI;AAAA,IAAA,GACxC;AAAA,IAGD,WACC,qBAAC,OAAA,EAAI,WAAU,oDACb,UAAA;AAAA,MAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,iCAAgC,UAAA,SAAK;AAAA,QACrD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI;AAAA,YACX,QAAQ,CAAC,SACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,KAAA,GAAQ;AAAA,YAEjD,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MACA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,iCAAgC,UAAA,QAAI;AAAA,QACpD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI,UAAU;AAAA,YACrB,QAAQ,CAAC,WACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,OAAA,GAAU;AAAA,YAEnD,aAAY;AAAA,YACZ,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,EAAA,CACF;AAAA,IAAA,GACF;AAAA,IAGD,cAAc,GAAG,KAChB,qBAAC,OAAA,EAAI,WAAU,QAGb,UAAA;AAAA,MAAA,oBAAC,KAAA,EAAE,WAAU,wCAAuC,UAAA,+BAEpD;AAAA,MACA,oBAAC,gBAAa,IAAA,CAAU;AAAA,IAAA,GAC1B;AAAA,IAKD,IAAI,eACF,MAAM;AACL,YAAM,gBAAgB,IAAI,WAAW,SAAS;AAC9C,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WACE,gBACI,uFACA;AAAA,UAGN,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WACE,gBACI,8DACA;AAAA,gBAGL,0BACG,sBACA;AAAA,cAAA;AAAA,YAAA;AAAA,gCAEL,KAAA,EAAE,WAAU,iCACV,UAAA,IAAI,WAAW,SAClB;AAAA,YACC,kBACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,yCAAa;AAAA,gBAC5B,WAAU;AAAA,gBACX,UAAA;AAAA,kBAAA;AAAA,kBACoB,eAAe;AAAA,kBAClC,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UACnC;AAAA,QAAA;AAAA,MAAA;AAAA,IAIR,GAAA;AAAA,EAAG,GACP;AAGF,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,wBACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QAGA,eAAc;AAAA,QAGd,WAAW,WAAW,kBAAkB,IAAI,YAAY;AAAA,QACxD,UAAU,CAAC,WACT;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,cAAW;AAAA,YACX,SAAS,MAAM;AACb,qBAAO,IAAI,MAAM;AACjB,0BAAY,CAAC,MAAM,IAAI,CAAC;AAAA,YAC1B;AAAA,YAEA,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,kBAAiB,UAAA,uBAAmB;AAAA,cACpD,oBAAC,MAAA,EAAK,WAAU,oBAAmB,eAAY,OAAA,CAAO;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACxD;AAAA,IAAA,GAGN;AAAA,IAGA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,eAAe,IAAI,iBAAiB,CAAA;AAAA,QACpC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,QACd,OAAO,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAC/C,GACF;AAEJ;"}
package/dist/index.css CHANGED
@@ -479,31 +479,7 @@
479
479
  Comments come first; each later group sits under a hairline with generous
480
480
  room either side, and a plain title. */
481
481
  .behind-panel {
482
- container-type: inline-size;
483
- container-name: behind;
484
- }
485
- @container behind (min-width: 900px) {
486
- .behind-layout {
487
- display: grid;
488
- grid-template-columns: minmax(0, 68ch) minmax(240px, 1fr);
489
- column-gap: 3.5rem;
490
- }
491
- .behind-main {
492
- grid-column: 1;
493
- }
494
- .behind-aside {
495
- grid-column: 2;
496
- }
497
- .behind-aside .behind-group {
498
- margin-top: 0;
499
- padding-top: 0;
500
- border-top: none;
501
- }
502
- .behind-aside .behind-group + .behind-group {
503
- margin-top: 2.5rem;
504
- padding-top: 0;
505
- border-top: none;
506
- }
482
+ max-width: 62ch;
507
483
  }
508
484
  .behind-group + .behind-group {
509
485
  margin-top: 3.5rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "6.10.0",
3
+ "version": "6.10.1",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -118,7 +118,7 @@
118
118
  "tailwindcss": "^4.1.7",
119
119
  "tiptap-markdown": "^0.8.0",
120
120
  "tslib": "^2.3.0",
121
- "@igstack/app-catalog-shared-core": "6.10.0"
121
+ "@igstack/app-catalog-shared-core": "6.10.1"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@tailwindcss/vite": "^4.0.5",
@@ -146,7 +146,7 @@
146
146
  "vite-plugin-static-copy": "^3.1.4",
147
147
  "vite-plugin-svgr": "^4.2.0",
148
148
  "vitest": "^4.1.2",
149
- "@igstack/app-catalog-backend-core": "6.10.0"
149
+ "@igstack/app-catalog-backend-core": "6.10.1"
150
150
  },
151
151
  "peerDependencies": {
152
152
  "@tanstack/react-router": "1.170.41",
@@ -156,7 +156,7 @@
156
156
  "vite": "^6.3.5",
157
157
  "vite-plugin-svgr": "^4.2.0"
158
158
  },
159
- "gitHead": "8a9a5fb554ef3414c474c608d32f830907dfcc72",
159
+ "gitHead": "4c83cecec5cf3d8a70d28faad5a7e773adadaea5",
160
160
  "scripts": {
161
161
  "build": "vite build",
162
162
  "build:lenient": "vite build --mode lenient",
package/src/index.css CHANGED
@@ -479,31 +479,7 @@
479
479
  Comments come first; each later group sits under a hairline with generous
480
480
  room either side, and a plain title. */
481
481
  .behind-panel {
482
- container-type: inline-size;
483
- container-name: behind;
484
- }
485
- @container behind (min-width: 900px) {
486
- .behind-layout {
487
- display: grid;
488
- grid-template-columns: minmax(0, 68ch) minmax(240px, 1fr);
489
- column-gap: 3.5rem;
490
- }
491
- .behind-main {
492
- grid-column: 1;
493
- }
494
- .behind-aside {
495
- grid-column: 2;
496
- }
497
- .behind-aside .behind-group {
498
- margin-top: 0;
499
- padding-top: 0;
500
- border-top: none;
501
- }
502
- .behind-aside .behind-group + .behind-group {
503
- margin-top: 2.5rem;
504
- padding-top: 0;
505
- border-top: none;
506
- }
482
+ max-width: 62ch;
507
483
  }
508
484
  .behind-group + .behind-group {
509
485
  margin-top: 3.5rem;
@@ -456,193 +456,181 @@ export function AppDetails({
456
456
  : undefined,
457
457
  render: () => (
458
458
  <div className="behind-panel">
459
- <div className="behind-layout">
460
- <div className="behind-main">
461
- <CommunitySection
462
- ref={knowMoreRef}
463
- appSlug={app.slug}
464
- appName={app.displayName}
465
- claims={app.claims}
466
- />
467
- </div>
459
+ <CommunitySection
460
+ ref={knowMoreRef}
461
+ appSlug={app.slug}
462
+ appName={app.displayName}
463
+ claims={app.claims}
464
+ />
468
465
 
469
- <div className="behind-aside">
470
- {(isAdmin ||
471
- sourceUrls.length > 0 ||
472
- app.createdAt ||
473
- app.freshness?.lastCheckedAt) && (
474
- <Group title="Kept up to date from">
475
- {isAdmin ? (
476
- <>
477
- <ul className="space-y-2">
478
- {displaySources.map((url, index) => {
479
- const isDraft =
480
- draftSource !== null && index === sourceUrls.length
481
- return (
482
- <li
483
- key={isDraft ? 'draft' : `${index}-${url}`}
484
- className="group flex items-center gap-2 text-xs"
485
- >
486
- <span className="text-muted-foreground shrink-0 tabular-nums">
487
- {index + 1}.
488
- </span>
489
- <InlineEditableField
490
- value={url}
491
- initialEditMode={isDraft}
492
- onCancel={
493
- isDraft
494
- ? () => setDraftSource(null)
495
- : undefined
496
- }
497
- onSave={(newUrl) => {
498
- if (isDraft) {
499
- setDraftSource(null)
500
- if (newUrl) {
501
- updateApp.mutate({
502
- id: app.id,
503
- data: {
504
- sources: [...sourceUrls, newUrl],
505
- },
506
- })
507
- }
508
- } else {
509
- const next = [...sourceUrls]
510
- next[index] = newUrl
511
- updateApp.mutate({
512
- id: app.id,
513
- data: { sources: next.filter(Boolean) },
514
- })
515
- }
516
- }}
517
- placeholder="https://..."
518
- viewClassName="flex-1 min-w-0"
519
- renderView={(val) =>
520
- val ? (
521
- <a
522
- href={val}
523
- target="_blank"
524
- rel="noopener noreferrer"
525
- className="hover:text-primary inline-flex items-center gap-1 truncate"
526
- >
527
- {displayUrl(val)}
528
- <ExternalLink className="size-3 shrink-0" />
529
- </a>
530
- ) : (
531
- <span className="text-muted-foreground">
532
- —
533
- </span>
534
- )
535
- }
536
- />
537
- {!isDraft && pulseByUrl.has(url) && (
538
- <SourcePulse
539
- pulse={pulseByUrl.get(url)!}
540
- className="ml-auto"
541
- />
542
- )}
543
- {!isDraft && (
544
- <Button
545
- type="button"
546
- variant="ghost"
547
- size="icon-sm"
548
- aria-label="Remove source"
549
- className="shrink-0 text-muted-foreground hover:text-destructive"
550
- onClick={() => {
551
- const next = sourceUrls.filter(
552
- (_, i) => i !== index,
553
- )
554
- updateApp.mutate({
555
- id: app.id,
556
- data: { sources: next },
557
- })
558
- }}
559
- >
560
- <Trash2 className="size-3.5" />
561
- </Button>
562
- )}
563
- </li>
564
- )
565
- })}
566
- </ul>
567
- <Button
568
- type="button"
569
- variant="ghost"
570
- size="sm"
571
- className="mt-2 gap-1 text-muted-foreground"
572
- onClick={() => setDraftSource('')}
573
- >
574
- <Plus className="size-3.5" />
575
- Add source
576
- </Button>
577
- </>
578
- ) : (
579
- <ul className="space-y-2">
580
- {sourceUrls.map((url, index) => (
581
- // `group` so hovering anywhere on the row fades that source's
582
- // track in — the mark sits in a right-hand column, and row
583
- // hover is what ties the two ends of a wide row together.
466
+ {(isAdmin ||
467
+ sourceUrls.length > 0 ||
468
+ app.createdAt ||
469
+ app.freshness?.lastCheckedAt) && (
470
+ <Group title="Kept up to date from">
471
+ {isAdmin ? (
472
+ <>
473
+ <ul className="space-y-2">
474
+ {displaySources.map((url, index) => {
475
+ const isDraft =
476
+ draftSource !== null && index === sourceUrls.length
477
+ return (
584
478
  <li
585
- key={index}
479
+ key={isDraft ? 'draft' : `${index}-${url}`}
586
480
  className="group flex items-center gap-2 text-xs"
587
481
  >
588
482
  <span className="text-muted-foreground shrink-0 tabular-nums">
589
483
  {index + 1}.
590
484
  </span>
591
- {url ? (
592
- <a
593
- href={url}
594
- target="_blank"
595
- rel="noopener noreferrer"
596
- className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
597
- >
598
- {displayUrl(url)}
599
- <ExternalLink className="size-3 shrink-0" />
600
- </a>
601
- ) : (
602
- <span className="text-muted-foreground">—</span>
603
- )}
604
- {pulseByUrl.has(url) && (
485
+ <InlineEditableField
486
+ value={url}
487
+ initialEditMode={isDraft}
488
+ onCancel={
489
+ isDraft ? () => setDraftSource(null) : undefined
490
+ }
491
+ onSave={(newUrl) => {
492
+ if (isDraft) {
493
+ setDraftSource(null)
494
+ if (newUrl) {
495
+ updateApp.mutate({
496
+ id: app.id,
497
+ data: { sources: [...sourceUrls, newUrl] },
498
+ })
499
+ }
500
+ } else {
501
+ const next = [...sourceUrls]
502
+ next[index] = newUrl
503
+ updateApp.mutate({
504
+ id: app.id,
505
+ data: { sources: next.filter(Boolean) },
506
+ })
507
+ }
508
+ }}
509
+ placeholder="https://..."
510
+ viewClassName="flex-1 min-w-0"
511
+ renderView={(val) =>
512
+ val ? (
513
+ <a
514
+ href={val}
515
+ target="_blank"
516
+ rel="noopener noreferrer"
517
+ className="hover:text-primary inline-flex items-center gap-1 truncate"
518
+ >
519
+ {displayUrl(val)}
520
+ <ExternalLink className="size-3 shrink-0" />
521
+ </a>
522
+ ) : (
523
+ <span className="text-muted-foreground">—</span>
524
+ )
525
+ }
526
+ />
527
+ {!isDraft && pulseByUrl.has(url) && (
605
528
  <SourcePulse
606
529
  pulse={pulseByUrl.get(url)!}
607
530
  className="ml-auto"
608
531
  />
609
532
  )}
533
+ {!isDraft && (
534
+ <Button
535
+ type="button"
536
+ variant="ghost"
537
+ size="icon-sm"
538
+ aria-label="Remove source"
539
+ className="shrink-0 text-muted-foreground hover:text-destructive"
540
+ onClick={() => {
541
+ const next = sourceUrls.filter(
542
+ (_, i) => i !== index,
543
+ )
544
+ updateApp.mutate({
545
+ id: app.id,
546
+ data: { sources: next },
547
+ })
548
+ }}
549
+ >
550
+ <Trash2 className="size-3.5" />
551
+ </Button>
552
+ )}
610
553
  </li>
611
- ))}
612
- </ul>
613
- )}
614
- {datesLine && (
615
- <p className="behind-dates" title={datesTitle}>
616
- {datesLine}
617
- </p>
618
- )}
619
- </Group>
554
+ )
555
+ })}
556
+ </ul>
557
+ <Button
558
+ type="button"
559
+ variant="ghost"
560
+ size="sm"
561
+ className="mt-2 gap-1 text-muted-foreground"
562
+ onClick={() => setDraftSource('')}
563
+ >
564
+ <Plus className="size-3.5" />
565
+ Add source
566
+ </Button>
567
+ </>
568
+ ) : (
569
+ <ul className="space-y-2">
570
+ {sourceUrls.map((url, index) => (
571
+ // `group` so hovering anywhere on the row fades that source's
572
+ // track in — the mark sits in a right-hand column, and row
573
+ // hover is what ties the two ends of a wide row together.
574
+ <li
575
+ key={index}
576
+ className="group flex items-center gap-2 text-xs"
577
+ >
578
+ <span className="text-muted-foreground shrink-0 tabular-nums">
579
+ {index + 1}.
580
+ </span>
581
+ {url ? (
582
+ <a
583
+ href={url}
584
+ target="_blank"
585
+ rel="noopener noreferrer"
586
+ className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
587
+ >
588
+ {displayUrl(url)}
589
+ <ExternalLink className="size-3 shrink-0" />
590
+ </a>
591
+ ) : (
592
+ <span className="text-muted-foreground">—</span>
593
+ )}
594
+ {pulseByUrl.has(url) && (
595
+ <SourcePulse
596
+ pulse={pulseByUrl.get(url)!}
597
+ className="ml-auto"
598
+ />
599
+ )}
600
+ </li>
601
+ ))}
602
+ </ul>
603
+ )}
604
+ {datesLine && (
605
+ <p className="behind-dates" title={datesTitle}>
606
+ {datesLine}
607
+ </p>
620
608
  )}
609
+ </Group>
610
+ )}
621
611
 
622
- {(app.aiPrompt || app.aiMemory) && (
623
- <Group title="How the AI maintains this card">
624
- {app.aiPrompt && (
625
- <>
626
- <p className="behind-ai__label">Prompt</p>
627
- <p className="behind-ai">{app.aiPrompt}</p>
628
- </>
629
- )}
630
- {app.aiMemory && (
631
- <>
632
- <p className="behind-ai__label">Memory</p>
633
- <p className="behind-ai">{app.aiMemory}</p>
634
- </>
635
- )}
636
- </Group>
612
+ {(app.aiPrompt || app.aiMemory) && (
613
+ <Group title="How the AI maintains this card">
614
+ {app.aiPrompt && (
615
+ <>
616
+ <p className="behind-ai__label">Prompt</p>
617
+ <p className="behind-ai">{app.aiPrompt}</p>
618
+ </>
619
+ )}
620
+ {app.aiMemory && (
621
+ <>
622
+ <p className="behind-ai__label">Memory</p>
623
+ <p className="behind-ai">{app.aiMemory}</p>
624
+ </>
637
625
  )}
626
+ </Group>
627
+ )}
638
628
 
639
- <div className="behind-card-source">
640
- <ResourceDetailCardSource
641
- appSlug={app.slug}
642
- appName={app.displayName}
643
- />
644
- </div>
645
- </div>
629
+ <div className="behind-card-source">
630
+ <ResourceDetailCardSource
631
+ appSlug={app.slug}
632
+ appName={app.displayName}
633
+ />
646
634
  </div>
647
635
  </div>
648
636
  ),