@igstack/app-catalog-frontend-core 6.11.0 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
4
4
  import { useOptionalUser } from "../../../auth/AuthContext.js";
5
5
  import "@tanstack/react-query";
6
6
  import "../../../../ui/button.js";
7
+ import "../../../auth/AuthModalContext.js";
7
8
  import { getChildResources } from "../../utils/resolveHelpers.js";
8
9
  import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
9
10
  import { AppDetails } from "../detail/AppDetails.js";
@@ -1 +1 @@
1
- {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\nimport { DetailDialogShell } from './DetailDialogShell'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * The overlay itself — scrim, centring, Widen, Close, Escape — is\n * `DetailDialogShell`, shared with the community tool card.\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const shownSlug = subResource?.slug ?? app.slug\n\n return (\n <DetailDialogShell\n label={`${subResource?.displayName ?? app.displayName} details`}\n shownKey={shownSlug}\n // Wide enough for data tables (sub-resources have 5 columns incl.\n // \"Access Contacts\"/\"AWS Account\" that wrapped at 760px); prose inside\n // AppDetails is width-capped separately so it stays readable. Caps at\n // 94vw so it never touches the edges.\n maxWidthClass=\"max-w-[min(1120px,94vw)]\"\n focusSelector=\".app-door\"\n onClose={onClose}\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={<ResourceDetailAccessActions {...subResourceActionsCtx} />}\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </DetailDialogShell>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA0BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,OAAO,gBAAA;AAIb,QAAM,wBAAwB;AAAA,IAC5B,OAAO;AAAA,MACL,UAAU,eAAe;AAAA,MACzB,QAAQ,cAAc,MAAM;AAAA,MAC5B,cAAc,kBAAkB,cAAc,IAAI,IAAI;AAAA,MACtD,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU;AAAA,IAAA;AAAA,IAEvE,CAAC,aAAa,KAAK,cAAc,IAAI;AAAA,EAAA;AAEvC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAE3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,MACrD,UAAU;AAAA,MAKV,eAAc;AAAA,MACd,eAAc;AAAA,MACd;AAAA,MAKA,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,UAAU,eAAe;AAAA,UACzB,QAAQ,cAAc,MAAM;AAAA,UAE3B,UAAA,cACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR;AAAA,cACA,QAAQ,kBAAkB;AAAA,cAC1B,SAAS,oBAAC,6BAAA,EAA6B,GAAG,sBAAA,CAAuB;AAAA,YAAA;AAAA,UAAA,IAGnE,oBAAC,YAAA,EAAW,KAAU,WAAA,CAAwB;AAAA,QAAA;AAAA,MAAA;AAAA,IAElD;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\nimport { DetailDialogShell } from './DetailDialogShell'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * The overlay itself — scrim, centring, Widen, Close, Escape — is\n * `DetailDialogShell`, shared with the community tool card.\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const shownSlug = subResource?.slug ?? app.slug\n\n return (\n <DetailDialogShell\n label={`${subResource?.displayName ?? app.displayName} details`}\n shownKey={shownSlug}\n // Wide enough for data tables (sub-resources have 5 columns incl.\n // \"Access Contacts\"/\"AWS Account\" that wrapped at 760px); prose inside\n // AppDetails is width-capped separately so it stays readable. Caps at\n // 94vw so it never touches the edges.\n maxWidthClass=\"max-w-[min(1120px,94vw)]\"\n focusSelector=\".app-door\"\n onClose={onClose}\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={<ResourceDetailAccessActions {...subResourceActionsCtx} />}\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </DetailDialogShell>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AA0BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,OAAO,gBAAA;AAIb,QAAM,wBAAwB;AAAA,IAC5B,OAAO;AAAA,MACL,UAAU,eAAe;AAAA,MACzB,QAAQ,cAAc,MAAM;AAAA,MAC5B,cAAc,kBAAkB,cAAc,IAAI,IAAI;AAAA,MACtD,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU;AAAA,IAAA;AAAA,IAEvE,CAAC,aAAa,KAAK,cAAc,IAAI;AAAA,EAAA;AAEvC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAE3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,MACrD,UAAU;AAAA,MAKV,eAAc;AAAA,MACd,eAAc;AAAA,MACd;AAAA,MAKA,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,UAAU,eAAe;AAAA,UACzB,QAAQ,cAAc,MAAM;AAAA,UAE3B,UAAA,cACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR;AAAA,cACA,QAAQ,kBAAkB;AAAA,cAC1B,SAAS,oBAAC,6BAAA,EAA6B,GAAG,sBAAA,CAAuB;AAAA,YAAA;AAAA,UAAA,IAGnE,oBAAC,YAAA,EAAW,KAAU,WAAA,CAAwB;AAAA,QAAA;AAAA,MAAA;AAAA,IAElD;AAAA,EAAA;AAGN;"}
@@ -12,6 +12,7 @@ import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
12
12
  import { PersonBadge } from "../components/PersonBadge.js";
13
13
  import { useUser } from "../../../auth/AuthContext.js";
14
14
  import "@tanstack/react-query";
15
+ import "../../../auth/AuthModalContext.js";
15
16
  import { InlineEditableField } from "../components/InlineEditableField.js";
16
17
  import { MarkdownText } from "../components/MarkdownText.js";
17
18
  import { ScreenshotGallery } from "../components/ScreenshotGallery.js";
@@ -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 { Link, useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { useCommunityToolCount } from './useCommunityToolCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n const communityCount = useCommunityToolCount(app.slug)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n target={{ resourceSlug: app.slug }}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {communityCount > 0 && (\n <p className=\"mt-2.5 text-xs\">\n <Link\n to=\"/community\"\n search={{ tag: `app:${app.slug}` }}\n data-testid=\"app-community-tools\"\n className=\"text-muted-foreground hover:text-primary hover:underline\"\n >\n Community tools for {app.displayName} · {communityCount}\n </Link>\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
1
+ {"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { Link, useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { useCommunityToolCount } from './useCommunityToolCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n const communityCount = useCommunityToolCount(app.slug)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n target={{ resourceSlug: app.slug }}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {communityCount > 0 && (\n <p className=\"mt-2.5 text-xs\">\n <Link\n to=\"/community\"\n search={{ tag: `app:${app.slug}` }}\n data-testid=\"app-community-tools\"\n className=\"text-muted-foreground hover:text-primary hover:underline\"\n >\n Community tools for {app.displayName} · {communityCount}\n </Link>\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,MAAM;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,eAAe;AAAA,EACf,QAAQ;AACV;AAMA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AACF,GAGG;AACD,SACE,qBAAC,WAAA,EAAQ,WAAU,gBACjB,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,uBAAuB,UAAA,OAAM;AAAA,IAC1C;AAAA,EAAA,GACH;AAEJ;AAGA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE,qBAAC,OAAA,EAAI,WAAW,QAAQ,KAAK,QAC3B,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,wBAAwB,UAAA,OAAM;AAAA,IAC3C,QAAQ,oBAAC,KAAA,EAAE,WAAU,sCAAsC,UAAA,MAAK;AAAA,IAChE;AAAA,EAAA,GACH;AAEJ;AAEA,SAAS,cAAc,EAAE,OAA0B;;AACjD,QAAM,CAAC,YAAY,aAAa,IAAIA,eAAM,SAAS,KAAK;AACxD,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,eAAM,SAAS,IAAI;AAE/D,QAAM,gBAAe,SAAI,kBAAJ,mBAAoB;AACzC,MAAI,CAAC,cAAc;AACjB,WACE,oBAAC,SAAI,WAAU,2FACb,8BAAC,OAAA,EAAI,WAAU,0FAAyF,UAAA,0BAAA,CAExG,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,qBAAqB,oBAAoB,YAAY;AAE3D,6BACG,OAAA,EAAI,WAAU,8BACb,UAAA,qBAAC,OAAA,EAAI,WAAU,+EACZ,UAAA;AAAA,IAAA,CAAC,aACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,QAC3C,WAAU;AAAA,QACV,SAAS,MAAM;AACb,wBAAc,IAAI;AAClB,4BAAkB,KAAK;AAAA,QACzB;AAAA,QACA,QAAQ,MAAM,kBAAkB,KAAK;AAAA,MAAA;AAAA,IAAA,IAErC;AAAA,KACF,cAAc,mBACd,oBAAC,OAAA,EAAI,WAAU,0FACZ,UAAA,iBACG,0BACA,0BAAA,CACN;AAAA,EAAA,EAAA,CAEJ,EAAA,CACF;AAEJ;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAGG;;AACD,QAAM,CAAC,eAAe,gBAAgB,IAAIA,eAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,qBAAqB,sBAAsB,IAAIA,eAAM,SAAS,CAAC;AAEtE,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,EAAE,YAAA,IAAgB,mBAAA;AACxB,QAAM,YAAY,aAAA;AAClB,QAAM,CAAC,aAAa,cAAc,IAAIA,eAAM,SAAwB,IAAI;AACxE,QAAM,OAAO,QAAA;AACb,QAAM,WAAU,6BAAM,YAAW;AACjC,QAAM,EAAE,OAAO,YAAA,IAAgB,qBAAA;AAE/B,QAAM,cAAc,gBAAgB,IAAI,MAAM,IAAI,MAAM;AACxD,QAAM,iBAAiB,sBAAsB,IAAI,IAAI;AAGrD,QAAM,eAAc,SAAI,cAAJ,mBAAe;AACnC,QAAM,YAAY,CAAA;AAClB,MAAI,aAAa;AACf,UAAM,WAAS,SAAI,cAAJ,mBAAe,WAAU,0BAA0B;AAClE,cAAU,KAAK,gBAAgB,mBAAmB,WAAW,CAAC,GAAG,MAAM,EAAE;AAAA,EAC3E;AACA,MAAI,IAAI,WAAW;AACjB,UAAM,aAAa,UAAU,SAAS,IAAI,UAAU;AACpD,cAAU,KAAK,GAAG,UAAU,IAAI,mBAAmB,IAAI,SAAS,CAAC,EAAE;AAAA,EACrE;AACA,QAAM,YAAY,UAAU,KAAK,KAAK;AACtC,QAAM,aAAa;AAAA,MACjB,SAAI,cAAJ,mBAAe,wBACb,wBAAwB,IAAI,UAAU,mBAAmB;AAAA,IAC3D,eAAe,gBAAgB,WAAW;AAAA,IAC1C,IAAI,aAAa,SAAS,IAAI,SAAS;AAAA,EAAA,EAEtC,OAAO,OAAO,EACd,KAAK,KAAK;AAMb,QAAM,cAAcA,eAAM,OAA+B,IAAI;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAIA,eAAM,SAAS,CAAC;AAChDA,iBAAM,UAAU,MAAM;;AACpB,QAAI,SAAU,EAAAC,MAAA,YAAY,YAAZ,gBAAAA,IAAqB;AAAA,EACrC,GAAG,CAAC,QAAQ,CAAC;AAKb,QAAM,SAAS,UAAU,EAAE,QAAQ,OAAO;AAC1C,QAAM,UAAU,OAAO;AAEvB,QAAM,eAAe,IAAI,QAAQ,CAAA,GAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,SAAS,GAAG,CAAC;AACvE,QAAM,eACJ,SAAI,YAAJ,mBAAa,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,IAAI,EAAE,SAAS,CAAA;AAClE,QAAM,iBACJ,gBAAgB,OAAO,CAAC,GAAG,YAAY,WAAW,IAAI;AAIxD,QAAM,aAAaD,eAAM,QAAQ,MAAM;AACrC,UAAM,4BAAY,IAAA;AAClB,eAAW,UAAU,IAAI,WAAW,CAAA,GAAI;AACtC,UAAI,OAAO,WAAW,YAAY,OAAO,OAAO;AAC9C,cAAM,IAAI,OAAO,KAAK,OAAO,KAAK;AAAA,MACpC;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,OAAO,CAAC;AAEhB,QAAM,WAAWA,eAAM;AAAA,IACrB,MAAM,kBAAkB,cAAc,IAAI,IAAI;AAAA,IAC9C,CAAC,cAAc,IAAI,IAAI;AAAA,EAAA;AAMzB,QAAM,SAASA,eAAM;AAAA,IACnB,MACE,IAAI,aACA,aAAa,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,UAAU,IAClD;AAAA,IACN,CAAC,cAAc,IAAI,UAAU;AAAA,EAAA;AAK/B,QAAM,mBAAmBA,eAAM;AAAA,IAC7B,OAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,cAAc,SACV,kBAAkB,cAAc,OAAO,IAAI,IAC3C;AAAA,MACJ,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,YAAY;AAAA,IAAA;AAAA,IAEhD,CAAC,KAAK,QAAQ,cAAc,UAAU,MAAM,OAAO;AAAA,EAAA;AAKrD;AAAA,IACE;AAAA,IACA,MAAM;;AACJ,YAAM,OAAMC,MAAA,SAAS,kBAAT,gBAAAA,IAAwB;AACpC,UACE,QAAQ,YACR,QAAQ,OACR,QAAQ,WACR,QAAQ,YACR,QAAQ;AAER;AAEF,UAAI,IAAI,iBAAiB,IAAI,cAAc,SAAS,GAAG;AACrD,+BAAuB,CAAC;AACxB,yBAAiB,IAAI;AAAA,MACvB;AAAA,IACF;AAAA,IACA,EAAE,SAAS,CAAC,cAAA;AAAA,IACZ,CAAC,KAAK,aAAa;AAAA,EAAA;AAGrB,QAAM,wBAAwB,CAAC,UAAkB;AAC/C,2BAAuB,KAAK;AAC5B,qBAAiB,IAAI;AAAA,EACvB;AAGA,QAAM,mBAAiB,SAAI,eAAJ,mBAAgB,mBACnC,aAAa,KAAK,CAAC,MAAA;;AAAM,aAAE,WAASA,MAAA,IAAI,eAAJ,gBAAAA,IAAgB;AAAA,GAAe,IACnE;AAEJ,QAAM,SAAS,eAAe,KAAK,eAAe;AAElD,QAAM,kBAAkB,SAAS,SAAS,QAAM,SAAI,UAAJ,mBAAW,WAAU,KAAK;AAC1E,QAAM,iBAAiB,SAAS,SAC3B,IAAI,iBAAiB,cACtB;AAEJ,QAAM,yBAAuB,SAAI,UAAJ,mBAAW,WAAU,KAAK,KAAK,CAAC,CAAC,IAAI;AAElE,QAAM,OAAoB;AAAA,IACxB;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO;AAAA,MACP,QAAQ,MACN,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,SAAA,EAAQ,OAAM,eAAc,OAAK,MAC/B,UAAA,UACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI,eAAe;AAAA,YAC1B,QAAQ,CAAC,gBACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,YAAA,GAAe;AAAA,YAExD,WAAS;AAAA,YACT,aAAY;AAAA,YACZ,WAAU;AAAA,UAAA;AAAA,QAAA,IAEV,IAAI,cACN,oBAAC,gBAAa,WAAU,qEACrB,UAAA,IAAI,YAAA,CACP,IAEA,oBAAC,KAAA,EAAE,WAAU,iCAAgC,eAAC,GAElD;AAAA,QAEC,IAAI,iBAAiB,IAAI,cAAc,SAAS,KAC/C,oBAAC,SAAA,EAAQ,OAAO,gBAAgB,IAAI,cAAc,MAAM,KAGtD,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,uBAAuB,IAAI,WAAW;AAAA,YAClD,WAAU;AAAA,YACV,SAAS,MAAM,sBAAsB,CAAC;AAAA,YAEtC,UAAA;AAAA,cAAA,oBAAC,iBAAc,KAAU;AAAA,cACxB,IAAI,cAAc,SAAS,KAC1B,qBAAC,KAAA,EAAE,WAAU,kDAAiD,UAAA;AAAA,gBAAA;AAAA,gBACzC,IAAI,cAAc;AAAA,gBAAO;AAAA,cAAA,EAAA,CAC9C;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,GAGN;AAAA,QAKD,IAAI,mBACH,oBAAC,SAAA,EAAQ,OAAM,SAAQ,MAAK,wCAC1B,UAAA,oBAAC,aAAA,EAAY,MAAM,IAAI,iBAAiB,GAC1C;AAAA,QAOD,YAAY,SAAS,KACpB,oBAAC,SAAA,EAAQ,OAAM,QACb,UAAA,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,YAAY,IAAI,CAAC,QAChB,oBAAC,OAAA,EAAgB,SAAQ,aAAY,WAAU,WAC5C,UAAA,IAAA,GADS,GAEZ,CACD,EAAA,CACH,EAAA,CACF;AAAA,QAGD,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,SAAA,EAAQ,OAAM,SACb,UAAA,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,IAAI,MAAM,IAAI,CAAC,SACd,oBAAC,OAAA,EAAiB,SAAQ,WAAU,WAAU,WAC3C,UAAA,KAAA,GADS,IAEZ,CACD,GACH,EAAA,CACF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA;AAAA,IAGJ;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO,YAAY,OAAO,KAAK;AAAA,MAC/B,MAAM,OAAO;AAAA,MACb,QAAQ,MACN,qBAAA,UAAA,EAGE,UAAA;AAAA,QAAA,oBAAC,yBAAA,EAAwB,UAAU,KAAK,cAAc,YAAY;AAAA,QAClE,oBAAC,sBAAA,EAAqB,KAAU,gBAAA,CAAkC;AAAA,MAAA,EAAA,CACpE;AAAA,IAAA;AAAA,IAGJ,GAAI,kBACA;AAAA,MACE;AAAA,QACE,IAAI,IAAI;AAAA,QACR,OAAO;AAAA,QACP,MAAM,SAAS,SAAS,OAAO,SAAS,MAAM,IAAI;AAAA,QAClD,QAAQ,MAAA;;AACN,sCAAA,UAAA,EACG,UAAA;AAAA,YAAA,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,qBAAA,EAAoB,OAAO,IAAI,MAAA,CAAO;AAAA,YAExC,SAAS,SAAS,KACjB,oBAAC,OAAA,EAAI,aAAWA,MAAA,IAAI,UAAJ,gBAAAA,IAAW,UAAS,SAAS,IAI3C,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,cAAc;AAAA,gBACd,YAAY,IAAI;AAAA,gBAChB,QAAQ;AAAA,gBACR,eAAe,YAAY;AAAA,gBAC3B,eAAe,IAAI;AAAA,cAAA;AAAA,YAAA,EACrB,CACF;AAAA,UAAA,EAAA,CAEJ;AAAA;AAAA,MAAA;AAAA,IAEJ,IAEF,CAAA;AAAA,IACJ,GAAI,sBACA;AAAA,MACE;AAAA,QACE,IAAI,IAAI;AAAA,QACR,OAAO;AAAA,QACP,QAAQ,MAAA;;AACN,sCAAA,UAAA,EACG,UAAA;AAAA,YAAA,IAAI,SAAS,IAAI,MAAM,SAAS,KAC/B,oBAAC,WAAQ,OAAM,iBAAgB,OAAK,MAClC,UAAA,oBAAC,SAAI,WAAU,eACZ,cAAI,MAAM,IAAI,CAAC,SACd;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,MAAM,KAAK;AAAA,gBACX,QAAO;AAAA,gBACP,KAAI;AAAA,gBACJ,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAA,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,kBACzC,KAAK,SAAS,WAAW,KAAK,GAAG;AAAA,gBAAA;AAAA,cAAA;AAAA,cAP7B,KAAK;AAAA,YAAA,CASb,GACH,EAAA,CACF;AAAA,YAKD,IAAI,cACH,oBAAC,SAAA,EAAQ,OAAM,cAAa,OAAO,GAACA,MAAA,IAAI,UAAJ,gBAAAA,IAAW,SAC7C,UAAA,oBAAC,cAAA,EAAa,WAAU,uGACrB,UAAA,IAAI,YACP,EAAA,CACF;AAAA,UAAA,EAAA,CAEJ;AAAA;AAAA,MAAA;AAAA,IAEJ,IAEF,CAAA;AAAA,IACJ;AAAA,MACE,IAAI,IAAI;AAAA,MACR,OAAO;AAAA,MACP,MAAM,cAAc,OAAO,WAAW,IAAI;AAAA,MAC1C,WAAW,cACP,qBAAqB,WAAW,wBAChC;AAAA,MACJ,QAAQ,MAAA;;AACN,oCAAC,OAAA,EAAI,WAAU,gBACb,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,QAAQ,EAAE,cAAc,IAAI,KAAA;AAAA,cAC5B,SAAS,IAAI;AAAA,cACb,QAAQ,IAAI;AAAA,YAAA;AAAA,UAAA;AAAA,WAGZ,WACA,WAAW,SAAS,KACpB,IAAI,eACJA,MAAA,IAAI,cAAJ,gBAAAA,IAAe,mBACf,qBAAC,OAAA,EAAM,OAAM,wBACV,UAAA;AAAA,YAAA,UACC,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,QAAG,WAAU,aACX,yBAAe,IAAI,CAAC,KAAK,UAAU;AAClC,sBAAM,UACJ,gBAAgB,QAAQ,UAAU,WAAW;AAC/C,uBACE;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBAEC,WAAU;AAAA,oBAEV,UAAA;AAAA,sBAAA,qBAAC,QAAA,EAAK,WAAU,+CACb,UAAA;AAAA,wBAAA,QAAQ;AAAA,wBAAE;AAAA,sBAAA,GACb;AAAA,sBACA;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,OAAO;AAAA,0BACP,iBAAiB;AAAA,0BACjB,UACE,UAAU,MAAM,eAAe,IAAI,IAAI;AAAA,0BAEzC,QAAQ,CAAC,WAAW;AAClB,gCAAI,SAAS;AACX,6CAAe,IAAI;AACnB,kCAAI,QAAQ;AACV,0CAAU,OAAO;AAAA,kCACf,IAAI,IAAI;AAAA,kCACR,MAAM,EAAE,SAAS,CAAC,GAAG,YAAY,MAAM,EAAA;AAAA,gCAAE,CAC1C;AAAA,8BACH;AAAA,4BACF,OAAO;AACL,oCAAM,OAAO,CAAC,GAAG,UAAU;AAC3B,mCAAK,KAAK,IAAI;AACd,wCAAU,OAAO;AAAA,gCACf,IAAI,IAAI;AAAA,gCACR,MAAM,EAAE,SAAS,KAAK,OAAO,OAAO,EAAA;AAAA,8BAAE,CACvC;AAAA,4BACH;AAAA,0BACF;AAAA,0BACA,aAAY;AAAA,0BACZ,eAAc;AAAA,0BACd,YAAY,CAAC,QACX,MACE;AAAA,4BAAC;AAAA,4BAAA;AAAA,8BACC,MAAM;AAAA,8BACN,QAAO;AAAA,8BACP,KAAI;AAAA,8BACJ,WAAU;AAAA,8BAET,UAAA;AAAA,gCAAA,WAAW,GAAG;AAAA,gCACf,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,8BAAA;AAAA,4BAAA;AAAA,0BAAA,IAG5C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC;AAAA,wBAAA;AAAA,sBAAA;AAAA,sBAI9C,CAAC,WAAW,WAAW,IAAI,GAAG,KAC7B;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,OAAO,WAAW,IAAI,GAAG;AAAA,0BACzB,WAAU;AAAA,wBAAA;AAAA,sBAAA;AAAA,sBAGb,CAAC,WACA;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,MAAK;AAAA,0BACL,SAAQ;AAAA,0BACR,MAAK;AAAA,0BACL,cAAW;AAAA,0BACX,WAAU;AAAA,0BACV,SAAS,MAAM;AACb,kCAAM,OAAO,WAAW;AAAA,8BACtB,CAAC,GAAG,MAAM,MAAM;AAAA,4BAAA;AAElB,sCAAU,OAAO;AAAA,8BACf,IAAI,IAAI;AAAA,8BACR,MAAM,EAAE,SAAS,KAAA;AAAA,4BAAK,CACvB;AAAA,0BACH;AAAA,0BAEA,UAAA,oBAAC,QAAA,EAAO,WAAU,WAAA,CAAW;AAAA,wBAAA;AAAA,sBAAA;AAAA,oBAC/B;AAAA,kBAAA;AAAA,kBAxEG,UAAU,UAAU,GAAG,KAAK,IAAI,GAAG;AAAA,gBAAA;AAAA,cA4E9C,CAAC,EAAA,CACH;AAAA,cACA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,SAAS,MAAM,eAAe,EAAE;AAAA,kBAEhC,UAAA;AAAA,oBAAA,oBAAC,MAAA,EAAK,WAAU,WAAA,CAAW;AAAA,oBAAE;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,YAE/B,EAAA,CACF,wBAEC,MAAA,EAAG,WAAU,aACX,UAAA,WAAW,IAAI,CAAC,KAAK;AAAA;AAAA;AAAA;AAAA,cAIpB;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBAEC,WAAU;AAAA,kBAEV,UAAA;AAAA,oBAAA,qBAAC,QAAA,EAAK,WAAU,+CACb,UAAA;AAAA,sBAAA,QAAQ;AAAA,sBAAE;AAAA,oBAAA,GACb;AAAA,oBACC,MACC;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,MAAM;AAAA,wBACN,QAAO;AAAA,wBACP,KAAI;AAAA,wBACJ,WAAU;AAAA,wBAET,UAAA;AAAA,0BAAA,WAAW,GAAG;AAAA,0BACf,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,wBAAA;AAAA,sBAAA;AAAA,oBAAA,IAG5C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,oBAE1C,WAAW,IAAI,GAAG,KACjB;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,OAAO,WAAW,IAAI,GAAG;AAAA,wBACzB,WAAU;AAAA,sBAAA;AAAA,oBAAA;AAAA,kBACZ;AAAA,gBAAA;AAAA,gBAvBG;AAAA,cAAA;AAAA,aA0BR,GACH;AAAA,YAED,aACC,oBAAC,KAAA,EAAE,WAAU,gBAAe,OAAO,YAChC,UAAA,UAAA,CACH;AAAA,UAAA,GAEJ;AAAA,WAGA,IAAI,YAAY,IAAI,aACpB,qBAAC,OAAA,EAAM,OAAM,kCACV,UAAA;AAAA,YAAA,IAAI,YACH,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,KAAA,EAAE,WAAU,oBAAmB,UAAA,UAAM;AAAA,cACtC,oBAAC,KAAA,EAAE,WAAU,aAAa,cAAI,SAAA,CAAS;AAAA,YAAA,GACzC;AAAA,YAED,IAAI,YACH,qBAAA,UAAA,EACE,UAAA;AAAA,cAAA,oBAAC,KAAA,EAAE,WAAU,oBAAmB,UAAA,UAAM;AAAA,cACtC,oBAAC,KAAA,EAAE,WAAU,aAAa,cAAI,SAAA,CAAS;AAAA,YAAA,EAAA,CACzC;AAAA,UAAA,GAEJ;AAAA,UAGF,oBAAC,OAAA,EAAI,WAAU,sBACb,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS,IAAI;AAAA,cACb,SAAS,IAAI;AAAA,YAAA;AAAA,UAAA,EACf,CACF;AAAA,QAAA,EAAA,CACF;AAAA;AAAA,IAAA;AAAA,EAEJ;AAaF,QAAM,OACJ,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,+CACb,UAAA;AAAA,MAAA,oBAAC,WAAQ,KAAU,QAAQ,MAAM,YAAY,IAAI,IAAI,GAAG;AAAA,MACvD,IAAI,cACH;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SACE,IAAI,WAAW,SAAS,gBACpB,cACA;AAAA,UAGL,UAAA,IAAI,WAAW,SAAS,gBACrB,gBACA;AAAA,QAAA;AAAA,MAAA;AAAA,MAOR,oBAAC,SAAI,WAAU,yCACb,8BAAC,6BAAA,EAA6B,GAAG,kBAAkB,EAAA,CACrD;AAAA,IAAA,GACF;AAAA,IAEC,IAAI,aAAa,IAAI,UAAU,SAAS,KACvC,qBAAC,KAAA,EAAE,WAAU,wCAAuC,UAAA;AAAA,MAAA;AAAA,MACnC,IAAI,UAAU,KAAK,IAAI;AAAA,IAAA,GACxC;AAAA,IAGD,iBAAiB,KAChB,oBAAC,KAAA,EAAE,WAAU,kBACX,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,IAAG;AAAA,QACH,QAAQ,EAAE,KAAK,OAAO,IAAI,IAAI,GAAA;AAAA,QAC9B,eAAY;AAAA,QACZ,WAAU;AAAA,QACX,UAAA;AAAA,UAAA;AAAA,UACsB,IAAI;AAAA,UAAY;AAAA,UAAI;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAE7C;AAAA,IAGD,WACC,qBAAC,OAAA,EAAI,WAAU,oDACb,UAAA;AAAA,MAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,iCAAgC,UAAA,SAAK;AAAA,QACrD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI;AAAA,YACX,QAAQ,CAAC,SACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,KAAA,GAAQ;AAAA,YAEjD,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MACA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,iCAAgC,UAAA,QAAI;AAAA,QACpD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO,IAAI,UAAU;AAAA,YACrB,QAAQ,CAAC,WACP,UAAU,OAAO,EAAE,IAAI,IAAI,IAAI,MAAM,EAAE,OAAA,GAAU;AAAA,YAEnD,aAAY;AAAA,YACZ,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,EAAA,CACF;AAAA,IAAA,GACF;AAAA,IAGD,cAAc,GAAG,KAChB,qBAAC,OAAA,EAAI,WAAU,QAGb,UAAA;AAAA,MAAA,oBAAC,KAAA,EAAE,WAAU,wCAAuC,UAAA,+BAEpD;AAAA,MACA,oBAAC,gBAAa,IAAA,CAAU;AAAA,IAAA,GAC1B;AAAA,IAKD,IAAI,eACF,MAAM;AACL,YAAM,gBAAgB,IAAI,WAAW,SAAS;AAC9C,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WACE,gBACI,uFACA;AAAA,UAGN,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WACE,gBACI,8DACA;AAAA,gBAGL,0BACG,sBACA;AAAA,cAAA;AAAA,YAAA;AAAA,gCAEL,KAAA,EAAE,WAAU,iCACV,UAAA,IAAI,WAAW,SAClB;AAAA,YACC,kBACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,yCAAa;AAAA,gBAC5B,WAAU;AAAA,gBACX,UAAA;AAAA,kBAAA;AAAA,kBACoB,eAAe;AAAA,kBAClC,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UACnC;AAAA,QAAA;AAAA,MAAA;AAAA,IAIR,GAAA;AAAA,EAAG,GACP;AAGF,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,wBACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QAGA,eAAc;AAAA,QAGd,WAAW,WAAW,kBAAkB,IAAI,YAAY;AAAA,QACxD,UAAU,CAAC,WACT;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,cAAW;AAAA,YACX,SAAS,MAAM;AACb,qBAAO,IAAI,MAAM;AACjB,0BAAY,CAAC,MAAM,IAAI,CAAC;AAAA,YAC1B;AAAA,YAEA,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,kBAAiB,UAAA,uBAAmB;AAAA,cACpD,oBAAC,MAAA,EAAK,WAAU,oBAAmB,eAAY,OAAA,CAAO;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACxD;AAAA,IAAA,GAGN;AAAA,IAGA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,eAAe,IAAI,iBAAiB,CAAA;AAAA,QACpC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,QACd,OAAO,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAC/C,GACF;AAEJ;"}
@@ -64,7 +64,16 @@ function CommentsFeed({
64
64
  })
65
65
  );
66
66
  const dismiss = useMutation(
67
- trpc.feedback.dismiss.mutationOptions({ onSuccess: () => void refresh() })
67
+ trpc.feedback.dismiss.mutationOptions({
68
+ onSuccess: () => {
69
+ void refresh();
70
+ if ("communityToolSlug" in target) {
71
+ void queryClient.invalidateQueries({
72
+ queryKey: trpc.communityTools.pathKey()
73
+ });
74
+ }
75
+ }
76
+ })
68
77
  );
69
78
  const saveEdit = (id) => {
70
79
  const body = editDraft.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"CommentsFeed.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommentsFeed.tsx"],"sourcesContent":["import type {\n Claim,\n FeedbackView as Note,\n} from '@igstack/app-catalog-backend-core'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport React from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { attachmentUrl } from './communityDraft'\nimport type { CommentTarget } from './communityDraft'\nimport { formatCommunityError } from './communityError'\nimport { composeComments } from './composeComments'\nimport type { CommentEntry } from './composeComments'\n\n/** Same shape the input primitive uses, minus the fixed height. */\nconst FIELD_CLASSES =\n 'placeholder:text-muted-foreground dark:bg-input/30 border-input w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50'\n\nconst BADGES = {\n pending: { text: 'Pending', className: 'is-pending' },\n used: { text: 'Used as source', className: 'is-used' },\n} as const\n\n/**\n * Screenshots sent with a note: a thumbnail strip under the words. The words\n * are the substance and a screenshot is the evidence, so it should be\n * glanceable and openable, not dominate the row.\n */\nfunction AttachmentStrip({ ids }: { ids: string[] }) {\n if (ids.length === 0) return null\n return (\n <ul className=\"community-shots\">\n {ids.map((id) => (\n <li key={id}>\n <a href={attachmentUrl(id)} target=\"_blank\" rel=\"noreferrer\">\n <img\n src={attachmentUrl(id)}\n alt=\"Attached screenshot\"\n loading=\"lazy\"\n />\n </a>\n </li>\n ))}\n </ul>\n )\n}\n\n/** Who, when and where it stands, on one small line above the words. */\nfunction Meta({ entry, edited }: { entry: CommentEntry; edited: boolean }) {\n const badge = entry.badge && BADGES[entry.badge]\n return (\n <p className=\"comment__meta\">\n {entry.who && (\n <>\n <span className=\"comment__who\">{entry.who}</span>\n {' · '}\n </>\n )}\n <time dateTime={entry.at} title={entry.at}>\n {formatRelativeTime(entry.at)}\n </time>\n {edited && ' (edited)'}\n {badge && (\n <span\n className={cn('comment__badge', badge.className)}\n data-badge={entry.badge}\n >\n {badge.text}\n </span>\n )}\n </p>\n )\n}\n\n/**\n * Everything already said about the entry, under the input: one feed, newest\n * first, one entry per visitor note. See `composeComments` for how claims fold\n * into the notes they came from.\n *\n * Nobody logs in, so the server attributes each note to a pseudonym from an\n * httpOnly cookie, and that cookie is what authorises editing (for an hour) or\n * withdrawing (until someone reviews it). `canEditUntil` arrives as an absolute\n * instant so a skewed client clock cannot show a control the server would refuse.\n */\nexport function CommentsFeed({\n target,\n claims,\n labelledBy,\n}: {\n target: CommentTarget\n claims: Claim[]\n /** Id of the heading that names the list. */\n labelledBy: string\n}) {\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n const listOptions = trpc.feedback.list.queryOptions(target)\n const { data, error, isPending } = useQuery(listOptions)\n const [editingId, setEditingId] = React.useState<string | null>(null)\n const [editDraft, setEditDraft] = React.useState('')\n\n const refresh = () =>\n queryClient.invalidateQueries({ queryKey: listOptions.queryKey })\n const edit = useMutation(\n trpc.feedback.edit.mutationOptions({\n onSuccess: () => {\n setEditingId(null)\n void refresh()\n },\n }),\n )\n const dismiss = useMutation(\n trpc.feedback.dismiss.mutationOptions({ onSuccess: () => void refresh() }),\n )\n const saveEdit = (id: string) => {\n const body = editDraft.trim()\n if (body) edit.mutate({ id, body })\n }\n\n const entries =\n data || error ? composeComments(data?.items ?? [], claims) : []\n const failure = error ?? edit.error ?? dismiss.error\n\n const noteBody = (note: Note) => {\n const canEdit =\n note.isMine &&\n !!note.canEditUntil &&\n Date.parse(note.canEditUntil) > Date.now()\n // Withdrawing stays available for as long as nobody has reviewed it.\n const canDismiss = note.isMine && note.status === null\n const editing = editingId === note.id\n return (\n <>\n {editing ? (\n <div className=\"mt-1 space-y-2\">\n <textarea\n autoFocus\n rows={3}\n className={FIELD_CLASSES}\n aria-label=\"Edit your note\"\n value={editDraft}\n onChange={(e) => {\n setEditDraft(e.target.value)\n if (edit.isError) edit.reset()\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n saveEdit(note.id)\n }\n }}\n />\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={!editDraft.trim() || edit.isPending}\n onClick={() => saveEdit(note.id)}\n >\n Save\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setEditingId(null)}\n >\n Cancel\n </Button>\n </div>\n </div>\n ) : (\n <p\n data-feedback-body=\"\"\n className={cn('comment__text', !note.body && 'is-empty')}\n >\n {note.body ??\n (note.attachmentIds.length > 0\n ? 'Screenshot only — no words added.'\n : 'Flagged as out of date — no detail given.')}\n </p>\n )}\n <AttachmentStrip ids={note.attachmentIds} />\n {note.reviewerReply && (\n <p className=\"comment__reply\">↳ {note.reviewerReply}</p>\n )}\n {!editing && (canEdit || canDismiss) && (\n <p className=\"comment__actions\">\n {canEdit && (\n <button\n type=\"button\"\n onClick={() => {\n setEditingId(note.id)\n setEditDraft(note.body ?? '')\n }}\n >\n Edit\n </button>\n )}\n {canDismiss && (\n <button\n type=\"button\"\n className=\"is-destructive\"\n onClick={() => dismiss.mutate({ id: note.id })}\n >\n Dismiss\n </button>\n )}\n </p>\n )}\n </>\n )\n }\n\n return (\n <>\n {isPending ? (\n <div\n className=\"comments-skeleton\"\n aria-busy=\"true\"\n aria-labelledby={labelledBy}\n >\n <span className=\"sr-only\" role=\"status\">\n Loading comments…\n </span>\n <div className=\"comments-skeleton__item\" />\n <div className=\"comments-skeleton__item\" />\n <div className=\"comments-skeleton__item\" />\n </div>\n ) : entries.length > 0 ? (\n <ul className=\"comments\" aria-labelledby={labelledBy}>\n {entries.map((entry) => (\n <li key={entry.key} className=\"comment\">\n <Meta entry={entry} edited={!!entry.note?.editedAt} />\n {entry.note?.isSystem ? (\n <p className=\"comment__text is-system\" data-system-line=\"\">\n {entry.text}\n </p>\n ) : entry.note ? (\n noteBody(entry.note)\n ) : (\n <p className=\"comment__text\">{entry.text}</p>\n )}\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"comments-empty\">\n No comments yet. Be the first to share what you know.\n </p>\n )}\n {failure && (\n <p className=\"know-more-error\">{formatCommunityError(failure)}</p>\n )}\n </>\n )\n}\n"],"names":["React"],"mappings":"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"}
1
+ {"version":3,"file":"CommentsFeed.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommentsFeed.tsx"],"sourcesContent":["import type {\n Claim,\n FeedbackView as Note,\n} from '@igstack/app-catalog-backend-core'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport React from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { attachmentUrl } from './communityDraft'\nimport type { CommentTarget } from './communityDraft'\nimport { formatCommunityError } from './communityError'\nimport { composeComments } from './composeComments'\nimport type { CommentEntry } from './composeComments'\n\n/** Same shape the input primitive uses, minus the fixed height. */\nconst FIELD_CLASSES =\n 'placeholder:text-muted-foreground dark:bg-input/30 border-input w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50'\n\nconst BADGES = {\n pending: { text: 'Pending', className: 'is-pending' },\n used: { text: 'Used as source', className: 'is-used' },\n} as const\n\n/**\n * Screenshots sent with a note: a thumbnail strip under the words. The words\n * are the substance and a screenshot is the evidence, so it should be\n * glanceable and openable, not dominate the row.\n */\nfunction AttachmentStrip({ ids }: { ids: string[] }) {\n if (ids.length === 0) return null\n return (\n <ul className=\"community-shots\">\n {ids.map((id) => (\n <li key={id}>\n <a href={attachmentUrl(id)} target=\"_blank\" rel=\"noreferrer\">\n <img\n src={attachmentUrl(id)}\n alt=\"Attached screenshot\"\n loading=\"lazy\"\n />\n </a>\n </li>\n ))}\n </ul>\n )\n}\n\n/** Who, when and where it stands, on one small line above the words. */\nfunction Meta({ entry, edited }: { entry: CommentEntry; edited: boolean }) {\n const badge = entry.badge && BADGES[entry.badge]\n return (\n <p className=\"comment__meta\">\n {entry.who && (\n <>\n <span className=\"comment__who\">{entry.who}</span>\n {' · '}\n </>\n )}\n <time dateTime={entry.at} title={entry.at}>\n {formatRelativeTime(entry.at)}\n </time>\n {edited && ' (edited)'}\n {badge && (\n <span\n className={cn('comment__badge', badge.className)}\n data-badge={entry.badge}\n >\n {badge.text}\n </span>\n )}\n </p>\n )\n}\n\n/**\n * Everything already said about the entry, under the input: one feed, newest\n * first, one entry per visitor note. See `composeComments` for how claims fold\n * into the notes they came from.\n *\n * Nobody logs in, so the server attributes each note to a pseudonym from an\n * httpOnly cookie, and that cookie is what authorises editing (for an hour) or\n * withdrawing (until someone reviews it). `canEditUntil` arrives as an absolute\n * instant so a skewed client clock cannot show a control the server would refuse.\n */\nexport function CommentsFeed({\n target,\n claims,\n labelledBy,\n}: {\n target: CommentTarget\n claims: Claim[]\n /** Id of the heading that names the list. */\n labelledBy: string\n}) {\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n const listOptions = trpc.feedback.list.queryOptions(target)\n const { data, error, isPending } = useQuery(listOptions)\n const [editingId, setEditingId] = React.useState<string | null>(null)\n const [editDraft, setEditDraft] = React.useState('')\n\n const refresh = () =>\n queryClient.invalidateQueries({ queryKey: listOptions.queryKey })\n const edit = useMutation(\n trpc.feedback.edit.mutationOptions({\n onSuccess: () => {\n setEditingId(null)\n void refresh()\n },\n }),\n )\n const dismiss = useMutation(\n trpc.feedback.dismiss.mutationOptions({\n onSuccess: () => {\n void refresh()\n // A tool's row counts its comments.\n if ('communityToolSlug' in target) {\n void queryClient.invalidateQueries({\n queryKey: trpc.communityTools.pathKey(),\n })\n }\n },\n }),\n )\n const saveEdit = (id: string) => {\n const body = editDraft.trim()\n if (body) edit.mutate({ id, body })\n }\n\n const entries =\n data || error ? composeComments(data?.items ?? [], claims) : []\n const failure = error ?? edit.error ?? dismiss.error\n\n const noteBody = (note: Note) => {\n const canEdit =\n note.isMine &&\n !!note.canEditUntil &&\n Date.parse(note.canEditUntil) > Date.now()\n // Withdrawing stays available for as long as nobody has reviewed it.\n const canDismiss = note.isMine && note.status === null\n const editing = editingId === note.id\n return (\n <>\n {editing ? (\n <div className=\"mt-1 space-y-2\">\n <textarea\n autoFocus\n rows={3}\n className={FIELD_CLASSES}\n aria-label=\"Edit your note\"\n value={editDraft}\n onChange={(e) => {\n setEditDraft(e.target.value)\n if (edit.isError) edit.reset()\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n saveEdit(note.id)\n }\n }}\n />\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={!editDraft.trim() || edit.isPending}\n onClick={() => saveEdit(note.id)}\n >\n Save\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setEditingId(null)}\n >\n Cancel\n </Button>\n </div>\n </div>\n ) : (\n <p\n data-feedback-body=\"\"\n className={cn('comment__text', !note.body && 'is-empty')}\n >\n {note.body ??\n (note.attachmentIds.length > 0\n ? 'Screenshot only — no words added.'\n : 'Flagged as out of date — no detail given.')}\n </p>\n )}\n <AttachmentStrip ids={note.attachmentIds} />\n {note.reviewerReply && (\n <p className=\"comment__reply\">↳ {note.reviewerReply}</p>\n )}\n {!editing && (canEdit || canDismiss) && (\n <p className=\"comment__actions\">\n {canEdit && (\n <button\n type=\"button\"\n onClick={() => {\n setEditingId(note.id)\n setEditDraft(note.body ?? '')\n }}\n >\n Edit\n </button>\n )}\n {canDismiss && (\n <button\n type=\"button\"\n className=\"is-destructive\"\n onClick={() => dismiss.mutate({ id: note.id })}\n >\n Dismiss\n </button>\n )}\n </p>\n )}\n </>\n )\n }\n\n return (\n <>\n {isPending ? (\n <div\n className=\"comments-skeleton\"\n aria-busy=\"true\"\n aria-labelledby={labelledBy}\n >\n <span className=\"sr-only\" role=\"status\">\n Loading comments…\n </span>\n <div className=\"comments-skeleton__item\" />\n <div className=\"comments-skeleton__item\" />\n <div className=\"comments-skeleton__item\" />\n </div>\n ) : entries.length > 0 ? (\n <ul className=\"comments\" aria-labelledby={labelledBy}>\n {entries.map((entry) => (\n <li key={entry.key} className=\"comment\">\n <Meta entry={entry} edited={!!entry.note?.editedAt} />\n {entry.note?.isSystem ? (\n <p className=\"comment__text is-system\" data-system-line=\"\">\n {entry.text}\n </p>\n ) : entry.note ? (\n noteBody(entry.note)\n ) : (\n <p className=\"comment__text\">{entry.text}</p>\n )}\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"comments-empty\">\n No comments yet. Be the first to share what you know.\n </p>\n )}\n {failure && (\n <p className=\"know-more-error\">{formatCommunityError(failure)}</p>\n )}\n </>\n )\n}\n"],"names":["React"],"mappings":"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"}
@@ -103,6 +103,11 @@ function CommunityInput({
103
103
  void queryClient.invalidateQueries({
104
104
  queryKey: trpc.feedback.list.queryKey(target)
105
105
  });
106
+ if ("communityToolSlug" in target) {
107
+ void queryClient.invalidateQueries({
108
+ queryKey: trpc.communityTools.pathKey()
109
+ });
110
+ }
106
111
  }
107
112
  })
108
113
  );
@@ -1 +1 @@
1
- {"version":3,"file":"CommunityInput.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommunityInput.tsx"],"sourcesContent":["import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Paperclip, X } from 'lucide-react'\nimport React from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport {\n attachmentUrl,\n draftSlugOf,\n readAttachments,\n readDraft,\n writeAttachments,\n writeDraft,\n} from './communityDraft'\nimport type { CommentTarget, PendingImage } from './communityDraft'\nimport { formatCommunityError } from './communityError'\n\nconst MAX_HEIGHT = 200\n\n/** How long the Share highlight stays on the field, ms. */\nconst FLASH_MS = 900\n\n/** Mirrors the server's cap, which refuses anything above it regardless. */\nconst MAX_ATTACHMENTS = 3\n\nconst UPLOAD_URL = '/api/feedback-attachments/upload'\n\n/** What the card holds on to, so \"Share what you know\" can point at the field. */\nexport interface CommunityInputHandle {\n /** Focuses the field and highlights it briefly. Re-triggers on every call. */\n focus: () => void\n}\n\nconst isImage = (file: File) => file.type.startsWith('image/')\n\n/**\n * Upload on pick, not on send: the person sees whether their screenshot\n * arrived while they can still do something about it, and Send stays one fast\n * request that cannot half-fail with images in flight.\n */\nasync function upload(files: File[]): Promise<string[] | string> {\n const form = new FormData()\n for (const file of files) form.append('image', file)\n try {\n const response = await fetch(UPLOAD_URL, {\n method: 'POST',\n body: form,\n // The visitor cookie is the identity the upload is recorded against, and\n // it is httpOnly — so it has to ride along explicitly.\n credentials: 'same-origin',\n })\n const payload: unknown = await response.json().catch(() => null)\n const field = (key: string): unknown =>\n typeof payload === 'object' && payload !== null\n ? (payload as Record<string, unknown>)[key]\n : undefined\n const ids = field('ids')\n if (response.ok && Array.isArray(ids)) return ids.map(String)\n const reported = field('error')\n return typeof reported === 'string'\n ? reported\n : 'Could not attach that image'\n } catch {\n return 'Could not attach that image'\n }\n}\n\n/**\n * The card's one ask for more information, at the top of Behind this card:\n * one line with Send beside it, grey until there is something to send. The\n * field grows as the text wraps. Screenshots come in through the\n * paperclip, paste or drop, and a screenshot alone is a valid note.\n */\nexport function CommunityInput({\n target,\n appName,\n ref,\n}: {\n target: CommentTarget\n appName: string\n ref?: React.Ref<CommunityInputHandle>\n}) {\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n const draftSlug = draftSlugOf(target)\n // Seeded from storage so a draft survives the tab panel unmounting under it.\n const [text, setText] = React.useState(() => readDraft(draftSlug))\n const [images, setImages] = React.useState<PendingImage[]>(() =>\n readAttachments(draftSlug),\n )\n const [sent, setSent] = React.useState(false)\n // Counts successful sends, so a click on Send hands focus back every time.\n const [sends, setSends] = React.useState(0)\n const [uploading, setUploading] = React.useState(false)\n const [uploadError, setUploadError] = React.useState<string | null>(null)\n const [dragging, setDragging] = React.useState(false)\n // The guard events read, set synchronously: `uploading` only updates on the\n // next render, so a second paste or drop in the same tick would slip past it.\n const inFlightRef = React.useRef(false)\n const dragDepthRef = React.useRef(0)\n // Cleared draft stays cleared: Esc increments this; the upload checks it\n // on completion so an in-flight upload does not re-add its image.\n const draftGenRef = React.useRef(0)\n const [height, setHeight] = React.useState(40)\n const fieldRef = React.useRef<HTMLTextAreaElement>(null)\n // A hidden twin measures the height the text needs, so the real field can\n // animate to it. Setting `height: auto` on the real one snaps instead.\n const mirrorRef = React.useRef<HTMLTextAreaElement>(null)\n const fileRef = React.useRef<HTMLInputElement>(null)\n const formRef = React.useRef<HTMLFormElement>(null)\n const flashTimerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n\n // Detect coarse pointer (touch) to adjust placeholder and hide keyboard hints.\n const [isCoarse, setIsCoarse] = React.useState(() => {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(pointer: coarse)').matches\n })\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const mq = window.matchMedia('(pointer: coarse)')\n const handler = () => setIsCoarse(mq.matches)\n mq.addEventListener('change', handler)\n return () => mq.removeEventListener('change', handler)\n }, [])\n\n React.useEffect(() => writeDraft(draftSlug, text), [draftSlug, text])\n React.useEffect(\n () => writeAttachments(draftSlug, images),\n [draftSlug, images],\n )\n\n // The class goes on the form, whose className React never rewrites, and\n // is removed and re-added with a reflow between so a second call restarts\n // the highlight instead of merely extending it.\n React.useImperativeHandle(ref, () => ({\n focus: () => {\n fieldRef.current?.focus()\n const form = formRef.current\n if (!form) return\n form.classList.remove('is-flash')\n void form.offsetWidth\n form.classList.add('is-flash')\n clearTimeout(flashTimerRef.current)\n flashTimerRef.current = setTimeout(\n () => form.classList.remove('is-flash'),\n FLASH_MS,\n )\n },\n }))\n React.useEffect(() => () => clearTimeout(flashTimerRef.current), [])\n\n const add = useMutation(\n trpc.feedback.add.mutationOptions({\n onSuccess: () => {\n setText('')\n setImages([])\n setUploadError(null)\n setSent(true)\n setSends((n) => n + 1)\n void queryClient.invalidateQueries({\n queryKey: trpc.feedback.list.queryKey(target),\n })\n },\n }),\n )\n\n // Focus stays in the field so the next Tab doesn't walk the grid behind the scrim.\n React.useEffect(() => {\n if (sends) fieldRef.current?.focus()\n }, [sends])\n\n const typing = text.trim() !== ''\n const canSend = typing || images.length > 0\n\n const fit = React.useCallback(() => {\n const field = fieldRef.current\n const mirror = mirrorRef.current\n if (!field || !mirror) return\n mirror.style.width = `${field.offsetWidth}px`\n mirror.value = text || ' '\n // Compute ONE_LINE from the actual field styles (varies by viewport width).\n const styles = window.getComputedStyle(field)\n const lineHeight = parseFloat(styles.lineHeight)\n const paddingTop = parseFloat(styles.paddingTop)\n const paddingBottom = parseFloat(styles.paddingBottom)\n const borderTop = parseFloat(styles.borderTopWidth)\n const borderBottom = parseFloat(styles.borderBottomWidth)\n const oneLine =\n lineHeight + paddingTop + paddingBottom + borderTop + borderBottom\n // +2: scrollHeight excludes the border, the box height includes it.\n setHeight(Math.min(Math.max(mirror.scrollHeight + 2, oneLine), MAX_HEIGHT))\n }, [text])\n React.useLayoutEffect(fit, [fit])\n React.useEffect(() => {\n const field = fieldRef.current\n if (!field || typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(fit)\n observer.observe(field)\n return () => observer.disconnect()\n }, [fit])\n\n /**\n * One upload at a time: a paste or drop while one is in flight is ignored,\n * and the \"Attaching…\" line says why. The updater clamps as well, so the\n * draft can never hold more than the server accepts on send.\n */\n const attach = async (picked: File[]) => {\n const files = picked.filter(isImage)\n if (files.length === 0 || inFlightRef.current) return\n const room = MAX_ATTACHMENTS - images.length\n if (room <= 0) {\n setUploadError(`Up to ${MAX_ATTACHMENTS} images`)\n return\n }\n setUploadError(null)\n inFlightRef.current = true\n setUploading(true)\n const startGen = draftGenRef.current\n const result = await upload(files.slice(0, room))\n inFlightRef.current = false\n setUploading(false)\n // Draft was cleared (Esc) while upload was in flight: stay cleared.\n if (draftGenRef.current !== startGen) return\n if (typeof result === 'string') {\n setUploadError(result)\n return\n }\n const newImages = result.map((id, i) => ({\n id,\n name: files[i]?.name ?? 'image',\n }))\n // Merge into both state and persisted draft so unmount restores it.\n setImages((current) => {\n const updated = [...current, ...newImages].slice(0, MAX_ATTACHMENTS)\n writeAttachments(draftSlug, updated)\n return updated\n })\n if (files.length > room)\n setUploadError(`Only the first ${room} were attached`)\n }\n\n /** Only a drag carrying files is ours; dragged text still drops into the field. */\n const carriesFiles = (e: React.DragEvent) =>\n [...e.dataTransfer.types].includes('Files')\n\n const submit = () => {\n if (!canSend || add.isPending || uploading) return\n const body = text.trim()\n add.mutate({\n ...target,\n ...(body ? { body } : {}),\n ...(images.length > 0 ? { attachmentIds: images.map((i) => i.id) } : {}),\n })\n }\n\n return (\n <form\n ref={formRef}\n // State rides on data attributes: the className stays static so the\n // imperative `is-flash` below survives a re-render.\n className=\"know-more\"\n data-drop={dragging || undefined}\n onSubmit={(e) => {\n e.preventDefault()\n submit()\n }}\n onDragEnter={(e) => {\n if (!carriesFiles(e)) return\n dragDepthRef.current += 1\n setDragging(true)\n }}\n onDragOver={(e) => {\n // Accepting the drop is what lets it fire.\n if (carriesFiles(e)) e.preventDefault()\n }}\n onDragLeave={(e) => {\n if (!carriesFiles(e)) return\n // Every child the drag crosses fires its own enter and leave, so only\n // the last leave means the drag left the form.\n dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)\n if (dragDepthRef.current === 0) setDragging(false)\n }}\n onDrop={(e) => {\n if (!carriesFiles(e)) return\n e.preventDefault()\n dragDepthRef.current = 0\n setDragging(false)\n void attach([...e.dataTransfer.files])\n }}\n >\n {images.length > 0 && (\n <ul className=\"know-more-thumbs\">\n {images.map((image) => (\n <li key={image.id} className=\"know-more-thumb\">\n <img src={attachmentUrl(image.id)} alt={image.name} />\n <button\n type=\"button\"\n aria-label={`Remove ${image.name}`}\n onClick={() =>\n setImages((current) =>\n current.filter((i) => i.id !== image.id),\n )\n }\n >\n <X className=\"size-2.5\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n <div className=\"know-more-field\">\n <button\n type=\"button\"\n className=\"know-more-clip\"\n aria-label=\"Attach a screenshot\"\n title=\"Attach a screenshot\"\n aria-busy={uploading || undefined}\n disabled={uploading || images.length >= MAX_ATTACHMENTS}\n onClick={() => fileRef.current?.click()}\n >\n <Paperclip className=\"size-4\" />\n </button>\n <input\n ref={fileRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n onChange={(e) => {\n void attach([...(e.target.files ?? [])])\n // Cleared so picking the SAME file again still fires a change.\n e.target.value = ''\n }}\n />\n <textarea\n ref={fieldRef}\n rows={1}\n value={text}\n aria-label={`Know more about ${appName}?`}\n placeholder={\n isCoarse\n ? 'Add a fact, a fix or a question'\n : 'Add a fact, a fix or a question — press Enter'\n }\n style={{\n height,\n overflowY: height >= MAX_HEIGHT ? 'auto' : 'hidden',\n }}\n onChange={(e) => {\n setText(e.target.value)\n setSent(false)\n if (add.isError) add.reset()\n }}\n onPaste={(e) => {\n const files = [...e.clipboardData.files].filter(isImage)\n if (files.length === 0) return\n e.preventDefault()\n void attach(files)\n }}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing\n ) {\n e.preventDefault()\n submit()\n }\n // Esc clears a draft, and only a draft: with nothing typed or\n // attached it must reach the card's layer so the card still closes.\n if (\n e.key === 'Escape' &&\n !e.nativeEvent.isComposing &&\n (text !== '' || images.length > 0)\n ) {\n e.preventDefault()\n e.stopPropagation()\n draftGenRef.current += 1\n setText('')\n setImages([])\n }\n }}\n />\n <Button\n type=\"submit\"\n size=\"sm\"\n className=\"know-more-send\"\n disabled={!canSend || add.isPending || uploading}\n >\n Send\n </Button>\n <textarea\n ref={mirrorRef}\n rows={1}\n readOnly\n tabIndex={-1}\n aria-hidden\n className=\"know-more-mirror\"\n />\n </div>\n {!isCoarse && (\n <p className={cn('know-more-hint', typing && 'is-on')}>\n Enter sends · Shift+Enter new line · Esc clears · paste or drop a\n screenshot\n </p>\n )}\n <p className=\"know-more-attaching\" aria-live=\"polite\">\n {uploading ? 'Attaching…' : ''}\n </p>\n {(add.isError || uploadError) && (\n <p role=\"alert\" className=\"know-more-error\">\n {add.isError\n ? `Could not send: ${formatCommunityError(add.error)}. Your note is still here.`\n : uploadError}\n </p>\n )}\n <p role=\"status\" className={cn('know-more-sent', sent && 'is-on')}>\n {sent ? 'Thanks — sent. It shows below.' : ''}\n </p>\n </form>\n )\n}\n"],"names":["React"],"mappings":"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"}
1
+ {"version":3,"file":"CommunityInput.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommunityInput.tsx"],"sourcesContent":["import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Paperclip, X } from 'lucide-react'\nimport React from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport {\n attachmentUrl,\n draftSlugOf,\n readAttachments,\n readDraft,\n writeAttachments,\n writeDraft,\n} from './communityDraft'\nimport type { CommentTarget, PendingImage } from './communityDraft'\nimport { formatCommunityError } from './communityError'\n\nconst MAX_HEIGHT = 200\n\n/** How long the Share highlight stays on the field, ms. */\nconst FLASH_MS = 900\n\n/** Mirrors the server's cap, which refuses anything above it regardless. */\nconst MAX_ATTACHMENTS = 3\n\nconst UPLOAD_URL = '/api/feedback-attachments/upload'\n\n/** What the card holds on to, so \"Share what you know\" can point at the field. */\nexport interface CommunityInputHandle {\n /** Focuses the field and highlights it briefly. Re-triggers on every call. */\n focus: () => void\n}\n\nconst isImage = (file: File) => file.type.startsWith('image/')\n\n/**\n * Upload on pick, not on send: the person sees whether their screenshot\n * arrived while they can still do something about it, and Send stays one fast\n * request that cannot half-fail with images in flight.\n */\nasync function upload(files: File[]): Promise<string[] | string> {\n const form = new FormData()\n for (const file of files) form.append('image', file)\n try {\n const response = await fetch(UPLOAD_URL, {\n method: 'POST',\n body: form,\n // The visitor cookie is the identity the upload is recorded against, and\n // it is httpOnly — so it has to ride along explicitly.\n credentials: 'same-origin',\n })\n const payload: unknown = await response.json().catch(() => null)\n const field = (key: string): unknown =>\n typeof payload === 'object' && payload !== null\n ? (payload as Record<string, unknown>)[key]\n : undefined\n const ids = field('ids')\n if (response.ok && Array.isArray(ids)) return ids.map(String)\n const reported = field('error')\n return typeof reported === 'string'\n ? reported\n : 'Could not attach that image'\n } catch {\n return 'Could not attach that image'\n }\n}\n\n/**\n * The card's one ask for more information, at the top of Behind this card:\n * one line with Send beside it, grey until there is something to send. The\n * field grows as the text wraps. Screenshots come in through the\n * paperclip, paste or drop, and a screenshot alone is a valid note.\n */\nexport function CommunityInput({\n target,\n appName,\n ref,\n}: {\n target: CommentTarget\n appName: string\n ref?: React.Ref<CommunityInputHandle>\n}) {\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n const draftSlug = draftSlugOf(target)\n // Seeded from storage so a draft survives the tab panel unmounting under it.\n const [text, setText] = React.useState(() => readDraft(draftSlug))\n const [images, setImages] = React.useState<PendingImage[]>(() =>\n readAttachments(draftSlug),\n )\n const [sent, setSent] = React.useState(false)\n // Counts successful sends, so a click on Send hands focus back every time.\n const [sends, setSends] = React.useState(0)\n const [uploading, setUploading] = React.useState(false)\n const [uploadError, setUploadError] = React.useState<string | null>(null)\n const [dragging, setDragging] = React.useState(false)\n // The guard events read, set synchronously: `uploading` only updates on the\n // next render, so a second paste or drop in the same tick would slip past it.\n const inFlightRef = React.useRef(false)\n const dragDepthRef = React.useRef(0)\n // Cleared draft stays cleared: Esc increments this; the upload checks it\n // on completion so an in-flight upload does not re-add its image.\n const draftGenRef = React.useRef(0)\n const [height, setHeight] = React.useState(40)\n const fieldRef = React.useRef<HTMLTextAreaElement>(null)\n // A hidden twin measures the height the text needs, so the real field can\n // animate to it. Setting `height: auto` on the real one snaps instead.\n const mirrorRef = React.useRef<HTMLTextAreaElement>(null)\n const fileRef = React.useRef<HTMLInputElement>(null)\n const formRef = React.useRef<HTMLFormElement>(null)\n const flashTimerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n\n // Detect coarse pointer (touch) to adjust placeholder and hide keyboard hints.\n const [isCoarse, setIsCoarse] = React.useState(() => {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(pointer: coarse)').matches\n })\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const mq = window.matchMedia('(pointer: coarse)')\n const handler = () => setIsCoarse(mq.matches)\n mq.addEventListener('change', handler)\n return () => mq.removeEventListener('change', handler)\n }, [])\n\n React.useEffect(() => writeDraft(draftSlug, text), [draftSlug, text])\n React.useEffect(\n () => writeAttachments(draftSlug, images),\n [draftSlug, images],\n )\n\n // The class goes on the form, whose className React never rewrites, and\n // is removed and re-added with a reflow between so a second call restarts\n // the highlight instead of merely extending it.\n React.useImperativeHandle(ref, () => ({\n focus: () => {\n fieldRef.current?.focus()\n const form = formRef.current\n if (!form) return\n form.classList.remove('is-flash')\n void form.offsetWidth\n form.classList.add('is-flash')\n clearTimeout(flashTimerRef.current)\n flashTimerRef.current = setTimeout(\n () => form.classList.remove('is-flash'),\n FLASH_MS,\n )\n },\n }))\n React.useEffect(() => () => clearTimeout(flashTimerRef.current), [])\n\n const add = useMutation(\n trpc.feedback.add.mutationOptions({\n onSuccess: () => {\n setText('')\n setImages([])\n setUploadError(null)\n setSent(true)\n setSends((n) => n + 1)\n void queryClient.invalidateQueries({\n queryKey: trpc.feedback.list.queryKey(target),\n })\n // A tool's row counts its comments.\n if ('communityToolSlug' in target) {\n void queryClient.invalidateQueries({\n queryKey: trpc.communityTools.pathKey(),\n })\n }\n },\n }),\n )\n\n // Focus stays in the field so the next Tab doesn't walk the grid behind the scrim.\n React.useEffect(() => {\n if (sends) fieldRef.current?.focus()\n }, [sends])\n\n const typing = text.trim() !== ''\n const canSend = typing || images.length > 0\n\n const fit = React.useCallback(() => {\n const field = fieldRef.current\n const mirror = mirrorRef.current\n if (!field || !mirror) return\n mirror.style.width = `${field.offsetWidth}px`\n mirror.value = text || ' '\n // Compute ONE_LINE from the actual field styles (varies by viewport width).\n const styles = window.getComputedStyle(field)\n const lineHeight = parseFloat(styles.lineHeight)\n const paddingTop = parseFloat(styles.paddingTop)\n const paddingBottom = parseFloat(styles.paddingBottom)\n const borderTop = parseFloat(styles.borderTopWidth)\n const borderBottom = parseFloat(styles.borderBottomWidth)\n const oneLine =\n lineHeight + paddingTop + paddingBottom + borderTop + borderBottom\n // +2: scrollHeight excludes the border, the box height includes it.\n setHeight(Math.min(Math.max(mirror.scrollHeight + 2, oneLine), MAX_HEIGHT))\n }, [text])\n React.useLayoutEffect(fit, [fit])\n React.useEffect(() => {\n const field = fieldRef.current\n if (!field || typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(fit)\n observer.observe(field)\n return () => observer.disconnect()\n }, [fit])\n\n /**\n * One upload at a time: a paste or drop while one is in flight is ignored,\n * and the \"Attaching…\" line says why. The updater clamps as well, so the\n * draft can never hold more than the server accepts on send.\n */\n const attach = async (picked: File[]) => {\n const files = picked.filter(isImage)\n if (files.length === 0 || inFlightRef.current) return\n const room = MAX_ATTACHMENTS - images.length\n if (room <= 0) {\n setUploadError(`Up to ${MAX_ATTACHMENTS} images`)\n return\n }\n setUploadError(null)\n inFlightRef.current = true\n setUploading(true)\n const startGen = draftGenRef.current\n const result = await upload(files.slice(0, room))\n inFlightRef.current = false\n setUploading(false)\n // Draft was cleared (Esc) while upload was in flight: stay cleared.\n if (draftGenRef.current !== startGen) return\n if (typeof result === 'string') {\n setUploadError(result)\n return\n }\n const newImages = result.map((id, i) => ({\n id,\n name: files[i]?.name ?? 'image',\n }))\n // Merge into both state and persisted draft so unmount restores it.\n setImages((current) => {\n const updated = [...current, ...newImages].slice(0, MAX_ATTACHMENTS)\n writeAttachments(draftSlug, updated)\n return updated\n })\n if (files.length > room)\n setUploadError(`Only the first ${room} were attached`)\n }\n\n /** Only a drag carrying files is ours; dragged text still drops into the field. */\n const carriesFiles = (e: React.DragEvent) =>\n [...e.dataTransfer.types].includes('Files')\n\n const submit = () => {\n if (!canSend || add.isPending || uploading) return\n const body = text.trim()\n add.mutate({\n ...target,\n ...(body ? { body } : {}),\n ...(images.length > 0 ? { attachmentIds: images.map((i) => i.id) } : {}),\n })\n }\n\n return (\n <form\n ref={formRef}\n // State rides on data attributes: the className stays static so the\n // imperative `is-flash` below survives a re-render.\n className=\"know-more\"\n data-drop={dragging || undefined}\n onSubmit={(e) => {\n e.preventDefault()\n submit()\n }}\n onDragEnter={(e) => {\n if (!carriesFiles(e)) return\n dragDepthRef.current += 1\n setDragging(true)\n }}\n onDragOver={(e) => {\n // Accepting the drop is what lets it fire.\n if (carriesFiles(e)) e.preventDefault()\n }}\n onDragLeave={(e) => {\n if (!carriesFiles(e)) return\n // Every child the drag crosses fires its own enter and leave, so only\n // the last leave means the drag left the form.\n dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)\n if (dragDepthRef.current === 0) setDragging(false)\n }}\n onDrop={(e) => {\n if (!carriesFiles(e)) return\n e.preventDefault()\n dragDepthRef.current = 0\n setDragging(false)\n void attach([...e.dataTransfer.files])\n }}\n >\n {images.length > 0 && (\n <ul className=\"know-more-thumbs\">\n {images.map((image) => (\n <li key={image.id} className=\"know-more-thumb\">\n <img src={attachmentUrl(image.id)} alt={image.name} />\n <button\n type=\"button\"\n aria-label={`Remove ${image.name}`}\n onClick={() =>\n setImages((current) =>\n current.filter((i) => i.id !== image.id),\n )\n }\n >\n <X className=\"size-2.5\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n <div className=\"know-more-field\">\n <button\n type=\"button\"\n className=\"know-more-clip\"\n aria-label=\"Attach a screenshot\"\n title=\"Attach a screenshot\"\n aria-busy={uploading || undefined}\n disabled={uploading || images.length >= MAX_ATTACHMENTS}\n onClick={() => fileRef.current?.click()}\n >\n <Paperclip className=\"size-4\" />\n </button>\n <input\n ref={fileRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n onChange={(e) => {\n void attach([...(e.target.files ?? [])])\n // Cleared so picking the SAME file again still fires a change.\n e.target.value = ''\n }}\n />\n <textarea\n ref={fieldRef}\n rows={1}\n value={text}\n aria-label={`Know more about ${appName}?`}\n placeholder={\n isCoarse\n ? 'Add a fact, a fix or a question'\n : 'Add a fact, a fix or a question — press Enter'\n }\n style={{\n height,\n overflowY: height >= MAX_HEIGHT ? 'auto' : 'hidden',\n }}\n onChange={(e) => {\n setText(e.target.value)\n setSent(false)\n if (add.isError) add.reset()\n }}\n onPaste={(e) => {\n const files = [...e.clipboardData.files].filter(isImage)\n if (files.length === 0) return\n e.preventDefault()\n void attach(files)\n }}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing\n ) {\n e.preventDefault()\n submit()\n }\n // Esc clears a draft, and only a draft: with nothing typed or\n // attached it must reach the card's layer so the card still closes.\n if (\n e.key === 'Escape' &&\n !e.nativeEvent.isComposing &&\n (text !== '' || images.length > 0)\n ) {\n e.preventDefault()\n e.stopPropagation()\n draftGenRef.current += 1\n setText('')\n setImages([])\n }\n }}\n />\n <Button\n type=\"submit\"\n size=\"sm\"\n className=\"know-more-send\"\n disabled={!canSend || add.isPending || uploading}\n >\n Send\n </Button>\n <textarea\n ref={mirrorRef}\n rows={1}\n readOnly\n tabIndex={-1}\n aria-hidden\n className=\"know-more-mirror\"\n />\n </div>\n {!isCoarse && (\n <p className={cn('know-more-hint', typing && 'is-on')}>\n Enter sends · Shift+Enter new line · Esc clears · paste or drop a\n screenshot\n </p>\n )}\n <p className=\"know-more-attaching\" aria-live=\"polite\">\n {uploading ? 'Attaching…' : ''}\n </p>\n {(add.isError || uploadError) && (\n <p role=\"alert\" className=\"know-more-error\">\n {add.isError\n ? `Could not send: ${formatCommunityError(add.error)}. Your note is still here.`\n : uploadError}\n </p>\n )}\n <p role=\"status\" className={cn('know-more-sent', sent && 'is-on')}>\n {sent ? 'Thanks — sent. It shows below.' : ''}\n </p>\n </form>\n )\n}\n"],"names":["React"],"mappings":";;;;;;;;;AAiBA,MAAM,aAAa;AAGnB,MAAM,WAAW;AAGjB,MAAM,kBAAkB;AAExB,MAAM,aAAa;AAQnB,MAAM,UAAU,CAAC,SAAe,KAAK,KAAK,WAAW,QAAQ;AAO7D,eAAe,OAAO,OAA2C;AAC/D,QAAM,OAAO,IAAI,SAAA;AACjB,aAAW,QAAQ,MAAO,MAAK,OAAO,SAAS,IAAI;AACnD,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,YAAY;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA;AAAA;AAAA,MAGN,aAAa;AAAA,IAAA,CACd;AACD,UAAM,UAAmB,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI;AAC/D,UAAM,QAAQ,CAAC,QACb,OAAO,YAAY,YAAY,YAAY,OACtC,QAAoC,GAAG,IACxC;AACN,UAAM,MAAM,MAAM,KAAK;AACvB,QAAI,SAAS,MAAM,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI,IAAI,MAAM;AAC5D,UAAM,WAAW,MAAM,OAAO;AAC9B,WAAO,OAAO,aAAa,WACvB,WACA;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,QAAA;AACb,QAAM,cAAc,eAAA;AACpB,QAAM,YAAY,YAAY,MAAM;AAEpC,QAAM,CAAC,MAAM,OAAO,IAAIA,eAAM,SAAS,MAAM,UAAU,SAAS,CAAC;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAIA,eAAM;AAAA,IAAyB,MACzD,gBAAgB,SAAS;AAAA,EAAA;AAE3B,QAAM,CAAC,MAAM,OAAO,IAAIA,eAAM,SAAS,KAAK;AAE5C,QAAM,CAAC,OAAO,QAAQ,IAAIA,eAAM,SAAS,CAAC;AAC1C,QAAM,CAAC,WAAW,YAAY,IAAIA,eAAM,SAAS,KAAK;AACtD,QAAM,CAAC,aAAa,cAAc,IAAIA,eAAM,SAAwB,IAAI;AACxE,QAAM,CAAC,UAAU,WAAW,IAAIA,eAAM,SAAS,KAAK;AAGpD,QAAM,cAAcA,eAAM,OAAO,KAAK;AACtC,QAAM,eAAeA,eAAM,OAAO,CAAC;AAGnC,QAAM,cAAcA,eAAM,OAAO,CAAC;AAClC,QAAM,CAAC,QAAQ,SAAS,IAAIA,eAAM,SAAS,EAAE;AAC7C,QAAM,WAAWA,eAAM,OAA4B,IAAI;AAGvD,QAAM,YAAYA,eAAM,OAA4B,IAAI;AACxD,QAAM,UAAUA,eAAM,OAAyB,IAAI;AACnD,QAAM,UAAUA,eAAM,OAAwB,IAAI;AAClD,QAAM,gBAAgBA,eAAM,OAAsC,MAAS;AAG3E,QAAM,CAAC,UAAU,WAAW,IAAIA,eAAM,SAAS,MAAM;AACnD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,WAAO,OAAO,WAAW,mBAAmB,EAAE;AAAA,EAChD,CAAC;AACDA,iBAAM,UAAU,MAAM;AACpB,QAAI,OAAO,WAAW,YAAa;AACnC,UAAM,KAAK,OAAO,WAAW,mBAAmB;AAChD,UAAM,UAAU,MAAM,YAAY,GAAG,OAAO;AAC5C,OAAG,iBAAiB,UAAU,OAAO;AACrC,WAAO,MAAM,GAAG,oBAAoB,UAAU,OAAO;AAAA,EACvD,GAAG,CAAA,CAAE;AAELA,iBAAM,UAAU,MAAM,WAAW,WAAW,IAAI,GAAG,CAAC,WAAW,IAAI,CAAC;AACpEA,iBAAM;AAAA,IACJ,MAAM,iBAAiB,WAAW,MAAM;AAAA,IACxC,CAAC,WAAW,MAAM;AAAA,EAAA;AAMpBA,iBAAM,oBAAoB,KAAK,OAAO;AAAA,IACpC,OAAO,MAAM;;AACX,qBAAS,YAAT,mBAAkB;AAClB,YAAM,OAAO,QAAQ;AACrB,UAAI,CAAC,KAAM;AACX,WAAK,UAAU,OAAO,UAAU;AAChC,WAAK,KAAK;AACV,WAAK,UAAU,IAAI,UAAU;AAC7B,mBAAa,cAAc,OAAO;AAClC,oBAAc,UAAU;AAAA,QACtB,MAAM,KAAK,UAAU,OAAO,UAAU;AAAA,QACtC;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA,EACA;AACFA,iBAAM,UAAU,MAAM,MAAM,aAAa,cAAc,OAAO,GAAG,EAAE;AAEnE,QAAM,MAAM;AAAA,IACV,KAAK,SAAS,IAAI,gBAAgB;AAAA,MAChC,WAAW,MAAM;AACf,gBAAQ,EAAE;AACV,kBAAU,CAAA,CAAE;AACZ,uBAAe,IAAI;AACnB,gBAAQ,IAAI;AACZ,iBAAS,CAAC,MAAM,IAAI,CAAC;AACrB,aAAK,YAAY,kBAAkB;AAAA,UACjC,UAAU,KAAK,SAAS,KAAK,SAAS,MAAM;AAAA,QAAA,CAC7C;AAED,YAAI,uBAAuB,QAAQ;AACjC,eAAK,YAAY,kBAAkB;AAAA,YACjC,UAAU,KAAK,eAAe,QAAA;AAAA,UAAQ,CACvC;AAAA,QACH;AAAA,MACF;AAAA,IAAA,CACD;AAAA,EAAA;AAIHA,iBAAM,UAAU,MAAM;;AACpB,QAAI,MAAO,gBAAS,YAAT,mBAAkB;AAAA,EAC/B,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,SAAS,KAAK,KAAA,MAAW;AAC/B,QAAM,UAAU,UAAU,OAAO,SAAS;AAE1C,QAAM,MAAMA,eAAM,YAAY,MAAM;AAClC,UAAM,QAAQ,SAAS;AACvB,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,SAAS,CAAC,OAAQ;AACvB,WAAO,MAAM,QAAQ,GAAG,MAAM,WAAW;AACzC,WAAO,QAAQ,QAAQ;AAEvB,UAAM,SAAS,OAAO,iBAAiB,KAAK;AAC5C,UAAM,aAAa,WAAW,OAAO,UAAU;AAC/C,UAAM,aAAa,WAAW,OAAO,UAAU;AAC/C,UAAM,gBAAgB,WAAW,OAAO,aAAa;AACrD,UAAM,YAAY,WAAW,OAAO,cAAc;AAClD,UAAM,eAAe,WAAW,OAAO,iBAAiB;AACxD,UAAM,UACJ,aAAa,aAAa,gBAAgB,YAAY;AAExD,cAAU,KAAK,IAAI,KAAK,IAAI,OAAO,eAAe,GAAG,OAAO,GAAG,UAAU,CAAC;AAAA,EAC5E,GAAG,CAAC,IAAI,CAAC;AACTA,iBAAM,gBAAgB,KAAK,CAAC,GAAG,CAAC;AAChCA,iBAAM,UAAU,MAAM;AACpB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,OAAO,mBAAmB,YAAa;AACrD,UAAM,WAAW,IAAI,eAAe,GAAG;AACvC,aAAS,QAAQ,KAAK;AACtB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,GAAG,CAAC;AAOR,QAAM,SAAS,OAAO,WAAmB;AACvC,UAAM,QAAQ,OAAO,OAAO,OAAO;AACnC,QAAI,MAAM,WAAW,KAAK,YAAY,QAAS;AAC/C,UAAM,OAAO,kBAAkB,OAAO;AACtC,QAAI,QAAQ,GAAG;AACb,qBAAe,SAAS,eAAe,SAAS;AAChD;AAAA,IACF;AACA,mBAAe,IAAI;AACnB,gBAAY,UAAU;AACtB,iBAAa,IAAI;AACjB,UAAM,WAAW,YAAY;AAC7B,UAAM,SAAS,MAAM,OAAO,MAAM,MAAM,GAAG,IAAI,CAAC;AAChD,gBAAY,UAAU;AACtB,iBAAa,KAAK;AAElB,QAAI,YAAY,YAAY,SAAU;AACtC,QAAI,OAAO,WAAW,UAAU;AAC9B,qBAAe,MAAM;AACrB;AAAA,IACF;AACA,UAAM,YAAY,OAAO,IAAI,CAAC,IAAI,MAAA;;AAAO;AAAA,QACvC;AAAA,QACA,QAAM,WAAM,CAAC,MAAP,mBAAU,SAAQ;AAAA,MAAA;AAAA,KACxB;AAEF,cAAU,CAAC,YAAY;AACrB,YAAM,UAAU,CAAC,GAAG,SAAS,GAAG,SAAS,EAAE,MAAM,GAAG,eAAe;AACnE,uBAAiB,WAAW,OAAO;AACnC,aAAO;AAAA,IACT,CAAC;AACD,QAAI,MAAM,SAAS;AACjB,qBAAe,kBAAkB,IAAI,gBAAgB;AAAA,EACzD;AAGA,QAAM,eAAe,CAAC,MACpB,CAAC,GAAG,EAAE,aAAa,KAAK,EAAE,SAAS,OAAO;AAE5C,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,WAAW,IAAI,aAAa,UAAW;AAC5C,UAAM,OAAO,KAAK,KAAA;AAClB,QAAI,OAAO;AAAA,MACT,GAAG;AAAA,MACH,GAAI,OAAO,EAAE,KAAA,IAAS,CAAA;AAAA,MACtB,GAAI,OAAO,SAAS,IAAI,EAAE,eAAe,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE,EAAA,IAAM,CAAA;AAAA,IAAC,CACvE;AAAA,EACH;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MAGL,WAAU;AAAA,MACV,aAAW,YAAY;AAAA,MACvB,UAAU,CAAC,MAAM;AACf,UAAE,eAAA;AACF,eAAA;AAAA,MACF;AAAA,MACA,aAAa,CAAC,MAAM;AAClB,YAAI,CAAC,aAAa,CAAC,EAAG;AACtB,qBAAa,WAAW;AACxB,oBAAY,IAAI;AAAA,MAClB;AAAA,MACA,YAAY,CAAC,MAAM;AAEjB,YAAI,aAAa,CAAC,EAAG,GAAE,eAAA;AAAA,MACzB;AAAA,MACA,aAAa,CAAC,MAAM;AAClB,YAAI,CAAC,aAAa,CAAC,EAAG;AAGtB,qBAAa,UAAU,KAAK,IAAI,GAAG,aAAa,UAAU,CAAC;AAC3D,YAAI,aAAa,YAAY,EAAG,aAAY,KAAK;AAAA,MACnD;AAAA,MACA,QAAQ,CAAC,MAAM;AACb,YAAI,CAAC,aAAa,CAAC,EAAG;AACtB,UAAE,eAAA;AACF,qBAAa,UAAU;AACvB,oBAAY,KAAK;AACjB,aAAK,OAAO,CAAC,GAAG,EAAE,aAAa,KAAK,CAAC;AAAA,MACvC;AAAA,MAEC,UAAA;AAAA,QAAA,OAAO,SAAS,KACf,oBAAC,MAAA,EAAG,WAAU,oBACX,UAAA,OAAO,IAAI,CAAC,UACX,qBAAC,MAAA,EAAkB,WAAU,mBAC3B,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,KAAK,cAAc,MAAM,EAAE,GAAG,KAAK,MAAM,MAAM;AAAA,UACpD;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAY,UAAU,MAAM,IAAI;AAAA,cAChC,SAAS,MACP;AAAA,gBAAU,CAAC,YACT,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,MAAM,EAAE;AAAA,cAAA;AAAA,cAI3C,UAAA,oBAAC,GAAA,EAAE,WAAU,WAAA,CAAW;AAAA,YAAA;AAAA,UAAA;AAAA,QAC1B,EAAA,GAZO,MAAM,EAaf,CACD,GACH;AAAA,QAEF,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAW;AAAA,cACX,OAAM;AAAA,cACN,aAAW,aAAa;AAAA,cACxB,UAAU,aAAa,OAAO,UAAU;AAAA,cACxC,SAAS,MAAA;;AAAM,qCAAQ,YAAR,mBAAiB;AAAA;AAAA,cAEhC,UAAA,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAEhC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,MAAK;AAAA,cACL,QAAO;AAAA,cACP,UAAQ;AAAA,cACR,UAAU;AAAA,cACV,eAAW;AAAA,cACX,WAAU;AAAA,cACV,UAAU,CAAC,MAAM;AACf,qBAAK,OAAO,CAAC,GAAI,EAAE,OAAO,SAAS,CAAA,CAAG,CAAC;AAEvC,kBAAE,OAAO,QAAQ;AAAA,cACnB;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,MAAM;AAAA,cACN,OAAO;AAAA,cACP,cAAY,mBAAmB,OAAO;AAAA,cACtC,aACE,WACI,oCACA;AAAA,cAEN,OAAO;AAAA,gBACL;AAAA,gBACA,WAAW,UAAU,aAAa,SAAS;AAAA,cAAA;AAAA,cAE7C,UAAU,CAAC,MAAM;AACf,wBAAQ,EAAE,OAAO,KAAK;AACtB,wBAAQ,KAAK;AACb,oBAAI,IAAI,QAAS,KAAI,MAAA;AAAA,cACvB;AAAA,cACA,SAAS,CAAC,MAAM;AACd,sBAAM,QAAQ,CAAC,GAAG,EAAE,cAAc,KAAK,EAAE,OAAO,OAAO;AACvD,oBAAI,MAAM,WAAW,EAAG;AACxB,kBAAE,eAAA;AACF,qBAAK,OAAO,KAAK;AAAA,cACnB;AAAA,cACA,WAAW,CAAC,MAAM;AAChB,oBACE,EAAE,QAAQ,WACV,CAAC,EAAE,YACH,CAAC,EAAE,YAAY,aACf;AACA,oBAAE,eAAA;AACF,yBAAA;AAAA,gBACF;AAGA,oBACE,EAAE,QAAQ,YACV,CAAC,EAAE,YAAY,gBACd,SAAS,MAAM,OAAO,SAAS,IAChC;AACA,oBAAE,eAAA;AACF,oBAAE,gBAAA;AACF,8BAAY,WAAW;AACvB,0BAAQ,EAAE;AACV,4BAAU,CAAA,CAAE;AAAA,gBACd;AAAA,cACF;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,WAAU;AAAA,cACV,UAAU,CAAC,WAAW,IAAI,aAAa;AAAA,cACxC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGD;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,MAAM;AAAA,cACN,UAAQ;AAAA,cACR,UAAU;AAAA,cACV,eAAW;AAAA,cACX,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ,GACF;AAAA,QACC,CAAC,YACA,oBAAC,KAAA,EAAE,WAAW,GAAG,kBAAkB,UAAU,OAAO,GAAG,UAAA,+EAAA,CAGvD;AAAA,QAEF,oBAAC,OAAE,WAAU,uBAAsB,aAAU,UAC1C,UAAA,YAAY,eAAe,GAAA,CAC9B;AAAA,SACE,IAAI,WAAW,gBACf,oBAAC,KAAA,EAAE,MAAK,SAAQ,WAAU,mBACvB,UAAA,IAAI,UACD,mBAAmB,qBAAqB,IAAI,KAAK,CAAC,+BAClD,aACN;AAAA,QAEF,oBAAC,KAAA,EAAE,MAAK,UAAS,WAAW,GAAG,kBAAkB,QAAQ,OAAO,GAC7D,UAAA,OAAO,mCAAmC,GAAA,CAC7C;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -13,7 +13,7 @@ export interface FeedRowProps {
13
13
  tool: CommunityTool;
14
14
  /** Shown in the ranked feed; search results have no rank. */
15
15
  rank?: number;
16
- /** A date sort shows its date on the row. */
16
+ /** Recently updated shows when the site changed; every other sort, when it was added. */
17
17
  sort: FeedSort;
18
18
  tokens: string[];
19
19
  /** Set while searching: which fields the query hit. */
@@ -36,7 +36,6 @@ export declare const ROW_THUMB = "h-[150px] w-[240px] rounded-[10px]";
36
36
  * One tool in the feed or the search results. Everything left of the end
37
37
  * panel (rank, picture, name, summary, meta) is one link that opens the site
38
38
  * in a new tab; the 120 px panel opens the card. The star sits in the meta
39
- * line as its own target, above that link. The bottom strip shows the address
40
- * the big click goes to, on hover or focus only.
39
+ * line as its own target, above that link.
41
40
  */
42
41
  export declare function FeedRow({ tool, rank, sort, tokens, matched, selected, optionId, pinned, pinBusy, onTogglePin, onToggleLike, }: FeedRowProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { Link } from "@tanstack/react-router";
3
- import { CornerDownLeft, Eye, ExternalLink, PanelRight, Globe } from "lucide-react";
3
+ import { CornerDownLeft, Eye, MessageSquare, PanelRight, Globe } from "lucide-react";
4
4
  import { cn } from "../../../lib/utils.js";
5
5
  import { highlightTokens } from "../../../util/highlightMatches.js";
6
6
  import { timeAgo } from "../timeAgo.js";
@@ -84,8 +84,7 @@ function FeedRow({
84
84
  const archived = tool.lifecycle.state === "archived";
85
85
  const cell = optionId ? "gridcell" : void 0;
86
86
  const owners = tool.owners.map((o) => o.name).join(", ");
87
- const dated = sort === "newest" ? `Added ${timeAgo(tool.createdAt)}` : sort === "updated" && tool.siteUpdatedAt ? `Updated ${timeAgo(tool.siteUpdatedAt)}` : null;
88
- const address = tool.url.replace(/^https?:\/\//, "").replace(/\/$/, "");
87
+ const dated = sort !== "updated" ? `Added ${timeAgo(tool.createdAt)}` : tool.siteUpdatedAt ? `Updated ${timeAgo(tool.siteUpdatedAt)}` : null;
89
88
  return /* @__PURE__ */ jsxs(
90
89
  "div",
91
90
  {
@@ -191,32 +190,25 @@ function FeedRow({
191
190
  ]
192
191
  }
193
192
  ),
194
- dated && /* @__PURE__ */ jsxs(Fragment, { children: [
195
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "·" }),
196
- /* @__PURE__ */ jsx(
197
- "span",
198
- {
199
- "data-testid": "feed-row-date",
200
- className: "text-muted-foreground",
201
- children: dated
202
- }
203
- )
204
- ] })
193
+ tool.comments > 0 && /* @__PURE__ */ jsxs(
194
+ "span",
195
+ {
196
+ "data-testid": "feed-row-comments",
197
+ className: "inline-flex items-center gap-1 text-muted-foreground",
198
+ children: [
199
+ /* @__PURE__ */ jsx(MessageSquare, { "aria-hidden": "true", className: "size-[15px]" }),
200
+ tool.comments,
201
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: tool.comments === 1 ? " comment" : " comments" })
202
+ ]
203
+ }
204
+ )
205
205
  ] }),
206
- /* @__PURE__ */ jsxs(
207
- "div",
206
+ dated && /* @__PURE__ */ jsx(
207
+ "span",
208
208
  {
209
- "aria-hidden": "true",
210
- "data-testid": "feed-row-address",
211
- className: cn(
212
- "flex h-full min-w-[150px] flex-1 items-center justify-end gap-1.5 px-4 font-mono text-[12.5px] text-muted-foreground opacity-0 transition-opacity",
213
- "group-hover/row:text-foreground group-hover/row:opacity-100 group-has-[[data-site]:focus-visible]/row:opacity-100 group-has-[[data-details]:hover]/row:opacity-0 [@media(hover:none)]:opacity-100",
214
- selected && "opacity-100"
215
- ),
216
- children: [
217
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: address }),
218
- /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-3.5 shrink-0" })
219
- ]
209
+ "data-testid": "feed-row-date",
210
+ className: "ml-auto shrink-0 pr-4 text-[13px] font-semibold whitespace-nowrap text-muted-foreground",
211
+ children: dated
220
212
  }
221
213
  )
222
214
  ]