@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,115 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { screenshotUrl } from "./FeedRow.js";
4
+ const MIN_MARGIN = 420;
5
+ const FIRST_SHOW_MS = 300;
6
+ const GAP = 12;
7
+ function MarginPreview({
8
+ list,
9
+ tools,
10
+ disabled
11
+ }) {
12
+ var _a;
13
+ const [slug, setSlug] = useState(null);
14
+ const [box, setBox] = useState(null);
15
+ const rowRef = useRef(null);
16
+ const [canHover] = useState(
17
+ () => typeof window !== "undefined" && window.matchMedia("(hover: hover) and (pointer: fine)").matches
18
+ );
19
+ useEffect(() => {
20
+ const scroller = list == null ? void 0 : list.closest("[data-preview-scroller]");
21
+ const column = list == null ? void 0 : list.closest("[data-preview-column]");
22
+ if (!list || !scroller || !column || !canHover || disabled) return;
23
+ let timer;
24
+ let shown = false;
25
+ const place = () => {
26
+ const row = rowRef.current;
27
+ if (!(row == null ? void 0 : row.isConnected)) return null;
28
+ const area = scroller.getBoundingClientRect();
29
+ const margin = column.getBoundingClientRect().left - area.left;
30
+ if (margin < MIN_MARGIN) return null;
31
+ const width = margin - 2 * GAP;
32
+ const height = Math.round(width * 10 / 16);
33
+ const top = Math.max(
34
+ area.top,
35
+ Math.min(
36
+ row.getBoundingClientRect().top,
37
+ window.innerHeight - height - GAP
38
+ )
39
+ );
40
+ return { left: area.left + GAP, top, width, height };
41
+ };
42
+ const show = () => {
43
+ var _a2;
44
+ shown = true;
45
+ setSlug(((_a2 = rowRef.current) == null ? void 0 : _a2.dataset["slug"]) ?? null);
46
+ setBox(place());
47
+ };
48
+ const hide = () => {
49
+ clearTimeout(timer);
50
+ shown = false;
51
+ rowRef.current = null;
52
+ setSlug(null);
53
+ setBox(null);
54
+ };
55
+ const enter = (target) => {
56
+ const row = target instanceof Element ? target.closest('[data-testid="feed-row"]') : null;
57
+ if (!row || row === rowRef.current) return;
58
+ rowRef.current = row;
59
+ clearTimeout(timer);
60
+ if (shown) show();
61
+ else timer = setTimeout(show, FIRST_SHOW_MS);
62
+ };
63
+ const onPointerOver = (e) => enter(e.target);
64
+ const onFocusIn = (e) => enter(e.target);
65
+ const onFocusOut = (e) => {
66
+ if (!list.contains(e.relatedTarget)) hide();
67
+ };
68
+ const follow = () => {
69
+ if (shown) setBox(place());
70
+ };
71
+ list.addEventListener("pointerover", onPointerOver);
72
+ list.addEventListener("pointerleave", hide);
73
+ list.addEventListener("focusin", onFocusIn);
74
+ list.addEventListener("focusout", onFocusOut);
75
+ scroller.addEventListener("scroll", follow, { passive: true });
76
+ window.addEventListener("resize", follow);
77
+ return () => {
78
+ hide();
79
+ list.removeEventListener("pointerover", onPointerOver);
80
+ list.removeEventListener("pointerleave", hide);
81
+ list.removeEventListener("focusin", onFocusIn);
82
+ list.removeEventListener("focusout", onFocusOut);
83
+ scroller.removeEventListener("scroll", follow);
84
+ window.removeEventListener("resize", follow);
85
+ };
86
+ }, [list, canHover, disabled]);
87
+ const shot = (_a = tools.find((t) => t.slug === slug)) == null ? void 0 : _a.screenshotId;
88
+ if (disabled || !box || !shot) return null;
89
+ return /* @__PURE__ */ jsx(
90
+ "div",
91
+ {
92
+ "aria-hidden": "true",
93
+ "data-testid": "margin-preview",
94
+ className: "pointer-events-none fixed top-0 z-20 overflow-hidden rounded-[14px] border border-border bg-muted shadow-lg transition-transform duration-[120ms] ease-out",
95
+ style: {
96
+ left: box.left,
97
+ width: box.width,
98
+ height: box.height,
99
+ transform: `translateY(${box.top}px)`
100
+ },
101
+ children: /* @__PURE__ */ jsx(
102
+ "img",
103
+ {
104
+ src: screenshotUrl(shot, 1280),
105
+ alt: "",
106
+ className: "size-full object-cover object-left-top"
107
+ }
108
+ )
109
+ }
110
+ );
111
+ }
112
+ export {
113
+ MarginPreview
114
+ };
115
+ //# sourceMappingURL=MarginPreview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarginPreview.js","sources":["../../../../../src/modules/communityTools/ui/MarginPreview.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { useEffect, useRef, useState } from 'react'\nimport { screenshotUrl } from './FeedRow'\n\n/** Less free margin than this beside the column and there is no preview. */\nconst MIN_MARGIN = 420\n/** Hover or focus this long before the first preview shows. */\nconst FIRST_SHOW_MS = 300\nconst GAP = 12\n\ninterface Box {\n left: number\n top: number\n width: number\n height: number\n}\n\n/**\n * The hovered or focused row's screenshot, big, in the empty margin left of\n * the feed: one reused panel whose top lines up with the row's, moved up only\n * as far as it takes to stay in the window. The first one waits a moment;\n * moving to another row swaps it at once. Nothing on a narrow window, on a\n * touch screen, or while a dialog is open.\n */\nexport function MarginPreview({\n list,\n tools,\n disabled,\n}: {\n /**\n * The element the rows are in; hover and focus are read off it. Inside it\n * sits `[data-preview-column]`, the centred column whose left margin holds\n * the preview, inside `[data-preview-scroller]`.\n */\n list: HTMLElement | null\n tools: CommunityTool[]\n disabled: boolean\n}) {\n const [slug, setSlug] = useState<string | null>(null)\n const [box, setBox] = useState<Box | null>(null)\n const rowRef = useRef<HTMLElement | null>(null)\n const [canHover] = useState(\n () =>\n typeof window !== 'undefined' &&\n window.matchMedia('(hover: hover) and (pointer: fine)').matches,\n )\n\n useEffect(() => {\n const scroller = list?.closest<HTMLElement>('[data-preview-scroller]')\n const column = list?.closest<HTMLElement>('[data-preview-column]')\n if (!list || !scroller || !column || !canHover || disabled) return\n let timer: ReturnType<typeof setTimeout> | undefined\n let shown = false\n\n const place = (): Box | null => {\n const row = rowRef.current\n if (!row?.isConnected) return null\n const area = scroller.getBoundingClientRect()\n const margin = column.getBoundingClientRect().left - area.left\n if (margin < MIN_MARGIN) return null\n const width = margin - 2 * GAP\n const height = Math.round((width * 10) / 16)\n const top = Math.max(\n area.top,\n Math.min(\n row.getBoundingClientRect().top,\n window.innerHeight - height - GAP,\n ),\n )\n return { left: area.left + GAP, top, width, height }\n }\n const show = () => {\n shown = true\n setSlug(rowRef.current?.dataset['slug'] ?? null)\n setBox(place())\n }\n const hide = () => {\n clearTimeout(timer)\n shown = false\n rowRef.current = null\n setSlug(null)\n setBox(null)\n }\n const enter = (target: EventTarget | null) => {\n const row =\n target instanceof Element\n ? target.closest<HTMLElement>('[data-testid=\"feed-row\"]')\n : null\n if (!row || row === rowRef.current) return\n rowRef.current = row\n clearTimeout(timer)\n if (shown) show()\n else timer = setTimeout(show, FIRST_SHOW_MS)\n }\n const onPointerOver = (e: PointerEvent) => enter(e.target)\n const onFocusIn = (e: FocusEvent) => enter(e.target)\n const onFocusOut = (e: FocusEvent) => {\n if (!list.contains(e.relatedTarget as Node | null)) hide()\n }\n // Scrolling under a still-hovered row keeps the panel on it.\n const follow = () => {\n if (shown) setBox(place())\n }\n\n list.addEventListener('pointerover', onPointerOver)\n list.addEventListener('pointerleave', hide)\n list.addEventListener('focusin', onFocusIn)\n list.addEventListener('focusout', onFocusOut)\n scroller.addEventListener('scroll', follow, { passive: true })\n window.addEventListener('resize', follow)\n return () => {\n hide()\n list.removeEventListener('pointerover', onPointerOver)\n list.removeEventListener('pointerleave', hide)\n list.removeEventListener('focusin', onFocusIn)\n list.removeEventListener('focusout', onFocusOut)\n scroller.removeEventListener('scroll', follow)\n window.removeEventListener('resize', follow)\n }\n }, [list, canHover, disabled])\n\n const shot = tools.find((t) => t.slug === slug)?.screenshotId\n if (disabled || !box || !shot) return null\n return (\n <div\n aria-hidden=\"true\"\n data-testid=\"margin-preview\"\n className=\"pointer-events-none fixed top-0 z-20 overflow-hidden rounded-[14px] border border-border bg-muted shadow-lg transition-transform duration-[120ms] ease-out\"\n style={{\n left: box.left,\n width: box.width,\n height: box.height,\n transform: `translateY(${box.top}px)`,\n }}\n >\n <img\n src={screenshotUrl(shot, 1280)}\n alt=\"\"\n className=\"size-full object-cover object-left-top\"\n />\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { CommunityTool } from '@igstack/app-catalog-shared-core';
2
+ /**
3
+ * The visitor's pinned tools, in the order they arranged them. Native drag and
4
+ * drop moves a tile; `onReorder` gets the whole new order. Nothing at all
5
+ * shows until there is a pin: the star on every row is the way in.
6
+ */
7
+ export declare function MyToolsShelf({ pins, isLoading, onUnpin, onReorder, }: {
8
+ pins: CommunityTool[];
9
+ isLoading: boolean;
10
+ onUnpin: (tool: CommunityTool) => void;
11
+ onReorder: (slugs: string[]) => void;
12
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,88 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Skeleton } from "../../../ui/skeleton.js";
4
+ import { ShelfTile } from "./ShelfTile.js";
5
+ const GRID = "grid grid-cols-2 gap-3.5 md:grid-cols-4";
6
+ function moved(items, from, to) {
7
+ const next = [...items];
8
+ const [item] = next.splice(from, 1);
9
+ next.splice(to, 0, item);
10
+ return next;
11
+ }
12
+ function ShelfSkeleton() {
13
+ return /* @__PURE__ */ jsx("div", { "data-testid": "shelf-skeleton", "aria-busy": "true", className: GRID, children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsxs(
14
+ "div",
15
+ {
16
+ className: "overflow-hidden rounded-[14px] border border-border bg-card",
17
+ children: [
18
+ /* @__PURE__ */ jsx(Skeleton, { className: "aspect-[16/9.5] rounded-none" }),
19
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 px-3 py-3", children: [
20
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-2/3" }),
21
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-2/5" })
22
+ ] })
23
+ ]
24
+ },
25
+ i
26
+ )) });
27
+ }
28
+ function MyToolsShelf({
29
+ pins,
30
+ isLoading,
31
+ onUnpin,
32
+ onReorder
33
+ }) {
34
+ const [dragFrom, setDragFrom] = useState(null);
35
+ const move = (from, to) => {
36
+ if (to < 0 || to >= pins.length || from === to) return;
37
+ onReorder(moved(pins, from, to).map((t) => t.slug));
38
+ };
39
+ if (!isLoading && pins.length === 0) return null;
40
+ let body;
41
+ if (isLoading) {
42
+ body = /* @__PURE__ */ jsx(ShelfSkeleton, {});
43
+ } else {
44
+ body = /* @__PURE__ */ jsx("div", { className: GRID, children: pins.map((tool, index) => /* @__PURE__ */ jsx(
45
+ ShelfTile,
46
+ {
47
+ tool,
48
+ dragging: dragFrom === index,
49
+ onUnpin: () => onUnpin(tool),
50
+ onMove: (step) => move(index, index + step),
51
+ dragProps: {
52
+ onDragStart: (e) => {
53
+ e.dataTransfer.effectAllowed = "move";
54
+ setDragFrom(index);
55
+ },
56
+ onDragOver: (e) => {
57
+ if (dragFrom !== null) e.preventDefault();
58
+ },
59
+ onDrop: (e) => {
60
+ e.preventDefault();
61
+ if (dragFrom !== null) move(dragFrom, index);
62
+ setDragFrom(null);
63
+ },
64
+ onDragEnd: () => setDragFrom(null)
65
+ }
66
+ },
67
+ tool.slug
68
+ )) });
69
+ }
70
+ return /* @__PURE__ */ jsxs("section", { "aria-labelledby": "my-tools-heading", "data-testid": "my-tools", children: [
71
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-baseline gap-3", children: [
72
+ /* @__PURE__ */ jsx(
73
+ "h2",
74
+ {
75
+ id: "my-tools-heading",
76
+ className: "font-serif text-[19px] font-semibold",
77
+ children: "My tools"
78
+ }
79
+ ),
80
+ pins.length > 1 && /* @__PURE__ */ jsx("span", { className: "text-[12.5px] font-medium text-muted-foreground", children: "Drag to reorder" })
81
+ ] }),
82
+ body
83
+ ] });
84
+ }
85
+ export {
86
+ MyToolsShelf
87
+ };
88
+ //# sourceMappingURL=MyToolsShelf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MyToolsShelf.js","sources":["../../../../../src/modules/communityTools/ui/MyToolsShelf.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { useState } from 'react'\nimport { Skeleton } from '~/ui/skeleton'\nimport { ShelfTile } from './ShelfTile'\n\nconst GRID = 'grid grid-cols-2 gap-3.5 md:grid-cols-4'\n\nfunction moved<T>(items: T[], from: number, to: number): T[] {\n const next = [...items]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item!)\n return next\n}\n\n/** Same size as the real shelf, so nothing jumps when the pins arrive. */\nfunction ShelfSkeleton() {\n return (\n <div data-testid=\"shelf-skeleton\" aria-busy=\"true\" className={GRID}>\n {[0, 1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"overflow-hidden rounded-[14px] border border-border bg-card\"\n >\n <Skeleton className=\"aspect-[16/9.5] rounded-none\" />\n <div className=\"flex flex-col gap-1.5 px-3 py-3\">\n <Skeleton className=\"h-2.5 w-2/3\" />\n <Skeleton className=\"h-2.5 w-2/5\" />\n </div>\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * The visitor's pinned tools, in the order they arranged them. Native drag and\n * drop moves a tile; `onReorder` gets the whole new order. Nothing at all\n * shows until there is a pin: the star on every row is the way in.\n */\nexport function MyToolsShelf({\n pins,\n isLoading,\n onUnpin,\n onReorder,\n}: {\n pins: CommunityTool[]\n isLoading: boolean\n onUnpin: (tool: CommunityTool) => void\n onReorder: (slugs: string[]) => void\n}) {\n const [dragFrom, setDragFrom] = useState<number | null>(null)\n\n const move = (from: number, to: number) => {\n if (to < 0 || to >= pins.length || from === to) return\n onReorder(moved(pins, from, to).map((t) => t.slug))\n }\n\n if (!isLoading && pins.length === 0) return null\n\n let body: React.ReactNode\n if (isLoading) {\n body = <ShelfSkeleton />\n } else {\n body = (\n <div className={GRID}>\n {pins.map((tool, index) => (\n <ShelfTile\n key={tool.slug}\n tool={tool}\n dragging={dragFrom === index}\n onUnpin={() => onUnpin(tool)}\n onMove={(step) => move(index, index + step)}\n dragProps={{\n onDragStart: (e) => {\n e.dataTransfer.effectAllowed = 'move'\n setDragFrom(index)\n },\n onDragOver: (e) => {\n if (dragFrom !== null) e.preventDefault()\n },\n onDrop: (e) => {\n e.preventDefault()\n if (dragFrom !== null) move(dragFrom, index)\n setDragFrom(null)\n },\n onDragEnd: () => setDragFrom(null),\n }}\n />\n ))}\n </div>\n )\n }\n\n return (\n <section aria-labelledby=\"my-tools-heading\" data-testid=\"my-tools\">\n <div className=\"mb-3 flex items-baseline gap-3\">\n <h2\n id=\"my-tools-heading\"\n className=\"font-serif text-[19px] font-semibold\"\n >\n My tools\n </h2>\n {pins.length > 1 && (\n <span className=\"text-[12.5px] font-medium text-muted-foreground\">\n Drag to reorder\n </span>\n )}\n </div>\n {body}\n </section>\n )\n}\n"],"names":[],"mappings":";;;;AAKA,MAAM,OAAO;AAEb,SAAS,MAAS,OAAY,MAAc,IAAiB;AAC3D,QAAM,OAAO,CAAC,GAAG,KAAK;AACtB,QAAM,CAAC,IAAI,IAAI,KAAK,OAAO,MAAM,CAAC;AAClC,OAAK,OAAO,IAAI,GAAG,IAAK;AACxB,SAAO;AACT;AAGA,SAAS,gBAAgB;AACvB,6BACG,OAAA,EAAI,eAAY,kBAAiB,aAAU,QAAO,WAAW,MAC3D,UAAA,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MACjB;AAAA,IAAC;AAAA,IAAA;AAAA,MAEC,WAAU;AAAA,MAEV,UAAA;AAAA,QAAA,oBAAC,UAAA,EAAS,WAAU,+BAAA,CAA+B;AAAA,QACnD,qBAAC,OAAA,EAAI,WAAU,mCACb,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,WAAU,cAAA,CAAc;AAAA,UAClC,oBAAC,UAAA,EAAS,WAAU,cAAA,CAAc;AAAA,QAAA,EAAA,CACpC;AAAA,MAAA;AAAA,IAAA;AAAA,IAPK;AAAA,EAAA,CASR,GACH;AAEJ;AAOO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAE5D,QAAM,OAAO,CAAC,MAAc,OAAe;AACzC,QAAI,KAAK,KAAK,MAAM,KAAK,UAAU,SAAS,GAAI;AAChD,cAAU,MAAM,MAAM,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,EACpD;AAEA,MAAI,CAAC,aAAa,KAAK,WAAW,EAAG,QAAO;AAE5C,MAAI;AACJ,MAAI,WAAW;AACb,+BAAQ,eAAA,EAAc;AAAA,EACxB,OAAO;AACL,WACE,oBAAC,SAAI,WAAW,MACb,eAAK,IAAI,CAAC,MAAM,UACf;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC;AAAA,QACA,UAAU,aAAa;AAAA,QACvB,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC3B,QAAQ,CAAC,SAAS,KAAK,OAAO,QAAQ,IAAI;AAAA,QAC1C,WAAW;AAAA,UACT,aAAa,CAAC,MAAM;AAClB,cAAE,aAAa,gBAAgB;AAC/B,wBAAY,KAAK;AAAA,UACnB;AAAA,UACA,YAAY,CAAC,MAAM;AACjB,gBAAI,aAAa,KAAM,GAAE,eAAA;AAAA,UAC3B;AAAA,UACA,QAAQ,CAAC,MAAM;AACb,cAAE,eAAA;AACF,gBAAI,aAAa,KAAM,MAAK,UAAU,KAAK;AAC3C,wBAAY,IAAI;AAAA,UAClB;AAAA,UACA,WAAW,MAAM,YAAY,IAAI;AAAA,QAAA;AAAA,MACnC;AAAA,MAnBK,KAAK;AAAA,IAAA,CAqBb,GACH;AAAA,EAEJ;AAEA,SACE,qBAAC,WAAA,EAAQ,mBAAgB,oBAAmB,eAAY,YACtD,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,IAAG;AAAA,UACH,WAAU;AAAA,UACX,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAGA,KAAK,SAAS,yBACZ,QAAA,EAAK,WAAU,mDAAkD,UAAA,kBAAA,CAElE;AAAA,IAAA,GAEJ;AAAA,IACC;AAAA,EAAA,GACH;AAEJ;"}
@@ -0,0 +1,56 @@
1
+ import { Person } from '@igstack/app-catalog-backend-core';
2
+ import { PersonRef } from '@igstack/app-catalog-shared-core';
3
+ export declare const FIELD_BOX = "flex min-h-10 w-full items-center gap-2 rounded-[10px] border border-border bg-card px-3 text-sm transition-[box-shadow,border-color] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/30";
4
+ export declare const FIELD_BAD = "border-destructive ring-[3px] ring-destructive/15 focus-within:border-destructive focus-within:ring-destructive/15";
5
+ export interface SuggestChip {
6
+ key: string;
7
+ label: string;
8
+ icon?: React.ReactNode;
9
+ }
10
+ export interface SuggestOption {
11
+ key: string;
12
+ label: string;
13
+ sub?: string;
14
+ icon?: React.ReactNode;
15
+ /** The typed text is exactly this option, so Enter takes it unasked. */
16
+ exact?: boolean;
17
+ }
18
+ /**
19
+ * Chips with a text box that suggests what to add. ↑↓ move through the
20
+ * suggestions, Enter adds one (or hands the typed text to `onEnterText`),
21
+ * Backspace in an empty box takes the last chip off, Esc closes the list.
22
+ * With `textFirst`, nothing is highlighted until an arrow moves onto a
23
+ * suggestion: Enter adds the typed text, or the option it exactly names.
24
+ */
25
+ export declare function SuggestInput({ id, chips, onRemove, placeholder, suggest, onPick, onEnterText, invalid, describedBy, disabled, atLimit, textFirst, }: {
26
+ id: string;
27
+ chips: SuggestChip[];
28
+ onRemove: (key: string) => void;
29
+ placeholder: string;
30
+ /** What to offer for the text typed so far. */
31
+ suggest: (query: string) => SuggestOption[];
32
+ onPick: (key: string) => void;
33
+ /** Enter with no suggestion chosen; return true when the text was used. */
34
+ onEnterText?: (text: string) => boolean;
35
+ invalid?: boolean;
36
+ describedBy?: string;
37
+ disabled?: boolean;
38
+ /** Nothing more can be added; chips can still come off. */
39
+ atLimit?: boolean;
40
+ textFirst?: boolean;
41
+ }): import("react/jsx-runtime").JSX.Element;
42
+ export declare function personName(person: Person): string;
43
+ /**
44
+ * Owners from the person directory, searched by name or email, and people it
45
+ * does not know, by name: chips, remove.
46
+ */
47
+ export declare function OwnerPicker({ id, persons, value, onChange, invalid, describedBy, disabled, }: {
48
+ id: string;
49
+ persons: Person[];
50
+ /** In the order they were added; `slug` is null outside the directory. */
51
+ value: PersonRef[];
52
+ onChange: (owners: PersonRef[]) => void;
53
+ invalid?: boolean;
54
+ describedBy?: string;
55
+ disabled?: boolean;
56
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,245 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { X, Search } from "lucide-react";
3
+ import { useId, useState } from "react";
4
+ import { cn } from "../../../lib/utils.js";
5
+ import { highlightMatches } from "../../../util/highlightMatches.js";
6
+ import { PersonAvatar } from "./FeedRow.js";
7
+ const FIELD_BOX = "flex min-h-10 w-full items-center gap-2 rounded-[10px] border border-border bg-card px-3 text-sm transition-[box-shadow,border-color] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/30";
8
+ const FIELD_BAD = "border-destructive ring-[3px] ring-destructive/15 focus-within:border-destructive focus-within:ring-destructive/15";
9
+ function SuggestInput({
10
+ id,
11
+ chips,
12
+ onRemove,
13
+ placeholder,
14
+ suggest,
15
+ onPick,
16
+ onEnterText,
17
+ invalid,
18
+ describedBy,
19
+ disabled,
20
+ atLimit,
21
+ textFirst = false
22
+ }) {
23
+ const listId = useId();
24
+ const [query, setQuery] = useState("");
25
+ const [active, setActive] = useState(0);
26
+ const [open, setOpen] = useState(false);
27
+ const [moved, setMoved] = useState(false);
28
+ const options = query.trim() ? suggest(query.trim()) : [];
29
+ const shown = open && options.length > 0;
30
+ const activeIndex = textFirst && !moved ? -1 : Math.min(active, options.length - 1);
31
+ const optionId = (i) => `${listId}-${i}`;
32
+ const pick = (key) => {
33
+ onPick(key);
34
+ setQuery("");
35
+ setActive(0);
36
+ setMoved(false);
37
+ };
38
+ const onKeyDown = (e) => {
39
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
40
+ e.preventDefault();
41
+ if (!shown) {
42
+ setOpen(true);
43
+ return;
44
+ }
45
+ const step = e.key === "ArrowDown" ? 1 : -1;
46
+ setActive(Math.min(options.length - 1, Math.max(0, activeIndex + step)));
47
+ setMoved(true);
48
+ } else if (e.key === "Enter") {
49
+ e.preventDefault();
50
+ const option = !shown ? void 0 : activeIndex >= 0 ? options[activeIndex] : options.find((o) => o.exact);
51
+ if (option) pick(option.key);
52
+ else if (query.trim() && (onEnterText == null ? void 0 : onEnterText(query))) setQuery("");
53
+ } else if (e.key === "Backspace" && !query && chips.length > 0) {
54
+ onRemove(chips[chips.length - 1].key);
55
+ } else if (e.key === "Escape" && shown) {
56
+ e.preventDefault();
57
+ setOpen(false);
58
+ }
59
+ };
60
+ return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
61
+ /* @__PURE__ */ jsxs(
62
+ "div",
63
+ {
64
+ className: cn(
65
+ FIELD_BOX,
66
+ "flex-wrap gap-1.5 px-2 py-[5px]",
67
+ invalid && FIELD_BAD
68
+ ),
69
+ children: [
70
+ chips.map((chip) => /* @__PURE__ */ jsxs(
71
+ "span",
72
+ {
73
+ "data-testid": "picker-chip",
74
+ className: "inline-flex h-7 items-center gap-1.5 rounded-full bg-secondary pr-1 pl-[3px] text-[13px] font-semibold",
75
+ children: [
76
+ chip.icon,
77
+ /* @__PURE__ */ jsx("span", { "data-testid": "picker-chip-label", children: chip.label }),
78
+ /* @__PURE__ */ jsx(
79
+ "button",
80
+ {
81
+ type: "button",
82
+ "aria-label": `Remove ${chip.label}`,
83
+ disabled,
84
+ onClick: () => onRemove(chip.key),
85
+ className: "inline-flex size-5 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
86
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3" })
87
+ }
88
+ )
89
+ ]
90
+ },
91
+ chip.key
92
+ )),
93
+ /* @__PURE__ */ jsx(
94
+ "input",
95
+ {
96
+ id,
97
+ type: "text",
98
+ role: "combobox",
99
+ autoComplete: "off",
100
+ "aria-expanded": shown,
101
+ "aria-controls": shown ? listId : void 0,
102
+ "aria-autocomplete": "list",
103
+ "aria-activedescendant": shown && activeIndex >= 0 ? optionId(activeIndex) : void 0,
104
+ "aria-invalid": invalid || void 0,
105
+ "aria-describedby": describedBy,
106
+ disabled: disabled || atLimit,
107
+ placeholder,
108
+ value: query,
109
+ onChange: (e) => {
110
+ setQuery(e.target.value);
111
+ setActive(0);
112
+ setMoved(false);
113
+ setOpen(true);
114
+ },
115
+ onFocus: () => setOpen(true),
116
+ onBlur: () => setOpen(false),
117
+ onKeyDown,
118
+ className: "h-7 min-w-32 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground"
119
+ }
120
+ ),
121
+ /* @__PURE__ */ jsx(
122
+ Search,
123
+ {
124
+ "aria-hidden": "true",
125
+ className: "size-4 text-muted-foreground/70"
126
+ }
127
+ )
128
+ ]
129
+ }
130
+ ),
131
+ shown && /* @__PURE__ */ jsx(
132
+ "ul",
133
+ {
134
+ id: listId,
135
+ role: "listbox",
136
+ className: "absolute inset-x-0 top-full z-10 mt-0.5 flex flex-col rounded-[10px] border border-border bg-card p-1 shadow-lg",
137
+ children: options.map((option, i) => /* @__PURE__ */ jsxs(
138
+ "li",
139
+ {
140
+ id: optionId(i),
141
+ role: "option",
142
+ "aria-selected": i === activeIndex,
143
+ onMouseDown: (e) => e.preventDefault(),
144
+ onClick: () => pick(option.key),
145
+ onMouseEnter: () => setActive(i),
146
+ className: cn(
147
+ "flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-[7px] text-[13.5px] hover:bg-accent",
148
+ i === activeIndex && "bg-accent"
149
+ ),
150
+ children: [
151
+ option.icon,
152
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col leading-tight", children: [
153
+ /* @__PURE__ */ jsx("b", { className: "font-semibold", children: highlightMatches(option.label, query.trim()) }),
154
+ option.sub && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: option.sub })
155
+ ] })
156
+ ]
157
+ },
158
+ option.key
159
+ ))
160
+ }
161
+ )
162
+ ] });
163
+ }
164
+ function personName(person) {
165
+ return `${person.firstName} ${person.lastName}`.trim() || person.slug;
166
+ }
167
+ const MAX_OWNERS = 5;
168
+ const MAX_SUGGESTIONS = 6;
169
+ const OWNER_NAME_MAX = 80;
170
+ const NAMED = "named:";
171
+ function ownerKey(owner) {
172
+ return owner.slug ?? `${NAMED}${owner.name}`;
173
+ }
174
+ function OwnerPicker({
175
+ id,
176
+ persons,
177
+ value,
178
+ onChange,
179
+ invalid,
180
+ describedBy,
181
+ disabled
182
+ }) {
183
+ const full = value.length >= MAX_OWNERS;
184
+ const taken = new Set(value.map(ownerKey));
185
+ return /* @__PURE__ */ jsx(
186
+ SuggestInput,
187
+ {
188
+ id,
189
+ chips: value.map((owner) => ({
190
+ key: ownerKey(owner),
191
+ label: owner.name,
192
+ icon: /* @__PURE__ */ jsx(PersonAvatar, { name: owner.name })
193
+ })),
194
+ onRemove: (key) => onChange(value.filter((o) => ownerKey(o) !== key)),
195
+ placeholder: full ? "Five owners at most" : "Add a person",
196
+ disabled,
197
+ atLimit: full,
198
+ invalid,
199
+ describedBy,
200
+ suggest: (query) => {
201
+ const words = query.toLowerCase().split(/\s+/);
202
+ const people = persons.filter((p) => !taken.has(p.slug)).filter((p) => {
203
+ const text = `${personName(p)} ${p.email ?? p.slug}`.toLowerCase();
204
+ return words.every((w) => text.includes(w));
205
+ }).slice(0, MAX_SUGGESTIONS).map((p) => ({
206
+ key: p.slug,
207
+ label: personName(p),
208
+ sub: p.email ?? p.slug,
209
+ icon: /* @__PURE__ */ jsx(PersonAvatar, { name: personName(p) })
210
+ }));
211
+ const name = query.trim().slice(0, OWNER_NAME_MAX);
212
+ const known = people.some(
213
+ (p) => p.label.toLowerCase() === name.toLowerCase()
214
+ );
215
+ if (known || taken.has(`${NAMED}${name}`)) return people;
216
+ return [
217
+ ...people,
218
+ {
219
+ key: `${NAMED}${name}`,
220
+ label: `Add “${name}”`,
221
+ sub: "Not in the people list",
222
+ icon: /* @__PURE__ */ jsx(PersonAvatar, { name })
223
+ }
224
+ ];
225
+ },
226
+ onPick: (key) => {
227
+ if (key.startsWith(NAMED)) {
228
+ onChange([...value, { slug: null, name: key.slice(NAMED.length) }]);
229
+ return;
230
+ }
231
+ const person = persons.find((p) => p.slug === key);
232
+ if (person)
233
+ onChange([...value, { slug: key, name: personName(person) }]);
234
+ }
235
+ }
236
+ );
237
+ }
238
+ export {
239
+ FIELD_BAD,
240
+ FIELD_BOX,
241
+ OwnerPicker,
242
+ SuggestInput,
243
+ personName
244
+ };
245
+ //# sourceMappingURL=OwnerPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OwnerPicker.js","sources":["../../../../../src/modules/communityTools/ui/OwnerPicker.tsx"],"sourcesContent":["import type { Person } from '@igstack/app-catalog-backend-core'\nimport type { PersonRef } from '@igstack/app-catalog-shared-core'\nimport { Search, X } from 'lucide-react'\nimport { useId, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { highlightMatches } from '~/util/highlightMatches'\nimport { PersonAvatar } from './FeedRow'\n\nexport const FIELD_BOX =\n 'flex min-h-10 w-full items-center gap-2 rounded-[10px] border border-border bg-card px-3 text-sm transition-[box-shadow,border-color] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/30'\n\nexport const FIELD_BAD =\n 'border-destructive ring-[3px] ring-destructive/15 focus-within:border-destructive focus-within:ring-destructive/15'\n\nexport interface SuggestChip {\n key: string\n label: string\n icon?: React.ReactNode\n}\n\nexport interface SuggestOption {\n key: string\n label: string\n sub?: string\n icon?: React.ReactNode\n /** The typed text is exactly this option, so Enter takes it unasked. */\n exact?: boolean\n}\n\n/**\n * Chips with a text box that suggests what to add. ↑↓ move through the\n * suggestions, Enter adds one (or hands the typed text to `onEnterText`),\n * Backspace in an empty box takes the last chip off, Esc closes the list.\n * With `textFirst`, nothing is highlighted until an arrow moves onto a\n * suggestion: Enter adds the typed text, or the option it exactly names.\n */\nexport function SuggestInput({\n id,\n chips,\n onRemove,\n placeholder,\n suggest,\n onPick,\n onEnterText,\n invalid,\n describedBy,\n disabled,\n atLimit,\n textFirst = false,\n}: {\n id: string\n chips: SuggestChip[]\n onRemove: (key: string) => void\n placeholder: string\n /** What to offer for the text typed so far. */\n suggest: (query: string) => SuggestOption[]\n onPick: (key: string) => void\n /** Enter with no suggestion chosen; return true when the text was used. */\n onEnterText?: (text: string) => boolean\n invalid?: boolean\n describedBy?: string\n disabled?: boolean\n /** Nothing more can be added; chips can still come off. */\n atLimit?: boolean\n textFirst?: boolean\n}) {\n const listId = useId()\n const [query, setQuery] = useState('')\n const [active, setActive] = useState(0)\n const [open, setOpen] = useState(false)\n // An arrow moved the highlight since the text last changed.\n const [moved, setMoved] = useState(false)\n const options = query.trim() ? suggest(query.trim()) : []\n const shown = open && options.length > 0\n const activeIndex =\n textFirst && !moved ? -1 : Math.min(active, options.length - 1)\n const optionId = (i: number) => `${listId}-${i}`\n\n const pick = (key: string) => {\n onPick(key)\n setQuery('')\n setActive(0)\n setMoved(false)\n }\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n e.preventDefault()\n // After Esc the list is shut; an arrow brings it back.\n if (!shown) {\n setOpen(true)\n return\n }\n const step = e.key === 'ArrowDown' ? 1 : -1\n setActive(Math.min(options.length - 1, Math.max(0, activeIndex + step)))\n setMoved(true)\n } else if (e.key === 'Enter') {\n // Never submits the form: Enter here means \"add this\".\n e.preventDefault()\n const option = !shown\n ? undefined\n : activeIndex >= 0\n ? options[activeIndex]\n : options.find((o) => o.exact)\n if (option) pick(option.key)\n else if (query.trim() && onEnterText?.(query)) setQuery('')\n } else if (e.key === 'Backspace' && !query && chips.length > 0) {\n onRemove(chips[chips.length - 1]!.key)\n } else if (e.key === 'Escape' && shown) {\n // The dialog leaves an open list's Escape to it (see ToolFormDialog).\n e.preventDefault()\n setOpen(false)\n }\n }\n\n return (\n <div className=\"relative\">\n <div\n className={cn(\n FIELD_BOX,\n 'flex-wrap gap-1.5 px-2 py-[5px]',\n invalid && FIELD_BAD,\n )}\n >\n {chips.map((chip) => (\n <span\n key={chip.key}\n data-testid=\"picker-chip\"\n className=\"inline-flex h-7 items-center gap-1.5 rounded-full bg-secondary pr-1 pl-[3px] text-[13px] font-semibold\"\n >\n {chip.icon}\n <span data-testid=\"picker-chip-label\">{chip.label}</span>\n <button\n type=\"button\"\n aria-label={`Remove ${chip.label}`}\n disabled={disabled}\n onClick={() => onRemove(chip.key)}\n className=\"inline-flex size-5 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <X aria-hidden=\"true\" className=\"size-3\" />\n </button>\n </span>\n ))}\n <input\n id={id}\n type=\"text\"\n role=\"combobox\"\n autoComplete=\"off\"\n aria-expanded={shown}\n aria-controls={shown ? listId : undefined}\n aria-autocomplete=\"list\"\n aria-activedescendant={\n shown && activeIndex >= 0 ? optionId(activeIndex) : undefined\n }\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n disabled={disabled || atLimit}\n placeholder={placeholder}\n value={query}\n onChange={(e) => {\n setQuery(e.target.value)\n setActive(0)\n setMoved(false)\n setOpen(true)\n }}\n onFocus={() => setOpen(true)}\n onBlur={() => setOpen(false)}\n onKeyDown={onKeyDown}\n className=\"h-7 min-w-32 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground\"\n />\n <Search\n aria-hidden=\"true\"\n className=\"size-4 text-muted-foreground/70\"\n />\n </div>\n {shown && (\n <ul\n id={listId}\n role=\"listbox\"\n className=\"absolute inset-x-0 top-full z-10 mt-0.5 flex flex-col rounded-[10px] border border-border bg-card p-1 shadow-lg\"\n >\n {options.map((option, i) => (\n <li\n key={option.key}\n id={optionId(i)}\n role=\"option\"\n aria-selected={i === activeIndex}\n // Keeps the caret in the box, so the next one can be typed at once.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => pick(option.key)}\n onMouseEnter={() => setActive(i)}\n className={cn(\n 'flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-[7px] text-[13.5px] hover:bg-accent',\n i === activeIndex && 'bg-accent',\n )}\n >\n {option.icon}\n <span className=\"flex flex-col leading-tight\">\n <b className=\"font-semibold\">\n {highlightMatches(option.label, query.trim())}\n </b>\n {option.sub && (\n <span className=\"text-xs text-muted-foreground\">\n {option.sub}\n </span>\n )}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n )\n}\n\nexport function personName(person: Person): string {\n return `${person.firstName} ${person.lastName}`.trim() || person.slug\n}\n\nconst MAX_OWNERS = 5\nconst MAX_SUGGESTIONS = 6\nconst OWNER_NAME_MAX = 80\n/** The option that adds the typed text as an owner outside the directory. */\nconst NAMED = 'named:'\n\nfunction ownerKey(owner: PersonRef): string {\n return owner.slug ?? `${NAMED}${owner.name}`\n}\n\n/**\n * Owners from the person directory, searched by name or email, and people it\n * does not know, by name: chips, remove.\n */\nexport function OwnerPicker({\n id,\n persons,\n value,\n onChange,\n invalid,\n describedBy,\n disabled,\n}: {\n id: string\n persons: Person[]\n /** In the order they were added; `slug` is null outside the directory. */\n value: PersonRef[]\n onChange: (owners: PersonRef[]) => void\n invalid?: boolean\n describedBy?: string\n disabled?: boolean\n}) {\n const full = value.length >= MAX_OWNERS\n const taken = new Set(value.map(ownerKey))\n\n return (\n <SuggestInput\n id={id}\n chips={value.map((owner) => ({\n key: ownerKey(owner),\n label: owner.name,\n icon: <PersonAvatar name={owner.name} />,\n }))}\n onRemove={(key) => onChange(value.filter((o) => ownerKey(o) !== key))}\n placeholder={full ? 'Five owners at most' : 'Add a person'}\n disabled={disabled}\n atLimit={full}\n invalid={invalid}\n describedBy={describedBy}\n suggest={(query) => {\n const words = query.toLowerCase().split(/\\s+/)\n const people = persons\n .filter((p) => !taken.has(p.slug))\n .filter((p) => {\n const text = `${personName(p)} ${p.email ?? p.slug}`.toLowerCase()\n return words.every((w) => text.includes(w))\n })\n .slice(0, MAX_SUGGESTIONS)\n .map((p) => ({\n key: p.slug,\n label: personName(p),\n sub: p.email ?? p.slug,\n icon: <PersonAvatar name={personName(p)} />,\n }))\n // Someone the directory does not know is still someone to ask.\n const name = query.trim().slice(0, OWNER_NAME_MAX)\n const known = people.some(\n (p) => p.label.toLowerCase() === name.toLowerCase(),\n )\n if (known || taken.has(`${NAMED}${name}`)) return people\n return [\n ...people,\n {\n key: `${NAMED}${name}`,\n label: `Add “${name}”`,\n sub: 'Not in the people list',\n icon: <PersonAvatar name={name} />,\n },\n ]\n }}\n onPick={(key) => {\n if (key.startsWith(NAMED)) {\n onChange([...value, { slug: null, name: key.slice(NAMED.length) }])\n return\n }\n const person = persons.find((p) => p.slug === key)\n if (person)\n onChange([...value, { slug: key, name: personName(person) }])\n }}\n />\n )\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The star on rows, tiles and the card: pins the tool to My tools, or takes it
3
+ * off. The card's is `labelled`, a bordered button that says Pin or Pinned;
4
+ * a feed row's is `small`, a 32 px target in its meta line.
5
+ */
6
+ export declare function PinButton({ pinned, name, onToggle, labelled, small, busy, }: {
7
+ pinned: boolean;
8
+ name: string;
9
+ onToggle: () => void;
10
+ labelled?: boolean;
11
+ small?: boolean;
12
+ /** The pin is being saved; the star shows the state it will have. */
13
+ busy?: boolean;
14
+ }): import("react/jsx-runtime").JSX.Element;