@igstack/app-catalog-frontend-core 6.6.0 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/App.d.ts +29 -3
- package/dist/esm/App.js +4 -2
- package/dist/esm/App.js.map +1 -1
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/dismiss/DismissStack.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/extensions/slots.test.d.ts +0 -0
- package/dist/esm/__tests__/publicEntryResolves.test.d.ts +1 -0
- package/dist/esm/appPropsFactory.d.ts +19 -1
- package/dist/esm/appPropsFactory.js +4 -3
- package/dist/esm/appPropsFactory.js.map +1 -1
- package/dist/esm/index.d.ts +15 -2
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal.d.ts +7 -1
- package/dist/esm/internal.js +0 -2
- package/dist/esm/internal.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +17 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +7 -16
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +20 -11
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +39 -17
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js +5 -18
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +12 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +3 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +17 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +212 -79
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.d.ts +1 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +31 -23
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +5 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +5 -3
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.d.ts +12 -0
- package/dist/esm/modules/auth/AuthContext.js +5 -0
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/index.d.ts +1 -1
- package/dist/esm/modules/dismiss/DismissStack.d.ts +102 -0
- package/dist/esm/modules/dismiss/DismissStack.js +82 -0
- package/dist/esm/modules/dismiss/DismissStack.js.map +1 -0
- package/dist/esm/modules/extensions/ExtensionsContext.d.ts +17 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js +15 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js.map +1 -0
- package/dist/esm/modules/extensions/factories.d.ts +18 -0
- package/dist/esm/modules/extensions/factories.js +12 -0
- package/dist/esm/modules/extensions/factories.js.map +1 -0
- package/dist/esm/modules/extensions/features/resourceDetail.d.ts +92 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js +14 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -0
- package/dist/esm/modules/extensions/index.d.ts +79 -0
- package/dist/esm/modules/extensions/index.js +26 -0
- package/dist/esm/modules/extensions/index.js.map +1 -0
- package/dist/esm/modules/extensions/makeSlot.d.ts +46 -0
- package/dist/esm/modules/extensions/makeSlot.js +62 -0
- package/dist/esm/modules/extensions/makeSlot.js.map +1 -0
- package/dist/esm/modules/extensions/types.d.ts +46 -0
- package/dist/esm/modules/gallery/Gallery.js +6 -14
- package/dist/esm/modules/gallery/Gallery.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +34 -34
- package/dist/esm/routeTree.gen.js +14 -14
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/router.d.ts +23 -0
- package/dist/esm/router.js +5 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug_.sub.$subSlug.d.ts +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.d.ts +2 -2
- package/dist/esm/routes/_layout/index.d.ts +1 -1
- package/dist/esm/routes/_layout/login.d.ts +1 -1
- package/dist/esm/routes/_layout/mcp.d.ts +1 -1
- package/dist/esm/routes/_layout/service-desks.d.ts +1 -1
- package/dist/esm/routes/_layout.d.ts +1 -1
- package/dist/esm/routes/_layout.js +13 -2
- package/dist/esm/routes/_layout.js.map +1 -1
- package/dist/esm/routes/auth.callback.d.ts +1 -1
- package/dist/esm/ui/components/error/DefaultErrorComponent.js +9 -2
- package/dist/esm/ui/components/error/DefaultErrorComponent.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +0 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/ui/dialog.d.ts +1 -1
- package/dist/esm/ui/dialog.js +9 -0
- package/dist/esm/ui/dialog.js.map +1 -1
- package/dist/esm/ui/table.d.ts +25 -1
- package/dist/esm/ui/table.js +20 -3
- package/dist/esm/ui/table.js.map +1 -1
- package/dist/esm/util/createAcRouter.d.ts +1 -1
- package/package.json +19 -11
- package/src/App.tsx +34 -3
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.focus.test.tsx +1 -1
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +115 -0
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.tsx +120 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +9 -9
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +31 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -3
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +48 -6
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +1 -7
- package/src/__tests__/modules/dismiss/DismissStack.test.tsx +330 -0
- package/src/__tests__/modules/extensions/slots.test.tsx +252 -0
- package/src/__tests__/publicEntryResolves.test.ts +63 -0
- package/src/appPropsFactory.ts +23 -3
- package/src/index.tsx +32 -2
- package/src/internal.ts +7 -1
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +21 -0
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +52 -12
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +59 -18
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +12 -20
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +19 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +396 -111
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +46 -26
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +20 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +17 -3
- package/src/modules/auth/AuthContext.tsx +15 -0
- package/src/modules/auth/index.ts +1 -0
- package/src/modules/dismiss/DismissStack.tsx +224 -0
- package/src/modules/extensions/ExtensionsContext.tsx +36 -0
- package/src/modules/extensions/factories.ts +19 -0
- package/src/modules/extensions/features/resourceDetail.ts +74 -0
- package/src/modules/extensions/index.ts +121 -0
- package/src/modules/extensions/makeSlot.tsx +123 -0
- package/src/modules/extensions/types.ts +51 -0
- package/src/modules/gallery/Gallery.tsx +13 -17
- package/src/routeTree.gen.ts +31 -31
- package/src/router.ts +23 -0
- package/src/routes/_layout.tsx +51 -1
- package/src/ui/components/error/DefaultErrorComponent.tsx +22 -4
- package/src/ui/dialog.tsx +26 -0
- package/src/ui/table.tsx +46 -3
|
@@ -13,7 +13,6 @@ import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
|
|
|
13
13
|
import { PersonBadge } from "../components/PersonBadge.js";
|
|
14
14
|
import { useUser } from "../../../auth/AuthContext.js";
|
|
15
15
|
import "@tanstack/react-query";
|
|
16
|
-
import "../../../auth/AuthModalContext.js";
|
|
17
16
|
import { InlineEditableField } from "../components/InlineEditableField.js";
|
|
18
17
|
import { MarkdownText } from "../components/MarkdownText.js";
|
|
19
18
|
import { ScreenshotGallery } from "../components/ScreenshotGallery.js";
|
|
@@ -30,6 +29,7 @@ import { displayUrl } from "../../utils/displayUrl.js";
|
|
|
30
29
|
import { accessTabLabel } from "../../utils/accessTabLabel.js";
|
|
31
30
|
import { AppDoor } from "./AppDoor.js";
|
|
32
31
|
import { DetailTabs } from "./DetailTabs.js";
|
|
32
|
+
import { ResourceDetailAccessActions } from "../../../extensions/features/resourceDetail.js";
|
|
33
33
|
const TAB = {
|
|
34
34
|
overview: "overview",
|
|
35
35
|
access: "access",
|
|
@@ -77,8 +77,7 @@ function AppScreenshot({ app }) {
|
|
|
77
77
|
}
|
|
78
78
|
function AppDetails({
|
|
79
79
|
app,
|
|
80
|
-
onAppClick
|
|
81
|
-
onClosePanel
|
|
80
|
+
onAppClick
|
|
82
81
|
}) {
|
|
83
82
|
var _a, _b, _c;
|
|
84
83
|
const [isGalleryOpen, setIsGalleryOpen] = React__default.useState(false);
|
|
@@ -109,6 +108,19 @@ function AppDetails({
|
|
|
109
108
|
() => getChildResources(allResources, app.slug),
|
|
110
109
|
[allResources, app.slug]
|
|
111
110
|
);
|
|
111
|
+
const parent = React__default.useMemo(
|
|
112
|
+
() => app.parentSlug ? allResources.find((r) => r.slug === app.parentSlug) : void 0,
|
|
113
|
+
[allResources, app.parentSlug]
|
|
114
|
+
);
|
|
115
|
+
const accessActionsCtx = React__default.useMemo(
|
|
116
|
+
() => ({
|
|
117
|
+
resource: app,
|
|
118
|
+
parent,
|
|
119
|
+
subResources: parent ? getChildResources(allResources, parent.slug) : children,
|
|
120
|
+
user: user ? { email: user.email, isAdmin } : null
|
|
121
|
+
}),
|
|
122
|
+
[app, parent, allResources, children, user, isAdmin]
|
|
123
|
+
);
|
|
112
124
|
useHotkeys(
|
|
113
125
|
"enter",
|
|
114
126
|
() => {
|
|
@@ -124,14 +136,6 @@ function AppDetails({
|
|
|
124
136
|
{ enabled: !isGalleryOpen },
|
|
125
137
|
[app, isGalleryOpen]
|
|
126
138
|
);
|
|
127
|
-
useHotkeys(
|
|
128
|
-
"escape",
|
|
129
|
-
() => {
|
|
130
|
-
onClosePanel();
|
|
131
|
-
},
|
|
132
|
-
{ enabled: !isGalleryOpen, enableOnFormTags: ["input"] },
|
|
133
|
-
[isGalleryOpen, onClosePanel]
|
|
134
|
-
);
|
|
135
139
|
const handleScreenshotClick = (index) => {
|
|
136
140
|
setGalleryInitialIndex(index);
|
|
137
141
|
setIsGalleryOpen(true);
|
|
@@ -203,7 +207,9 @@ function AppDetails({
|
|
|
203
207
|
{
|
|
204
208
|
subResources: children,
|
|
205
209
|
parentSlug: app.slug,
|
|
206
|
-
|
|
210
|
+
parent: app,
|
|
211
|
+
initialSearch: filterState.searchValue,
|
|
212
|
+
childrenLabel: app.childrenLabel
|
|
207
213
|
}
|
|
208
214
|
) })
|
|
209
215
|
] });
|
|
@@ -440,17 +446,19 @@ function AppDetails({
|
|
|
440
446
|
children: app.deprecated.type === "discouraged" ? "Discouraged" : "Deprecated"
|
|
441
447
|
}
|
|
442
448
|
),
|
|
443
|
-
/* @__PURE__ */
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
449
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto mr-10 flex items-center gap-2", children: [
|
|
450
|
+
/* @__PURE__ */ jsx(ResourceDetailAccessActions, { ...accessActionsCtx }),
|
|
451
|
+
/* @__PURE__ */ jsx(
|
|
452
|
+
Button,
|
|
453
|
+
{
|
|
454
|
+
type: "button",
|
|
455
|
+
size: "sm",
|
|
456
|
+
variant: "outline",
|
|
457
|
+
onClick: () => setComposeRequests((n) => n + 1),
|
|
458
|
+
children: "✎ Suggest a change"
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
] })
|
|
454
462
|
] }),
|
|
455
463
|
app.nicknames && app.nicknames.length > 0 && /* @__PURE__ */ jsxs("p", { className: "mt-2.5 text-xs text-muted-foreground", children: [
|
|
456
464
|
"Also known as ",
|
|
@@ -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 { CalendarPlus, Clock, 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 {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\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 { FeedbackSection } from './FeedbackSection'\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 notes: 'notes',\n} as const\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 onClosePanel,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n /**\n * Set by the band's \"Suggest a change\", which has to do two things the button\n * cannot do alone: bring the notes tab forward, and open the composer inside it.\n * Held here because the band and the panel are siblings — neither can reach the\n * other, and routing it through the URL would put a transient intent in a\n * shareable link.\n *\n * A count, not a flag: the composer closes itself on send, so asking a second\n * time has to be distinguishable from the first ask still being set. As a\n * boolean the second click changed nothing and the button was dead for the rest\n * of the card's life.\n */\n const [composeRequests, setComposeRequests] = 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\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 // Enter: open screenshot gallery\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n // Esc: close the details panel (only when gallery is NOT open)\n //\n // From inside a text field too: the Quick Jump field holds the caret from the\n // moment the card opens, so the default \"ignore form tags\" left the card with\n // no keyboard way out. A field that wants Esc for itself stops the event —\n // InlineEditableField does, so Esc there cancels the edit and nothing more.\n useHotkeys(\n 'escape',\n () => {\n onClosePanel()\n },\n { enabled: !isGalleryOpen, enableOnFormTags: ['input'] },\n [isGalleryOpen, onClosePanel],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n 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 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 initialSearch={filterState.searchValue}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\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\">\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 <Section title=\"Sources\" first={!app.background}>\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 </Section>\n\n {/* These used to sit unlabelled in a 64px gap between two sections,\n which is what made that gap look like a mistake. Labelled, in\n their own block. \"Dates\" rather than \"Added / updated\" on purpose:\n a label that repeats the first word of every line below it adds\n nothing, and it made \"Added\"/\"Updated\" ambiguous to anything\n matching on text — including this card's own tests. */}\n {(app.createdAt || app.freshness?.lastCheckedAt) && (\n <Section title=\"Dates\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1\">\n {app.createdAt && (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n title={app.createdAt}\n >\n <CalendarPlus className=\"size-3\" />\n Added {formatRelativeTime(app.createdAt)}\n </span>\n )}\n {app.freshness?.lastCheckedAt && (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n title={\n app.freshness.lastContentChangeAt\n ? `Content last changed ${app.freshness.lastContentChangeAt} · last checked ${app.freshness.lastCheckedAt}`\n : `Last checked ${app.freshness.lastCheckedAt}`\n }\n >\n <Clock className=\"size-3\" />\n {/* \"Updated\" means the content changed, not that a check\n ran — entries predating the field have none, so fall\n back. */}\n Updated{' '}\n {formatRelativeTime(\n app.freshness.lastContentChangeAt ??\n app.freshness.lastCheckedAt,\n )}\n {app.freshness.isStale && (\n <span className=\"italic opacity-75\">\n {' '}\n · may be out of date\n </span>\n )}\n </span>\n )}\n </div>\n </Section>\n )}\n\n {/* Technical information: AI-facing fields, de-emphasized/collapsed */}\n {(app.aiPrompt || app.aiMemory) && (\n <Accordion type=\"single\" collapsible className=\"mt-7\">\n <AccordionItem\n value=\"technical-info\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Technical information\n </AccordionTrigger>\n <AccordionContent className=\"pb-3 space-y-3\">\n {app.aiPrompt && (\n <div>\n <h4 className=\"section-label mb-1\">AI Prompt</h4>\n <p className=\"text-xs whitespace-pre-wrap\">\n {app.aiPrompt}\n </p>\n </div>\n )}\n {app.aiMemory && (\n <div>\n <h4 className=\"section-label mb-1\">AI Memory</h4>\n <p className=\"text-xs whitespace-pre-wrap\">\n {app.aiMemory}\n </p>\n </div>\n )}\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </>\n ),\n },\n {\n id: TAB.notes,\n label: 'Notes & requests',\n render: () => (\n <FeedbackSection appSlug={app.slug} openComposer={composeRequests} />\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 {/* In the band, not at the foot of the notes panel.\n Down there it was invisible twice over: below the fold of a panel, and\n behind a tab nobody opens unless they already know what is in it — so\n the one affordance the feature exists for was the hardest thing on the\n card to find. Up here it is present on every tab, which is the whole\n reason the band does not scroll away.\n `mr-10` keeps it clear of the dialog's own close button at top-right. */}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"ml-auto mr-10\"\n onClick={() => setComposeRequests((n) => n + 1)}\n >\n ✎ Suggest a change\n </Button>\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 requestTab={composeRequests > 0 ? TAB.notes : undefined}\n requestNonce={composeRequests}\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","_b"],"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 { CalendarPlus, Clock, 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 {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\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 { ResourceDetailAccessActions } from '~/modules/extensions'\nimport { FeedbackSection } from './FeedbackSection'\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 notes: 'notes',\n} as const\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 * Set by the band's \"Suggest a change\", which has to do two things the button\n * cannot do alone: bring the notes tab forward, and open the composer inside it.\n * Held here because the band and the panel are siblings — neither can reach the\n * other, and routing it through the URL would put a transient intent in a\n * shareable link.\n *\n * A count, not a flag: the composer closes itself on send, so asking a second\n * time has to be distinguishable from the first ask still being set. As a\n * boolean the second click changed nothing and the button was dead for the rest\n * of the card's life.\n */\n const [composeRequests, setComposeRequests] = 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\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\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 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 {\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\">\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 <Section title=\"Sources\" first={!app.background}>\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 </Section>\n\n {/* These used to sit unlabelled in a 64px gap between two sections,\n which is what made that gap look like a mistake. Labelled, in\n their own block. \"Dates\" rather than \"Added / updated\" on purpose:\n a label that repeats the first word of every line below it adds\n nothing, and it made \"Added\"/\"Updated\" ambiguous to anything\n matching on text — including this card's own tests. */}\n {(app.createdAt || app.freshness?.lastCheckedAt) && (\n <Section title=\"Dates\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1\">\n {app.createdAt && (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n title={app.createdAt}\n >\n <CalendarPlus className=\"size-3\" />\n Added {formatRelativeTime(app.createdAt)}\n </span>\n )}\n {app.freshness?.lastCheckedAt && (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n title={\n app.freshness.lastContentChangeAt\n ? `Content last changed ${app.freshness.lastContentChangeAt} · last checked ${app.freshness.lastCheckedAt}`\n : `Last checked ${app.freshness.lastCheckedAt}`\n }\n >\n <Clock className=\"size-3\" />\n {/* \"Updated\" means the content changed, not that a check\n ran — entries predating the field have none, so fall\n back. */}\n Updated{' '}\n {formatRelativeTime(\n app.freshness.lastContentChangeAt ??\n app.freshness.lastCheckedAt,\n )}\n {app.freshness.isStale && (\n <span className=\"italic opacity-75\">\n {' '}\n · may be out of date\n </span>\n )}\n </span>\n )}\n </div>\n </Section>\n )}\n\n {/* Technical information: AI-facing fields, de-emphasized/collapsed */}\n {(app.aiPrompt || app.aiMemory) && (\n <Accordion type=\"single\" collapsible className=\"mt-7\">\n <AccordionItem\n value=\"technical-info\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Technical information\n </AccordionTrigger>\n <AccordionContent className=\"pb-3 space-y-3\">\n {app.aiPrompt && (\n <div>\n <h4 className=\"section-label mb-1\">AI Prompt</h4>\n <p className=\"text-xs whitespace-pre-wrap\">\n {app.aiPrompt}\n </p>\n </div>\n )}\n {app.aiMemory && (\n <div>\n <h4 className=\"section-label mb-1\">AI Memory</h4>\n <p className=\"text-xs whitespace-pre-wrap\">\n {app.aiMemory}\n </p>\n </div>\n )}\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </>\n ),\n },\n {\n id: TAB.notes,\n label: 'Notes & requests',\n render: () => (\n <FeedbackSection appSlug={app.slug} openComposer={composeRequests} />\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 {/* In the band, not at the foot of the notes panel.\n Down there it was invisible twice over: below the fold of a panel, and\n behind a tab nobody opens unless they already know what is in it — so\n the one affordance the feature exists for was the hardest thing on the\n card to find. Up here it is present on every tab, which is the whole\n reason the band does not scroll away.\n `mr-10` keeps it clear of the dialog's own close button at top-right. */}\n {/* Plugin actions sit beside \"Suggest a change\", sharing its reasoning:\n the band is mounted on every tab and never scrolls away. A slot in a\n tab panel would be invisible until a second click, and one inside\n SubResourceDetailPanel's `hasAnyAccess` gate would vanish for exactly\n the resources that need help most.\n Renders nothing when no plugin contributes, so the band keeps its\n current shape in the open-source build. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setComposeRequests((n) => n + 1)}\n >\n ✎ Suggest a change\n </Button>\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 requestTab={composeRequests > 0 ? TAB.notes : undefined}\n requestNonce={composeRequests}\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","_b"],"mappings":"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"}
|
|
@@ -25,6 +25,11 @@ function DetailTabs({
|
|
|
25
25
|
setUserChose(true);
|
|
26
26
|
setUrlTab(requestTab);
|
|
27
27
|
}, [requestTab, requestNonce, setUrlTab]);
|
|
28
|
+
const [lastForced, setLastForced] = React__default.useState(forcedTab);
|
|
29
|
+
if (forcedTab !== lastForced) {
|
|
30
|
+
setLastForced(forcedTab);
|
|
31
|
+
if (lastForced && !forcedTab && !userChose) setUrlTab(lastForced);
|
|
32
|
+
}
|
|
28
33
|
const asked = userChose || !forcedTab ? urlTab : forcedTab;
|
|
29
34
|
const requested = RETIRED[asked] ?? asked;
|
|
30
35
|
const activeId = ids.includes(requested) ? requested : ids[0] ?? "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DetailTabs.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/DetailTabs.tsx"],"sourcesContent":["import React from 'react'\nimport { cn } from '~/lib/utils'\nimport { useUrlSyncedState } from '~/modules/appCatalog/hooks/useUrlSyncedState'\n\nexport interface DetailTab {\n /** Stable id — this is what lands in `?tab=`, so treat it as public. */\n id: string\n label: string\n /** One short count beside the label (\"430\", \"55 roles\", \"2 steps\"). */\n chip?: string\n /** Marks unreviewed content. Also stated in the accessible name. */\n dot?: boolean\n /** Spoken instead of `label` when the chip and dot need explaining. */\n ariaLabel?: string\n render: () => React.ReactNode\n}\n\n/**\n * The detail card's tabs.\n *\n * Every entry gets them, including thin ones. Two earlier rules were worse:\n * hiding a tab with little in it turned \"we have not documented the way in\"\n * into a dead end you had to click to discover, and showing them only on heavy\n * entries made the card's shape change between apps, so \"where is access?\" had\n * two answers and no habit could form across a few hundred entries. Naming the\n * route in the tab itself removes the reason to hide anything — the label tells\n * you whether opening it is worth it.\n *\n * The active tab lives in the url so a card is linkable mid-conversation. A\n * `forcedTab` (today: arriving with `?sub=`) wins over `?tab=`, because a url\n * that selects a child and then shows a different tab is lying about itself.\n *\n * Panels mount only while active, which also resets their internal state on\n * the way out — the same choice the card makes when it navigates between apps.\n */\nexport function DetailTabs({\n tabs,\n forcedTab,\n requestTab,\n requestNonce,\n band,\n bandClassName,\n}: {\n tabs: DetailTab[]\n /** Overrides `?tab=` while set. */\n forcedTab?: string\n /**\n * A one-shot \"please show this tab\" from outside the strip — the band's\n * \"Suggest a change\" uses it.\n *\n * It has to come through here rather than the caller writing `?tab=` itself:\n * `useUrlSyncedState` keeps its own `useState` seeded once from the URL and only\n * ever pushes state OUT to the URL, so a second instance writing the same key\n * does not update this one. The two then fight — the strip's next render sees the\n * URL disagreeing with its own state and navigates back over it.\n */\n requestTab?: string\n /**\n * Bumped by the caller every time it asks again. Without it a repeat request is\n * indistinguishable from the same standing value, so the effect below does not\n * re-fire and the second ask does nothing — which is what made the band's\n * button work exactly once per card.\n */\n requestNonce?: number\n /**\n * Identity and actions, shown ABOVE the strip and inside the same tinted\n * surface. Owned here rather than by the caller because the band and the\n * strip have to be one visual unit with the panel outside it — render the\n * panel inside the band and it inherits the tint, which is the mistake this\n * prop exists to make impossible.\n */\n band?: React.ReactNode\n /** Cancels the dialog's own padding so the tint reaches the card's edges. */\n bandClassName?: string\n}) {\n /**\n * The first tab is what the card opens on, so naming it in the URL says nothing.\n * `encode` returning undefined drops the param, which keeps `?tab=` out of a link\n * until someone has actually chosen something other than the default — and keeps a\n * shared link honest about what the sender was looking at.\n */\n const defaultTabId = tabs[0]?.id\n const [urlTab, setUrlTab] = useUrlSyncedState<string>({\n key: 'tab',\n defaultValue: '',\n encode: (value) => (value && value !== defaultTabId ? value : undefined),\n })\n\n const ids = tabs.map((t) => t.id)\n\n /**\n * Tab ids are public — they are in `?tab=` in links people have already shared.\n * Renaming one would silently drop those onto the first tab, so a retired id\n * keeps resolving to its replacement instead.\n */\n const RETIRED: Record<string, string> = { metadata: 'documentation' }\n\n /**\n * `forcedTab` picks the tab you ARRIVE on, not the tab you are stuck on.\n *\n * It used to win over `?tab=` for as long as it was set, and `?sub=` keeps it set\n * for the whole life of the card — so on any entry reached by a sub-resource\n * search (667 accounts on one of them) the strip was dead: every click wrote\n * nothing and the tab never moved. The same override also silently killed the\n * band's \"Suggest a change\", which navigates by writing `?tab=`.\n *\n * So it applies until the user expresses a preference, and then gets out of the\n * way. `userChose` is what distinguishes \"no opinion yet\" from \"chose the tab\n * that happens to be the forced one\".\n */\n const [userChose, setUserChose] = React.useState(false)\n\n // Applied when it changes, through the state that actually owns the tab.\n React.useEffect(() => {\n if (!requestTab) return\n setUserChose(true)\n setUrlTab(requestTab)\n }, [requestTab, requestNonce, setUrlTab])\n const asked = userChose || !forcedTab ? urlTab : forcedTab\n const requested = RETIRED[asked] ?? asked\n const activeId = ids.includes(requested) ? requested : (ids[0] ?? '')\n const active = tabs.find((t) => t.id === activeId)\n\n // Arrow keys move between tabs, per the tablist pattern. Selection follows\n // focus, which is correct here because every panel is already loaded.\n const onKeyDown = (event: React.KeyboardEvent) => {\n const delta =\n event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0\n if (!delta) return\n event.preventDefault()\n const from = ids.indexOf(activeId)\n const next = ids[(from + delta + ids.length) % ids.length]\n if (!next) return\n setUserChose(true)\n setUrlTab(next)\n // Focus follows selection, or the arrow keys would walk the visual\n // selection away from the keyboard's position. By id rather than by\n // position in a NodeList: each tab already carries one, and an id lookup\n // says \"missing\" with a null instead of an out-of-range index.\n document.getElementById(`detail-tab-${next}`)?.focus()\n }\n\n return (\n <>\n <div className={cn('detail-band', bandClassName)}>\n {band}\n <div role=\"tablist\" className=\"detail-tabs mt-5\" onKeyDown={onKeyDown}>\n {tabs.map((tab) => {\n const selected = tab.id === activeId\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n id={`detail-tab-${tab.id}`}\n aria-selected={selected}\n aria-controls={`detail-panel-${tab.id}`}\n // Only the selected tab stays in the tab order; the rest are\n // reached with the arrow keys, as the pattern requires.\n tabIndex={selected ? 0 : -1}\n aria-label={tab.ariaLabel}\n className=\"detail-tab\"\n onClick={() => {\n setUserChose(true)\n setUrlTab(tab.id)\n }}\n >\n {tab.label}\n {tab.chip && (\n <span className=\"detail-tab__chip\">{tab.chip}</span>\n )}\n {tab.dot && (\n <span className=\"detail-tab__dot\" aria-hidden=\"true\" />\n )}\n </button>\n )\n })}\n </div>\n </div>\n {active && (\n <div\n role=\"tabpanel\"\n id={`detail-panel-${active.id}`}\n aria-labelledby={`detail-tab-${active.id}`}\n // Focusable so that tabbing out of the strip lands in the content it\n // controls rather than skipping past it.\n tabIndex={0}\n className=\"pt-5 outline-none\"\n >\n {active.render()}\n </div>\n )}\n </>\n )\n}\n"],"names":["React","_a"],"mappings":";;;;AAmCO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAgCG;;AAOD,QAAM,gBAAe,UAAK,CAAC,MAAN,mBAAS;AAC9B,QAAM,CAAC,QAAQ,SAAS,IAAI,kBAA0B;AAAA,IACpD,KAAK;AAAA,IACL,cAAc;AAAA,IACd,QAAQ,CAAC,UAAW,SAAS,UAAU,eAAe,QAAQ;AAAA,EAAA,CAC/D;AAED,QAAM,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAOhC,QAAM,UAAkC,EAAE,UAAU,gBAAA;AAepD,QAAM,CAAC,WAAW,YAAY,IAAIA,eAAM,SAAS,KAAK;AAGtDA,iBAAM,UAAU,MAAM;AACpB,QAAI,CAAC,WAAY;AACjB,iBAAa,IAAI;AACjB,cAAU,UAAU;AAAA,EACtB,GAAG,CAAC,YAAY,cAAc,SAAS,CAAC;AACxC,QAAM,QAAQ,aAAa,CAAC,YAAY,SAAS;AACjD,QAAM,YAAY,QAAQ,KAAK,KAAK;AACpC,QAAM,WAAW,IAAI,SAAS,SAAS,IAAI,YAAa,IAAI,CAAC,KAAK;AAClE,QAAM,SAAS,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ;AAIjD,QAAM,YAAY,CAAC,UAA+B;;AAChD,UAAM,QACJ,MAAM,QAAQ,eAAe,IAAI,MAAM,QAAQ,cAAc,KAAK;AACpE,QAAI,CAAC,MAAO;AACZ,UAAM,eAAA;AACN,UAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,UAAM,OAAO,KAAK,OAAO,QAAQ,IAAI,UAAU,IAAI,MAAM;AACzD,QAAI,CAAC,KAAM;AACX,iBAAa,IAAI;AACjB,cAAU,IAAI;AAKd,KAAAC,MAAA,SAAS,eAAe,cAAc,IAAI,EAAE,MAA5C,gBAAAA,IAA+C;AAAA,EACjD;AAEA,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAW,GAAG,eAAe,aAAa,GAC5C,UAAA;AAAA,MAAA;AAAA,MACD,oBAAC,OAAA,EAAI,MAAK,WAAU,WAAU,oBAAmB,WAC9C,UAAA,KAAK,IAAI,CAAC,QAAQ;AACjB,cAAM,WAAW,IAAI,OAAO;AAC5B,eACE;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,IAAI,cAAc,IAAI,EAAE;AAAA,YACxB,iBAAe;AAAA,YACf,iBAAe,gBAAgB,IAAI,EAAE;AAAA,YAGrC,UAAU,WAAW,IAAI;AAAA,YACzB,cAAY,IAAI;AAAA,YAChB,WAAU;AAAA,YACV,SAAS,MAAM;AACb,2BAAa,IAAI;AACjB,wBAAU,IAAI,EAAE;AAAA,YAClB;AAAA,YAEC,UAAA;AAAA,cAAA,IAAI;AAAA,cACJ,IAAI,QACH,oBAAC,UAAK,WAAU,oBAAoB,cAAI,MAAK;AAAA,cAE9C,IAAI,OACH,oBAAC,UAAK,WAAU,mBAAkB,eAAY,OAAA,CAAO;AAAA,YAAA;AAAA,UAAA;AAAA,UArBlD,IAAI;AAAA,QAAA;AAAA,MAyBf,CAAC,EAAA,CACH;AAAA,IAAA,GACF;AAAA,IACC,UACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,IAAI,gBAAgB,OAAO,EAAE;AAAA,QAC7B,mBAAiB,cAAc,OAAO,EAAE;AAAA,QAGxC,UAAU;AAAA,QACV,WAAU;AAAA,QAET,iBAAO,OAAA;AAAA,MAAO;AAAA,IAAA;AAAA,EACjB,GAEJ;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"DetailTabs.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/DetailTabs.tsx"],"sourcesContent":["import React from 'react'\nimport { cn } from '~/lib/utils'\nimport { useUrlSyncedState } from '~/modules/appCatalog/hooks/useUrlSyncedState'\n\nexport interface DetailTab {\n /** Stable id — this is what lands in `?tab=`, so treat it as public. */\n id: string\n label: string\n /** One short count beside the label (\"430\", \"55 roles\", \"2 steps\"). */\n chip?: string\n /** Marks unreviewed content. Also stated in the accessible name. */\n dot?: boolean\n /** Spoken instead of `label` when the chip and dot need explaining. */\n ariaLabel?: string\n render: () => React.ReactNode\n}\n\n/**\n * The detail card's tabs.\n *\n * Every entry gets them, including thin ones. Two earlier rules were worse:\n * hiding a tab with little in it turned \"we have not documented the way in\"\n * into a dead end you had to click to discover, and showing them only on heavy\n * entries made the card's shape change between apps, so \"where is access?\" had\n * two answers and no habit could form across a few hundred entries. Naming the\n * route in the tab itself removes the reason to hide anything — the label tells\n * you whether opening it is worth it.\n *\n * The active tab lives in the url so a card is linkable mid-conversation. A\n * `forcedTab` (today: arriving with `?sub=`) wins over `?tab=`, because a url\n * that selects a child and then shows a different tab is lying about itself.\n *\n * Panels mount only while active, which also resets their internal state on\n * the way out — the same choice the card makes when it navigates between apps.\n */\nexport function DetailTabs({\n tabs,\n forcedTab,\n requestTab,\n requestNonce,\n band,\n bandClassName,\n}: {\n tabs: DetailTab[]\n /** Overrides `?tab=` while set. */\n forcedTab?: string\n /**\n * A one-shot \"please show this tab\" from outside the strip — the band's\n * \"Suggest a change\" uses it.\n *\n * It has to come through here rather than the caller writing `?tab=` itself:\n * `useUrlSyncedState` keeps its own `useState` seeded once from the URL and only\n * ever pushes state OUT to the URL, so a second instance writing the same key\n * does not update this one. The two then fight — the strip's next render sees the\n * URL disagreeing with its own state and navigates back over it.\n */\n requestTab?: string\n /**\n * Bumped by the caller every time it asks again. Without it a repeat request is\n * indistinguishable from the same standing value, so the effect below does not\n * re-fire and the second ask does nothing — which is what made the band's\n * button work exactly once per card.\n */\n requestNonce?: number\n /**\n * Identity and actions, shown ABOVE the strip and inside the same tinted\n * surface. Owned here rather than by the caller because the band and the\n * strip have to be one visual unit with the panel outside it — render the\n * panel inside the band and it inherits the tint, which is the mistake this\n * prop exists to make impossible.\n */\n band?: React.ReactNode\n /** Cancels the dialog's own padding so the tint reaches the card's edges. */\n bandClassName?: string\n}) {\n /**\n * The first tab is what the card opens on, so naming it in the URL says nothing.\n * `encode` returning undefined drops the param, which keeps `?tab=` out of a link\n * until someone has actually chosen something other than the default — and keeps a\n * shared link honest about what the sender was looking at.\n */\n const defaultTabId = tabs[0]?.id\n const [urlTab, setUrlTab] = useUrlSyncedState<string>({\n key: 'tab',\n defaultValue: '',\n encode: (value) => (value && value !== defaultTabId ? value : undefined),\n })\n\n const ids = tabs.map((t) => t.id)\n\n /**\n * Tab ids are public — they are in `?tab=` in links people have already shared.\n * Renaming one would silently drop those onto the first tab, so a retired id\n * keeps resolving to its replacement instead.\n */\n const RETIRED: Record<string, string> = { metadata: 'documentation' }\n\n /**\n * `forcedTab` picks the tab you ARRIVE on, not the tab you are stuck on.\n *\n * It used to win over `?tab=` for as long as it was set, and `?sub=` keeps it set\n * for the whole life of the card — so on any entry reached by a sub-resource\n * search (667 accounts on one of them) the strip was dead: every click wrote\n * nothing and the tab never moved. The same override also silently killed the\n * band's \"Suggest a change\", which navigates by writing `?tab=`.\n *\n * So it applies until the user expresses a preference, and then gets out of the\n * way. `userChose` is what distinguishes \"no opinion yet\" from \"chose the tab\n * that happens to be the forced one\".\n */\n const [userChose, setUserChose] = React.useState(false)\n\n // Applied when it changes, through the state that actually owns the tab.\n React.useEffect(() => {\n if (!requestTab) return\n setUserChose(true)\n setUrlTab(requestTab)\n }, [requestTab, requestNonce, setUrlTab])\n /**\n * Releasing the force must not move the view.\n *\n * Arriving with `?sub=` forces the sub-resources tab, but only through\n * `forcedTab` — the tab's own state is still the default. Clearing the\n * selection drops `?sub=`, the force goes with it, and the strip fell back to\n * that stale default: the reader was moved to Overview while looking at the\n * table they had just cleared a filter on. So when the force is released\n * before the user picked anything, the forced tab becomes the real choice.\n *\n * Adjusted during render rather than in an effect, the pattern React\n * documents for responding to a prop change: an effect would paint one frame\n * of Overview first, mounting that panel only to tear it down again.\n */\n const [lastForced, setLastForced] = React.useState(forcedTab)\n if (forcedTab !== lastForced) {\n setLastForced(forcedTab)\n if (lastForced && !forcedTab && !userChose) setUrlTab(lastForced)\n }\n\n const asked = userChose || !forcedTab ? urlTab : forcedTab\n const requested = RETIRED[asked] ?? asked\n const activeId = ids.includes(requested) ? requested : (ids[0] ?? '')\n const active = tabs.find((t) => t.id === activeId)\n\n // Arrow keys move between tabs, per the tablist pattern. Selection follows\n // focus, which is correct here because every panel is already loaded.\n const onKeyDown = (event: React.KeyboardEvent) => {\n const delta =\n event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0\n if (!delta) return\n event.preventDefault()\n const from = ids.indexOf(activeId)\n const next = ids[(from + delta + ids.length) % ids.length]\n if (!next) return\n setUserChose(true)\n setUrlTab(next)\n // Focus follows selection, or the arrow keys would walk the visual\n // selection away from the keyboard's position. By id rather than by\n // position in a NodeList: each tab already carries one, and an id lookup\n // says \"missing\" with a null instead of an out-of-range index.\n document.getElementById(`detail-tab-${next}`)?.focus()\n }\n\n return (\n <>\n <div className={cn('detail-band', bandClassName)}>\n {band}\n <div role=\"tablist\" className=\"detail-tabs mt-5\" onKeyDown={onKeyDown}>\n {tabs.map((tab) => {\n const selected = tab.id === activeId\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n id={`detail-tab-${tab.id}`}\n aria-selected={selected}\n aria-controls={`detail-panel-${tab.id}`}\n // Only the selected tab stays in the tab order; the rest are\n // reached with the arrow keys, as the pattern requires.\n tabIndex={selected ? 0 : -1}\n aria-label={tab.ariaLabel}\n className=\"detail-tab\"\n onClick={() => {\n setUserChose(true)\n setUrlTab(tab.id)\n }}\n >\n {tab.label}\n {tab.chip && (\n <span className=\"detail-tab__chip\">{tab.chip}</span>\n )}\n {tab.dot && (\n <span className=\"detail-tab__dot\" aria-hidden=\"true\" />\n )}\n </button>\n )\n })}\n </div>\n </div>\n {active && (\n <div\n role=\"tabpanel\"\n id={`detail-panel-${active.id}`}\n aria-labelledby={`detail-tab-${active.id}`}\n // Focusable so that tabbing out of the strip lands in the content it\n // controls rather than skipping past it.\n tabIndex={0}\n className=\"pt-5 outline-none\"\n >\n {active.render()}\n </div>\n )}\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
@@ -45,6 +45,10 @@ function AppCatalogPage({
|
|
|
45
45
|
() => resources.filter((r) => !r.parentSlug),
|
|
46
46
|
[resources]
|
|
47
47
|
);
|
|
48
|
+
const visibleRootResources = useMemo(
|
|
49
|
+
() => rootResources.filter((a) => filterState.showDeprecated || !a.deprecated),
|
|
50
|
+
[rootResources, filterState.showDeprecated]
|
|
51
|
+
);
|
|
48
52
|
useEffect(() => {
|
|
49
53
|
}, [isLoadingApps, resources.length, rootResources.length]);
|
|
50
54
|
useEffect(() => {
|
|
@@ -111,9 +115,7 @@ function AppCatalogPage({
|
|
|
111
115
|
/* @__PURE__ */ jsx(
|
|
112
116
|
AppCatalogGrid,
|
|
113
117
|
{
|
|
114
|
-
apps:
|
|
115
|
-
(a) => filterState.showDeprecated || !a.deprecated
|
|
116
|
-
),
|
|
118
|
+
apps: visibleRootResources,
|
|
117
119
|
allResources: resources,
|
|
118
120
|
searchValue,
|
|
119
121
|
onSearchChange: setSearchValue,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Suspense, lazy, useCallback, useEffect, useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { AppCatalogGrid } from '../catalog/AppCatalogGrid'\nimport { AttributionFooter } from '../catalog/AttributionFooter'\n\n// The detail card (and the markdown stack it renders) loads on first open, not\n// with the catalog grid every visitor sees first (#120).\nconst AppDetailPanel = lazy(() =>\n import('../catalog/AppDetailPanel').then((m) => ({\n default: m.AppDetailPanel,\n })),\n)\n\nexport function AppCatalogPage({\n selectedSlug,\n selectedSubSlug: subPageSlug,\n}: { selectedSlug?: string; selectedSubSlug?: string } = {}) {\n const { resources, isLoadingApps } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const navigate = useNavigate()\n\n // Selected app comes from the route path (/app/<slug>); undefined = nothing open\n const selectedAppSlug = selectedSlug\n\n // Search value from context (module-scoped, see AppCatalogFiltersContext)\n const search = useSearch({ strict: false })\n\n // Two distinct states, deliberately on different parts of the URL:\n // - `?sub=<slug>` the parent's detail, singled out to one child\n // - `/app/<slug>/sub/<child>` that child's OWN page, with its access chain\n // Clicking a matched sub-resource in the search results reaches the first;\n // clicking its name in the parent's table reaches the second.\n const highlightSubSlug = search.sub\n\n const handleSubClick = useCallback(\n (parentSlug: string, subSlug: string) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: parentSlug },\n search: (prev) => ({ ...prev, sub: subSlug }),\n })\n },\n [navigate],\n )\n\n const handleBackToParent = useCallback(() => {\n // Leave the sub-resource's page for its parent's detail, keeping `?sub=` so\n // the row the user came from stays singled out — going \"back\" shouldn't\n // dump them into all forty-seven siblings.\n if (selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: selectedAppSlug },\n search: (prev) => ({ ...prev, sub: subPageSlug }),\n })\n }\n }, [navigate, selectedAppSlug, subPageSlug])\n\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Get root resources for display (children surface through search)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see #22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\n }\n\n const handleLaunch = (app: Resource) => {\n if (app.appUrl) window.open(app.appUrl, '_blank', 'noopener,noreferrer')\n }\n\n // The app whose detail card is open over the catalog backdrop.\n /**\n * Strip the card's own params once no card is open.\n *\n * Clearing them inside the close handler does not hold: `useUrlSyncedState` only\n * ever pushes its state OUT to the url, so the tab strip — still mounted for one\n * more render — sees the url disagreeing with its own state and writes `tab` straight\n * back. Two owners, and the one on its way out gets the last word. Measured on a\n * live host: `?tab=notes` survived the close every time.\n *\n * Doing it here instead means one owner decides, and it decides AFTER the strip has\n * gone, so there is nothing left to argue with. Only the card's params go; the\n * catalog's own state (filters, recents, the deprecated toggle) outlives any card.\n */\n useEffect(() => {\n if (selectedSlug || subPageSlug) return\n const stale = ['tab', 'sub', 'qj'].filter(\n (key) => (search as Record<string, unknown>)[key] !== undefined,\n )\n if (stale.length === 0) return\n void navigate({\n to: '/',\n replace: true,\n search: (prev) => {\n const next = { ...(prev as Record<string, unknown>) }\n for (const key of stale) delete next[key]\n return next\n },\n })\n }, [selectedSlug, subPageSlug, search, navigate])\n\n const selectedApp = useMemo(\n () =>\n selectedAppSlug\n ? (resources.find((r) => r.slug === selectedAppSlug) ?? null)\n : null,\n [selectedAppSlug, resources],\n )\n\n // …and the sub-resource whose OWN page is open (`/app/<slug>/sub/<child>`).\n // `?sub=` deliberately does NOT land here: that state stays on the parent's\n // detail with the child singled out in its table.\n const selectedSub = useMemo(() => {\n if (!subPageSlug || !selectedApp) return null\n return (\n resources.find(\n (r) => r.slug === subPageSlug && r.parentSlug === selectedApp.slug,\n ) ?? null\n )\n }, [subPageSlug, selectedApp, resources])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // The catalog is one persistent shell (#38): the discovery spine or, while\n // typing, the in-place results list — both handled inside AppCatalogGrid, so\n // searching never swaps containers and the hero never moves. It owns vertical\n // scroll (MainLayout is h-screen/overflow-hidden), and `scrollbar-gutter:\n // stable` reserves the track so a shorter results list doesn't shift the hero\n // sideways when the scrollbar disappears.\n return (\n // `pb-24` lives here, on whatever ends up last — the footer when the app\n // configures one, the final card row otherwise — so nothing sits flush\n // against the bottom edge or under the floating support bubble.\n <div className=\"flex-1 min-h-0 overflow-y-auto [scrollbar-gutter:stable] pb-24\">\n <AppCatalogGrid\n apps={rootResources.filter(\n (a) => filterState.showDeprecated || !a.deprecated,\n )}\n // All resources incl. children — so search matches sub-resources\n // (e.g. a cloud account) and surfaces their parent.\n allResources={resources}\n searchValue={searchValue}\n onSearchChange={setSearchValue}\n onAppClick={handleAppClick}\n onSubClick={handleSubClick}\n onLaunch={handleLaunch}\n detailOpen={selectedApp !== null}\n selectedSubSlug={highlightSubSlug}\n />\n {/* Outside the grid's 1000px column so its rule spans the full width. */}\n <AttributionFooter />\n {/* #38 item B: the detail as a card over the catalog backdrop. */}\n {selectedApp && (\n <Suspense fallback={null}>\n <AppDetailPanel\n app={selectedApp}\n subResource={selectedSub}\n onClose={() => void navigate({ to: '/' })}\n onAppClick={handleAppClick}\n onBackToParent={handleBackToParent}\n />\n </Suspense>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Suspense, lazy, useCallback, useEffect, useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { AppCatalogGrid } from '../catalog/AppCatalogGrid'\nimport { AttributionFooter } from '../catalog/AttributionFooter'\n\n// The detail card (and the markdown stack it renders) loads on first open, not\n// with the catalog grid every visitor sees first (#120).\nconst AppDetailPanel = lazy(() =>\n import('../catalog/AppDetailPanel').then((m) => ({\n default: m.AppDetailPanel,\n })),\n)\n\nexport function AppCatalogPage({\n selectedSlug,\n selectedSubSlug: subPageSlug,\n}: { selectedSlug?: string; selectedSubSlug?: string } = {}) {\n const { resources, isLoadingApps } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const navigate = useNavigate()\n\n // Selected app comes from the route path (/app/<slug>); undefined = nothing open\n const selectedAppSlug = selectedSlug\n\n // Search value from context (module-scoped, see AppCatalogFiltersContext)\n const search = useSearch({ strict: false })\n\n // Two distinct states, deliberately on different parts of the URL:\n // - `?sub=<slug>` the parent's detail, singled out to one child\n // - `/app/<slug>/sub/<child>` that child's OWN page, with its access chain\n // Clicking a matched sub-resource in the search results reaches the first;\n // clicking its name in the parent's table reaches the second.\n const highlightSubSlug = search.sub\n\n const handleSubClick = useCallback(\n (parentSlug: string, subSlug: string) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: parentSlug },\n search: (prev) => ({ ...prev, sub: subSlug }),\n })\n },\n [navigate],\n )\n\n const handleBackToParent = useCallback(() => {\n // Leave the sub-resource's page for its parent's detail, keeping `?sub=` so\n // the row the user came from stays singled out — going \"back\" shouldn't\n // dump them into all forty-seven siblings.\n if (selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: selectedAppSlug },\n search: (prev) => ({ ...prev, sub: subPageSlug }),\n })\n }\n }, [navigate, selectedAppSlug, subPageSlug])\n\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Get root resources for display (children surface through search)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n /**\n * What the grid actually shows, memoized because its IDENTITY matters.\n *\n * Filtering inline in the JSX produced a new array on every render, and the\n * grid keys four `useMemo`s off this prop (the slug index, \"new this week\",\n * the browse list and its area grouping). A fresh identity invalidated all\n * four on every keystroke — recomputed even while searching, when the\n * discovery spine they feed is not on screen. The search box lives in a\n * context above this component, so every keystroke re-renders here.\n */\n const visibleRootResources = useMemo(\n () =>\n rootResources.filter((a) => filterState.showDeprecated || !a.deprecated),\n [rootResources, filterState.showDeprecated],\n )\n\n // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see #22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\n }\n\n const handleLaunch = (app: Resource) => {\n if (app.appUrl) window.open(app.appUrl, '_blank', 'noopener,noreferrer')\n }\n\n // The app whose detail card is open over the catalog backdrop.\n /**\n * Strip the card's own params once no card is open.\n *\n * Clearing them inside the close handler does not hold: `useUrlSyncedState` only\n * ever pushes its state OUT to the url, so the tab strip — still mounted for one\n * more render — sees the url disagreeing with its own state and writes `tab` straight\n * back. Two owners, and the one on its way out gets the last word. Measured on a\n * live host: `?tab=notes` survived the close every time.\n *\n * Doing it here instead means one owner decides, and it decides AFTER the strip has\n * gone, so there is nothing left to argue with. Only the card's params go; the\n * catalog's own state (filters, recents, the deprecated toggle) outlives any card.\n */\n useEffect(() => {\n if (selectedSlug || subPageSlug) return\n const stale = ['tab', 'sub', 'qj'].filter(\n (key) => (search as Record<string, unknown>)[key] !== undefined,\n )\n if (stale.length === 0) return\n void navigate({\n to: '/',\n replace: true,\n search: (prev) => {\n const next = { ...(prev as Record<string, unknown>) }\n for (const key of stale) delete next[key]\n return next\n },\n })\n }, [selectedSlug, subPageSlug, search, navigate])\n\n const selectedApp = useMemo(\n () =>\n selectedAppSlug\n ? (resources.find((r) => r.slug === selectedAppSlug) ?? null)\n : null,\n [selectedAppSlug, resources],\n )\n\n // …and the sub-resource whose OWN page is open (`/app/<slug>/sub/<child>`).\n // `?sub=` deliberately does NOT land here: that state stays on the parent's\n // detail with the child singled out in its table.\n const selectedSub = useMemo(() => {\n if (!subPageSlug || !selectedApp) return null\n return (\n resources.find(\n (r) => r.slug === subPageSlug && r.parentSlug === selectedApp.slug,\n ) ?? null\n )\n }, [subPageSlug, selectedApp, resources])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // The catalog is one persistent shell (#38): the discovery spine or, while\n // typing, the in-place results list — both handled inside AppCatalogGrid, so\n // searching never swaps containers and the hero never moves. It owns vertical\n // scroll (MainLayout is h-screen/overflow-hidden), and `scrollbar-gutter:\n // stable` reserves the track so a shorter results list doesn't shift the hero\n // sideways when the scrollbar disappears.\n return (\n // `pb-24` lives here, on whatever ends up last — the footer when the app\n // configures one, the final card row otherwise — so nothing sits flush\n // against the bottom edge or under the floating support bubble.\n <div className=\"flex-1 min-h-0 overflow-y-auto [scrollbar-gutter:stable] pb-24\">\n <AppCatalogGrid\n apps={visibleRootResources}\n // All resources incl. children — so search matches sub-resources\n // (e.g. a cloud account) and surfaces their parent.\n allResources={resources}\n searchValue={searchValue}\n onSearchChange={setSearchValue}\n onAppClick={handleAppClick}\n onSubClick={handleSubClick}\n onLaunch={handleLaunch}\n detailOpen={selectedApp !== null}\n selectedSubSlug={highlightSubSlug}\n />\n {/* Outside the grid's 1000px column so its rule spans the full width. */}\n <AttributionFooter />\n {/* #38 item B: the detail as a card over the catalog backdrop. */}\n {selectedApp && (\n <Suspense fallback={null}>\n <AppDetailPanel\n app={selectedApp}\n subResource={selectedSub}\n onClose={() => void navigate({ to: '/' })}\n onAppClick={handleAppClick}\n onBackToParent={handleBackToParent}\n />\n </Suspense>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAUA,MAAM,iBAAiB;AAAA,EAAK,MAC1B,OAAO,8BAA2B,EAAE,KAAK,CAAC,OAAO;AAAA,IAC/C,SAAS,EAAE;AAAA,EAAA,EACX;AACJ;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,iBAAiB;AACnB,IAAyD,IAAI;AAC3D,QAAM,EAAE,WAAW,cAAA,IAAkB,qBAAA;AACrC,QAAM,EAAE,OAAO,aAAa,QAAA,IAAY,qBAAA;AACxC,QAAM,WAAW,YAAA;AAGjB,QAAM,kBAAkB;AAGxB,QAAM,SAAS,UAAU,EAAE,QAAQ,OAAO;AAO1C,QAAM,mBAAmB,OAAO;AAEhC,QAAM,iBAAiB;AAAA,IACrB,CAAC,YAAoB,YAAoB;AACvC,WAAK,SAAS;AAAA,QACZ,IAAI;AAAA,QACJ,QAAQ,EAAE,MAAM,WAAA;AAAA,QAChB,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,KAAK,QAAA;AAAA,MAAQ,CAC5C;AAAA,IACH;AAAA,IACA,CAAC,QAAQ;AAAA,EAAA;AAGX,QAAM,qBAAqB,YAAY,MAAM;AAI3C,QAAI,iBAAiB;AACnB,WAAK,SAAS;AAAA,QACZ,IAAI;AAAA,QACJ,QAAQ,EAAE,MAAM,gBAAA;AAAA,QAChB,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,KAAK,YAAA;AAAA,MAAY,CAChD;AAAA,IACH;AAAA,EACF,GAAG,CAAC,UAAU,iBAAiB,WAAW,CAAC;AAE3C,QAAM,cAAc,YAAY;AAChC,QAAM,iBAAiB,QAAQ;AAG/B,QAAM,gBAAgB;AAAA,IACpB,MAAM,UAAU,OAAO,CAAC,MAAM,CAAC,EAAE,UAAU;AAAA,IAC3C,CAAC,SAAS;AAAA,EAAA;AAaZ,QAAM,uBAAuB;AAAA,IAC3B,MACE,cAAc,OAAO,CAAC,MAAM,YAAY,kBAAkB,CAAC,EAAE,UAAU;AAAA,IACzE,CAAC,eAAe,YAAY,cAAc;AAAA,EAAA;AAO5C,YAAU,MAAM;AAAA,EAahB,GAAG,CAAC,eAAe,UAAU,QAAQ,cAAc,MAAM,CAAC;AAQ1D,YAAU,MAAM;AACd,QAAI,CAAC,mBAAmB,UAAU,WAAW,EAAG;AAChD,UAAM,YAAY,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,eAAe;AAClE,QAAI,UAAW;AACf,UAAM,UAAU,UAAU,KAAK,CAAC;;AAAM,qBAAE,YAAF,mBAAW,SAAS;AAAA,KAAgB;AAC1E,QAAI,SAAS;AACX,WAAK,SAAS;AAAA,QACZ,IAAI;AAAA,QACJ,QAAQ,EAAE,MAAM,QAAQ,KAAA;AAAA,QACxB,QAAQ,CAAC,SAAS;AAAA,QAClB,SAAS;AAAA,MAAA,CACV;AAAA,IACH;AAAA,EACF,GAAG,CAAC,iBAAiB,WAAW,QAAQ,CAAC;AAEzC,QAAM,iBAAiB,CAAC,QAAkB;AACxC,SAAK,SAAS;AAAA,MACZ,IAAI;AAAA,MACJ,QAAQ,EAAE,MAAM,IAAI,KAAA;AAAA,MACpB,QAAQ,CAAC,SAAS;AAAA,IAAA,CACnB;AAAA,EACH;AAEA,QAAM,eAAe,CAAC,QAAkB;AACtC,QAAI,IAAI,OAAQ,QAAO,KAAK,IAAI,QAAQ,UAAU,qBAAqB;AAAA,EACzE;AAgBA,YAAU,MAAM;AACd,QAAI,gBAAgB,YAAa;AACjC,UAAM,QAAQ,CAAC,OAAO,OAAO,IAAI,EAAE;AAAA,MACjC,CAAC,QAAS,OAAmC,GAAG,MAAM;AAAA,IAAA;AAExD,QAAI,MAAM,WAAW,EAAG;AACxB,SAAK,SAAS;AAAA,MACZ,IAAI;AAAA,MACJ,SAAS;AAAA,MACT,QAAQ,CAAC,SAAS;AAChB,cAAM,OAAO,EAAE,GAAI,KAAA;AACnB,mBAAW,OAAO,MAAO,QAAO,KAAK,GAAG;AACxC,eAAO;AAAA,MACT;AAAA,IAAA,CACD;AAAA,EACH,GAAG,CAAC,cAAc,aAAa,QAAQ,QAAQ,CAAC;AAEhD,QAAM,cAAc;AAAA,IAClB,MACE,kBACK,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,eAAe,KAAK,OACtD;AAAA,IACN,CAAC,iBAAiB,SAAS;AAAA,EAAA;AAM7B,QAAM,cAAc,QAAQ,MAAM;AAChC,QAAI,CAAC,eAAe,CAAC,YAAa,QAAO;AACzC,WACE,UAAU;AAAA,MACR,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,eAAe,YAAY;AAAA,IAAA,KAC3D;AAAA,EAET,GAAG,CAAC,aAAa,aAAa,SAAS,CAAC;AAExC,MAAI,eAAe;AACjB,+BAAQ,OAAA,EAAI,WAAU,8BAA6B,UAAA,YAAQ;AAAA,EAC7D;AAQA;AAAA;AAAA;AAAA;AAAA,IAIE,qBAAC,OAAA,EAAI,WAAU,kEACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM;AAAA,UAGN,cAAc;AAAA,UACd;AAAA,UACA,gBAAgB;AAAA,UAChB,YAAY;AAAA,UACZ,YAAY;AAAA,UACZ,UAAU;AAAA,UACV,YAAY,gBAAgB;AAAA,UAC5B,iBAAiB;AAAA,QAAA;AAAA,MAAA;AAAA,0BAGlB,mBAAA,EAAkB;AAAA,MAElB,eACC,oBAAC,UAAA,EAAS,UAAU,MAClB,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK;AAAA,UACL,aAAa;AAAA,UACb,SAAS,MAAM,KAAK,SAAS,EAAE,IAAI,KAAK;AAAA,UACxC,YAAY;AAAA,UACZ,gBAAgB;AAAA,QAAA;AAAA,MAAA,GAEpB;AAAA,IAAA,GAEJ;AAAA;AAEJ;"}
|
|
@@ -36,4 +36,16 @@ export declare function useIsAuthenticated(): boolean;
|
|
|
36
36
|
* @returns Current user or null
|
|
37
37
|
*/
|
|
38
38
|
export declare function useUser(): User | null;
|
|
39
|
+
/**
|
|
40
|
+
* The current user, or null when there is no `AuthProvider` above.
|
|
41
|
+
*
|
|
42
|
+
* `useUser()` throws without a provider, which is correct for UI that only ever
|
|
43
|
+
* renders inside the app but wrong for anything a test may mount bare. Several
|
|
44
|
+
* components are rendered bare on purpose (`AppDetailPanel`, `AppCatalogGrid`),
|
|
45
|
+
* so assembling a plugin slot payload with `useUser()` there turns every one of
|
|
46
|
+
* those tests red — "useAuth must be used within AuthProvider".
|
|
47
|
+
*
|
|
48
|
+
* Signed-out and no-provider are the same answer for a slot payload: null.
|
|
49
|
+
*/
|
|
50
|
+
export declare function useOptionalUser(): User | null;
|
|
39
51
|
export {};
|
|
@@ -80,10 +80,15 @@ function useUser() {
|
|
|
80
80
|
const { user } = useAuth();
|
|
81
81
|
return user;
|
|
82
82
|
}
|
|
83
|
+
function useOptionalUser() {
|
|
84
|
+
var _a;
|
|
85
|
+
return ((_a = use(AuthContext)) == null ? void 0 : _a.user) ?? null;
|
|
86
|
+
}
|
|
83
87
|
export {
|
|
84
88
|
AuthProvider,
|
|
85
89
|
useAuth,
|
|
86
90
|
useIsAuthenticated,
|
|
91
|
+
useOptionalUser,
|
|
87
92
|
useUser
|
|
88
93
|
};
|
|
89
94
|
//# sourceMappingURL=AuthContext.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthContext.js","sources":["../../../../src/modules/auth/AuthContext.tsx"],"sourcesContent":["import type { Session } from 'better-auth/types'\nimport { createContext, use, useEffect, useMemo, useState } from 'react'\n\ninterface User {\n id: string\n email?: string | null\n name?: string | null\n image?: string | null\n emailVerified?: boolean\n isAdmin?: boolean\n}\n\ninterface AuthContextType {\n session: Session | null\n user: User | null\n isLoading: boolean\n isAuthenticated: boolean\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nconst STORAGE_KEY = 'ac_auth_user'\n\n/**\n * Provider component for authentication state\n * Fetches and manages session from the backend\n * Uses localStorage for optimistic rendering to avoid flickering\n */\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [session, setSession] = useState<Session | null>(null)\n const [user, setUser] = useState<User | null>(() => {\n // Hydrate from localStorage on mount to avoid flickering\n if (typeof window === 'undefined') return null\n try {\n const stored = localStorage.getItem(STORAGE_KEY)\n return stored ? JSON.parse(stored) : null\n } catch {\n return null\n }\n })\n const [isLoading, setIsLoading] = useState(true)\n\n useEffect(() => {\n // Use AbortController to cancel fetch on unmount (e.g., React StrictMode double-mount)\n const abortController = new AbortController()\n\n // Fetch session from backend\n async function fetchSession() {\n try {\n const baseUrl =\n typeof window !== 'undefined' ? window.location.origin : ''\n const response = await fetch(`${baseUrl}/api/auth/session`, {\n credentials: 'include',\n signal: abortController.signal,\n })\n if (response.ok) {\n const data = await response.json()\n setSession(data)\n setUser(data?.user || null)\n if (data?.user) {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data.user))\n } else {\n localStorage.removeItem(STORAGE_KEY)\n }\n } else {\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n }\n } catch (error) {\n // Ignore abort errors (happens on cleanup)\n if (error instanceof Error && error.name === 'AbortError') {\n return\n }\n console.error('Failed to fetch session:', error)\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n } finally {\n setIsLoading(false)\n }\n }\n\n fetchSession()\n\n // Cleanup: abort fetch on unmount\n return () => {\n abortController.abort()\n }\n }, [])\n\n const value: AuthContextType = useMemo(\n () => ({\n session,\n user,\n isLoading,\n isAuthenticated: !!user,\n }),\n [session, user, isLoading],\n )\n\n return <AuthContext value={value}>{children}</AuthContext>\n}\n\n/**\n * Hook to access authentication state\n * @returns Authentication context containing session, user, and auth state\n */\nexport function useAuth() {\n const context = use(AuthContext)\n if (!context) {\n throw new Error('useAuth must be used within AuthProvider')\n }\n return context\n}\n\n/**\n * Hook to check if user is authenticated\n * @returns Boolean indicating if user is authenticated\n */\nexport function useIsAuthenticated() {\n const { isAuthenticated } = useAuth()\n return isAuthenticated\n}\n\n/**\n * Hook to get the current user\n * @returns Current user or null\n */\nexport function useUser() {\n const { user } = useAuth()\n return user\n}\n"],"names":[],"mappings":";;AAmBA,MAAM,cAAc,cAA2C,MAAS;AAExE,MAAM,cAAc;AAOb,SAAS,aAAa,EAAE,YAA2C;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,MAAM;AAElD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAI;AACF,YAAM,SAAS,aAAa,QAAQ,WAAW;AAC/C,aAAO,SAAS,KAAK,MAAM,MAAM,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MAAM;AAEd,UAAM,kBAAkB,IAAI,gBAAA;AAG5B,mBAAe,eAAe;AAC5B,UAAI;AACF,cAAM,UACJ,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AAC3D,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,qBAAqB;AAAA,UAC1D,aAAa;AAAA,UACb,QAAQ,gBAAgB;AAAA,QAAA,CACzB;AACD,YAAI,SAAS,IAAI;AACf,gBAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,qBAAW,IAAI;AACf,mBAAQ,6BAAM,SAAQ,IAAI;AAC1B,cAAI,6BAAM,MAAM;AACd,yBAAa,QAAQ,aAAa,KAAK,UAAU,KAAK,IAAI,CAAC;AAAA,UAC7D,OAAO;AACL,yBAAa,WAAW,WAAW;AAAA,UACrC;AAAA,QACF,OAAO;AACL,qBAAW,IAAI;AACf,kBAAQ,IAAI;AACZ,uBAAa,WAAW,WAAW;AAAA,QACrC;AAAA,MACF,SAAS,OAAO;AAEd,YAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD;AAAA,QACF;AACA,gBAAQ,MAAM,4BAA4B,KAAK;AAC/C,mBAAW,IAAI;AACf,gBAAQ,IAAI;AACZ,qBAAa,WAAW,WAAW;AAAA,MACrC,UAAA;AACE,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,iBAAA;AAGA,WAAO,MAAM;AACX,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAyB;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB,CAAC,CAAC;AAAA,IAAA;AAAA,IAErB,CAAC,SAAS,MAAM,SAAS;AAAA,EAAA;AAG3B,SAAO,oBAAC,aAAA,EAAY,OAAe,SAAA,CAAS;AAC9C;AAMO,SAAS,UAAU;AACxB,QAAM,UAAU,IAAI,WAAW;AAC/B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB;AACnC,QAAM,EAAE,gBAAA,IAAoB,QAAA;AAC5B,SAAO;AACT;AAMO,SAAS,UAAU;AACxB,QAAM,EAAE,KAAA,IAAS,QAAA;AACjB,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"AuthContext.js","sources":["../../../../src/modules/auth/AuthContext.tsx"],"sourcesContent":["import type { Session } from 'better-auth/types'\nimport { createContext, use, useEffect, useMemo, useState } from 'react'\n\ninterface User {\n id: string\n email?: string | null\n name?: string | null\n image?: string | null\n emailVerified?: boolean\n isAdmin?: boolean\n}\n\ninterface AuthContextType {\n session: Session | null\n user: User | null\n isLoading: boolean\n isAuthenticated: boolean\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nconst STORAGE_KEY = 'ac_auth_user'\n\n/**\n * Provider component for authentication state\n * Fetches and manages session from the backend\n * Uses localStorage for optimistic rendering to avoid flickering\n */\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [session, setSession] = useState<Session | null>(null)\n const [user, setUser] = useState<User | null>(() => {\n // Hydrate from localStorage on mount to avoid flickering\n if (typeof window === 'undefined') return null\n try {\n const stored = localStorage.getItem(STORAGE_KEY)\n return stored ? JSON.parse(stored) : null\n } catch {\n return null\n }\n })\n const [isLoading, setIsLoading] = useState(true)\n\n useEffect(() => {\n // Use AbortController to cancel fetch on unmount (e.g., React StrictMode double-mount)\n const abortController = new AbortController()\n\n // Fetch session from backend\n async function fetchSession() {\n try {\n const baseUrl =\n typeof window !== 'undefined' ? window.location.origin : ''\n const response = await fetch(`${baseUrl}/api/auth/session`, {\n credentials: 'include',\n signal: abortController.signal,\n })\n if (response.ok) {\n const data = await response.json()\n setSession(data)\n setUser(data?.user || null)\n if (data?.user) {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data.user))\n } else {\n localStorage.removeItem(STORAGE_KEY)\n }\n } else {\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n }\n } catch (error) {\n // Ignore abort errors (happens on cleanup)\n if (error instanceof Error && error.name === 'AbortError') {\n return\n }\n console.error('Failed to fetch session:', error)\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n } finally {\n setIsLoading(false)\n }\n }\n\n fetchSession()\n\n // Cleanup: abort fetch on unmount\n return () => {\n abortController.abort()\n }\n }, [])\n\n const value: AuthContextType = useMemo(\n () => ({\n session,\n user,\n isLoading,\n isAuthenticated: !!user,\n }),\n [session, user, isLoading],\n )\n\n return <AuthContext value={value}>{children}</AuthContext>\n}\n\n/**\n * Hook to access authentication state\n * @returns Authentication context containing session, user, and auth state\n */\nexport function useAuth() {\n const context = use(AuthContext)\n if (!context) {\n throw new Error('useAuth must be used within AuthProvider')\n }\n return context\n}\n\n/**\n * Hook to check if user is authenticated\n * @returns Boolean indicating if user is authenticated\n */\nexport function useIsAuthenticated() {\n const { isAuthenticated } = useAuth()\n return isAuthenticated\n}\n\n/**\n * Hook to get the current user\n * @returns Current user or null\n */\nexport function useUser() {\n const { user } = useAuth()\n return user\n}\n\n/**\n * The current user, or null when there is no `AuthProvider` above.\n *\n * `useUser()` throws without a provider, which is correct for UI that only ever\n * renders inside the app but wrong for anything a test may mount bare. Several\n * components are rendered bare on purpose (`AppDetailPanel`, `AppCatalogGrid`),\n * so assembling a plugin slot payload with `useUser()` there turns every one of\n * those tests red — \"useAuth must be used within AuthProvider\".\n *\n * Signed-out and no-provider are the same answer for a slot payload: null.\n */\nexport function useOptionalUser() {\n return use(AuthContext)?.user ?? null\n}\n"],"names":[],"mappings":";;AAmBA,MAAM,cAAc,cAA2C,MAAS;AAExE,MAAM,cAAc;AAOb,SAAS,aAAa,EAAE,YAA2C;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,MAAM;AAElD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAI;AACF,YAAM,SAAS,aAAa,QAAQ,WAAW;AAC/C,aAAO,SAAS,KAAK,MAAM,MAAM,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MAAM;AAEd,UAAM,kBAAkB,IAAI,gBAAA;AAG5B,mBAAe,eAAe;AAC5B,UAAI;AACF,cAAM,UACJ,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AAC3D,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,qBAAqB;AAAA,UAC1D,aAAa;AAAA,UACb,QAAQ,gBAAgB;AAAA,QAAA,CACzB;AACD,YAAI,SAAS,IAAI;AACf,gBAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,qBAAW,IAAI;AACf,mBAAQ,6BAAM,SAAQ,IAAI;AAC1B,cAAI,6BAAM,MAAM;AACd,yBAAa,QAAQ,aAAa,KAAK,UAAU,KAAK,IAAI,CAAC;AAAA,UAC7D,OAAO;AACL,yBAAa,WAAW,WAAW;AAAA,UACrC;AAAA,QACF,OAAO;AACL,qBAAW,IAAI;AACf,kBAAQ,IAAI;AACZ,uBAAa,WAAW,WAAW;AAAA,QACrC;AAAA,MACF,SAAS,OAAO;AAEd,YAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD;AAAA,QACF;AACA,gBAAQ,MAAM,4BAA4B,KAAK;AAC/C,mBAAW,IAAI;AACf,gBAAQ,IAAI;AACZ,qBAAa,WAAW,WAAW;AAAA,MACrC,UAAA;AACE,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,iBAAA;AAGA,WAAO,MAAM;AACX,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAyB;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB,CAAC,CAAC;AAAA,IAAA;AAAA,IAErB,CAAC,SAAS,MAAM,SAAS;AAAA,EAAA;AAG3B,SAAO,oBAAC,aAAA,EAAY,OAAe,SAAA,CAAS;AAC9C;AAMO,SAAS,UAAU;AACxB,QAAM,UAAU,IAAI,WAAW;AAC/B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB;AACnC,QAAM,EAAE,gBAAA,IAAoB,QAAA;AAC5B,SAAO;AACT;AAMO,SAAS,UAAU;AACxB,QAAM,EAAE,KAAA,IAAS,QAAA;AACjB,SAAO;AACT;AAaO,SAAS,kBAAkB;;AAChC,WAAO,SAAI,WAAW,MAAf,mBAAkB,SAAQ;AACnC;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { authClient } from './authClient.js';
|
|
2
|
-
export { AuthProvider, useAuth, useIsAuthenticated, useUser, } from './AuthContext.js';
|
|
2
|
+
export { AuthProvider, useAuth, useIsAuthenticated, useUser, useOptionalUser, } from './AuthContext.js';
|
|
3
3
|
export { ProtectedContent, withProtection } from './ProtectedContent.js';
|
|
4
4
|
export { LoginPage } from './ui/LoginPage.js';
|
|
5
5
|
export { useAuthActions } from './useAuthActions.js';
|