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