@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
@@ -0,0 +1,467 @@
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
3
+ import { ArrowLeft, ArrowRight, Pencil, Eye, Check, Archive } from "lucide-react";
4
+ import { useState, useRef, useEffect, useId } from "react";
5
+ import { useTRPC } from "../../../api/infra/trpc.js";
6
+ import { DIALOG_ROUND_BUTTON, DetailDialogShell } from "../../appCatalog/ui/catalog/DetailDialogShell.js";
7
+ import { MarkdownText } from "../../appCatalog/ui/components/MarkdownText.js";
8
+ import { CommentsFeed } from "../../appCatalog/ui/detail/CommentsFeed.js";
9
+ import { CommentsHelp } from "../../appCatalog/ui/detail/CommentsHelp.js";
10
+ import { CommunityInput } from "../../appCatalog/ui/detail/CommunityInput.js";
11
+ import { cn } from "../../../lib/utils.js";
12
+ import { useAuth } from "../../auth/AuthContext.js";
13
+ import "../../../ui/button.js";
14
+ import { Skeleton } from "../../../ui/skeleton.js";
15
+ import { formatCardDate, CardScreenshot } from "./CardScreenshot.js";
16
+ import { CardTitleBlock } from "./CardTitleBlock.js";
17
+ import { PersonAvatar } from "./FeedRow.js";
18
+ import { useTimeUp } from "../useTimeUp.js";
19
+ import { PinButton } from "./PinButton.js";
20
+ import { TagChip, tagLabel, tagKey } from "./TagRow.js";
21
+ import { GHOST_BUTTON, ArchiveConfirm, ToolFormDialog } from "./ToolFormDialog.js";
22
+ const KBD = "rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground";
23
+ const SECTION = "mt-6 border-t border-border pt-[22px]";
24
+ const LABEL = "text-[11.5px] font-bold tracking-wider text-muted-foreground uppercase";
25
+ const NO_CLAIMS = [];
26
+ const DAY_MS = 24 * 60 * 60 * 1e3;
27
+ const CAPTURE_POLL_MS = 5e3;
28
+ const CAPTURE_WAIT_MS = 2 * 60 * 1e3;
29
+ function isTyping(target) {
30
+ return target.isContentEditable || target.closest("input, textarea, select") !== null;
31
+ }
32
+ function ToolCard({
33
+ slug,
34
+ siblings,
35
+ onClose,
36
+ onNavigate,
37
+ pinned,
38
+ pinBusy,
39
+ onTogglePin,
40
+ appNames,
41
+ onChooseTag
42
+ }) {
43
+ var _a, _b;
44
+ const { user } = useAuth();
45
+ const isAdmin = (user == null ? void 0 : user.isAdmin) === true;
46
+ const trpc = useTRPC();
47
+ const queryClient = useQueryClient();
48
+ const list = useQuery(trpc.communityTools.list.queryOptions());
49
+ const fromList = (_a = list.data) == null ? void 0 : _a.find((t) => t.slug === slug);
50
+ const gaveUpOnCapture = useTimeUp(slug, CAPTURE_WAIT_MS);
51
+ const get = useQuery({
52
+ ...trpc.communityTools.get.queryOptions({ slug }),
53
+ placeholderData: fromList,
54
+ // Until the first visit lands, keep asking, so the screenshot replaces
55
+ // the placeholder without a reload.
56
+ refetchInterval: (query) => {
57
+ var _a2;
58
+ return ((_a2 = query.state.data) == null ? void 0 : _a2.capturePending) && !gaveUpOnCapture ? CAPTURE_POLL_MS : false;
59
+ }
60
+ });
61
+ const tool = get.data;
62
+ const [counts, setCounts] = useState(null);
63
+ const { mutate: recordView } = useMutation(
64
+ trpc.communityTools.recordView.mutationOptions({
65
+ onSuccess: (c, { slug: viewed }) => {
66
+ setCounts({ slug: viewed, ...c });
67
+ }
68
+ })
69
+ );
70
+ const recordedForRef = useRef(null);
71
+ useEffect(() => {
72
+ if (recordedForRef.current === slug) return;
73
+ recordedForRef.current = slug;
74
+ recordView({ slug });
75
+ }, [slug, recordView]);
76
+ const views = (counts == null ? void 0 : counts.slug) === slug ? counts : tool;
77
+ const [editing, setEditing] = useState(false);
78
+ const [archiving, setArchiving] = useState(false);
79
+ const refreshTools = () => queryClient.invalidateQueries({
80
+ queryKey: trpc.communityTools.pathKey()
81
+ });
82
+ const approve = useMutation(
83
+ trpc.communityTools.approve.mutationOptions({ onSuccess: refreshTools })
84
+ );
85
+ const archive = useMutation(
86
+ trpc.communityTools.archive.mutationOptions({
87
+ onSuccess: () => {
88
+ setArchiving(false);
89
+ return refreshTools();
90
+ }
91
+ })
92
+ );
93
+ const adminError = approve.error ?? archive.error;
94
+ const commentsRef = useRef(null);
95
+ const commentsHeadingId = useId();
96
+ const commentTarget = { communityToolSlug: slug };
97
+ const comments = useQuery(trpc.feedback.list.queryOptions(commentTarget));
98
+ const commentCount = ((_b = comments.data) == null ? void 0 : _b.items.length) ?? 0;
99
+ const index = siblings.indexOf(slug);
100
+ const prev = index > 0 ? siblings[index - 1] : void 0;
101
+ const next = index >= 0 ? siblings[index + 1] : void 0;
102
+ const nameOf = (s) => {
103
+ var _a2, _b2;
104
+ return ((_b2 = (_a2 = list.data) == null ? void 0 : _a2.find((t) => t.slug === s)) == null ? void 0 : _b2.name) ?? s;
105
+ };
106
+ const onKeyDown = (e) => {
107
+ const target = e.target;
108
+ if (!e.currentTarget.contains(target)) return;
109
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
110
+ if (isTyping(target)) return;
111
+ if (e.key === "ArrowLeft" && prev) {
112
+ e.preventDefault();
113
+ onNavigate(prev);
114
+ } else if (e.key === "ArrowRight" && next) {
115
+ e.preventDefault();
116
+ onNavigate(next);
117
+ } else if (e.key === "Enter" && tool) {
118
+ if (target.closest("a, button")) return;
119
+ e.preventDefault();
120
+ window.open(tool.url, "_blank", "noopener,noreferrer");
121
+ }
122
+ };
123
+ const controls = index >= 0 && siblings.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-[13px] font-semibold text-muted-foreground", children: [
125
+ /* @__PURE__ */ jsx(
126
+ "button",
127
+ {
128
+ type: "button",
129
+ "aria-label": prev ? `Previous tool: ${nameOf(prev)}` : "Previous tool",
130
+ disabled: !prev,
131
+ onClick: () => prev && onNavigate(prev),
132
+ className: cn(DIALOG_ROUND_BUTTON, "disabled:opacity-40"),
133
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "size-4" })
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsxs("span", { "data-testid": "card-position", children: [
137
+ index + 1,
138
+ " of ",
139
+ siblings.length
140
+ ] }),
141
+ /* @__PURE__ */ jsx(
142
+ "button",
143
+ {
144
+ type: "button",
145
+ "aria-label": next ? `Next tool: ${nameOf(next)}` : "Next tool",
146
+ disabled: !next,
147
+ onClick: () => next && onNavigate(next),
148
+ className: cn(DIALOG_ROUND_BUTTON, "disabled:opacity-40"),
149
+ children: /* @__PURE__ */ jsx(ArrowRight, { className: "size-4" })
150
+ }
151
+ )
152
+ ] }),
153
+ /* @__PURE__ */ jsx("i", { "aria-hidden": "true", className: "mx-1 h-5 w-px bg-border" })
154
+ ] });
155
+ return /* @__PURE__ */ jsx(
156
+ DetailDialogShell,
157
+ {
158
+ label: `${(tool == null ? void 0 : tool.name) ?? "Community tool"} details`,
159
+ shownKey: slug,
160
+ maxWidthClass: "max-w-[min(920px,94vw)]",
161
+ focusSelector: '[data-testid="card-title-block"]',
162
+ controls,
163
+ testId: "tool-card",
164
+ onClose,
165
+ onKeyDown,
166
+ children: tool ? /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-border pb-[22px]", children: [
168
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between gap-4 pr-44", children: [
169
+ /* @__PURE__ */ jsx(CardTitleBlock, { tool }),
170
+ /* @__PURE__ */ jsxs("div", { className: "-mr-40 flex shrink-0 items-center gap-2", children: [
171
+ tool.canEdit && /* @__PURE__ */ jsxs(
172
+ "button",
173
+ {
174
+ type: "button",
175
+ "data-testid": "card-edit-button",
176
+ onClick: () => setEditing(true),
177
+ className: cn(GHOST_BUTTON, "h-10 px-3.5 text-[13.5px]"),
178
+ children: [
179
+ /* @__PURE__ */ jsx(Pencil, { "aria-hidden": "true", className: "size-4" }),
180
+ "Edit"
181
+ ]
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx(
185
+ PinButton,
186
+ {
187
+ labelled: true,
188
+ pinned,
189
+ busy: pinBusy,
190
+ name: tool.name,
191
+ onToggle: () => onTogglePin(tool)
192
+ }
193
+ )
194
+ ] })
195
+ ] }),
196
+ /* @__PURE__ */ jsxs(
197
+ "div",
198
+ {
199
+ className: cn(
200
+ tool.lifecycle.state === "archived" && "opacity-60"
201
+ ),
202
+ children: [
203
+ tool.summary && /* @__PURE__ */ jsx("p", { className: "mt-3.5 text-[15px] text-foreground/80", children: tool.summary }),
204
+ tool.tags.length > 0 && /* @__PURE__ */ jsx(
205
+ "div",
206
+ {
207
+ "data-testid": "card-tags",
208
+ className: "mt-2.5 flex flex-wrap gap-1.5",
209
+ children: tool.tags.map((tag) => /* @__PURE__ */ jsx(
210
+ TagChip,
211
+ {
212
+ label: tagLabel(tag, appNames),
213
+ onClick: () => onChooseTag(tagKey(tag))
214
+ },
215
+ tagKey(tag)
216
+ ))
217
+ }
218
+ ),
219
+ views && /* @__PURE__ */ jsxs(
220
+ "p",
221
+ {
222
+ "data-testid": "card-views",
223
+ className: "mt-2.5 flex gap-[18px] text-[13px] font-semibold text-muted-foreground",
224
+ children: [
225
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
226
+ /* @__PURE__ */ jsx(Eye, { "aria-hidden": "true", className: "size-[15px]" }),
227
+ /* @__PURE__ */ jsx("b", { className: "text-foreground", children: views.views30d.toLocaleString("en-US") }),
228
+ " ",
229
+ "opens this month"
230
+ ] }),
231
+ /* @__PURE__ */ jsxs("span", { children: [
232
+ /* @__PURE__ */ jsx("b", { className: "text-foreground", children: views.viewsAll.toLocaleString("en-US") }),
233
+ " ",
234
+ "all time"
235
+ ] })
236
+ ]
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsxs(
240
+ "p",
241
+ {
242
+ "data-testid": "card-people",
243
+ className: "mt-2.5 flex flex-wrap items-center gap-x-[18px] gap-y-1.5 text-[13px] font-semibold text-muted-foreground",
244
+ children: [
245
+ tool.owners.length > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex flex-wrap items-center gap-x-3 gap-y-1.5", children: [
246
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: "Owners" }),
247
+ tool.owners.map((owner) => /* @__PURE__ */ jsxs(
248
+ "span",
249
+ {
250
+ className: "inline-flex items-center gap-1.5 text-foreground",
251
+ children: [
252
+ /* @__PURE__ */ jsx(PersonAvatar, { name: owner.name }),
253
+ /* @__PURE__ */ jsx("span", { "data-testid": "card-owner", children: owner.name })
254
+ ]
255
+ },
256
+ owner.slug ?? `named:${owner.name}`
257
+ ))
258
+ ] }),
259
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
260
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: "Added by" }),
261
+ /* @__PURE__ */ jsx("span", { className: "text-foreground", children: tool.addedBy.name }),
262
+ /* @__PURE__ */ jsx("span", { className: "font-normal", children: formatCardDate(tool.createdAt) })
263
+ ] })
264
+ ]
265
+ }
266
+ )
267
+ ]
268
+ }
269
+ ),
270
+ /* @__PURE__ */ jsx(
271
+ LifecycleNote,
272
+ {
273
+ tool,
274
+ onTellOwner: () => {
275
+ var _a2;
276
+ return (_a2 = commentsRef.current) == null ? void 0 : _a2.focus();
277
+ }
278
+ }
279
+ ),
280
+ isAdmin && /* @__PURE__ */ jsxs(
281
+ "div",
282
+ {
283
+ "data-testid": "card-admin",
284
+ className: "mt-3.5 flex flex-col gap-2.5 rounded-[10px] border border-dashed border-border px-3 py-2.5",
285
+ children: [
286
+ archiving ? /* @__PURE__ */ jsx(
287
+ ArchiveConfirm,
288
+ {
289
+ busy: archive.isPending,
290
+ onCancel: () => setArchiving(false),
291
+ onArchive: (reason) => archive.mutate({ slug, reason })
292
+ }
293
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2.5", children: [
294
+ /* @__PURE__ */ jsx("span", { className: "mr-auto text-[12.5px] font-bold tracking-wider text-muted-foreground uppercase", children: "Admin" }),
295
+ !tool.reviewed && /* @__PURE__ */ jsxs(
296
+ "button",
297
+ {
298
+ type: "button",
299
+ disabled: approve.isPending,
300
+ "aria-busy": approve.isPending || void 0,
301
+ onClick: () => approve.mutate({ slug }),
302
+ className: GHOST_BUTTON,
303
+ children: [
304
+ /* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-4" }),
305
+ approve.isPending ? "Approving…" : "Approve"
306
+ ]
307
+ }
308
+ ),
309
+ tool.lifecycle.state !== "archived" && /* @__PURE__ */ jsxs(
310
+ "button",
311
+ {
312
+ type: "button",
313
+ onClick: () => setArchiving(true),
314
+ className: GHOST_BUTTON,
315
+ children: [
316
+ /* @__PURE__ */ jsx(Archive, { "aria-hidden": "true", className: "size-4" }),
317
+ "Archive"
318
+ ]
319
+ }
320
+ )
321
+ ] }),
322
+ adminError && /* @__PURE__ */ jsx(
323
+ "p",
324
+ {
325
+ role: "alert",
326
+ className: "text-[13px] font-semibold text-destructive",
327
+ children: adminError.message
328
+ }
329
+ )
330
+ ]
331
+ }
332
+ )
333
+ ] }),
334
+ /* @__PURE__ */ jsx(
335
+ CardScreenshot,
336
+ {
337
+ tool,
338
+ capturing: tool.capturePending && !gaveUpOnCapture
339
+ }
340
+ ),
341
+ tool.descriptionMd.trim() && /* @__PURE__ */ jsxs(
342
+ "section",
343
+ {
344
+ "data-testid": "card-how-to-use",
345
+ className: cn(SECTION, "text-[14.5px]"),
346
+ children: [
347
+ /* @__PURE__ */ jsx("h3", { className: "mb-2 font-serif text-[17px] font-semibold", children: "How to use" }),
348
+ /* @__PURE__ */ jsx("div", { className: "prose prose-sm dark:prose-invert max-w-[68ch]", children: /* @__PURE__ */ jsx(MarkdownText, { children: tool.descriptionMd }) })
349
+ ]
350
+ }
351
+ ),
352
+ /* @__PURE__ */ jsxs("section", { "data-testid": "card-comments", className: SECTION, children: [
353
+ /* @__PURE__ */ jsxs("div", { className: "comments-head", children: [
354
+ /* @__PURE__ */ jsx("h2", { id: commentsHeadingId, className: "behind-group__title", children: "Comments" }),
355
+ commentCount > 0 && /* @__PURE__ */ jsx(
356
+ "span",
357
+ {
358
+ "data-testid": "card-comment-count",
359
+ className: "rounded-full bg-muted px-2 text-xs leading-5 font-bold text-muted-foreground",
360
+ children: commentCount
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsx(CommentsHelp, { appName: tool.name })
364
+ ] }),
365
+ /* @__PURE__ */ jsx(
366
+ CommentsFeed,
367
+ {
368
+ target: commentTarget,
369
+ claims: NO_CLAIMS,
370
+ labelledBy: commentsHeadingId
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsx("div", { className: "mt-3.5", children: /* @__PURE__ */ jsx(
374
+ CommunityInput,
375
+ {
376
+ ref: commentsRef,
377
+ target: commentTarget,
378
+ appName: tool.name
379
+ }
380
+ ) })
381
+ ] }),
382
+ /* @__PURE__ */ jsxs("p", { className: "mt-[22px] flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12.5px] font-semibold text-muted-foreground", children: [
383
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
384
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "Esc" }),
385
+ " close"
386
+ ] }),
387
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
388
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "←" }),
389
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "→" }),
390
+ " previous / next tool"
391
+ ] }),
392
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
393
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "Enter" }),
394
+ " open site",
395
+ /* @__PURE__ */ jsx("span", { className: "font-normal", children: "(in the comment box, Enter sends)" })
396
+ ] })
397
+ ] }),
398
+ editing && /* @__PURE__ */ jsx(
399
+ ToolFormDialog,
400
+ {
401
+ mode: "edit",
402
+ initial: tool,
403
+ onClose: () => setEditing(false),
404
+ onDone: () => setEditing(false)
405
+ }
406
+ )
407
+ ] }) : get.isError ? /* @__PURE__ */ jsx("p", { className: "py-10 text-center text-muted-foreground", children: "This tool is not listed any more." }) : /* @__PURE__ */ jsxs("div", { "aria-busy": "true", className: "flex flex-col gap-4", children: [
408
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[72px] w-2/3 rounded-[14px]" }),
409
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" }),
410
+ /* @__PURE__ */ jsx(Skeleton, { className: "aspect-[16/10] w-full rounded-[14px]" })
411
+ ] })
412
+ }
413
+ );
414
+ }
415
+ function LifecycleNote({
416
+ tool,
417
+ onTellOwner
418
+ }) {
419
+ const { lifecycle } = tool;
420
+ const [now] = useState(Date.now);
421
+ if (lifecycle.state === "not-working") {
422
+ const since = new Date(now - lifecycle.days * DAY_MS).toISOString();
423
+ return /* @__PURE__ */ jsxs(
424
+ "p",
425
+ {
426
+ "data-testid": "lifecycle-note",
427
+ className: "mt-3 rounded-[10px] bg-amber-50 px-3 py-[9px] text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200",
428
+ children: [
429
+ "The daily check has not reached this site since",
430
+ " ",
431
+ formatCardDate(since).replace(/ \d{4}$/, ""),
432
+ ". It may be down or moved.",
433
+ " ",
434
+ /* @__PURE__ */ jsx(
435
+ "button",
436
+ {
437
+ type: "button",
438
+ onClick: onTellOwner,
439
+ className: "font-bold underline underline-offset-2",
440
+ children: "Tell the owner"
441
+ }
442
+ )
443
+ ]
444
+ }
445
+ );
446
+ }
447
+ if (lifecycle.state === "archived") {
448
+ const on = formatCardDate(lifecycle.at);
449
+ return /* @__PURE__ */ jsxs(
450
+ "p",
451
+ {
452
+ "data-testid": "lifecycle-note",
453
+ className: "mt-3 rounded-[10px] bg-muted px-3 py-[9px] text-[13.5px] text-muted-foreground",
454
+ children: [
455
+ lifecycle.byName ? `Archived by ${lifecycle.byName} on ${on}${lifecycle.reason ? `: ${lifecycle.reason}` : ""}.` : `Archived automatically on ${on} after 30 days not working.`,
456
+ " ",
457
+ "Kept here so old links still land somewhere."
458
+ ]
459
+ }
460
+ );
461
+ }
462
+ return null;
463
+ }
464
+ export {
465
+ ToolCard
466
+ };
467
+ //# sourceMappingURL=ToolCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolCard.js","sources":["../../../../../src/modules/communityTools/ui/ToolCard.tsx"],"sourcesContent":["import type { Claim } from '@igstack/app-catalog-backend-core'\nimport type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n Archive,\n ArrowLeft,\n ArrowRight,\n Check,\n Eye,\n Pencil,\n} from 'lucide-react'\nimport { useEffect, useId, useRef, useState } from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport {\n DIALOG_ROUND_BUTTON,\n DetailDialogShell,\n} from '~/modules/appCatalog/ui/catalog/DetailDialogShell'\nimport { MarkdownText } from '~/modules/appCatalog/ui/components/MarkdownText'\nimport { CommentsFeed } from '~/modules/appCatalog/ui/detail/CommentsFeed'\nimport { CommentsHelp } from '~/modules/appCatalog/ui/detail/CommentsHelp'\nimport { CommunityInput } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport type { CommunityInputHandle } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport { cn } from '~/lib/utils'\nimport { useAuth } from '~/modules/auth'\nimport { Skeleton } from '~/ui/skeleton'\nimport { CardScreenshot, formatCardDate } from './CardScreenshot'\nimport { CardTitleBlock } from './CardTitleBlock'\nimport { PersonAvatar } from './FeedRow'\nimport { useTimeUp } from '../useTimeUp'\nimport { PinButton } from './PinButton'\nimport { TagChip, tagKey, tagLabel } from './TagRow'\nimport { ArchiveConfirm, GHOST_BUTTON, ToolFormDialog } from './ToolFormDialog'\n\nconst KBD =\n 'rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground'\n\nconst SECTION = 'mt-6 border-t border-border pt-[22px]'\n\nconst LABEL =\n 'text-[11.5px] font-bold tracking-wider text-muted-foreground uppercase'\n\nconst NO_CLAIMS: Claim[] = []\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/** How often a card whose first screenshot is on its way asks again. */\nconst CAPTURE_POLL_MS = 5000\n\n/** How long one open card waits for a first screenshot before it stops asking. */\nconst CAPTURE_WAIT_MS = 2 * 60 * 1000\n\nexport interface ToolCardProps {\n slug: string\n /** The list the card was opened from, in its order: ←/→ step through it. */\n siblings: string[]\n onClose: () => void\n onNavigate: (slug: string) => void\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: (tool: CommunityTool) => void\n /** Display names of official apps by slug, for app tags. */\n appNames: Record<string, string>\n /** A tag chip on the card: close it and filter the feed to that tag. */\n onChooseTag: (tag: string) => void\n}\n\n/** Typing belongs to the field: the card's keys stay out of it. */\nfunction isTyping(target: HTMLElement): boolean {\n return (\n target.isContentEditable ||\n target.closest('input, textarea, select') !== null\n )\n}\n\n/**\n * A community tool over the Community page, in the official card's shell: no\n * tabs, one scroll. Title and screenshot to recognise it, how to use it, who\n * to ask, and the comments.\n */\nexport function ToolCard({\n slug,\n siblings,\n onClose,\n onNavigate,\n pinned,\n pinBusy,\n onTogglePin,\n appNames,\n onChooseTag,\n}: ToolCardProps) {\n const { user } = useAuth()\n const isAdmin = user?.isAdmin === true\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n // The feed already holds every tool, so the card paints at once from it\n // and the tool's own query only refreshes it.\n const list = useQuery(trpc.communityTools.list.queryOptions())\n const fromList = list.data?.find((t) => t.slug === slug)\n // Whatever the server says, an open card stops waiting after a while: a\n // capture that never reports back must not keep it asking forever.\n const gaveUpOnCapture = useTimeUp(slug, CAPTURE_WAIT_MS)\n const get = useQuery({\n ...trpc.communityTools.get.queryOptions({ slug }),\n placeholderData: fromList,\n // Until the first visit lands, keep asking, so the screenshot replaces\n // the placeholder without a reload.\n refetchInterval: (query) =>\n query.state.data?.capturePending && !gaveUpOnCapture\n ? CAPTURE_POLL_MS\n : false,\n })\n const tool = get.data\n\n // One view per open. The ref keeps StrictMode's second effect from counting\n // twice; stepping to another tool is another open.\n const [counts, setCounts] = useState<{\n slug: string\n views30d: number\n viewsAll: number\n } | null>(null)\n // The hook's own onSuccess, not one passed to `mutate`: StrictMode's\n // remount detaches the observer a `mutate` callback hangs on, so that one\n // never fires and the count stayed one short.\n const { mutate: recordView } = useMutation(\n trpc.communityTools.recordView.mutationOptions({\n onSuccess: (c, { slug: viewed }) => {\n setCounts({ slug: viewed, ...c })\n },\n }),\n )\n const recordedForRef = useRef<string | null>(null)\n useEffect(() => {\n if (recordedForRef.current === slug) return\n recordedForRef.current = slug\n recordView({ slug })\n }, [slug, recordView])\n const views = counts?.slug === slug ? counts : tool\n\n const [editing, setEditing] = useState(false)\n const [archiving, setArchiving] = useState(false)\n const refreshTools = () =>\n queryClient.invalidateQueries({\n queryKey: trpc.communityTools.pathKey(),\n })\n const approve = useMutation(\n trpc.communityTools.approve.mutationOptions({ onSuccess: refreshTools }),\n )\n const archive = useMutation(\n trpc.communityTools.archive.mutationOptions({\n onSuccess: () => {\n setArchiving(false)\n return refreshTools()\n },\n }),\n )\n const adminError = approve.error ?? archive.error\n\n const commentsRef = useRef<CommunityInputHandle>(null)\n const commentsHeadingId = useId()\n const commentTarget = { communityToolSlug: slug }\n const comments = useQuery(trpc.feedback.list.queryOptions(commentTarget))\n const commentCount = comments.data?.items.length ?? 0\n\n const index = siblings.indexOf(slug)\n const prev = index > 0 ? siblings[index - 1] : undefined\n const next = index >= 0 ? siblings[index + 1] : undefined\n const nameOf = (s: string) => list.data?.find((t) => t.slug === s)?.name ?? s\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n // React bubbles through portals: keys in the screenshot viewer are not ours.\n const target = e.target as HTMLElement\n if (!e.currentTarget.contains(target)) return\n if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return\n if (isTyping(target)) return\n if (e.key === 'ArrowLeft' && prev) {\n e.preventDefault()\n onNavigate(prev)\n } else if (e.key === 'ArrowRight' && next) {\n e.preventDefault()\n onNavigate(next)\n } else if (e.key === 'Enter' && tool) {\n // A link or button answers Enter itself; the title block is the link\n // that opens the site.\n if (target.closest('a, button')) return\n e.preventDefault()\n window.open(tool.url, '_blank', 'noopener,noreferrer')\n }\n }\n\n const controls = index >= 0 && siblings.length > 1 && (\n <>\n <span className=\"flex items-center gap-1 text-[13px] font-semibold text-muted-foreground\">\n <button\n type=\"button\"\n aria-label={prev ? `Previous tool: ${nameOf(prev)}` : 'Previous tool'}\n disabled={!prev}\n onClick={() => prev && onNavigate(prev)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n <span data-testid=\"card-position\">\n {index + 1} of {siblings.length}\n </span>\n <button\n type=\"button\"\n aria-label={next ? `Next tool: ${nameOf(next)}` : 'Next tool'}\n disabled={!next}\n onClick={() => next && onNavigate(next)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowRight className=\"size-4\" />\n </button>\n </span>\n <i aria-hidden=\"true\" className=\"mx-1 h-5 w-px bg-border\" />\n </>\n )\n\n return (\n <DetailDialogShell\n label={`${tool?.name ?? 'Community tool'} details`}\n shownKey={slug}\n maxWidthClass=\"max-w-[min(920px,94vw)]\"\n focusSelector='[data-testid=\"card-title-block\"]'\n controls={controls}\n testId=\"tool-card\"\n onClose={onClose}\n onKeyDown={onKeyDown}\n >\n {tool ? (\n <>\n <div className=\"border-b border-border pb-[22px]\">\n <div className=\"flex items-end justify-between gap-4 pr-44\">\n <CardTitleBlock tool={tool} />\n <div className=\"-mr-40 flex shrink-0 items-center gap-2\">\n {tool.canEdit && (\n <button\n type=\"button\"\n data-testid=\"card-edit-button\"\n onClick={() => setEditing(true)}\n className={cn(GHOST_BUTTON, 'h-10 px-3.5 text-[13.5px]')}\n >\n <Pencil aria-hidden=\"true\" className=\"size-4\" />\n Edit\n </button>\n )}\n <PinButton\n labelled\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={() => onTogglePin(tool)}\n />\n </div>\n </div>\n <div\n className={cn(\n tool.lifecycle.state === 'archived' && 'opacity-60',\n )}\n >\n {tool.summary && (\n <p className=\"mt-3.5 text-[15px] text-foreground/80\">\n {tool.summary}\n </p>\n )}\n {tool.tags.length > 0 && (\n <div\n data-testid=\"card-tags\"\n className=\"mt-2.5 flex flex-wrap gap-1.5\"\n >\n {tool.tags.map((tag) => (\n <TagChip\n key={tagKey(tag)}\n label={tagLabel(tag, appNames)}\n onClick={() => onChooseTag(tagKey(tag))}\n />\n ))}\n </div>\n )}\n {views && (\n <p\n data-testid=\"card-views\"\n className=\"mt-2.5 flex gap-[18px] text-[13px] font-semibold text-muted-foreground\"\n >\n <span className=\"inline-flex items-center gap-1.5\">\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n <b className=\"text-foreground\">\n {views.views30d.toLocaleString('en-US')}\n </b>{' '}\n opens this month\n </span>\n <span>\n <b className=\"text-foreground\">\n {views.viewsAll.toLocaleString('en-US')}\n </b>{' '}\n all time\n </span>\n </p>\n )}\n {/* Who to ask, up here with the facts: below the screenshot\n it fell under the fold. */}\n <p\n data-testid=\"card-people\"\n className=\"mt-2.5 flex flex-wrap items-center gap-x-[18px] gap-y-1.5 text-[13px] font-semibold text-muted-foreground\"\n >\n {tool.owners.length > 0 && (\n <span className=\"inline-flex flex-wrap items-center gap-x-3 gap-y-1.5\">\n <span className={LABEL}>Owners</span>\n {tool.owners.map((owner) => (\n <span\n key={owner.slug ?? `named:${owner.name}`}\n className=\"inline-flex items-center gap-1.5 text-foreground\"\n >\n <PersonAvatar name={owner.name} />\n <span data-testid=\"card-owner\">{owner.name}</span>\n </span>\n ))}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className={LABEL}>Added by</span>\n <span className=\"text-foreground\">{tool.addedBy.name}</span>\n <span className=\"font-normal\">\n {formatCardDate(tool.createdAt)}\n </span>\n </span>\n </p>\n </div>\n <LifecycleNote\n tool={tool}\n onTellOwner={() => commentsRef.current?.focus()}\n />\n {isAdmin && (\n <div\n data-testid=\"card-admin\"\n className=\"mt-3.5 flex flex-col gap-2.5 rounded-[10px] border border-dashed border-border px-3 py-2.5\"\n >\n {archiving ? (\n <ArchiveConfirm\n busy={archive.isPending}\n onCancel={() => setArchiving(false)}\n onArchive={(reason) => archive.mutate({ slug, reason })}\n />\n ) : (\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <span className=\"mr-auto text-[12.5px] font-bold tracking-wider text-muted-foreground uppercase\">\n Admin\n </span>\n {!tool.reviewed && (\n <button\n type=\"button\"\n disabled={approve.isPending}\n aria-busy={approve.isPending || undefined}\n onClick={() => approve.mutate({ slug })}\n className={GHOST_BUTTON}\n >\n <Check aria-hidden=\"true\" className=\"size-4\" />\n {approve.isPending ? 'Approving…' : 'Approve'}\n </button>\n )}\n {tool.lifecycle.state !== 'archived' && (\n <button\n type=\"button\"\n onClick={() => setArchiving(true)}\n className={GHOST_BUTTON}\n >\n <Archive aria-hidden=\"true\" className=\"size-4\" />\n Archive\n </button>\n )}\n </div>\n )}\n {adminError && (\n <p\n role=\"alert\"\n className=\"text-[13px] font-semibold text-destructive\"\n >\n {adminError.message}\n </p>\n )}\n </div>\n )}\n </div>\n\n <CardScreenshot\n tool={tool}\n capturing={tool.capturePending && !gaveUpOnCapture}\n />\n\n {tool.descriptionMd.trim() && (\n <section\n data-testid=\"card-how-to-use\"\n className={cn(SECTION, 'text-[14.5px]')}\n >\n <h3 className=\"mb-2 font-serif text-[17px] font-semibold\">\n How to use\n </h3>\n <div className=\"prose prose-sm dark:prose-invert max-w-[68ch]\">\n <MarkdownText>{tool.descriptionMd}</MarkdownText>\n </div>\n </section>\n )}\n\n {/* The card's own order: what was said first, the input last. The\n official card's CommunitySection puts the input on top. */}\n <section data-testid=\"card-comments\" className={SECTION}>\n <div className=\"comments-head\">\n <h2 id={commentsHeadingId} className=\"behind-group__title\">\n Comments\n </h2>\n {commentCount > 0 && (\n <span\n data-testid=\"card-comment-count\"\n className=\"rounded-full bg-muted px-2 text-xs leading-5 font-bold text-muted-foreground\"\n >\n {commentCount}\n </span>\n )}\n <CommentsHelp appName={tool.name} />\n </div>\n <CommentsFeed\n target={commentTarget}\n claims={NO_CLAIMS}\n labelledBy={commentsHeadingId}\n />\n <div className=\"mt-3.5\">\n <CommunityInput\n ref={commentsRef}\n target={commentTarget}\n appName={tool.name}\n />\n </div>\n </section>\n\n <p className=\"mt-[22px] flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Esc</kbd> close\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>←</kbd>\n <kbd className={KBD}>→</kbd> previous / next tool\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Enter</kbd> open site\n <span className=\"font-normal\">\n (in the comment box, Enter sends)\n </span>\n </span>\n </p>\n {editing && (\n <ToolFormDialog\n mode=\"edit\"\n initial={tool}\n onClose={() => setEditing(false)}\n onDone={() => setEditing(false)}\n />\n )}\n </>\n ) : get.isError ? (\n <p className=\"py-10 text-center text-muted-foreground\">\n This tool is not listed any more.\n </p>\n ) : (\n <div aria-busy=\"true\" className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-[72px] w-2/3 rounded-[14px]\" />\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"aspect-[16/10] w-full rounded-[14px]\" />\n </div>\n )}\n </DetailDialogShell>\n )\n}\n\n/** Why a tool that is not active is shown the way it is. */\nfunction LifecycleNote({\n tool,\n onTellOwner,\n}: {\n tool: CommunityTool\n onTellOwner: () => void\n}) {\n const { lifecycle } = tool\n const [now] = useState(Date.now)\n if (lifecycle.state === 'not-working') {\n const since = new Date(now - lifecycle.days * DAY_MS).toISOString()\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-amber-50 px-3 py-[9px] text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200\"\n >\n The daily check has not reached this site since{' '}\n {formatCardDate(since).replace(/ \\d{4}$/, '')}. It may be down or moved.{' '}\n <button\n type=\"button\"\n onClick={onTellOwner}\n className=\"font-bold underline underline-offset-2\"\n >\n Tell the owner\n </button>\n </p>\n )\n }\n if (lifecycle.state === 'archived') {\n const on = formatCardDate(lifecycle.at)\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-muted px-3 py-[9px] text-[13.5px] text-muted-foreground\"\n >\n {lifecycle.byName\n ? `Archived by ${lifecycle.byName} on ${on}${lifecycle.reason ? `: ${lifecycle.reason}` : ''}.`\n : `Archived automatically on ${on} after 30 days not working.`}{' '}\n Kept here so old links still land somewhere.\n </p>\n )\n }\n return null\n}\n"],"names":["_a","_b"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { CommunityTool } from '@igstack/app-catalog-shared-core';
2
+ export declare const GHOST_BUTTON = "inline-flex h-[34px] items-center gap-[7px] rounded-[10px] border border-border bg-card px-[11px] text-[13px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-50";
3
+ export declare const PRIMARY_BUTTON = "inline-flex h-[34px] items-center gap-2 rounded-[10px] bg-primary px-4 text-[13.5px] font-extrabold whitespace-nowrap text-primary-foreground shadow-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-not-allowed disabled:opacity-50";
4
+ export interface ToolFormDialogProps {
5
+ mode: 'add' | 'edit';
6
+ /** The tool being edited. */
7
+ initial?: CommunityTool;
8
+ /** What the visitor searched for: a first guess at the name. */
9
+ prefillName?: string;
10
+ /** Published, saved or archived. */
11
+ onDone: (slug: string) => void;
12
+ onClose: () => void;
13
+ /** The address is already listed, and they chose to look at that card. */
14
+ onOpenCard?: (slug: string) => void;
15
+ }
16
+ /**
17
+ * Add a tool, or edit one: the same form. The address comes first; once it
18
+ * sits still the backend reads the page, the title fills the name and a
19
+ * screenshot preview shows. A host already listed is caught before anything
20
+ * else is asked. Publish makes the tool live at once and puts it in the
21
+ * review list.
22
+ */
23
+ export declare function ToolFormDialog({ mode, initial, prefillName, onDone, onClose, onOpenCard, }: ToolFormDialogProps): import("react/jsx-runtime").JSX.Element;
24
+ /**
25
+ * Archiving asks why, in one step: the reason is what colleagues read on the
26
+ * card, and what the review list records.
27
+ */
28
+ export declare function ArchiveConfirm({ busy, onArchive, onCancel, }: {
29
+ busy: boolean;
30
+ onArchive: (reason: string) => void;
31
+ onCancel: () => void;
32
+ }): import("react/jsx-runtime").JSX.Element;