@igstack/app-catalog-frontend-core 6.10.3 → 6.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/modules/auth/AuthContext.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/likedBy.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/timeAgo.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/useTimeUp.test.d.ts +1 -0
- package/dist/esm/api/infra/trpc.d.ts +419 -0
- package/dist/esm/api/infra/trpc.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.d.ts +2 -4
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +30 -101
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.d.ts +26 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js +108 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.d.ts +8 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js +73 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +20 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +14 -5
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +2 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +18 -9
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +4 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +10 -4
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +12 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +4 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js +16 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js +2 -2
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.js +7 -3
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/ui/LoginModal.js +16 -8
- package/dist/esm/modules/auth/ui/LoginModal.js.map +1 -1
- package/dist/esm/modules/communityTools/likedBy.d.ts +8 -0
- package/dist/esm/modules/communityTools/likedBy.js +23 -0
- package/dist/esm/modules/communityTools/likedBy.js.map +1 -0
- package/dist/esm/modules/communityTools/timeAgo.d.ts +2 -0
- package/dist/esm/modules/communityTools/timeAgo.js +18 -0
- package/dist/esm/modules/communityTools/timeAgo.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js +112 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.d.ts +8 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js +59 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js +547 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.d.ts +41 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js +252 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.d.ts +17 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js +84 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.d.ts +7 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js +50 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.d.ts +9 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js +56 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.d.ts +18 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js +115 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js +88 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.d.ts +56 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js +245 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/PinButton.d.ts +14 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js +72 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js +81 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.d.ts +21 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js +87 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagRow.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js +92 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/Toast.d.ts +19 -0
- package/dist/esm/modules/communityTools/ui/Toast.js +87 -0
- package/dist/esm/modules/communityTools/ui/Toast.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js +468 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.d.ts +32 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js +766 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.d.ts +15 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js +92 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityTools.d.ts +159 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js +123 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.d.ts +10 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js +20 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js.map +1 -0
- package/dist/esm/modules/communityTools/useTimeUp.d.ts +5 -0
- package/dist/esm/modules/communityTools/useTimeUp.js +13 -0
- package/dist/esm/modules/communityTools/useTimeUp.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +2 -2
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +1 -1
- package/dist/esm/routeTree.gen.d.ts +96 -3
- package/dist/esm/routeTree.gen.js +19 -0
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/community.$slug.d.ts +22 -0
- package/dist/esm/routes/_layout/community._slug.js +10 -0
- package/dist/esm/routes/_layout/community._slug.js.map +1 -0
- package/dist/esm/routes/_layout/community.d.ts +23 -0
- package/dist/esm/routes/_layout/community.js +71 -0
- package/dist/esm/routes/_layout/community.js.map +1 -0
- package/dist/esm/ui/components/header/Header.js +7 -3
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/util/highlightMatches.d.ts +2 -0
- package/dist/esm/util/highlightMatches.js +32 -0
- package/dist/esm/util/highlightMatches.js.map +1 -0
- package/dist/index.css +6 -0
- package/package.json +5 -5
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +4 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +10 -3
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +12 -2
- package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +1 -0
- package/src/__tests__/modules/auth/AuthContext.test.tsx +55 -0
- package/src/__tests__/modules/communityTools/likedBy.test.ts +45 -0
- package/src/__tests__/modules/communityTools/timeAgo.test.ts +28 -0
- package/src/__tests__/modules/communityTools/useTimeUp.test.ts +31 -0
- package/src/__tests__/ui/components/header/Header.login.test.tsx +4 -4
- package/src/api/infra/trpc.ts +2 -0
- package/src/index.css +6 -0
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +33 -176
- package/src/modules/appCatalog/ui/catalog/DetailDialogShell.tsx +199 -0
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -1
- package/src/modules/appCatalog/ui/components/SectionTabs.tsx +81 -0
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +17 -2
- package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +20 -5
- package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +6 -3
- package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +21 -10
- package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +10 -5
- package/src/modules/appCatalog/ui/detail/communityDraft.ts +16 -0
- package/src/modules/appCatalog/ui/detail/composeComments.ts +1 -0
- package/src/modules/appCatalog/ui/detail/useCommunityToolCount.ts +18 -0
- package/src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx +2 -2
- package/src/modules/auth/AuthContext.tsx +9 -3
- package/src/modules/auth/ui/LoginModal.tsx +7 -2
- package/src/modules/communityTools/likedBy.ts +32 -0
- package/src/modules/communityTools/timeAgo.ts +16 -0
- package/src/modules/communityTools/ui/CardScreenshot.tsx +124 -0
- package/src/modules/communityTools/ui/CardTitleBlock.tsx +58 -0
- package/src/modules/communityTools/ui/CommunityPage.tsx +626 -0
- package/src/modules/communityTools/ui/FeedRow.tsx +303 -0
- package/src/modules/communityTools/ui/FeedSortControl.tsx +97 -0
- package/src/modules/communityTools/ui/LifecycleBadge.tsx +44 -0
- package/src/modules/communityTools/ui/LikeButton.tsx +56 -0
- package/src/modules/communityTools/ui/MarginPreview.tsx +143 -0
- package/src/modules/communityTools/ui/MyToolsShelf.tsx +112 -0
- package/src/modules/communityTools/ui/OwnerPicker.tsx +311 -0
- package/src/modules/communityTools/ui/PinButton.tsx +87 -0
- package/src/modules/communityTools/ui/ShelfTile.tsx +83 -0
- package/src/modules/communityTools/ui/TagPicker.tsx +118 -0
- package/src/modules/communityTools/ui/TagRow.tsx +118 -0
- package/src/modules/communityTools/ui/Toast.tsx +105 -0
- package/src/modules/communityTools/ui/ToolCard.tsx +519 -0
- package/src/modules/communityTools/ui/ToolFormDialog.tsx +922 -0
- package/src/modules/communityTools/ui/ToolSearch.tsx +111 -0
- package/src/modules/communityTools/useCommunityTools.ts +146 -0
- package/src/modules/communityTools/useCommunityToolsConfig.ts +26 -0
- package/src/modules/communityTools/useTimeUp.ts +14 -0
- package/src/routeTree.gen.ts +52 -0
- package/src/routes/_layout/community.$slug.tsx +7 -0
- package/src/routes/_layout/community.tsx +97 -0
- package/src/ui/components/header/Header.tsx +9 -3
- package/src/util/highlightMatches.tsx +34 -0
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +0 -12
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +0 -56
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +0 -1
- package/src/modules/appCatalog/ui/components/ViewToggle.tsx +0 -59
|
@@ -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":"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"}
|
|
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 { useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\nimport { DetailDialogShell } from './DetailDialogShell'\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 * The overlay itself — scrim, centring, Widen, Close, Escape — is\n * `DetailDialogShell`, shared with the community tool card.\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 shownSlug = subResource?.slug ?? app.slug\n\n return (\n <DetailDialogShell\n label={`${subResource?.displayName ?? app.displayName} details`}\n shownKey={shownSlug}\n // Wide enough for data tables (sub-resources have 5 columns incl.\n // \"Access Contacts\"/\"AWS Account\" that wrapped at 760px); prose inside\n // AppDetails is width-capped separately so it stays readable. Caps at\n // 94vw so it never touches the edges.\n maxWidthClass=\"max-w-[min(1120px,94vw)]\"\n focusSelector=\".app-door\"\n onClose={onClose}\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={<ResourceDetailAccessActions {...subResourceActionsCtx} />}\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </DetailDialogShell>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AA0BO,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,aAAY,2CAAa,SAAQ,IAAI;AAE3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,MACrD,UAAU;AAAA,MAKV,eAAc;AAAA,MACd,eAAc;AAAA,MACd;AAAA,MAKA,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,UAAU,eAAe;AAAA,UACzB,QAAQ,cAAc,MAAM;AAAA,UAE3B,UAAA,cACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR;AAAA,cACA,QAAQ,kBAAkB;AAAA,cAC1B,SAAS,oBAAC,6BAAA,EAA6B,GAAG,sBAAA,CAAuB;AAAA,YAAA;AAAA,UAAA,IAGnE,oBAAC,YAAA,EAAW,KAAU,WAAA,CAAwB;AAAA,QAAA;AAAA,MAAA;AAAA,IAElD;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay a detail card lives in: dimmed page, a card centred once on
|
|
3
|
+
* open, Widen and Close at its top right, and a body that scrolls inside it.
|
|
4
|
+
* The official app card and the community tool card both sit in it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function DetailDialogShell({ label, shownKey, maxWidthClass, focusSelector, controls, testId, onClose, onKeyDown, children, }: {
|
|
7
|
+
/** The dialog's accessible name. */
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* What the card shows. A new value is a new open: the card re-centres for
|
|
11
|
+
* its own height, takes the caret back, and the body remounts.
|
|
12
|
+
*/
|
|
13
|
+
shownKey: string;
|
|
14
|
+
/** The width cap while not widened, e.g. `max-w-[min(1120px,94vw)]`. */
|
|
15
|
+
maxWidthClass: string;
|
|
16
|
+
/** What takes the caret on open, when it exists; otherwise the dialog. */
|
|
17
|
+
focusSelector?: string;
|
|
18
|
+
/** Extra header buttons, placed before Widen. */
|
|
19
|
+
controls?: React.ReactNode;
|
|
20
|
+
testId?: string;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
/** The round header button style, for a card's own `controls`. */
|
|
26
|
+
export declare const DIALOG_ROUND_BUTTON = "rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Minimize2, Maximize2, X } from "lucide-react";
|
|
3
|
+
import { useRef, useState, useLayoutEffect, useEffect } from "react";
|
|
4
|
+
import { useDismissStack, useDismissable } from "../../../dismiss/DismissStack.js";
|
|
5
|
+
const ROUND_BUTTON = "rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors";
|
|
6
|
+
function DetailDialogShell({
|
|
7
|
+
label,
|
|
8
|
+
shownKey,
|
|
9
|
+
maxWidthClass,
|
|
10
|
+
focusSelector,
|
|
11
|
+
controls,
|
|
12
|
+
testId,
|
|
13
|
+
onClose,
|
|
14
|
+
onKeyDown,
|
|
15
|
+
children
|
|
16
|
+
}) {
|
|
17
|
+
const dialogRef = useRef(null);
|
|
18
|
+
const [isWide, setIsWide] = useState(false);
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
const card = dialogRef.current;
|
|
21
|
+
const scroller = card == null ? void 0 : card.parentElement;
|
|
22
|
+
if (!card || !scroller) return;
|
|
23
|
+
const pinCentre = () => {
|
|
24
|
+
card.style.marginTop = "0px";
|
|
25
|
+
const style = getComputedStyle(scroller);
|
|
26
|
+
const usable = scroller.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
27
|
+
const slack = usable - card.offsetHeight;
|
|
28
|
+
card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`;
|
|
29
|
+
};
|
|
30
|
+
pinCentre();
|
|
31
|
+
window.addEventListener("resize", pinCentre);
|
|
32
|
+
return () => window.removeEventListener("resize", pinCentre);
|
|
33
|
+
}, [shownKey]);
|
|
34
|
+
const stack = useDismissStack();
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const dialog = dialogRef.current;
|
|
37
|
+
if (!dialog || dialog.contains(document.activeElement)) return;
|
|
38
|
+
const top = stack.topElement();
|
|
39
|
+
if (top && top !== dialog) return;
|
|
40
|
+
const target = focusSelector && dialog.querySelector(focusSelector) || dialog;
|
|
41
|
+
target.focus();
|
|
42
|
+
}, [shownKey, stack, focusSelector]);
|
|
43
|
+
useDismissable(dialogRef, onClose);
|
|
44
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10", children: [
|
|
45
|
+
/* @__PURE__ */ jsx(
|
|
46
|
+
"button",
|
|
47
|
+
{
|
|
48
|
+
type: "button",
|
|
49
|
+
"aria-label": "Close details panel",
|
|
50
|
+
onClick: onClose,
|
|
51
|
+
className: "fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in"
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
/* @__PURE__ */ jsxs(
|
|
55
|
+
"div",
|
|
56
|
+
{
|
|
57
|
+
ref: dialogRef,
|
|
58
|
+
role: "dialog",
|
|
59
|
+
"aria-modal": "true",
|
|
60
|
+
"aria-label": label,
|
|
61
|
+
tabIndex: -1,
|
|
62
|
+
"data-testid": testId,
|
|
63
|
+
onKeyDown,
|
|
64
|
+
className: `relative w-full ${isWide ? "max-w-none" : maxWidthClass} overflow-hidden rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none`,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute top-3 right-3 z-10 flex items-center gap-1 rounded-full bg-background/90 p-0.5 backdrop-blur-sm", children: [
|
|
67
|
+
controls,
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
"button",
|
|
70
|
+
{
|
|
71
|
+
type: "button",
|
|
72
|
+
"aria-label": isWide ? "Restore width" : "Widen card",
|
|
73
|
+
title: isWide ? "Restore width" : "Widen card",
|
|
74
|
+
onClick: () => setIsWide((wide) => !wide),
|
|
75
|
+
className: ROUND_BUTTON,
|
|
76
|
+
children: isWide ? /* @__PURE__ */ jsx(Minimize2, { className: "size-4" }) : /* @__PURE__ */ jsx(Maximize2, { className: "size-4" })
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"button",
|
|
81
|
+
{
|
|
82
|
+
type: "button",
|
|
83
|
+
"aria-label": "Close",
|
|
84
|
+
onClick: onClose,
|
|
85
|
+
className: ROUND_BUTTON,
|
|
86
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
] }),
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
"div",
|
|
92
|
+
{
|
|
93
|
+
className: "max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7",
|
|
94
|
+
children
|
|
95
|
+
},
|
|
96
|
+
shownKey
|
|
97
|
+
)
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
const DIALOG_ROUND_BUTTON = ROUND_BUTTON;
|
|
104
|
+
export {
|
|
105
|
+
DIALOG_ROUND_BUTTON,
|
|
106
|
+
DetailDialogShell
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=DetailDialogShell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetailDialogShell.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/DetailDialogShell.tsx"],"sourcesContent":["import { Maximize2, Minimize2, X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { useDismissStack, useDismissable } from '~/modules/dismiss/DismissStack'\n\nconst ROUND_BUTTON =\n 'rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors'\n\n/**\n * The overlay a detail card lives in: dimmed page, a card centred once on\n * open, Widen and Close at its top right, and a body that scrolls inside it.\n * The official app card and the community tool card both sit in it.\n */\nexport function DetailDialogShell({\n label,\n shownKey,\n maxWidthClass,\n focusSelector,\n controls,\n testId,\n onClose,\n onKeyDown,\n children,\n}: {\n /** The dialog's accessible name. */\n label: string\n /**\n * What the card shows. A new value is a new open: the card re-centres for\n * its own height, takes the caret back, and the body remounts.\n */\n shownKey: string\n /** The width cap while not widened, e.g. `max-w-[min(1120px,94vw)]`. */\n maxWidthClass: string\n /** What takes the caret on open, when it exists; otherwise the dialog. */\n focusSelector?: string\n /** Extra header buttons, placed before Widen. */\n controls?: React.ReactNode\n testId?: string\n onClose: () => void\n onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>\n children: React.ReactNode\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 }, [shownKey])\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 card 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 entry, 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 page behind the\n // scrim, since the card has no focus trap.\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 =\n (focusSelector && dialog.querySelector<HTMLElement>(focusSelector)) ||\n dialog\n target.focus()\n }, [shownKey, stack, focusSelector])\n\n // Escape closes the card. Registered on the layer's own element, so a layer\n // opened over it (the screenshot viewer) gets Escape first.\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 <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={label}\n tabIndex={-1}\n data-testid={testId}\n onKeyDown={onKeyDown}\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' : maxWidthClass} 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 {/* On a backdrop of its own: the body scrolls under these buttons. */}\n <div className=\"absolute top-3 right-3 z-10 flex items-center gap-1 rounded-full bg-background/90 p-0.5 backdrop-blur-sm\">\n {controls}\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={ROUND_BUTTON}\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={ROUND_BUTTON}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n {/* Keyed by the entry on screen, so the body remounts when the card\n navigates from one entry to another instead of updating in place.\n Everything per-entry inside it is component state with no reset of\n its own — the roles table's expansion, the icon/screenshot error\n flags, a draft source edit — and it used to arrive at the next\n entry still carrying the previous one's, plus the previous scroll\n 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={shownKey}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {children}\n </div>\n </div>\n </div>\n )\n}\n\n/** The round header button style, for a card's own `controls`. */\nexport const DIALOG_ROUND_BUTTON = ROUND_BUTTON\n"],"names":[],"mappings":";;;;AAIA,MAAM,eACJ;AAOK,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAkBG;AACD,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,QAAQ,CAAC;AAkBb,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,SACH,iBAAiB,OAAO,cAA2B,aAAa,KACjE;AACF,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,UAAU,OAAO,aAAa,CAAC;AAInC,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,IAEZ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY;AAAA,QACZ,UAAU;AAAA,QACV,eAAa;AAAA,QACb;AAAA,QASA,WAAW,mBAAmB,SAAS,eAAe,aAAa;AAAA,QAGnE,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,4GACZ,UAAA;AAAA,YAAA;AAAA,YACD;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,WAAW;AAAA,gBAEV,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,WAAW;AAAA,gBAEX,UAAA,oBAAC,GAAA,EAAE,WAAU,SAAA,CAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACxB,GACF;AAAA,UAYA;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cAET;AAAA,YAAA;AAAA,YAHI;AAAA,UAAA;AAAA,QAIP;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;AAGO,MAAM,sBAAsB;"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
export interface ScreenshotGalleryProps {
|
|
3
|
-
|
|
3
|
+
/** Names the images; an entry, or anything else with a display name. */
|
|
4
|
+
app: Pick<Resource, 'displayName' | 'abbreviation'>;
|
|
4
5
|
screenshotIds: string[];
|
|
5
6
|
initialIndex?: number;
|
|
6
7
|
open: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\nimport { useDismissable } from '~/modules/dismiss/DismissStack'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource
|
|
1
|
+
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\nimport { useDismissable } from '~/modules/dismiss/DismissStack'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n /** Names the images; an entry, or anything else with a display name. */\n app: Pick<Resource, 'displayName' | 'abbreviation'>\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n /**\n * The dialog is dismissable through the stack, not through Radix.\n *\n * It no longer needs to know whether the gallery inside it is fullscreen:\n * fullscreen registers above this layer while it is on, so Escape reaches it\n * first and this dialog only hears Escape once fullscreen is off.\n */\n const contentRef = useRef<HTMLDivElement>(null)\n useDismissable(contentRef, () => onOpenChange(false), { enabled: open })\n\n // Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen\n // viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads\n // for every slide in the carousel (all slides mount at once, unlazy).\n const images: GalleryImage[] = useMemo(\n () =>\n screenshotIds.map((id) => ({\n url: `/api/screenshots/${id}?size=2000`,\n alt: `${app.abbreviation || app.displayName} screenshot`,\n })),\n [screenshotIds, app.abbreviation, app.displayName],\n )\n\n // Don't render if no screenshots\n if (screenshotIds.length === 0) {\n return null\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n ref={contentRef}\n aria-describedby={undefined}\n className=\"h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden\"\n showCloseButton={true}\n >\n <VisuallyHidden>\n <DialogTitle>\n {title || `${app.abbreviation || app.displayName} screenshots`}\n </DialogTitle>\n </VisuallyHidden>\n <Gallery images={images} initialIndex={initialIndex} title={title} />\n </DialogContent>\n </Dialog>\n )\n}\n"],"names":[],"mappings":";;;;;;AAoBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AAQzB,QAAM,aAAa,OAAuB,IAAI;AAC9C,iBAAe,YAAY,MAAM,aAAa,KAAK,GAAG,EAAE,SAAS,MAAM;AAKvE,QAAM,SAAyB;AAAA,IAC7B,MACE,cAAc,IAAI,CAAC,QAAQ;AAAA,MACzB,KAAK,oBAAoB,EAAE;AAAA,MAC3B,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,IAAA,EAC3C;AAAA,IACJ,CAAC,eAAe,IAAI,cAAc,IAAI,WAAW;AAAA,EAAA;AAInD,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,QAAA,EAAO,MAAY,cAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,iBAAiB;AAAA,MAEjB,UAAA;AAAA,QAAA,oBAAC,gBAAA,EACC,UAAA,oBAAC,aAAA,EACE,UAAA,SAAS,GAAG,IAAI,gBAAgB,IAAI,WAAW,eAAA,CAClD,GACF;AAAA,QACA,oBAAC,SAAA,EAAQ,QAAgB,cAA4B,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEvE;AAEJ;"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header's section tabs: one captioned link per section, so each is
|
|
3
|
+
* deep-linkable. The active tab is derived from the pathname rather than an
|
|
4
|
+
* exact-match Link, because Catalog must stay active on the app-detail routes
|
|
5
|
+
* (`/app/<slug>`, #23) and on `/catalog/apps`, which renders the same page.
|
|
6
|
+
* Community tools is hidden until the server says the feature is on.
|
|
7
|
+
*/
|
|
8
|
+
export declare function SectionTabs(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRouterState, Link } from "@tanstack/react-router";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useTRPC } from "../../../../api/infra/trpc.js";
|
|
5
|
+
import { cn } from "../../../../lib/utils.js";
|
|
6
|
+
function SectionTabs() {
|
|
7
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
8
|
+
const trpc = useTRPC();
|
|
9
|
+
const { data: communityConfig } = useQuery(
|
|
10
|
+
trpc.communityTools.config.queryOptions()
|
|
11
|
+
);
|
|
12
|
+
const tabs = [
|
|
13
|
+
{
|
|
14
|
+
to: "/",
|
|
15
|
+
title: "Catalog",
|
|
16
|
+
caption: "Official company apps",
|
|
17
|
+
active: pathname === "/" || pathname.startsWith("/app/") || pathname.startsWith("/catalog/")
|
|
18
|
+
},
|
|
19
|
+
...(communityConfig == null ? void 0 : communityConfig.enabled) ? [
|
|
20
|
+
{
|
|
21
|
+
to: "/community",
|
|
22
|
+
title: "Community tools",
|
|
23
|
+
caption: "Built by colleagues",
|
|
24
|
+
active: pathname.startsWith("/community")
|
|
25
|
+
}
|
|
26
|
+
] : [],
|
|
27
|
+
{
|
|
28
|
+
to: "/service-desks",
|
|
29
|
+
title: "Service Desks",
|
|
30
|
+
caption: "Where to file a ticket",
|
|
31
|
+
active: pathname.startsWith("/service-desks")
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
return /* @__PURE__ */ jsx(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
role: "tablist",
|
|
38
|
+
"aria-label": "Sections",
|
|
39
|
+
"data-testid": "section-tabs",
|
|
40
|
+
className: "flex items-stretch gap-2",
|
|
41
|
+
children: tabs.map((tab) => /* @__PURE__ */ jsxs(
|
|
42
|
+
Link,
|
|
43
|
+
{
|
|
44
|
+
to: tab.to,
|
|
45
|
+
"aria-current": tab.active ? "page" : void 0,
|
|
46
|
+
className: cn(
|
|
47
|
+
"flex flex-col items-start rounded-sm border-b-2 px-3.5 py-1.5 transition-colors",
|
|
48
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
49
|
+
tab.active ? "border-primary text-foreground" : "border-transparent text-foreground/90 hover:text-primary"
|
|
50
|
+
),
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx("span", { className: "text-base font-semibold", children: tab.title }),
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"span",
|
|
55
|
+
{
|
|
56
|
+
className: cn(
|
|
57
|
+
"text-xs",
|
|
58
|
+
tab.active ? "text-muted-foreground" : "text-foreground/80"
|
|
59
|
+
),
|
|
60
|
+
children: tab.caption
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
tab.to
|
|
66
|
+
))
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
SectionTabs
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=SectionTabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SectionTabs.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SectionTabs.tsx"],"sourcesContent":["import { Link, useRouterState } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { cn } from '~/lib/utils'\n\n/**\n * The header's section tabs: one captioned link per section, so each is\n * deep-linkable. The active tab is derived from the pathname rather than an\n * exact-match Link, because Catalog must stay active on the app-detail routes\n * (`/app/<slug>`, #23) and on `/catalog/apps`, which renders the same page.\n * Community tools is hidden until the server says the feature is on.\n */\nexport function SectionTabs() {\n const pathname = useRouterState({ select: (s) => s.location.pathname })\n const trpc = useTRPC()\n const { data: communityConfig } = useQuery(\n trpc.communityTools.config.queryOptions(),\n )\n\n const tabs = [\n {\n to: '/',\n title: 'Catalog',\n caption: 'Official company apps',\n active:\n pathname === '/' ||\n pathname.startsWith('/app/') ||\n pathname.startsWith('/catalog/'),\n },\n ...(communityConfig?.enabled\n ? [\n {\n to: '/community',\n title: 'Community tools',\n caption: 'Built by colleagues',\n active: pathname.startsWith('/community'),\n } as const,\n ]\n : []),\n {\n to: '/service-desks',\n title: 'Service Desks',\n caption: 'Where to file a ticket',\n active: pathname.startsWith('/service-desks'),\n },\n ] as const\n\n return (\n <div\n role=\"tablist\"\n aria-label=\"Sections\"\n data-testid=\"section-tabs\"\n className=\"flex items-stretch gap-2\"\n >\n {tabs.map((tab) => (\n <Link\n key={tab.to}\n to={tab.to}\n aria-current={tab.active ? 'page' : undefined}\n className={cn(\n 'flex flex-col items-start rounded-sm border-b-2 px-3.5 py-1.5 transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',\n tab.active\n ? 'border-primary text-foreground'\n : 'border-transparent text-foreground/90 hover:text-primary',\n )}\n >\n <span className=\"text-base font-semibold\">{tab.title}</span>\n <span\n className={cn(\n 'text-xs',\n tab.active ? 'text-muted-foreground' : 'text-foreground/80',\n )}\n >\n {tab.caption}\n </span>\n </Link>\n ))}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;AAYO,SAAS,cAAc;AAC5B,QAAM,WAAW,eAAe,EAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU;AACtE,QAAM,OAAO,QAAA;AACb,QAAM,EAAE,MAAM,gBAAA,IAAoB;AAAA,IAChC,KAAK,eAAe,OAAO,aAAA;AAAA,EAAa;AAG1C,QAAM,OAAO;AAAA,IACX;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QACE,aAAa,OACb,SAAS,WAAW,OAAO,KAC3B,SAAS,WAAW,WAAW;AAAA,IAAA;AAAA,IAEnC,IAAI,mDAAiB,WACjB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,SAAS;AAAA,QACT,QAAQ,SAAS,WAAW,YAAY;AAAA,MAAA;AAAA,IAC1C,IAEF,CAAA;AAAA,IACJ;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,SAAS,WAAW,gBAAgB;AAAA,IAAA;AAAA,EAC9C;AAGF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,eAAY;AAAA,MACZ,WAAU;AAAA,MAET,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,IAAI,IAAI;AAAA,UACR,gBAAc,IAAI,SAAS,SAAS;AAAA,UACpC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,IAAI,SACA,mCACA;AAAA,UAAA;AAAA,UAGN,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,2BAA2B,UAAA,IAAI,OAAM;AAAA,YACrD;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA,IAAI,SAAS,0BAA0B;AAAA,gBAAA;AAAA,gBAGxC,UAAA,IAAI;AAAA,cAAA;AAAA,YAAA;AAAA,UACP;AAAA,QAAA;AAAA,QAnBK,IAAI;AAAA,MAAA,CAqBZ;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useSearch } from "@tanstack/react-router";
|
|
2
|
+
import { useSearch, Link } from "@tanstack/react-router";
|
|
3
3
|
import { ExternalLink, Trash2, Plus } from "lucide-react";
|
|
4
4
|
import React__default from "react";
|
|
5
5
|
import { useHotkeys } from "react-hotkeys-hook";
|
|
@@ -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";
|
|
@@ -25,6 +26,7 @@ import { SubResourcesSection } from "../components/SubResourcesSection.js";
|
|
|
25
26
|
import { getChildResources } from "../../utils/resolveHelpers.js";
|
|
26
27
|
import { CommunitySection } from "./CommunitySection.js";
|
|
27
28
|
import { usePendingCount } from "./usePendingCount.js";
|
|
29
|
+
import { useCommunityToolCount } from "./useCommunityToolCount.js";
|
|
28
30
|
import { displayUrl } from "../../utils/displayUrl.js";
|
|
29
31
|
import { accessTabLabel } from "../../utils/accessTabLabel.js";
|
|
30
32
|
import { AppDoor } from "./AppDoor.js";
|
|
@@ -99,6 +101,7 @@ function AppDetails({
|
|
|
99
101
|
const isAdmin = (user == null ? void 0 : user.isAdmin) ?? false;
|
|
100
102
|
const { state: filterState } = useAppCatalogFilters();
|
|
101
103
|
const behindCount = usePendingCount(app.slug, app.claims);
|
|
104
|
+
const communityCount = useCommunityToolCount(app.slug);
|
|
102
105
|
const lastChecked = (_a = app.freshness) == null ? void 0 : _a.lastCheckedAt;
|
|
103
106
|
const dateParts = [];
|
|
104
107
|
if (lastChecked) {
|
|
@@ -286,7 +289,7 @@ function AppDetails({
|
|
|
286
289
|
CommunitySection,
|
|
287
290
|
{
|
|
288
291
|
ref: knowMoreRef,
|
|
289
|
-
|
|
292
|
+
target: { resourceSlug: app.slug },
|
|
290
293
|
appName: app.displayName,
|
|
291
294
|
claims: app.claims
|
|
292
295
|
}
|
|
@@ -468,6 +471,21 @@ function AppDetails({
|
|
|
468
471
|
"Also known as ",
|
|
469
472
|
app.nicknames.join(", ")
|
|
470
473
|
] }),
|
|
474
|
+
communityCount > 0 && /* @__PURE__ */ jsx("p", { className: "mt-2.5 text-xs", children: /* @__PURE__ */ jsxs(
|
|
475
|
+
Link,
|
|
476
|
+
{
|
|
477
|
+
to: "/community",
|
|
478
|
+
search: { tag: `app:${app.slug}` },
|
|
479
|
+
"data-testid": "app-community-tools",
|
|
480
|
+
className: "text-muted-foreground hover:text-primary hover:underline",
|
|
481
|
+
children: [
|
|
482
|
+
"Community tools for ",
|
|
483
|
+
app.displayName,
|
|
484
|
+
" · ",
|
|
485
|
+
communityCount
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
) }),
|
|
471
489
|
isAdmin && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-x-6 gap-y-1", children: [
|
|
472
490
|
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
|
|
473
491
|
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Slug:" }),
|