@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.
- package/dist/esm/__tests__/modules/auth/AuthContext.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/likedBy.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/timeAgo.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/useTimeUp.test.d.ts +1 -0
- package/dist/esm/api/infra/trpc.d.ts +419 -0
- package/dist/esm/api/infra/trpc.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.d.ts +2 -4
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +29 -101
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.d.ts +26 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js +108 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.d.ts +8 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js +73 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +19 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +4 -4
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +2 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +13 -9
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +4 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +10 -4
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +12 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +4 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js +16 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js +2 -2
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.js +7 -3
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/ui/LoginModal.js +16 -8
- package/dist/esm/modules/auth/ui/LoginModal.js.map +1 -1
- package/dist/esm/modules/communityTools/likedBy.d.ts +8 -0
- package/dist/esm/modules/communityTools/likedBy.js +23 -0
- package/dist/esm/modules/communityTools/likedBy.js.map +1 -0
- package/dist/esm/modules/communityTools/timeAgo.d.ts +2 -0
- package/dist/esm/modules/communityTools/timeAgo.js +18 -0
- package/dist/esm/modules/communityTools/timeAgo.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js +112 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.d.ts +8 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js +59 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js +547 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.d.ts +42 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js +260 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.d.ts +17 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js +84 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.d.ts +7 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js +50 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.d.ts +9 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js +56 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.d.ts +18 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js +115 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js +88 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.d.ts +56 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js +245 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/PinButton.d.ts +14 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js +72 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js +81 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.d.ts +21 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js +87 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagRow.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js +92 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/Toast.d.ts +19 -0
- package/dist/esm/modules/communityTools/ui/Toast.js +87 -0
- package/dist/esm/modules/communityTools/ui/Toast.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js +467 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.d.ts +32 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js +765 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.d.ts +15 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js +92 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityTools.d.ts +157 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js +122 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.d.ts +10 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js +20 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js.map +1 -0
- package/dist/esm/modules/communityTools/useTimeUp.d.ts +5 -0
- package/dist/esm/modules/communityTools/useTimeUp.js +13 -0
- package/dist/esm/modules/communityTools/useTimeUp.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +2 -2
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +1 -1
- package/dist/esm/routeTree.gen.d.ts +96 -3
- package/dist/esm/routeTree.gen.js +19 -0
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/community.$slug.d.ts +22 -0
- package/dist/esm/routes/_layout/community._slug.js +10 -0
- package/dist/esm/routes/_layout/community._slug.js.map +1 -0
- package/dist/esm/routes/_layout/community.d.ts +23 -0
- package/dist/esm/routes/_layout/community.js +71 -0
- package/dist/esm/routes/_layout/community.js.map +1 -0
- package/dist/esm/ui/components/header/Header.js +7 -3
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/util/highlightMatches.d.ts +2 -0
- package/dist/esm/util/highlightMatches.js +32 -0
- package/dist/esm/util/highlightMatches.js.map +1 -0
- package/dist/index.css +6 -0
- package/package.json +5 -5
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +4 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +10 -3
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +12 -2
- package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +1 -0
- package/src/__tests__/modules/auth/AuthContext.test.tsx +55 -0
- package/src/__tests__/modules/communityTools/likedBy.test.ts +45 -0
- package/src/__tests__/modules/communityTools/timeAgo.test.ts +28 -0
- package/src/__tests__/modules/communityTools/useTimeUp.test.ts +31 -0
- package/src/__tests__/ui/components/header/Header.login.test.tsx +4 -4
- package/src/api/infra/trpc.ts +2 -0
- package/src/index.css +6 -0
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +33 -176
- package/src/modules/appCatalog/ui/catalog/DetailDialogShell.tsx +199 -0
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -1
- package/src/modules/appCatalog/ui/components/SectionTabs.tsx +81 -0
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +17 -2
- package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +9 -4
- package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +6 -3
- package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +15 -10
- package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +10 -5
- package/src/modules/appCatalog/ui/detail/communityDraft.ts +16 -0
- package/src/modules/appCatalog/ui/detail/composeComments.ts +1 -0
- package/src/modules/appCatalog/ui/detail/useCommunityToolCount.ts +18 -0
- package/src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx +2 -2
- package/src/modules/auth/AuthContext.tsx +9 -3
- package/src/modules/auth/ui/LoginModal.tsx +7 -2
- package/src/modules/communityTools/likedBy.ts +32 -0
- package/src/modules/communityTools/timeAgo.ts +16 -0
- package/src/modules/communityTools/ui/CardScreenshot.tsx +124 -0
- package/src/modules/communityTools/ui/CardTitleBlock.tsx +58 -0
- package/src/modules/communityTools/ui/CommunityPage.tsx +626 -0
- package/src/modules/communityTools/ui/FeedRow.tsx +310 -0
- package/src/modules/communityTools/ui/FeedSortControl.tsx +97 -0
- package/src/modules/communityTools/ui/LifecycleBadge.tsx +44 -0
- package/src/modules/communityTools/ui/LikeButton.tsx +56 -0
- package/src/modules/communityTools/ui/MarginPreview.tsx +143 -0
- package/src/modules/communityTools/ui/MyToolsShelf.tsx +112 -0
- package/src/modules/communityTools/ui/OwnerPicker.tsx +311 -0
- package/src/modules/communityTools/ui/PinButton.tsx +87 -0
- package/src/modules/communityTools/ui/ShelfTile.tsx +83 -0
- package/src/modules/communityTools/ui/TagPicker.tsx +118 -0
- package/src/modules/communityTools/ui/TagRow.tsx +118 -0
- package/src/modules/communityTools/ui/Toast.tsx +105 -0
- package/src/modules/communityTools/ui/ToolCard.tsx +519 -0
- package/src/modules/communityTools/ui/ToolFormDialog.tsx +922 -0
- package/src/modules/communityTools/ui/ToolSearch.tsx +111 -0
- package/src/modules/communityTools/useCommunityTools.ts +146 -0
- package/src/modules/communityTools/useCommunityToolsConfig.ts +26 -0
- package/src/modules/communityTools/useTimeUp.ts +14 -0
- package/src/routeTree.gen.ts +52 -0
- package/src/routes/_layout/community.$slug.tsx +7 -0
- package/src/routes/_layout/community.tsx +97 -0
- package/src/ui/components/header/Header.tsx +9 -3
- package/src/util/highlightMatches.tsx +34 -0
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +0 -12
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +0 -56
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +0 -1
- package/src/modules/appCatalog/ui/components/ViewToggle.tsx +0 -59
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Link } from "@tanstack/react-router";
|
|
3
|
+
import { CornerDownLeft, Eye, ExternalLink, PanelRight, Globe } from "lucide-react";
|
|
4
|
+
import { cn } from "../../../lib/utils.js";
|
|
5
|
+
import { highlightTokens } from "../../../util/highlightMatches.js";
|
|
6
|
+
import { timeAgo } from "../timeAgo.js";
|
|
7
|
+
import { LifecycleBadge, UnreviewedBadge } from "./LifecycleBadge.js";
|
|
8
|
+
import { LikeButton } from "./LikeButton.js";
|
|
9
|
+
import { PinButton } from "./PinButton.js";
|
|
10
|
+
function screenshotUrl(id, size) {
|
|
11
|
+
return `/api/screenshots/${id}?size=${size}`;
|
|
12
|
+
}
|
|
13
|
+
function ToolThumb({
|
|
14
|
+
tool,
|
|
15
|
+
size,
|
|
16
|
+
className
|
|
17
|
+
}) {
|
|
18
|
+
return /* @__PURE__ */ jsx(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
className: cn(
|
|
22
|
+
"flex shrink-0 items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground",
|
|
23
|
+
className
|
|
24
|
+
),
|
|
25
|
+
children: tool.screenshotId ? /* @__PURE__ */ jsx(
|
|
26
|
+
"img",
|
|
27
|
+
{
|
|
28
|
+
src: screenshotUrl(tool.screenshotId, size),
|
|
29
|
+
alt: "",
|
|
30
|
+
loading: "lazy",
|
|
31
|
+
className: "size-full object-cover object-left-top"
|
|
32
|
+
}
|
|
33
|
+
) : /* @__PURE__ */ jsx(Globe, { "aria-hidden": "true", className: "size-1/4 min-w-4" })
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const AVATAR_TONES = [
|
|
38
|
+
"bg-orange-200",
|
|
39
|
+
"bg-sky-200",
|
|
40
|
+
"bg-lime-200",
|
|
41
|
+
"bg-amber-200",
|
|
42
|
+
"bg-rose-200",
|
|
43
|
+
"bg-violet-200"
|
|
44
|
+
];
|
|
45
|
+
function PersonAvatar({ name }) {
|
|
46
|
+
const initials = name.split(/\s+/).slice(0, 2).map((w) => w.charAt(0).toUpperCase()).join("");
|
|
47
|
+
let hash = 0;
|
|
48
|
+
for (const ch of name) hash = hash * 31 + ch.charCodeAt(0) | 0;
|
|
49
|
+
return /* @__PURE__ */ jsx(
|
|
50
|
+
"span",
|
|
51
|
+
{
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
className: cn(
|
|
54
|
+
"inline-flex size-[22px] shrink-0 items-center justify-center rounded-full text-[9.5px] font-extrabold text-stone-700",
|
|
55
|
+
AVATAR_TONES[Math.abs(hash) % AVATAR_TONES.length]
|
|
56
|
+
),
|
|
57
|
+
children: initials
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const FIELD_LABELS = {
|
|
62
|
+
name: "name",
|
|
63
|
+
summary: "summary",
|
|
64
|
+
description: "how to use",
|
|
65
|
+
owner: "author",
|
|
66
|
+
address: "address",
|
|
67
|
+
tag: "tag"
|
|
68
|
+
};
|
|
69
|
+
const ROW_THUMB = "h-[150px] w-[240px] rounded-[10px]";
|
|
70
|
+
const PANEL = "w-[120px]";
|
|
71
|
+
function FeedRow({
|
|
72
|
+
tool,
|
|
73
|
+
rank,
|
|
74
|
+
sort,
|
|
75
|
+
tokens,
|
|
76
|
+
matched,
|
|
77
|
+
selected = false,
|
|
78
|
+
optionId,
|
|
79
|
+
pinned,
|
|
80
|
+
pinBusy,
|
|
81
|
+
onTogglePin,
|
|
82
|
+
onToggleLike
|
|
83
|
+
}) {
|
|
84
|
+
const archived = tool.lifecycle.state === "archived";
|
|
85
|
+
const cell = optionId ? "gridcell" : void 0;
|
|
86
|
+
const owners = tool.owners.map((o) => o.name).join(", ");
|
|
87
|
+
const dated = sort === "newest" ? `Added ${timeAgo(tool.createdAt)}` : sort === "updated" && tool.siteUpdatedAt ? `Updated ${timeAgo(tool.siteUpdatedAt)}` : null;
|
|
88
|
+
const address = tool.url.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
89
|
+
return /* @__PURE__ */ jsxs(
|
|
90
|
+
"div",
|
|
91
|
+
{
|
|
92
|
+
"data-testid": "feed-row",
|
|
93
|
+
"data-slug": tool.slug,
|
|
94
|
+
"data-selected": selected || void 0,
|
|
95
|
+
role: optionId ? "row" : void 0,
|
|
96
|
+
className: cn(
|
|
97
|
+
"group/row relative flex min-h-[176px] items-center overflow-hidden rounded-[14px] border bg-card transition-[border-color,box-shadow]",
|
|
98
|
+
selected ? "border-primary ring-[3px] ring-primary/20" : "border-border hover:border-primary/35 hover:shadow-sm has-[:focus-visible]:border-primary/35",
|
|
99
|
+
archived && "opacity-55"
|
|
100
|
+
),
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsxs(
|
|
103
|
+
"div",
|
|
104
|
+
{
|
|
105
|
+
id: optionId,
|
|
106
|
+
role: cell,
|
|
107
|
+
"aria-selected": optionId ? selected : void 0,
|
|
108
|
+
className: "flex min-w-0 flex-1 items-center gap-4 self-stretch py-3 pl-3",
|
|
109
|
+
children: [
|
|
110
|
+
rank !== void 0 && /* @__PURE__ */ jsx("span", { className: "w-[26px] shrink-0 text-center font-serif text-[22px] font-semibold text-muted-foreground/70", children: rank }),
|
|
111
|
+
/* @__PURE__ */ jsx(ToolThumb, { tool, size: 512, className: ROW_THUMB }),
|
|
112
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 self-start pt-5 pr-6 pb-[52px]", children: [
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 text-[15.5px] font-extrabold", children: [
|
|
114
|
+
/* @__PURE__ */ jsx(
|
|
115
|
+
"a",
|
|
116
|
+
{
|
|
117
|
+
href: tool.url,
|
|
118
|
+
target: "_blank",
|
|
119
|
+
rel: "noopener noreferrer",
|
|
120
|
+
"data-site": true,
|
|
121
|
+
"data-testid": "feed-row-name",
|
|
122
|
+
"aria-label": `${tool.name}, opens ${tool.host} in a new tab`,
|
|
123
|
+
className: cn(
|
|
124
|
+
"truncate decoration-1 underline-offset-[3px] after:absolute after:inset-y-0 after:left-0 after:right-[120px] after:z-[1] after:rounded-l-[13px] focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-[3px] focus-visible:after:outline-ring",
|
|
125
|
+
"group-hover/row:underline group-has-[[data-details]:hover]/row:no-underline",
|
|
126
|
+
archived && "text-muted-foreground"
|
|
127
|
+
),
|
|
128
|
+
children: highlightTokens(tool.name, tokens)
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
selected && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11.5px] font-bold text-primary", children: [
|
|
132
|
+
/* @__PURE__ */ jsx(CornerDownLeft, { "aria-hidden": "true", className: "size-3" }),
|
|
133
|
+
"Enter opens site"
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsx(LifecycleBadge, { lifecycle: tool.lifecycle }),
|
|
136
|
+
!tool.reviewed && /* @__PURE__ */ jsx(UnreviewedBadge, {})
|
|
137
|
+
] }),
|
|
138
|
+
tool.summary && /* @__PURE__ */ jsx("div", { className: "mt-1 line-clamp-2 text-foreground/75", children: highlightTokens(tool.summary, tokens) }),
|
|
139
|
+
matched && matched.length > 0 && /* @__PURE__ */ jsxs(
|
|
140
|
+
"div",
|
|
141
|
+
{
|
|
142
|
+
"data-testid": "match-reason",
|
|
143
|
+
className: "mt-1 text-xs font-semibold text-muted-foreground",
|
|
144
|
+
children: [
|
|
145
|
+
"Matched: ",
|
|
146
|
+
matched.map((f) => FIELD_LABELS[f]).join(", ")
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ jsxs(
|
|
152
|
+
"div",
|
|
153
|
+
{
|
|
154
|
+
className: cn(
|
|
155
|
+
"pointer-events-none absolute right-[120px] bottom-0 z-[2] flex h-12 items-center gap-4",
|
|
156
|
+
rank !== void 0 ? "left-[310px]" : "left-[268px]"
|
|
157
|
+
),
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-[13px] font-semibold whitespace-nowrap text-foreground/75 [&>*]:shrink-0", children: [
|
|
160
|
+
tool.owners[0] && /* @__PURE__ */ jsx(PersonAvatar, { name: tool.owners[0].name }),
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
"span",
|
|
163
|
+
{
|
|
164
|
+
"data-testid": "feed-row-owner",
|
|
165
|
+
className: "mr-1 min-w-0 !shrink truncate",
|
|
166
|
+
children: highlightTokens(owners, tokens)
|
|
167
|
+
}
|
|
168
|
+
),
|
|
169
|
+
/* @__PURE__ */ jsxs("span", { className: "pointer-events-auto -mr-1 flex items-center", children: [
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
PinButton,
|
|
172
|
+
{
|
|
173
|
+
small: true,
|
|
174
|
+
pinned,
|
|
175
|
+
busy: pinBusy,
|
|
176
|
+
name: tool.name,
|
|
177
|
+
onToggle: onTogglePin
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
/* @__PURE__ */ jsx(LikeButton, { tool, onToggle: onToggleLike })
|
|
181
|
+
] }),
|
|
182
|
+
/* @__PURE__ */ jsxs(
|
|
183
|
+
"span",
|
|
184
|
+
{
|
|
185
|
+
"data-testid": "feed-row-views",
|
|
186
|
+
className: "inline-flex items-center gap-1 text-muted-foreground",
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ jsx(Eye, { "aria-hidden": "true", className: "size-[15px]" }),
|
|
189
|
+
tool.views30d,
|
|
190
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: " views this month" })
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
dated && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
195
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "·" }),
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
"span",
|
|
198
|
+
{
|
|
199
|
+
"data-testid": "feed-row-date",
|
|
200
|
+
className: "text-muted-foreground",
|
|
201
|
+
children: dated
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
] })
|
|
205
|
+
] }),
|
|
206
|
+
/* @__PURE__ */ jsxs(
|
|
207
|
+
"div",
|
|
208
|
+
{
|
|
209
|
+
"aria-hidden": "true",
|
|
210
|
+
"data-testid": "feed-row-address",
|
|
211
|
+
className: cn(
|
|
212
|
+
"flex h-full min-w-[150px] flex-1 items-center justify-end gap-1.5 px-4 font-mono text-[12.5px] text-muted-foreground opacity-0 transition-opacity",
|
|
213
|
+
"group-hover/row:text-foreground group-hover/row:opacity-100 group-has-[[data-site]:focus-visible]/row:opacity-100 group-has-[[data-details]:hover]/row:opacity-0 [@media(hover:none)]:opacity-100",
|
|
214
|
+
selected && "opacity-100"
|
|
215
|
+
),
|
|
216
|
+
children: [
|
|
217
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: address }),
|
|
218
|
+
/* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-3.5 shrink-0" })
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
/* @__PURE__ */ jsx("div", { role: cell, className: "flex self-stretch", children: /* @__PURE__ */ jsx(
|
|
229
|
+
Link,
|
|
230
|
+
{
|
|
231
|
+
to: "/community/$slug",
|
|
232
|
+
params: { slug: tool.slug },
|
|
233
|
+
search: (prev) => prev,
|
|
234
|
+
"data-details": true,
|
|
235
|
+
"aria-label": `Details for ${tool.name}`,
|
|
236
|
+
className: cn(
|
|
237
|
+
PANEL,
|
|
238
|
+
"relative z-[2] flex flex-none flex-col items-center justify-center gap-1.5 border-l border-border/55 text-[13.5px] font-extrabold text-muted-foreground transition-colors",
|
|
239
|
+
"group-hover/row:border-border group-hover/row:bg-primary/[0.025] group-has-[:focus-visible]/row:bg-primary/[0.025]",
|
|
240
|
+
"hover:!border-primary/30 hover:!bg-primary/12 hover:text-foreground",
|
|
241
|
+
"focus-visible:rounded-r-[13px] focus-visible:!bg-primary/12 focus-visible:text-foreground focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-ring"
|
|
242
|
+
),
|
|
243
|
+
children: /* @__PURE__ */ jsxs("span", { className: "flex flex-col items-center gap-1.5 opacity-0 transition-opacity group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 [@media(hover:none)]:opacity-100", children: [
|
|
244
|
+
/* @__PURE__ */ jsx(PanelRight, { "aria-hidden": "true", className: "size-[18px]" }),
|
|
245
|
+
/* @__PURE__ */ jsx("span", { children: "Details" })
|
|
246
|
+
] })
|
|
247
|
+
}
|
|
248
|
+
) })
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
FeedRow,
|
|
255
|
+
PersonAvatar,
|
|
256
|
+
ROW_THUMB,
|
|
257
|
+
ToolThumb,
|
|
258
|
+
screenshotUrl
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=FeedRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedRow.js","sources":["../../../../../src/modules/communityTools/ui/FeedRow.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n} from '@igstack/app-catalog-shared-core'\nimport { Link } from '@tanstack/react-router'\nimport {\n CornerDownLeft,\n ExternalLink,\n Eye,\n Globe,\n PanelRight,\n} from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { highlightTokens } from '~/util/highlightMatches'\nimport { timeAgo } from '../timeAgo'\nimport { LifecycleBadge, UnreviewedBadge } from './LifecycleBadge'\nimport { LikeButton } from './LikeButton'\nimport { PinButton } from './PinButton'\n\nexport function screenshotUrl(id: string, size: number): string {\n return `/api/screenshots/${id}?size=${size}`\n}\n\n/** The tool's screenshot, cropped to its top-left, or a globe when there is none. */\nexport function ToolThumb({\n tool,\n size,\n className,\n}: {\n tool: CommunityTool\n size: number\n className?: string\n}) {\n return (\n <div\n className={cn(\n 'flex shrink-0 items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n {tool.screenshotId ? (\n <img\n src={screenshotUrl(tool.screenshotId, size)}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full object-cover object-left-top\"\n />\n ) : (\n <Globe aria-hidden=\"true\" className=\"size-1/4 min-w-4\" />\n )}\n </div>\n )\n}\n\nconst AVATAR_TONES = [\n 'bg-orange-200',\n 'bg-sky-200',\n 'bg-lime-200',\n 'bg-amber-200',\n 'bg-rose-200',\n 'bg-violet-200',\n]\n\nexport function PersonAvatar({ name }: { name: string }) {\n const initials = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w.charAt(0).toUpperCase())\n .join('')\n let hash = 0\n for (const ch of name) hash = (hash * 31 + ch.charCodeAt(0)) | 0\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex size-[22px] shrink-0 items-center justify-center rounded-full text-[9.5px] font-extrabold text-stone-700',\n AVATAR_TONES[Math.abs(hash) % AVATAR_TONES.length],\n )}\n >\n {initials}\n </span>\n )\n}\n\nconst FIELD_LABELS: Record<MatchField, string> = {\n name: 'name',\n summary: 'summary',\n description: 'how to use',\n owner: 'author',\n address: 'address',\n tag: 'tag',\n}\n\nexport interface FeedRowProps {\n tool: CommunityTool\n /** Shown in the ranked feed; search results have no rank. */\n rank?: number\n /** A date sort shows its date on the row. */\n sort: FeedSort\n tokens: string[]\n /** Set while searching: which fields the query hit. */\n matched?: MatchField[]\n selected?: boolean\n /**\n * Set while searching: the row is a row of the results grid, and this is the\n * id of its main cell, the one the search box points at.\n */\n optionId?: string\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: () => void\n onToggleLike: () => void\n}\n\n/** The feed row's thumbnail: big enough to read the page title in its top left. */\nexport const ROW_THUMB = 'h-[150px] w-[240px] rounded-[10px]'\n\n/** The Details panel's width, reserved at rest so nothing moves when it shows. */\nconst PANEL = 'w-[120px]'\n\n/**\n * One tool in the feed or the search results. Everything left of the end\n * panel (rank, picture, name, summary, meta) is one link that opens the site\n * in a new tab; the 120 px panel opens the card. The star sits in the meta\n * line as its own target, above that link. The bottom strip shows the address\n * the big click goes to, on hover or focus only.\n */\nexport function FeedRow({\n tool,\n rank,\n sort,\n tokens,\n matched,\n selected = false,\n optionId,\n pinned,\n pinBusy,\n onTogglePin,\n onToggleLike,\n}: FeedRowProps) {\n const archived = tool.lifecycle.state === 'archived'\n const cell = optionId ? 'gridcell' : undefined\n const owners = tool.owners.map((o) => o.name).join(', ')\n const dated =\n sort === 'newest'\n ? `Added ${timeAgo(tool.createdAt)}`\n : sort === 'updated' && tool.siteUpdatedAt\n ? `Updated ${timeAgo(tool.siteUpdatedAt)}`\n : null\n const address = tool.url.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n\n return (\n <div\n data-testid=\"feed-row\"\n data-slug={tool.slug}\n data-selected={selected || undefined}\n role={optionId ? 'row' : undefined}\n className={cn(\n 'group/row relative flex min-h-[176px] items-center overflow-hidden rounded-[14px] border bg-card transition-[border-color,box-shadow]',\n selected\n ? 'border-primary ring-[3px] ring-primary/20'\n : 'border-border hover:border-primary/35 hover:shadow-sm has-[:focus-visible]:border-primary/35',\n archived && 'opacity-55',\n )}\n >\n {/* Two cells while searching: the site, and the card. A grid rather\n than a listbox, because an option's content is presentational and\n would hide the link and the star. */}\n <div\n id={optionId}\n role={cell}\n aria-selected={optionId ? selected : undefined}\n className=\"flex min-w-0 flex-1 items-center gap-4 self-stretch py-3 pl-3\"\n >\n {rank !== undefined && (\n <span className=\"w-[26px] shrink-0 text-center font-serif text-[22px] font-semibold text-muted-foreground/70\">\n {rank}\n </span>\n )}\n <ToolThumb tool={tool} size={512} className={ROW_THUMB} />\n <div className=\"min-w-0 flex-1 self-start pt-5 pr-6 pb-[52px]\">\n <div className=\"flex items-center gap-2.5 text-[15.5px] font-extrabold\">\n <a\n href={tool.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-site\n data-testid=\"feed-row-name\"\n aria-label={`${tool.name}, opens ${tool.host} in a new tab`}\n className={cn(\n 'truncate decoration-1 underline-offset-[3px] after:absolute after:inset-y-0 after:left-0 after:right-[120px] after:z-[1] after:rounded-l-[13px] focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-[3px] focus-visible:after:outline-ring',\n 'group-hover/row:underline group-has-[[data-details]:hover]/row:no-underline',\n archived && 'text-muted-foreground',\n )}\n >\n {highlightTokens(tool.name, tokens)}\n </a>\n {selected && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11.5px] font-bold text-primary\">\n <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n Enter opens site\n </span>\n )}\n <LifecycleBadge lifecycle={tool.lifecycle} />\n {!tool.reviewed && <UnreviewedBadge />}\n </div>\n {tool.summary && (\n <div className=\"mt-1 line-clamp-2 text-foreground/75\">\n {highlightTokens(tool.summary, tokens)}\n </div>\n )}\n {matched && matched.length > 0 && (\n <div\n data-testid=\"match-reason\"\n className=\"mt-1 text-xs font-semibold text-muted-foreground\"\n >\n Matched: {matched.map((f) => FIELD_LABELS[f]).join(', ')}\n </div>\n )}\n </div>\n {/* Meta on the left, the address on the right, flush to the panel.\n The address keeps 150 px; a long author list gives way first.\n Clicks fall through to the row link, except on star and like. */}\n <div\n className={cn(\n 'pointer-events-none absolute right-[120px] bottom-0 z-[2] flex h-12 items-center gap-4',\n rank !== undefined ? 'left-[310px]' : 'left-[268px]',\n )}\n >\n <div className=\"flex min-w-0 items-center gap-1.5 text-[13px] font-semibold whitespace-nowrap text-foreground/75 [&>*]:shrink-0\">\n {tool.owners[0] && <PersonAvatar name={tool.owners[0].name} />}\n <span\n data-testid=\"feed-row-owner\"\n className=\"mr-1 min-w-0 !shrink truncate\"\n >\n {highlightTokens(owners, tokens)}\n </span>\n <span className=\"pointer-events-auto -mr-1 flex items-center\">\n <PinButton\n small\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={onTogglePin}\n />\n <LikeButton tool={tool} onToggle={onToggleLike} />\n </span>\n <span\n data-testid=\"feed-row-views\"\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n {tool.views30d}\n <span className=\"sr-only\"> views this month</span>\n </span>\n {dated && (\n <>\n <span aria-hidden=\"true\" className=\"text-muted-foreground\">\n ·\n </span>\n <span\n data-testid=\"feed-row-date\"\n className=\"text-muted-foreground\"\n >\n {dated}\n </span>\n </>\n )}\n </div>\n <div\n aria-hidden=\"true\"\n data-testid=\"feed-row-address\"\n className={cn(\n 'flex h-full min-w-[150px] flex-1 items-center justify-end gap-1.5 px-4 font-mono text-[12.5px] text-muted-foreground opacity-0 transition-opacity',\n 'group-hover/row:text-foreground group-hover/row:opacity-100 group-has-[[data-site]:focus-visible]/row:opacity-100 group-has-[[data-details]:hover]/row:opacity-0 [@media(hover:none)]:opacity-100',\n selected && 'opacity-100',\n )}\n >\n <span className=\"min-w-0 truncate\">{address}</span>\n <ExternalLink aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n </div>\n </div>\n </div>\n <div role={cell} className=\"flex self-stretch\">\n <Link\n to=\"/community/$slug\"\n params={{ slug: tool.slug }}\n search={(prev) => prev}\n data-details\n aria-label={`Details for ${tool.name}`}\n className={cn(\n PANEL,\n 'relative z-[2] flex flex-none flex-col items-center justify-center gap-1.5 border-l border-border/55 text-[13.5px] font-extrabold text-muted-foreground transition-colors',\n 'group-hover/row:border-border group-hover/row:bg-primary/[0.025] group-has-[:focus-visible]/row:bg-primary/[0.025]',\n 'hover:!border-primary/30 hover:!bg-primary/12 hover:text-foreground',\n 'focus-visible:rounded-r-[13px] focus-visible:!bg-primary/12 focus-visible:text-foreground focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-ring',\n )}\n >\n {/* Shown on hover or focus; always where nothing hovers. */}\n <span className=\"flex flex-col items-center gap-1.5 opacity-0 transition-opacity group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 [@media(hover:none)]:opacity-100\">\n <PanelRight aria-hidden=\"true\" className=\"size-[18px]\" />\n <span>Details</span>\n </span>\n </Link>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FeedSort, SortDir } from '@igstack/app-catalog-shared-core';
|
|
2
|
+
export declare const SORT_OPTIONS: {
|
|
3
|
+
value: FeedSort;
|
|
4
|
+
label: string;
|
|
5
|
+
}[];
|
|
6
|
+
/** The feed's heading, saying the order it is in. */
|
|
7
|
+
export declare function feedHeading(sort: FeedSort, dir: SortDir): string;
|
|
8
|
+
/**
|
|
9
|
+
* Segmented control above the feed. Choosing a sort starts it descending;
|
|
10
|
+
* choosing the active one again flips the direction. Arrow keys move between
|
|
11
|
+
* segments.
|
|
12
|
+
*/
|
|
13
|
+
export declare function FeedSortControl({ sort, dir, onChange, }: {
|
|
14
|
+
sort: FeedSort;
|
|
15
|
+
dir: SortDir;
|
|
16
|
+
onChange: (sort: FeedSort, dir: SortDir) => void;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { cn } from "../../../lib/utils.js";
|
|
5
|
+
const SORT_OPTIONS = [
|
|
6
|
+
{ value: "liked", label: "Most liked" },
|
|
7
|
+
{ value: "popular", label: "Popular" },
|
|
8
|
+
{ value: "newest", label: "Newest" },
|
|
9
|
+
{ value: "updated", label: "Recently updated" }
|
|
10
|
+
];
|
|
11
|
+
const HEADINGS = {
|
|
12
|
+
liked: { desc: "Most liked", asc: "Least liked" },
|
|
13
|
+
popular: { desc: "Most opened", asc: "Least opened" },
|
|
14
|
+
newest: { desc: "Newest", asc: "Oldest" },
|
|
15
|
+
updated: { desc: "Recently updated", asc: "Longest unchanged" }
|
|
16
|
+
};
|
|
17
|
+
function feedHeading(sort, dir) {
|
|
18
|
+
return HEADINGS[sort][dir];
|
|
19
|
+
}
|
|
20
|
+
const DIR_LABEL = {
|
|
21
|
+
desc: "descending",
|
|
22
|
+
asc: "ascending"
|
|
23
|
+
};
|
|
24
|
+
function FeedSortControl({
|
|
25
|
+
sort,
|
|
26
|
+
dir,
|
|
27
|
+
onChange
|
|
28
|
+
}) {
|
|
29
|
+
var _a;
|
|
30
|
+
const buttonsRef = useRef([]);
|
|
31
|
+
const label = ((_a = SORT_OPTIONS.find((o) => o.value === sort)) == null ? void 0 : _a.label) ?? sort;
|
|
32
|
+
const onKeyDown = (e, index) => {
|
|
33
|
+
var _a2;
|
|
34
|
+
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
|
35
|
+
if (step === 0) return;
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
const next = (index + step + SORT_OPTIONS.length) % SORT_OPTIONS.length;
|
|
38
|
+
onChange(SORT_OPTIONS[next].value, "desc");
|
|
39
|
+
(_a2 = buttonsRef.current[next]) == null ? void 0 : _a2.focus();
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
"div",
|
|
43
|
+
{
|
|
44
|
+
role: "radiogroup",
|
|
45
|
+
"aria-label": `Sort by ${label}, ${DIR_LABEL[dir]}`,
|
|
46
|
+
"data-testid": "feed-sort",
|
|
47
|
+
className: "inline-flex gap-0.5 rounded-[10px] bg-muted p-[3px] text-[13px] font-semibold",
|
|
48
|
+
children: SORT_OPTIONS.map((option, index) => {
|
|
49
|
+
const on = option.value === sort;
|
|
50
|
+
const Arrow = dir === "desc" ? ArrowDown : ArrowUp;
|
|
51
|
+
return /* @__PURE__ */ jsxs(
|
|
52
|
+
"button",
|
|
53
|
+
{
|
|
54
|
+
ref: (el) => {
|
|
55
|
+
buttonsRef.current[index] = el;
|
|
56
|
+
},
|
|
57
|
+
type: "button",
|
|
58
|
+
role: "radio",
|
|
59
|
+
"aria-checked": on,
|
|
60
|
+
"aria-label": on ? `${option.label}, ${DIR_LABEL[dir]}` : option.label,
|
|
61
|
+
tabIndex: on ? 0 : -1,
|
|
62
|
+
onClick: () => onChange(option.value, on && dir === "desc" ? "asc" : "desc"),
|
|
63
|
+
onKeyDown: (e) => onKeyDown(e, index),
|
|
64
|
+
className: cn(
|
|
65
|
+
"inline-flex items-center gap-1 rounded-lg px-3 py-[5px] transition-colors focus-visible:outline-2 focus-visible:outline-ring",
|
|
66
|
+
on ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
|
67
|
+
),
|
|
68
|
+
children: [
|
|
69
|
+
option.label,
|
|
70
|
+
on && /* @__PURE__ */ jsx(Arrow, { "aria-hidden": "true", className: "size-3.5" })
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
option.value
|
|
74
|
+
);
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
FeedSortControl,
|
|
81
|
+
SORT_OPTIONS,
|
|
82
|
+
feedHeading
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=FeedSortControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedSortControl.js","sources":["../../../../../src/modules/communityTools/ui/FeedSortControl.tsx"],"sourcesContent":["import type { FeedSort, SortDir } from '@igstack/app-catalog-shared-core'\nimport { ArrowDown, ArrowUp } from 'lucide-react'\nimport { useRef } from 'react'\nimport { cn } from '~/lib/utils'\n\nexport const SORT_OPTIONS: { value: FeedSort; label: string }[] = [\n { value: 'liked', label: 'Most liked' },\n { value: 'popular', label: 'Popular' },\n { value: 'newest', label: 'Newest' },\n { value: 'updated', label: 'Recently updated' },\n]\n\nconst HEADINGS: Record<FeedSort, Record<SortDir, string>> = {\n liked: { desc: 'Most liked', asc: 'Least liked' },\n popular: { desc: 'Most opened', asc: 'Least opened' },\n newest: { desc: 'Newest', asc: 'Oldest' },\n updated: { desc: 'Recently updated', asc: 'Longest unchanged' },\n}\n\n/** The feed's heading, saying the order it is in. */\nexport function feedHeading(sort: FeedSort, dir: SortDir): string {\n return HEADINGS[sort][dir]\n}\n\nconst DIR_LABEL: Record<SortDir, string> = {\n desc: 'descending',\n asc: 'ascending',\n}\n\n/**\n * Segmented control above the feed. Choosing a sort starts it descending;\n * choosing the active one again flips the direction. Arrow keys move between\n * segments.\n */\nexport function FeedSortControl({\n sort,\n dir,\n onChange,\n}: {\n sort: FeedSort\n dir: SortDir\n onChange: (sort: FeedSort, dir: SortDir) => void\n}) {\n const buttonsRef = useRef<(HTMLButtonElement | null)[]>([])\n const label = SORT_OPTIONS.find((o) => o.value === sort)?.label ?? sort\n\n const onKeyDown = (e: React.KeyboardEvent, index: number) => {\n const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0\n if (step === 0) return\n e.preventDefault()\n const next = (index + step + SORT_OPTIONS.length) % SORT_OPTIONS.length\n onChange(SORT_OPTIONS[next]!.value, 'desc')\n buttonsRef.current[next]?.focus()\n }\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={`Sort by ${label}, ${DIR_LABEL[dir]}`}\n data-testid=\"feed-sort\"\n className=\"inline-flex gap-0.5 rounded-[10px] bg-muted p-[3px] text-[13px] font-semibold\"\n >\n {SORT_OPTIONS.map((option, index) => {\n const on = option.value === sort\n const Arrow = dir === 'desc' ? ArrowDown : ArrowUp\n return (\n <button\n key={option.value}\n ref={(el) => {\n buttonsRef.current[index] = el\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={on}\n aria-label={\n on ? `${option.label}, ${DIR_LABEL[dir]}` : option.label\n }\n tabIndex={on ? 0 : -1}\n onClick={() =>\n onChange(option.value, on && dir === 'desc' ? 'asc' : 'desc')\n }\n onKeyDown={(e) => onKeyDown(e, index)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-[5px] transition-colors focus-visible:outline-2 focus-visible:outline-ring',\n on\n ? 'bg-card text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {option.label}\n {on && <Arrow aria-hidden=\"true\" className=\"size-3.5\" />}\n </button>\n )\n })}\n </div>\n )\n}\n"],"names":["_a"],"mappings":";;;;AAKO,MAAM,eAAqD;AAAA,EAChE,EAAE,OAAO,SAAS,OAAO,aAAA;AAAA,EACzB,EAAE,OAAO,WAAW,OAAO,UAAA;AAAA,EAC3B,EAAE,OAAO,UAAU,OAAO,SAAA;AAAA,EAC1B,EAAE,OAAO,WAAW,OAAO,mBAAA;AAC7B;AAEA,MAAM,WAAsD;AAAA,EAC1D,OAAO,EAAE,MAAM,cAAc,KAAK,cAAA;AAAA,EAClC,SAAS,EAAE,MAAM,eAAe,KAAK,eAAA;AAAA,EACrC,QAAQ,EAAE,MAAM,UAAU,KAAK,SAAA;AAAA,EAC/B,SAAS,EAAE,MAAM,oBAAoB,KAAK,oBAAA;AAC5C;AAGO,SAAS,YAAY,MAAgB,KAAsB;AAChE,SAAO,SAAS,IAAI,EAAE,GAAG;AAC3B;AAEA,MAAM,YAAqC;AAAA,EACzC,MAAM;AAAA,EACN,KAAK;AACP;AAOO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAIG;;AACD,QAAM,aAAa,OAAqC,EAAE;AAC1D,QAAM,UAAQ,kBAAa,KAAK,CAAC,MAAM,EAAE,UAAU,IAAI,MAAzC,mBAA4C,UAAS;AAEnE,QAAM,YAAY,CAAC,GAAwB,UAAkB;;AAC3D,UAAM,OAAO,EAAE,QAAQ,eAAe,IAAI,EAAE,QAAQ,cAAc,KAAK;AACvE,QAAI,SAAS,EAAG;AAChB,MAAE,eAAA;AACF,UAAM,QAAQ,QAAQ,OAAO,aAAa,UAAU,aAAa;AACjE,aAAS,aAAa,IAAI,EAAG,OAAO,MAAM;AAC1C,KAAAA,MAAA,WAAW,QAAQ,IAAI,MAAvB,gBAAAA,IAA0B;AAAA,EAC5B;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,WAAW,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,MAC/C,eAAY;AAAA,MACZ,WAAU;AAAA,MAET,UAAA,aAAa,IAAI,CAAC,QAAQ,UAAU;AACnC,cAAM,KAAK,OAAO,UAAU;AAC5B,cAAM,QAAQ,QAAQ,SAAS,YAAY;AAC3C,eACE;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,KAAK,CAAC,OAAO;AACX,yBAAW,QAAQ,KAAK,IAAI;AAAA,YAC9B;AAAA,YACA,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,cACE,KAAK,GAAG,OAAO,KAAK,KAAK,UAAU,GAAG,CAAC,KAAK,OAAO;AAAA,YAErD,UAAU,KAAK,IAAI;AAAA,YACnB,SAAS,MACP,SAAS,OAAO,OAAO,MAAM,QAAQ,SAAS,QAAQ,MAAM;AAAA,YAE9D,WAAW,CAAC,MAAM,UAAU,GAAG,KAAK;AAAA,YACpC,WAAW;AAAA,cACT;AAAA,cACA,KACI,sCACA;AAAA,YAAA;AAAA,YAGL,UAAA;AAAA,cAAA,OAAO;AAAA,cACP,MAAM,oBAAC,OAAA,EAAM,eAAY,QAAO,WAAU,WAAA,CAAW;AAAA,YAAA;AAAA,UAAA;AAAA,UAvBjD,OAAO;AAAA,QAAA;AAAA,MA0BlB,CAAC;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Lifecycle } from '@igstack/app-catalog-shared-core';
|
|
2
|
+
/** Next to the name on rows, tiles and the card. An active tool shows nothing. */
|
|
3
|
+
export declare function LifecycleBadge({ lifecycle }: {
|
|
4
|
+
lifecycle: Lifecycle;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
6
|
+
/** Live since it was published, but no admin has looked at it yet. */
|
|
7
|
+
export declare function UnreviewedBadge(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangle, Archive } from "lucide-react";
|
|
3
|
+
const PILL = "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-px text-[11.5px] font-bold";
|
|
4
|
+
function LifecycleBadge({ lifecycle }) {
|
|
5
|
+
if (lifecycle.state === "not-working") {
|
|
6
|
+
return /* @__PURE__ */ jsxs(
|
|
7
|
+
"span",
|
|
8
|
+
{
|
|
9
|
+
"data-testid": "lifecycle-badge",
|
|
10
|
+
className: `${PILL} border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300`,
|
|
11
|
+
children: [
|
|
12
|
+
/* @__PURE__ */ jsx(AlertTriangle, { "aria-hidden": "true", className: "size-3" }),
|
|
13
|
+
"Not working · ",
|
|
14
|
+
lifecycle.days,
|
|
15
|
+
" ",
|
|
16
|
+
lifecycle.days === 1 ? "day" : "days"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (lifecycle.state === "archived") {
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"span",
|
|
24
|
+
{
|
|
25
|
+
"data-testid": "lifecycle-badge",
|
|
26
|
+
className: `${PILL} border-border bg-muted text-muted-foreground`,
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(Archive, { "aria-hidden": "true", className: "size-3" }),
|
|
29
|
+
"Archived"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function UnreviewedBadge() {
|
|
37
|
+
return /* @__PURE__ */ jsx(
|
|
38
|
+
"span",
|
|
39
|
+
{
|
|
40
|
+
"data-testid": "unreviewed-badge",
|
|
41
|
+
className: `${PILL} border-primary/25 bg-primary/10 text-primary`,
|
|
42
|
+
children: "New · not reviewed"
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
LifecycleBadge,
|
|
48
|
+
UnreviewedBadge
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=LifecycleBadge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LifecycleBadge.js","sources":["../../../../../src/modules/communityTools/ui/LifecycleBadge.tsx"],"sourcesContent":["import type { Lifecycle } from '@igstack/app-catalog-shared-core'\nimport { AlertTriangle, Archive } from 'lucide-react'\n\nconst PILL =\n 'inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-px text-[11.5px] font-bold'\n\n/** Next to the name on rows, tiles and the card. An active tool shows nothing. */\nexport function LifecycleBadge({ lifecycle }: { lifecycle: Lifecycle }) {\n if (lifecycle.state === 'not-working') {\n return (\n <span\n data-testid=\"lifecycle-badge\"\n className={`${PILL} border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300`}\n >\n <AlertTriangle aria-hidden=\"true\" className=\"size-3\" />\n Not working · {lifecycle.days} {lifecycle.days === 1 ? 'day' : 'days'}\n </span>\n )\n }\n if (lifecycle.state === 'archived') {\n return (\n <span\n data-testid=\"lifecycle-badge\"\n className={`${PILL} border-border bg-muted text-muted-foreground`}\n >\n <Archive aria-hidden=\"true\" className=\"size-3\" />\n Archived\n </span>\n )\n }\n return null\n}\n\n/** Live since it was published, but no admin has looked at it yet. */\nexport function UnreviewedBadge() {\n return (\n <span\n data-testid=\"unreviewed-badge\"\n className={`${PILL} border-primary/25 bg-primary/10 text-primary`}\n >\n New · not reviewed\n </span>\n )\n}\n"],"names":[],"mappings":";;AAGA,MAAM,OACJ;AAGK,SAAS,eAAe,EAAE,aAAuC;AACtE,MAAI,UAAU,UAAU,eAAe;AACrC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW,GAAG,IAAI;AAAA,QAElB,UAAA;AAAA,UAAA,oBAAC,eAAA,EAAc,eAAY,QAAO,WAAU,UAAS;AAAA,UAAE;AAAA,UACxC,UAAU;AAAA,UAAK;AAAA,UAAE,UAAU,SAAS,IAAI,QAAQ;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGrE;AACA,MAAI,UAAU,UAAU,YAAY;AAClC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW,GAAG,IAAI;AAAA,QAElB,UAAA;AAAA,UAAA,oBAAC,SAAA,EAAQ,eAAY,QAAO,WAAU,UAAS;AAAA,UAAE;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAIvD;AACA,SAAO;AACT;AAGO,SAAS,kBAAkB;AAChC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,WAAW,GAAG,IAAI;AAAA,MACnB,UAAA;AAAA,IAAA;AAAA,EAAA;AAIL;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CommunityTool } from '@igstack/app-catalog-shared-core';
|
|
2
|
+
/**
|
|
3
|
+
* A thumb and the count, filled in the brand colour once I liked it; hover or
|
|
4
|
+
* focus says who liked it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function LikeButton({ tool, onToggle, }: {
|
|
7
|
+
tool: Pick<CommunityTool, 'name' | 'likes' | 'likedByMe' | 'likedBy'>;
|
|
8
|
+
onToggle: () => void;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ThumbsUp } from "lucide-react";
|
|
3
|
+
import { cn } from "../../../lib/utils.js";
|
|
4
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from "../../../ui/tooltip.js";
|
|
5
|
+
import { likedByText } from "../likedBy.js";
|
|
6
|
+
function LikeButton({
|
|
7
|
+
tool,
|
|
8
|
+
onToggle
|
|
9
|
+
}) {
|
|
10
|
+
const { name, likes, likedByMe } = tool;
|
|
11
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
12
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
13
|
+
"button",
|
|
14
|
+
{
|
|
15
|
+
type: "button",
|
|
16
|
+
"data-testid": "like-button",
|
|
17
|
+
"aria-pressed": likedByMe,
|
|
18
|
+
"aria-label": `Like ${name}`,
|
|
19
|
+
onClick: (e) => {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
onToggle();
|
|
22
|
+
},
|
|
23
|
+
className: cn(
|
|
24
|
+
"relative z-10 inline-flex h-8 min-w-8 items-center justify-center gap-1 rounded-lg px-1.5 transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring",
|
|
25
|
+
likedByMe ? "text-foreground" : "text-muted-foreground"
|
|
26
|
+
),
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(
|
|
29
|
+
ThumbsUp,
|
|
30
|
+
{
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
className: cn(
|
|
33
|
+
"size-[15px]",
|
|
34
|
+
likedByMe && "fill-primary/85 stroke-foreground"
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx("span", { "data-testid": "like-count", children: likes })
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
) }),
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
TooltipContent,
|
|
44
|
+
{
|
|
45
|
+
"data-testid": "liked-by",
|
|
46
|
+
sideOffset: 8,
|
|
47
|
+
className: "max-w-[300px] rounded-[10px] px-2.5 py-[7px] text-[12.5px] leading-[1.4] font-semibold",
|
|
48
|
+
children: likedByText(tool)
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
] });
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
LikeButton
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=LikeButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LikeButton.js","sources":["../../../../../src/modules/communityTools/ui/LikeButton.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { ThumbsUp } from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '~/ui/tooltip'\nimport { likedByText } from '../likedBy'\n\n/**\n * A thumb and the count, filled in the brand colour once I liked it; hover or\n * focus says who liked it.\n */\nexport function LikeButton({\n tool,\n onToggle,\n}: {\n tool: Pick<CommunityTool, 'name' | 'likes' | 'likedByMe' | 'likedBy'>\n onToggle: () => void\n}) {\n const { name, likes, likedByMe } = tool\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n data-testid=\"like-button\"\n aria-pressed={likedByMe}\n // One name; aria-pressed says whether it is liked.\n aria-label={`Like ${name}`}\n onClick={(e) => {\n e.stopPropagation()\n onToggle()\n }}\n className={cn(\n 'relative z-10 inline-flex h-8 min-w-8 items-center justify-center gap-1 rounded-lg px-1.5 transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring',\n likedByMe ? 'text-foreground' : 'text-muted-foreground',\n )}\n >\n <ThumbsUp\n aria-hidden=\"true\"\n className={cn(\n 'size-[15px]',\n likedByMe && 'fill-primary/85 stroke-foreground',\n )}\n />\n <span data-testid=\"like-count\">{likes}</span>\n </button>\n </TooltipTrigger>\n <TooltipContent\n data-testid=\"liked-by\"\n sideOffset={8}\n className=\"max-w-[300px] rounded-[10px] px-2.5 py-[7px] text-[12.5px] leading-[1.4] font-semibold\"\n >\n {likedByText(tool)}\n </TooltipContent>\n </Tooltip>\n )\n}\n"],"names":[],"mappings":";;;;;AAUO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,MAAM,OAAO,UAAA,IAAc;AACnC,8BACG,SAAA,EACC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,eAAY;AAAA,QACZ,gBAAc;AAAA,QAEd,cAAY,QAAQ,IAAI;AAAA,QACxB,SAAS,CAAC,MAAM;AACd,YAAE,gBAAA;AACF,mBAAA;AAAA,QACF;AAAA,QACA,WAAW;AAAA,UACT;AAAA,UACA,YAAY,oBAAoB;AAAA,QAAA;AAAA,QAGlC,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAY;AAAA,cACZ,WAAW;AAAA,gBACT;AAAA,gBACA,aAAa;AAAA,cAAA;AAAA,YACf;AAAA,UAAA;AAAA,UAEF,oBAAC,QAAA,EAAK,eAAY,cAAc,UAAA,MAAA,CAAM;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAE1C;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAY;AAAA,QACZ,YAAY;AAAA,QACZ,WAAU;AAAA,QAET,sBAAY,IAAI;AAAA,MAAA;AAAA,IAAA;AAAA,EACnB,GACF;AAEJ;"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CommunityTool } from '@igstack/app-catalog-shared-core';
|
|
2
|
+
/**
|
|
3
|
+
* The hovered or focused row's screenshot, big, in the empty margin left of
|
|
4
|
+
* the feed: one reused panel whose top lines up with the row's, moved up only
|
|
5
|
+
* as far as it takes to stay in the window. The first one waits a moment;
|
|
6
|
+
* moving to another row swaps it at once. Nothing on a narrow window, on a
|
|
7
|
+
* touch screen, or while a dialog is open.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MarginPreview({ list, tools, disabled, }: {
|
|
10
|
+
/**
|
|
11
|
+
* The element the rows are in; hover and focus are read off it. Inside it
|
|
12
|
+
* sits `[data-preview-column]`, the centred column whose left margin holds
|
|
13
|
+
* the preview, inside `[data-preview-scroller]`.
|
|
14
|
+
*/
|
|
15
|
+
list: HTMLElement | null;
|
|
16
|
+
tools: CommunityTool[];
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|