@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,547 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { rankTools, searchTools } from "@igstack/app-catalog-shared-core";
3
+ import { useNavigate, useLocation } from "@tanstack/react-router";
4
+ import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { Plus, Star, AlertTriangle, RefreshCw, Search } from "lucide-react";
6
+ import { useState, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from "react";
7
+ import { useAuthModal } from "../../auth/AuthModalContext.js";
8
+ import { Skeleton } from "../../../ui/skeleton.js";
9
+ import { useCommunityTools } from "../useCommunityTools.js";
10
+ import { FeedRow, ROW_THUMB } from "./FeedRow.js";
11
+ import { feedHeading, FeedSortControl } from "./FeedSortControl.js";
12
+ import { MarginPreview } from "./MarginPreview.js";
13
+ import { MyToolsShelf } from "./MyToolsShelf.js";
14
+ import { TagRow } from "./TagRow.js";
15
+ import { Toast } from "./Toast.js";
16
+ import { ToolCard } from "./ToolCard.js";
17
+ import { ToolFormDialog } from "./ToolFormDialog.js";
18
+ import { ToolSearch } from "./ToolSearch.js";
19
+ const KBD = "rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground";
20
+ const RESULTS_ID = "community-results-grid";
21
+ function optionIdOf(slug) {
22
+ return `community-option-${slug}`;
23
+ }
24
+ function openSite(url) {
25
+ window.open(url, "_blank", "noopener,noreferrer");
26
+ }
27
+ function CommunityPage({
28
+ tag,
29
+ sort,
30
+ dir,
31
+ onTagChange,
32
+ onSortChange,
33
+ cardSlug
34
+ }) {
35
+ var _a;
36
+ const {
37
+ list,
38
+ pins,
39
+ appNames,
40
+ isAuthenticated,
41
+ isAuthLoading,
42
+ pin,
43
+ unpin,
44
+ pinBusySlug,
45
+ reorder,
46
+ like,
47
+ unlike,
48
+ withMyLike
49
+ } = useCommunityTools();
50
+ const { open: openSignIn } = useAuthModal();
51
+ const navigate = useNavigate();
52
+ const here = useLocation({ select: (l) => l.href });
53
+ const signInHere = () => openSignIn(here);
54
+ const [query, setQuery] = useState("");
55
+ const [selected, setSelected] = useState(0);
56
+ const tokens = useMemo(
57
+ () => query.trim().toLowerCase().split(/\s+/).filter(Boolean),
58
+ [query]
59
+ );
60
+ const searching = tokens.length > 0;
61
+ const [toast, setToast] = useState(null);
62
+ const toastIdRef = useRef(0);
63
+ const showToast = useCallback((message) => {
64
+ toastIdRef.current += 1;
65
+ setToast({ ...message, id: toastIdRef.current });
66
+ }, []);
67
+ const dismissToast = useCallback(() => setToast(null), []);
68
+ const tools = useMemo(() => list.data ?? [], [list.data]);
69
+ const withTag = useCallback(
70
+ (all) => tag ? all.filter((t) => t.tags.some((g) => `${g.kind}:${g.value}` === tag)) : all,
71
+ [tag]
72
+ );
73
+ const feed = useMemo(
74
+ () => withTag(rankTools(tools, sort, dir)).map((tool) => ({ tool })),
75
+ [tools, sort, dir, withTag]
76
+ );
77
+ const results = useMemo(
78
+ () => searching ? searchTools(withTag(tools), query, appNames, sort, dir).map(
79
+ ({ tool, fields }) => ({ tool, matched: fields })
80
+ ) : [],
81
+ [searching, tools, query, appNames, sort, dir, withTag]
82
+ );
83
+ const items = searching ? results : feed;
84
+ const selectedIndex = Math.min(selected, results.length - 1);
85
+ const selectedTool = (_a = results[selectedIndex]) == null ? void 0 : _a.tool;
86
+ const activeOptionId = searching && selectedTool ? optionIdOf(selectedTool.slug) : void 0;
87
+ const pinnedSlugs = useMemo(
88
+ () => new Set((pins.data ?? []).map((t) => t.slug)),
89
+ [pins.data]
90
+ );
91
+ const pinError = (retry) => () => showToast({
92
+ tone: "error",
93
+ text: "Could not save the pin. Check your connection.",
94
+ action: { label: "Try again", run: retry }
95
+ });
96
+ const pinTool = (slug) => pin.mutate(
97
+ { slug },
98
+ {
99
+ onSuccess: () => showToast({
100
+ tone: "info",
101
+ text: "Pinned to My tools",
102
+ action: { label: "Undo", run: () => unpinTool(slug, false) }
103
+ }),
104
+ onError: pinError(() => pinTool(slug))
105
+ }
106
+ );
107
+ const unpinTool = (slug, offerUndo = true) => unpin.mutate(
108
+ { slug },
109
+ {
110
+ onSuccess: () => {
111
+ if (offerUndo) {
112
+ showToast({
113
+ tone: "info",
114
+ text: "Removed from My tools",
115
+ action: { label: "Undo", run: () => pinTool(slug) }
116
+ });
117
+ }
118
+ },
119
+ onError: pinError(() => unpinTool(slug, offerUndo))
120
+ }
121
+ );
122
+ const togglePin = (tool) => {
123
+ if (!isAuthenticated) {
124
+ signInHere();
125
+ return;
126
+ }
127
+ if (pinnedSlugs.has(tool.slug)) unpinTool(tool.slug);
128
+ else pinTool(tool.slug);
129
+ };
130
+ const toggleLike = (tool) => {
131
+ if (!isAuthenticated) {
132
+ signInHere();
133
+ return;
134
+ }
135
+ const save = tool.likedByMe ? unlike : like;
136
+ save.mutate(
137
+ { slug: tool.slug },
138
+ {
139
+ onError: () => showToast({
140
+ tone: "error",
141
+ text: "Could not save the like. Check your connection.",
142
+ action: { label: "Try again", run: () => toggleLike(tool) }
143
+ })
144
+ }
145
+ );
146
+ };
147
+ const reorderPins = (slugs) => reorder.mutate(
148
+ { slugs },
149
+ {
150
+ onError: () => showToast({
151
+ tone: "error",
152
+ text: "Couldn't save the new order",
153
+ action: { label: "Try again", run: () => reorderPins(slugs) }
154
+ })
155
+ }
156
+ );
157
+ const [adding, setAdding] = useState(null);
158
+ const startAdd = (prefillName) => {
159
+ if (!isAuthenticated) signInHere();
160
+ else setAdding({ prefillName });
161
+ };
162
+ const openCard = (slug, replace = false) => navigate({
163
+ to: "/community/$slug",
164
+ params: { slug },
165
+ search: (prev) => prev,
166
+ replace
167
+ });
168
+ const closeCard = () => void navigate({ to: "/community", search: (prev) => prev });
169
+ const scrollRef = useRef(null);
170
+ const pageRef = useRef(null);
171
+ const [listEl, setListEl] = useState(null);
172
+ const [scrollMargin, setScrollMargin] = useState(0);
173
+ useEffect(() => {
174
+ const scroller = scrollRef.current;
175
+ const page = pageRef.current;
176
+ if (!scroller || !page || !listEl) return;
177
+ const measure = () => setScrollMargin(
178
+ listEl.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop
179
+ );
180
+ const observer = new ResizeObserver(measure);
181
+ observer.observe(page);
182
+ measure();
183
+ return () => observer.disconnect();
184
+ }, [listEl]);
185
+ const [hasLayout, setHasLayout] = useState(true);
186
+ useLayoutEffect(() => {
187
+ var _a2;
188
+ setHasLayout((((_a2 = scrollRef.current) == null ? void 0 : _a2.clientHeight) ?? 0) > 0);
189
+ }, []);
190
+ const getItemKey = useCallback(
191
+ (i) => {
192
+ var _a2;
193
+ return ((_a2 = items[i]) == null ? void 0 : _a2.tool.slug) ?? i;
194
+ },
195
+ [items]
196
+ );
197
+ const virtualizer = useVirtualizer({
198
+ getScrollElement: () => scrollRef.current,
199
+ // The column's height before it is measured, so the first paint already
200
+ // holds the rows that will be visible.
201
+ initialRect: { width: 0, height: 800 },
202
+ count: items.length,
203
+ // The thumbnail sets the row: 150 px, 12 px padding either side, borders.
204
+ estimateSize: () => 176,
205
+ overscan: 6,
206
+ gap: 8,
207
+ scrollMargin,
208
+ getItemKey
209
+ });
210
+ useEffect(() => {
211
+ if (!searching || selectedIndex < 0) return;
212
+ const row = listEl == null ? void 0 : listEl.querySelector(`[data-index="${selectedIndex}"]`);
213
+ if (row) row.scrollIntoView({ block: "nearest" });
214
+ else virtualizer.scrollToIndex(selectedIndex, { align: "auto" });
215
+ }, [searching, selectedIndex, virtualizer, listEl]);
216
+ const onSearchKey = (e) => {
217
+ const current = results[selectedIndex];
218
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
219
+ if (results.length === 0) return;
220
+ e.preventDefault();
221
+ const step = e.key === "ArrowDown" ? 1 : -1;
222
+ setSelected(
223
+ Math.min(results.length - 1, Math.max(0, selectedIndex + step))
224
+ );
225
+ } else if (e.key === "Enter" && current) {
226
+ e.preventDefault();
227
+ openSite(current.tool.url);
228
+ } else if (e.key === "ArrowRight" && current && e.currentTarget.selectionStart === e.currentTarget.value.length) {
229
+ e.preventDefault();
230
+ void openCard(current.tool.slug);
231
+ }
232
+ };
233
+ const virtualItems = hasLayout ? virtualizer.getVirtualItems() : items.map((_, index) => ({
234
+ key: items[index].tool.slug,
235
+ index,
236
+ start: virtualizer.options.scrollMargin
237
+ }));
238
+ return /* @__PURE__ */ jsx(
239
+ "div",
240
+ {
241
+ ref: scrollRef,
242
+ "data-preview-scroller": true,
243
+ className: "min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]",
244
+ children: /* @__PURE__ */ jsxs(
245
+ "div",
246
+ {
247
+ ref: pageRef,
248
+ "data-preview-column": true,
249
+ className: "mx-auto w-full max-w-[1000px] pt-1.5 pb-10",
250
+ children: [
251
+ /* @__PURE__ */ jsxs("div", { className: "mb-[22px] flex items-center gap-2.5", children: [
252
+ /* @__PURE__ */ jsx(
253
+ ToolSearch,
254
+ {
255
+ value: query,
256
+ onChange: (value) => {
257
+ setQuery(value);
258
+ setSelected(0);
259
+ },
260
+ resultCount: results.length,
261
+ resultsId: searching && results.length > 0 ? RESULTS_ID : void 0,
262
+ activeDescendant: activeOptionId,
263
+ onKey: onSearchKey
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsxs(
267
+ "button",
268
+ {
269
+ type: "button",
270
+ onClick: () => startAdd(),
271
+ className: "inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring",
272
+ children: [
273
+ /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true", className: "size-4" }),
274
+ "Add a tool"
275
+ ]
276
+ }
277
+ )
278
+ ] }),
279
+ searching ? /* @__PURE__ */ jsxs("div", { className: "-mt-2 mb-3.5 ml-1 flex items-center gap-2 text-[13px] font-semibold text-muted-foreground", children: [
280
+ /* @__PURE__ */ jsx(
281
+ Star,
282
+ {
283
+ "aria-hidden": "true",
284
+ className: "size-4 fill-amber-400 text-amber-600"
285
+ }
286
+ ),
287
+ "My tools is hidden while you search"
288
+ ] }) : /* @__PURE__ */ jsx("div", { className: "mb-[26px]", children: /* @__PURE__ */ jsx(
289
+ MyToolsShelf,
290
+ {
291
+ pins: pins.data ?? [],
292
+ isLoading: isAuthLoading || isAuthenticated && pins.isPending,
293
+ onUnpin: (tool) => togglePin(tool),
294
+ onReorder: reorderPins
295
+ }
296
+ ) }),
297
+ /* @__PURE__ */ jsxs(
298
+ "section",
299
+ {
300
+ "aria-label": searching ? "Search results" : feedHeading(sort, dir),
301
+ "data-testid": searching ? "community-results" : "community-feed",
302
+ children: [
303
+ !searching && /* @__PURE__ */ jsxs(Fragment, { children: [
304
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
305
+ /* @__PURE__ */ jsx("h2", { className: "font-serif text-[19px] font-semibold", children: feedHeading(sort, dir) }),
306
+ /* @__PURE__ */ jsx(
307
+ FeedSortControl,
308
+ {
309
+ sort,
310
+ dir,
311
+ onChange: onSortChange
312
+ }
313
+ )
314
+ ] }),
315
+ /* @__PURE__ */ jsx(
316
+ TagRow,
317
+ {
318
+ tools: rankTools(tools, sort, dir),
319
+ activeTag: tag,
320
+ appNames,
321
+ onChoose: onTagChange
322
+ }
323
+ )
324
+ ] }),
325
+ list.isPending ? /* @__PURE__ */ jsx(FeedSkeleton, {}) : list.isError ? /* @__PURE__ */ jsx(FeedError, { onRetry: () => void list.refetch() }) : searching && results.length === 0 ? /* @__PURE__ */ jsx(
326
+ NoResults,
327
+ {
328
+ query: query.trim(),
329
+ onAdd: () => startAdd(query.trim())
330
+ }
331
+ ) : /* @__PURE__ */ jsx(
332
+ "div",
333
+ {
334
+ ref: setListEl,
335
+ id: searching ? RESULTS_ID : void 0,
336
+ role: searching ? "grid" : void 0,
337
+ "aria-label": searching ? "Search results" : void 0,
338
+ "aria-activedescendant": activeOptionId,
339
+ className: hasLayout ? "relative" : "flex flex-col gap-2",
340
+ style: hasLayout ? { height: virtualizer.getTotalSize() } : void 0,
341
+ children: virtualItems.map((row) => {
342
+ const item = items[row.index];
343
+ if (!item) return null;
344
+ return /* @__PURE__ */ jsx(
345
+ "div",
346
+ {
347
+ "data-index": row.index,
348
+ ref: virtualizer.measureElement,
349
+ className: hasLayout ? "absolute inset-x-0 top-0" : void 0,
350
+ style: hasLayout ? {
351
+ transform: `translateY(${row.start - virtualizer.options.scrollMargin}px)`
352
+ } : void 0,
353
+ children: /* @__PURE__ */ jsx(
354
+ FeedRow,
355
+ {
356
+ tool: withMyLike(item.tool),
357
+ rank: searching ? void 0 : row.index + 1,
358
+ sort,
359
+ tokens,
360
+ matched: item.matched,
361
+ selected: searching && row.index === selectedIndex,
362
+ optionId: searching ? optionIdOf(item.tool.slug) : void 0,
363
+ pinned: pinnedSlugs.has(item.tool.slug),
364
+ pinBusy: pinBusySlug === item.tool.slug,
365
+ onTogglePin: () => togglePin(item.tool),
366
+ onToggleLike: () => toggleLike(withMyLike(item.tool))
367
+ }
368
+ )
369
+ },
370
+ row.key
371
+ );
372
+ })
373
+ }
374
+ ),
375
+ searching && results.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3.5 flex items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground", children: [
376
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "↑" }),
377
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "↓" }),
378
+ /* @__PURE__ */ jsx("span", { className: "mr-2.5", children: "move" }),
379
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "Enter" }),
380
+ /* @__PURE__ */ jsx("span", { className: "mr-2.5", children: "open site" }),
381
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "→" }),
382
+ /* @__PURE__ */ jsx("span", { className: "mr-2.5", children: "open card" }),
383
+ /* @__PURE__ */ jsx("kbd", { className: KBD, children: "Esc" }),
384
+ /* @__PURE__ */ jsx("span", { children: "clear" })
385
+ ] })
386
+ ]
387
+ }
388
+ ),
389
+ /* @__PURE__ */ jsx(
390
+ MarginPreview,
391
+ {
392
+ list: listEl,
393
+ tools,
394
+ disabled: !!cardSlug || !!adding
395
+ }
396
+ ),
397
+ cardSlug && /* @__PURE__ */ jsx(
398
+ ToolCard,
399
+ {
400
+ slug: cardSlug,
401
+ siblings: items.map((item) => item.tool.slug),
402
+ onClose: closeCard,
403
+ onNavigate: (slug) => void openCard(slug, true),
404
+ pinned: pinnedSlugs.has(cardSlug),
405
+ pinBusy: pinBusySlug === cardSlug,
406
+ onTogglePin: togglePin,
407
+ appNames,
408
+ onChooseTag: (chosen) => void navigate({
409
+ to: "/community",
410
+ search: (prev) => ({ ...prev, tag: chosen })
411
+ })
412
+ }
413
+ ),
414
+ adding && /* @__PURE__ */ jsx(
415
+ ToolFormDialog,
416
+ {
417
+ mode: "add",
418
+ prefillName: adding.prefillName,
419
+ onClose: () => setAdding(null),
420
+ onOpenCard: (slug) => {
421
+ setAdding(null);
422
+ void openCard(slug);
423
+ },
424
+ onDone: (slug) => {
425
+ setAdding(null);
426
+ showToast({
427
+ tone: "ok",
428
+ text: /* @__PURE__ */ jsxs(Fragment, { children: [
429
+ /* @__PURE__ */ jsx("b", { children: "Live now" }),
430
+ ", in the review list"
431
+ ] }),
432
+ action: { label: "Open card", run: () => void openCard(slug) }
433
+ });
434
+ }
435
+ }
436
+ ),
437
+ /* @__PURE__ */ jsx(Toast, { toast, onDismiss: dismissToast })
438
+ ]
439
+ }
440
+ )
441
+ }
442
+ );
443
+ }
444
+ function FeedSkeleton() {
445
+ return /* @__PURE__ */ jsx(
446
+ "div",
447
+ {
448
+ "data-testid": "feed-skeleton",
449
+ "aria-busy": "true",
450
+ className: "flex flex-col gap-2",
451
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs(
452
+ "div",
453
+ {
454
+ className: "flex min-h-[176px] items-center gap-4 rounded-[14px] border border-border bg-card py-3 pl-3",
455
+ children: [
456
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[22px] w-[26px]" }),
457
+ /* @__PURE__ */ jsx(Skeleton, { className: ROW_THUMB }),
458
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [
459
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-2/5" }),
460
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-4/5" }),
461
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-1/4" })
462
+ ] }),
463
+ /* @__PURE__ */ jsx("div", { className: "w-[120px] shrink-0" })
464
+ ]
465
+ },
466
+ i
467
+ ))
468
+ }
469
+ );
470
+ }
471
+ function FeedError({ onRetry }) {
472
+ return /* @__PURE__ */ jsxs(
473
+ "div",
474
+ {
475
+ "data-testid": "feed-error",
476
+ role: "alert",
477
+ className: "flex items-center gap-3.5 rounded-[14px] border border-destructive/30 bg-destructive/5 px-5 py-[18px]",
478
+ children: [
479
+ /* @__PURE__ */ jsx(
480
+ AlertTriangle,
481
+ {
482
+ "aria-hidden": "true",
483
+ className: "size-[22px] shrink-0 text-destructive"
484
+ }
485
+ ),
486
+ /* @__PURE__ */ jsxs("div", { children: [
487
+ /* @__PURE__ */ jsx("b", { className: "font-bold", children: "Could not load community tools." }),
488
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-muted-foreground", children: "The server did not answer. Your pinned tools above still work." })
489
+ ] }),
490
+ /* @__PURE__ */ jsxs(
491
+ "button",
492
+ {
493
+ type: "button",
494
+ onClick: onRetry,
495
+ className: "ml-auto inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring",
496
+ children: [
497
+ /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true", className: "size-4" }),
498
+ "Try again"
499
+ ]
500
+ }
501
+ )
502
+ ]
503
+ }
504
+ );
505
+ }
506
+ function NoResults({ query, onAdd }) {
507
+ return /* @__PURE__ */ jsxs(
508
+ "div",
509
+ {
510
+ "data-testid": "no-results",
511
+ className: "flex gap-3.5 rounded-[14px] border border-border bg-card px-5 py-[18px]",
512
+ children: [
513
+ /* @__PURE__ */ jsx(
514
+ Search,
515
+ {
516
+ "aria-hidden": "true",
517
+ className: "mt-0.5 size-5 shrink-0 text-muted-foreground"
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsxs("div", { children: [
521
+ /* @__PURE__ */ jsxs("b", { className: "font-bold", children: [
522
+ "Nothing matches “",
523
+ query,
524
+ "”."
525
+ ] }),
526
+ /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-muted-foreground", children: [
527
+ "Try a word from the page itself, or the name of the person who shared it. Know the tool?",
528
+ " ",
529
+ /* @__PURE__ */ jsx(
530
+ "button",
531
+ {
532
+ type: "button",
533
+ onClick: onAdd,
534
+ className: "font-bold text-primary hover:underline focus-visible:outline-2 focus-visible:outline-ring",
535
+ children: "Add it"
536
+ }
537
+ )
538
+ ] })
539
+ ] })
540
+ ]
541
+ }
542
+ );
543
+ }
544
+ export {
545
+ CommunityPage
546
+ };
547
+ //# sourceMappingURL=CommunityPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommunityPage.js","sources":["../../../../../src/modules/communityTools/ui/CommunityPage.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n SortDir,\n} from '@igstack/app-catalog-shared-core'\nimport { rankTools, searchTools } from '@igstack/app-catalog-shared-core'\nimport { useLocation, useNavigate } from '@tanstack/react-router'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { AlertTriangle, Plus, RefreshCw, Search, Star } from 'lucide-react'\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Skeleton } from '~/ui/skeleton'\nimport { useCommunityTools } from '../useCommunityTools'\nimport { FeedRow, ROW_THUMB } from './FeedRow'\nimport { FeedSortControl, feedHeading } from './FeedSortControl'\nimport { MarginPreview } from './MarginPreview'\nimport { MyToolsShelf } from './MyToolsShelf'\nimport { TagRow } from './TagRow'\nimport { Toast } from './Toast'\nimport type { ToastMessage } from './Toast'\nimport { ToolCard } from './ToolCard'\nimport { ToolFormDialog } from './ToolFormDialog'\nimport { ToolSearch } from './ToolSearch'\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\ninterface ListItem {\n tool: CommunityTool\n matched?: MatchField[]\n}\n\nexport interface CommunityPageProps {\n /** `?tag=`: `<kind>:<value>`, or none for the whole feed. */\n tag: string | undefined\n sort: FeedSort\n dir: SortDir\n onTagChange: (tag: string | undefined) => void\n onSortChange: (sort: FeedSort, dir: SortDir) => void\n /** The tool whose card is open over the page (`/community/<slug>`). */\n cardSlug?: string\n}\n\nconst RESULTS_ID = 'community-results-grid'\n\n/** Stable per tool, so the active descendant survives re-renders and re-ranking. */\nfunction optionIdOf(slug: string): string {\n return `community-option-${slug}`\n}\n\nfunction openSite(url: string) {\n window.open(url, '_blank', 'noopener,noreferrer')\n}\n\nexport function CommunityPage({\n tag,\n sort,\n dir,\n onTagChange,\n onSortChange,\n cardSlug,\n}: CommunityPageProps) {\n const {\n list,\n pins,\n appNames,\n isAuthenticated,\n isAuthLoading,\n pin,\n unpin,\n pinBusySlug,\n reorder,\n like,\n unlike,\n withMyLike,\n } = useCommunityTools()\n const { open: openSignIn } = useAuthModal()\n const navigate = useNavigate()\n // Signing in comes back here: this page, or the card open over it. The\n // router's own location, so it holds under any history (and in tests).\n const here = useLocation({ select: (l) => l.href })\n const signInHere = () => openSignIn(here)\n\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState(0)\n const tokens = useMemo(\n () => query.trim().toLowerCase().split(/\\s+/).filter(Boolean),\n [query],\n )\n const searching = tokens.length > 0\n\n const [toast, setToast] = useState<ToastMessage | null>(null)\n const toastIdRef = useRef(0)\n const showToast = useCallback((message: Omit<ToastMessage, 'id'>) => {\n toastIdRef.current += 1\n setToast({ ...message, id: toastIdRef.current })\n }, [])\n const dismissToast = useCallback(() => setToast(null), [])\n\n const tools = useMemo(() => list.data ?? [], [list.data])\n const withTag = useCallback(\n (all: CommunityTool[]) =>\n tag\n ? all.filter((t) => t.tags.some((g) => `${g.kind}:${g.value}` === tag))\n : all,\n [tag],\n )\n\n const feed = useMemo<ListItem[]>(\n () => withTag(rankTools(tools, sort, dir)).map((tool) => ({ tool })),\n [tools, sort, dir, withTag],\n )\n\n // Strongest match first; archived tools still found, after the rest.\n const results = useMemo<ListItem[]>(\n () =>\n searching\n ? searchTools(withTag(tools), query, appNames, sort, dir).map(\n ({ tool, fields }) => ({ tool, matched: fields }),\n )\n : [],\n [searching, tools, query, appNames, sort, dir, withTag],\n )\n\n const items = searching ? results : feed\n const selectedIndex = Math.min(selected, results.length - 1)\n const selectedTool = results[selectedIndex]?.tool\n const activeOptionId =\n searching && selectedTool ? optionIdOf(selectedTool.slug) : undefined\n\n const pinnedSlugs = useMemo(\n () => new Set((pins.data ?? []).map((t) => t.slug)),\n [pins.data],\n )\n\n const pinError = (retry: () => void) => () =>\n showToast({\n tone: 'error',\n text: 'Could not save the pin. Check your connection.',\n action: { label: 'Try again', run: retry },\n })\n\n const pinTool = (slug: string) =>\n pin.mutate(\n { slug },\n {\n onSuccess: () =>\n showToast({\n tone: 'info',\n text: 'Pinned to My tools',\n action: { label: 'Undo', run: () => unpinTool(slug, false) },\n }),\n onError: pinError(() => pinTool(slug)),\n },\n )\n const unpinTool = (slug: string, offerUndo = true) =>\n unpin.mutate(\n { slug },\n {\n onSuccess: () => {\n if (offerUndo) {\n showToast({\n tone: 'info',\n text: 'Removed from My tools',\n action: { label: 'Undo', run: () => pinTool(slug) },\n })\n }\n },\n onError: pinError(() => unpinTool(slug, offerUndo)),\n },\n )\n const togglePin = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n if (pinnedSlugs.has(tool.slug)) unpinTool(tool.slug)\n else pinTool(tool.slug)\n }\n\n const toggleLike = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n const save = tool.likedByMe ? unlike : like\n save.mutate(\n { slug: tool.slug },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: 'Could not save the like. Check your connection.',\n action: { label: 'Try again', run: () => toggleLike(tool) },\n }),\n },\n )\n }\n\n // A failed save puts the tiles back (the pins refetch); say why they moved.\n const reorderPins = (slugs: string[]) =>\n reorder.mutate(\n { slugs },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: \"Couldn't save the new order\",\n action: { label: 'Try again', run: () => reorderPins(slugs) },\n }),\n },\n )\n\n // Open: `{}` from Add a tool, the search text from \"Add it\".\n const [adding, setAdding] = useState<{ prefillName?: string } | null>(null)\n const startAdd = (prefillName?: string) => {\n // Adding needs an identity: sign in, then come back to this page.\n if (!isAuthenticated) signInHere()\n else setAdding({ prefillName })\n }\n\n const openCard = (slug: string, replace = false) =>\n navigate({\n to: '/community/$slug',\n params: { slug },\n search: (prev) => prev,\n replace,\n })\n const closeCard = () =>\n void navigate({ to: '/community', search: (prev) => prev })\n\n // The page column scrolls (the layout pins the header), not the list: the\n // virtualizer needs where the list starts inside that column. Everything\n // above it can change height — the shelf loads, hides while searching, goes\n // from 2 to 4 columns at `md` — and all of that resizes the page, so one\n // observer on it catches each case.\n const scrollRef = useRef<HTMLDivElement>(null)\n const pageRef = useRef<HTMLDivElement>(null)\n const [listEl, setListEl] = useState<HTMLDivElement | null>(null)\n const [scrollMargin, setScrollMargin] = useState(0)\n useEffect(() => {\n const scroller = scrollRef.current\n const page = pageRef.current\n if (!scroller || !page || !listEl) return\n const measure = () =>\n setScrollMargin(\n listEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top +\n scroller.scrollTop,\n )\n const observer = new ResizeObserver(measure)\n observer.observe(page)\n measure()\n return () => observer.disconnect()\n }, [listEl])\n // No layout, no window (jsdom, a print preview): the column measures 0 tall,\n // a virtualizer mounts nothing, so every row is rendered instead.\n const [hasLayout, setHasLayout] = useState(true)\n useLayoutEffect(() => {\n setHasLayout((scrollRef.current?.clientHeight ?? 0) > 0)\n }, [])\n // Stable on purpose: the virtualizer keys its measurement cache on this\n // function's identity, so a new arrow every render re-renders forever.\n const getItemKey = useCallback(\n (i: number) => items[i]?.tool.slug ?? i,\n [items],\n )\n // TanStack Virtual hands back functions the React Compiler cannot memoize\n // safely, so it skips this hook; registered in the root eslint.config.js.\n // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual\n const virtualizer = useVirtualizer({\n getScrollElement: () => scrollRef.current,\n // The column's height before it is measured, so the first paint already\n // holds the rows that will be visible.\n initialRect: { width: 0, height: 800 },\n count: items.length,\n // The thumbnail sets the row: 150 px, 12 px padding either side, borders.\n estimateSize: () => 176,\n overscan: 6,\n gap: 8,\n scrollMargin,\n getItemKey,\n })\n\n // Keep the selected result on screen. The next row is almost always already\n // mounted (overscan), and the browser scrolls it exactly; the virtualizer's\n // own estimate-based scroll is only for a row that is not there yet.\n useEffect(() => {\n if (!searching || selectedIndex < 0) return\n const row = listEl?.querySelector(`[data-index=\"${selectedIndex}\"]`)\n if (row) row.scrollIntoView({ block: 'nearest' })\n else virtualizer.scrollToIndex(selectedIndex, { align: 'auto' })\n }, [searching, selectedIndex, virtualizer, listEl])\n\n const onSearchKey = (e: React.KeyboardEvent<HTMLInputElement>) => {\n const current = results[selectedIndex]\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n if (results.length === 0) return\n e.preventDefault()\n const step = e.key === 'ArrowDown' ? 1 : -1\n setSelected(\n Math.min(results.length - 1, Math.max(0, selectedIndex + step)),\n )\n } else if (e.key === 'Enter' && current) {\n e.preventDefault()\n openSite(current.tool.url)\n } else if (\n e.key === 'ArrowRight' &&\n current &&\n e.currentTarget.selectionStart === e.currentTarget.value.length\n ) {\n // Only at the end of the text, so → still moves the caret inside it.\n e.preventDefault()\n void openCard(current.tool.slug)\n }\n }\n\n // Without layout every row is \"virtual\", at its natural place in the flow.\n const virtualItems = hasLayout\n ? virtualizer.getVirtualItems()\n : items.map((_, index) => ({\n key: items[index]!.tool.slug,\n index,\n start: virtualizer.options.scrollMargin,\n }))\n\n return (\n <div\n ref={scrollRef}\n data-preview-scroller\n className=\"min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]\"\n >\n <div\n ref={pageRef}\n data-preview-column\n className=\"mx-auto w-full max-w-[1000px] pt-1.5 pb-10\"\n >\n <div className=\"mb-[22px] flex items-center gap-2.5\">\n <ToolSearch\n value={query}\n onChange={(value) => {\n setQuery(value)\n setSelected(0)\n }}\n resultCount={results.length}\n resultsId={searching && results.length > 0 ? RESULTS_ID : undefined}\n activeDescendant={activeOptionId}\n onKey={onSearchKey}\n />\n <button\n type=\"button\"\n onClick={() => startAdd()}\n className=\"inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <Plus aria-hidden=\"true\" className=\"size-4\" />\n Add a tool\n </button>\n </div>\n\n {searching ? (\n <div className=\"-mt-2 mb-3.5 ml-1 flex items-center gap-2 text-[13px] font-semibold text-muted-foreground\">\n <Star\n aria-hidden=\"true\"\n className=\"size-4 fill-amber-400 text-amber-600\"\n />\n My tools is hidden while you search\n </div>\n ) : (\n <div className=\"mb-[26px]\">\n <MyToolsShelf\n pins={pins.data ?? []}\n isLoading={isAuthLoading || (isAuthenticated && pins.isPending)}\n onUnpin={(tool) => togglePin(tool)}\n onReorder={reorderPins}\n />\n </div>\n )}\n\n <section\n aria-label={searching ? 'Search results' : feedHeading(sort, dir)}\n data-testid={searching ? 'community-results' : 'community-feed'}\n >\n {!searching && (\n <>\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"font-serif text-[19px] font-semibold\">\n {feedHeading(sort, dir)}\n </h2>\n <FeedSortControl\n sort={sort}\n dir={dir}\n onChange={onSortChange}\n />\n </div>\n <TagRow\n tools={rankTools(tools, sort, dir)}\n activeTag={tag}\n appNames={appNames}\n onChoose={onTagChange}\n />\n </>\n )}\n\n {list.isPending ? (\n <FeedSkeleton />\n ) : list.isError ? (\n <FeedError onRetry={() => void list.refetch()} />\n ) : searching && results.length === 0 ? (\n <NoResults\n query={query.trim()}\n onAdd={() => startAdd(query.trim())}\n />\n ) : (\n <div\n ref={setListEl}\n id={searching ? RESULTS_ID : undefined}\n role={searching ? 'grid' : undefined}\n aria-label={searching ? 'Search results' : undefined}\n aria-activedescendant={activeOptionId}\n className={hasLayout ? 'relative' : 'flex flex-col gap-2'}\n style={\n hasLayout ? { height: virtualizer.getTotalSize() } : undefined\n }\n >\n {virtualItems.map((row) => {\n const item = items[row.index]\n if (!item) return null\n return (\n <div\n key={row.key}\n data-index={row.index}\n ref={virtualizer.measureElement}\n className={\n hasLayout ? 'absolute inset-x-0 top-0' : undefined\n }\n style={\n hasLayout\n ? {\n transform: `translateY(${row.start - virtualizer.options.scrollMargin}px)`,\n }\n : undefined\n }\n >\n <FeedRow\n tool={withMyLike(item.tool)}\n rank={searching ? undefined : row.index + 1}\n sort={sort}\n tokens={tokens}\n matched={item.matched}\n selected={searching && row.index === selectedIndex}\n optionId={\n searching ? optionIdOf(item.tool.slug) : undefined\n }\n pinned={pinnedSlugs.has(item.tool.slug)}\n pinBusy={pinBusySlug === item.tool.slug}\n onTogglePin={() => togglePin(item.tool)}\n onToggleLike={() => toggleLike(withMyLike(item.tool))}\n />\n </div>\n )\n })}\n </div>\n )}\n\n {searching && results.length > 0 && (\n <div className=\"mt-3.5 flex items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <kbd className={KBD}>↑</kbd>\n <kbd className={KBD}>↓</kbd>\n <span className=\"mr-2.5\">move</span>\n <kbd className={KBD}>Enter</kbd>\n <span className=\"mr-2.5\">open site</span>\n <kbd className={KBD}>→</kbd>\n <span className=\"mr-2.5\">open card</span>\n <kbd className={KBD}>Esc</kbd>\n <span>clear</span>\n </div>\n )}\n </section>\n\n <MarginPreview\n list={listEl}\n tools={tools}\n disabled={!!cardSlug || !!adding}\n />\n {cardSlug && (\n <ToolCard\n slug={cardSlug}\n siblings={items.map((item) => item.tool.slug)}\n onClose={closeCard}\n // Stepping swaps the card in place; Back still leaves the card.\n onNavigate={(slug) => void openCard(slug, true)}\n pinned={pinnedSlugs.has(cardSlug)}\n pinBusy={pinBusySlug === cardSlug}\n onTogglePin={togglePin}\n appNames={appNames}\n // One step: the card closes onto the feed already filtered.\n onChooseTag={(chosen) =>\n void navigate({\n to: '/community',\n search: (prev) => ({ ...prev, tag: chosen }),\n })\n }\n />\n )}\n {adding && (\n <ToolFormDialog\n mode=\"add\"\n prefillName={adding.prefillName}\n onClose={() => setAdding(null)}\n onOpenCard={(slug) => {\n setAdding(null)\n void openCard(slug)\n }}\n onDone={(slug) => {\n setAdding(null)\n showToast({\n tone: 'ok',\n text: (\n <>\n <b>Live now</b>, in the review list\n </>\n ),\n action: { label: 'Open card', run: () => void openCard(slug) },\n })\n }}\n />\n )}\n {/* After the card, so a toast raised from it shows above its scrim. */}\n <Toast toast={toast} onDismiss={dismissToast} />\n </div>\n </div>\n )\n}\n\n/** Three placeholder rows at the real row height. */\nfunction FeedSkeleton() {\n return (\n <div\n data-testid=\"feed-skeleton\"\n aria-busy=\"true\"\n className=\"flex flex-col gap-2\"\n >\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className=\"flex min-h-[176px] items-center gap-4 rounded-[14px] border border-border bg-card py-3 pl-3\"\n >\n <Skeleton className=\"h-[22px] w-[26px]\" />\n <Skeleton className={ROW_THUMB} />\n <div className=\"flex flex-1 flex-col gap-2\">\n <Skeleton className=\"h-2.5 w-2/5\" />\n <Skeleton className=\"h-2.5 w-4/5\" />\n <Skeleton className=\"h-2.5 w-1/4\" />\n </div>\n {/* The Details panel's reserved width. */}\n <div className=\"w-[120px] shrink-0\" />\n </div>\n ))}\n </div>\n )\n}\n\nfunction FeedError({ onRetry }: { onRetry: () => void }) {\n return (\n <div\n data-testid=\"feed-error\"\n role=\"alert\"\n className=\"flex items-center gap-3.5 rounded-[14px] border border-destructive/30 bg-destructive/5 px-5 py-[18px]\"\n >\n <AlertTriangle\n aria-hidden=\"true\"\n className=\"size-[22px] shrink-0 text-destructive\"\n />\n <div>\n <b className=\"font-bold\">Could not load community tools.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n The server did not answer. Your pinned tools above still work.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"ml-auto inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n Try again\n </button>\n </div>\n )\n}\n\nfunction NoResults({ query, onAdd }: { query: string; onAdd: () => void }) {\n return (\n <div\n data-testid=\"no-results\"\n className=\"flex gap-3.5 rounded-[14px] border border-border bg-card px-5 py-[18px]\"\n >\n <Search\n aria-hidden=\"true\"\n className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n />\n <div>\n <b className=\"font-bold\">Nothing matches “{query}”.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n Try a word from the page itself, or the name of the person who shared\n it. Know the tool?{' '}\n <button\n type=\"button\"\n onClick={onAdd}\n className=\"font-bold text-primary hover:underline focus-visible:outline-2 focus-visible:outline-ring\"\n >\n Add it\n </button>\n </p>\n </div>\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { CommunityTool, FeedSort, MatchField } from '@igstack/app-catalog-shared-core';
2
+ export declare function screenshotUrl(id: string, size: number): string;
3
+ /** The tool's screenshot, cropped to its top-left, or a globe when there is none. */
4
+ export declare function ToolThumb({ tool, size, className, }: {
5
+ tool: CommunityTool;
6
+ size: number;
7
+ className?: string;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function PersonAvatar({ name }: {
10
+ name: string;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ export interface FeedRowProps {
13
+ tool: CommunityTool;
14
+ /** Shown in the ranked feed; search results have no rank. */
15
+ rank?: number;
16
+ /** A date sort shows its date on the row. */
17
+ sort: FeedSort;
18
+ tokens: string[];
19
+ /** Set while searching: which fields the query hit. */
20
+ matched?: MatchField[];
21
+ selected?: boolean;
22
+ /**
23
+ * Set while searching: the row is a row of the results grid, and this is the
24
+ * id of its main cell, the one the search box points at.
25
+ */
26
+ optionId?: string;
27
+ pinned: boolean;
28
+ /** Its pin is being saved. */
29
+ pinBusy?: boolean;
30
+ onTogglePin: () => void;
31
+ onToggleLike: () => void;
32
+ }
33
+ /** The feed row's thumbnail: big enough to read the page title in its top left. */
34
+ export declare const ROW_THUMB = "h-[150px] w-[240px] rounded-[10px]";
35
+ /**
36
+ * One tool in the feed or the search results. Everything left of the end
37
+ * panel (rank, picture, name, summary, meta) is one link that opens the site
38
+ * in a new tab; the 120 px panel opens the card. The star sits in the meta
39
+ * line as its own target, above that link. The bottom strip shows the address
40
+ * the big click goes to, on hover or focus only.
41
+ */
42
+ export declare function FeedRow({ tool, rank, sort, tokens, matched, selected, optionId, pinned, pinBusy, onTogglePin, onToggleLike, }: FeedRowProps): import("react/jsx-runtime").JSX.Element;