blodemd 0.0.8 → 0.0.9
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/README.md +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +132 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +22 -11
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +322 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +1 -41
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +3 -55
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- package/packages/@repo/validation/src/index.ts +4 -4
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
23
23
|
import type { ComponentType, ReactNode, SVGProps } from "react";
|
|
24
24
|
|
|
25
|
+
import {
|
|
26
|
+
Popover,
|
|
27
|
+
PopoverContent,
|
|
28
|
+
PopoverTrigger,
|
|
29
|
+
} from "@/components/ui/popover";
|
|
25
30
|
import {
|
|
26
31
|
buildBuiltinUrl,
|
|
27
32
|
builtinOptions,
|
|
@@ -29,6 +34,29 @@ import {
|
|
|
29
34
|
} from "@/lib/contextual-options";
|
|
30
35
|
|
|
31
36
|
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
|
|
37
|
+
type ActionId = "add-mcp" | "assistant" | "copy" | "mcp";
|
|
38
|
+
|
|
39
|
+
interface ResolvedOption {
|
|
40
|
+
key: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
icon: IconComponent;
|
|
44
|
+
type: "action" | "link";
|
|
45
|
+
action?: ActionId;
|
|
46
|
+
href?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ContextualContext {
|
|
50
|
+
pageUrl: string;
|
|
51
|
+
pageContent: string;
|
|
52
|
+
pagePath: string;
|
|
53
|
+
mcpServerUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ActionFeedback {
|
|
57
|
+
id: string;
|
|
58
|
+
state: "copied" | "error";
|
|
59
|
+
}
|
|
32
60
|
|
|
33
61
|
const iconMap: Record<string, IconComponent> = {
|
|
34
62
|
ClaudeaiIcon,
|
|
@@ -50,36 +78,64 @@ const iconMap: Record<string, IconComponent> = {
|
|
|
50
78
|
const getBuiltinIcon = (iconName: string): IconComponent =>
|
|
51
79
|
iconMap[iconName] ?? CopySimpleIcon;
|
|
52
80
|
|
|
53
|
-
|
|
81
|
+
const getFeedbackLabel = (
|
|
82
|
+
feedbackState: ActionFeedback["state"] | null,
|
|
83
|
+
defaultLabel: string
|
|
84
|
+
) => {
|
|
85
|
+
switch (feedbackState) {
|
|
86
|
+
case "copied": {
|
|
87
|
+
return "Copied";
|
|
88
|
+
}
|
|
89
|
+
case "error": {
|
|
90
|
+
return "Copy failed";
|
|
91
|
+
}
|
|
92
|
+
default: {
|
|
93
|
+
return defaultLabel;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
54
97
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
const getFeedbackDescription = (
|
|
99
|
+
feedbackState: ActionFeedback["state"] | null,
|
|
100
|
+
defaultDescription: string
|
|
101
|
+
) => {
|
|
102
|
+
switch (feedbackState) {
|
|
103
|
+
case "copied": {
|
|
104
|
+
return "Copied to clipboard";
|
|
105
|
+
}
|
|
106
|
+
case "error": {
|
|
107
|
+
return "Clipboard access was blocked";
|
|
108
|
+
}
|
|
109
|
+
default: {
|
|
110
|
+
return defaultDescription;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
64
114
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
const getFeedbackIcon = (
|
|
116
|
+
feedbackState: ActionFeedback["state"] | null,
|
|
117
|
+
defaultIcon: IconComponent
|
|
118
|
+
) => {
|
|
119
|
+
if (feedbackState === "copied") {
|
|
120
|
+
return Checkmark1Icon;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return defaultIcon;
|
|
124
|
+
};
|
|
71
125
|
|
|
72
126
|
const resolveOptions = (
|
|
73
127
|
options: ContextualOption[],
|
|
74
128
|
context: ContextualContext
|
|
75
129
|
): ResolvedOption[] => {
|
|
76
130
|
const resolved: ResolvedOption[] = [];
|
|
131
|
+
|
|
77
132
|
for (const option of options) {
|
|
78
133
|
if (typeof option === "string") {
|
|
79
134
|
const definition = builtinOptions[option];
|
|
80
135
|
if (!definition) {
|
|
81
136
|
continue;
|
|
82
137
|
}
|
|
138
|
+
|
|
83
139
|
if (definition.type === "action") {
|
|
84
140
|
resolved.push({
|
|
85
141
|
action: option as ActionId,
|
|
@@ -89,59 +145,96 @@ const resolveOptions = (
|
|
|
89
145
|
title: definition.title,
|
|
90
146
|
type: "action",
|
|
91
147
|
});
|
|
92
|
-
|
|
93
|
-
const href = buildBuiltinUrl(option, context);
|
|
94
|
-
if (href) {
|
|
95
|
-
resolved.push({
|
|
96
|
-
description: definition.description,
|
|
97
|
-
href,
|
|
98
|
-
icon: getBuiltinIcon(definition.iconName),
|
|
99
|
-
key: option,
|
|
100
|
-
title: definition.title,
|
|
101
|
-
type: "link",
|
|
102
|
-
});
|
|
103
|
-
}
|
|
148
|
+
continue;
|
|
104
149
|
}
|
|
105
|
-
|
|
150
|
+
|
|
151
|
+
const href = buildBuiltinUrl(option, context);
|
|
152
|
+
if (!href) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
resolved.push({
|
|
107
|
-
description:
|
|
108
|
-
href
|
|
109
|
-
icon:
|
|
110
|
-
key:
|
|
111
|
-
title:
|
|
157
|
+
description: definition.description,
|
|
158
|
+
href,
|
|
159
|
+
icon: getBuiltinIcon(definition.iconName),
|
|
160
|
+
key: option,
|
|
161
|
+
title: definition.title,
|
|
112
162
|
type: "link",
|
|
113
163
|
});
|
|
164
|
+
continue;
|
|
114
165
|
}
|
|
166
|
+
|
|
167
|
+
resolved.push({
|
|
168
|
+
description: option.description,
|
|
169
|
+
href: resolveCustomHref(option.href, context),
|
|
170
|
+
icon: CopySimpleIcon,
|
|
171
|
+
key: `custom-${option.title}`,
|
|
172
|
+
title: option.title,
|
|
173
|
+
type: "link",
|
|
174
|
+
});
|
|
115
175
|
}
|
|
176
|
+
|
|
116
177
|
return resolved;
|
|
117
178
|
};
|
|
118
179
|
|
|
119
|
-
const useContextualActions = (content: string, title: string) => {
|
|
120
|
-
const
|
|
180
|
+
const useContextualActions = (content: string | undefined, title: string) => {
|
|
181
|
+
const resetTimerRef = useRef<number | null>(null);
|
|
182
|
+
const [feedback, setFeedback] = useState<ActionFeedback | null>(null);
|
|
183
|
+
|
|
184
|
+
useEffect(
|
|
185
|
+
() => () => {
|
|
186
|
+
if (resetTimerRef.current !== null) {
|
|
187
|
+
window.clearTimeout(resetTimerRef.current);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
[]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const setActionFeedback = useCallback(
|
|
194
|
+
(id: string, state: ActionFeedback["state"]) => {
|
|
195
|
+
if (resetTimerRef.current !== null) {
|
|
196
|
+
window.clearTimeout(resetTimerRef.current);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setFeedback({ id, state });
|
|
200
|
+
resetTimerRef.current = window.setTimeout(() => {
|
|
201
|
+
setFeedback(null);
|
|
202
|
+
resetTimerRef.current = null;
|
|
203
|
+
}, 2000);
|
|
204
|
+
},
|
|
205
|
+
[]
|
|
206
|
+
);
|
|
121
207
|
|
|
122
208
|
const handleAction = useCallback(
|
|
123
209
|
async (action: string, key?: string) => {
|
|
124
210
|
const id = key ?? action;
|
|
211
|
+
|
|
125
212
|
switch (action) {
|
|
126
213
|
case "copy": {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
214
|
+
try {
|
|
215
|
+
await navigator.clipboard.writeText(
|
|
216
|
+
`# ${title}\n\n${content ?? ""}`
|
|
217
|
+
);
|
|
218
|
+
setActionFeedback(id, "copied");
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
setActionFeedback(id, "error");
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
131
224
|
}
|
|
132
225
|
default: {
|
|
133
|
-
|
|
226
|
+
return true;
|
|
134
227
|
}
|
|
135
228
|
}
|
|
136
229
|
},
|
|
137
|
-
[content, title]
|
|
230
|
+
[content, setActionFeedback, title]
|
|
138
231
|
);
|
|
139
232
|
|
|
140
|
-
return {
|
|
233
|
+
return { feedback, handleAction };
|
|
141
234
|
};
|
|
142
235
|
|
|
143
236
|
const usePageContext = (
|
|
144
|
-
content: string,
|
|
237
|
+
content: string | undefined,
|
|
145
238
|
title: string,
|
|
146
239
|
pagePath: string
|
|
147
240
|
): ContextualContext => {
|
|
@@ -149,14 +242,17 @@ const usePageContext = (
|
|
|
149
242
|
|
|
150
243
|
useEffect(() => {
|
|
151
244
|
setPageUrl(window.location.href);
|
|
152
|
-
}, []);
|
|
153
|
-
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
}, [pagePath]);
|
|
246
|
+
|
|
247
|
+
return useMemo(
|
|
248
|
+
() => ({
|
|
249
|
+
mcpServerUrl: undefined,
|
|
250
|
+
pageContent: `# ${title}\n\n${content ?? ""}`,
|
|
251
|
+
pagePath,
|
|
252
|
+
pageUrl,
|
|
253
|
+
}),
|
|
254
|
+
[content, pagePath, pageUrl, title]
|
|
255
|
+
);
|
|
160
256
|
};
|
|
161
257
|
|
|
162
258
|
const MenuIcon = ({ children }: { children: ReactNode }) => (
|
|
@@ -172,11 +268,11 @@ const MenuItem = ({
|
|
|
172
268
|
}: {
|
|
173
269
|
children: ReactNode;
|
|
174
270
|
href?: string;
|
|
175
|
-
onSelect?: () => void;
|
|
271
|
+
onSelect?: () => Promise<void> | void;
|
|
176
272
|
}) =>
|
|
177
273
|
href ? (
|
|
178
274
|
<a
|
|
179
|
-
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25"
|
|
275
|
+
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25 focus-visible:bg-secondary/25 focus-visible:outline-none"
|
|
180
276
|
href={href}
|
|
181
277
|
onClick={onSelect}
|
|
182
278
|
rel="noopener noreferrer"
|
|
@@ -186,7 +282,7 @@ const MenuItem = ({
|
|
|
186
282
|
</a>
|
|
187
283
|
) : (
|
|
188
284
|
<button
|
|
189
|
-
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-secondary/25"
|
|
285
|
+
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-secondary/25 focus-visible:bg-secondary/25 focus-visible:outline-none"
|
|
190
286
|
onClick={onSelect}
|
|
191
287
|
type="button"
|
|
192
288
|
>
|
|
@@ -211,7 +307,7 @@ const ExternalArrow = () => (
|
|
|
211
307
|
|
|
212
308
|
interface ContextualMenuProps {
|
|
213
309
|
options: ContextualOption[];
|
|
214
|
-
content
|
|
310
|
+
content?: string;
|
|
215
311
|
title: string;
|
|
216
312
|
pagePath: string;
|
|
217
313
|
}
|
|
@@ -222,42 +318,21 @@ export const ContextualMenu = ({
|
|
|
222
318
|
title,
|
|
223
319
|
pagePath,
|
|
224
320
|
}: ContextualMenuProps) => {
|
|
225
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
226
321
|
const context = usePageContext(content, title, pagePath);
|
|
227
|
-
const {
|
|
322
|
+
const { feedback, handleAction } = useContextualActions(content, title);
|
|
228
323
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
229
324
|
|
|
230
325
|
const resolved = useMemo(
|
|
231
326
|
() => resolveOptions(options, context),
|
|
232
327
|
[context, options]
|
|
233
328
|
);
|
|
234
|
-
|
|
235
329
|
const [primaryOption] = resolved;
|
|
236
|
-
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (!menuOpen) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const handlePointerDown = (event: MouseEvent) => {
|
|
243
|
-
if (menuRef.current?.contains(event.target as Node)) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
setMenuOpen(false);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
document.addEventListener("mousedown", handlePointerDown);
|
|
250
|
-
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
251
|
-
}, [menuOpen]);
|
|
330
|
+
const hasSecondaryOptions = resolved.length > 1;
|
|
252
331
|
|
|
253
332
|
const closeMenu = useCallback(() => {
|
|
254
333
|
setMenuOpen(false);
|
|
255
334
|
}, []);
|
|
256
335
|
|
|
257
|
-
const toggleMenu = useCallback(() => {
|
|
258
|
-
setMenuOpen((current) => !current);
|
|
259
|
-
}, []);
|
|
260
|
-
|
|
261
336
|
const handlePrimaryAction = useCallback(async () => {
|
|
262
337
|
if (!primaryOption) {
|
|
263
338
|
return;
|
|
@@ -279,11 +354,16 @@ export const ContextualMenu = ({
|
|
|
279
354
|
.map((item) => [
|
|
280
355
|
item.key,
|
|
281
356
|
async () => {
|
|
282
|
-
await handleAction(
|
|
283
|
-
|
|
357
|
+
const didComplete = await handleAction(
|
|
358
|
+
item.action ?? "",
|
|
359
|
+
item.key
|
|
360
|
+
);
|
|
361
|
+
if (didComplete) {
|
|
362
|
+
closeMenu();
|
|
363
|
+
}
|
|
284
364
|
},
|
|
285
365
|
])
|
|
286
|
-
),
|
|
366
|
+
) as Record<string, () => Promise<void>>,
|
|
287
367
|
[closeMenu, handleAction, resolved]
|
|
288
368
|
);
|
|
289
369
|
|
|
@@ -291,34 +371,38 @@ export const ContextualMenu = ({
|
|
|
291
371
|
return null;
|
|
292
372
|
}
|
|
293
373
|
|
|
294
|
-
const
|
|
374
|
+
const primaryFeedback =
|
|
375
|
+
feedback?.id === primaryOption.key ? feedback.state : null;
|
|
376
|
+
const primaryLabel = getFeedbackLabel(primaryFeedback, primaryOption.title);
|
|
377
|
+
const PrimaryIcon = getFeedbackIcon(primaryFeedback, primaryOption.icon);
|
|
378
|
+
const primaryButtonClassName = hasSecondaryOptions
|
|
379
|
+
? "inline-flex items-center gap-2 rounded-l-xl border border-r-0 border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-secondary/25"
|
|
380
|
+
: "inline-flex items-center gap-2 rounded-xl border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-secondary/25";
|
|
381
|
+
const primaryButton = (
|
|
382
|
+
<button
|
|
383
|
+
className={primaryButtonClassName}
|
|
384
|
+
onClick={handlePrimaryAction}
|
|
385
|
+
type="button"
|
|
386
|
+
>
|
|
387
|
+
<PrimaryIcon aria-hidden="true" className="size-[18px]" />
|
|
388
|
+
<span>{primaryLabel}</span>
|
|
389
|
+
</button>
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (!hasSecondaryOptions) {
|
|
393
|
+
return <div className="flex shrink-0 items-center">{primaryButton}</div>;
|
|
394
|
+
}
|
|
295
395
|
|
|
296
396
|
return (
|
|
297
|
-
<
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
>
|
|
303
|
-
{primaryOption.type === "action" && isCopied ? (
|
|
304
|
-
<Checkmark1Icon aria-hidden="true" className="size-[18px]" />
|
|
305
|
-
) : (
|
|
306
|
-
<primaryOption.icon aria-hidden="true" className="size-[18px]" />
|
|
307
|
-
)}
|
|
308
|
-
<span>
|
|
309
|
-
{primaryOption.type === "action" && isCopied
|
|
310
|
-
? "Copied"
|
|
311
|
-
: primaryOption.title}
|
|
312
|
-
</span>
|
|
313
|
-
</button>
|
|
314
|
-
|
|
315
|
-
{resolved.length > 1 ? (
|
|
316
|
-
<>
|
|
397
|
+
<Popover onOpenChange={setMenuOpen} open={menuOpen}>
|
|
398
|
+
<div className="flex shrink-0 items-center">
|
|
399
|
+
{primaryButton}
|
|
400
|
+
|
|
401
|
+
<PopoverTrigger asChild>
|
|
317
402
|
<button
|
|
318
403
|
aria-expanded={menuOpen}
|
|
319
404
|
aria-label="More actions"
|
|
320
405
|
className="inline-flex items-center self-stretch rounded-r-xl border border-border px-2 transition-colors hover:bg-secondary/25"
|
|
321
|
-
onClick={toggleMenu}
|
|
322
406
|
type="button"
|
|
323
407
|
>
|
|
324
408
|
<ChevronDownSmallIcon
|
|
@@ -326,44 +410,45 @@ export const ContextualMenu = ({
|
|
|
326
410
|
className="size-[18px] text-muted-foreground"
|
|
327
411
|
/>
|
|
328
412
|
</button>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
413
|
+
</PopoverTrigger>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<PopoverContent align="end" className="w-[320px] rounded-xl p-1">
|
|
417
|
+
{resolved.slice(1).map((item) => {
|
|
418
|
+
const itemFeedback =
|
|
419
|
+
feedback?.id === item.key ? feedback.state : null;
|
|
420
|
+
const itemLabel = getFeedbackLabel(itemFeedback, item.title);
|
|
421
|
+
const itemDescription = getFeedbackDescription(
|
|
422
|
+
itemFeedback,
|
|
423
|
+
item.description
|
|
424
|
+
);
|
|
425
|
+
const ItemIcon = getFeedbackIcon(itemFeedback, item.icon);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<MenuItem
|
|
429
|
+
href={item.type === "link" ? item.href : undefined}
|
|
430
|
+
key={item.key}
|
|
431
|
+
onSelect={
|
|
432
|
+
item.type === "action" ? actionHandlers[item.key] : closeMenu
|
|
433
|
+
}
|
|
434
|
+
>
|
|
435
|
+
<MenuIcon>
|
|
436
|
+
<ItemIcon aria-hidden="true" className="size-[18px]" />
|
|
437
|
+
</MenuIcon>
|
|
438
|
+
<div className="flex-1">
|
|
439
|
+
<div className="font-medium">
|
|
440
|
+
{itemLabel}
|
|
441
|
+
{item.type === "link" ? <ExternalArrow /> : null}
|
|
442
|
+
</div>
|
|
443
|
+
<div className="text-xs text-muted-foreground">
|
|
444
|
+
{itemDescription}
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</MenuItem>
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
450
|
+
</PopoverContent>
|
|
451
|
+
</Popover>
|
|
367
452
|
);
|
|
368
453
|
};
|
|
369
454
|
|