artifacty 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.
@@ -0,0 +1,1181 @@
1
+ import { EDITOR_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
2
+ import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
3
+
4
+ export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEFAULT_LOCALE, currentPath = "/" }) {
5
+ const view = viewContext(locale, currentPath);
6
+ const rows = artifacts
7
+ .map((artifact) => {
8
+ const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
9
+ const status = artifact.archivedAt ? statusBadge("archived") : "";
10
+ return `
11
+ <a class="artifact-row" href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">
12
+ <span class="row-main">
13
+ <strong>${escapeHtml(artifact.title)}</strong>
14
+ <span>${escapeHtml(artifact.id)}</span>
15
+ </span>
16
+ <span>${escapeHtml(artifact.sourceAgent)}</span>
17
+ ${typeBadge(artifact.artifactType || "document")}
18
+ ${formatBadge(artifact.format || "text")}
19
+ <span>v${artifact.latestVersion}</span>
20
+ <span class="tags">${status}${tags}</span>
21
+ </a>
22
+ `;
23
+ })
24
+ .join("");
25
+
26
+ return pageShell({
27
+ title: "Artifacty",
28
+ body: `
29
+ <header class="topbar">
30
+ <a class="brand" href="${view.href("/")}">
31
+ ${brandMark()}
32
+ <span class="brand-text">
33
+ <span class="brand-name">artifacty</span>
34
+ <span class="brand-tag">local artifact exchange · ${escapeHtml(baseUrl)}</span>
35
+ </span>
36
+ </a>
37
+ <nav>
38
+ <a href="${view.href("/new")}">${view.text("nav.new")}</a>
39
+ <a href="${view.href("/import")}">${view.text("nav.import")}</a>
40
+ <a href="/api/artifacts">${view.text("nav.api")}</a>
41
+ ${languageSwitcher(view)}
42
+ </nav>
43
+ </header>
44
+ <main class="dashboard">
45
+ <section class="toolbar">
46
+ <span>${view.text("dashboard.count", { count: artifacts.length })}</span>
47
+ </section>
48
+ <form class="filter-form" method="get" action="/">
49
+ ${localeInput(view.locale)}
50
+ <input name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("filter.search")}">
51
+ <input name="tag" value="${escapeAttribute(filters.tag || "")}" placeholder="${view.attr("filter.tag")}">
52
+ <input name="sourceAgent" value="${escapeAttribute(filters.sourceAgent || "")}" placeholder="${view.attr("filter.source")}">
53
+ <label class="checkbox-field"><input type="checkbox" name="includeArchived" value="true"${filters.includeArchived ? " checked" : ""}> ${view.text("filter.archived")}</label>
54
+ <button type="submit">${view.text("filter.submit")}</button>
55
+ <a href="${view.href("/")}">${view.text("filter.clear")}</a>
56
+ </form>
57
+ <section class="artifact-list">
58
+ ${rows || `<div class="empty">${view.text("dashboard.empty")}</div>`}
59
+ </section>
60
+ </main>
61
+ `,
62
+ locale: view.locale
63
+ });
64
+ }
65
+
66
+ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
67
+ const view = viewContext(locale, currentPath);
68
+ const isEdit = mode === "edit";
69
+ const title = isEdit ? view.raw("form.editTitle", { title: artifact.title }) : view.raw("form.newTitle");
70
+ const action = isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}/edit` : "/new";
71
+ const format = version?.format || "markdown";
72
+ const sourceAgent = artifact?.sourceAgent || "artifacty";
73
+ const artifactType = artifact?.artifactType || "document";
74
+ const tags = artifact?.tags?.join(", ") || "";
75
+ const body = content ?? "# New artifact";
76
+
77
+ return pageShell({
78
+ title,
79
+ body: `
80
+ <header class="topbar">
81
+ <div>
82
+ <h1>${escapeHtml(title)}</h1>
83
+ <p>${escapeHtml(baseUrl)}</p>
84
+ </div>
85
+ <nav>
86
+ <a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
87
+ ${languageSwitcher(view)}
88
+ </nav>
89
+ </header>
90
+ <main class="artifact-editor">
91
+ <form class="editor-form" method="post" action="${action}">
92
+ ${hiddenToken(authToken)}
93
+ ${localeInput(view.locale)}
94
+ <section class="editor-fields">
95
+ <label class="field">
96
+ <span>${view.text("form.title")}</span>
97
+ <input name="title" value="${escapeAttribute(artifact?.title || "")}" autocomplete="off" required>
98
+ </label>
99
+ <label class="field">
100
+ <span>${view.text("form.format")}</span>
101
+ ${formatSelect(format)}
102
+ </label>
103
+ <label class="field">
104
+ <span>${view.text("form.source")}</span>
105
+ <input name="sourceAgent" value="${escapeAttribute(sourceAgent)}" autocomplete="off">
106
+ </label>
107
+ <label class="field">
108
+ <span>${view.text("form.type")}</span>
109
+ ${artifactTypeSelect(artifactType)}
110
+ </label>
111
+ <label class="field">
112
+ <span>${view.text("form.tags")}</span>
113
+ <input name="tags" value="${escapeAttribute(tags)}" autocomplete="off">
114
+ </label>
115
+ </section>
116
+ <section class="field content-field">
117
+ <label for="artifact-content">${view.text("form.content")}</label>
118
+ <textarea id="artifact-content" name="content" data-artifacty-editor data-editor-format="${escapeAttribute(format)}" spellcheck="false" required>${escapeHtml(body)}</textarea>
119
+ </section>
120
+ <footer class="editor-actions">
121
+ <a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
122
+ <button type="submit">${isEdit ? view.text("form.saveVersion") : view.text("form.create")}</button>
123
+ </footer>
124
+ </form>
125
+ </main>
126
+ `,
127
+ head: editorHead(),
128
+ afterBody: editorScript(view.locale),
129
+ locale: view.locale
130
+ });
131
+ }
132
+
133
+ export function renderNewArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/new" }) {
134
+ return renderArtifactFormPage({ mode: "new", baseUrl, authToken, locale, currentPath });
135
+ }
136
+
137
+ export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/import" }) {
138
+ const view = viewContext(locale, currentPath);
139
+ const title = view.raw("form.importTitle");
140
+ return pageShell({
141
+ title,
142
+ body: `
143
+ <header class="topbar">
144
+ <div>
145
+ <h1>${escapeHtml(title)}</h1>
146
+ <p>${escapeHtml(baseUrl)}</p>
147
+ </div>
148
+ <nav>
149
+ <a href="${view.href("/")}">${view.text("nav.index")}</a>
150
+ ${languageSwitcher(view)}
151
+ </nav>
152
+ </header>
153
+ <main class="artifact-editor">
154
+ <form class="editor-form" method="post" action="/import">
155
+ ${hiddenToken(authToken)}
156
+ ${localeInput(view.locale)}
157
+ <section class="editor-fields">
158
+ <label class="field">
159
+ <span>${view.text("form.agent")}</span>
160
+ <select name="agent">
161
+ <option value="auto">${view.text("form.auto")}</option>
162
+ <option value="claude">Claude</option>
163
+ <option value="codex">Codex</option>
164
+ <option value="gemini">Gemini</option>
165
+ <option value="generic">Generic</option>
166
+ </select>
167
+ </label>
168
+ <label class="field">
169
+ <span>${view.text("form.title")}</span>
170
+ <input name="title" autocomplete="off">
171
+ </label>
172
+ <label class="field">
173
+ <span>${view.text("form.fileName")}</span>
174
+ <input name="fileName" autocomplete="off">
175
+ </label>
176
+ <label class="field">
177
+ <span>${view.text("form.tags")}</span>
178
+ <input name="tags" autocomplete="off">
179
+ </label>
180
+ </section>
181
+ <section class="field content-field">
182
+ <label for="import-content">${view.text("form.importContent")}</label>
183
+ <textarea id="import-content" name="content" data-artifacty-editor data-editor-format="auto" spellcheck="false" required></textarea>
184
+ </section>
185
+ <footer class="editor-actions">
186
+ <a href="${view.href("/")}">${view.text("nav.cancel")}</a>
187
+ <button type="submit">${view.text("form.importSubmit")}</button>
188
+ </footer>
189
+ </form>
190
+ </main>
191
+ `,
192
+ head: editorHead(),
193
+ afterBody: editorScript(view.locale),
194
+ locale: view.locale
195
+ });
196
+ }
197
+
198
+ export function renderArtifactPage({ artifact, version, content, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
199
+ const view = viewContext(locale, currentPath);
200
+ const versionLinks = artifact.versions
201
+ .map((item) => {
202
+ const active = item.version === version.version ? " active" : "";
203
+ return `<a class="version${active}" href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}?version=${item.version}`)}">v${item.version}</a>`;
204
+ })
205
+ .join("");
206
+
207
+ const rendered = renderContent(version.format, content);
208
+ const rawUrl = `/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`;
209
+ const archiveAction = artifact.archivedAt ? "restore" : "archive";
210
+ const archiveLabel = artifact.archivedAt ? view.text("artifact.restore") : view.text("artifact.archive");
211
+
212
+ return pageShell({
213
+ title: artifact.title,
214
+ body: `
215
+ <header class="topbar">
216
+ <div>
217
+ <h1>${escapeHtml(artifact.title)}</h1>
218
+ <p>${escapeHtml(artifact.id)} · ${escapeHtml(artifact.sourceAgent)} · ${escapeHtml(baseUrl)}</p>
219
+ </div>
220
+ <nav>
221
+ <a href="${view.href("/")}">${view.text("nav.index")}</a>
222
+ <a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/edit`)}">${view.text("nav.edit")}</a>
223
+ <a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/diff`)}">${view.text("nav.diff")}</a>
224
+ <a href="${view.href(rawUrl)}">${view.text("nav.raw")}</a>
225
+ ${languageSwitcher(view)}
226
+ </nav>
227
+ </header>
228
+ <main class="artifact-view">
229
+ <section class="meta-strip">
230
+ ${formatBadge(version.format)}
231
+ ${typeBadge(artifact.artifactType || "document")}
232
+ <span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
233
+ ${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}
234
+ <span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
235
+ <span>${escapeHtml(version.createdAt)}</span>
236
+ <span>${escapeHtml(version.sha256.slice(0, 12))}</span>
237
+ </section>
238
+ <form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/${archiveAction}">
239
+ ${hiddenToken(authToken)}
240
+ ${localeInput(view.locale)}
241
+ <button type="submit">${archiveLabel}</button>
242
+ </form>
243
+ <section class="version-strip">${versionLinks}</section>
244
+ ${rendered}
245
+ </main>
246
+ `,
247
+ afterBody: frameResizeScript(),
248
+ locale: view.locale
249
+ });
250
+ }
251
+
252
+ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent, toContent, diffRows, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
253
+ const view = viewContext(locale, currentPath);
254
+ const title = view.raw("diff.title", { title: artifact.title });
255
+ const versionOptions = artifact.versions
256
+ .map((item) => `<option value="${item.version}">v${item.version}</option>`)
257
+ .join("");
258
+
259
+ const rows = diffRows
260
+ .map((row) => `
261
+ <tr class="diff-${row.type}">
262
+ <td>${escapeHtml(row.beforeLine)}</td>
263
+ <td>${escapeHtml(row.afterLine)}</td>
264
+ <td><code>${escapeHtml(diffPrefix(row.type))}${escapeHtml(row.text)}</code></td>
265
+ </tr>
266
+ `)
267
+ .join("");
268
+
269
+ const preview = toVersion.format === "html"
270
+ ? `<section class="split-preview">
271
+ <div>${renderContent(fromVersion.format, fromContent)}</div>
272
+ <div>${renderContent(toVersion.format, toContent)}</div>
273
+ </section>`
274
+ : "";
275
+
276
+ return pageShell({
277
+ title,
278
+ body: `
279
+ <header class="topbar">
280
+ <div>
281
+ <h1>${escapeHtml(title)}</h1>
282
+ <p>${escapeHtml(artifact.id)} · ${escapeHtml(baseUrl)}</p>
283
+ </div>
284
+ <nav>
285
+ <a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a>
286
+ ${languageSwitcher(view)}
287
+ </nav>
288
+ </header>
289
+ <main class="artifact-view">
290
+ <form class="diff-form" method="get" action="/artifacts/${encodeURIComponent(artifact.id)}/diff">
291
+ ${localeInput(view.locale)}
292
+ <label class="field">
293
+ <span>${view.text("diff.from")}</span>
294
+ <select name="from">${versionOptions}</select>
295
+ </label>
296
+ <label class="field">
297
+ <span>${view.text("diff.to")}</span>
298
+ <select name="to">${versionOptions}</select>
299
+ </label>
300
+ <button type="submit">${view.text("diff.compare")}</button>
301
+ </form>
302
+ <script>
303
+ document.querySelector('select[name="from"]').value = ${JSON.stringify(String(fromVersion.version))};
304
+ document.querySelector('select[name="to"]').value = ${JSON.stringify(String(toVersion.version))};
305
+ </script>
306
+ <table class="diff-table">
307
+ <thead><tr><th>${view.text("diff.from")}</th><th>${view.text("diff.to")}</th><th>${view.text("diff.line")}</th></tr></thead>
308
+ <tbody>${rows}</tbody>
309
+ </table>
310
+ ${preview}
311
+ </main>
312
+ `,
313
+ locale: view.locale
314
+ });
315
+ }
316
+
317
+ export function renderContent(format, content) {
318
+ if (format === "html") {
319
+ return `<iframe class="artifact-frame" sandbox="allow-scripts allow-forms allow-popups" srcdoc="${escapeAttribute(htmlFrameContent(content))}"></iframe>`;
320
+ }
321
+
322
+ if (format === "markdown") {
323
+ return `<article class="artifact-doc">${markdownToHtml(content)}</article>`;
324
+ }
325
+
326
+ if (format === "json") {
327
+ return `<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>`;
328
+ }
329
+
330
+ return `<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
331
+ }
332
+
333
+ export function pageShell({ title, body, head = "", afterBody = "", locale = DEFAULT_LOCALE }) {
334
+ return `<!doctype html>
335
+ <html lang="${escapeAttribute(locale)}">
336
+ <head>
337
+ <meta charset="utf-8">
338
+ <meta name="viewport" content="width=device-width, initial-scale=1">
339
+ <title>${escapeHtml(title)}</title>
340
+ ${head}
341
+ <style>
342
+ :root {
343
+ color-scheme: light dark;
344
+ --bg: #eef1f5;
345
+ --grid: rgba(31, 58, 99, 0.05);
346
+ --panel: #ffffff;
347
+ --panel-2: #f3f6f9;
348
+ --text: #151b24;
349
+ --muted: #586273;
350
+ --faint: #828c9b;
351
+ --line: #e0e5ec;
352
+ --line-2: #cfd6df;
353
+ --accent: #0e7490;
354
+ --accent-2: #0b5e75;
355
+ --accent-ink: #ffffff;
356
+ --accent-soft: rgba(14, 116, 144, 0.10);
357
+ --ring: rgba(14, 116, 144, 0.32);
358
+ --code: #0e1320;
359
+ --code-text: #e6edf3;
360
+ --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
361
+ --mono: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
362
+ }
363
+ @media (prefers-color-scheme: dark) {
364
+ :root {
365
+ --bg: #0b0f14;
366
+ --grid: rgba(120, 160, 200, 0.06);
367
+ --panel: #12171f;
368
+ --panel-2: #171d27;
369
+ --text: #e6edf3;
370
+ --muted: #9aa6b3;
371
+ --faint: #6b7686;
372
+ --line: #222a35;
373
+ --line-2: #2d3744;
374
+ --accent: #22b8cf;
375
+ --accent-2: #3ccbdd;
376
+ --accent-ink: #04161c;
377
+ --accent-soft: rgba(34, 184, 207, 0.14);
378
+ --ring: rgba(34, 184, 207, 0.40);
379
+ --code: #080c12;
380
+ --code-text: #e6edf3;
381
+ }
382
+ }
383
+ * { box-sizing: border-box; }
384
+ body {
385
+ margin: 0;
386
+ min-height: 100vh;
387
+ background-color: var(--bg);
388
+ background-image:
389
+ linear-gradient(var(--grid) 1px, transparent 1px),
390
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
391
+ background-size: 28px 28px;
392
+ background-position: center top;
393
+ color: var(--text);
394
+ font-family: var(--sans);
395
+ line-height: 1.55;
396
+ letter-spacing: 0;
397
+ -webkit-font-smoothing: antialiased;
398
+ }
399
+ a { color: var(--accent); text-decoration: none; }
400
+ a:hover { color: var(--accent-2); text-decoration: underline; }
401
+ .topbar {
402
+ position: sticky;
403
+ top: 0;
404
+ z-index: 10;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: space-between;
408
+ gap: 24px;
409
+ padding: 15px 28px;
410
+ border-bottom: 1px solid var(--line);
411
+ background: color-mix(in srgb, var(--panel) 86%, transparent);
412
+ backdrop-filter: saturate(150%) blur(10px);
413
+ }
414
+ .topbar h1 {
415
+ margin: 0;
416
+ font-size: 20px;
417
+ line-height: 1.25;
418
+ letter-spacing: -0.01em;
419
+ }
420
+ .topbar p {
421
+ margin: 5px 0 0;
422
+ color: var(--muted);
423
+ font-family: var(--mono);
424
+ font-size: 12px;
425
+ overflow-wrap: anywhere;
426
+ }
427
+ .brand {
428
+ display: inline-flex;
429
+ align-items: center;
430
+ gap: 12px;
431
+ color: var(--text);
432
+ }
433
+ .brand:hover { text-decoration: none; }
434
+ .brand-mark { display: inline-flex; color: var(--accent); }
435
+ .brand-text { display: grid; gap: 2px; }
436
+ .brand-name {
437
+ font-family: var(--mono);
438
+ font-size: 19px;
439
+ font-weight: 600;
440
+ letter-spacing: -0.01em;
441
+ }
442
+ .brand-tag {
443
+ color: var(--muted);
444
+ font-family: var(--mono);
445
+ font-size: 12px;
446
+ overflow-wrap: anywhere;
447
+ }
448
+ .topbar nav {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 3px;
452
+ flex-wrap: wrap;
453
+ font-family: var(--mono);
454
+ font-size: 13px;
455
+ }
456
+ .topbar nav > a {
457
+ padding: 5px 10px;
458
+ border-radius: 7px;
459
+ color: var(--muted);
460
+ }
461
+ .topbar nav > a:hover {
462
+ background: var(--panel-2);
463
+ color: var(--text);
464
+ text-decoration: none;
465
+ }
466
+ .language-switcher {
467
+ display: inline-flex;
468
+ gap: 2px;
469
+ margin-left: 5px;
470
+ padding-left: 9px;
471
+ border-left: 1px solid var(--line);
472
+ }
473
+ .language-switcher a,
474
+ .language-switcher .active {
475
+ padding: 5px 8px;
476
+ border-radius: 7px;
477
+ }
478
+ .language-switcher .active {
479
+ color: var(--text);
480
+ font-weight: 700;
481
+ text-decoration: none;
482
+ cursor: default;
483
+ }
484
+ .dashboard,
485
+ .artifact-view,
486
+ .artifact-editor {
487
+ width: min(1180px, calc(100vw - 32px));
488
+ margin: 28px auto 72px;
489
+ }
490
+ .toolbar {
491
+ font-family: var(--mono);
492
+ font-size: 12px;
493
+ letter-spacing: 0.08em;
494
+ text-transform: uppercase;
495
+ color: var(--faint);
496
+ }
497
+ .toolbar,
498
+ .filter-form,
499
+ .diff-form,
500
+ .meta-strip,
501
+ .version-strip {
502
+ display: flex;
503
+ gap: 10px;
504
+ align-items: center;
505
+ flex-wrap: wrap;
506
+ margin-bottom: 12px;
507
+ color: var(--muted);
508
+ font-size: 13px;
509
+ }
510
+ .filter-form,
511
+ .diff-form {
512
+ display: grid;
513
+ grid-template-columns: minmax(180px, 1fr) 160px 160px auto auto auto;
514
+ gap: 10px;
515
+ margin-bottom: 12px;
516
+ align-items: end;
517
+ }
518
+ .checkbox-field {
519
+ display: inline-flex;
520
+ align-items: center;
521
+ gap: 6px;
522
+ min-height: 38px;
523
+ color: var(--muted);
524
+ font-size: 13px;
525
+ white-space: nowrap;
526
+ }
527
+ .checkbox-field input {
528
+ width: auto;
529
+ min-height: auto;
530
+ }
531
+ .inline-action {
532
+ margin-bottom: 12px;
533
+ }
534
+ .diff-form {
535
+ grid-template-columns: 160px 160px auto;
536
+ width: fit-content;
537
+ }
538
+ .filter-form > a,
539
+ .diff-form > a {
540
+ display: inline-flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ min-height: 38px;
544
+ padding: 0 14px;
545
+ border: 1px solid var(--line-2);
546
+ border-radius: 8px;
547
+ background: var(--panel);
548
+ color: var(--muted);
549
+ font-size: 13px;
550
+ }
551
+ .filter-form > a:hover,
552
+ .diff-form > a:hover {
553
+ background: var(--panel-2);
554
+ color: var(--text);
555
+ text-decoration: none;
556
+ }
557
+ .artifact-list {
558
+ border: 1px solid var(--line);
559
+ border-radius: 12px;
560
+ overflow: hidden;
561
+ background: var(--panel);
562
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
563
+ }
564
+ .editor-form {
565
+ display: grid;
566
+ gap: 16px;
567
+ }
568
+ .editor-fields {
569
+ display: grid;
570
+ grid-template-columns: minmax(220px, 1fr) 180px 180px minmax(160px, 240px);
571
+ gap: 12px;
572
+ align-items: end;
573
+ }
574
+ .field {
575
+ display: grid;
576
+ gap: 6px;
577
+ color: var(--muted);
578
+ font-size: 13px;
579
+ }
580
+ .field > span {
581
+ font-family: var(--mono);
582
+ font-size: 11.5px;
583
+ letter-spacing: 0.06em;
584
+ text-transform: uppercase;
585
+ color: var(--faint);
586
+ }
587
+ input,
588
+ select,
589
+ textarea {
590
+ width: 100%;
591
+ border: 1px solid var(--line-2);
592
+ border-radius: 8px;
593
+ background: var(--panel);
594
+ color: var(--text);
595
+ font: inherit;
596
+ letter-spacing: 0;
597
+ }
598
+ input::placeholder,
599
+ textarea::placeholder { color: var(--faint); }
600
+ input,
601
+ select {
602
+ min-height: 38px;
603
+ padding: 7px 10px;
604
+ }
605
+ textarea {
606
+ min-height: 52vh;
607
+ padding: 14px;
608
+ resize: vertical;
609
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
610
+ font-size: 13px;
611
+ line-height: 1.55;
612
+ white-space: pre;
613
+ overflow: auto;
614
+ }
615
+ .textarea-enhanced {
616
+ display: none;
617
+ }
618
+ .content-field {
619
+ gap: 10px;
620
+ }
621
+ .codemirror-shell {
622
+ min-height: 52vh;
623
+ }
624
+ .editor-toolbar {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: space-between;
628
+ gap: 10px;
629
+ min-height: 38px;
630
+ margin-top: 2px;
631
+ color: var(--muted);
632
+ font-size: 13px;
633
+ }
634
+ .editor-status {
635
+ overflow-wrap: anywhere;
636
+ }
637
+ .secondary-button {
638
+ border-color: var(--line-2);
639
+ background: var(--panel);
640
+ color: var(--text);
641
+ }
642
+ .secondary-button:hover {
643
+ background: var(--panel-2);
644
+ border-color: var(--accent);
645
+ }
646
+ .editor-preview {
647
+ min-height: 160px;
648
+ border: 1px solid var(--line);
649
+ border-radius: 8px;
650
+ background: var(--panel);
651
+ overflow: auto;
652
+ }
653
+ .editor-preview:empty {
654
+ display: none;
655
+ }
656
+ .editor-preview pre {
657
+ margin: 0;
658
+ padding: 16px;
659
+ white-space: pre-wrap;
660
+ overflow-wrap: anywhere;
661
+ }
662
+ .editor-preview-frame {
663
+ display: block;
664
+ width: 100%;
665
+ min-height: 38vh;
666
+ border: 0;
667
+ background: var(--panel);
668
+ }
669
+ input:focus,
670
+ select:focus,
671
+ textarea:focus {
672
+ outline: 3px solid var(--ring);
673
+ outline-offset: 0;
674
+ border-color: var(--accent);
675
+ }
676
+ .editor-actions {
677
+ display: flex;
678
+ align-items: center;
679
+ justify-content: flex-end;
680
+ gap: 12px;
681
+ }
682
+ button {
683
+ min-height: 38px;
684
+ border: 1px solid var(--accent);
685
+ border-radius: 8px;
686
+ padding: 7px 15px;
687
+ background: var(--accent);
688
+ color: var(--accent-ink);
689
+ font: inherit;
690
+ font-weight: 600;
691
+ cursor: pointer;
692
+ transition: background 0.15s ease, border-color 0.15s ease;
693
+ }
694
+ button:hover { background: var(--accent-2); border-color: var(--accent-2); }
695
+ .inline-action button {
696
+ min-height: 32px;
697
+ padding: 5px 12px;
698
+ font-family: var(--mono);
699
+ font-size: 12px;
700
+ font-weight: 500;
701
+ border-color: var(--line-2);
702
+ background: var(--panel);
703
+ color: var(--muted);
704
+ }
705
+ .inline-action button:hover {
706
+ background: var(--panel-2);
707
+ border-color: var(--accent);
708
+ color: var(--text);
709
+ }
710
+ .artifact-row {
711
+ display: grid;
712
+ grid-template-columns: minmax(220px, 1fr) 120px 120px 90px 64px minmax(120px, 220px);
713
+ gap: 16px;
714
+ align-items: center;
715
+ min-height: 64px;
716
+ padding: 14px 18px;
717
+ color: var(--text);
718
+ border-bottom: 1px solid var(--line);
719
+ box-shadow: inset 3px 0 0 transparent;
720
+ transition: background 0.12s ease, box-shadow 0.12s ease;
721
+ }
722
+ .artifact-row:last-child { border-bottom: 0; }
723
+ .artifact-row:hover {
724
+ background: var(--panel-2);
725
+ box-shadow: inset 3px 0 0 var(--accent);
726
+ text-decoration: none;
727
+ }
728
+ .row-main {
729
+ display: grid;
730
+ gap: 3px;
731
+ min-width: 0;
732
+ }
733
+ .row-main strong {
734
+ font-size: 14.5px;
735
+ letter-spacing: -0.005em;
736
+ }
737
+ .row-main strong,
738
+ .row-main span {
739
+ overflow: hidden;
740
+ text-overflow: ellipsis;
741
+ white-space: nowrap;
742
+ }
743
+ .row-main span {
744
+ font-family: var(--mono);
745
+ font-size: 12px;
746
+ color: var(--faint);
747
+ }
748
+ .artifact-row > span:not(.row-main):not(.tags):not(.badge) {
749
+ font-family: var(--mono);
750
+ font-size: 12.5px;
751
+ color: var(--muted);
752
+ }
753
+ .empty {
754
+ color: var(--muted);
755
+ font-size: 13px;
756
+ }
757
+ .tags {
758
+ display: flex;
759
+ gap: 6px;
760
+ flex-wrap: wrap;
761
+ }
762
+ .tag {
763
+ padding: 2px 8px;
764
+ border: 1px solid var(--line);
765
+ border-radius: 999px;
766
+ background: var(--panel-2);
767
+ color: var(--muted);
768
+ font-family: var(--mono);
769
+ font-size: 11.5px;
770
+ }
771
+ .badge {
772
+ display: inline-flex;
773
+ align-items: center;
774
+ padding: 3px 9px;
775
+ border-radius: 999px;
776
+ border: 1px solid color-mix(in srgb, var(--bh, var(--muted)) 38%, var(--line));
777
+ background: color-mix(in srgb, var(--bh, var(--muted)) 13%, transparent);
778
+ color: color-mix(in srgb, var(--bh, var(--muted)) 62%, var(--text));
779
+ font-family: var(--mono);
780
+ font-size: 11.5px;
781
+ line-height: 1.45;
782
+ white-space: nowrap;
783
+ }
784
+ .badge.t-document { --bh: #5b6b7f; }
785
+ .badge.t-html-page { --bh: #e0795b; }
786
+ .badge.t-handoff { --bh: #a855f7; }
787
+ .badge.t-code-review { --bh: #2dab76; }
788
+ .badge.t-test-report { --bh: #d99a2b; }
789
+ .badge.t-dashboard { --bh: #3b82f6; }
790
+ .badge.t-design-option { --bh: #ec4899; }
791
+ .badge.t-diff-walkthrough { --bh: #14b8a6; }
792
+ .badge.t-bundle { --bh: #8a8d98; }
793
+ .badge.t-asset { --bh: #f59e0b; }
794
+ .badge.t-unknown { --bh: #94a3b8; }
795
+ .badge.f-html { --bh: #e0795b; }
796
+ .badge.f-markdown { --bh: #3b82f6; }
797
+ .badge.f-text { --bh: #5b6b7f; }
798
+ .badge.f-json { --bh: #16a34a; }
799
+ .badge.s-archived { --bh: #94a3b8; }
800
+ .empty {
801
+ padding: 28px;
802
+ }
803
+ .meta-strip {
804
+ padding: 0;
805
+ gap: 8px;
806
+ }
807
+ .meta-strip > span:not(.badge) {
808
+ padding: 4px 10px;
809
+ border: 1px solid var(--line);
810
+ border-radius: 999px;
811
+ background: var(--panel);
812
+ font-family: var(--mono);
813
+ font-size: 12px;
814
+ color: var(--muted);
815
+ }
816
+ .version-strip {
817
+ padding: 8px 10px;
818
+ border: 1px solid var(--line);
819
+ border-radius: 10px;
820
+ background: var(--panel);
821
+ }
822
+ .version {
823
+ display: inline-flex;
824
+ min-width: 36px;
825
+ min-height: 28px;
826
+ align-items: center;
827
+ justify-content: center;
828
+ padding: 3px 9px;
829
+ border: 1px solid var(--line-2);
830
+ border-radius: 7px;
831
+ background: var(--panel-2);
832
+ color: var(--muted);
833
+ font-family: var(--mono);
834
+ font-size: 12.5px;
835
+ }
836
+ .version:hover {
837
+ border-color: var(--accent);
838
+ color: var(--text);
839
+ text-decoration: none;
840
+ }
841
+ .version.active {
842
+ border-color: var(--accent);
843
+ background: var(--accent-soft);
844
+ color: var(--accent);
845
+ font-weight: 600;
846
+ }
847
+ .artifact-frame {
848
+ display: block;
849
+ width: 100%;
850
+ min-height: 320px;
851
+ border: 1px solid var(--line);
852
+ border-radius: 12px;
853
+ background: var(--panel);
854
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
855
+ }
856
+ .split-preview {
857
+ display: grid;
858
+ grid-template-columns: 1fr 1fr;
859
+ gap: 12px;
860
+ margin-top: 12px;
861
+ }
862
+ .split-preview .artifact-frame {
863
+ min-height: 45vh;
864
+ }
865
+ .diff-table {
866
+ width: 100%;
867
+ border-collapse: collapse;
868
+ border: 1px solid var(--line);
869
+ border-radius: 8px;
870
+ overflow: hidden;
871
+ background: var(--panel);
872
+ font-size: 13px;
873
+ }
874
+ .diff-table th,
875
+ .diff-table td {
876
+ padding: 5px 8px;
877
+ border-bottom: 1px solid var(--line);
878
+ vertical-align: top;
879
+ }
880
+ .diff-table th {
881
+ color: var(--muted);
882
+ text-align: left;
883
+ font-family: var(--mono);
884
+ background: var(--panel-2);
885
+ }
886
+ .diff-table td:first-child,
887
+ .diff-table td:nth-child(2) {
888
+ width: 62px;
889
+ color: var(--muted);
890
+ text-align: right;
891
+ user-select: none;
892
+ }
893
+ .diff-added { background: color-mix(in srgb, #2ea043 16%, transparent); }
894
+ .diff-removed { background: color-mix(in srgb, #f85149 15%, transparent); }
895
+ .diff-table code {
896
+ white-space: pre-wrap;
897
+ overflow-wrap: anywhere;
898
+ }
899
+ .artifact-doc,
900
+ .artifact-code {
901
+ margin: 0;
902
+ border: 1px solid var(--line);
903
+ border-radius: 12px;
904
+ background: var(--panel);
905
+ }
906
+ .artifact-doc {
907
+ padding: 32px;
908
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
909
+ }
910
+ .artifact-doc h1,
911
+ .artifact-doc h2,
912
+ .artifact-doc h3 {
913
+ line-height: 1.25;
914
+ letter-spacing: -0.01em;
915
+ }
916
+ .artifact-doc :not(pre) > code {
917
+ padding: 1px 6px;
918
+ border: 1px solid var(--line);
919
+ border-radius: 5px;
920
+ background: var(--panel-2);
921
+ font-size: 0.88em;
922
+ }
923
+ .artifact-code {
924
+ padding: 22px;
925
+ overflow: auto;
926
+ color: var(--code-text);
927
+ background: var(--code);
928
+ min-height: 50vh;
929
+ white-space: pre-wrap;
930
+ overflow-wrap: anywhere;
931
+ }
932
+ code,
933
+ pre {
934
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
935
+ font-size: 13px;
936
+ }
937
+ @media (max-width: 760px) {
938
+ .topbar {
939
+ align-items: flex-start;
940
+ flex-direction: column;
941
+ padding: 20px;
942
+ }
943
+ .artifact-row {
944
+ grid-template-columns: 1fr;
945
+ gap: 6px;
946
+ }
947
+ .editor-fields {
948
+ grid-template-columns: 1fr;
949
+ }
950
+ .filter-form,
951
+ .diff-form,
952
+ .split-preview {
953
+ grid-template-columns: 1fr;
954
+ width: 100%;
955
+ }
956
+ .row-main strong,
957
+ .row-main span {
958
+ white-space: normal;
959
+ }
960
+ .artifact-doc {
961
+ padding: 18px;
962
+ }
963
+ }
964
+ </style>
965
+ </head>
966
+ <body>
967
+ ${body}
968
+ ${afterBody}
969
+ </body>
970
+ </html>`;
971
+ }
972
+
973
+ function formatSelect(selected) {
974
+ return `<select name="format">
975
+ ${["markdown", "html", "text", "json"].map((format) => {
976
+ const label = format[0].toUpperCase() + format.slice(1);
977
+ return `<option value="${format}"${format === selected ? " selected" : ""}>${label}</option>`;
978
+ }).join("")}
979
+ </select>`;
980
+ }
981
+
982
+ function artifactTypeSelect(selected) {
983
+ const types = ["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"];
984
+ return `<select name="artifactType">
985
+ ${types.map((type) => `<option value="${type}"${type === selected ? " selected" : ""}>${escapeHtml(type)}</option>`).join("")}
986
+ </select>`;
987
+ }
988
+
989
+ function diffPrefix(type) {
990
+ if (type === "added") {
991
+ return "+ ";
992
+ }
993
+ if (type === "removed") {
994
+ return "- ";
995
+ }
996
+ return " ";
997
+ }
998
+
999
+ function hiddenToken(authToken) {
1000
+ return authToken ? `<input type="hidden" name="_token" value="${escapeAttribute(authToken)}">` : "";
1001
+ }
1002
+
1003
+ function editorHead() {
1004
+ return `<script type="importmap">${editorImportMapJson()}</script>`;
1005
+ }
1006
+
1007
+ function editorScript(locale) {
1008
+ const config = JSON.stringify(editorMessages(locale)).replaceAll("<", "\\u003c");
1009
+ return `<script>window.ARTIFACTY_I18N=${config};</script><script type="module" src="${EDITOR_CLIENT_PATH}"></script>`;
1010
+ }
1011
+
1012
+ function viewContext(locale, currentPath) {
1013
+ const i18n = createI18n(locale);
1014
+ return {
1015
+ locale: i18n.locale,
1016
+ currentPath,
1017
+ href(href) {
1018
+ return localizedHref(href, i18n.locale);
1019
+ },
1020
+ raw(key, params = {}) {
1021
+ return i18n.t(key, params);
1022
+ },
1023
+ text(key, params = {}) {
1024
+ return escapeHtml(i18n.t(key, params));
1025
+ },
1026
+ attr(key, params = {}) {
1027
+ return escapeAttribute(i18n.t(key, params));
1028
+ }
1029
+ };
1030
+ }
1031
+
1032
+ function localeInput(locale) {
1033
+ return `<input type="hidden" name="lang" value="${escapeAttribute(locale)}">`;
1034
+ }
1035
+
1036
+ function brandMark() {
1037
+ return `<span class="brand-mark" aria-hidden="true"><svg width="26" height="26" viewBox="0 0 24 24" fill="none">
1038
+ <path d="M12 2.5 20.5 7v10L12 21.5 3.5 17V7z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1039
+ <path d="M8 16.5 12 7.5l4 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
1040
+ <path d="M9.7 13.3h4.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
1041
+ </svg></span>`;
1042
+ }
1043
+
1044
+ function typeBadge(value) {
1045
+ const type = String(value || "document");
1046
+ const slug = type.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
1047
+ return `<span class="badge t-${escapeAttribute(slug)}">${escapeHtml(type)}</span>`;
1048
+ }
1049
+
1050
+ function formatBadge(value) {
1051
+ const format = String(value || "text");
1052
+ const slug = format.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
1053
+ return `<span class="badge f-${escapeAttribute(slug)}">${escapeHtml(format)}</span>`;
1054
+ }
1055
+
1056
+ function statusBadge(label) {
1057
+ return `<span class="badge s-archived">${label}</span>`;
1058
+ }
1059
+
1060
+ function htmlFrameContent(content) {
1061
+ const reporter = `<script>(function(){function report(){var doc=document.documentElement;var body=document.body;var height=Math.max(doc.scrollHeight,doc.offsetHeight,body?body.scrollHeight:0,body?body.offsetHeight:0);parent.postMessage({__artifactyHeight:height},"*");}window.addEventListener("load",report);window.addEventListener("resize",report);if(window.ResizeObserver){try{new ResizeObserver(report).observe(document.documentElement);}catch(error){}}var ticks=0;var timer=setInterval(function(){report();if(++ticks>8){clearInterval(timer);}},250);report();})();</script>`;
1062
+ if (content.includes("</body>")) {
1063
+ return content.replace("</body>", `${reporter}</body>`);
1064
+ }
1065
+ return `${content}${reporter}`;
1066
+ }
1067
+
1068
+ function frameResizeScript() {
1069
+ return `<script>(function(){var frame=document.querySelector(".artifact-frame");if(!frame){return;}window.addEventListener("message",function(event){if(event.source!==frame.contentWindow){return;}var data=event.data;if(!data||typeof data.__artifactyHeight!=="number"){return;}var height=Math.min(Math.max(Math.ceil(data.__artifactyHeight),200),200000);frame.style.height=height+"px";});})();</script>`;
1070
+ }
1071
+
1072
+ function languageSwitcher(view) {
1073
+ const english = view.locale === "en"
1074
+ ? `<span class="active">${view.text("language.english")}</span>`
1075
+ : `<a href="${switchLocaleHref(view.currentPath, "en")}">${view.text("language.english")}</a>`;
1076
+ const korean = view.locale === "ko"
1077
+ ? `<span class="active">${view.text("language.korean")}</span>`
1078
+ : `<a href="${switchLocaleHref(view.currentPath, "ko")}">${view.text("language.korean")}</a>`;
1079
+ return `<span class="language-switcher">${english}${korean}</span>`;
1080
+ }
1081
+
1082
+ export function escapeHtml(value) {
1083
+ return String(value)
1084
+ .replaceAll("&", "&amp;")
1085
+ .replaceAll("<", "&lt;")
1086
+ .replaceAll(">", "&gt;")
1087
+ .replaceAll('"', "&quot;")
1088
+ .replaceAll("'", "&#39;");
1089
+ }
1090
+
1091
+ export function escapeAttribute(value) {
1092
+ return escapeHtml(value).replaceAll("\n", "&#10;");
1093
+ }
1094
+
1095
+ function markdownToHtml(markdown) {
1096
+ const lines = markdown.split(/\r?\n/);
1097
+ const html = [];
1098
+ let inCode = false;
1099
+ let inList = false;
1100
+ let paragraph = [];
1101
+
1102
+ const flushParagraph = () => {
1103
+ if (paragraph.length > 0) {
1104
+ html.push(`<p>${inlineMarkdown(paragraph.join(" "))}</p>`);
1105
+ paragraph = [];
1106
+ }
1107
+ };
1108
+ const closeList = () => {
1109
+ if (inList) {
1110
+ html.push("</ul>");
1111
+ inList = false;
1112
+ }
1113
+ };
1114
+
1115
+ for (const line of lines) {
1116
+ if (line.startsWith("```")) {
1117
+ flushParagraph();
1118
+ closeList();
1119
+ if (inCode) {
1120
+ html.push("</code></pre>");
1121
+ inCode = false;
1122
+ } else {
1123
+ html.push("<pre><code>");
1124
+ inCode = true;
1125
+ }
1126
+ continue;
1127
+ }
1128
+
1129
+ if (inCode) {
1130
+ html.push(`${escapeHtml(line)}\n`);
1131
+ continue;
1132
+ }
1133
+
1134
+ if (line.trim() === "") {
1135
+ flushParagraph();
1136
+ closeList();
1137
+ continue;
1138
+ }
1139
+
1140
+ const heading = /^(#{1,3})\s+(.+)$/.exec(line);
1141
+ if (heading) {
1142
+ flushParagraph();
1143
+ closeList();
1144
+ const level = heading[1].length;
1145
+ html.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
1146
+ continue;
1147
+ }
1148
+
1149
+ const listItem = /^[-*]\s+(.+)$/.exec(line);
1150
+ if (listItem) {
1151
+ flushParagraph();
1152
+ if (!inList) {
1153
+ html.push("<ul>");
1154
+ inList = true;
1155
+ }
1156
+ html.push(`<li>${inlineMarkdown(listItem[1])}</li>`);
1157
+ continue;
1158
+ }
1159
+
1160
+ paragraph.push(line.trim());
1161
+ }
1162
+
1163
+ flushParagraph();
1164
+ closeList();
1165
+ if (inCode) {
1166
+ html.push("</code></pre>");
1167
+ }
1168
+ return html.join("\n");
1169
+ }
1170
+
1171
+ function inlineMarkdown(value) {
1172
+ return escapeHtml(value).replace(/`([^`]+)`/g, "<code>$1</code>");
1173
+ }
1174
+
1175
+ function formatJson(content) {
1176
+ try {
1177
+ return JSON.stringify(JSON.parse(content), null, 2);
1178
+ } catch {
1179
+ return content;
1180
+ }
1181
+ }