@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,766 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { hostOf, TAG_VALUE_MAX } from "@igstack/app-catalog-shared-core";
3
+ import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
4
+ import { Link } from "@tanstack/react-router";
5
+ import { X, Loader2, CheckCircle2, AlertTriangle, Archive, AlertCircle, Camera, ImageOff } from "lucide-react";
6
+ import { useId, useState, useRef, useEffect } from "react";
7
+ import { useTRPC } from "../../../api/infra/trpc.js";
8
+ import { cn } from "../../../lib/utils.js";
9
+ import { useAppCatalogContext } from "../../appCatalog/context/AppCatalogContext.js";
10
+ import { MarkdownText } from "../../appCatalog/ui/components/MarkdownText.js";
11
+ import { useAuth } from "../../auth/AuthContext.js";
12
+ import "../../../ui/button.js";
13
+ import "../../auth/AuthModalContext.js";
14
+ import { useDismissable } from "../../dismiss/DismissStack.js";
15
+ import { Dialog, DialogContent, DialogTitle } from "../../../ui/dialog.js";
16
+ import { formatCardDate } from "./CardScreenshot.js";
17
+ import { screenshotUrl } from "./FeedRow.js";
18
+ import { personName, FIELD_BAD, FIELD_BOX, OwnerPicker } from "./OwnerPicker.js";
19
+ import { TagPicker } from "./TagPicker.js";
20
+ const PREVIEW_DELAY_MS = 400;
21
+ const NAME_MAX = 80;
22
+ const SUMMARY_MAX = 160;
23
+ const DESCRIPTION_MAX = 5e3;
24
+ const GHOST_BUTTON = "inline-flex h-[34px] items-center gap-[7px] rounded-[10px] border border-border bg-card px-[11px] text-[13px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-50";
25
+ const PRIMARY_BUTTON = "inline-flex h-[34px] items-center gap-2 rounded-[10px] bg-primary px-4 text-[13.5px] font-extrabold whitespace-nowrap text-primary-foreground shadow-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-not-allowed disabled:opacity-50";
26
+ const LABEL = "text-[13px] font-bold";
27
+ const HINT = "text-[12.5px] text-muted-foreground";
28
+ function FieldError({ id, children }) {
29
+ return /* @__PURE__ */ jsxs(
30
+ "p",
31
+ {
32
+ id,
33
+ role: "alert",
34
+ className: "flex items-center gap-1.5 text-[12.5px] font-bold text-destructive",
35
+ children: [
36
+ /* @__PURE__ */ jsx(AlertCircle, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
37
+ children
38
+ ]
39
+ }
40
+ );
41
+ }
42
+ function ToolFormDialog({
43
+ mode,
44
+ initial,
45
+ prefillName,
46
+ onDone,
47
+ onClose,
48
+ onOpenCard
49
+ }) {
50
+ const trpc = useTRPC();
51
+ const queryClient = useQueryClient();
52
+ const { user } = useAuth();
53
+ const { persons, resources } = useAppCatalogContext();
54
+ const list = useQuery(trpc.communityTools.list.queryOptions());
55
+ const formId = useId();
56
+ const ids = {
57
+ url: `${formId}-url`,
58
+ name: `${formId}-name`,
59
+ summary: `${formId}-summary`,
60
+ owners: `${formId}-owners`,
61
+ tags: `${formId}-tags`,
62
+ description: `${formId}-description`
63
+ };
64
+ const errorId = (field) => `${field}-error`;
65
+ const [url, setUrl] = useState((initial == null ? void 0 : initial.url) ?? "");
66
+ const [name, setName] = useState((initial == null ? void 0 : initial.name) ?? prefillName ?? "");
67
+ const [nameTouched, setNameTouched] = useState(mode === "edit");
68
+ const nameTouchedRef = useRef(nameTouched);
69
+ const [summary, setSummary] = useState((initial == null ? void 0 : initial.summary) ?? "");
70
+ const [descriptionMd, setDescriptionMd] = useState(
71
+ (initial == null ? void 0 : initial.descriptionMd) ?? ""
72
+ );
73
+ const [owners, setOwners] = useState(() => {
74
+ var _a;
75
+ if (initial) return initial.owners;
76
+ const email = (_a = user == null ? void 0 : user.email) == null ? void 0 : _a.toLowerCase();
77
+ const me = email ? persons.find(
78
+ (p) => {
79
+ var _a2;
80
+ return ((_a2 = p.email) == null ? void 0 : _a2.toLowerCase()) === email || p.slug.toLowerCase() === email;
81
+ }
82
+ ) : void 0;
83
+ return me ? [{ slug: me.slug, name: personName(me) }] : [];
84
+ });
85
+ const [tags, setTags] = useState((initial == null ? void 0 : initial.tags) ?? []);
86
+ const [tab, setTab] = useState("write");
87
+ const [errors, setErrors] = useState({});
88
+ const [archiving, setArchiving] = useState(false);
89
+ const [checked, setChecked] = useState((initial == null ? void 0 : initial.url) ?? "");
90
+ const [preview, setPreview] = useState(null);
91
+ const [addAnywayFor, setAddAnywayFor] = useState(null);
92
+ const previewMutation = useMutation(
93
+ trpc.communityTools.preview.mutationOptions()
94
+ );
95
+ const { mutateAsync: readSite } = previewMutation;
96
+ useEffect(() => {
97
+ const address2 = url.trim();
98
+ let stale = false;
99
+ const timer = setTimeout(() => {
100
+ setChecked(address2);
101
+ if (!needsPreviewFor(address2, mode, initial == null ? void 0 : initial.url)) return;
102
+ readSite({ url: address2 }).then(
103
+ (data) => {
104
+ if (stale) return;
105
+ setPreview({ url: address2, data });
106
+ if (data.title && !nameTouchedRef.current) setName(data.title);
107
+ },
108
+ () => {
109
+ if (!stale) setPreview({ url: address2, data: null });
110
+ }
111
+ );
112
+ }, PREVIEW_DELAY_MS);
113
+ return () => {
114
+ stale = true;
115
+ clearTimeout(timer);
116
+ };
117
+ }, [url, mode, initial == null ? void 0 : initial.url, readSite]);
118
+ const address = url.trim();
119
+ const validAddress = hostOf(address) !== null;
120
+ const showBadAddress = checked !== "" && checked === address && !validAddress;
121
+ const reading = needsPreviewFor(address, mode, initial == null ? void 0 : initial.url) && (checked !== address || (preview == null ? void 0 : preview.url) !== address);
122
+ const current = (preview == null ? void 0 : preview.url) === address ? preview.data : null;
123
+ const duplicate = (current == null ? void 0 : current.duplicate) && current.duplicate.slug !== (initial == null ? void 0 : initial.slug) ? current.duplicate : null;
124
+ const officialApp = (current == null ? void 0 : current.officialApp) ?? null;
125
+ const blockedByDuplicate = (officialApp !== null || duplicate !== null) && addAnywayFor !== address;
126
+ const showDetails = mode === "edit" || validAddress && !reading && !blockedByDuplicate || !validAddress && name.trim() !== "" && !showBadAddress;
127
+ const invalidate = () => queryClient.invalidateQueries({
128
+ queryKey: trpc.communityTools.pathKey()
129
+ });
130
+ const add = useMutation(
131
+ trpc.communityTools.add.mutationOptions({ onSuccess: invalidate })
132
+ );
133
+ const edit = useMutation(
134
+ trpc.communityTools.edit.mutationOptions({ onSuccess: invalidate })
135
+ );
136
+ const archive = useMutation(
137
+ trpc.communityTools.archive.mutationOptions({ onSuccess: invalidate })
138
+ );
139
+ const save = mode === "add" ? add : edit;
140
+ const busy = save.isPending || archive.isPending;
141
+ const urlRef = useRef(null);
142
+ const submit = (e) => {
143
+ var _a;
144
+ e.preventDefault();
145
+ if (busy || !validAddress) return;
146
+ const found = {};
147
+ if (!name.trim()) found.name = "Give it the name colleagues call it.";
148
+ if (owners.length === 0) {
149
+ found.owners = "Add at least one person to ask about it.";
150
+ }
151
+ setErrors(found);
152
+ const first = ["name", "owners"].find((f) => found[f]);
153
+ if (first) {
154
+ (_a = document.getElementById(ids[first])) == null ? void 0 : _a.focus();
155
+ return;
156
+ }
157
+ const input = {
158
+ url: address,
159
+ name: name.trim(),
160
+ summary: summary.trim(),
161
+ descriptionMd,
162
+ ownerSlugs: owners.flatMap((o) => o.slug ? [o.slug] : []),
163
+ ownerNames: owners.flatMap((o) => o.slug ? [] : [o.name]),
164
+ tags
165
+ };
166
+ if (mode === "add") {
167
+ add.mutate(input, { onSuccess: (tool) => onDone(tool.slug) });
168
+ } else if (initial) {
169
+ edit.mutate(
170
+ { ...input, slug: initial.slug },
171
+ { onSuccess: (tool) => onDone(tool.slug) }
172
+ );
173
+ }
174
+ };
175
+ const contentRef = useRef(null);
176
+ useDismissable(contentRef, () => {
177
+ if (!busy) onClose();
178
+ });
179
+ const officialApps = resources.filter((r) => !r.parentSlug);
180
+ const knownTopics = [
181
+ ...new Set(
182
+ (list.data ?? []).flatMap(
183
+ (t) => t.tags.filter((g) => g.kind === "topic").map((g) => g.value)
184
+ )
185
+ )
186
+ ].sort();
187
+ const title = mode === "add" ? "Add a tool" : "Edit tool";
188
+ const serverError = save.error ?? archive.error;
189
+ return /* @__PURE__ */ jsx(
190
+ Dialog,
191
+ {
192
+ open: true,
193
+ onOpenChange: (open) => {
194
+ if (!open && !busy) onClose();
195
+ },
196
+ children: /* @__PURE__ */ jsx(
197
+ DialogContent,
198
+ {
199
+ ref: contentRef,
200
+ "data-testid": mode === "add" ? "add-tool-dialog" : "edit-tool-dialog",
201
+ "aria-describedby": void 0,
202
+ showCloseButton: false,
203
+ className: "top-[6vh] max-h-[88vh] translate-y-0 gap-0 overflow-y-auto p-0 sm:max-w-[680px]",
204
+ onOpenAutoFocus: (e) => {
205
+ var _a;
206
+ e.preventDefault();
207
+ (_a = urlRef.current) == null ? void 0 : _a.focus();
208
+ },
209
+ onInteractOutside: (e) => e.preventDefault(),
210
+ onEscapeKeyDown: (e) => {
211
+ const target = e.target;
212
+ if (target == null ? void 0 : target.closest('[aria-expanded="true"]')) e.preventDefault();
213
+ },
214
+ children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, noValidate: true, "aria-busy": busy || void 0, children: [
215
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 pt-4", children: [
216
+ /* @__PURE__ */ jsx(DialogTitle, { className: "font-serif text-[19px] leading-tight font-semibold", children: title }),
217
+ /* @__PURE__ */ jsx(
218
+ "button",
219
+ {
220
+ type: "button",
221
+ "aria-label": "Close",
222
+ disabled: busy,
223
+ onClick: onClose,
224
+ className: "rounded-full p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50",
225
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
226
+ }
227
+ )
228
+ ] }),
229
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3.5 px-5 pt-4 pb-1.5", children: [
230
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
231
+ /* @__PURE__ */ jsx("label", { htmlFor: ids.url, className: LABEL, children: "Site address" }),
232
+ /* @__PURE__ */ jsxs("div", { className: cn(FIELD_BOX, showBadAddress && FIELD_BAD), children: [
233
+ /* @__PURE__ */ jsx(
234
+ "input",
235
+ {
236
+ ref: urlRef,
237
+ id: ids.url,
238
+ type: "url",
239
+ inputMode: "url",
240
+ autoComplete: "off",
241
+ placeholder: "Paste the link to the tool",
242
+ value: url,
243
+ disabled: busy,
244
+ "aria-invalid": showBadAddress || void 0,
245
+ "aria-describedby": showBadAddress ? errorId(ids.url) : `${ids.url}-hint`,
246
+ onChange: (e) => setUrl(e.target.value),
247
+ className: "h-10 min-w-0 flex-1 bg-transparent font-mono text-[13px] outline-none placeholder:font-sans placeholder:text-muted-foreground"
248
+ }
249
+ ),
250
+ validAddress && reading && /* @__PURE__ */ jsxs(
251
+ "span",
252
+ {
253
+ "data-testid": "reading-site",
254
+ className: "inline-flex shrink-0 items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground",
255
+ children: [
256
+ /* @__PURE__ */ jsx(
257
+ Loader2,
258
+ {
259
+ "aria-hidden": "true",
260
+ className: "size-3.5 animate-spin"
261
+ }
262
+ ),
263
+ "Reading the site…"
264
+ ]
265
+ }
266
+ ),
267
+ validAddress && !reading && ((current == null ? void 0 : current.title) || (current == null ? void 0 : current.screenshotDataUrl)) && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5 text-[12.5px] font-semibold text-green-700 dark:text-green-400", children: [
268
+ /* @__PURE__ */ jsx(CheckCircle2, { "aria-hidden": "true", className: "size-3.5" }),
269
+ "Found"
270
+ ] })
271
+ ] }),
272
+ showBadAddress ? /* @__PURE__ */ jsx(FieldError, { id: errorId(ids.url), children: "That is not a web address. Paste the full link, starting with https://" }) : !validAddress && !showDetails && !blockedByDuplicate && /* @__PURE__ */ jsx("p", { id: `${ids.url}-hint`, className: HINT, children: "Paste it first. The name and screenshot fill in from the site." })
273
+ ] }),
274
+ validAddress && reading && /* @__PURE__ */ jsx(ReadingSkeleton, {}),
275
+ blockedByDuplicate && officialApp && /* @__PURE__ */ jsxs(
276
+ "div",
277
+ {
278
+ "data-testid": "official-app-warning",
279
+ role: "alert",
280
+ className: "flex gap-2.5 rounded-[10px] bg-amber-50 px-3.5 py-3 text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200",
281
+ children: [
282
+ /* @__PURE__ */ jsx(
283
+ AlertTriangle,
284
+ {
285
+ "aria-hidden": "true",
286
+ className: "mt-px size-[18px] shrink-0"
287
+ }
288
+ ),
289
+ /* @__PURE__ */ jsxs("div", { children: [
290
+ /* @__PURE__ */ jsxs("b", { children: [
291
+ "This is already in the catalog as ",
292
+ officialApp.name,
293
+ "."
294
+ ] }),
295
+ /* @__PURE__ */ jsxs("div", { className: "mt-2.5 flex items-center gap-3.5", children: [
296
+ /* @__PURE__ */ jsxs(
297
+ Link,
298
+ {
299
+ to: "/app/$slug",
300
+ params: { slug: officialApp.slug },
301
+ className: GHOST_BUTTON,
302
+ children: [
303
+ "Open ",
304
+ officialApp.name
305
+ ]
306
+ }
307
+ ),
308
+ /* @__PURE__ */ jsx(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ className: "font-bold underline underline-offset-2",
313
+ onClick: () => setAddAnywayFor(address),
314
+ children: "Add anyway"
315
+ }
316
+ )
317
+ ] })
318
+ ] })
319
+ ]
320
+ }
321
+ ),
322
+ blockedByDuplicate && !officialApp && duplicate && /* @__PURE__ */ jsxs(
323
+ "div",
324
+ {
325
+ "data-testid": "duplicate-warning",
326
+ role: "alert",
327
+ className: "flex gap-2.5 rounded-[10px] bg-amber-50 px-3.5 py-3 text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200",
328
+ children: [
329
+ /* @__PURE__ */ jsx(
330
+ AlertTriangle,
331
+ {
332
+ "aria-hidden": "true",
333
+ className: "mt-px size-[18px] shrink-0"
334
+ }
335
+ ),
336
+ /* @__PURE__ */ jsxs("div", { children: [
337
+ /* @__PURE__ */ jsx("b", { children: "This site is already here." }),
338
+ " “",
339
+ duplicate.name,
340
+ "” uses the same address.",
341
+ /* @__PURE__ */ jsxs("div", { className: "mt-2.5 flex items-center gap-3.5", children: [
342
+ /* @__PURE__ */ jsx(
343
+ "button",
344
+ {
345
+ type: "button",
346
+ className: GHOST_BUTTON,
347
+ onClick: () => onOpenCard == null ? void 0 : onOpenCard(duplicate.slug),
348
+ children: "Open that card"
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsx(
352
+ "button",
353
+ {
354
+ type: "button",
355
+ className: "font-bold underline underline-offset-2",
356
+ onClick: () => setAddAnywayFor(address),
357
+ children: "Add anyway"
358
+ }
359
+ )
360
+ ] })
361
+ ] })
362
+ ]
363
+ }
364
+ ),
365
+ showDetails && /* @__PURE__ */ jsxs(Fragment, { children: [
366
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_190px] items-start gap-4", children: [
367
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-3.5", children: [
368
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
369
+ /* @__PURE__ */ jsx("label", { htmlFor: ids.name, className: LABEL, children: "Name" }),
370
+ /* @__PURE__ */ jsx(
371
+ "div",
372
+ {
373
+ className: cn(
374
+ FIELD_BOX,
375
+ !nameTouched && (current == null ? void 0 : current.title) && name === current.title && "bg-amber-50/70 dark:bg-amber-950/40",
376
+ errors.name && FIELD_BAD
377
+ ),
378
+ children: /* @__PURE__ */ jsx(
379
+ "input",
380
+ {
381
+ id: ids.name,
382
+ type: "text",
383
+ maxLength: NAME_MAX,
384
+ value: name,
385
+ disabled: busy,
386
+ "aria-invalid": !!errors.name || void 0,
387
+ "aria-describedby": errors.name ? errorId(ids.name) : `${ids.name}-hint`,
388
+ onChange: (e) => {
389
+ setName(e.target.value);
390
+ setNameTouched(true);
391
+ nameTouchedRef.current = true;
392
+ setErrors((prev) => ({ ...prev, name: void 0 }));
393
+ },
394
+ className: "h-10 min-w-0 flex-1 bg-transparent outline-none"
395
+ }
396
+ )
397
+ }
398
+ ),
399
+ errors.name ? /* @__PURE__ */ jsx(FieldError, { id: errorId(ids.name), children: errors.name }) : (current == null ? void 0 : current.title) && !nameTouched && /* @__PURE__ */ jsx("p", { id: `${ids.name}-hint`, className: HINT, children: "From the page title. Change it if colleagues call it something else." })
400
+ ] }),
401
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
402
+ /* @__PURE__ */ jsx("label", { htmlFor: ids.owners, className: LABEL, children: "Owners" }),
403
+ /* @__PURE__ */ jsx(
404
+ OwnerPicker,
405
+ {
406
+ id: ids.owners,
407
+ persons,
408
+ value: owners,
409
+ disabled: busy,
410
+ invalid: !!errors.owners,
411
+ describedBy: errors.owners ? errorId(ids.owners) : `${ids.owners}-hint`,
412
+ onChange: (next) => {
413
+ setOwners(next);
414
+ setErrors((prev) => ({ ...prev, owners: void 0 }));
415
+ }
416
+ }
417
+ ),
418
+ errors.owners ? /* @__PURE__ */ jsx(FieldError, { id: errorId(ids.owners), children: errors.owners }) : /* @__PURE__ */ jsx("p", { id: `${ids.owners}-hint`, className: HINT, children: "Who to ask about it. You are added by default." })
419
+ ] })
420
+ ] }),
421
+ /* @__PURE__ */ jsx(
422
+ ShotPreview,
423
+ {
424
+ preview: current,
425
+ initial: address === (initial == null ? void 0 : initial.url) ? initial : void 0,
426
+ validAddress
427
+ }
428
+ )
429
+ ] }),
430
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
431
+ /* @__PURE__ */ jsxs("label", { htmlFor: ids.summary, className: LABEL, children: [
432
+ "Summary",
433
+ " ",
434
+ /* @__PURE__ */ jsx("span", { className: "font-normal text-muted-foreground", children: "(optional)" })
435
+ ] }),
436
+ /* @__PURE__ */ jsx("div", { className: FIELD_BOX, children: /* @__PURE__ */ jsx(
437
+ "input",
438
+ {
439
+ id: ids.summary,
440
+ type: "text",
441
+ maxLength: SUMMARY_MAX,
442
+ value: summary,
443
+ disabled: busy,
444
+ "aria-describedby": `${ids.summary}-hint`,
445
+ onChange: (e) => setSummary(e.target.value),
446
+ className: "h-10 min-w-0 flex-1 bg-transparent outline-none"
447
+ }
448
+ ) }),
449
+ /* @__PURE__ */ jsx("p", { id: `${ids.summary}-hint`, className: HINT, children: "One line under the name, in the list and on the card." })
450
+ ] }),
451
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
452
+ /* @__PURE__ */ jsxs("label", { htmlFor: ids.tags, className: LABEL, children: [
453
+ "Tags",
454
+ " ",
455
+ /* @__PURE__ */ jsx("span", { className: "font-normal text-muted-foreground", children: "(optional)" })
456
+ ] }),
457
+ /* @__PURE__ */ jsx(
458
+ TagPicker,
459
+ {
460
+ id: ids.tags,
461
+ apps: officialApps,
462
+ knownTopics,
463
+ value: tags,
464
+ disabled: busy,
465
+ describedBy: errors.tags ? errorId(ids.tags) : `${ids.tags}-hint`,
466
+ onChange: (next) => {
467
+ setTags(next);
468
+ setErrors((prev) => ({ ...prev, tags: void 0 }));
469
+ },
470
+ onTooLong: () => setErrors((prev) => ({
471
+ ...prev,
472
+ tags: `A topic is at most ${TAG_VALUE_MAX} characters.`
473
+ }))
474
+ }
475
+ ),
476
+ errors.tags ? /* @__PURE__ */ jsx(FieldError, { id: errorId(ids.tags), children: errors.tags }) : /* @__PURE__ */ jsx("p", { id: `${ids.tags}-hint`, className: HINT, children: "The app it is for, or a topic word. Press Enter to add a new topic." })
477
+ ] }),
478
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
479
+ /* @__PURE__ */ jsx("label", { htmlFor: ids.description, className: LABEL, children: "How to use" }),
480
+ /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-[10px] border border-border bg-card", children: [
481
+ /* @__PURE__ */ jsxs(
482
+ "div",
483
+ {
484
+ role: "tablist",
485
+ "aria-label": "Write or preview",
486
+ className: "flex items-center gap-0.5 border-b border-border bg-muted p-1",
487
+ children: [
488
+ ["write", "preview"].map((t) => /* @__PURE__ */ jsx(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ role: "tab",
493
+ "aria-selected": tab === t,
494
+ onClick: () => setTab(t),
495
+ className: cn(
496
+ "rounded-[7px] px-3 py-1 text-[12.5px] font-bold text-muted-foreground",
497
+ tab === t && "bg-card text-foreground shadow-sm"
498
+ ),
499
+ children: t === "write" ? "Write" : "Preview"
500
+ },
501
+ t
502
+ )),
503
+ /* @__PURE__ */ jsx("span", { className: "ml-auto pr-2 text-[11.5px] text-muted-foreground", children: "Markdown" })
504
+ ]
505
+ }
506
+ ),
507
+ /* @__PURE__ */ jsx(
508
+ "textarea",
509
+ {
510
+ id: ids.description,
511
+ rows: 6,
512
+ maxLength: DESCRIPTION_MAX,
513
+ value: descriptionMd,
514
+ disabled: busy,
515
+ hidden: tab !== "write",
516
+ "aria-describedby": `${ids.description}-hint`,
517
+ onChange: (e) => setDescriptionMd(e.target.value),
518
+ className: "block w-full resize-y bg-transparent px-3 py-2.5 font-mono text-[12.5px] leading-relaxed outline-none"
519
+ }
520
+ ),
521
+ tab === "preview" && /* @__PURE__ */ jsx(
522
+ "div",
523
+ {
524
+ "data-testid": "description-preview",
525
+ role: "tabpanel",
526
+ className: "prose prose-sm dark:prose-invert min-h-24 max-w-none px-3.5 py-2.5",
527
+ children: descriptionMd.trim() ? /* @__PURE__ */ jsx(MarkdownText, { children: descriptionMd }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Nothing written yet." })
528
+ }
529
+ )
530
+ ] }),
531
+ /* @__PURE__ */ jsx("p", { id: `${ids.description}-hint`, className: HINT, children: "Short steps work best. This is the text people read on the card." })
532
+ ] })
533
+ ] })
534
+ ] }),
535
+ serverError && /* @__PURE__ */ jsxs(
536
+ "p",
537
+ {
538
+ role: "alert",
539
+ className: "mx-5 mt-2 rounded-[10px] bg-destructive/10 px-3 py-2 text-[13px] font-semibold text-destructive",
540
+ children: [
541
+ archive.error ? "Could not archive it: " : "Could not save it: ",
542
+ serverError.message
543
+ ]
544
+ }
545
+ ),
546
+ archiving && initial ? /* @__PURE__ */ jsx("div", { className: "mt-2 border-t border-border px-5 pt-3 pb-4", children: /* @__PURE__ */ jsx(
547
+ ArchiveConfirm,
548
+ {
549
+ busy: archive.isPending,
550
+ onCancel: () => setArchiving(false),
551
+ onArchive: (reason) => archive.mutate(
552
+ { slug: initial.slug, reason },
553
+ { onSuccess: () => onDone(initial.slug) }
554
+ )
555
+ }
556
+ ) }) : /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-end gap-2.5 border-t border-border px-5 pt-3 pb-4", children: [
557
+ mode === "edit" && (initial == null ? void 0 : initial.lifecycle.state) !== "archived" && /* @__PURE__ */ jsxs(
558
+ "button",
559
+ {
560
+ type: "button",
561
+ disabled: busy,
562
+ onClick: () => setArchiving(true),
563
+ className: "inline-flex items-center gap-1.5 text-[13px] font-bold text-destructive hover:underline disabled:opacity-50",
564
+ children: [
565
+ /* @__PURE__ */ jsx(Archive, { "aria-hidden": "true", className: "size-3.5" }),
566
+ "Archive this tool"
567
+ ]
568
+ }
569
+ ),
570
+ /* @__PURE__ */ jsx(
571
+ "span",
572
+ {
573
+ className: cn(
574
+ "text-[12.5px] text-muted-foreground",
575
+ mode === "add" ? "mr-auto" : "ml-auto"
576
+ ),
577
+ children: mode === "add" ? "Goes live now and joins the review list." : "Changes show on the card right away."
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ disabled: busy,
585
+ onClick: onClose,
586
+ className: GHOST_BUTTON,
587
+ children: "Cancel"
588
+ }
589
+ ),
590
+ /* @__PURE__ */ jsxs(
591
+ "button",
592
+ {
593
+ type: "submit",
594
+ disabled: !validAddress || reading || busy,
595
+ "aria-busy": save.isPending || void 0,
596
+ className: PRIMARY_BUTTON,
597
+ children: [
598
+ save.isPending && /* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "size-4 animate-spin" }),
599
+ mode === "add" ? save.isPending ? "Publishing…" : "Publish" : save.isPending ? "Saving…" : "Save changes"
600
+ ]
601
+ }
602
+ )
603
+ ] })
604
+ ] })
605
+ }
606
+ )
607
+ }
608
+ );
609
+ }
610
+ function needsPreviewFor(address, mode, initialUrl) {
611
+ return hostOf(address) !== null && (mode === "add" || address !== initialUrl);
612
+ }
613
+ function ReadingSkeleton() {
614
+ return /* @__PURE__ */ jsxs(
615
+ "div",
616
+ {
617
+ "aria-hidden": "true",
618
+ className: "grid grid-cols-[minmax(0,1fr)_190px] items-start gap-4",
619
+ children: [
620
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3.5", children: ["55%", "40%"].map((w, i) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
621
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: i === 0 ? "Name" : "Owners" }),
622
+ /* @__PURE__ */ jsx("span", { className: cn(FIELD_BOX, "pointer-events-none"), children: /* @__PURE__ */ jsx(
623
+ "i",
624
+ {
625
+ className: "block h-3 animate-pulse rounded-md bg-muted",
626
+ style: { width: w }
627
+ }
628
+ ) })
629
+ ] }, w)) }),
630
+ /* @__PURE__ */ jsxs("div", { className: "mt-[22px] flex aspect-[16/11] flex-col items-center justify-center gap-1.5 rounded-[10px] border border-border bg-muted text-xs font-semibold text-muted-foreground", children: [
631
+ /* @__PURE__ */ jsx(Camera, { "aria-hidden": "true", className: "size-5 animate-pulse" }),
632
+ "Taking a screenshot…"
633
+ ] })
634
+ ]
635
+ }
636
+ );
637
+ }
638
+ function ShotPreview({
639
+ preview,
640
+ initial,
641
+ validAddress
642
+ }) {
643
+ const box = "relative mt-[22px] flex aspect-[16/11] flex-col items-center justify-center gap-1.5 overflow-hidden rounded-[10px] border border-border bg-muted px-3 text-center text-xs font-semibold text-muted-foreground";
644
+ const caption = "absolute bottom-1.5 left-1.5 inline-flex items-center gap-1 rounded-md bg-white/90 px-[7px] py-0.5 text-[11px] font-bold text-muted-foreground dark:bg-black/70";
645
+ if (preview == null ? void 0 : preview.screenshotDataUrl) {
646
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "shot-preview", className: box, children: [
647
+ /* @__PURE__ */ jsx(
648
+ "img",
649
+ {
650
+ src: preview.screenshotDataUrl,
651
+ alt: "Screenshot preview of the site",
652
+ className: "absolute inset-0 size-full object-cover object-left-top"
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsxs("span", { className: caption, children: [
656
+ /* @__PURE__ */ jsx(Camera, { "aria-hidden": "true", className: "size-3" }),
657
+ "Captured just now"
658
+ ] })
659
+ ] });
660
+ }
661
+ if (initial == null ? void 0 : initial.screenshotId) {
662
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "shot-preview", className: box, children: [
663
+ /* @__PURE__ */ jsx(
664
+ "img",
665
+ {
666
+ src: screenshotUrl(initial.screenshotId, 640),
667
+ alt: `Screenshot of ${initial.name}`,
668
+ className: "absolute inset-0 size-full object-cover object-left-top"
669
+ }
670
+ ),
671
+ initial.capturedAt && /* @__PURE__ */ jsxs("span", { className: caption, children: [
672
+ /* @__PURE__ */ jsx(Camera, { "aria-hidden": "true", className: "size-3" }),
673
+ "Captured ",
674
+ formatCardDate(initial.capturedAt)
675
+ ] })
676
+ ] });
677
+ }
678
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "shot-preview", "data-state": "none", className: box, children: [
679
+ /* @__PURE__ */ jsx(ImageOff, { "aria-hidden": "true", className: "size-5" }),
680
+ preview && !preview.captureAllowed ? "This site's address is outside the hosts we take screenshots of, so it will be listed without a picture." : validAddress ? "No preview. The site may need a login, or it did not load." : "The screenshot shows here once the address is in."
681
+ ] });
682
+ }
683
+ function ArchiveConfirm({
684
+ busy,
685
+ onArchive,
686
+ onCancel
687
+ }) {
688
+ const id = useId();
689
+ const inputRef = useRef(null);
690
+ const [reason, setReason] = useState("");
691
+ const [missing, setMissing] = useState(false);
692
+ useEffect(() => {
693
+ var _a;
694
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
695
+ }, []);
696
+ const confirm = () => {
697
+ var _a;
698
+ if (!reason.trim()) {
699
+ setMissing(true);
700
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
701
+ return;
702
+ }
703
+ onArchive(reason.trim());
704
+ };
705
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "archive-confirm", className: "flex flex-col gap-1.5", children: [
706
+ /* @__PURE__ */ jsx("label", { htmlFor: id, className: LABEL, children: "Why archive it?" }),
707
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
708
+ /* @__PURE__ */ jsx("div", { className: cn(FIELD_BOX, "flex-1", missing && FIELD_BAD), children: /* @__PURE__ */ jsx(
709
+ "input",
710
+ {
711
+ ref: inputRef,
712
+ id,
713
+ type: "text",
714
+ maxLength: 500,
715
+ value: reason,
716
+ disabled: busy,
717
+ "aria-invalid": missing || void 0,
718
+ "aria-describedby": missing ? `${id}-error` : `${id}-hint`,
719
+ onChange: (e) => {
720
+ setReason(e.target.value);
721
+ setMissing(false);
722
+ },
723
+ onKeyDown: (e) => {
724
+ if (e.key === "Enter") {
725
+ e.preventDefault();
726
+ confirm();
727
+ }
728
+ },
729
+ className: "h-10 min-w-0 flex-1 bg-transparent outline-none"
730
+ }
731
+ ) }),
732
+ /* @__PURE__ */ jsx(
733
+ "button",
734
+ {
735
+ type: "button",
736
+ disabled: busy,
737
+ onClick: onCancel,
738
+ className: GHOST_BUTTON,
739
+ children: "Keep it"
740
+ }
741
+ ),
742
+ /* @__PURE__ */ jsxs(
743
+ "button",
744
+ {
745
+ type: "button",
746
+ disabled: busy,
747
+ "aria-busy": busy || void 0,
748
+ onClick: confirm,
749
+ className: cn(PRIMARY_BUTTON, "bg-destructive text-white"),
750
+ children: [
751
+ busy && /* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "size-4 animate-spin" }),
752
+ busy ? "Archiving…" : "Archive"
753
+ ]
754
+ }
755
+ )
756
+ ] }),
757
+ missing ? /* @__PURE__ */ jsx(FieldError, { id: `${id}-error`, children: "Say why, so colleagues know what happened to it." }) : /* @__PURE__ */ jsx("p", { id: `${id}-hint`, className: HINT, children: "Colleagues read this on the card. It stays reachable by its link." })
758
+ ] });
759
+ }
760
+ export {
761
+ ArchiveConfirm,
762
+ GHOST_BUTTON,
763
+ PRIMARY_BUTTON,
764
+ ToolFormDialog
765
+ };
766
+ //# sourceMappingURL=ToolFormDialog.js.map