mdx-artifacts 0.1.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/LICENSE +21 -0
- package/README.md +234 -0
- package/README.zh-CN.md +129 -0
- package/agents/AGENTS.snippet.md +13 -0
- package/artifact-docs/examples/commentable-feedback.mdx +126 -0
- package/artifact-docs/examples/decision-matrix.mdx +80 -0
- package/artifact-docs/examples/layout-composition.mdx +216 -0
- package/artifact-docs/examples/streamlit-style-mixed.mdx +183 -0
- package/dist/lib/cli/artifact-state.d.ts +27 -0
- package/dist/lib/cli/artifact-state.js +115 -0
- package/dist/lib/cli/build.d.ts +1 -0
- package/dist/lib/cli/build.js +25 -0
- package/dist/lib/cli/components.d.ts +3 -0
- package/dist/lib/cli/components.js +58 -0
- package/dist/lib/cli/config.d.ts +2 -0
- package/dist/lib/cli/config.js +36 -0
- package/dist/lib/cli/dev.d.ts +1 -0
- package/dist/lib/cli/dev.js +24 -0
- package/dist/lib/cli/index.d.ts +2 -0
- package/dist/lib/cli/index.js +69 -0
- package/dist/lib/cli/review.d.ts +33 -0
- package/dist/lib/cli/review.js +390 -0
- package/dist/lib/cli/scaffold.d.ts +1 -0
- package/dist/lib/cli/scaffold.js +56 -0
- package/dist/lib/cli/types.d.ts +7 -0
- package/dist/lib/cli/types.js +1 -0
- package/dist/lib/cli/validate.d.ts +6 -0
- package/dist/lib/cli/validate.js +79 -0
- package/dist/lib/cli/vite-artifact.d.ts +13 -0
- package/dist/lib/cli/vite-artifact.js +213 -0
- package/dist/lib/react/components/AnnotatedCode.d.ts +19 -0
- package/dist/lib/react/components/AnnotatedCode.js +30 -0
- package/dist/lib/react/components/ArtifactState.d.ts +68 -0
- package/dist/lib/react/components/ArtifactState.js +286 -0
- package/dist/lib/react/components/Callout.d.ts +9 -0
- package/dist/lib/react/components/Callout.js +21 -0
- package/dist/lib/react/components/CodeBlock.d.ts +10 -0
- package/dist/lib/react/components/CodeBlock.js +28 -0
- package/dist/lib/react/components/Comments.d.ts +53 -0
- package/dist/lib/react/components/Comments.js +613 -0
- package/dist/lib/react/components/ComparisonSet.d.ts +24 -0
- package/dist/lib/react/components/ComparisonSet.js +30 -0
- package/dist/lib/react/components/DecisionMatrix.d.ts +16 -0
- package/dist/lib/react/components/DecisionMatrix.js +27 -0
- package/dist/lib/react/components/DiffBlock.d.ts +15 -0
- package/dist/lib/react/components/DiffBlock.js +24 -0
- package/dist/lib/react/components/ExportPanel.d.ts +7 -0
- package/dist/lib/react/components/ExportPanel.js +84 -0
- package/dist/lib/react/components/InlineText.d.ts +9 -0
- package/dist/lib/react/components/InlineText.js +18 -0
- package/dist/lib/react/components/Layout.d.ts +44 -0
- package/dist/lib/react/components/Layout.js +28 -0
- package/dist/lib/react/components/MarkdownBody.d.ts +7 -0
- package/dist/lib/react/components/MarkdownBody.js +36 -0
- package/dist/lib/react/components/OptionGrid.d.ts +13 -0
- package/dist/lib/react/components/OptionGrid.js +21 -0
- package/dist/lib/react/components/Section.d.ts +7 -0
- package/dist/lib/react/components/Section.js +41 -0
- package/dist/lib/react/components/SeverityBadge.d.ts +7 -0
- package/dist/lib/react/components/SeverityBadge.js +14 -0
- package/dist/lib/react/index.d.ts +33 -0
- package/dist/lib/react/index.js +16 -0
- package/dist/lib/react/registry.d.ts +24 -0
- package/dist/lib/react/registry.js +1002 -0
- package/dist/lib/react/styles.css +1417 -0
- package/docs/component-protocol.md +273 -0
- package/docs/component-taxonomy.md +273 -0
- package/docs/design.md +239 -0
- package/docs/design.zh-CN.md +217 -0
- package/docs/naming.md +123 -0
- package/docs/testing.md +138 -0
- package/package.json +90 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import * as Tabs from "@radix-ui/react-tabs";
|
|
4
|
+
import { createArtifactThreadsFromState as createThreadsFromState, useOptionalArtifactState } from "./ArtifactState.js";
|
|
5
|
+
import { InlineText } from "./InlineText.js";
|
|
6
|
+
export { createArtifactCommentsFromState, createArtifactThreadsFromState } from "./ArtifactState.js";
|
|
7
|
+
const CommentContext = createContext(null);
|
|
8
|
+
export function CommentLayer({ children }) {
|
|
9
|
+
const artifactState = useOptionalArtifactState();
|
|
10
|
+
const [activeTargetId, setActiveTargetId] = useState();
|
|
11
|
+
const [threads, setThreads] = useState([]);
|
|
12
|
+
const [openTargetIds, setOpenTargetIds] = useState([]);
|
|
13
|
+
const [targets, setTargets] = useState({});
|
|
14
|
+
const [targetIds, setTargetIds] = useState([]);
|
|
15
|
+
const nextId = useRef(1);
|
|
16
|
+
const pendingPersistThreads = useRef(undefined);
|
|
17
|
+
const panels = createReviewPanels(activeTargetId, threads, openTargetIds, targetIds, targets);
|
|
18
|
+
const unplacedPanels = createUnplacedReviewPanels(threads, targetIds, targets);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!artifactState?.state) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const savedThreads = createThreadsFromState(artifactState.state);
|
|
24
|
+
if (savedThreads.length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
setThreads((current) => (sameReviewThreads(current, savedThreads) ? current : savedThreads));
|
|
28
|
+
setOpenTargetIds((current) => current.length > 0 ? current : uniqueValues(savedThreads.map((thread) => thread.blockId)));
|
|
29
|
+
}, [artifactState?.state]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!artifactState || pendingPersistThreads.current !== threads) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
pendingPersistThreads.current = undefined;
|
|
35
|
+
void artifactState.actions.saveThreads(threads);
|
|
36
|
+
}, [artifactState, threads]);
|
|
37
|
+
const value = useMemo(() => ({
|
|
38
|
+
activeTargetId,
|
|
39
|
+
threads,
|
|
40
|
+
addComment(input) {
|
|
41
|
+
const thread = createArtifactThread(input, `thread-${nextId.current}`, `comment-${nextId.current}`, new Date().toISOString());
|
|
42
|
+
if (!thread) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const nextThreads = threads.some((item) => item.blockId === thread.blockId)
|
|
46
|
+
? threads.map((item) => (item.blockId === thread.blockId ? replaceUserMessage(item, thread.messages[0]) : item))
|
|
47
|
+
: [...threads, thread];
|
|
48
|
+
nextId.current += 1;
|
|
49
|
+
pendingPersistThreads.current = nextThreads;
|
|
50
|
+
setThreads(nextThreads);
|
|
51
|
+
setOpenTargetIds((current) => (current.includes(thread.blockId) ? current : [...current, thread.blockId]));
|
|
52
|
+
},
|
|
53
|
+
closePanel(targetId) {
|
|
54
|
+
setOpenTargetIds((current) => current.filter((item) => item !== targetId));
|
|
55
|
+
setActiveTargetId((current) => (current === targetId ? undefined : current));
|
|
56
|
+
},
|
|
57
|
+
closeTarget() {
|
|
58
|
+
setActiveTargetId(undefined);
|
|
59
|
+
},
|
|
60
|
+
getActiveTarget() {
|
|
61
|
+
return activeTargetId ? targets[activeTargetId] : undefined;
|
|
62
|
+
},
|
|
63
|
+
getPanels() {
|
|
64
|
+
return panels;
|
|
65
|
+
},
|
|
66
|
+
getUnplacedPanels() {
|
|
67
|
+
return unplacedPanels;
|
|
68
|
+
},
|
|
69
|
+
getTargetNumber(targetId) {
|
|
70
|
+
const index = targetIds.indexOf(targetId);
|
|
71
|
+
return index >= 0 ? index + 1 : undefined;
|
|
72
|
+
},
|
|
73
|
+
openTarget(input) {
|
|
74
|
+
setActiveTargetId((current) => (current === input.targetId ? undefined : input.targetId));
|
|
75
|
+
setOpenTargetIds((current) => current.includes(input.targetId)
|
|
76
|
+
? current.filter((item) => item !== input.targetId)
|
|
77
|
+
: [...current, input.targetId]);
|
|
78
|
+
},
|
|
79
|
+
registerTarget(input) {
|
|
80
|
+
const nextTarget = anchorFromRect(input);
|
|
81
|
+
setTargetIds((current) => (current.includes(input.targetId) ? current : [...current, input.targetId]));
|
|
82
|
+
setTargets((current) => {
|
|
83
|
+
const previous = current[input.targetId];
|
|
84
|
+
if (previous && sameAnchor(previous, nextTarget)) {
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
return { ...current, [input.targetId]: nextTarget };
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
updateComment(commentId, comment) {
|
|
91
|
+
const normalized = comment.trim();
|
|
92
|
+
if (!normalized) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const nextThreads = threads.map((thread) => ({
|
|
96
|
+
...thread,
|
|
97
|
+
messages: thread.messages.map((message) => message.id === commentId && message.role === "user" ? { ...message, body: normalized } : message)
|
|
98
|
+
}));
|
|
99
|
+
pendingPersistThreads.current = nextThreads;
|
|
100
|
+
setThreads(nextThreads);
|
|
101
|
+
}
|
|
102
|
+
}), [activeTargetId, panels, targetIds, targets, threads, unplacedPanels]);
|
|
103
|
+
const isReviewLayoutActive = (activeTargetId !== undefined && targets[activeTargetId] !== undefined) ||
|
|
104
|
+
openTargetIds.some((targetId) => targets[targetId] !== undefined);
|
|
105
|
+
return (_jsxs(CommentContext.Provider, { value: value, children: [_jsxs("div", { className: classNames("ak-review-layout", isReviewLayoutActive ? "ak-review-layout-active" : "ak-review-layout-inactive"), children: [_jsx(CommentReviewRail, { side: "left" }), _jsx("div", { className: "ak-review-content", children: children }), _jsx(CommentReviewRail, { side: "right" })] }), _jsx(UnplacedReviewDock, {})] }));
|
|
106
|
+
}
|
|
107
|
+
export function CommentableBlock({ blockId, title, description, children, className }) {
|
|
108
|
+
return (_jsx(CommentTarget, { className: className, description: description, targetId: blockId, title: title, children: children }));
|
|
109
|
+
}
|
|
110
|
+
export function CommentTarget({ targetId, title, description, children, className }) {
|
|
111
|
+
const layer = useOptionalCommentLayer();
|
|
112
|
+
const targetRef = useRef(null);
|
|
113
|
+
const [isCompactPopoverOpen, setCompactPopoverOpen] = useState(false);
|
|
114
|
+
const [isCompactComposerOpen, setCompactComposerOpen] = useState(false);
|
|
115
|
+
const [compactDraft, setCompactDraft] = useState("");
|
|
116
|
+
const [compactEditingCommentId, setCompactEditingCommentId] = useState();
|
|
117
|
+
const [compactEditingDraft, setCompactEditingDraft] = useState("");
|
|
118
|
+
const blockThread = layer?.threads.find((thread) => thread.blockId === targetId);
|
|
119
|
+
const blockMessageCount = blockThread?.messages.length ?? 0;
|
|
120
|
+
const targetNumber = layer?.getTargetNumber(targetId);
|
|
121
|
+
const isOpen = layer?.activeTargetId === targetId;
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!layer) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
function registerCurrentTarget() {
|
|
127
|
+
const rect = targetRef.current?.getBoundingClientRect();
|
|
128
|
+
if (!rect || !layer) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
layer.registerTarget({
|
|
132
|
+
targetId,
|
|
133
|
+
title,
|
|
134
|
+
description,
|
|
135
|
+
rect,
|
|
136
|
+
contentRect: findReviewContentRect(targetRef.current)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
registerCurrentTarget();
|
|
140
|
+
window.addEventListener("resize", registerCurrentTarget);
|
|
141
|
+
window.addEventListener("scroll", registerCurrentTarget, true);
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener("resize", registerCurrentTarget);
|
|
144
|
+
window.removeEventListener("scroll", registerCurrentTarget, true);
|
|
145
|
+
};
|
|
146
|
+
}, [description, targetId, title]);
|
|
147
|
+
if (!layer) {
|
|
148
|
+
return _jsx(_Fragment, { children: children });
|
|
149
|
+
}
|
|
150
|
+
function toggleTarget() {
|
|
151
|
+
if (!layer) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const rect = targetRef.current?.getBoundingClientRect();
|
|
155
|
+
if (!rect) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
layer.openTarget({
|
|
159
|
+
targetId,
|
|
160
|
+
rect
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function closeCompactPopover() {
|
|
164
|
+
setCompactPopoverOpen(false);
|
|
165
|
+
setCompactEditingCommentId(undefined);
|
|
166
|
+
setCompactEditingDraft("");
|
|
167
|
+
}
|
|
168
|
+
function closeCompactComposer() {
|
|
169
|
+
setCompactComposerOpen(false);
|
|
170
|
+
}
|
|
171
|
+
function handleMarkerClick() {
|
|
172
|
+
if (isCompactReviewLayout()) {
|
|
173
|
+
setCompactPopoverOpen((current) => !current);
|
|
174
|
+
setCompactComposerOpen(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
toggleTarget();
|
|
178
|
+
}
|
|
179
|
+
function handleAffordanceClick() {
|
|
180
|
+
if (isCompactReviewLayout()) {
|
|
181
|
+
setCompactComposerOpen((current) => !current);
|
|
182
|
+
setCompactPopoverOpen(false);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
toggleTarget();
|
|
186
|
+
}
|
|
187
|
+
function submitCompactComment(event) {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
if (!layer) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!compactDraft.trim()) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
layer.addComment({
|
|
196
|
+
blockId: targetId,
|
|
197
|
+
blockTitle: title,
|
|
198
|
+
blockDescription: description,
|
|
199
|
+
comment: compactDraft
|
|
200
|
+
});
|
|
201
|
+
setCompactDraft("");
|
|
202
|
+
setCompactComposerOpen(false);
|
|
203
|
+
}
|
|
204
|
+
function beginCompactEdit(message) {
|
|
205
|
+
setCompactEditingCommentId(message.id);
|
|
206
|
+
setCompactEditingDraft(message.body);
|
|
207
|
+
}
|
|
208
|
+
function cancelCompactEdit() {
|
|
209
|
+
setCompactEditingCommentId(undefined);
|
|
210
|
+
setCompactEditingDraft("");
|
|
211
|
+
}
|
|
212
|
+
function submitCompactEdit(event, commentId) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
if (!layer) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
layer.updateComment(commentId, compactEditingDraft);
|
|
218
|
+
cancelCompactEdit();
|
|
219
|
+
}
|
|
220
|
+
return (_jsxs("section", { className: classNames("ak-comment-target-block", blockThread ? "ak-comment-target-has-comments" : undefined, isOpen ? "ak-comment-target-selected" : undefined, isCompactPopoverOpen ? "ak-comment-target-popover-open" : undefined, isCompactComposerOpen ? "ak-comment-target-composer-open" : undefined, className), "data-comment-target-id": targetId, "data-anchor-id": targetId, id: targetId, ref: targetRef, children: [_jsx("div", { className: "ak-comment-target-content", children: children }), blockThread ? (_jsxs("span", { className: "ak-comment-marker-anchor", children: [_jsxs("button", { "aria-label": `Show comments on ${title}`, className: "ak-comment-marker", onClick: handleMarkerClick, type: "button", children: ["#", targetNumber ?? "?", " \u00B7 ", blockMessageCount] }), _jsx(CommentTargetPopover, { editingCommentId: compactEditingCommentId, editingDraft: compactEditingDraft, number: targetNumber, onBeginEdit: beginCompactEdit, onCancelEdit: cancelCompactEdit, onClose: closeCompactPopover, onEditingDraftChange: setCompactEditingDraft, onSubmitEdit: submitCompactEdit, thread: blockThread, title: title })] })) : null, !blockThread ? (_jsxs("span", { className: "ak-comment-affordance-anchor", children: [_jsx("button", { "aria-label": `Comment on ${title}`, className: "ak-comment-affordance", onClick: handleAffordanceClick, type: "button", children: "Comment" }), _jsx(CommentTargetComposer, { draft: compactDraft, onDraftChange: setCompactDraft, onClose: closeCompactComposer, onSubmit: submitCompactComment, title: title })] })) : null] }));
|
|
221
|
+
}
|
|
222
|
+
function CommentTargetComposer({ draft, onDraftChange, onClose, onSubmit, title }) {
|
|
223
|
+
return (_jsxs("aside", { className: "ak-comment-composer-popover", role: "dialog", children: [_jsxs("div", { className: "ak-comment-form-header", children: [_jsx(InlineText, { as: "span", text: title, variant: "label" }), _jsx("button", { "aria-label": "Close comment composer", className: "ak-comment-close-button", onClick: onClose, type: "button", children: "Close" })] }), _jsxs("form", { className: "ak-comment-form", onSubmit: onSubmit, children: [_jsx("textarea", { "aria-label": `Comment on ${title}`, className: "ak-comment-input", onChange: (event) => onDraftChange(event.currentTarget.value), placeholder: "Write a block-level comment...", value: draft }), _jsx("div", { className: "ak-comment-form-actions", children: _jsx("button", { className: "ak-button ak-button-primary", disabled: !draft.trim(), type: "submit", children: "Add comment" }) })] })] }));
|
|
224
|
+
}
|
|
225
|
+
function CommentTargetPopover({ editingCommentId, editingDraft, number, onBeginEdit, onCancelEdit, onClose, onEditingDraftChange, onSubmitEdit, thread, title }) {
|
|
226
|
+
return (_jsxs("aside", { className: "ak-comment-target-popover", role: "dialog", children: [_jsxs("div", { className: "ak-comment-form-header", children: [_jsxs("span", { className: "ak-comment-target-label", children: ["#", number ?? "?"] }), _jsx(InlineText, { as: "span", text: title, variant: "label" }), _jsx("button", { "aria-label": "Close comments", className: "ak-comment-close-button", onClick: onClose, type: "button", children: "Close" })] }), _jsx(ThreadMessageList, { editingCommentId: editingCommentId, editingDraft: editingDraft, number: number, onBeginEdit: onBeginEdit, onCancelEdit: onCancelEdit, onEditingDraftChange: onEditingDraftChange, onSubmitEdit: onSubmitEdit, thread: thread })] }));
|
|
227
|
+
}
|
|
228
|
+
function ThreadMessageList({ editingCommentId, editingDraft, number, onBeginEdit, onCancelEdit, onEditingDraftChange, onSubmitEdit, thread }) {
|
|
229
|
+
return (_jsx("ul", { className: "ak-comment-list", children: thread.messages.map((message, index) => (_jsxs("li", { className: classNames("ak-comment-item", `ak-comment-item-${message.role}`), children: [_jsxs("span", { className: "ak-comment-item-number", children: ["#", number ?? "?", ".", index + 1] }), _jsx("span", { className: "ak-comment-role-label", children: message.role === "assistant" ? "Agent" : "User" }), editingCommentId === message.id ? (_jsxs("form", { className: "ak-comment-edit-form", onSubmit: (event) => onSubmitEdit(event, message.id), children: [_jsx("textarea", { "aria-label": `Edit comment ${number ?? "?"}.${index + 1}`, className: "ak-comment-input ak-comment-edit-input", onChange: (event) => onEditingDraftChange(event.currentTarget.value), value: editingDraft }), _jsxs("div", { className: "ak-comment-form-actions", children: [_jsx("button", { className: "ak-button", onClick: onCancelEdit, type: "button", children: "Cancel" }), _jsx("button", { className: "ak-button ak-button-primary", disabled: !editingDraft.trim(), type: "submit", children: "Save" })] })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { children: message.body }), _jsxs("div", { className: "ak-comment-item-footer", children: [message.createdAt ? _jsx("time", { dateTime: message.createdAt, children: message.createdAt }) : _jsx("span", {}), message.role === "user" ? (_jsx("button", { className: "ak-comment-edit-button", onClick: () => onBeginEdit(message), type: "button", children: "Edit" })) : null] })] }))] }, message.id))) }));
|
|
230
|
+
}
|
|
231
|
+
function CommentReviewRail({ side }) {
|
|
232
|
+
const [draft, setDraft] = useState("");
|
|
233
|
+
const [editingCommentId, setEditingCommentId] = useState();
|
|
234
|
+
const [editingDraft, setEditingDraft] = useState("");
|
|
235
|
+
const [panelHeights, setPanelHeights] = useState({});
|
|
236
|
+
const [railTop, setRailTop] = useState(0);
|
|
237
|
+
const context = useCommentLayer("CommentReviewRail");
|
|
238
|
+
const activeTarget = context.getActiveTarget();
|
|
239
|
+
const panels = context.getPanels().filter((panel) => panel.side === side);
|
|
240
|
+
const positionedPanels = positionReviewPanels(panels, panelHeights, railTop);
|
|
241
|
+
const panelSignature = panels
|
|
242
|
+
.map((panel) => `${panel.targetId}:${panel.thread?.messages.length ?? 0}:${panel.isActive ? "active" : "idle"}`)
|
|
243
|
+
.join("|");
|
|
244
|
+
const railRef = useRef(null);
|
|
245
|
+
const panelRefs = useRef(new Map());
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
function measurePanels() {
|
|
248
|
+
const nextHeights = {};
|
|
249
|
+
panelRefs.current.forEach((element, targetId) => {
|
|
250
|
+
nextHeights[targetId] = element.offsetHeight;
|
|
251
|
+
});
|
|
252
|
+
const nextRailTop = railRef.current
|
|
253
|
+
? railRef.current.getBoundingClientRect().top + window.scrollY
|
|
254
|
+
: 0;
|
|
255
|
+
setPanelHeights((current) => (samePanelHeights(current, nextHeights) ? current : nextHeights));
|
|
256
|
+
setRailTop((current) => (Math.round(current) === Math.round(nextRailTop) ? current : nextRailTop));
|
|
257
|
+
}
|
|
258
|
+
measurePanels();
|
|
259
|
+
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(measurePanels);
|
|
260
|
+
if (railRef.current) {
|
|
261
|
+
observer?.observe(railRef.current);
|
|
262
|
+
}
|
|
263
|
+
panelRefs.current.forEach((element) => observer?.observe(element));
|
|
264
|
+
window.addEventListener("resize", measurePanels);
|
|
265
|
+
return () => {
|
|
266
|
+
observer?.disconnect();
|
|
267
|
+
window.removeEventListener("resize", measurePanels);
|
|
268
|
+
};
|
|
269
|
+
}, [editingCommentId, panelSignature]);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
setDraft("");
|
|
272
|
+
setEditingCommentId(undefined);
|
|
273
|
+
setEditingDraft("");
|
|
274
|
+
}, [context.activeTargetId]);
|
|
275
|
+
function setPanelRef(targetId) {
|
|
276
|
+
return (element) => {
|
|
277
|
+
if (element) {
|
|
278
|
+
panelRefs.current.set(targetId, element);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
panelRefs.current.delete(targetId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function submitComment(event) {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
if (!activeTarget) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!draft.trim()) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
context.addComment({
|
|
294
|
+
blockId: activeTarget.targetId,
|
|
295
|
+
blockTitle: activeTarget.title,
|
|
296
|
+
blockDescription: activeTarget.description,
|
|
297
|
+
comment: draft
|
|
298
|
+
});
|
|
299
|
+
setDraft("");
|
|
300
|
+
context.closeTarget();
|
|
301
|
+
}
|
|
302
|
+
function beginEdit(message) {
|
|
303
|
+
setEditingCommentId(message.id);
|
|
304
|
+
setEditingDraft(message.body);
|
|
305
|
+
}
|
|
306
|
+
function submitEdit(event, commentId) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
context.updateComment(commentId, editingDraft);
|
|
309
|
+
setEditingCommentId(undefined);
|
|
310
|
+
setEditingDraft("");
|
|
311
|
+
}
|
|
312
|
+
return (_jsx("aside", { className: `ak-review-rail ak-review-rail-${side}`, ref: railRef, children: positionedPanels.map((panel) => {
|
|
313
|
+
const isActive = panel.targetId === context.activeTargetId;
|
|
314
|
+
return (_jsxs("section", { className: classNames("ak-comment-panel", isActive ? "ak-comment-panel-active" : undefined), ref: setPanelRef(panel.targetId), style: commentPanelStyle(panel.top), children: [_jsxs("div", { className: "ak-comment-form-header", children: [_jsxs("span", { className: "ak-comment-target-label", children: ["#", panel.number] }), _jsx(InlineText, { as: "span", text: panel.title, variant: "label" }), _jsx("button", { "aria-label": `Close comments on ${panel.title}`, className: "ak-comment-close-button", onClick: () => context.closePanel(panel.targetId), type: "button", children: "Close" })] }), isActive && !panel.thread ? (_jsxs("form", { className: "ak-comment-form", onSubmit: submitComment, children: [_jsx("textarea", { "aria-label": `Comment on ${panel.title}`, className: "ak-comment-input", onChange: (event) => setDraft(event.currentTarget.value), placeholder: "Write a block-level comment...", value: draft }), _jsx("div", { className: "ak-comment-form-actions", children: _jsx("button", { className: "ak-button ak-button-primary", disabled: !draft.trim(), type: "submit", children: "Add comment" }) })] })) : null, panel.thread ? (_jsx(ThreadMessageList, { editingCommentId: editingCommentId, editingDraft: editingDraft, number: panel.number, onBeginEdit: beginEdit, onCancelEdit: () => {
|
|
315
|
+
setEditingCommentId(undefined);
|
|
316
|
+
setEditingDraft("");
|
|
317
|
+
}, onEditingDraftChange: setEditingDraft, onSubmitEdit: submitEdit, thread: panel.thread })) : null] }, panel.targetId));
|
|
318
|
+
}) }));
|
|
319
|
+
}
|
|
320
|
+
function UnplacedReviewDock() {
|
|
321
|
+
const context = useCommentLayer("UnplacedReviewDock");
|
|
322
|
+
const panels = context.getUnplacedPanels();
|
|
323
|
+
const [isOpen, setOpen] = useState(false);
|
|
324
|
+
const [editingCommentId, setEditingCommentId] = useState();
|
|
325
|
+
const [editingDraft, setEditingDraft] = useState("");
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
if (panels.length === 0) {
|
|
328
|
+
setOpen(false);
|
|
329
|
+
}
|
|
330
|
+
}, [panels.length]);
|
|
331
|
+
if (panels.length === 0) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function beginEdit(message) {
|
|
335
|
+
setEditingCommentId(message.id);
|
|
336
|
+
setEditingDraft(message.body);
|
|
337
|
+
}
|
|
338
|
+
function cancelEdit() {
|
|
339
|
+
setEditingCommentId(undefined);
|
|
340
|
+
setEditingDraft("");
|
|
341
|
+
}
|
|
342
|
+
function submitEdit(event, commentId) {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
context.updateComment(commentId, editingDraft);
|
|
345
|
+
cancelEdit();
|
|
346
|
+
}
|
|
347
|
+
return (_jsxs("aside", { className: "ak-unplaced-review-dock", "aria-label": "Unplaced comments", children: [_jsxs("button", { className: "ak-unplaced-review-trigger", onClick: () => setOpen((current) => !current), type: "button", children: ["Unplaced \u00B7 ", panels.length] }), isOpen ? (_jsxs("div", { className: "ak-unplaced-review-sheet", role: "dialog", children: [_jsxs("div", { className: "ak-section-header ak-unplaced-review-header", children: [_jsxs("div", { children: [_jsx("p", { className: "ak-eyebrow", children: "Review" }), _jsx(InlineText, { as: "h2", text: "Unplaced comments", variant: "title" })] }), _jsx("button", { className: "ak-button", onClick: () => setOpen(false), type: "button", children: "Close" })] }), _jsx("div", { className: "ak-unplaced-review-list", children: panels.map((panel) => (_jsxs("section", { className: "ak-unplaced-review-card", children: [_jsxs("div", { className: "ak-comment-form-header", children: [_jsxs("span", { className: "ak-comment-target-label", children: ["#", panel.number] }), _jsx(InlineText, { as: "span", text: panel.title, variant: "label" })] }), _jsxs("p", { className: "ak-unplaced-review-anchor", children: ["Missing anchor: ", panel.targetId] }), panel.thread ? (_jsx(ThreadMessageList, { editingCommentId: editingCommentId, editingDraft: editingDraft, number: panel.number, onBeginEdit: beginEdit, onCancelEdit: cancelEdit, onEditingDraftChange: setEditingDraft, onSubmitEdit: submitEdit, thread: panel.thread })) : null] }, panel.targetId))) })] })) : null] }));
|
|
348
|
+
}
|
|
349
|
+
export function CommentExport({ title = "Export Comments", formats = ["markdown", "json"] }) {
|
|
350
|
+
const layer = useCommentLayer("CommentExport");
|
|
351
|
+
const [format, setFormat] = useState(formats[0] ?? "markdown");
|
|
352
|
+
const [isOpen, setOpen] = useState(false);
|
|
353
|
+
const [copied, setCopied] = useState(false);
|
|
354
|
+
const exportValue = { comments: createCommentsFromThreads(layer.threads), threads: layer.threads };
|
|
355
|
+
const output = format === "json" ? JSON.stringify(exportValue, null, 2) : serializeCommentsToMarkdown(exportValue);
|
|
356
|
+
async function copyOutput() {
|
|
357
|
+
if (navigator.clipboard?.writeText) {
|
|
358
|
+
await navigator.clipboard.writeText(output);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
fallbackCopy(output);
|
|
362
|
+
}
|
|
363
|
+
setCopied(true);
|
|
364
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
365
|
+
}
|
|
366
|
+
return (_jsxs("aside", { className: "ak-export-dock ak-comment-export", "aria-label": title, children: [_jsx("button", { className: "ak-export-dock-trigger", onClick: () => setOpen((current) => !current), type: "button", children: "Comments" }), isOpen ? (_jsxs("div", { className: "ak-export-drawer ak-comment-export-drawer", role: "dialog", children: [_jsxs("div", { className: "ak-section-header ak-export-header", children: [_jsxs("div", { children: [_jsx("p", { className: "ak-eyebrow", children: "Comments" }), _jsx(InlineText, { as: "h2", text: title, variant: "title" })] }), _jsx("button", { className: "ak-button ak-comment-export-close", onClick: () => setOpen(false), type: "button", children: "Close" })] }), _jsxs("div", { className: "ak-export-toolbar", children: [_jsx(Tabs.Root, { className: "ak-format-tabs", onValueChange: (value) => setFormat(value), value: format, children: _jsx(Tabs.List, { "aria-label": "Comment export format", className: "ak-format-tabs-list", children: formats.map((item) => (_jsx(Tabs.Trigger, { className: "ak-format-trigger", value: item, children: item }, item))) }) }), _jsx("button", { className: "ak-button ak-button-primary", onClick: copyOutput, type: "button", children: copied ? "Copied" : "Copy" })] }), _jsx("pre", { className: "ak-export-output", children: output })] })) : null] }));
|
|
367
|
+
}
|
|
368
|
+
export function useOptionalCommentExportValue() {
|
|
369
|
+
const layer = useOptionalCommentLayer();
|
|
370
|
+
return layer ? { comments: createCommentsFromThreads(layer.threads), threads: layer.threads } : undefined;
|
|
371
|
+
}
|
|
372
|
+
export function serializeCommentsToMarkdown(value) {
|
|
373
|
+
const threads = value.threads ?? createThreadsFromComments(value.comments ?? []);
|
|
374
|
+
if (threads.length === 0) {
|
|
375
|
+
return "# Artifact comments\n\nNo comments yet.";
|
|
376
|
+
}
|
|
377
|
+
return [
|
|
378
|
+
"# Artifact comments",
|
|
379
|
+
"",
|
|
380
|
+
...threads.flatMap((thread) => [
|
|
381
|
+
`## ${thread.blockTitle}`,
|
|
382
|
+
`- blockId: ${thread.blockId}`,
|
|
383
|
+
`- status: ${thread.status}`,
|
|
384
|
+
thread.blockDescription ? `- description: ${thread.blockDescription}` : undefined,
|
|
385
|
+
"",
|
|
386
|
+
"### Messages",
|
|
387
|
+
"",
|
|
388
|
+
...thread.messages.map((message) => {
|
|
389
|
+
const createdAt = message.createdAt ? ` (${message.createdAt})` : "";
|
|
390
|
+
return `- ${message.role}${createdAt}: ${message.body}`;
|
|
391
|
+
}),
|
|
392
|
+
""
|
|
393
|
+
].filter(Boolean))
|
|
394
|
+
].join("\n").trimEnd();
|
|
395
|
+
}
|
|
396
|
+
export function createArtifactComment(input, id, createdAt) {
|
|
397
|
+
const normalized = input.comment.trim();
|
|
398
|
+
if (!normalized) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
id,
|
|
403
|
+
blockId: input.blockId,
|
|
404
|
+
blockTitle: input.blockTitle,
|
|
405
|
+
blockDescription: input.blockDescription,
|
|
406
|
+
comment: normalized,
|
|
407
|
+
createdAt
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
export function createArtifactThread(input, threadId, messageId, createdAt) {
|
|
411
|
+
const comment = createArtifactComment(input, messageId, createdAt);
|
|
412
|
+
if (!comment) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
id: threadId,
|
|
417
|
+
blockId: comment.blockId,
|
|
418
|
+
blockTitle: comment.blockTitle,
|
|
419
|
+
blockDescription: comment.blockDescription,
|
|
420
|
+
status: "open",
|
|
421
|
+
messages: [
|
|
422
|
+
{
|
|
423
|
+
id: comment.id,
|
|
424
|
+
role: "user",
|
|
425
|
+
body: comment.comment,
|
|
426
|
+
createdAt: comment.createdAt
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function replaceUserMessage(thread, message) {
|
|
432
|
+
if (!message) {
|
|
433
|
+
return thread;
|
|
434
|
+
}
|
|
435
|
+
const hasUserMessage = thread.messages.some((item) => item.role === "user");
|
|
436
|
+
return {
|
|
437
|
+
...thread,
|
|
438
|
+
messages: hasUserMessage
|
|
439
|
+
? thread.messages.map((item) => (item.role === "user" ? { ...item, body: message.body } : item))
|
|
440
|
+
: [message, ...thread.messages]
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function createCommentsFromThreads(threads) {
|
|
444
|
+
return threads.flatMap((thread) => {
|
|
445
|
+
const userMessage = thread.messages.find((message) => message.role === "user");
|
|
446
|
+
if (!userMessage) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
return [
|
|
450
|
+
{
|
|
451
|
+
id: userMessage.id,
|
|
452
|
+
blockId: thread.blockId,
|
|
453
|
+
blockTitle: thread.blockTitle,
|
|
454
|
+
blockDescription: thread.blockDescription,
|
|
455
|
+
comment: userMessage.body,
|
|
456
|
+
createdAt: userMessage.createdAt ?? ""
|
|
457
|
+
}
|
|
458
|
+
];
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function createThreadsFromComments(comments) {
|
|
462
|
+
return comments.flatMap((comment) => {
|
|
463
|
+
const thread = createArtifactThread({
|
|
464
|
+
blockId: comment.blockId,
|
|
465
|
+
blockTitle: comment.blockTitle,
|
|
466
|
+
blockDescription: comment.blockDescription,
|
|
467
|
+
comment: comment.comment
|
|
468
|
+
}, `thread-${comment.id}`, comment.id, comment.createdAt);
|
|
469
|
+
return thread ? [thread] : [];
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function createReviewPanels(activeTargetId, threads, openTargetIds, targetIds, targets) {
|
|
473
|
+
const panelMap = new Map();
|
|
474
|
+
for (const thread of threads) {
|
|
475
|
+
if (!openTargetIds.includes(thread.blockId) && activeTargetId !== thread.blockId) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const target = targets[thread.blockId];
|
|
479
|
+
if (!target) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const number = targetIds.indexOf(thread.blockId) + 1 || panelMap.size + 1;
|
|
483
|
+
panelMap.set(thread.blockId, {
|
|
484
|
+
number,
|
|
485
|
+
side: panelMap.get(thread.blockId)?.side ?? target.side,
|
|
486
|
+
targetId: thread.blockId,
|
|
487
|
+
thread,
|
|
488
|
+
title: thread.blockTitle,
|
|
489
|
+
top: panelMap.get(thread.blockId)?.top ?? target.top,
|
|
490
|
+
isActive: activeTargetId === thread.blockId
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (activeTargetId && targets[activeTargetId]) {
|
|
494
|
+
const activeTarget = targets[activeTargetId];
|
|
495
|
+
const current = panelMap.get(activeTargetId);
|
|
496
|
+
panelMap.set(activeTargetId, {
|
|
497
|
+
number: targetIds.indexOf(activeTargetId) + 1 || panelMap.size + 1,
|
|
498
|
+
side: activeTarget.side,
|
|
499
|
+
targetId: activeTargetId,
|
|
500
|
+
thread: current?.thread,
|
|
501
|
+
title: activeTarget.title,
|
|
502
|
+
top: activeTarget.top,
|
|
503
|
+
isActive: true
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return Array.from(panelMap.values());
|
|
507
|
+
}
|
|
508
|
+
function createUnplacedReviewPanels(threads, targetIds, targets) {
|
|
509
|
+
const placedThreadIds = new Set(targetIds);
|
|
510
|
+
const placedCount = targetIds.length;
|
|
511
|
+
let unplacedCount = 0;
|
|
512
|
+
return threads.flatMap((thread) => {
|
|
513
|
+
if (targets[thread.blockId] || placedThreadIds.has(thread.blockId)) {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
unplacedCount += 1;
|
|
517
|
+
return [
|
|
518
|
+
{
|
|
519
|
+
number: placedCount + unplacedCount,
|
|
520
|
+
side: "right",
|
|
521
|
+
targetId: thread.blockId,
|
|
522
|
+
thread,
|
|
523
|
+
title: thread.blockTitle,
|
|
524
|
+
top: 0
|
|
525
|
+
}
|
|
526
|
+
];
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function positionReviewPanels(panels, panelHeights, railTop) {
|
|
530
|
+
let nextTop = 16;
|
|
531
|
+
return [...panels].sort((first, second) => first.top - second.top).map((panel) => {
|
|
532
|
+
const preferredTop = Math.max(0, panel.top - railTop);
|
|
533
|
+
const top = Math.max(preferredTop, nextTop);
|
|
534
|
+
nextTop = top + (panelHeights[panel.targetId] ?? estimatePanelHeight(panel)) + 12;
|
|
535
|
+
return { ...panel, top };
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function estimatePanelHeight(panel) {
|
|
539
|
+
return 54 + (panel.isActive ? 150 : 0) + (panel.thread?.messages.length ?? 0) * 96;
|
|
540
|
+
}
|
|
541
|
+
function anchorFromRect(input) {
|
|
542
|
+
const side = choosePanelSide(input.rect, input.contentRect);
|
|
543
|
+
return {
|
|
544
|
+
targetId: input.targetId,
|
|
545
|
+
title: input.title,
|
|
546
|
+
description: input.description,
|
|
547
|
+
top: Math.max(0, input.rect.top + window.scrollY),
|
|
548
|
+
side
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function sameAnchor(previous, next) {
|
|
552
|
+
return (previous.title === next.title &&
|
|
553
|
+
previous.description === next.description &&
|
|
554
|
+
previous.side === next.side &&
|
|
555
|
+
Math.round(previous.top) === Math.round(next.top));
|
|
556
|
+
}
|
|
557
|
+
function samePanelHeights(first, second) {
|
|
558
|
+
const firstKeys = Object.keys(first);
|
|
559
|
+
const secondKeys = Object.keys(second);
|
|
560
|
+
if (firstKeys.length !== secondKeys.length) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
return firstKeys.every((key) => Math.round(first[key] ?? 0) === Math.round(second[key] ?? 0));
|
|
564
|
+
}
|
|
565
|
+
function sameReviewThreads(first, second) {
|
|
566
|
+
return JSON.stringify(first) === JSON.stringify(second);
|
|
567
|
+
}
|
|
568
|
+
function isCompactReviewLayout() {
|
|
569
|
+
return typeof window !== "undefined" && window.matchMedia("(max-width: 1548px)").matches;
|
|
570
|
+
}
|
|
571
|
+
function choosePanelSide(rect, contentRect) {
|
|
572
|
+
if (!contentRect) {
|
|
573
|
+
const rightSpace = window.innerWidth - rect.right;
|
|
574
|
+
const leftSpace = rect.left;
|
|
575
|
+
return rightSpace >= 320 || rightSpace >= leftSpace ? "right" : "left";
|
|
576
|
+
}
|
|
577
|
+
const relativeCenter = rect.left + rect.width / 2 - contentRect.left;
|
|
578
|
+
const leftBand = contentRect.width / 3;
|
|
579
|
+
return relativeCenter <= leftBand ? "left" : "right";
|
|
580
|
+
}
|
|
581
|
+
function findReviewContentRect(element) {
|
|
582
|
+
return element?.closest(".ak-review-content")?.getBoundingClientRect();
|
|
583
|
+
}
|
|
584
|
+
function useCommentLayer(componentName) {
|
|
585
|
+
const context = useContext(CommentContext);
|
|
586
|
+
if (!context) {
|
|
587
|
+
throw new Error(`${componentName} must be used inside CommentLayer.`);
|
|
588
|
+
}
|
|
589
|
+
return context;
|
|
590
|
+
}
|
|
591
|
+
function useOptionalCommentLayer() {
|
|
592
|
+
return useContext(CommentContext);
|
|
593
|
+
}
|
|
594
|
+
function fallbackCopy(value) {
|
|
595
|
+
const textarea = document.createElement("textarea");
|
|
596
|
+
textarea.value = value;
|
|
597
|
+
textarea.setAttribute("readonly", "true");
|
|
598
|
+
textarea.style.position = "fixed";
|
|
599
|
+
textarea.style.left = "-9999px";
|
|
600
|
+
document.body.appendChild(textarea);
|
|
601
|
+
textarea.select();
|
|
602
|
+
document.execCommand("copy");
|
|
603
|
+
document.body.removeChild(textarea);
|
|
604
|
+
}
|
|
605
|
+
function commentPanelStyle(top) {
|
|
606
|
+
return { "--ak-comment-panel-top": `${top}px` };
|
|
607
|
+
}
|
|
608
|
+
function classNames(...values) {
|
|
609
|
+
return values.filter(Boolean).join(" ");
|
|
610
|
+
}
|
|
611
|
+
function uniqueValues(values) {
|
|
612
|
+
return Array.from(new Set(values));
|
|
613
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type AkCollapseAt, type AkGap } from "./Layout";
|
|
3
|
+
export type ComparisonSetProps = {
|
|
4
|
+
id?: string;
|
|
5
|
+
title: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
columns?: 2 | 3 | 4;
|
|
8
|
+
gap?: AkGap;
|
|
9
|
+
collapseAt?: AkCollapseAt;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ComparisonSetItemProps = {
|
|
13
|
+
id?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
value?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
declare function ComparisonSetRoot({ id, title, children, columns, gap, collapseAt, className }: ComparisonSetProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
declare function ComparisonSetItem({ id, title, children, value, className }: ComparisonSetItemProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare const ComparisonSet: typeof ComparisonSetRoot & {
|
|
22
|
+
Item: typeof ComparisonSetItem;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { Grid } from "./Layout.js";
|
|
4
|
+
import { InlineText } from "./InlineText.js";
|
|
5
|
+
import { CommentTarget } from "./Comments.js";
|
|
6
|
+
const ComparisonSetIdContext = createContext(undefined);
|
|
7
|
+
function ComparisonSetRoot({ id, title, children, columns = 2, gap = "md", collapseAt = "md", className }) {
|
|
8
|
+
const targetId = id ?? `comparison:${slugify(title)}`;
|
|
9
|
+
return (_jsx(CommentTarget, { className: classNames("ak-comment-target-section", className), description: "ComparisonSet component", targetId: targetId, title: title, children: _jsxs("section", { className: "ak-section ak-comparison-set", children: [_jsxs("div", { className: "ak-section-header", children: [_jsx("p", { className: "ak-eyebrow", children: "Comparison Set" }), _jsx(InlineText, { as: "h2", text: title, variant: "title" })] }), _jsx(ComparisonSetIdContext.Provider, { value: id, children: _jsx(Grid, { collapseAt: collapseAt, columns: columns, gap: gap, children: children }) })] }) }));
|
|
10
|
+
}
|
|
11
|
+
function ComparisonSetItem({ id, title, children, value, className }) {
|
|
12
|
+
const comparisonSetId = useContext(ComparisonSetIdContext);
|
|
13
|
+
const targetId = comparisonSetId && id ? `${comparisonSetId}.${id}` : id ?? `comparison:${slugify(value ?? title)}`;
|
|
14
|
+
return (_jsx(CommentTarget, { className: classNames("ak-comment-target-card", className), description: "ComparisonSet item", targetId: targetId, title: title, children: _jsxs("article", { className: "ak-frame ak-surface-outlined ak-padding-md ak-comparison-item", "data-value": value, children: [_jsx(InlineText, { as: "h3", text: title, variant: "subtitle" }), _jsx("div", { className: "ak-comparison-item-body", children: children })] }) }));
|
|
15
|
+
}
|
|
16
|
+
export const ComparisonSet = Object.assign(ComparisonSetRoot, {
|
|
17
|
+
Item: ComparisonSetItem
|
|
18
|
+
});
|
|
19
|
+
function classNames(...values) {
|
|
20
|
+
return values.filter(Boolean).join(" ");
|
|
21
|
+
}
|
|
22
|
+
function slugify(value) {
|
|
23
|
+
const slug = value
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[`*_~[\]()]/g, "")
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "")
|
|
28
|
+
.slice(0, 64);
|
|
29
|
+
return slug || "item";
|
|
30
|
+
}
|