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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +30 -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 +20 -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 +14 -5
  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 +18 -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 +41 -0
  63. package/dist/esm/modules/communityTools/ui/FeedRow.js +252 -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 +468 -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 +766 -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 +159 -0
  108. package/dist/esm/modules/communityTools/useCommunityTools.js +123 -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 +20 -5
  161. package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +6 -3
  162. package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +21 -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 +303 -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,72 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { Loader2, Star } from "lucide-react";
3
+ import { useState, useEffect } from "react";
4
+ import { cn } from "../../../lib/utils.js";
5
+ const BUSY_AFTER_MS = 400;
6
+ function PinButton({
7
+ pinned,
8
+ name,
9
+ onToggle,
10
+ labelled = false,
11
+ small = false,
12
+ busy = false
13
+ }) {
14
+ const [slow, setSlow] = useState(false);
15
+ useEffect(() => {
16
+ if (!busy) return;
17
+ const timer = setTimeout(() => setSlow(true), BUSY_AFTER_MS);
18
+ return () => {
19
+ clearTimeout(timer);
20
+ setSlow(false);
21
+ };
22
+ }, [busy]);
23
+ const showBusy = busy && slow;
24
+ return /* @__PURE__ */ jsxs(
25
+ "button",
26
+ {
27
+ type: "button",
28
+ "data-testid": "pin-button",
29
+ "aria-pressed": pinned,
30
+ "aria-busy": showBusy || void 0,
31
+ "aria-label": `Pin ${name}`,
32
+ onClick: (e) => {
33
+ e.stopPropagation();
34
+ onToggle();
35
+ },
36
+ className: cn(
37
+ "relative z-10 inline-flex shrink-0 items-center justify-center rounded-[9px] transition-colors",
38
+ "hover:bg-accent hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
39
+ labelled ? cn(
40
+ "h-10 gap-[7px] rounded-[10px] border px-3.5 text-[13.5px] font-bold whitespace-nowrap",
41
+ pinned ? "border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300" : "border-border bg-card text-foreground"
42
+ ) : cn(
43
+ "text-muted-foreground",
44
+ small ? "size-8 rounded-lg focus-visible:outline-offset-0" : "size-[34px]"
45
+ )
46
+ ),
47
+ children: [
48
+ showBusy ? /* @__PURE__ */ jsx(
49
+ Loader2,
50
+ {
51
+ "aria-hidden": "true",
52
+ className: cn("animate-spin", labelled ? "size-4" : "size-[18px]")
53
+ }
54
+ ) : /* @__PURE__ */ jsx(
55
+ Star,
56
+ {
57
+ "aria-hidden": "true",
58
+ className: cn(
59
+ labelled ? "size-4" : small ? "size-[15px]" : "size-[18px]",
60
+ pinned && "fill-amber-400 text-amber-600"
61
+ )
62
+ }
63
+ ),
64
+ labelled && (pinned ? "Pinned" : "Pin")
65
+ ]
66
+ }
67
+ );
68
+ }
69
+ export {
70
+ PinButton
71
+ };
72
+ //# sourceMappingURL=PinButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PinButton.js","sources":["../../../../../src/modules/communityTools/ui/PinButton.tsx"],"sourcesContent":["import { Loader2, Star } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { cn } from '~/lib/utils'\n\n/** A save quicker than this shows no spinner at all: the star already moved. */\nconst BUSY_AFTER_MS = 400\n\n/**\n * The star on rows, tiles and the card: pins the tool to My tools, or takes it\n * off. The card's is `labelled`, a bordered button that says Pin or Pinned;\n * a feed row's is `small`, a 32 px target in its meta line.\n */\nexport function PinButton({\n pinned,\n name,\n onToggle,\n labelled = false,\n small = false,\n busy = false,\n}: {\n pinned: boolean\n name: string\n onToggle: () => void\n labelled?: boolean\n small?: boolean\n /** The pin is being saved; the star shows the state it will have. */\n busy?: boolean\n}) {\n const [slow, setSlow] = useState(false)\n useEffect(() => {\n if (!busy) return\n const timer = setTimeout(() => setSlow(true), BUSY_AFTER_MS)\n return () => {\n clearTimeout(timer)\n setSlow(false)\n }\n }, [busy])\n const showBusy = busy && slow\n\n return (\n <button\n type=\"button\"\n data-testid=\"pin-button\"\n aria-pressed={pinned}\n aria-busy={showBusy || undefined}\n // One name; aria-pressed says whether it is pinned.\n aria-label={`Pin ${name}`}\n onClick={(e) => {\n e.stopPropagation()\n onToggle()\n }}\n className={cn(\n 'relative z-10 inline-flex shrink-0 items-center justify-center rounded-[9px] transition-colors',\n 'hover:bg-accent hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',\n labelled\n ? cn(\n 'h-10 gap-[7px] rounded-[10px] border px-3.5 text-[13.5px] font-bold whitespace-nowrap',\n pinned\n ? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'\n : 'border-border bg-card text-foreground',\n )\n : cn(\n 'text-muted-foreground',\n small\n ? 'size-8 rounded-lg focus-visible:outline-offset-0'\n : 'size-[34px]',\n ),\n )}\n >\n {showBusy ? (\n <Loader2\n aria-hidden=\"true\"\n className={cn('animate-spin', labelled ? 'size-4' : 'size-[18px]')}\n />\n ) : (\n <Star\n aria-hidden=\"true\"\n className={cn(\n labelled ? 'size-4' : small ? 'size-[15px]' : 'size-[18px]',\n pinned && 'fill-amber-400 text-amber-600',\n )}\n />\n )}\n {labelled && (pinned ? 'Pinned' : 'Pin')}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;AAKA,MAAM,gBAAgB;AAOf,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,OAAO;AACT,GAQG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,WAAW,MAAM,QAAQ,IAAI,GAAG,aAAa;AAC3D,WAAO,MAAM;AACX,mBAAa,KAAK;AAClB,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AACT,QAAM,WAAW,QAAQ;AAEzB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,eAAY;AAAA,MACZ,gBAAc;AAAA,MACd,aAAW,YAAY;AAAA,MAEvB,cAAY,OAAO,IAAI;AAAA,MACvB,SAAS,CAAC,MAAM;AACd,UAAE,gBAAA;AACF,iBAAA;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,WACI;AAAA,UACE;AAAA,UACA,SACI,4GACA;AAAA,QAAA,IAEN;AAAA,UACE;AAAA,UACA,QACI,qDACA;AAAA,QAAA;AAAA,MACN;AAAA,MAGL,UAAA;AAAA,QAAA,WACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW,GAAG,gBAAgB,WAAW,WAAW,aAAa;AAAA,UAAA;AAAA,QAAA,IAGnE;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT,WAAW,WAAW,QAAQ,gBAAgB;AAAA,cAC9C,UAAU;AAAA,YAAA;AAAA,UACZ;AAAA,QAAA;AAAA,QAGH,aAAa,SAAS,WAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGxC;"}
@@ -0,0 +1,13 @@
1
+ import { CommunityTool } from '@igstack/app-catalog-shared-core';
2
+ /**
3
+ * A pinned tool: screenshot first, then name and author. A click opens the
4
+ * site. Drag the tile, or press Alt+↑/↓ on it, to move it.
5
+ */
6
+ export declare function ShelfTile({ tool, dragging, onUnpin, onMove, dragProps, }: {
7
+ tool: CommunityTool;
8
+ dragging: boolean;
9
+ onUnpin: () => void;
10
+ /** -1 moves it one place earlier, +1 one place later. */
11
+ onMove: (step: -1 | 1) => void;
12
+ dragProps: React.HTMLAttributes<HTMLDivElement>;
13
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,81 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { ExternalLink, GripVertical } from "lucide-react";
3
+ import { cn } from "../../../lib/utils.js";
4
+ import { ToolThumb, PersonAvatar } from "./FeedRow.js";
5
+ import { PinButton } from "./PinButton.js";
6
+ function ShelfTile({
7
+ tool,
8
+ dragging,
9
+ onUnpin,
10
+ onMove,
11
+ dragProps
12
+ }) {
13
+ return /* @__PURE__ */ jsxs(
14
+ "div",
15
+ {
16
+ "data-testid": "shelf-tile",
17
+ draggable: true,
18
+ ...dragProps,
19
+ className: cn(
20
+ "group relative overflow-hidden rounded-[14px] border border-border bg-card transition-[border-color,box-shadow]",
21
+ "hover:border-primary/40 hover:shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/30",
22
+ dragging && "opacity-40"
23
+ ),
24
+ children: [
25
+ /* @__PURE__ */ jsxs(
26
+ "a",
27
+ {
28
+ href: tool.url,
29
+ target: "_blank",
30
+ rel: "noopener noreferrer",
31
+ "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown",
32
+ onKeyDown: (e) => {
33
+ if (!e.altKey) return;
34
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
35
+ e.preventDefault();
36
+ onMove(e.key === "ArrowUp" ? -1 : 1);
37
+ }
38
+ },
39
+ className: "block focus-visible:outline-none",
40
+ children: [
41
+ /* @__PURE__ */ jsxs("span", { className: "relative block aspect-[16/9.5] border-b border-border", children: [
42
+ /* @__PURE__ */ jsx(ToolThumb, { tool, size: 512, className: "size-full border-0" }),
43
+ /* @__PURE__ */ jsxs("span", { className: "absolute right-2.5 bottom-2.5 inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-extrabold text-primary-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100", children: [
44
+ /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-4" }),
45
+ "Open site"
46
+ ] })
47
+ ] }),
48
+ /* @__PURE__ */ jsxs("span", { className: "block py-2.5 pr-12 pl-3", children: [
49
+ /* @__PURE__ */ jsx(
50
+ "span",
51
+ {
52
+ "data-testid": "shelf-tile-name",
53
+ className: "block truncate text-sm font-bold",
54
+ children: tool.name
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsxs("span", { className: "mt-0.5 flex min-w-0 items-center gap-1.5 text-[12.5px] font-medium text-muted-foreground", children: [
58
+ tool.owners[0] && /* @__PURE__ */ jsx(PersonAvatar, { name: tool.owners[0].name }),
59
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: tool.owners.map((o) => o.name).join(", ") })
60
+ ] })
61
+ ] })
62
+ ]
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ "aria-hidden": "true",
69
+ className: "absolute top-2 right-2 inline-flex size-7 cursor-grab items-center justify-center rounded-md bg-card/90 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
70
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "size-4" })
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx("div", { className: "absolute right-2 bottom-2", children: /* @__PURE__ */ jsx(PinButton, { pinned: true, name: tool.name, onToggle: onUnpin }) })
74
+ ]
75
+ }
76
+ );
77
+ }
78
+ export {
79
+ ShelfTile
80
+ };
81
+ //# sourceMappingURL=ShelfTile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShelfTile.js","sources":["../../../../../src/modules/communityTools/ui/ShelfTile.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { ExternalLink, GripVertical } from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { PersonAvatar, ToolThumb } from './FeedRow'\nimport { PinButton } from './PinButton'\n\n/**\n * A pinned tool: screenshot first, then name and author. A click opens the\n * site. Drag the tile, or press Alt+↑/↓ on it, to move it.\n */\nexport function ShelfTile({\n tool,\n dragging,\n onUnpin,\n onMove,\n dragProps,\n}: {\n tool: CommunityTool\n dragging: boolean\n onUnpin: () => void\n /** -1 moves it one place earlier, +1 one place later. */\n onMove: (step: -1 | 1) => void\n dragProps: React.HTMLAttributes<HTMLDivElement>\n}) {\n return (\n <div\n data-testid=\"shelf-tile\"\n draggable\n {...dragProps}\n className={cn(\n 'group relative overflow-hidden rounded-[14px] border border-border bg-card transition-[border-color,box-shadow]',\n 'hover:border-primary/40 hover:shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/30',\n dragging && 'opacity-40',\n )}\n >\n <a\n href={tool.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-keyshortcuts=\"Alt+ArrowUp Alt+ArrowDown\"\n onKeyDown={(e) => {\n if (!e.altKey) return\n if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n e.preventDefault()\n onMove(e.key === 'ArrowUp' ? -1 : 1)\n }\n }}\n className=\"block focus-visible:outline-none\"\n >\n <span className=\"relative block aspect-[16/9.5] border-b border-border\">\n <ToolThumb tool={tool} size={512} className=\"size-full border-0\" />\n <span className=\"absolute right-2.5 bottom-2.5 inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-extrabold text-primary-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100\">\n <ExternalLink aria-hidden=\"true\" className=\"size-4\" />\n Open site\n </span>\n </span>\n <span className=\"block py-2.5 pr-12 pl-3\">\n <span\n data-testid=\"shelf-tile-name\"\n className=\"block truncate text-sm font-bold\"\n >\n {tool.name}\n </span>\n <span className=\"mt-0.5 flex min-w-0 items-center gap-1.5 text-[12.5px] font-medium text-muted-foreground\">\n {tool.owners[0] && <PersonAvatar name={tool.owners[0].name} />}\n <span className=\"truncate\">\n {tool.owners.map((o) => o.name).join(', ')}\n </span>\n </span>\n </span>\n </a>\n <span\n aria-hidden=\"true\"\n className=\"absolute top-2 right-2 inline-flex size-7 cursor-grab items-center justify-center rounded-md bg-card/90 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100\"\n >\n <GripVertical className=\"size-4\" />\n </span>\n <div className=\"absolute right-2 bottom-2\">\n <PinButton pinned name={tool.name} onToggle={onUnpin} />\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;AAUO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,WAAS;AAAA,MACR,GAAG;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAGd,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX,QAAO;AAAA,YACP,KAAI;AAAA,YACJ,qBAAkB;AAAA,YAClB,WAAW,CAAC,MAAM;AAChB,kBAAI,CAAC,EAAE,OAAQ;AACf,kBAAI,EAAE,QAAQ,aAAa,EAAE,QAAQ,aAAa;AAChD,kBAAE,eAAA;AACF,uBAAO,EAAE,QAAQ,YAAY,KAAK,CAAC;AAAA,cACrC;AAAA,YACF;AAAA,YACA,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,qBAAC,QAAA,EAAK,WAAU,yDACd,UAAA;AAAA,gBAAA,oBAAC,WAAA,EAAU,MAAY,MAAM,KAAK,WAAU,sBAAqB;AAAA,gBACjE,qBAAC,QAAA,EAAK,WAAU,iPACd,UAAA;AAAA,kBAAA,oBAAC,cAAA,EAAa,eAAY,QAAO,WAAU,UAAS;AAAA,kBAAE;AAAA,gBAAA,EAAA,CAExD;AAAA,cAAA,GACF;AAAA,cACA,qBAAC,QAAA,EAAK,WAAU,2BACd,UAAA;AAAA,gBAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,WAAU;AAAA,oBAET,UAAA,KAAK;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAER,qBAAC,QAAA,EAAK,WAAU,4FACb,UAAA;AAAA,kBAAA,KAAK,OAAO,CAAC,KAAK,oBAAC,cAAA,EAAa,MAAM,KAAK,OAAO,CAAC,EAAE,KAAA,CAAM;AAAA,kBAC5D,oBAAC,QAAA,EAAK,WAAU,YACb,eAAK,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,EAAA,CAC3C;AAAA,gBAAA,EAAA,CACF;AAAA,cAAA,EAAA,CACF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAEF;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA,YAEV,UAAA,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,QAEnC,oBAAC,OAAA,EAAI,WAAU,6BACb,UAAA,oBAAC,WAAA,EAAU,QAAM,MAAC,MAAM,KAAK,MAAM,UAAU,SAAS,EAAA,CACxD;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,21 @@
1
+ import { Resource } from '@igstack/app-catalog-backend-core';
2
+ import { CommunityTag } from '@igstack/app-catalog-shared-core';
3
+ /**
4
+ * App tags, picked from the catalog's own apps so they always resolve, and
5
+ * topic words, suggested from the tags already in use so they converge. One
6
+ * box offers both. Enter adds the typed text as a topic, unless it is exactly
7
+ * an app's name or abbreviation, or an arrow chose a suggestion.
8
+ */
9
+ export declare function TagPicker({ id, apps, knownTopics, value, onChange, onTooLong, describedBy, disabled, }: {
10
+ id: string;
11
+ /** The official apps (no sub-resources). */
12
+ apps: Resource[];
13
+ /** Every topic some tool already carries. */
14
+ knownTopics: string[];
15
+ value: CommunityTag[];
16
+ onChange: (tags: CommunityTag[]) => void;
17
+ /** A typed topic over the length cap; the form says so next to the field. */
18
+ onTooLong: () => void;
19
+ describedBy?: string;
20
+ disabled?: boolean;
21
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,87 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { normalizeTopic, TAG_VALUE_MAX } from "@igstack/app-catalog-shared-core";
3
+ import { AppWindow, Hash } from "lucide-react";
4
+ import { SuggestInput } from "./OwnerPicker.js";
5
+ import { tagKey } from "./TagRow.js";
6
+ const MAX_TAGS = 8;
7
+ const MAX_SUGGESTIONS = 6;
8
+ function TagPicker({
9
+ id,
10
+ apps,
11
+ knownTopics,
12
+ value,
13
+ onChange,
14
+ onTooLong,
15
+ describedBy,
16
+ disabled
17
+ }) {
18
+ const appName = (slug) => {
19
+ var _a;
20
+ return ((_a = apps.find((a) => a.slug === slug)) == null ? void 0 : _a.displayName) ?? slug;
21
+ };
22
+ const has = (tag) => value.some((t) => tagKey(t) === tagKey(tag));
23
+ const add = (tag) => {
24
+ if (!has(tag)) onChange([...value, tag]);
25
+ };
26
+ return /* @__PURE__ */ jsx(
27
+ SuggestInput,
28
+ {
29
+ id,
30
+ chips: value.map((tag) => ({
31
+ key: tagKey(tag),
32
+ label: tag.kind === "app" ? appName(tag.value) : tag.value,
33
+ icon: tag.kind === "app" ? /* @__PURE__ */ jsx(AppWindow, { "aria-hidden": "true", className: "ml-1 size-3.5" }) : /* @__PURE__ */ jsx(Hash, { "aria-hidden": "true", className: "ml-1 size-3.5" })
34
+ })),
35
+ onRemove: (key) => onChange(value.filter((t) => tagKey(t) !== key)),
36
+ placeholder: value.length >= MAX_TAGS ? "Eight tags at most" : "An app it is for, or a topic word",
37
+ atLimit: value.length >= MAX_TAGS,
38
+ disabled,
39
+ describedBy,
40
+ textFirst: true,
41
+ suggest: (query) => {
42
+ const q = query.toLowerCase();
43
+ const topic = normalizeTopic(query);
44
+ const appOptions = apps.filter(
45
+ (a) => {
46
+ var _a;
47
+ return a.displayName.toLowerCase().includes(q) || ((_a = a.abbreviation) == null ? void 0 : _a.toLowerCase().includes(q));
48
+ }
49
+ ).filter((a) => !has({ kind: "app", value: a.slug })).slice(0, MAX_SUGGESTIONS).map((a) => {
50
+ var _a;
51
+ return {
52
+ key: `app:${a.slug}`,
53
+ label: a.displayName,
54
+ sub: a.abbreviation ? `App · ${a.abbreviation}` : "App",
55
+ icon: /* @__PURE__ */ jsx(AppWindow, { "aria-hidden": "true", className: "size-4" }),
56
+ exact: a.displayName.toLowerCase() === q || ((_a = a.abbreviation) == null ? void 0 : _a.toLowerCase()) === q
57
+ };
58
+ });
59
+ const topicOptions = knownTopics.filter((t) => t.includes(topic)).filter((t) => !has({ kind: "topic", value: t })).slice(0, MAX_SUGGESTIONS).map((t) => ({
60
+ key: `topic:${t}`,
61
+ label: t,
62
+ sub: "Topic in use",
63
+ icon: /* @__PURE__ */ jsx(Hash, { "aria-hidden": "true", className: "size-4" })
64
+ }));
65
+ return [...appOptions, ...topicOptions];
66
+ },
67
+ onPick: (key) => {
68
+ const [kind, ...rest] = key.split(":");
69
+ add({ kind, value: rest.join(":") });
70
+ },
71
+ onEnterText: (text) => {
72
+ const topic = normalizeTopic(text);
73
+ if (!topic) return false;
74
+ if (topic.length > TAG_VALUE_MAX) {
75
+ onTooLong();
76
+ return false;
77
+ }
78
+ add({ kind: "topic", value: topic });
79
+ return true;
80
+ }
81
+ }
82
+ );
83
+ }
84
+ export {
85
+ TagPicker
86
+ };
87
+ //# sourceMappingURL=TagPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TagPicker.js","sources":["../../../../../src/modules/communityTools/ui/TagPicker.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport type { CommunityTag } from '@igstack/app-catalog-shared-core'\nimport { TAG_VALUE_MAX, normalizeTopic } from '@igstack/app-catalog-shared-core'\nimport { AppWindow, Hash } from 'lucide-react'\nimport { SuggestInput } from './OwnerPicker'\nimport { tagKey } from './TagRow'\n\nconst MAX_TAGS = 8\nconst MAX_SUGGESTIONS = 6\n\n/**\n * App tags, picked from the catalog's own apps so they always resolve, and\n * topic words, suggested from the tags already in use so they converge. One\n * box offers both. Enter adds the typed text as a topic, unless it is exactly\n * an app's name or abbreviation, or an arrow chose a suggestion.\n */\nexport function TagPicker({\n id,\n apps,\n knownTopics,\n value,\n onChange,\n onTooLong,\n describedBy,\n disabled,\n}: {\n id: string\n /** The official apps (no sub-resources). */\n apps: Resource[]\n /** Every topic some tool already carries. */\n knownTopics: string[]\n value: CommunityTag[]\n onChange: (tags: CommunityTag[]) => void\n /** A typed topic over the length cap; the form says so next to the field. */\n onTooLong: () => void\n describedBy?: string\n disabled?: boolean\n}) {\n const appName = (slug: string) =>\n apps.find((a) => a.slug === slug)?.displayName ?? slug\n const has = (tag: CommunityTag) =>\n value.some((t) => tagKey(t) === tagKey(tag))\n const add = (tag: CommunityTag) => {\n if (!has(tag)) onChange([...value, tag])\n }\n\n return (\n <SuggestInput\n id={id}\n chips={value.map((tag) => ({\n key: tagKey(tag),\n label: tag.kind === 'app' ? appName(tag.value) : tag.value,\n icon:\n tag.kind === 'app' ? (\n <AppWindow aria-hidden=\"true\" className=\"ml-1 size-3.5\" />\n ) : (\n <Hash aria-hidden=\"true\" className=\"ml-1 size-3.5\" />\n ),\n }))}\n onRemove={(key) => onChange(value.filter((t) => tagKey(t) !== key))}\n placeholder={\n value.length >= MAX_TAGS\n ? 'Eight tags at most'\n : 'An app it is for, or a topic word'\n }\n atLimit={value.length >= MAX_TAGS}\n disabled={disabled}\n describedBy={describedBy}\n textFirst\n suggest={(query) => {\n const q = query.toLowerCase()\n const topic = normalizeTopic(query)\n const appOptions = apps\n .filter(\n (a) =>\n a.displayName.toLowerCase().includes(q) ||\n a.abbreviation?.toLowerCase().includes(q),\n )\n .filter((a) => !has({ kind: 'app', value: a.slug }))\n .slice(0, MAX_SUGGESTIONS)\n .map((a) => ({\n key: `app:${a.slug}`,\n label: a.displayName,\n sub: a.abbreviation ? `App · ${a.abbreviation}` : 'App',\n icon: <AppWindow aria-hidden=\"true\" className=\"size-4\" />,\n exact:\n a.displayName.toLowerCase() === q ||\n a.abbreviation?.toLowerCase() === q,\n }))\n const topicOptions = knownTopics\n .filter((t) => t.includes(topic))\n .filter((t) => !has({ kind: 'topic', value: t }))\n .slice(0, MAX_SUGGESTIONS)\n .map((t) => ({\n key: `topic:${t}`,\n label: t,\n sub: 'Topic in use',\n icon: <Hash aria-hidden=\"true\" className=\"size-4\" />,\n }))\n return [...appOptions, ...topicOptions]\n }}\n onPick={(key) => {\n const [kind, ...rest] = key.split(':')\n add({ kind: kind as CommunityTag['kind'], value: rest.join(':') })\n }}\n onEnterText={(text) => {\n const topic = normalizeTopic(text)\n if (!topic) return false\n if (topic.length > TAG_VALUE_MAX) {\n onTooLong()\n return false\n }\n add({ kind: 'topic', value: topic })\n return true\n }}\n />\n )\n}\n"],"names":[],"mappings":";;;;;AAOA,MAAM,WAAW;AACjB,MAAM,kBAAkB;AAQjB,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYG;AACD,QAAM,UAAU,CAAC,SAAA;;AACf,uBAAK,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,MAAhC,mBAAmC,gBAAe;AAAA;AACpD,QAAM,MAAM,CAAC,QACX,MAAM,KAAK,CAAC,MAAM,OAAO,CAAC,MAAM,OAAO,GAAG,CAAC;AAC7C,QAAM,MAAM,CAAC,QAAsB;AACjC,QAAI,CAAC,IAAI,GAAG,YAAY,CAAC,GAAG,OAAO,GAAG,CAAC;AAAA,EACzC;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO,MAAM,IAAI,CAAC,SAAS;AAAA,QACzB,KAAK,OAAO,GAAG;AAAA,QACf,OAAO,IAAI,SAAS,QAAQ,QAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,QACrD,MACE,IAAI,SAAS,QACX,oBAAC,aAAU,eAAY,QAAO,WAAU,gBAAA,CAAgB,IAExD,oBAAC,MAAA,EAAK,eAAY,QAAO,WAAU,gBAAA,CAAgB;AAAA,MAAA,EAEvD;AAAA,MACF,UAAU,CAAC,QAAQ,SAAS,MAAM,OAAO,CAAC,MAAM,OAAO,CAAC,MAAM,GAAG,CAAC;AAAA,MAClE,aACE,MAAM,UAAU,WACZ,uBACA;AAAA,MAEN,SAAS,MAAM,UAAU;AAAA,MACzB;AAAA,MACA;AAAA,MACA,WAAS;AAAA,MACT,SAAS,CAAC,UAAU;AAClB,cAAM,IAAI,MAAM,YAAA;AAChB,cAAM,QAAQ,eAAe,KAAK;AAClC,cAAM,aAAa,KAChB;AAAA,UACC,CAAC,MAAA;;AACC,qBAAE,YAAY,cAAc,SAAS,CAAC,OACtC,OAAE,iBAAF,mBAAgB,cAAc,SAAS;AAAA;AAAA,QAAC,EAE3C,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,OAAO,OAAO,EAAE,KAAA,CAAM,CAAC,EAClD,MAAM,GAAG,eAAe,EACxB,IAAI,CAAC,MAAA;;AAAO;AAAA,YACX,KAAK,OAAO,EAAE,IAAI;AAAA,YAClB,OAAO,EAAE;AAAA,YACT,KAAK,EAAE,eAAe,SAAS,EAAE,YAAY,KAAK;AAAA,YAClD,MAAM,oBAAC,WAAA,EAAU,eAAY,QAAO,WAAU,UAAS;AAAA,YACvD,OACE,EAAE,YAAY,YAAA,MAAkB,OAChC,OAAE,iBAAF,mBAAgB,mBAAkB;AAAA,UAAA;AAAA,SACpC;AACJ,cAAM,eAAe,YAClB,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC,EAC/B,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,SAAS,OAAO,EAAA,CAAG,CAAC,EAC/C,MAAM,GAAG,eAAe,EACxB,IAAI,CAAC,OAAO;AAAA,UACX,KAAK,SAAS,CAAC;AAAA,UACf,OAAO;AAAA,UACP,KAAK;AAAA,UACL,MAAM,oBAAC,MAAA,EAAK,eAAY,QAAO,WAAU,SAAA,CAAS;AAAA,QAAA,EAClD;AACJ,eAAO,CAAC,GAAG,YAAY,GAAG,YAAY;AAAA,MACxC;AAAA,MACA,QAAQ,CAAC,QAAQ;AACf,cAAM,CAAC,MAAM,GAAG,IAAI,IAAI,IAAI,MAAM,GAAG;AACrC,YAAI,EAAE,MAAoC,OAAO,KAAK,KAAK,GAAG,GAAG;AAAA,MACnE;AAAA,MACA,aAAa,CAAC,SAAS;AACrB,cAAM,QAAQ,eAAe,IAAI;AACjC,YAAI,CAAC,MAAO,QAAO;AACnB,YAAI,MAAM,SAAS,eAAe;AAChC,oBAAA;AACA,iBAAO;AAAA,QACT;AACA,YAAI,EAAE,MAAM,SAAS,OAAO,OAAO;AACnC,eAAO;AAAA,MACT;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,22 @@
1
+ import { CommunityTag, CommunityTool } from '@igstack/app-catalog-shared-core';
2
+ /** A tag as it travels in `?tag=`: `topic:sandbox`, `app:order-service`. */
3
+ export declare function tagKey(tag: CommunityTag): string;
4
+ /** An app tag reads as the app's name; a topic tag as its word. */
5
+ export declare function tagLabel(tag: CommunityTag, appNames: Record<string, string>): string;
6
+ export declare function TagChip({ label, active, count, onClick, }: {
7
+ label: string;
8
+ active?: boolean;
9
+ count?: number;
10
+ onClick: () => void;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ /**
13
+ * The most-used tags above the feed, as a way in for someone who does not know
14
+ * what to search for. The chosen tag stays in the row even when it is not a top
15
+ * one, so the filter can always be cleared where it is shown.
16
+ */
17
+ export declare function TagRow({ tools, activeTag, appNames, onChoose, }: {
18
+ tools: CommunityTool[];
19
+ activeTag: string | undefined;
20
+ appNames: Record<string, string>;
21
+ onChoose: (tag: string | undefined) => void;
22
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,92 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "../../../lib/utils.js";
4
+ function tagKey(tag) {
5
+ return `${tag.kind}:${tag.value}`;
6
+ }
7
+ function tagLabel(tag, appNames) {
8
+ return tag.kind === "app" ? appNames[tag.value] ?? tag.value : tag.value;
9
+ }
10
+ const TOP_TAGS = 8;
11
+ function TagChip({
12
+ label,
13
+ active = false,
14
+ count,
15
+ onClick
16
+ }) {
17
+ return /* @__PURE__ */ jsxs(
18
+ "button",
19
+ {
20
+ type: "button",
21
+ "data-testid": "tool-tag",
22
+ "aria-pressed": active,
23
+ onClick: (e) => {
24
+ e.stopPropagation();
25
+ onClick();
26
+ },
27
+ className: cn(
28
+ "relative z-10 inline-flex h-6 items-center gap-1 rounded-full border px-2.5 text-xs font-semibold transition-colors",
29
+ "focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring",
30
+ active ? "border-primary/40 bg-accent text-accent-foreground" : "border-border bg-card text-muted-foreground hover:border-primary/40 hover:text-foreground"
31
+ ),
32
+ children: [
33
+ label,
34
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: "font-bold text-muted-foreground/70", children: count }),
35
+ active && /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3" })
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ function TagRow({
41
+ tools,
42
+ activeTag,
43
+ appNames,
44
+ onChoose
45
+ }) {
46
+ const counts = /* @__PURE__ */ new Map();
47
+ for (const tool of tools) {
48
+ for (const tag of tool.tags) {
49
+ const key = tagKey(tag);
50
+ const entry = counts.get(key) ?? { tag, count: 0 };
51
+ entry.count += 1;
52
+ counts.set(key, entry);
53
+ }
54
+ }
55
+ const ranked = [...counts.values()].sort(
56
+ (a, b) => b.count - a.count || tagLabel(a.tag, appNames).localeCompare(tagLabel(b.tag, appNames))
57
+ );
58
+ const shown = ranked.slice(0, TOP_TAGS);
59
+ const active = activeTag ? counts.get(activeTag) : void 0;
60
+ if (active && !shown.includes(active)) shown.push(active);
61
+ if (shown.length === 0) return null;
62
+ return /* @__PURE__ */ jsx(
63
+ "div",
64
+ {
65
+ "data-testid": "tag-row",
66
+ "aria-label": "Popular tags",
67
+ role: "group",
68
+ className: "mb-3 flex flex-wrap items-center gap-1.5",
69
+ children: shown.map(({ tag, count }) => {
70
+ const key = tagKey(tag);
71
+ const on = key === activeTag;
72
+ return /* @__PURE__ */ jsx(
73
+ TagChip,
74
+ {
75
+ label: tagLabel(tag, appNames),
76
+ count,
77
+ active: on,
78
+ onClick: () => onChoose(on ? void 0 : key)
79
+ },
80
+ key
81
+ );
82
+ })
83
+ }
84
+ );
85
+ }
86
+ export {
87
+ TagChip,
88
+ TagRow,
89
+ tagKey,
90
+ tagLabel
91
+ };
92
+ //# sourceMappingURL=TagRow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TagRow.js","sources":["../../../../../src/modules/communityTools/ui/TagRow.tsx"],"sourcesContent":["import type {\n CommunityTag,\n CommunityTool,\n} from '@igstack/app-catalog-shared-core'\nimport { X } from 'lucide-react'\nimport { cn } from '~/lib/utils'\n\n/** A tag as it travels in `?tag=`: `topic:sandbox`, `app:order-service`. */\nexport function tagKey(tag: CommunityTag): string {\n return `${tag.kind}:${tag.value}`\n}\n\n/** An app tag reads as the app's name; a topic tag as its word. */\nexport function tagLabel(\n tag: CommunityTag,\n appNames: Record<string, string>,\n): string {\n return tag.kind === 'app' ? (appNames[tag.value] ?? tag.value) : tag.value\n}\n\nconst TOP_TAGS = 8\n\nexport function TagChip({\n label,\n active = false,\n count,\n onClick,\n}: {\n label: string\n active?: boolean\n count?: number\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid=\"tool-tag\"\n aria-pressed={active}\n onClick={(e) => {\n e.stopPropagation()\n onClick()\n }}\n className={cn(\n 'relative z-10 inline-flex h-6 items-center gap-1 rounded-full border px-2.5 text-xs font-semibold transition-colors',\n 'focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring',\n active\n ? 'border-primary/40 bg-accent text-accent-foreground'\n : 'border-border bg-card text-muted-foreground hover:border-primary/40 hover:text-foreground',\n )}\n >\n {label}\n {count !== undefined && (\n <span className=\"font-bold text-muted-foreground/70\">{count}</span>\n )}\n {active && <X aria-hidden=\"true\" className=\"size-3\" />}\n </button>\n )\n}\n\n/**\n * The most-used tags above the feed, as a way in for someone who does not know\n * what to search for. The chosen tag stays in the row even when it is not a top\n * one, so the filter can always be cleared where it is shown.\n */\nexport function TagRow({\n tools,\n activeTag,\n appNames,\n onChoose,\n}: {\n tools: CommunityTool[]\n activeTag: string | undefined\n appNames: Record<string, string>\n onChoose: (tag: string | undefined) => void\n}) {\n const counts = new Map<string, { tag: CommunityTag; count: number }>()\n for (const tool of tools) {\n for (const tag of tool.tags) {\n const key = tagKey(tag)\n const entry = counts.get(key) ?? { tag, count: 0 }\n entry.count += 1\n counts.set(key, entry)\n }\n }\n const ranked = [...counts.values()].sort(\n (a, b) =>\n b.count - a.count ||\n tagLabel(a.tag, appNames).localeCompare(tagLabel(b.tag, appNames)),\n )\n const shown = ranked.slice(0, TOP_TAGS)\n const active = activeTag ? counts.get(activeTag) : undefined\n if (active && !shown.includes(active)) shown.push(active)\n\n if (shown.length === 0) return null\n\n return (\n <div\n data-testid=\"tag-row\"\n aria-label=\"Popular tags\"\n role=\"group\"\n className=\"mb-3 flex flex-wrap items-center gap-1.5\"\n >\n {shown.map(({ tag, count }) => {\n const key = tagKey(tag)\n const on = key === activeTag\n return (\n <TagChip\n key={key}\n label={tagLabel(tag, appNames)}\n count={count}\n active={on}\n onClick={() => onChoose(on ? undefined : key)}\n />\n )\n })}\n </div>\n )\n}\n"],"names":[],"mappings":";;;AAQO,SAAS,OAAO,KAA2B;AAChD,SAAO,GAAG,IAAI,IAAI,IAAI,IAAI,KAAK;AACjC;AAGO,SAAS,SACd,KACA,UACQ;AACR,SAAO,IAAI,SAAS,QAAS,SAAS,IAAI,KAAK,KAAK,IAAI,QAAS,IAAI;AACvE;AAEA,MAAM,WAAW;AAEV,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAKG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,eAAY;AAAA,MACZ,gBAAc;AAAA,MACd,SAAS,CAAC,MAAM;AACd,UAAE,gBAAA;AACF,gBAAA;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,SACI,uDACA;AAAA,MAAA;AAAA,MAGL,UAAA;AAAA,QAAA;AAAA,QACA,UAAU,UACT,oBAAC,QAAA,EAAK,WAAU,sCAAsC,UAAA,OAAM;AAAA,QAE7D,UAAU,oBAAC,GAAA,EAAE,eAAY,QAAO,WAAU,SAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG1D;AAOO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,6BAAa,IAAA;AACnB,aAAW,QAAQ,OAAO;AACxB,eAAW,OAAO,KAAK,MAAM;AAC3B,YAAM,MAAM,OAAO,GAAG;AACtB,YAAM,QAAQ,OAAO,IAAI,GAAG,KAAK,EAAE,KAAK,OAAO,EAAA;AAC/C,YAAM,SAAS;AACf,aAAO,IAAI,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACA,QAAM,SAAS,CAAC,GAAG,OAAO,OAAA,CAAQ,EAAE;AAAA,IAClC,CAAC,GAAG,MACF,EAAE,QAAQ,EAAE,SACZ,SAAS,EAAE,KAAK,QAAQ,EAAE,cAAc,SAAS,EAAE,KAAK,QAAQ,CAAC;AAAA,EAAA;AAErE,QAAM,QAAQ,OAAO,MAAM,GAAG,QAAQ;AACtC,QAAM,SAAS,YAAY,OAAO,IAAI,SAAS,IAAI;AACnD,MAAI,UAAU,CAAC,MAAM,SAAS,MAAM,EAAG,OAAM,KAAK,MAAM;AAExD,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,cAAW;AAAA,MACX,MAAK;AAAA,MACL,WAAU;AAAA,MAET,gBAAM,IAAI,CAAC,EAAE,KAAK,YAAY;AAC7B,cAAM,MAAM,OAAO,GAAG;AACtB,cAAM,KAAK,QAAQ;AACnB,eACE;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,OAAO,SAAS,KAAK,QAAQ;AAAA,YAC7B;AAAA,YACA,QAAQ;AAAA,YACR,SAAS,MAAM,SAAS,KAAK,SAAY,GAAG;AAAA,UAAA;AAAA,UAJvC;AAAA,QAAA;AAAA,MAOX,CAAC;AAAA,IAAA;AAAA,EAAA;AAGP;"}
@@ -0,0 +1,19 @@
1
+ export interface ToastMessage {
2
+ /** Changes for every new toast, so the same text twice still restarts the timer. */
3
+ id: number;
4
+ tone: 'info' | 'ok' | 'error';
5
+ text: React.ReactNode;
6
+ action?: {
7
+ label: string;
8
+ run: () => void;
9
+ };
10
+ }
11
+ /**
12
+ * One toast at a time, bottom-centre. It leaves after 5 s unless the pointer or
13
+ * focus is on it. The live region is always mounted, so the first toast is
14
+ * announced too; an error is an alert.
15
+ */
16
+ export declare function Toast({ toast, onDismiss, }: {
17
+ toast: ToastMessage | null;
18
+ onDismiss: () => void;
19
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,87 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { AlertTriangle, Check, Star, X } from "lucide-react";
3
+ import { useState, useEffect } from "react";
4
+ import { cn } from "../../../lib/utils.js";
5
+ const SHOW_MS = 5e3;
6
+ const ICONS = { info: Star, ok: Check, error: AlertTriangle };
7
+ function Toast({
8
+ toast,
9
+ onDismiss
10
+ }) {
11
+ return /* @__PURE__ */ jsx(
12
+ "div",
13
+ {
14
+ "aria-live": "polite",
15
+ className: "pointer-events-none fixed inset-x-0 bottom-6 z-50 flex justify-center px-4",
16
+ children: toast && /* @__PURE__ */ jsx(ToastBody, { toast, onDismiss }, toast.id)
17
+ }
18
+ );
19
+ }
20
+ function ToastBody({
21
+ toast,
22
+ onDismiss
23
+ }) {
24
+ const [held, setHeld] = useState(false);
25
+ useEffect(() => {
26
+ if (held) return;
27
+ const timer = setTimeout(onDismiss, SHOW_MS);
28
+ return () => clearTimeout(timer);
29
+ }, [held, onDismiss]);
30
+ const Icon = ICONS[toast.tone];
31
+ return /* @__PURE__ */ jsxs(
32
+ "div",
33
+ {
34
+ "data-testid": "toast",
35
+ role: toast.tone === "error" ? "alert" : "status",
36
+ onMouseEnter: () => setHeld(true),
37
+ onMouseLeave: () => setHeld(false),
38
+ onFocus: () => setHeld(true),
39
+ onBlur: () => setHeld(false),
40
+ className: cn(
41
+ "pointer-events-auto inline-flex max-w-full items-center gap-2.5 rounded-xl py-2.5 pr-2.5 pl-3.5 text-sm font-semibold shadow-lg",
42
+ toast.tone === "error" ? "bg-destructive text-white" : "bg-foreground text-background"
43
+ ),
44
+ children: [
45
+ /* @__PURE__ */ jsx(
46
+ Icon,
47
+ {
48
+ "aria-hidden": "true",
49
+ className: cn(
50
+ "size-4 shrink-0",
51
+ toast.tone === "info" && "fill-amber-400 text-amber-400",
52
+ toast.tone === "ok" && "text-green-400"
53
+ )
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsx("span", { children: toast.text }),
57
+ toast.action && /* @__PURE__ */ jsx(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ onClick: () => {
62
+ var _a;
63
+ (_a = toast.action) == null ? void 0 : _a.run();
64
+ onDismiss();
65
+ },
66
+ className: "rounded-md px-2 py-1 font-bold underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-current",
67
+ children: toast.action.label
68
+ }
69
+ ),
70
+ /* @__PURE__ */ jsx(
71
+ "button",
72
+ {
73
+ type: "button",
74
+ "aria-label": "Dismiss",
75
+ onClick: onDismiss,
76
+ className: "rounded-md p-1 opacity-70 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-current",
77
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
78
+ }
79
+ )
80
+ ]
81
+ }
82
+ );
83
+ }
84
+ export {
85
+ Toast
86
+ };
87
+ //# sourceMappingURL=Toast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.js","sources":["../../../../../src/modules/communityTools/ui/Toast.tsx"],"sourcesContent":["import { AlertTriangle, Check, Star, X } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { cn } from '~/lib/utils'\n\nexport interface ToastMessage {\n /** Changes for every new toast, so the same text twice still restarts the timer. */\n id: number\n tone: 'info' | 'ok' | 'error'\n text: React.ReactNode\n action?: { label: string; run: () => void }\n}\n\nconst SHOW_MS = 5000\n\nconst ICONS = { info: Star, ok: Check, error: AlertTriangle } as const\n\n/**\n * One toast at a time, bottom-centre. It leaves after 5 s unless the pointer or\n * focus is on it. The live region is always mounted, so the first toast is\n * announced too; an error is an alert.\n */\nexport function Toast({\n toast,\n onDismiss,\n}: {\n toast: ToastMessage | null\n onDismiss: () => void\n}) {\n return (\n <div\n aria-live=\"polite\"\n className=\"pointer-events-none fixed inset-x-0 bottom-6 z-50 flex justify-center px-4\"\n >\n {toast && (\n <ToastBody key={toast.id} toast={toast} onDismiss={onDismiss} />\n )}\n </div>\n )\n}\n\n/** Keyed by toast id, so a new toast starts un-held with a fresh timer. */\nfunction ToastBody({\n toast,\n onDismiss,\n}: {\n toast: ToastMessage\n onDismiss: () => void\n}) {\n const [held, setHeld] = useState(false)\n\n useEffect(() => {\n if (held) return\n const timer = setTimeout(onDismiss, SHOW_MS)\n return () => clearTimeout(timer)\n }, [held, onDismiss])\n\n const Icon = ICONS[toast.tone]\n\n return (\n <div\n data-testid=\"toast\"\n role={toast.tone === 'error' ? 'alert' : 'status'}\n onMouseEnter={() => setHeld(true)}\n onMouseLeave={() => setHeld(false)}\n onFocus={() => setHeld(true)}\n onBlur={() => setHeld(false)}\n className={cn(\n 'pointer-events-auto inline-flex max-w-full items-center gap-2.5 rounded-xl py-2.5 pr-2.5 pl-3.5 text-sm font-semibold shadow-lg',\n toast.tone === 'error'\n ? 'bg-destructive text-white'\n : 'bg-foreground text-background',\n )}\n >\n <Icon\n aria-hidden=\"true\"\n className={cn(\n 'size-4 shrink-0',\n toast.tone === 'info' && 'fill-amber-400 text-amber-400',\n toast.tone === 'ok' && 'text-green-400',\n )}\n />\n <span>{toast.text}</span>\n {toast.action && (\n <button\n type=\"button\"\n onClick={() => {\n toast.action?.run()\n onDismiss()\n }}\n className=\"rounded-md px-2 py-1 font-bold underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-current\"\n >\n {toast.action.label}\n </button>\n )}\n <button\n type=\"button\"\n aria-label=\"Dismiss\"\n onClick={onDismiss}\n className=\"rounded-md p-1 opacity-70 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-current\"\n >\n <X aria-hidden=\"true\" className=\"size-4\" />\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;AAYA,MAAM,UAAU;AAEhB,MAAM,QAAQ,EAAE,MAAM,MAAM,IAAI,OAAO,OAAO,cAAA;AAOvC,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AACF,GAGG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAU;AAAA,MAET,mBACC,oBAAC,WAAA,EAAyB,OAAc,UAAA,GAAxB,MAAM,EAAwC;AAAA,IAAA;AAAA,EAAA;AAItE;AAGA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAEtC,YAAU,MAAM;AACd,QAAI,KAAM;AACV,UAAM,QAAQ,WAAW,WAAW,OAAO;AAC3C,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,MAAM,SAAS,CAAC;AAEpB,QAAM,OAAO,MAAM,MAAM,IAAI;AAE7B,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,MAAM,MAAM,SAAS,UAAU,UAAU;AAAA,MACzC,cAAc,MAAM,QAAQ,IAAI;AAAA,MAChC,cAAc,MAAM,QAAQ,KAAK;AAAA,MACjC,SAAS,MAAM,QAAQ,IAAI;AAAA,MAC3B,QAAQ,MAAM,QAAQ,KAAK;AAAA,MAC3B,WAAW;AAAA,QACT;AAAA,QACA,MAAM,SAAS,UACX,8BACA;AAAA,MAAA;AAAA,MAGN,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,MAAM,SAAS,UAAU;AAAA,cACzB,MAAM,SAAS,QAAQ;AAAA,YAAA;AAAA,UACzB;AAAA,QAAA;AAAA,QAEF,oBAAC,QAAA,EAAM,UAAA,MAAM,KAAA,CAAK;AAAA,QACjB,MAAM,UACL;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;;AACb,0BAAM,WAAN,mBAAc;AACd,wBAAA;AAAA,YACF;AAAA,YACA,WAAU;AAAA,YAET,gBAAM,OAAO;AAAA,UAAA;AAAA,QAAA;AAAA,QAGlB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS;AAAA,YACT,WAAU;AAAA,YAEV,UAAA,oBAAC,GAAA,EAAE,eAAY,QAAO,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,MAC3C;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,22 @@
1
+ import { CommunityTool } from '@igstack/app-catalog-shared-core';
2
+ export interface ToolCardProps {
3
+ slug: string;
4
+ /** The list the card was opened from, in its order: ←/→ step through it. */
5
+ siblings: string[];
6
+ onClose: () => void;
7
+ onNavigate: (slug: string) => void;
8
+ pinned: boolean;
9
+ /** Its pin is being saved. */
10
+ pinBusy?: boolean;
11
+ onTogglePin: (tool: CommunityTool) => void;
12
+ /** Display names of official apps by slug, for app tags. */
13
+ appNames: Record<string, string>;
14
+ /** A tag chip on the card: close it and filter the feed to that tag. */
15
+ onChooseTag: (tag: string) => void;
16
+ }
17
+ /**
18
+ * A community tool over the Community page, in the official card's shell: no
19
+ * tabs, one scroll. Title and screenshot to recognise it, how to use it, who
20
+ * to ask, and the comments.
21
+ */
22
+ export declare function ToolCard({ slug, siblings, onClose, onNavigate, pinned, pinBusy, onTogglePin, appNames, onChooseTag, }: ToolCardProps): import("react/jsx-runtime").JSX.Element;