@aparte/plugin-artifacts 0.16.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/index.js ADDED
@@ -0,0 +1,675 @@
1
+ import { escapeHtml, contextConfig, aparteGlobalConfig, escapeAttr, copyText, registerSegmentRenderer, unregisterSegmentRenderer } from "@aparte/core";
2
+ import { d as ARTIFACT_SEGMENT_TYPE, i as deriveArtifactKind, P as PREVIEW_CSP, h as buildSafePreviewDocument, c as createArtifactTool, a as artifactHandler, A as ARTIFACT_TAG, b as artifactBlock, f as artifactFromToolCall } from "./preview-document-xP-Ne-F0.js";
3
+ import { e, g } from "./preview-document-xP-Ne-F0.js";
4
+ function stripCodeFences(content) {
5
+ let s = content;
6
+ if (s.startsWith("```") || s.startsWith("~~~")) {
7
+ const fenceChar = s[0];
8
+ let i = 0;
9
+ while (i < s.length && s[i] === fenceChar) i++;
10
+ while (i < s.length && s[i] !== "\n") i++;
11
+ if (i < s.length && s[i] === "\n") i++;
12
+ s = s.slice(i);
13
+ }
14
+ const closeAt = findClosingFence(s);
15
+ if (closeAt !== -1) s = s.slice(0, closeAt);
16
+ return s.trim();
17
+ }
18
+ function findClosingFence(s) {
19
+ let i = 0;
20
+ while (i < s.length) {
21
+ const lineStart = i;
22
+ let k = lineStart;
23
+ while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
24
+ if (k < s.length && (s[k] === "`" || s[k] === "~")) {
25
+ const fenceChar = s[k];
26
+ let runs = 0;
27
+ while (k < s.length && s[k] === fenceChar) {
28
+ runs++;
29
+ k++;
30
+ }
31
+ if (runs >= 3) {
32
+ let onlyWs = true;
33
+ while (k < s.length && s[k] !== "\n") {
34
+ if (s[k] !== " " && s[k] !== " " && s[k] !== "\r") {
35
+ onlyWs = false;
36
+ break;
37
+ }
38
+ k++;
39
+ }
40
+ if (onlyWs) {
41
+ let cut = lineStart;
42
+ if (cut > 0 && s[cut - 1] === "\n") cut--;
43
+ return cut;
44
+ }
45
+ }
46
+ }
47
+ while (i < s.length && s[i] !== "\n") i++;
48
+ if (i < s.length) i++;
49
+ }
50
+ return -1;
51
+ }
52
+ function labelForKind(kind2) {
53
+ switch (kind2) {
54
+ case "react":
55
+ return "React component";
56
+ case "html":
57
+ return "HTML document";
58
+ case "svg":
59
+ return "SVG image";
60
+ case "js":
61
+ return "JavaScript snippet";
62
+ case "css":
63
+ return "CSS stylesheet";
64
+ case "json":
65
+ return "JSON document";
66
+ case "markdown":
67
+ return "Markdown document";
68
+ case "csv":
69
+ return "CSV table";
70
+ case "text":
71
+ return "Text file";
72
+ case "python":
73
+ return "Python script";
74
+ case "typescript":
75
+ return "TypeScript file";
76
+ case "bash":
77
+ return "Bash script";
78
+ case "sql":
79
+ return "SQL query";
80
+ case "pdf":
81
+ return "PDF generator";
82
+ case "xlsx":
83
+ return "Excel generator";
84
+ case "docx":
85
+ return "Word generator";
86
+ default:
87
+ return "Artifact";
88
+ }
89
+ }
90
+ const HIGHLIGHT_DEBOUNCE_MS = 400;
91
+ const MAX_THROTTLE_ENTRIES = 256;
92
+ const _lastHighlightAt = /* @__PURE__ */ new Map();
93
+ function markThrottle(map, id, at) {
94
+ map.delete(id);
95
+ if (map.size >= MAX_THROTTLE_ENTRIES) {
96
+ const oldest = map.keys().next().value;
97
+ if (oldest !== void 0) map.delete(oldest);
98
+ }
99
+ map.set(id, at);
100
+ }
101
+ function highlightedLen(pane) {
102
+ const n = Number(pane.dataset.aparteHlLen);
103
+ return Number.isFinite(n) && n > 0 ? n : 0;
104
+ }
105
+ function streamHighlight(element, paneSelector, content, lang, segId) {
106
+ const pane = element.querySelector(paneSelector);
107
+ if (!pane) return;
108
+ const tail = pane.querySelector("[data-aparte-tail]");
109
+ const hlLen = highlightedLen(pane);
110
+ if (tail && hlLen <= content.length) {
111
+ tail.textContent = content.slice(hlLen);
112
+ } else {
113
+ delete pane.dataset.aparteHlLen;
114
+ const codeEl = pane.querySelector("code");
115
+ if (codeEl) codeEl.textContent = content;
116
+ else pane.innerHTML = `<pre><code class="language-${escapeHtml(lang || "text")}">${escapeHtml(content)}</code></pre>`;
117
+ }
118
+ const cut = content.lastIndexOf("\n") + 1;
119
+ if (cut <= hlLen) return;
120
+ const now = Date.now();
121
+ if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
122
+ markThrottle(_lastHighlightAt, segId, now);
123
+ void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
124
+ const live = element.querySelector(paneSelector);
125
+ if (!live) return;
126
+ if (cut <= highlightedLen(live)) return;
127
+ live.innerHTML = html;
128
+ const codeEl = live.querySelector("code") ?? live;
129
+ const span = document.createElement("span");
130
+ span.dataset.aparteTail = "";
131
+ span.textContent = content.slice(cut);
132
+ codeEl.appendChild(span);
133
+ live.dataset.aparteHlLen = String(cut);
134
+ }).catch(() => {
135
+ });
136
+ }
137
+ const settings = /* @__PURE__ */ new WeakMap();
138
+ function setRenderOptions(config, options) {
139
+ settings.set(config, options);
140
+ }
141
+ function renderOptions(config) {
142
+ return settings.get(config) ?? settings.get(aparteGlobalConfig) ?? {};
143
+ }
144
+ function clearRenderOptions(config) {
145
+ settings.delete(config);
146
+ }
147
+ const FILE_ICON_LABEL = {
148
+ xlsx: "XLS",
149
+ pdf: "PDF",
150
+ docx: "DOC"
151
+ };
152
+ const produced = /* @__PURE__ */ new Map();
153
+ const MAX_PRODUCED = 24;
154
+ const inFlight = /* @__PURE__ */ new Map();
155
+ function remember(id, bin) {
156
+ produced.delete(id);
157
+ if (produced.size >= MAX_PRODUCED) {
158
+ const oldest = produced.keys().next().value;
159
+ if (oldest !== void 0) produced.delete(oldest);
160
+ }
161
+ produced.set(id, bin);
162
+ }
163
+ function renderBinaryFileArtifact(segment, kind2) {
164
+ const cfg = contextConfig();
165
+ const title = segment.title?.trim() || labelForKind(kind2);
166
+ const iconLabel = FILE_ICON_LABEL[kind2] ?? kind2.toUpperCase();
167
+ const isStreaming = !!segment.isStreaming;
168
+ const canProduce = typeof renderOptions(cfg).onBinary === "function";
169
+ const downloadLabel = escapeHtml(cfg.t("download"));
170
+ const done = produced.get(segment.id);
171
+ if (done && !isStreaming) {
172
+ const preview = previewMarkup(done, kind2);
173
+ return `
174
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
175
+ data-segment-id="${escapeHtml(segment.id)}"
176
+ data-artifact-type="${escapeHtml(kind2)}"
177
+ data-state="ready">
178
+ <div class="aparte-art-file__card">
179
+ <div class="aparte-art-file__icon" data-kind="${escapeHtml(kind2)}">${escapeHtml(iconLabel)}</div>
180
+ <div class="aparte-art-file__meta">
181
+ <div class="aparte-art-file__meta-name" data-role="file-name">${escapeHtml(done.filename)}</div>
182
+ <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(byteLength(done.buffer)))} · ${escapeHtml(kind2.toUpperCase())}</div>
183
+ </div>
184
+ <div class="aparte-art-file__actions">
185
+ <button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>
186
+ </div>
187
+ </div>
188
+ <div class="aparte-art-file__body">
189
+ <div class="aparte-art-file__code-pane" data-role="code-pane" hidden>
190
+ <pre><code class="language-js"></code></pre>
191
+ </div>
192
+ <div class="aparte-art-file__preview-pane" data-role="preview-pane">${preview}</div>
193
+ </div>
194
+ </div>
195
+ `;
196
+ }
197
+ const cleanContent = stripCodeFences(segment.content || "");
198
+ const subText = isStreaming ? cfg.t("generating") : canProduce ? cfg.t("rebuildingPreview") : kind2.toUpperCase();
199
+ return `
200
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
201
+ data-segment-id="${escapeHtml(segment.id)}"
202
+ data-artifact-type="${escapeHtml(kind2)}"
203
+ data-state="${escapeAttr(isStreaming ? "streaming" : canProduce ? "compiling" : "source")}">
204
+ <div class="aparte-art-file__card">
205
+ <div class="aparte-art-file__icon" data-kind="${escapeHtml(kind2)}">${escapeHtml(iconLabel)}</div>
206
+ <div class="aparte-art-file__meta">
207
+ <div class="aparte-art-file__meta-name" data-role="file-name">${escapeHtml(title)}</div>
208
+ <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
209
+ </div>
210
+ <div class="aparte-art-file__actions">
211
+ ${canProduce ? `<button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
212
+ </div>
213
+ </div>
214
+ <div class="aparte-art-file__body">
215
+ <div class="aparte-art-file__code-pane" data-role="code-pane">
216
+ <pre><code class="language-js">${escapeHtml(cleanContent)}</code></pre>
217
+ </div>
218
+ <div class="aparte-art-file__preview-pane" data-role="preview-pane" hidden></div>
219
+ </div>
220
+ </div>
221
+ `;
222
+ }
223
+ function setupBinaryFileArtifact(element, segment, kind2) {
224
+ if (element.dataset["aparteInit"] !== "true") {
225
+ element.dataset["aparteInit"] = "true";
226
+ element.addEventListener("click", (ev) => {
227
+ const target = ev.target;
228
+ const action = target.closest("[data-action]")?.getAttribute("data-action");
229
+ if (action !== "download") return;
230
+ const bin = produced.get(segment.id);
231
+ if (bin) downloadBinary(bin);
232
+ });
233
+ }
234
+ if (segment.isStreaming) return;
235
+ settle(element, segment, kind2);
236
+ }
237
+ function updateBinaryFileArtifact(element, segment, isStreaming) {
238
+ const state = element.getAttribute("data-state");
239
+ if (state === "ready" || state === "error") return;
240
+ const cleanContent = stripCodeFences(segment.content || "");
241
+ if (isStreaming) {
242
+ streamHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
243
+ return;
244
+ }
245
+ const codeEl = element.querySelector('[data-role="code-pane"] code');
246
+ if (codeEl) codeEl.textContent = cleanContent;
247
+ if (state === "streaming") settle(element, segment, kind(element));
248
+ }
249
+ function settle(element, segment, kind2) {
250
+ const cfg = contextConfig(element);
251
+ const cleanContent = stripCodeFences(segment.content || "");
252
+ const wrapper = element.querySelector('[data-role="code-pane"]');
253
+ if (wrapper) {
254
+ void cfg.highlightCode(cleanContent, "js").then((html) => {
255
+ wrapper.innerHTML = html;
256
+ }).catch(() => {
257
+ });
258
+ }
259
+ const already = produced.get(segment.id);
260
+ if (already) {
261
+ swapToPreview(element, already, kind2);
262
+ return;
263
+ }
264
+ const resolve = renderOptions(cfg).onBinary;
265
+ if (!resolve) {
266
+ element.setAttribute("data-state", "source");
267
+ return;
268
+ }
269
+ element.setAttribute("data-state", "compiling");
270
+ let job = inFlight.get(segment.id);
271
+ if (!job) {
272
+ job = resolve({ ...segment, content: cleanContent });
273
+ inFlight.set(segment.id, job);
274
+ const done = () => {
275
+ inFlight.delete(segment.id);
276
+ };
277
+ job.then(done, done);
278
+ }
279
+ void job.then((bin) => {
280
+ remember(segment.id, bin);
281
+ if (element.isConnected) swapToPreview(element, bin, kind2);
282
+ }).catch((err) => {
283
+ if (element.isConnected) showError(element, err instanceof Error ? err.message : String(err));
284
+ });
285
+ }
286
+ function kind(element) {
287
+ return (element.getAttribute("data-artifact-type") || "").toLowerCase();
288
+ }
289
+ function previewMarkup(bin, kind2) {
290
+ return bin.previewHtml ? contextConfig().sanitizeHtml(bin.previewHtml) : `<div class="aparte-art-file__preview-empty">${escapeHtml(contextConfig().t("previewPending"))} ${escapeHtml(kind2)}</div>`;
291
+ }
292
+ function swapToPreview(element, bin, kind2) {
293
+ element.setAttribute("data-state", "ready");
294
+ const codePane = element.querySelector('[data-role="code-pane"]');
295
+ if (codePane) codePane.hidden = true;
296
+ const preview = element.querySelector('[data-role="preview-pane"]');
297
+ if (preview) {
298
+ preview.innerHTML = previewMarkup(bin, kind2);
299
+ preview.hidden = false;
300
+ }
301
+ const nameEl = element.querySelector('[data-role="file-name"]');
302
+ if (nameEl) nameEl.textContent = bin.filename;
303
+ const sub = element.querySelector('[data-role="file-sub"]');
304
+ if (sub) sub.textContent = `${formatBytes(byteLength(bin.buffer))} · ${kind2.toUpperCase()}`;
305
+ const dlBtn = element.querySelector('[data-action="download"]');
306
+ if (dlBtn) dlBtn.disabled = false;
307
+ }
308
+ function downloadBinary(bin) {
309
+ const blob = new Blob([bin.buffer], { type: bin.mime });
310
+ const url = URL.createObjectURL(blob);
311
+ const a = document.createElement("a");
312
+ a.href = url;
313
+ a.download = bin.filename;
314
+ a.style.display = "none";
315
+ document.body.appendChild(a);
316
+ a.click();
317
+ document.body.removeChild(a);
318
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
319
+ }
320
+ function byteLength(part) {
321
+ if (typeof part === "string") return new TextEncoder().encode(part).length;
322
+ if (part instanceof Blob) return part.size;
323
+ return part.byteLength;
324
+ }
325
+ function formatBytes(n) {
326
+ if (n < 1024) return `${n} B`;
327
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
328
+ return `${(n / (1024 * 1024)).toFixed(2)} MB`;
329
+ }
330
+ function showError(element, errorMsg) {
331
+ if (element.getAttribute("data-state") === "ready") return;
332
+ element.setAttribute("data-state", "error");
333
+ const cfg = contextConfig(element);
334
+ const sub = element.querySelector('[data-role="file-sub"]');
335
+ if (sub) sub.textContent = cfg.t("sandboxError");
336
+ const body = element.querySelector(".aparte-art-file__body");
337
+ if (body) {
338
+ const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
339
+ body.innerHTML = `
340
+ <div class="aparte-art-file__error">
341
+ <div class="aparte-art-file__error-title">${escapeHtml(cfg.t("sandboxError"))}</div>
342
+ <div class="aparte-output aparte-art-file__error-msg">${escapeHtml(short)}</div>
343
+ <div class="aparte-art-file__error-hint">${escapeHtml(cfg.t("sandboxErrorHint"))}</div>
344
+ </div>
345
+ `;
346
+ }
347
+ }
348
+ const artifactStyles = '/*\n * @aparte/plugin-artifacts — the artifact card and the artifact file preview.\n *\n * Injected once per document through the renderer\'s `getStyles()`, the seam core\n * keeps for a renderer that is not core\'s. It reads core\'s tokens (`--aparte-space-*`,\n * `--aparte-surface-*`, `--aparte-code-bg`…) and declares its own below, on the two\n * roots it draws, so a consumer overrides them the way they override any other:\n * `.aparte-segment-artifact-card { --aparte-art-paper-bg: … }`.\n */\n.aparte-segment-artifact-card,\n.aparte-segment-artifact-file {\n /* Paper. An artifact preview is a DOCUMENT shown inside the chat — a spreadsheet,\n a rendered page — so it stays light whatever the app theme is, the way a PDF\n viewer shows a white page in a dark editor. */\n --aparte-art-paper-bg: #fff;\n --aparte-art-paper-text: #1f2937;\n --aparte-art-paper-row-alt: #f9fafb;\n --aparte-art-paper-head-bg: #f3f4f6;\n --aparte-art-paper-head-text: #111827;\n --aparte-art-paper-border: rgba(0, 0, 0, 0.1);\n /* File-type tiles. Brand colours, so they are literals on purpose — but named,\n because an app with its own file-type palette has nowhere else to put it. The\n lettering is fixed like the tiles: a themed ink would go near-black in dark\n mode, on a dark green tile. */\n --aparte-art-file-icon-bg: linear-gradient(135deg, #1d6f42, #0f5132);\n --aparte-art-file-icon-color: #fff;\n --aparte-art-file-icon-bg-pdf: linear-gradient(135deg, #c0392b, #7d1f17);\n --aparte-art-file-icon-bg-docx: linear-gradient(135deg, #1e5288, #0f3060);\n --aparte-art-file-error-msg-bg: rgba(0, 0, 0, 0.04);\n /* Sizes. */\n --aparte-art-card-header-min-height: 36px;\n --aparte-art-card-pulse-size: 8px;\n --aparte-art-card-btn-size: 28px;\n --aparte-art-file-icon-size: 40px;\n --aparte-art-file-preview-padding: 20px;\n --aparte-art-file-error-padding-inline: 18px;\n}\n/* Not the paper — that stays light on purpose. This is a code block inside the\n error panel, which does follow the theme. */\n[data-aparte-theme="dark"] .aparte-segment-artifact-file {\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\n}\n/* Artifact card */\n/* The shell is `.aparte-card`; what stays here is what a card in a TRANSCRIPT needs\n and a card in general does not — the vertical rhythm between messages, and `font:\n inherit` so a segment never picks up a host\'s form-control font. */\n.aparte-segment-artifact-card,\n.aparte-segment-artifact-file {\n margin: var(--aparte-space-4) 0;\n font: inherit;\n}\n.aparte-art-card__header {\n display: flex; align-items: center; justify-content: space-between;\n padding: var(--aparte-space-4) var(--aparte-space-5);\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\n background: var(--aparte-surface-2);\n min-height: var(--aparte-art-card-header-min-height);\n}\n.aparte-art-card__title-block { display: flex; align-items: center; gap: var(--aparte-space-4); min-width: 0; }\n/* An outline `.aparte-badge`. What stays is the lettering: a language tag reads as a\n code marker, so it is upper-cased and tracked out — which a badge in general is not. */\n.aparte-art-card__kind {\n text-transform: uppercase;\n letter-spacing: 0.04em;\n --aparte-badge-font-size: var(--aparte-font-size-xs);\n --aparte-badge-radius: var(--aparte-radius-sm);\n}\n.aparte-art-card__title {\n font-size: var(--aparte-font-size-lg); font-weight: var(--aparte-font-weight-medium);\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n/* The geometry is `.aparte-dot`; a streaming card breathes on the accent rather than\n the status colour, and larger, because it marks a whole card and not a line. */\n.aparte-art-card__pulse {\n --aparte-status-dot-size: var(--aparte-art-card-pulse-size);\n --aparte-status-color: var(--aparte-accent);\n}\n.aparte-art-card__actions { display: flex; gap: var(--aparte-space-2); }\n.aparte-art-card__btn {\n /* Same as .aparte-action-btn: declare the size, let the recipe draw. */\n --aparte-btn-size: var(--aparte-art-card-btn-size);\n}\n.aparte-art-card__btn:hover:not(:disabled) {\n background: var(--aparte-surface-hover);\n border-color: var(--aparte-border);\n color: var(--aparte-text);\n}\n.aparte-art-card__btn:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; }\n.aparte-art-card__tabs {\n /* justify-content and the padding are DECLARED, not left to a default.\n Core is light DOM on purpose - no shadow root, no ::part(), any selector\n reaches in - and the corollary is that a component must state what its\n layout depends on, because an undeclared property has nothing to override\n a host rule with. A consuming page with a bare nav selector setting\n justify-content: space-between and padding-top (this library\'s own docs\n site had exactly that) otherwise pushes these two tabs to opposite ends of\n the card and pads the row out.\n\n flex-end, and Code first in the DOM. The card OPENS on Code - mounting the\n preview would execute model-authored code with no gesture (ratified\n decision #8) - and a selected tab sitting second reads backwards. Right,\n because the header above puts the artifact\'s identity on the left and its\n copy/download buttons on the right, so this keeps every control in one\n column. DOM order is also keyboard order, so the tab a reader lands on\n first is the one already showing. */\n display: flex; justify-content: flex-end; align-items: stretch; gap: var(--aparte-space-1);\n padding: var(--aparte-space-2) var(--aparte-space-4) 0;\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\n background: var(--aparte-surface-2);\n}\n/* The tabs are `.aparte-tabs--underline`, like the elicitation panel\'s steps — one\n tab in this library, not one per component. Only the row\'s own business stays. */\n.aparte-art-card__tabs button { font-size: var(--aparte-font-size-sm); }\n/* The card\'s heights, as variables with ONE owner each.\n They were four hardcoded numbers in the only part of this card that did not\n use a variable - everything else here already reads var(--aparte-code-bg) and\n friends - and two of them had to agree while a third contradicted a fourth:\n the code pane repeated the body\'s 600px, and the "press Preview" placeholder\n was 120px tall inside a body whose min-height said 80, so that minimum applied\n to nothing.\n The frame stays a FIXED height rather than an aspect ratio, which is what\n embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz\n takes a height parameter - because a frame with an opaque origin cannot be\n measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part\n that was missing: a fixed 480px should not own a phone screen.\n Each default lives in its read, as var(--x, literal), the way every other\n value in this file already does - not in a declaration block on top of the\n fallbacks, which would be two owners of one number again. It also means the\n docs\' CSS-variable generator finds them: its sweep looks for reads that\n carry a fallback, so a read without one is a public knob nobody documents. */\n.aparte-art-card__body {\n position: relative;\n /* The placeholder is the tallest thing this box can hold while empty, so it\n IS the minimum - one number instead of two that disagreed. */\n min-height: var(--aparte-artifact-pending-height, 120px);\n max-height: var(--aparte-artifact-body-max, 600px);\n overflow: hidden;\n}\n.aparte-art-card__pane { display: none; height: 100%; }\n.aparte-segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }\n.aparte-segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }\n.aparte-art-card__pane[data-pane="code"] {\n /* The body already caps this; repeating the number was the second owner. */\n max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;\n}\n.aparte-art-card__frame {\n display: block;\n width: 100%;\n height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));\n border: 0;\n background: var(--aparte-art-paper-bg);\n}\n.aparte-art-card__pending {\n display: flex; align-items: center; justify-content: center;\n height: var(--aparte-artifact-pending-height, 120px);\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-lg);\n font-style: italic;\n}\n/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */\n.aparte-art-file__card {\n display: flex; align-items: center; gap: var(--aparte-space-6);\n padding: var(--aparte-space-6) var(--aparte-space-7);\n background: var(--aparte-surface-2);\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\n}\n.aparte-art-file__body {\n position: relative;\n}\n.aparte-art-file__code-pane {\n max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;\n background: var(--aparte-code-bg);\n}\n.aparte-art-file__preview-pane {\n max-height: var(--aparte-artifact-file-preview-max, 460px);\n overflow: auto;\n background: var(--aparte-art-paper-bg);\n /* Preview is a document view — force light scheme regardless of\n the app theme, with a dark text colour so cells stay readable. */\n color: var(--aparte-art-paper-text);\n}\n.aparte-art-file__icon {\n width: var(--aparte-art-file-icon-size); height: var(--aparte-art-file-icon-size);\n border-radius: var(--aparte-radius-lg);\n display: flex; align-items: center; justify-content: center;\n background: var(--aparte-art-file-icon-bg);\n color: var(--aparte-art-file-icon-color);\n font-weight: var(--aparte-font-weight-bold);\n font-size: var(--aparte-font-size-sm);\n letter-spacing: 0.04em;\n flex-shrink: 0;\n}\n.aparte-art-file__icon[data-kind="pdf"] { background: var(--aparte-art-file-icon-bg-pdf); }\n.aparte-art-file__icon[data-kind="docx"] { background: var(--aparte-art-file-icon-bg-docx); }\n.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }\n.aparte-art-file__meta-name {\n font-weight: var(--aparte-font-weight-semibold); font-size: var(--aparte-font-size-base);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.aparte-art-file__meta-sub {\n font-size: var(--aparte-font-size-sm);\n color: var(--aparte-text-muted);\n}\n.aparte-art-file__actions { display: flex; gap: var(--aparte-space-3); flex-shrink: 0; }\n/* The chrome is `.aparte-btn`; every one of these is a `--primary --solid`, so the\n fill comes from the recipe. Its own measurements stay: a labelled button in a card\n is roomier than an icon in a row. */\n.aparte-art-file__btn {\n padding: var(--aparte-space-3) var(--aparte-space-5);\n font-size: var(--aparte-font-size-md);\n}\n/* NOTHING here repaints the button, and the comment above is why. These seven lines\n re-declared `background`, `color`, `border-color` and a hover the recipe already\n paints — a migration leftover that contradicted the sentence directly above it.\n\n Five of them were inert duplicates. The sixth was not: `color: var(--aparte-text-inverse)`\n overrode `--aparte-btn-on-intent`, which the recipe derives from the fill. Measured in a\n browser on the built stylesheet: 3.54:1 in the light theme against the recipe\'s 5.27 —\n an AA failure on a visible button label, in the default theme. It was also the last\n place in `styles/` forcing `--aparte-text-inverse` as ink on a coloured fill; badge and\n field had already stopped.\n\n The consumer cost was the sharper one: the one-attribute rebrand this library documents\n re-derives the ink on every other solid-primary button and, here alone, kept a token\n bound to core\'s own palette. `.aparte-art-file__btn:hover` went with them — at 0,2,0 it\n was always beaten by `.aparte-btn--solid:hover:not(:disabled)` at 0,3,0, so it never\n applied either. */\n/* Hide the download button until the sandbox has produced the buffer.\n Avoids confusing the user with a disabled-but-styled-primary button\n during streaming / compiling. */\n.aparte-segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {\n display: none;\n}\n.aparte-art-file__preview-pane th,\n.aparte-art-file__preview-pane td {\n border: var(--aparte-border-width) solid var(--aparte-art-paper-border);\n padding: var(--aparte-space-3) var(--aparte-space-5);\n text-align: left;\n white-space: nowrap;\n color: var(--aparte-art-paper-text);\n background: var(--aparte-art-paper-bg);\n}\n.aparte-art-file__preview-pane tr:nth-child(odd) td { background: var(--aparte-art-paper-row-alt); }\n.aparte-art-file__preview-pane tr:first-child td {\n background: var(--aparte-art-paper-head-bg);\n font-weight: var(--aparte-font-weight-semibold);\n position: sticky; top: 0;\n color: var(--aparte-art-paper-head-text);\n}\n.aparte-art-file__preview-empty {\n padding: var(--aparte-art-file-preview-padding); text-align: center;\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-lg); font-style: italic;\n}\n.aparte-segment-artifact-file[data-state="error"] .aparte-art-file__icon {\n background: var(--aparte-art-file-icon-bg-pdf);\n}\n.aparte-art-file__error {\n padding: var(--aparte-space-8) var(--aparte-art-file-error-padding-inline);\n background: var(--aparte-error-bg);\n border-top: var(--aparte-border-width) solid var(--aparte-error-border);\n color: var(--aparte-text);\n}\n.aparte-art-file__error-title {\n font-weight: var(--aparte-font-weight-semibold);\n font-size: var(--aparte-font-size-lg);\n margin-bottom: var(--aparte-space-3);\n color: var(--aparte-error-title);\n}\n/* A computed value shown back, which is `.aparte-output`. What stays is that this\n one is an ERROR — tinted text on its own quiet ground — and that a raw message has\n no spaces to wrap on, so it must be allowed to break mid-word. */\n.aparte-art-file__error-msg {\n display: block;\n padding: var(--aparte-space-3) var(--aparte-space-5);\n margin-bottom: var(--aparte-space-4);\n background: var(--aparte-art-file-error-msg-bg);\n color: var(--aparte-error-text);\n word-break: break-word;\n}\n.aparte-art-file__error-hint {\n font-size: var(--aparte-font-size-sm);\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n/* ── The card\'s code and table panes ─────────────────────────\n These three lived in core\'s prose sheet, styling this card\'s DOM from a sheet\n that knew nothing else about it. */\n.aparte-art-card__pane[data-pane="code"] pre {\n margin: 0; padding: var(--aparte-space-6);\n font-size: var(--aparte-font-size-md);\n background: var(--aparte-code-bg);\n}\n.aparte-art-file__code-pane pre {\n margin: 0; padding: var(--aparte-space-6);\n font-size: var(--aparte-font-size-md);\n font-family: var(--aparte-code-font-family);\n}\n.aparte-art-file__preview-pane table {\n border-collapse: collapse;\n width: 100%;\n font-size: var(--aparte-font-size-md);\n font-family: inherit;\n}\n';
349
+ const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
350
+ const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
351
+ function previewEnabled() {
352
+ return renderOptions(contextConfig()).preview !== false;
353
+ }
354
+ const artifactRenderer = {
355
+ type: ARTIFACT_SEGMENT_TYPE,
356
+ render: (segment) => {
357
+ const kind2 = (segment.artifactType || "unknown").toLowerCase();
358
+ if (BINARY_FILE_KINDS.has(kind2)) {
359
+ return renderBinaryFileArtifact(segment, kind2);
360
+ }
361
+ const title = segment.title?.trim() || labelForKind(kind2);
362
+ const displayLang = languageForKind(kind2);
363
+ const isStreaming = !!segment.isStreaming;
364
+ const previewable = PREVIEWABLE_KINDS.has(kind2) && previewEnabled();
365
+ const cfg = contextConfig();
366
+ const downloadLabel = cfg.t("download");
367
+ const previewLabel = cfg.t("preview");
368
+ const codeLabel = cfg.t("code");
369
+ const isBinary = BINARY_FILE_KINDS.has(kind2);
370
+ const cleanContent = stripCodeFences(segment.content || "");
371
+ const cardId = escapeAttr(segment.id);
372
+ return `
373
+ <div class="aparte-segment aparte-card aparte-segment-artifact-card"
374
+ data-segment-id="${escapeHtml(segment.id)}"
375
+ data-artifact-type="${escapeHtml(kind2)}"
376
+ data-streaming="${isStreaming ? "true" : "false"}"
377
+ data-tab="code"
378
+ data-previewable="${previewable ? "true" : "false"}"
379
+ data-binary="${isBinary ? "true" : "false"}">
380
+ <header class="aparte-art-card__header">
381
+ <div class="aparte-art-card__title-block">
382
+ <span class="aparte-badge aparte-badge--outline aparte-art-card__kind" data-kind="${escapeHtml(kind2)}">${escapeHtml(displayLang)}</span>
383
+ <span class="aparte-art-card__title">${escapeHtml(title)}</span>
384
+ ${isStreaming ? `<span class="aparte-dot aparte-art-card__pulse" role="img" aria-label="${escapeAttr(cfg.t("generating"))}"></span>` : ""}
385
+ </div>
386
+ <div class="aparte-art-card__actions">
387
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
388
+ ${contextConfig().getIcon("copy")}
389
+ </button>
390
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
391
+ ${contextConfig().getIcon("download")}
392
+ </button>
393
+ </div>
394
+ </header>
395
+ <nav class="aparte-tabs aparte-tabs--underline aparte-art-card__tabs" role="tablist">
396
+ <button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-code" aria-controls="aparte-art-${cardId}-pane-code" aria-selected="true" tabindex="0" data-tab-target="code">${escapeHtml(codeLabel)}</button>
397
+ ${previewable ? `<button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-preview" aria-controls="aparte-art-${cardId}-pane-preview" aria-selected="false" tabindex="-1" data-tab-target="preview" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
398
+ </nav>
399
+ <div class="aparte-art-card__body">
400
+ <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-code" aria-labelledby="aparte-art-${cardId}-tab-code" tabindex="0" data-pane="code">
401
+ <div class="aparte-code-content-wrapper">
402
+ <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
403
+ </div>
404
+ </div>
405
+ ${previewable ? `
406
+ <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-preview" aria-labelledby="aparte-art-${cardId}-tab-preview" tabindex="0" data-pane="preview">
407
+ <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
408
+ </div>
409
+ ` : ""}
410
+ </div>
411
+ </div>
412
+ `;
413
+ },
414
+ /**
415
+ * Every string this card shows: the copy button's tooltip, glyph and accessible
416
+ * name, the download button's title and label, and the two tab names. They were
417
+ * all hardcoded literals until they got locale keys — including the copy button's
418
+ * `aria-label`, which said "Copy" while its own `title` one attribute away already
419
+ * went through `t('copy')`, so a French reader got a French tooltip and an English
420
+ * announcement.
421
+ *
422
+ * Nothing here touches the tab state or the preview pane: a mounted iframe is
423
+ * running model-authored code, and re-rendering this card is exactly what the
424
+ * hook exists to avoid.
425
+ */
426
+ relabel: (element) => {
427
+ const cfg = contextConfig();
428
+ const copyBtn = element.querySelector('.aparte-art-card__btn[data-action="copy"]');
429
+ if (copyBtn) {
430
+ copyBtn.setAttribute("title", cfg.t("copy"));
431
+ copyBtn.setAttribute("aria-label", cfg.t("copy"));
432
+ copyBtn.innerHTML = cfg.getIcon("copy");
433
+ }
434
+ const dl = element.querySelector('.aparte-art-card__btn[data-action="download"]');
435
+ if (dl) {
436
+ const label = cfg.t("download");
437
+ dl.setAttribute("title", label);
438
+ dl.setAttribute("aria-label", label);
439
+ }
440
+ const previewTab = element.querySelector('[data-tab-target="preview"]');
441
+ if (previewTab) previewTab.textContent = cfg.t("preview");
442
+ const codeTab = element.querySelector('[data-tab-target="code"]');
443
+ if (codeTab) codeTab.textContent = cfg.t("code");
444
+ },
445
+ setup: (element, segment) => {
446
+ latestSegment.set(element, segment);
447
+ const kind2 = (segment.artifactType || "").toLowerCase();
448
+ if (BINARY_FILE_KINDS.has(kind2)) {
449
+ setupBinaryFileArtifact(element, segment, kind2);
450
+ return;
451
+ }
452
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
453
+ if (wrapper) {
454
+ const displayLang = languageForKind(kind2);
455
+ const cleanContent = stripCodeFences(segment.content || "");
456
+ void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
457
+ wrapper.innerHTML = html;
458
+ }).catch(() => {
459
+ });
460
+ }
461
+ const tabs = [...element.querySelectorAll("[data-tab-target]")];
462
+ const select = (btn, focus) => {
463
+ const target = btn.getAttribute("data-tab-target");
464
+ if (!target || btn.disabled) return;
465
+ if (target === "preview") mountPreviewFrame(element, segment);
466
+ element.setAttribute("data-tab", target);
467
+ for (const b of tabs) {
468
+ const on = b === btn;
469
+ b.setAttribute("aria-selected", on ? "true" : "false");
470
+ b.tabIndex = on ? 0 : -1;
471
+ }
472
+ if (focus) btn.focus();
473
+ };
474
+ for (const [i, btn] of tabs.entries()) {
475
+ btn.addEventListener("click", () => select(btn, false));
476
+ btn.addEventListener("keydown", (e2) => {
477
+ const step = e2.key === "ArrowRight" ? 1 : e2.key === "ArrowLeft" ? -1 : 0;
478
+ let next = -1;
479
+ if (step) next = (i + step + tabs.length) % tabs.length;
480
+ else if (e2.key === "Home") next = 0;
481
+ else if (e2.key === "End") next = tabs.length - 1;
482
+ if (next < 0) return;
483
+ e2.preventDefault();
484
+ for (let n = 0; n < tabs.length; n++) {
485
+ const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];
486
+ if (candidate && !candidate.disabled) {
487
+ select(candidate, true);
488
+ return;
489
+ }
490
+ }
491
+ });
492
+ }
493
+ const copyBtn = element.querySelector('[data-action="copy"]');
494
+ if (copyBtn) {
495
+ copyBtn.addEventListener("click", () => {
496
+ const code = stripCodeFences(segment.content || "");
497
+ void copyText(code).catch(() => {
498
+ });
499
+ const original = copyBtn.innerHTML;
500
+ copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
501
+ copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
502
+ setTimeout(() => {
503
+ copyBtn.innerHTML = original;
504
+ copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
505
+ }, 1500);
506
+ });
507
+ }
508
+ const dlBtn = element.querySelector('[data-action="download"]');
509
+ if (dlBtn) {
510
+ dlBtn.addEventListener("click", () => {
511
+ if (dlBtn.disabled) return;
512
+ downloadTextArtifact(latestSegment.get(element) ?? segment);
513
+ });
514
+ }
515
+ },
516
+ update: (element, segment) => {
517
+ const previous = latestSegment.get(element);
518
+ latestSegment.set(element, segment);
519
+ if (previous && previous.content !== segment.content) {
520
+ element.querySelector('.aparte-art-card__pane[data-pane="preview"] iframe')?.remove();
521
+ }
522
+ const isStreaming = !!segment.isStreaming;
523
+ const kind2 = (segment.artifactType || "").toLowerCase();
524
+ if (BINARY_FILE_KINDS.has(kind2)) {
525
+ updateBinaryFileArtifact(element, segment, isStreaming);
526
+ return;
527
+ }
528
+ const wasStreaming = element.getAttribute("data-streaming") === "true";
529
+ const cleanContent = stripCodeFences(segment.content || "");
530
+ if (isStreaming) {
531
+ const segId = element.getAttribute("data-segment-id") ?? segment.id;
532
+ streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind2), segId);
533
+ } else {
534
+ const codeEl = element.querySelector(".aparte-code-content-wrapper code");
535
+ if (codeEl) {
536
+ codeEl.textContent = cleanContent;
537
+ } else {
538
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
539
+ if (wrapper) {
540
+ const displayLang = languageForKind(kind2);
541
+ wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
542
+ }
543
+ }
544
+ }
545
+ if (wasStreaming && !isStreaming) {
546
+ element.setAttribute("data-streaming", "false");
547
+ element.querySelector(".aparte-art-card__pulse")?.remove();
548
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
549
+ if (wrapper) {
550
+ const displayLang = languageForKind(kind2);
551
+ void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
552
+ wrapper.innerHTML = html;
553
+ }).catch(() => {
554
+ });
555
+ }
556
+ element.querySelectorAll("button[disabled]").forEach((b) => {
557
+ b.disabled = false;
558
+ });
559
+ }
560
+ },
561
+ // The card's sheet, injected once per document by core — the seam a renderer that
562
+ // is not core's has onto the page. It is a real `.css` file, inlined at build.
563
+ getStyles: () => artifactStyles
564
+ };
565
+ function languageForKind(kind2) {
566
+ if (kind2 === "react") return "jsx";
567
+ if (kind2 === "markdown") return "md";
568
+ if (kind2 === "pdf" || kind2 === "xlsx" || kind2 === "docx") return "js";
569
+ return kind2 || "text";
570
+ }
571
+ function downloadTextArtifact(segment) {
572
+ const content = stripCodeFences(segment.content || "");
573
+ const kind2 = (segment.artifactType || "").toLowerCase();
574
+ const ext = {
575
+ react: "jsx",
576
+ html: "html",
577
+ svg: "svg",
578
+ js: "js",
579
+ css: "css",
580
+ json: "json",
581
+ markdown: "md",
582
+ csv: "csv",
583
+ text: "txt",
584
+ python: "py",
585
+ typescript: "ts",
586
+ bash: "sh",
587
+ sql: "sql"
588
+ }[kind2] ?? "txt";
589
+ const baseTitle = (segment.title ?? labelForKind(kind2)).trim();
590
+ const safeBase = slugifyForFilename(baseTitle) || "artifact";
591
+ const filename = `${safeBase}.${ext}`;
592
+ const mime = segment.mimeType || "text/plain";
593
+ const blob = new Blob([content], { type: mime });
594
+ const url = URL.createObjectURL(blob);
595
+ const a = document.createElement("a");
596
+ a.href = url;
597
+ a.download = filename;
598
+ a.style.display = "none";
599
+ document.body.appendChild(a);
600
+ a.click();
601
+ document.body.removeChild(a);
602
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
603
+ }
604
+ function slugifyForFilename(text) {
605
+ const lower = text.trim().toLowerCase();
606
+ let out = "";
607
+ let prevDash = false;
608
+ for (let i = 0; i < lower.length && out.length < 40; i++) {
609
+ const ch = lower[i];
610
+ const isAlnum = ch >= "a" && ch <= "z" || ch >= "0" && ch <= "9";
611
+ if (isAlnum) {
612
+ out += ch;
613
+ prevDash = false;
614
+ continue;
615
+ }
616
+ if (!prevDash && out.length > 0) {
617
+ out += "-";
618
+ prevDash = true;
619
+ }
620
+ }
621
+ if (out.endsWith("-")) out = out.slice(0, -1);
622
+ return out;
623
+ }
624
+ const latestSegment = /* @__PURE__ */ new WeakMap();
625
+ function mountPreviewFrame(element, fallback) {
626
+ const pane = element.querySelector('.aparte-art-card__pane[data-pane="preview"]');
627
+ if (!pane || pane.querySelector("iframe")) return;
628
+ const segment = latestSegment.get(element) ?? fallback;
629
+ const kind2 = (segment.artifactType || deriveArtifactKind(segment.mimeType ?? "", "text")).toLowerCase();
630
+ if (!PREVIEWABLE_KINDS.has(kind2) || !previewEnabled()) return;
631
+ const title = segment.title?.trim() || labelForKind(kind2);
632
+ const option = renderOptions(contextConfig(element)).preview;
633
+ const build = typeof option === "function" ? option : buildSafePreviewDocument;
634
+ const srcdoc = build(kind2, stripCodeFences(segment.content || ""), title);
635
+ pane.innerHTML = `<iframe class="aparte-art-card__frame" sandbox="allow-scripts" csp="${escapeAttr(PREVIEW_CSP)}" referrerpolicy="no-referrer" loading="lazy" title="${escapeAttr(title)}" srcdoc="${escapeAttr(srcdoc)}"></iframe>`;
636
+ }
637
+ function setupArtifacts(options = {}, config = aparteGlobalConfig) {
638
+ const tool = createArtifactTool(options);
639
+ setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary });
640
+ config.registerTool(tool, artifactHandler);
641
+ const toolRenderer = {
642
+ render: (segment) => artifactRenderer.render(artifactFromToolCall(segment)),
643
+ setup: (element, segment) => artifactRenderer.setup?.(element, artifactFromToolCall(segment)),
644
+ update: (element, segment) => artifactRenderer.update?.(element, artifactFromToolCall(segment)),
645
+ relabel: (element, segment) => artifactRenderer.relabel?.(element, artifactFromToolCall(segment)),
646
+ getStyles: () => artifactRenderer.getStyles?.() ?? ""
647
+ };
648
+ config.registerToolRenderer(tool.name, toolRenderer);
649
+ registerSegmentRenderer(artifactRenderer, config);
650
+ const tag = options.tag === void 0 ? ARTIFACT_TAG : options.tag;
651
+ if (tag) config.registerStreamBlock(artifactBlock(tag));
652
+ return () => {
653
+ config.unregisterTool(tool.name);
654
+ config.unregisterToolRenderer(tool.name);
655
+ unregisterSegmentRenderer(ARTIFACT_SEGMENT_TYPE, config);
656
+ if (tag) config.unregisterStreamBlock(tag);
657
+ clearRenderOptions(config);
658
+ };
659
+ }
660
+ export {
661
+ ARTIFACT_SEGMENT_TYPE,
662
+ e as ARTIFACT_SYSTEM_PROMPT,
663
+ ARTIFACT_TAG,
664
+ PREVIEW_CSP,
665
+ artifactBlock,
666
+ artifactFromToolCall,
667
+ artifactHandler,
668
+ artifactRenderer,
669
+ g as artifactSegment,
670
+ buildSafePreviewDocument,
671
+ createArtifactTool,
672
+ deriveArtifactKind,
673
+ setupArtifacts
674
+ };
675
+ //# sourceMappingURL=index.js.map