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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/dist/esm/__tests__/modules/auth/AuthContext.test.d.ts +1 -0
  2. package/dist/esm/__tests__/modules/communityTools/likedBy.test.d.ts +1 -0
  3. package/dist/esm/__tests__/modules/communityTools/timeAgo.test.d.ts +1 -0
  4. package/dist/esm/__tests__/modules/communityTools/useTimeUp.test.d.ts +1 -0
  5. package/dist/esm/api/infra/trpc.d.ts +419 -0
  6. package/dist/esm/api/infra/trpc.js.map +1 -1
  7. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -1
  8. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.d.ts +2 -4
  9. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +29 -101
  10. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.d.ts +26 -0
  12. package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js +108 -0
  13. package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -1
  15. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/components/SectionTabs.d.ts +8 -0
  17. package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js +73 -0
  18. package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js.map +1 -0
  19. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +19 -2
  20. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +3 -2
  22. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +4 -4
  23. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +2 -1
  25. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +2 -2
  26. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -1
  27. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +3 -2
  28. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +13 -9
  29. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -1
  30. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +4 -2
  31. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +10 -4
  32. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -1
  33. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +12 -0
  34. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +4 -0
  35. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -1
  36. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +1 -0
  37. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -1
  38. package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.d.ts +6 -0
  39. package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js +16 -0
  40. package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js.map +1 -0
  41. package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js +2 -2
  42. package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js.map +1 -1
  43. package/dist/esm/modules/auth/AuthContext.js +7 -3
  44. package/dist/esm/modules/auth/AuthContext.js.map +1 -1
  45. package/dist/esm/modules/auth/ui/LoginModal.js +16 -8
  46. package/dist/esm/modules/auth/ui/LoginModal.js.map +1 -1
  47. package/dist/esm/modules/communityTools/likedBy.d.ts +8 -0
  48. package/dist/esm/modules/communityTools/likedBy.js +23 -0
  49. package/dist/esm/modules/communityTools/likedBy.js.map +1 -0
  50. package/dist/esm/modules/communityTools/timeAgo.d.ts +2 -0
  51. package/dist/esm/modules/communityTools/timeAgo.js +18 -0
  52. package/dist/esm/modules/communityTools/timeAgo.js.map +1 -0
  53. package/dist/esm/modules/communityTools/ui/CardScreenshot.d.ts +13 -0
  54. package/dist/esm/modules/communityTools/ui/CardScreenshot.js +112 -0
  55. package/dist/esm/modules/communityTools/ui/CardScreenshot.js.map +1 -0
  56. package/dist/esm/modules/communityTools/ui/CardTitleBlock.d.ts +8 -0
  57. package/dist/esm/modules/communityTools/ui/CardTitleBlock.js +59 -0
  58. package/dist/esm/modules/communityTools/ui/CardTitleBlock.js.map +1 -0
  59. package/dist/esm/modules/communityTools/ui/CommunityPage.d.ts +12 -0
  60. package/dist/esm/modules/communityTools/ui/CommunityPage.js +547 -0
  61. package/dist/esm/modules/communityTools/ui/CommunityPage.js.map +1 -0
  62. package/dist/esm/modules/communityTools/ui/FeedRow.d.ts +42 -0
  63. package/dist/esm/modules/communityTools/ui/FeedRow.js +260 -0
  64. package/dist/esm/modules/communityTools/ui/FeedRow.js.map +1 -0
  65. package/dist/esm/modules/communityTools/ui/FeedSortControl.d.ts +17 -0
  66. package/dist/esm/modules/communityTools/ui/FeedSortControl.js +84 -0
  67. package/dist/esm/modules/communityTools/ui/FeedSortControl.js.map +1 -0
  68. package/dist/esm/modules/communityTools/ui/LifecycleBadge.d.ts +7 -0
  69. package/dist/esm/modules/communityTools/ui/LifecycleBadge.js +50 -0
  70. package/dist/esm/modules/communityTools/ui/LifecycleBadge.js.map +1 -0
  71. package/dist/esm/modules/communityTools/ui/LikeButton.d.ts +9 -0
  72. package/dist/esm/modules/communityTools/ui/LikeButton.js +56 -0
  73. package/dist/esm/modules/communityTools/ui/LikeButton.js.map +1 -0
  74. package/dist/esm/modules/communityTools/ui/MarginPreview.d.ts +18 -0
  75. package/dist/esm/modules/communityTools/ui/MarginPreview.js +115 -0
  76. package/dist/esm/modules/communityTools/ui/MarginPreview.js.map +1 -0
  77. package/dist/esm/modules/communityTools/ui/MyToolsShelf.d.ts +12 -0
  78. package/dist/esm/modules/communityTools/ui/MyToolsShelf.js +88 -0
  79. package/dist/esm/modules/communityTools/ui/MyToolsShelf.js.map +1 -0
  80. package/dist/esm/modules/communityTools/ui/OwnerPicker.d.ts +56 -0
  81. package/dist/esm/modules/communityTools/ui/OwnerPicker.js +245 -0
  82. package/dist/esm/modules/communityTools/ui/OwnerPicker.js.map +1 -0
  83. package/dist/esm/modules/communityTools/ui/PinButton.d.ts +14 -0
  84. package/dist/esm/modules/communityTools/ui/PinButton.js +72 -0
  85. package/dist/esm/modules/communityTools/ui/PinButton.js.map +1 -0
  86. package/dist/esm/modules/communityTools/ui/ShelfTile.d.ts +13 -0
  87. package/dist/esm/modules/communityTools/ui/ShelfTile.js +81 -0
  88. package/dist/esm/modules/communityTools/ui/ShelfTile.js.map +1 -0
  89. package/dist/esm/modules/communityTools/ui/TagPicker.d.ts +21 -0
  90. package/dist/esm/modules/communityTools/ui/TagPicker.js +87 -0
  91. package/dist/esm/modules/communityTools/ui/TagPicker.js.map +1 -0
  92. package/dist/esm/modules/communityTools/ui/TagRow.d.ts +22 -0
  93. package/dist/esm/modules/communityTools/ui/TagRow.js +92 -0
  94. package/dist/esm/modules/communityTools/ui/TagRow.js.map +1 -0
  95. package/dist/esm/modules/communityTools/ui/Toast.d.ts +19 -0
  96. package/dist/esm/modules/communityTools/ui/Toast.js +87 -0
  97. package/dist/esm/modules/communityTools/ui/Toast.js.map +1 -0
  98. package/dist/esm/modules/communityTools/ui/ToolCard.d.ts +22 -0
  99. package/dist/esm/modules/communityTools/ui/ToolCard.js +467 -0
  100. package/dist/esm/modules/communityTools/ui/ToolCard.js.map +1 -0
  101. package/dist/esm/modules/communityTools/ui/ToolFormDialog.d.ts +32 -0
  102. package/dist/esm/modules/communityTools/ui/ToolFormDialog.js +765 -0
  103. package/dist/esm/modules/communityTools/ui/ToolFormDialog.js.map +1 -0
  104. package/dist/esm/modules/communityTools/ui/ToolSearch.d.ts +15 -0
  105. package/dist/esm/modules/communityTools/ui/ToolSearch.js +92 -0
  106. package/dist/esm/modules/communityTools/ui/ToolSearch.js.map +1 -0
  107. package/dist/esm/modules/communityTools/useCommunityTools.d.ts +157 -0
  108. package/dist/esm/modules/communityTools/useCommunityTools.js +122 -0
  109. package/dist/esm/modules/communityTools/useCommunityTools.js.map +1 -0
  110. package/dist/esm/modules/communityTools/useCommunityToolsConfig.d.ts +10 -0
  111. package/dist/esm/modules/communityTools/useCommunityToolsConfig.js +20 -0
  112. package/dist/esm/modules/communityTools/useCommunityToolsConfig.js.map +1 -0
  113. package/dist/esm/modules/communityTools/useTimeUp.d.ts +5 -0
  114. package/dist/esm/modules/communityTools/useTimeUp.js +13 -0
  115. package/dist/esm/modules/communityTools/useTimeUp.js.map +1 -0
  116. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +2 -2
  117. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +1 -1
  118. package/dist/esm/routeTree.gen.d.ts +96 -3
  119. package/dist/esm/routeTree.gen.js +19 -0
  120. package/dist/esm/routeTree.gen.js.map +1 -1
  121. package/dist/esm/routes/_layout/community.$slug.d.ts +22 -0
  122. package/dist/esm/routes/_layout/community._slug.js +10 -0
  123. package/dist/esm/routes/_layout/community._slug.js.map +1 -0
  124. package/dist/esm/routes/_layout/community.d.ts +23 -0
  125. package/dist/esm/routes/_layout/community.js +71 -0
  126. package/dist/esm/routes/_layout/community.js.map +1 -0
  127. package/dist/esm/ui/components/header/Header.js +7 -3
  128. package/dist/esm/ui/components/header/Header.js.map +1 -1
  129. package/dist/esm/util/highlightMatches.d.ts +2 -0
  130. package/dist/esm/util/highlightMatches.js +32 -0
  131. package/dist/esm/util/highlightMatches.js.map +1 -0
  132. package/dist/index.css +6 -0
  133. package/package.json +5 -5
  134. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +3 -0
  135. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +3 -0
  136. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -0
  137. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +4 -0
  138. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +3 -0
  139. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +3 -0
  140. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +3 -0
  141. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -0
  142. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +3 -0
  143. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +3 -0
  144. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +10 -3
  145. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +4 -1
  146. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +12 -2
  147. package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +1 -0
  148. package/src/__tests__/modules/auth/AuthContext.test.tsx +55 -0
  149. package/src/__tests__/modules/communityTools/likedBy.test.ts +45 -0
  150. package/src/__tests__/modules/communityTools/timeAgo.test.ts +28 -0
  151. package/src/__tests__/modules/communityTools/useTimeUp.test.ts +31 -0
  152. package/src/__tests__/ui/components/header/Header.login.test.tsx +4 -4
  153. package/src/api/infra/trpc.ts +2 -0
  154. package/src/index.css +6 -0
  155. package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +33 -176
  156. package/src/modules/appCatalog/ui/catalog/DetailDialogShell.tsx +199 -0
  157. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -1
  158. package/src/modules/appCatalog/ui/components/SectionTabs.tsx +81 -0
  159. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +17 -2
  160. package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +9 -4
  161. package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +6 -3
  162. package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +15 -10
  163. package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +10 -5
  164. package/src/modules/appCatalog/ui/detail/communityDraft.ts +16 -0
  165. package/src/modules/appCatalog/ui/detail/composeComments.ts +1 -0
  166. package/src/modules/appCatalog/ui/detail/useCommunityToolCount.ts +18 -0
  167. package/src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx +2 -2
  168. package/src/modules/auth/AuthContext.tsx +9 -3
  169. package/src/modules/auth/ui/LoginModal.tsx +7 -2
  170. package/src/modules/communityTools/likedBy.ts +32 -0
  171. package/src/modules/communityTools/timeAgo.ts +16 -0
  172. package/src/modules/communityTools/ui/CardScreenshot.tsx +124 -0
  173. package/src/modules/communityTools/ui/CardTitleBlock.tsx +58 -0
  174. package/src/modules/communityTools/ui/CommunityPage.tsx +626 -0
  175. package/src/modules/communityTools/ui/FeedRow.tsx +310 -0
  176. package/src/modules/communityTools/ui/FeedSortControl.tsx +97 -0
  177. package/src/modules/communityTools/ui/LifecycleBadge.tsx +44 -0
  178. package/src/modules/communityTools/ui/LikeButton.tsx +56 -0
  179. package/src/modules/communityTools/ui/MarginPreview.tsx +143 -0
  180. package/src/modules/communityTools/ui/MyToolsShelf.tsx +112 -0
  181. package/src/modules/communityTools/ui/OwnerPicker.tsx +311 -0
  182. package/src/modules/communityTools/ui/PinButton.tsx +87 -0
  183. package/src/modules/communityTools/ui/ShelfTile.tsx +83 -0
  184. package/src/modules/communityTools/ui/TagPicker.tsx +118 -0
  185. package/src/modules/communityTools/ui/TagRow.tsx +118 -0
  186. package/src/modules/communityTools/ui/Toast.tsx +105 -0
  187. package/src/modules/communityTools/ui/ToolCard.tsx +519 -0
  188. package/src/modules/communityTools/ui/ToolFormDialog.tsx +922 -0
  189. package/src/modules/communityTools/ui/ToolSearch.tsx +111 -0
  190. package/src/modules/communityTools/useCommunityTools.ts +146 -0
  191. package/src/modules/communityTools/useCommunityToolsConfig.ts +26 -0
  192. package/src/modules/communityTools/useTimeUp.ts +14 -0
  193. package/src/routeTree.gen.ts +52 -0
  194. package/src/routes/_layout/community.$slug.tsx +7 -0
  195. package/src/routes/_layout/community.tsx +97 -0
  196. package/src/ui/components/header/Header.tsx +9 -3
  197. package/src/util/highlightMatches.tsx +34 -0
  198. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +0 -12
  199. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +0 -56
  200. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +0 -1
  201. package/src/modules/appCatalog/ui/components/ViewToggle.tsx +0 -59
@@ -1 +1 @@
1
- {"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n appSlug={app.slug}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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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":"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"}
@@ -1,4 +1,5 @@
1
1
  import { Claim } from '@igstack/app-catalog-backend-core';
2
+ import { CommentTarget } from './communityDraft.js';
2
3
  /**
3
4
  * Everything already said about the entry, under the input: one feed, newest
4
5
  * first, one entry per visitor note. See `composeComments` for how claims fold
@@ -9,8 +10,8 @@ import { Claim } from '@igstack/app-catalog-backend-core';
9
10
  * withdrawing (until someone reviews it). `canEditUntil` arrives as an absolute
10
11
  * instant so a skewed client clock cannot show a control the server would refuse.
11
12
  */
12
- export declare function CommentsFeed({ appSlug, claims, labelledBy, }: {
13
- appSlug: string;
13
+ export declare function CommentsFeed({ target, claims, labelledBy, }: {
14
+ target: CommentTarget;
14
15
  claims: Claim[];
15
16
  /** Id of the heading that names the list. */
16
17
  labelledBy: string;
@@ -44,13 +44,13 @@ function Meta({ entry, edited }) {
44
44
  ] });
45
45
  }
46
46
  function CommentsFeed({
47
- appSlug,
47
+ target,
48
48
  claims,
49
49
  labelledBy
50
50
  }) {
51
51
  const trpc = useTRPC();
52
52
  const queryClient = useQueryClient();
53
- const listOptions = trpc.feedback.list.queryOptions({ resourceSlug: appSlug });
53
+ const listOptions = trpc.feedback.list.queryOptions(target);
54
54
  const { data, error, isPending } = useQuery(listOptions);
55
55
  const [editingId, setEditingId] = React__default.useState(null);
56
56
  const [editDraft, setEditDraft] = React__default.useState("");
@@ -172,10 +172,10 @@ function CommentsFeed({
172
172
  ]
173
173
  }
174
174
  ) : entries.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "comments", "aria-labelledby": labelledBy, children: entries.map((entry) => {
175
- var _a;
175
+ var _a, _b;
176
176
  return /* @__PURE__ */ jsxs("li", { className: "comment", children: [
177
177
  /* @__PURE__ */ jsx(Meta, { entry, edited: !!((_a = entry.note) == null ? void 0 : _a.editedAt) }),
178
- entry.note ? noteBody(entry.note) : /* @__PURE__ */ jsx("p", { className: "comment__text", children: entry.text })
178
+ ((_b = entry.note) == null ? void 0 : _b.isSystem) ? /* @__PURE__ */ jsx("p", { className: "comment__text is-system", "data-system-line": "", children: entry.text }) : entry.note ? noteBody(entry.note) : /* @__PURE__ */ jsx("p", { className: "comment__text", children: entry.text })
179
179
  ] }, entry.key);
180
180
  }) }) : /* @__PURE__ */ jsx("p", { className: "comments-empty", children: "No comments yet. Be the first to share what you know." }),
181
181
  failure && /* @__PURE__ */ jsx("p", { className: "know-more-error", children: formatCommunityError(failure) })
@@ -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 { 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 appSlug,\n claims,\n labelledBy,\n}: {\n appSlug: string\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({ resourceSlug: appSlug })\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 ? (\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({ 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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"}
@@ -4,6 +4,7 @@
4
4
  * While closed it is not a dismiss layer, so Esc still reaches the card.
5
5
  */
6
6
  export declare function CommentsHelp({ appSlug, appName, }: {
7
- appSlug: string;
7
+ /** Set for an official entry; a deployment's note speaks to entries only. */
8
+ appSlug?: string;
8
9
  appName: string;
9
10
  }): import("react/jsx-runtime").JSX.Element;
@@ -79,8 +79,8 @@ function CommentsHelp({
79
79
  /* @__PURE__ */ jsx("li", { children: "Attach a screenshot — paste it, drop it or use the paperclip." })
80
80
  ] }),
81
81
  /* @__PURE__ */ jsxs("p", { className: "comments-help__note", children: [
82
- /* @__PURE__ */ jsx(ResourceDetailFeedbackNote, { appSlug, appName }),
83
- !hasPluginNote && /* @__PURE__ */ jsx(Fragment, { children: "Every note is reviewed. Facts that check out update the card and are marked Used as source." })
82
+ appSlug && /* @__PURE__ */ jsx(ResourceDetailFeedbackNote, { appSlug, appName }),
83
+ !(appSlug && hasPluginNote) && /* @__PURE__ */ jsx(Fragment, { children: "Every note is reviewed. Facts that check out update the card and are marked Used as source." })
84
84
  ] })
85
85
  ] })
86
86
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"CommentsHelp.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommentsHelp.tsx"],"sourcesContent":["import React from 'react'\nimport { useDismissable } from '~/modules/dismiss/DismissStack'\nimport { ResourceDetailFeedbackNote, useSlotFilled } from '~/modules/extensions'\n\n/**\n * The `?` beside \"Comments\": what the input takes, and what becomes of a note.\n * Folded away so the input stays one line; Esc and a click outside close it.\n * While closed it is not a dismiss layer, so Esc still reaches the card.\n */\nexport function CommentsHelp({\n appSlug,\n appName,\n}: {\n appSlug: string\n appName: string\n}) {\n const [open, setOpen] = React.useState(false)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const popoverRef = React.useRef<HTMLDivElement>(null)\n const id = React.useId()\n const hasPluginNote = useSlotFilled('resourceDetailFeedbackNote')\n\n useDismissable(\n popoverRef,\n () => {\n setOpen(false)\n buttonRef.current?.focus()\n },\n { enabled: open },\n )\n\n React.useEffect(() => {\n if (!open) return\n const onPointerDown = (e: PointerEvent) => {\n const target = e.target as Node\n if (\n popoverRef.current?.contains(target) ||\n buttonRef.current?.contains(target)\n )\n return\n setOpen(false)\n }\n document.addEventListener('pointerdown', onPointerDown)\n return () => document.removeEventListener('pointerdown', onPointerDown)\n }, [open])\n\n return (\n <>\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"comments-help-button\"\n aria-label=\"How comments work\"\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen((v) => !v)}\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6.75\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n />\n <path\n d=\"M6.25 6.3a1.8 1.8 0 0 1 3.5.55c0 1.2-1.75 1.45-1.75 2.6\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"8\" cy=\"11.4\" r=\"0.8\" fill=\"currentColor\" />\n </svg>\n </button>\n <div ref={popoverRef} id={id} className=\"comments-help\" hidden={!open}>\n <p className=\"comments-help__title\">How comments work</p>\n <ul>\n <li>\n Paste a link — a wiki page, a doc or a ticket that explains\n something about this app.\n </li>\n <li>\n Fix something — \"Access is approved by your team lead, not IT.\"\n </li>\n <li>Ask a question — \"Can contractors get access?\"</li>\n <li>Attach a screenshot — paste it, drop it or use the paperclip.</li>\n </ul>\n <p className=\"comments-help__note\">\n <ResourceDetailFeedbackNote appSlug={appSlug} appName={appName} />\n {!hasPluginNote && (\n <>\n Every note is reviewed. Facts that check out update the card and\n are marked Used as source.\n </>\n )}\n </p>\n </div>\n </>\n )\n}\n"],"names":["React"],"mappings":";;;;;AASO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,MAAM,OAAO,IAAIA,eAAM,SAAS,KAAK;AAC5C,QAAM,YAAYA,eAAM,OAA0B,IAAI;AACtD,QAAM,aAAaA,eAAM,OAAuB,IAAI;AACpD,QAAM,KAAKA,eAAM,MAAA;AACjB,QAAM,gBAAgB,cAAc,4BAA4B;AAEhE;AAAA,IACE;AAAA,IACA,MAAM;;AACJ,cAAQ,KAAK;AACb,sBAAU,YAAV,mBAAmB;AAAA,IACrB;AAAA,IACA,EAAE,SAAS,KAAA;AAAA,EAAK;AAGlBA,iBAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,UAAM,gBAAgB,CAAC,MAAoB;;AACzC,YAAM,SAAS,EAAE;AACjB,YACE,gBAAW,YAAX,mBAAoB,SAAS,cAC7B,eAAU,YAAV,mBAAmB,SAAS;AAE5B;AACF,cAAQ,KAAK;AAAA,IACf;AACA,aAAS,iBAAiB,eAAe,aAAa;AACtD,WAAO,MAAM,SAAS,oBAAoB,eAAe,aAAa;AAAA,EACxE,GAAG,CAAC,IAAI,CAAC;AAET,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAU;AAAA,QACV,cAAW;AAAA,QACX,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,QAEhC,UAAA,qBAAC,SAAI,SAAQ,aAAY,OAAM,MAAK,QAAO,MAAK,eAAY,QAC1D,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAG;AAAA,cACH,IAAG;AAAA,cACH,GAAE;AAAA,cACF,MAAK;AAAA,cACL,QAAO;AAAA,cACP,aAAY;AAAA,YAAA;AAAA,UAAA;AAAA,UAEd;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,GAAE;AAAA,cACF,MAAK;AAAA,cACL,QAAO;AAAA,cACP,aAAY;AAAA,cACZ,eAAc;AAAA,YAAA;AAAA,UAAA;AAAA,UAEhB,oBAAC,YAAO,IAAG,KAAI,IAAG,QAAO,GAAE,OAAM,MAAK,eAAA,CAAe;AAAA,QAAA,EAAA,CACvD;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,qBAAC,SAAI,KAAK,YAAY,IAAQ,WAAU,iBAAgB,QAAQ,CAAC,MAC/D,UAAA;AAAA,MAAA,oBAAC,KAAA,EAAE,WAAU,wBAAuB,UAAA,qBAAiB;AAAA,2BACpD,MAAA,EACC,UAAA;AAAA,QAAA,oBAAC,QAAG,UAAA,wFAAA,CAGJ;AAAA,QACA,oBAAC,QAAG,UAAA,kEAAA,CAEJ;AAAA,QACA,oBAAC,QAAG,UAAA,iDAAA,CAA8C;AAAA,QAClD,oBAAC,QAAG,UAAA,gEAAA,CAA6D;AAAA,MAAA,GACnE;AAAA,MACA,qBAAC,KAAA,EAAE,WAAU,uBACX,UAAA;AAAA,QAAA,oBAAC,4BAAA,EAA2B,SAAkB,QAAA,CAAkB;AAAA,QAC/D,CAAC,iBACA,oBAAA,UAAA,EAAE,UAAA,8FAAA,CAGF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"CommentsHelp.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/CommentsHelp.tsx"],"sourcesContent":["import React from 'react'\nimport { useDismissable } from '~/modules/dismiss/DismissStack'\nimport { ResourceDetailFeedbackNote, useSlotFilled } from '~/modules/extensions'\n\n/**\n * The `?` beside \"Comments\": what the input takes, and what becomes of a note.\n * Folded away so the input stays one line; Esc and a click outside close it.\n * While closed it is not a dismiss layer, so Esc still reaches the card.\n */\nexport function CommentsHelp({\n appSlug,\n appName,\n}: {\n /** Set for an official entry; a deployment's note speaks to entries only. */\n appSlug?: string\n appName: string\n}) {\n const [open, setOpen] = React.useState(false)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const popoverRef = React.useRef<HTMLDivElement>(null)\n const id = React.useId()\n const hasPluginNote = useSlotFilled('resourceDetailFeedbackNote')\n\n useDismissable(\n popoverRef,\n () => {\n setOpen(false)\n buttonRef.current?.focus()\n },\n { enabled: open },\n )\n\n React.useEffect(() => {\n if (!open) return\n const onPointerDown = (e: PointerEvent) => {\n const target = e.target as Node\n if (\n popoverRef.current?.contains(target) ||\n buttonRef.current?.contains(target)\n )\n return\n setOpen(false)\n }\n document.addEventListener('pointerdown', onPointerDown)\n return () => document.removeEventListener('pointerdown', onPointerDown)\n }, [open])\n\n return (\n <>\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"comments-help-button\"\n aria-label=\"How comments work\"\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen((v) => !v)}\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6.75\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n />\n <path\n d=\"M6.25 6.3a1.8 1.8 0 0 1 3.5.55c0 1.2-1.75 1.45-1.75 2.6\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"8\" cy=\"11.4\" r=\"0.8\" fill=\"currentColor\" />\n </svg>\n </button>\n <div ref={popoverRef} id={id} className=\"comments-help\" hidden={!open}>\n <p className=\"comments-help__title\">How comments work</p>\n <ul>\n <li>\n Paste a link — a wiki page, a doc or a ticket that explains\n something about this app.\n </li>\n <li>\n Fix something — \"Access is approved by your team lead, not IT.\"\n </li>\n <li>Ask a question — \"Can contractors get access?\"</li>\n <li>Attach a screenshot — paste it, drop it or use the paperclip.</li>\n </ul>\n <p className=\"comments-help__note\">\n {appSlug && (\n <ResourceDetailFeedbackNote appSlug={appSlug} appName={appName} />\n )}\n {!(appSlug && hasPluginNote) && (\n <>\n Every note is reviewed. Facts that check out update the card and\n are marked Used as source.\n </>\n )}\n </p>\n </div>\n </>\n )\n}\n"],"names":["React"],"mappings":";;;;;AASO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,MAAM,OAAO,IAAIA,eAAM,SAAS,KAAK;AAC5C,QAAM,YAAYA,eAAM,OAA0B,IAAI;AACtD,QAAM,aAAaA,eAAM,OAAuB,IAAI;AACpD,QAAM,KAAKA,eAAM,MAAA;AACjB,QAAM,gBAAgB,cAAc,4BAA4B;AAEhE;AAAA,IACE;AAAA,IACA,MAAM;;AACJ,cAAQ,KAAK;AACb,sBAAU,YAAV,mBAAmB;AAAA,IACrB;AAAA,IACA,EAAE,SAAS,KAAA;AAAA,EAAK;AAGlBA,iBAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,UAAM,gBAAgB,CAAC,MAAoB;;AACzC,YAAM,SAAS,EAAE;AACjB,YACE,gBAAW,YAAX,mBAAoB,SAAS,cAC7B,eAAU,YAAV,mBAAmB,SAAS;AAE5B;AACF,cAAQ,KAAK;AAAA,IACf;AACA,aAAS,iBAAiB,eAAe,aAAa;AACtD,WAAO,MAAM,SAAS,oBAAoB,eAAe,aAAa;AAAA,EACxE,GAAG,CAAC,IAAI,CAAC;AAET,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAU;AAAA,QACV,cAAW;AAAA,QACX,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,QAEhC,UAAA,qBAAC,SAAI,SAAQ,aAAY,OAAM,MAAK,QAAO,MAAK,eAAY,QAC1D,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAG;AAAA,cACH,IAAG;AAAA,cACH,GAAE;AAAA,cACF,MAAK;AAAA,cACL,QAAO;AAAA,cACP,aAAY;AAAA,YAAA;AAAA,UAAA;AAAA,UAEd;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,GAAE;AAAA,cACF,MAAK;AAAA,cACL,QAAO;AAAA,cACP,aAAY;AAAA,cACZ,eAAc;AAAA,YAAA;AAAA,UAAA;AAAA,UAEhB,oBAAC,YAAO,IAAG,KAAI,IAAG,QAAO,GAAE,OAAM,MAAK,eAAA,CAAe;AAAA,QAAA,EAAA,CACvD;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,qBAAC,SAAI,KAAK,YAAY,IAAQ,WAAU,iBAAgB,QAAQ,CAAC,MAC/D,UAAA;AAAA,MAAA,oBAAC,KAAA,EAAE,WAAU,wBAAuB,UAAA,qBAAiB;AAAA,2BACpD,MAAA,EACC,UAAA;AAAA,QAAA,oBAAC,QAAG,UAAA,wFAAA,CAGJ;AAAA,QACA,oBAAC,QAAG,UAAA,kEAAA,CAEJ;AAAA,QACA,oBAAC,QAAG,UAAA,iDAAA,CAA8C;AAAA,QAClD,oBAAC,QAAG,UAAA,gEAAA,CAA6D;AAAA,MAAA,GACnE;AAAA,MACA,qBAAC,KAAA,EAAE,WAAU,uBACV,UAAA;AAAA,QAAA,WACC,oBAAC,4BAAA,EAA2B,SAAkB,QAAA,CAAkB;AAAA,QAEjE,EAAE,WAAW,kBACZ,oBAAA,UAAA,EAAE,UAAA,8FAAA,CAGF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -1,4 +1,5 @@
1
1
  import { default as React } from 'react';
2
+ import { CommentTarget } from './communityDraft.js';
2
3
  /** What the card holds on to, so "Share what you know" can point at the field. */
3
4
  export interface CommunityInputHandle {
4
5
  /** Focuses the field and highlights it briefly. Re-triggers on every call. */
@@ -10,8 +11,8 @@ export interface CommunityInputHandle {
10
11
  * field grows as the text wraps. Screenshots come in through the
11
12
  * paperclip, paste or drop, and a screenshot alone is a valid note.
12
13
  */
13
- export declare function CommunityInput({ appSlug, appName, ref, }: {
14
- appSlug: string;
14
+ export declare function CommunityInput({ target, appName, ref, }: {
15
+ target: CommentTarget;
15
16
  appName: string;
16
17
  ref?: React.Ref<CommunityInputHandle>;
17
18
  }): import("react/jsx-runtime").JSX.Element;
@@ -5,7 +5,7 @@ import React__default from "react";
5
5
  import { useTRPC } from "../../../../api/infra/trpc.js";
6
6
  import { cn } from "../../../../lib/utils.js";
7
7
  import { Button } from "../../../../ui/button.js";
8
- import { readDraft, readAttachments, writeDraft, writeAttachments, attachmentUrl } from "./communityDraft.js";
8
+ import { draftSlugOf, readDraft, readAttachments, writeDraft, writeAttachments, attachmentUrl } from "./communityDraft.js";
9
9
  import { formatCommunityError } from "./communityError.js";
10
10
  const MAX_HEIGHT = 200;
11
11
  const FLASH_MS = 900;
@@ -34,15 +34,16 @@ async function upload(files) {
34
34
  }
35
35
  }
36
36
  function CommunityInput({
37
- appSlug,
37
+ target,
38
38
  appName,
39
39
  ref
40
40
  }) {
41
41
  const trpc = useTRPC();
42
42
  const queryClient = useQueryClient();
43
- const [text, setText] = React__default.useState(() => readDraft(appSlug));
43
+ const draftSlug = draftSlugOf(target);
44
+ const [text, setText] = React__default.useState(() => readDraft(draftSlug));
44
45
  const [images, setImages] = React__default.useState(
45
- () => readAttachments(appSlug)
46
+ () => readAttachments(draftSlug)
46
47
  );
47
48
  const [sent, setSent] = React__default.useState(false);
48
49
  const [sends, setSends] = React__default.useState(0);
@@ -69,8 +70,11 @@ function CommunityInput({
69
70
  mq.addEventListener("change", handler);
70
71
  return () => mq.removeEventListener("change", handler);
71
72
  }, []);
72
- React__default.useEffect(() => writeDraft(appSlug, text), [appSlug, text]);
73
- React__default.useEffect(() => writeAttachments(appSlug, images), [appSlug, images]);
73
+ React__default.useEffect(() => writeDraft(draftSlug, text), [draftSlug, text]);
74
+ React__default.useEffect(
75
+ () => writeAttachments(draftSlug, images),
76
+ [draftSlug, images]
77
+ );
74
78
  React__default.useImperativeHandle(ref, () => ({
75
79
  focus: () => {
76
80
  var _a;
@@ -97,7 +101,7 @@ function CommunityInput({
97
101
  setSent(true);
98
102
  setSends((n) => n + 1);
99
103
  void queryClient.invalidateQueries({
100
- queryKey: trpc.feedback.list.queryKey({ resourceSlug: appSlug })
104
+ queryKey: trpc.feedback.list.queryKey(target)
101
105
  });
102
106
  }
103
107
  })
@@ -160,7 +164,7 @@ function CommunityInput({
160
164
  });
161
165
  setImages((current) => {
162
166
  const updated = [...current, ...newImages].slice(0, MAX_ATTACHMENTS);
163
- writeAttachments(appSlug, updated);
167
+ writeAttachments(draftSlug, updated);
164
168
  return updated;
165
169
  });
166
170
  if (files.length > room)
@@ -171,7 +175,7 @@ function CommunityInput({
171
175
  if (!canSend || add.isPending || uploading) return;
172
176
  const body = text.trim();
173
177
  add.mutate({
174
- resourceSlug: appSlug,
178
+ ...target,
175
179
  ...body ? { body } : {},
176
180
  ...images.length > 0 ? { attachmentIds: images.map((i) => i.id) } : {}
177
181
  });