artifacty 0.13.1 → 0.15.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/src/lib/render.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
2
3
  import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
3
4
  import { ARTIFACT_FORMATS, ARTIFACT_TYPES, REVIEW_STATUSES, SAVED_VIEW_FILTER_KEYS, VISIBILITY_VALUES } from "./storage.js";
@@ -12,62 +13,62 @@ import {
12
13
  import { isNotebookObject } from "./converters.js";
13
14
  import { GROUP_BY_VALUES, groupKeyFor } from "./listing.js";
14
15
 
16
+ const APP_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
17
+
15
18
  export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/", user = null, savedViews = [], embeddingsAvailable = false }) {
16
19
  const view = viewContext(locale, currentPath);
17
20
  const total = pagination?.total ?? artifacts.length;
18
21
  const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
19
22
  const end = artifacts.length ? (pagination?.offset ?? 0) + artifacts.length : 0;
20
- const searchBackend = pagination?.search?.backend;
21
23
  const pager = renderDashboardPager({ pagination, filters, view });
22
24
  const rowsHtml = renderArtifactRows(artifacts, filters.groupBy, view);
23
- const sidebar = renderSavedViewsSidebar({ savedViews, filters, view });
25
+ const activeFilters = [
26
+ ["tag", "filter.tag"], ["sourceAgent", "filter.source"], ["artifactType", "filter.artifactType"],
27
+ ["publisher", "filter.publisher"], ["createdAfter", "filter.createdAfter"], ["createdBefore", "filter.createdBefore"],
28
+ ["reviewStatus", "filter.reviewStatus"], ["mode", "filter.mode"], ["groupBy", "filter.groupBy"],
29
+ ["relatedTo", "filter.relatedTo"], ["relation", "filter.relation"], ["includeArchived", "filter.archived"]
30
+ ].filter(([key]) => filters[key]);
31
+ const field = (key, label, type = "text") => `<label class="dashboard-field"><span>${view.text(label)}</span><input type="${type}" name="${key}" value="${escapeAttribute(filters[key] || "")}"></label>`;
32
+ const selectField = (label, select) => `<label class="dashboard-field"><span>${view.text(label)}</span>${select}</label>`;
24
33
 
25
34
  return pageShell({
26
35
  title: "Artifacty",
27
36
  body: `
28
- <header class="topbar">
29
- <a class="brand" href="${view.href("/")}">
30
- ${brandMark()}
31
- <span class="brand-text">
32
- <span class="brand-name">artifacty</span>
33
- <span class="brand-tag">local artifact exchange · ${escapeHtml(baseUrl)}</span>
34
- </span>
35
- </a>
36
- <nav>
37
- <a href="${view.href("/new")}">${view.text("nav.new")}</a>
38
- <a href="${view.href("/import")}">${view.text("nav.import")}</a>
39
- <a href="/api/artifacts">${view.text("nav.api")}</a>
40
- ${authNav(user, view)}
41
- ${languageSwitcher(view)}
42
- </nav>
43
- </header>
37
+ ${workspaceHeader(user, view)}
44
38
  <div class="dashboard-layout">
45
- ${sidebar}
46
- <main class="dashboard">
47
- <section class="toolbar">
48
- <span>${view.text("dashboard.range", { start, end, total })}</span>
49
- ${searchBackend ? `<span>${view.text("dashboard.searchBackend", { backend: searchBackend })}</span>` : ""}
50
- </section>
39
+ <main class="dashboard" id="main-content">
40
+ <div class="dashboard-heading"><h1>${view.text("nav.artifacts")}</h1><nav><a class="button-link" href="${view.href("/new")}">${view.text("nav.new")}</a><a href="${view.href("/import")}">${view.text("nav.import")}</a></nav></div>
51
41
  <form class="filter-form" method="get" action="/">
52
42
  ${localeInput(view.locale)}
53
43
  ${filters.limit ? `<input type="hidden" name="limit" value="${escapeAttribute(filters.limit)}">` : ""}
54
- <input name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("filter.search")}">
55
- <input name="tag" value="${escapeAttribute(filters.tag || "")}" placeholder="${view.attr("filter.tag")}">
56
- <input name="sourceAgent" value="${escapeAttribute(filters.sourceAgent || "")}" placeholder="${view.attr("filter.source")}">
57
- ${filterArtifactTypeSelect(filters.artifactType, view)}
58
- <input name="publisher" value="${escapeAttribute(filters.publisher || "")}" placeholder="${view.attr("filter.publisher")}">
59
- <input type="date" name="createdAfter" value="${escapeAttribute(filters.createdAfter || "")}" title="${view.attr("filter.createdAfter")}">
60
- <input type="date" name="createdBefore" value="${escapeAttribute(filters.createdBefore || "")}" title="${view.attr("filter.createdBefore")}">
61
- ${filterReviewStatusSelect(filters.reviewStatus, view)}
62
- ${embeddingsAvailable ? filterModeSelect(filters.mode, view) : ""}
63
- ${filterGroupBySelect(filters.groupBy, view)}
64
- <label class="checkbox-field"><input type="checkbox" name="includeArchived" value="true"${filters.includeArchived ? " checked" : ""}> ${view.text("filter.archived")}</label>
65
- <button type="submit">${view.text("filter.submit")}</button>
66
- <a href="${view.href("/")}">${view.text("filter.clear")}</a>
44
+ ${!embeddingsAvailable && filters.mode ? `<input type="hidden" name="mode" value="${escapeAttribute(filters.mode)}">` : ""}
45
+ <div class="dashboard-search-row">
46
+ <label class="dashboard-search"><span class="dashboard-sr-only">${view.text("filter.search")}</span><input type="search" name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("dashboard.searchPlaceholder")}"></label>
47
+ <button type="submit">${view.text("filter.search")}</button>
48
+ <a class="dashboard-clear" href="${view.href("/")}">${view.text("filter.clear")}</a>
49
+ </div>
50
+ <details class="dashboard-advanced">
51
+ <summary>${view.text("dashboard.advanced")}${activeFilters.length ? `<span class="dashboard-filter-count">${view.text("dashboard.activeFilters", { count: activeFilters.length })}</span>` : ""}</summary>
52
+ <div class="dashboard-filter-grid">
53
+ ${field("tag", "filter.tag")}${field("sourceAgent", "filter.source")}
54
+ ${selectField("filter.artifactType", filterArtifactTypeSelect(filters.artifactType, view))}
55
+ ${field("publisher", "filter.publisher")}
56
+ ${field("createdAfter", "filter.createdAfter", "date")}${field("createdBefore", "filter.createdBefore", "date")}
57
+ ${selectField("filter.reviewStatus", filterReviewStatusSelect(filters.reviewStatus, view))}
58
+ ${embeddingsAvailable ? selectField("filter.mode", filterModeSelect(filters.mode, view)) : ""}
59
+ ${selectField("filter.groupBy", filterGroupBySelect(filters.groupBy, view))}
60
+ ${field("relatedTo", "filter.relatedTo")}${field("relation", "filter.relation")}
61
+ <label class="checkbox-field"><input type="checkbox" name="includeArchived" value="true"${filters.includeArchived ? " checked" : ""}> ${view.text("filter.archived")}</label>
62
+ <div class="dashboard-filter-actions"><button type="submit">${view.text("filter.submit")}</button></div>
63
+ </div>
64
+ </details>
65
+ ${activeFilters.length ? `<p class="dashboard-active-filters">${activeFilters.map(([key, label]) => `<span>${view.text(label)}${key === "includeArchived" ? "" : `: ${escapeHtml(filters[key])}`}</span>`).join("")}</p>` : ""}
67
66
  </form>
68
- <section class="artifact-list">
69
- ${rowsHtml || `<div class="empty">${view.text("dashboard.empty")}</div>`}
67
+ ${renderSavedViewsSidebar({ savedViews, filters, view })}
68
+ <section class="toolbar dashboard-results">
69
+ <span>${view.text("dashboard.range", { start, end, total })}</span>
70
70
  </section>
71
+ <section class="artifact-list">${rowsHtml || `<div class="empty">${view.text("dashboard.empty")}</div>`}</section>
71
72
  ${pager}
72
73
  </main>
73
74
  </div>
@@ -88,14 +89,11 @@ function renderArtifactRow(artifact, view) {
88
89
  <a class="artifact-row" href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">
89
90
  <span class="row-main">
90
91
  <strong>${escapeHtml(artifact.title)}</strong>
91
- <span>${escapeHtml(artifact.id)}</span>
92
+ <span class="dashboard-sr-only">${escapeHtml(artifact.id)}</span>
92
93
  ${snippet}
93
94
  </span>
94
- <span>${escapeHtml(artifact.sourceAgent)}</span>
95
- <span class="publisher-cell" title="${escapeAttribute(publisherTitle(artifact, view))}">${escapeHtml(publisher)}</span>
96
- ${typeBadge(artifact.artifactType || "document")}
97
- ${formatBadge(artifact.format || "text")}
98
- <span>v${artifact.latestVersion}</span>
95
+ <span class="row-agent"><span>${escapeHtml(artifact.sourceAgent)}</span>${artifact.publisherId || artifact.publisherName ? `<span class="publisher-cell" title="${escapeAttribute(publisherTitle(artifact, view))}">${escapeHtml(publisher)}</span>` : ""}</span>
96
+ <span class="row-formats">${typeBadge(artifact.artifactType || "document")}${formatBadge(artifact.format || "text")}<span>v${artifact.latestVersion}</span></span>
99
97
  <span class="tags">${status}${visibility}${tags}</span>
100
98
  </a>
101
99
  `;
@@ -188,17 +186,19 @@ function renderSavedViewsSidebar({ savedViews, filters, view }) {
188
186
  : `<li class="empty">${view.text("views.empty")}</li>`;
189
187
 
190
188
  return `
191
- <aside class="dashboard-sidebar">
192
- <h2>${view.text("views.title")}</h2>
189
+ <details class="dashboard-saved-views">
190
+ <summary>${view.text("views.title")} <span class="dashboard-filter-count">${savedViews.length}</span></summary>
191
+ <div class="dashboard-saved-content">
193
192
  <ul class="saved-view-list">${items}</ul>
194
193
  <form method="post" action="${view.href("/views")}" class="saved-view-form">
195
194
  ${localeInput(view.locale)}
196
195
  ${savedViewFilterHiddenInputs(filters)}
197
- <input name="name" placeholder="${view.attr("views.namePlaceholder")}" required>
196
+ <label class="dashboard-field"><span>${view.text("views.namePlaceholder")}</span><input name="name" placeholder="${view.attr("views.namePlaceholder")}" required></label>
198
197
  <label class="checkbox-field"><input type="checkbox" name="shared" value="true"> ${view.text("views.shared")}</label>
199
198
  <button type="submit">${view.text("views.save")}</button>
200
199
  </form>
201
- </aside>
200
+ </div>
201
+ </details>
202
202
  `;
203
203
  }
204
204
 
@@ -260,7 +260,7 @@ function dashboardPageHref(filters, offset) {
260
260
  return query ? `/?${query}` : "/";
261
261
  }
262
262
 
263
- export function renderArtifactFormPage({ mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/", expectedVersion, conflict = null }) {
263
+ export function renderArtifactFormPage({ user, mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/", expectedVersion, conflict = null }) {
264
264
  const view = viewContext(locale, currentPath);
265
265
  const isEdit = mode === "edit";
266
266
  const title = isEdit ? view.raw("form.editTitle", { title: artifact.title }) : view.raw("form.newTitle");
@@ -281,6 +281,7 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
281
281
  return pageShell({
282
282
  title,
283
283
  body: `
284
+ ${workspaceHeader(user, view)}
284
285
  <header class="topbar">
285
286
  <div>
286
287
  <h1>${escapeHtml(title)}</h1>
@@ -288,10 +289,10 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
288
289
  </div>
289
290
  <nav>
290
291
  <a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
291
- ${languageSwitcher(view)}
292
+
292
293
  </nav>
293
294
  </header>
294
- <main class="artifact-editor">
295
+ <main id="main-content" class="artifact-editor">
295
296
  ${conflictBanner}
296
297
  <form class="editor-form" method="post" action="${action}">
297
298
  ${hiddenToken(authToken)}
@@ -340,8 +341,8 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
340
341
  });
341
342
  }
342
343
 
343
- export function renderNewArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/new" }) {
344
- return renderArtifactFormPage({ mode: "new", baseUrl, authToken, locale, currentPath });
344
+ export function renderNewArtifactPage({ user, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/new" }) {
345
+ return renderArtifactFormPage({ mode: "new", baseUrl, authToken, locale, currentPath, user });
345
346
  }
346
347
 
347
348
  export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, content, baseUrl, user, error = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
@@ -371,19 +372,20 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
371
372
  return pageShell({
372
373
  title: `Manage versions · ${artifact.title}`,
373
374
  body: `
375
+ ${workspaceHeader(user, view)}
374
376
  <header class="topbar">
375
377
  <div>
376
378
  <h1>Manage versions</h1>
377
379
  <p>${escapeHtml(artifact.title)} · ${escapeHtml(artifact.id)} · ${escapeHtml(baseUrl)}</p>
378
380
  </div>
379
381
  <nav>
380
- <a href="${view.href(artifactPath)}">Artifact</a>
382
+ <a href="${view.href(artifactPath)}">${view.text("nav.artifact")}</a>
381
383
  <a href="${view.href(`${artifactPath}/diff`)}">${view.text("nav.diff")}</a>
382
- ${authNav(user, view)}
383
- ${languageSwitcher(view)}
384
+
385
+
384
386
  </nav>
385
387
  </header>
386
- <main class="artifact-editor">
388
+ <main id="main-content" class="artifact-editor">
387
389
  ${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
388
390
  <section class="meta-card">
389
391
  <h2>Versions</h2>
@@ -421,23 +423,21 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
421
423
  });
422
424
  }
423
425
 
424
- export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/import" }) {
426
+ export function renderImportArtifactPage({ user, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/import" }) {
425
427
  const view = viewContext(locale, currentPath);
426
428
  const title = view.raw("form.importTitle");
427
429
  return pageShell({
428
430
  title,
429
431
  body: `
432
+ ${workspaceHeader(user, view)}
430
433
  <header class="topbar">
431
434
  <div>
432
435
  <h1>${escapeHtml(title)}</h1>
433
436
  <p>${escapeHtml(baseUrl)}</p>
434
437
  </div>
435
- <nav>
436
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
437
- ${languageSwitcher(view)}
438
- </nav>
438
+
439
439
  </header>
440
- <main class="artifact-editor">
440
+ <main id="main-content" class="artifact-editor">
441
441
  <form class="editor-form" method="post" action="/import">
442
442
  ${hiddenToken(authToken)}
443
443
  ${localeInput(view.locale)}
@@ -490,17 +490,15 @@ export function renderLoginPage({ baseUrl, setup = false, error = "", locale = D
490
490
  return pageShell({
491
491
  title,
492
492
  body: `
493
+ ${workspaceHeader(null, view)}
493
494
  <header class="topbar">
494
495
  <div>
495
496
  <h1>${escapeHtml(title)}</h1>
496
497
  <p>${escapeHtml(baseUrl)}</p>
497
498
  </div>
498
- <nav>
499
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
500
- ${languageSwitcher(view)}
501
- </nav>
499
+
502
500
  </header>
503
- <main class="artifact-editor auth-panel">
501
+ <main id="main-content" class="artifact-editor auth-panel">
504
502
  ${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
505
503
  ${setup ? `<p class="muted">No users exist yet. The first account becomes an administrator.</p>` : ""}
506
504
  <form class="editor-form auth-form" method="post" action="/login">
@@ -556,20 +554,15 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
556
554
  return pageShell({
557
555
  title: "Account",
558
556
  body: `
557
+ ${workspaceHeader(user, view)}
559
558
  <header class="topbar">
560
559
  <div>
561
560
  <h1>Account</h1>
562
561
  <p>${escapeHtml(user.email)} · ${escapeHtml(user.role)} · ${escapeHtml(baseUrl)}</p>
563
562
  </div>
564
- <nav>
565
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
566
- ${user.role === "admin" ? `<a href="/admin/users">Users</a><a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>` : ""}
567
- <a href="/account/password">Password</a>
568
- <form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
569
- ${languageSwitcher(view)}
570
- </nav>
563
+
571
564
  </header>
572
- <main class="artifact-view">
565
+ <main id="main-content" class="settings-page artifact-view">
573
566
  ${createdToken ? `<section class="token-once">
574
567
  <h2>New API token</h2>
575
568
  <p>Copy this token now. Artifacty stores only its hash and cannot show it again.</p>
@@ -619,19 +612,15 @@ export function renderPasswordPage({ baseUrl, user, required = false, error = ""
619
612
  return pageShell({
620
613
  title: "Change Password",
621
614
  body: `
615
+ ${workspaceHeader(user, view)}
622
616
  <header class="topbar">
623
617
  <div>
624
618
  <h1>Change Password</h1>
625
619
  <p>${escapeHtml(user.email)} · ${escapeHtml(baseUrl)}</p>
626
620
  </div>
627
- <nav>
628
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
629
- ${required ? "" : `<a href="/account">Account</a>`}
630
- <form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
631
- ${languageSwitcher(view)}
632
- </nav>
621
+
633
622
  </header>
634
- <main class="artifact-editor auth-panel">
623
+ <main id="main-content" class="settings-page artifact-editor auth-panel">
635
624
  ${required ? `<p class="auth-warning">Password change is required before continuing.</p>` : ""}
636
625
  ${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
637
626
  ${success ? `<p class="auth-success">${escapeHtml(success)}</p>` : ""}
@@ -670,25 +659,20 @@ function renderFullImportSummary(tableCounts) {
670
659
  return parts ? ` (${parts})` : "";
671
660
  }
672
661
 
673
- export function renderAdminBackupPage({ baseUrl, integrity, importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/backup" }) {
662
+ export function renderAdminBackupPage({ user, baseUrl, integrity, importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/backup" }) {
674
663
  const view = viewContext(locale, currentPath);
675
664
  return pageShell({
676
665
  title: "Backup",
677
666
  body: `
667
+ ${workspaceHeader(user, view)}
678
668
  <header class="topbar">
679
669
  <div>
680
670
  <h1>Backup</h1>
681
671
  <p>${escapeHtml(baseUrl)}</p>
682
672
  </div>
683
- <nav>
684
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
685
- <a href="/admin/users">Users</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
686
- <a href="/admin/retention">${view.text("retention.navLabel")}</a>
687
- <a href="/account">Account</a>
688
- ${languageSwitcher(view)}
689
- </nav>
673
+
690
674
  </header>
691
- <main class="artifact-view">
675
+ <main id="main-content" class="settings-page artifact-view">
692
676
  ${importResult ? `<p class="auth-success">Restore complete. ${escapeHtml(importResult.artifactCount)} artifacts imported${importResult.scope === "full" ? renderFullImportSummary(importResult.tableCounts) : ""}.</p>` : ""}
693
677
  ${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
694
678
  <section class="meta-card">
@@ -737,14 +721,57 @@ export function renderAdminBackupPage({ baseUrl, integrity, importResult = null,
737
721
  });
738
722
  }
739
723
 
724
+ export function renderAdminUpdatePage({ baseUrl, user, status = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/update" }) {
725
+ const view = viewContext(locale, currentPath);
726
+ const check = status.check;
727
+ const run = status.run;
728
+ const active = ["queued", "running"].includes(run?.state);
729
+ const canStart = check?.supported && !check.errorCode && check.targetVersion && check.updateAvailable && check.confirmationId && Date.parse(check.expiresAt) > Date.now() && !active && run?.state !== "interrupted" && status.currentVersion !== check.targetVersion;
730
+ const states = Object.fromEntries(["queued", "running", "succeeded", "failed", "interrupted"].map((state) => [state, view.raw(`update.${state}`)]));
731
+ const messages = { ...states, reconnect: view.raw("update.reconnect"), signIn: view.raw("update.signIn") };
732
+ return pageShell({
733
+ title: view.text("update.title"), locale: view.locale,
734
+ body: `${workspaceHeader(user, view)}
735
+ <header class="topbar"><div><h1>${view.text("update.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
736
+ <main id="main-content" class="settings-page artifact-view">
737
+ <section class="meta-card"><h2>${view.text("update.installed")}</h2><p>Artifacty <strong data-update-version>${escapeHtml(status.currentVersion || APP_VERSION)}</strong></p>
738
+ <p>${view.text("update.help")}</p>
739
+ <form method="post" action="/admin/update/check">${localeInput(view.locale)}<button type="submit"${active ? " disabled" : ""}>${view.text("update.check")}</button></form>
740
+ </section>
741
+ ${check ? `<section class="meta-card"><h2>${view.text("update.latest")}</h2>
742
+ <p>${escapeHtml(status.currentVersion || APP_VERSION)} → <strong>${escapeHtml(check.targetVersion || "—")}</strong></p>
743
+ ${check.targetVersion ? `<p><a href="https://github.com/raeseoklee/artifacty/releases/tag/v${escapeAttribute(encodeURIComponent(check.targetVersion))}" target="_blank" rel="noopener noreferrer">${view.text("update.releaseNotes")}</a></p>` : ""}
744
+ ${check.errorCode ? `<p role="alert">${view.text("update.checkFailed")}</p>` : !check.supported ? `<p>${view.text("update.unsupported")}</p><p>${view.text("update.manual")}</p>` : !check.updateAvailable && !check.errorCode && check.targetVersion ? `<p>${view.text("update.current")}</p>` : ""}
745
+ ${check.supported && check.updateAvailable && !canStart && !active && !check.errorCode && status.currentVersion !== check.targetVersion ? `<p>${view.text("update.checkAgain")}</p>` : ""}
746
+ ${canStart ? `<p>${view.text("update.available", { version: check.targetVersion })}</p><p>${view.text("update.backup")}</p>
747
+ <form class="editor-form" method="post" action="/admin/update/start">${localeInput(view.locale)}
748
+ <input type="hidden" name="confirmationId" value="${escapeAttribute(check.confirmationId)}">
749
+ <input type="hidden" name="targetVersion" value="${escapeAttribute(check.targetVersion)}">
750
+ <label><input type="checkbox" name="confirmUpdate" value="true" required> ${view.text("update.confirm")}</label>
751
+ ${check.expiresAt ? `<p class="muted">${view.text("update.expires")}: ${`<time datetime="${escapeAttribute(check.expiresAt)}" title="${escapeAttribute(check.expiresAt)}">${escapeHtml(new Intl.DateTimeFormat(view.locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(new Date(check.expiresAt)))} UTC</time>`}</p>` : ""}
752
+ <button type="submit">${view.text("update.start")}</button>
753
+ </form>` : ""}
754
+ </section>` : ""}
755
+ ${run ? `<section class="meta-card" data-update-status data-active="${active}" data-messages="${escapeAttribute(JSON.stringify(messages))}"><h2>${view.text("update.progress")}</h2>
756
+ <p>${view.text("update.latest")}: ${escapeHtml(run.targetVersion || "—")}</p>
757
+ <p role="status" aria-live="polite" data-update-state>${escapeHtml(states[run.state] || states.running)}</p>
758
+ <p data-update-error>${escapeHtml(run.error || "")}</p>
759
+ <a href="${view.href("/admin/update")}">${view.text("update.refresh")}</a> <a data-update-login hidden href="${view.href("/login")}">${view.text("nav.signIn")}</a>
760
+ </section>` : ""}
761
+ </main>`,
762
+ afterBody: active ? '<script type="module" src="/assets/update.js"></script>' : ""
763
+ });
764
+ }
765
+
740
766
  export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users = [], networkPolicy = { allowedCidrs: [] }, currentIp = "", preview = null, pendingNetworkPolicy = null, shareLinkPolicy = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/access" }) {
741
767
  const view = viewContext(locale, currentPath);
742
768
  return pageShell({
743
769
  title: view.text("access.title"),
744
770
  locale: view.locale,
745
- body: `<header class="topbar"><div><h1>${view.text("access.title")}</h1><p>${escapeHtml(baseUrl)}</p></div>
746
- <nav><a href="${view.href("/")}">${view.text("nav.index")}</a>${authNav(user, view)}${languageSwitcher(view)}</nav></header>
747
- <main class="artifact-view">
771
+ body: `${workspaceHeader(user, view)}
772
+ <header class="topbar"><div><h1>${view.text("access.title")}</h1><p>${escapeHtml(baseUrl)}</p></div>
773
+ </header>
774
+ <main id="main-content" class="settings-page artifact-view">
748
775
  <section class="meta-card"><h2>${view.text("access.mode")}</h2>
749
776
  <form class="editor-form" method="post" action="/admin/access/preview">${localeInput(view.locale)}
750
777
  <label><input type="radio" name="mode" value="public"${mode === "public" ? " checked" : ""}> ${view.text("access.public")}</label>
@@ -768,8 +795,6 @@ export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users =
768
795
  <button type="submit">${view.text("access.saveNetwork")}</button>
769
796
  </form>
770
797
  </section>
771
- <section class="meta-card"><h2>${view.text("access.samples")}</h2><p>${view.text("access.samplesHelp")}</p>
772
- ${["install", "remove"].map((action) => `<form method="post" action="/admin/samples/${action}">${localeInput(view.locale)}<button type="submit">${view.text(`access.samples.${action}`)}</button></form>`).join("")}</section>
773
798
  <section class="meta-card"><h2>${view.text("access.teams")}</h2>
774
799
  <form class="editor-form auth-form" method="post" action="/admin/teams">${localeInput(view.locale)}
775
800
  <label class="field"><span>${view.text("access.teamName")}</span><input name="name" required></label>
@@ -781,6 +806,8 @@ export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users =
781
806
  <button type="submit">${view.text("access.saveMembers")}</button>
782
807
  </form></section>`).join("") : `<p class="muted">${view.text("access.noTeams")}</p>`}
783
808
  </section>
809
+ <section class="meta-card"><h2>${view.text("access.samples")}</h2><p>${view.text("access.samplesHelp")}</p>
810
+ ${["install", "remove"].map((action) => `<form method="post" action="/admin/samples/${action}">${localeInput(view.locale)}<button type="submit">${view.text(`access.samples.${action}`)}</button></form>`).join("")}</section>
784
811
  </main>`
785
812
  });
786
813
  }
@@ -790,9 +817,10 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
790
817
  return pageShell({
791
818
  title: view.text("sharing.title"),
792
819
  locale: view.locale,
793
- body: `<header class="topbar"><div><h1>${view.text("sharing.title")}: ${escapeHtml(artifact.title)}</h1><p>${escapeHtml(baseUrl)}</p></div>
794
- <nav><a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a>${authNav(user, view)}${languageSwitcher(view)}</nav></header>
795
- <main class="artifact-view"><section class="meta-card">
820
+ body: `${workspaceHeader(user, view)}
821
+ <header class="topbar"><div><h1>${view.text("sharing.title")}: ${escapeHtml(artifact.title)}</h1><p>${escapeHtml(baseUrl)}</p></div>
822
+ <nav><a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a></nav></header>
823
+ <main id="main-content" class="settings-page artifact-view"><section class="meta-card">
796
824
  <p>${view.text("sharing.help")}</p>
797
825
  <form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/sharing">${localeInput(view.locale)}
798
826
  <fieldset><legend>${view.text("sharing.users")}</legend>${accessCheckboxes(users, "userIds", sharing.userIds || [], view, sharing.userPermissions || {})}</fieldset>
@@ -821,8 +849,9 @@ function accessCheckboxes(items, name, selectedIds, view, permissions = null) {
821
849
  export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount = 0, locale = DEFAULT_LOCALE, currentPath = "/inbox" }) {
822
850
  const view = viewContext(locale, currentPath);
823
851
  return pageShell({ title: view.text("inbox.title"), locale: view.locale,
824
- body: `<header class="topbar"><div><h1>${view.text("inbox.title")}</h1><p>${escapeHtml(baseUrl)}</p></div><nav><a href="${view.href("/")}">${view.text("nav.index")}</a>${authNav(user, view)}${languageSwitcher(view)}</nav></header>
825
- <main class="artifact-view"><section class="meta-card"><p>${view.text("inbox.unread", { count: unreadCount })}</p>
852
+ body: `${workspaceHeader(user, view)}
853
+ <header class="topbar"><div><h1>${view.text("inbox.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
854
+ <main id="main-content" class="artifact-view"><section class="meta-card"><p>${view.text("inbox.unread", { count: unreadCount })}</p>
826
855
  <form method="post" action="/inbox/read-all">${localeInput(view.locale)}<button type="submit">${view.text("inbox.readAll")}</button></form>
827
856
  ${notifications.length ? notifications.map((item) => `<section><h2><a href="${view.href(`/artifacts/${encodeURIComponent(item.artifactId)}`)}">${escapeHtml(item.title)}</a></h2><p>${escapeHtml(item.type)} · ${escapeHtml(item.createdAt)}</p>
828
857
  ${item.readAt ? `<p>${view.text("inbox.read")}</p>` : `<form method="post" action="/inbox/${encodeURIComponent(item.id)}/read">${localeInput(view.locale)}<button type="submit">${view.text("inbox.markRead")}</button></form>`}</section>`).join("") : `<p>${view.text("inbox.empty")}</p>`}
@@ -842,7 +871,7 @@ export function renderSharedArtifactPage({ artifact, version, content, locale =
842
871
  });
843
872
  const needsViewer = ["code", "markdown", "notebook", "sarif", "csv"].includes(version.format);
844
873
  return pageShell({ title: artifact.title, locale: view.locale,
845
- body: `<header class="topbar"><div><h1>${escapeHtml(artifact.title)}</h1><p>${view.text("sharing.readOnly")} · v${Number(version.version)}</p></div></header><main class="artifact-view">${rendered}</main>`,
874
+ body: `<header class="topbar"><div><h1>${escapeHtml(artifact.title)}</h1><p>${view.text("sharing.readOnly")} · v${Number(version.version)}</p></div></header><main id="main-content" class="artifact-view">${rendered}</main>`,
846
875
  head: needsViewer ? editorHead() : "",
847
876
  afterBody: `${frameResizeScript()}${needsViewer ? viewerScript() : ""}` });
848
877
  }
@@ -899,20 +928,15 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
899
928
  return pageShell({
900
929
  title: "Users",
901
930
  body: `
931
+ ${workspaceHeader(user, view)}
902
932
  <header class="topbar">
903
933
  <div>
904
934
  <h1>Users</h1>
905
935
  <p>${escapeHtml(baseUrl)}</p>
906
936
  </div>
907
- <nav>
908
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
909
- <a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
910
- <a href="/admin/retention">${view.text("retention.navLabel")}</a>
911
- <a href="/account">Account</a>
912
- ${languageSwitcher(view)}
913
- </nav>
937
+
914
938
  </header>
915
- <main class="artifact-view">
939
+ <main id="main-content" class="settings-page artifact-view">
916
940
  <section class="meta-card">
917
941
  <h2>Create user</h2>
918
942
  <form class="editor-form auth-form" method="post" action="/admin/users">
@@ -952,7 +976,7 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
952
976
  });
953
977
  }
954
978
 
955
- export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret = null, createdWebhookId = null, locale = DEFAULT_LOCALE, currentPath = "/admin/webhooks" }) {
979
+ export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdSecret = null, createdWebhookId = null, locale = DEFAULT_LOCALE, currentPath = "/admin/webhooks" }) {
956
980
  const view = viewContext(locale, currentPath);
957
981
  const rows = webhooks.map((webhook) => `
958
982
  <tr>
@@ -981,21 +1005,15 @@ export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret
981
1005
  return pageShell({
982
1006
  title: view.text("webhooks.title"),
983
1007
  body: `
1008
+ ${workspaceHeader(user, view)}
984
1009
  <header class="topbar">
985
1010
  <div>
986
1011
  <h1>${view.text("webhooks.title")}</h1>
987
1012
  <p>${escapeHtml(baseUrl)}</p>
988
1013
  </div>
989
- <nav>
990
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
991
- <a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
992
- <a href="/admin/users">Users</a>
993
- <a href="/admin/retention">${view.text("retention.navLabel")}</a>
994
- <a href="/account">Account</a>
995
- ${languageSwitcher(view)}
996
- </nav>
1014
+
997
1015
  </header>
998
- <main class="artifact-view">
1016
+ <main id="main-content" class="settings-page artifact-view">
999
1017
  ${secretBanner}
1000
1018
  <section class="meta-card">
1001
1019
  <h2>${view.text("webhooks.createTitle")}</h2>
@@ -1020,7 +1038,7 @@ export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret
1020
1038
  });
1021
1039
  }
1022
1040
 
1023
- export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, report = null, locale = DEFAULT_LOCALE, currentPath = "/admin/retention" }) {
1041
+ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = null, report = null, locale = DEFAULT_LOCALE, currentPath = "/admin/retention" }) {
1024
1042
  const view = viewContext(locale, currentPath);
1025
1043
  const byTypeText = Object.entries(policy.archiveAfterDays.byType || {})
1026
1044
  .map(([type, days]) => `${type}=${days}`)
@@ -1070,20 +1088,15 @@ export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, re
1070
1088
  return pageShell({
1071
1089
  title: view.text("retention.title"),
1072
1090
  body: `
1091
+ ${workspaceHeader(user, view)}
1073
1092
  <header class="topbar">
1074
1093
  <div>
1075
1094
  <h1>${view.text("retention.title")}</h1>
1076
1095
  <p>${escapeHtml(baseUrl)}</p>
1077
1096
  </div>
1078
- <nav>
1079
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
1080
- <a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
1081
- <a href="/admin/users">Users</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
1082
- <a href="/account">Account</a>
1083
- ${languageSwitcher(view)}
1084
- </nav>
1097
+
1085
1098
  </header>
1086
- <main class="artifact-view">
1099
+ <main id="main-content" class="settings-page artifact-view">
1087
1100
  <section class="meta-card">
1088
1101
  <h2>${view.text("retention.policyTitle")}</h2>
1089
1102
  <form class="editor-form" method="post" action="/admin/retention">
@@ -1168,33 +1181,36 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
1168
1181
  return pageShell({
1169
1182
  title: artifact.title,
1170
1183
  body: `
1184
+ ${workspaceHeader(user, view)}
1171
1185
  <header class="topbar">
1172
1186
  <div>
1173
1187
  <h1>${escapeHtml(artifact.title)}</h1>
1174
1188
  <p>${escapeHtml(artifact.id)} · ${escapeHtml(artifact.sourceAgent)} · ${escapeHtml(baseUrl)}</p>
1175
1189
  </div>
1176
1190
  <nav>
1177
- <a href="${view.href("/")}">${view.text("nav.index")}</a>
1191
+
1178
1192
  ${canEdit ? `<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/edit`)}">${view.text("nav.edit")}</a>` : ""}
1179
1193
  <a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/diff`)}">${view.text("nav.diff")}</a>
1180
- ${user?.role === "admin" ? `<a href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions`)}">Versions</a>` : ""}
1194
+ ${user?.role === "admin" ? `<a href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions`)}">${view.text("nav.versions")}</a>` : ""}
1181
1195
  <a href="${view.href(rawUrl)}">${view.text("nav.raw")}</a>
1182
1196
  ${user && canManageVisibility ? `<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/sharing`)}">${view.text("sharing.title")}</a>` : ""}
1183
- ${authNav(user, view)}
1184
- ${languageSwitcher(view)}
1197
+
1198
+
1185
1199
  </nav>
1186
1200
  </header>
1187
- <main class="${viewClass}">
1188
- <section class="meta-strip">
1189
- ${formatBadge(version.format)}
1190
- ${typeBadge(artifact.artifactType || "document")}
1191
- <span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
1201
+ <main id="main-content" class="${viewClass}">
1202
+ <section class="meta-strip artifact-overview">
1203
+ <span>${escapeHtml(version.format)} · v${version.version}</span>
1192
1204
  ${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}
1193
1205
  ${visibilityBadge(artifact, view)}
1194
- <span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
1195
- <span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
1196
- <span>${escapeHtml(version.createdAt)}</span>
1197
- <span>${escapeHtml(version.sha256.slice(0, 12))}</span>
1206
+ <details class="artifact-details"><summary>${view.text("artifact.details")}</summary><div class="meta-strip">
1207
+ ${typeBadge(artifact.artifactType || "document")}
1208
+ <span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
1209
+ <span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
1210
+ <span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
1211
+ <span>${escapeHtml(version.createdAt)}</span>
1212
+ <span>${escapeHtml(version.sha256.slice(0, 12))}</span>
1213
+ </div></details>
1198
1214
  </section>
1199
1215
  ${canArchive ? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/${archiveAction}">
1200
1216
  ${hiddenToken(authToken)}
@@ -1456,7 +1472,7 @@ function artifactViewClass({ artifact, version }) {
1456
1472
  return classes.join(" ");
1457
1473
  }
1458
1474
 
1459
- export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent, toContent, diffRows, view: diffView = "lines", structuredDiff, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
1475
+ export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromContent, toContent, diffRows, view: diffView = "lines", structuredDiff, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
1460
1476
  const view = viewContext(locale, currentPath);
1461
1477
  const title = view.raw("diff.title", { title: artifact.title });
1462
1478
  const versionOptions = artifact.versions
@@ -1504,6 +1520,7 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
1504
1520
  return pageShell({
1505
1521
  title,
1506
1522
  body: `
1523
+ ${workspaceHeader(user, view)}
1507
1524
  <header class="topbar">
1508
1525
  <div>
1509
1526
  <h1>${escapeHtml(title)}</h1>
@@ -1511,10 +1528,10 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
1511
1528
  </div>
1512
1529
  <nav>
1513
1530
  <a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a>
1514
- ${languageSwitcher(view)}
1531
+
1515
1532
  </nav>
1516
1533
  </header>
1517
- <main class="artifact-view">
1534
+ <main id="main-content" class="artifact-view">
1518
1535
  <form class="diff-form" method="get" action="/artifacts/${encodeURIComponent(artifact.id)}/diff">
1519
1536
  ${localeInput(view.locale)}
1520
1537
  <input type="hidden" name="view" value="${escapeAttribute(resolvedView)}">
@@ -1915,20 +1932,20 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
1915
1932
  <style>
1916
1933
  :root {
1917
1934
  color-scheme: light dark;
1918
- --bg: #eef1f5;
1935
+ --bg: #f5f6f8;
1919
1936
  --grid: rgba(31, 58, 99, 0.05);
1920
1937
  --panel: #ffffff;
1921
- --panel-2: #f3f6f9;
1922
- --text: #151b24;
1923
- --muted: #586273;
1924
- --faint: #828c9b;
1925
- --line: #e0e5ec;
1926
- --line-2: #cfd6df;
1927
- --accent: #0e7490;
1928
- --accent-2: #0b5e75;
1938
+ --panel-2: #eef0f3;
1939
+ --text: #20242b;
1940
+ --muted: #58616e;
1941
+ --faint: #626c79;
1942
+ --line: #dfe3e8;
1943
+ --line-2: #b9c1cc;
1944
+ --accent: #315ca8;
1945
+ --accent-2: #264a89;
1929
1946
  --accent-ink: #ffffff;
1930
- --accent-soft: rgba(14, 116, 144, 0.10);
1931
- --ring: rgba(14, 116, 144, 0.32);
1947
+ --accent-soft: #eaf0fb;
1948
+ --ring: #315ca8;
1932
1949
  --code: #0e1320;
1933
1950
  --code-text: #e6edf3;
1934
1951
  --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
@@ -1936,20 +1953,20 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
1936
1953
  }
1937
1954
  @media (prefers-color-scheme: dark) {
1938
1955
  :root {
1939
- --bg: #0b0f14;
1956
+ --bg: #15171b;
1940
1957
  --grid: rgba(120, 160, 200, 0.06);
1941
- --panel: #12171f;
1942
- --panel-2: #171d27;
1958
+ --panel: #1c1f24;
1959
+ --panel-2: #25292f;
1943
1960
  --text: #e6edf3;
1944
- --muted: #9aa6b3;
1945
- --faint: #6b7686;
1946
- --line: #222a35;
1947
- --line-2: #2d3744;
1948
- --accent: #22b8cf;
1949
- --accent-2: #3ccbdd;
1950
- --accent-ink: #04161c;
1951
- --accent-soft: rgba(34, 184, 207, 0.14);
1952
- --ring: rgba(34, 184, 207, 0.40);
1961
+ --muted: #adb5c0;
1962
+ --faint: #9aa5b2;
1963
+ --line: #343b44;
1964
+ --line-2: #596575;
1965
+ --accent: #9cbcf5;
1966
+ --accent-2: #bbd1f8;
1967
+ --accent-ink: #18253d;
1968
+ --accent-soft: #29374e;
1969
+ --ring: #9cbcf5;
1953
1970
  --code: #080c12;
1954
1971
  --code-text: #e6edf3;
1955
1972
  }
@@ -1959,13 +1976,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
1959
1976
  margin: 0;
1960
1977
  min-height: 100vh;
1961
1978
  background-color: var(--bg);
1962
- background-image:
1963
- linear-gradient(var(--grid) 1px, transparent 1px),
1964
- linear-gradient(90deg, var(--grid) 1px, transparent 1px);
1965
- background-size: 28px 28px;
1966
- background-position: center top;
1979
+
1967
1980
  color: var(--text);
1968
1981
  font-family: var(--sans);
1982
+ font-size: 14px;
1969
1983
  line-height: 1.55;
1970
1984
  letter-spacing: 0;
1971
1985
  -webkit-font-smoothing: antialiased;
@@ -1973,28 +1987,28 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
1973
1987
  a { color: var(--accent); text-decoration: none; }
1974
1988
  a:hover { color: var(--accent-2); text-decoration: underline; }
1975
1989
  .topbar {
1976
- position: sticky;
1977
- top: 0;
1978
- z-index: 10;
1990
+ position: relative;
1991
+ width: min(1440px, calc(100% - 64px));
1992
+ margin: 28px auto 20px;
1979
1993
  display: flex;
1980
1994
  align-items: center;
1981
1995
  justify-content: space-between;
1982
1996
  gap: 24px;
1983
- padding: 15px 28px;
1997
+ padding: 0 0 20px;
1984
1998
  border-bottom: 1px solid var(--line);
1985
- background: color-mix(in srgb, var(--panel) 86%, transparent);
1986
- backdrop-filter: saturate(150%) blur(10px);
1999
+ background: transparent;
1987
2000
  }
1988
2001
  .topbar h1 {
1989
2002
  margin: 0;
1990
- font-size: 20px;
1991
- line-height: 1.25;
2003
+ font-size: 25px;
2004
+ font-weight: 650;
2005
+ line-height: 1.3;
1992
2006
  letter-spacing: -0.01em;
1993
2007
  }
1994
2008
  .topbar p {
1995
2009
  margin: 5px 0 0;
1996
2010
  color: var(--muted);
1997
- font-family: var(--mono);
2011
+ font-family: var(--sans);
1998
2012
  font-size: 12px;
1999
2013
  overflow-wrap: anywhere;
2000
2014
  }
@@ -2008,14 +2022,14 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2008
2022
  .brand-mark { display: inline-flex; color: var(--accent); }
2009
2023
  .brand-text { display: grid; gap: 2px; }
2010
2024
  .brand-name {
2011
- font-family: var(--mono);
2025
+ font-family: var(--sans);
2012
2026
  font-size: 19px;
2013
2027
  font-weight: 600;
2014
2028
  letter-spacing: -0.01em;
2015
2029
  }
2016
2030
  .brand-tag {
2017
2031
  color: var(--muted);
2018
- font-family: var(--mono);
2032
+ font-family: var(--sans);
2019
2033
  font-size: 12px;
2020
2034
  overflow-wrap: anywhere;
2021
2035
  }
@@ -2024,7 +2038,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2024
2038
  align-items: center;
2025
2039
  gap: 3px;
2026
2040
  flex-wrap: wrap;
2027
- font-family: var(--mono);
2041
+ font-family: var(--sans);
2028
2042
  font-size: 13px;
2029
2043
  }
2030
2044
  .topbar nav > a {
@@ -2058,17 +2072,17 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2058
2072
  .dashboard,
2059
2073
  .artifact-view,
2060
2074
  .artifact-editor {
2061
- width: min(1180px, calc(100vw - 32px));
2075
+ width: min(1440px, calc(100% - 64px));
2062
2076
  margin: 28px auto 72px;
2063
2077
  }
2064
2078
  .artifact-view-wide {
2065
- width: min(1760px, calc(100vw - 24px));
2079
+ width: min(1760px, calc(100% - 64px));
2066
2080
  }
2067
2081
  .toolbar {
2068
- font-family: var(--mono);
2082
+ font-family: var(--sans);
2069
2083
  font-size: 12px;
2070
- letter-spacing: 0.08em;
2071
- text-transform: uppercase;
2084
+ letter-spacing: 0;
2085
+ text-transform: none;
2072
2086
  color: var(--faint);
2073
2087
  }
2074
2088
  .toolbar,
@@ -2096,34 +2110,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2096
2110
  .filter-form select {
2097
2111
  min-width: 120px;
2098
2112
  }
2099
- .dashboard-layout {
2100
- display: flex;
2101
- align-items: flex-start;
2102
- gap: 24px;
2103
- width: min(1180px, calc(100vw - 32px));
2104
- margin: 28px auto 72px;
2105
- }
2106
- .dashboard-layout .dashboard {
2107
- flex: 1;
2108
- min-width: 0;
2109
- width: auto;
2110
- margin: 0;
2111
- }
2112
- .dashboard-sidebar {
2113
- width: 240px;
2114
- flex: none;
2115
- border: 1px solid var(--line);
2116
- border-radius: 12px;
2117
- background: var(--panel);
2118
- padding: 14px;
2119
- }
2120
- .dashboard-sidebar h2 {
2121
- margin: 0 0 10px;
2122
- font-size: 13px;
2123
- text-transform: uppercase;
2124
- letter-spacing: 0.06em;
2125
- color: var(--faint);
2126
- }
2113
+ .dashboard-layout { width: min(1440px, calc(100% - 64px)); margin: 28px auto 48px; }
2114
+ .dashboard-layout .dashboard { min-width: 0; width: 100%; margin: 0; }
2127
2115
  .saved-view-list {
2128
2116
  list-style: none;
2129
2117
  margin: 0 0 14px;
@@ -2160,8 +2148,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2160
2148
  margin: 0;
2161
2149
  padding: 10px 14px;
2162
2150
  font-size: 12px;
2163
- text-transform: uppercase;
2164
- letter-spacing: 0.06em;
2151
+ text-transform: none;
2152
+ letter-spacing: 0;
2165
2153
  color: var(--faint);
2166
2154
  background: var(--panel-2);
2167
2155
  border-bottom: 1px solid var(--line);
@@ -2226,10 +2214,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2226
2214
  }
2227
2215
  .artifact-list {
2228
2216
  border: 1px solid var(--line);
2229
- border-radius: 12px;
2217
+ border-radius: 8px;
2230
2218
  overflow: hidden;
2231
2219
  background: var(--panel);
2232
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
2220
+ box-shadow: none;
2233
2221
  }
2234
2222
  .pager {
2235
2223
  display: flex;
@@ -2342,8 +2330,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2342
2330
  color: var(--faint);
2343
2331
  font-family: var(--mono);
2344
2332
  font-size: 11.5px;
2345
- letter-spacing: 0.06em;
2346
- text-transform: uppercase;
2333
+ letter-spacing: 0;
2334
+ text-transform: none;
2347
2335
  }
2348
2336
  .data-table form {
2349
2337
  margin: 0;
@@ -2363,8 +2351,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2363
2351
  .field > span {
2364
2352
  font-family: var(--mono);
2365
2353
  font-size: 11.5px;
2366
- letter-spacing: 0.06em;
2367
- text-transform: uppercase;
2354
+ letter-spacing: 0;
2355
+ text-transform: none;
2368
2356
  color: var(--faint);
2369
2357
  }
2370
2358
  .check-field {
@@ -2380,7 +2368,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2380
2368
  min-height: 0;
2381
2369
  padding: 0;
2382
2370
  }
2383
- input,
2371
+ input:not([type="checkbox"]):not([type="radio"]),
2384
2372
  select,
2385
2373
  textarea {
2386
2374
  width: 100%;
@@ -2393,13 +2381,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2393
2381
  }
2394
2382
  input::placeholder,
2395
2383
  textarea::placeholder { color: var(--faint); }
2396
- input,
2384
+ input:not([type="checkbox"]):not([type="radio"]),
2397
2385
  select {
2398
2386
  min-height: 38px;
2399
2387
  padding: 7px 10px;
2400
2388
  }
2401
2389
  textarea {
2402
- min-height: 52vh;
2390
+ min-height: 120px;
2403
2391
  padding: 14px;
2404
2392
  resize: vertical;
2405
2393
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
@@ -2408,6 +2396,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2408
2396
  white-space: pre;
2409
2397
  overflow: auto;
2410
2398
  }
2399
+ textarea[data-artifacty-editor] { min-height: 52vh; }
2400
+ .comments textarea { min-height: 96px; white-space: pre-wrap; }
2411
2401
  .compact-textarea {
2412
2402
  min-height: 160px;
2413
2403
  }
@@ -2570,7 +2560,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2570
2560
  overflow-wrap: anywhere;
2571
2561
  }
2572
2562
  .artifact-row > span:not(.row-main):not(.tags):not(.badge) {
2573
- font-family: var(--mono);
2563
+ font-family: var(--sans);
2574
2564
  font-size: 12.5px;
2575
2565
  color: var(--muted);
2576
2566
  }
@@ -2596,7 +2586,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2596
2586
  border-radius: 999px;
2597
2587
  background: var(--panel-2);
2598
2588
  color: var(--muted);
2599
- font-family: var(--mono);
2589
+ font-family: var(--sans);
2600
2590
  font-size: 11.5px;
2601
2591
  }
2602
2592
  .badge {
@@ -2604,10 +2594,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2604
2594
  align-items: center;
2605
2595
  padding: 3px 9px;
2606
2596
  border-radius: 999px;
2607
- border: 1px solid color-mix(in srgb, var(--bh, var(--muted)) 38%, var(--line));
2608
- background: color-mix(in srgb, var(--bh, var(--muted)) 13%, transparent);
2609
- color: color-mix(in srgb, var(--bh, var(--muted)) 62%, var(--text));
2610
- font-family: var(--mono);
2597
+ border: 1px solid var(--line);
2598
+ background: var(--panel-2);
2599
+ color: var(--muted);
2600
+ font-family: var(--sans);
2611
2601
  font-size: 11.5px;
2612
2602
  line-height: 1.45;
2613
2603
  white-space: nowrap;
@@ -2694,9 +2684,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2694
2684
  width: 100%;
2695
2685
  min-height: 320px;
2696
2686
  border: 1px solid var(--line);
2697
- border-radius: 12px;
2687
+ border-radius: 8px;
2698
2688
  background: var(--panel);
2699
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
2689
+ box-shadow: none;
2700
2690
  }
2701
2691
  .split-preview {
2702
2692
  display: grid;
@@ -2790,12 +2780,12 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2790
2780
  .artifact-code {
2791
2781
  margin: 0;
2792
2782
  border: 1px solid var(--line);
2793
- border-radius: 12px;
2783
+ border-radius: 8px;
2794
2784
  background: var(--panel);
2795
2785
  }
2796
2786
  .artifact-doc {
2797
2787
  padding: 32px;
2798
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
2788
+ box-shadow: none;
2799
2789
  }
2800
2790
  .artifact-doc h1,
2801
2791
  .artifact-doc h2,
@@ -2831,10 +2821,16 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2831
2821
  border-radius: 10px;
2832
2822
  overflow: hidden;
2833
2823
  }
2834
- .artifact-doc-codemirror .cm-editor {
2835
- background: var(--code);
2836
- color: var(--code-text);
2837
- }
2824
+ .artifact-doc-codemirror .cm-editor,
2825
+ .artifact-code-viewer .cm-editor { min-height: 0; background: #f8f9fb; color: #20242b; }
2826
+ .artifact-doc-codemirror .cm-editor .cm-content,
2827
+ .artifact-code-viewer .cm-editor .cm-content { min-height: 0; }
2828
+ .artifact-doc-codemirror .cm-editor .cm-gutters,
2829
+ .artifact-code-viewer .cm-editor .cm-gutters { background: #eef0f3; color: #626c79; border-color: #dfe3e8; }
2830
+ .artifact-doc-codemirror .cm-editor .cm-activeLine,
2831
+ .artifact-doc-codemirror .cm-editor .cm-activeLineGutter,
2832
+ .artifact-code-viewer .cm-editor .cm-activeLine,
2833
+ .artifact-code-viewer .cm-editor .cm-activeLineGutter { background: transparent; }
2838
2834
  .artifact-doc-codemirror .cm-scroller {
2839
2835
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
2840
2836
  font-size: 13px;
@@ -2887,7 +2883,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2887
2883
  .artifact-notebook-cell-code {
2888
2884
  padding: 14px;
2889
2885
  border: 1px solid var(--line);
2890
- border-radius: 12px;
2886
+ border-radius: 8px;
2891
2887
  background: var(--panel);
2892
2888
  }
2893
2889
  .artifact-notebook-execcount {
@@ -2976,8 +2972,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
2976
2972
  color: var(--muted);
2977
2973
  font-family: var(--mono);
2978
2974
  font-size: 11.5px;
2979
- text-transform: uppercase;
2980
- letter-spacing: 0.06em;
2975
+ text-transform: none;
2976
+ letter-spacing: 0;
2981
2977
  }
2982
2978
  .summary-card strong {
2983
2979
  font-size: 20px;
@@ -3070,7 +3066,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
3070
3066
  }
3071
3067
  .artifact-raw-details {
3072
3068
  border: 1px solid var(--line);
3073
- border-radius: 12px;
3069
+ border-radius: 8px;
3074
3070
  background: var(--panel);
3075
3071
  overflow: hidden;
3076
3072
  }
@@ -3092,9 +3088,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
3092
3088
  margin: 0;
3093
3089
  padding: 14px;
3094
3090
  border: 1px solid var(--line);
3095
- border-radius: 12px;
3091
+ border-radius: 8px;
3096
3092
  background: var(--panel);
3097
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
3093
+ box-shadow: none;
3098
3094
  }
3099
3095
  .artifact-media img,
3100
3096
  .artifact-media video {
@@ -3123,25 +3119,17 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
3123
3119
  }
3124
3120
  .artifact-code-viewer {
3125
3121
  border: 1px solid var(--line);
3126
- border-radius: 12px;
3122
+ border-radius: 8px;
3127
3123
  overflow: hidden;
3128
3124
  background: var(--code);
3129
3125
  }
3130
- .artifact-code-viewer .cm-editor {
3131
- min-height: 50vh;
3132
- background: var(--code);
3133
- color: var(--code-text);
3134
- }
3126
+
3135
3127
  .artifact-code-viewer .cm-scroller {
3136
3128
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3137
3129
  font-size: 13px;
3138
3130
  line-height: 1.55;
3139
3131
  }
3140
- .artifact-code-viewer .cm-gutters {
3141
- background: color-mix(in srgb, var(--code) 84%, white);
3142
- color: var(--muted);
3143
- border-color: rgba(255, 255, 255, 0.08);
3144
- }
3132
+
3145
3133
  .artifact-react-disabled {
3146
3134
  display: grid;
3147
3135
  gap: 12px;
@@ -3160,11 +3148,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
3160
3148
  font-size: 13px;
3161
3149
  }
3162
3150
  @media (max-width: 760px) {
3163
- .topbar {
3164
- align-items: flex-start;
3165
- flex-direction: column;
3166
- padding: 20px;
3167
- }
3151
+ .topbar { align-items: flex-start; flex-direction: column; gap: 12px; }
3152
+ .topbar, .dashboard-layout, .artifact-view, .artifact-editor, .artifact-view-wide { width: calc(100% - 32px); margin-top: 20px; }
3153
+ input:not([type="checkbox"]):not([type="radio"]), select, button { min-height: 44px; }
3168
3154
  .artifact-row {
3169
3155
  grid-template-columns: 1fr;
3170
3156
  gap: 6px;
@@ -3186,6 +3172,114 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
3186
3172
  padding: 18px;
3187
3173
  }
3188
3174
  }
3175
+ .artifact-editor { max-width: 1000px; }
3176
+ .auth-panel { max-width: 520px; }
3177
+ .meta-card { padding: 24px; }
3178
+ .meta-card h2 { font-size: 16px; font-weight: 650; }
3179
+ .meta-card table { display: block; max-width: 100%; overflow-x: auto; }
3180
+ .artifact-doc { font-size: 15px; line-height: 1.7; }
3181
+ .workspace-header { background: var(--panel); border-bottom: 1px solid var(--line); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
3182
+ .workspace-brand { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: -.04em; }
3183
+ .workspace-brand svg { width: 27px; height: 27px; color: var(--accent); }
3184
+ .workspace-desktop { display: none; }
3185
+ .workspace-mobile > summary { cursor: pointer; padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 6px; }
3186
+ .workspace-mobile[open] { flex-basis: 100%; }
3187
+ .workspace-header:has(.workspace-mobile[open]) { flex-wrap: wrap; }
3188
+ .workspace-mobile nav { padding-top: 16px; }
3189
+ .workspace-primary, .workspace-actions { display: grid; gap: 4px; }
3190
+ .workspace-primary { padding-bottom: 16px; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
3191
+ .workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { display: block; min-height: 38px; padding: 8px 10px; color: var(--muted); border-radius: 6px; font-size: 14px; text-align: left; }
3192
+ .workspace-header nav a:hover, .workspace-dropdown summary:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
3193
+ .workspace-header [aria-current="page"], .workspace-header .is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
3194
+ .workspace-dropdown summary { cursor: pointer; list-style: none; }
3195
+ .workspace-dropdown summary::-webkit-details-marker { display: none; }
3196
+ .workspace-dropdown summary::after { content: "⌄"; float: right; margin-left: 8px; }
3197
+ .workspace-dropdown[open] > summary::after { content: "⌃"; }
3198
+ .workspace-version { color: var(--faint); font-size: 12px; padding: 10px; }
3199
+ .settings-page { max-width: 1100px; }
3200
+ .settings-page .meta-card { gap: 16px; }
3201
+ .settings-page .meta-card p { margin: 0; }
3202
+ .settings-page form > button { justify-self: start; width: fit-content; }
3203
+ .settings-page form:not(.editor-form) { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
3204
+ .settings-page .field { min-width: 0; }
3205
+ .settings-page textarea[name="allowedCidrs"] { min-height: 120px; height: 140px; }
3206
+ .settings-page .editor-form { gap: 12px; }
3207
+ .workspace-dropdown-content { display: grid; padding-left: 10px; border-left: 1px solid var(--line); margin: 4px 0 8px 10px; }
3208
+ .workspace-dropdown-content .nav-form { display: block; }
3209
+ .workspace-identity { overflow-wrap: anywhere; font-size: 12px; color: var(--faint); padding: 8px 10px; }
3210
+ .workspace-header .language-switcher { border: 0; margin: 12px 0 0; padding: 0; display: flex; flex-wrap: wrap; font-size: 12px; }
3211
+ .workspace-header .language-switcher a, .workspace-header .language-switcher .active { min-height: 38px; padding: 8px 10px; }
3212
+ .workspace-skip { position: absolute; left: 16px; top: -100px; z-index: 100; background: var(--panel); padding: 8px 16px; }
3213
+ .workspace-skip:focus { top: 8px; }
3214
+ :focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
3215
+ .dashboard-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
3216
+ .dashboard-heading h1 { font-size: 25px; font-weight: 650; line-height: 1.3; margin: 0; letter-spacing: -.025em; }
3217
+ .dashboard-heading nav { display: flex; align-items: center; gap: 16px; }
3218
+ .dashboard-heading .button-link { padding: 8px 14px; background: var(--accent); color: var(--accent-ink); border-radius: 6px; }
3219
+ .dashboard .filter-form { display: block; width: 100%; margin-bottom: 0; }
3220
+ .dashboard-search-row { display: flex; align-items: center; gap: 12px; }
3221
+ .dashboard-search { flex: 1; min-width: 0; }
3222
+ .dashboard-search input { width: 100%; min-width: 0; background: var(--panel); }
3223
+ .dashboard-clear { white-space: nowrap; padding: 8px 0; font-size: 13px; }
3224
+ .dashboard-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
3225
+ .dashboard-advanced, .dashboard-saved-views { margin: 0; border-bottom: 1px solid var(--line); }
3226
+ .dashboard-advanced > summary, .dashboard-saved-views > summary { padding: 10px 0; cursor: pointer; color: var(--muted); font-size: 13px; }
3227
+ .dashboard-filter-count { color: var(--faint); margin-left: 8px; font-size: 12px; }
3228
+ .dashboard-filter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; padding: 12px 0 20px; }
3229
+ .dashboard-field { display: grid; gap: 6px; min-width: 0; color: var(--muted); font-size: 12px; }
3230
+ .dashboard-field input, .dashboard-field select { min-width: 0; width: 100%; font-size: 14px; }
3231
+ .dashboard-filter-actions { grid-column: 1 / -1; }
3232
+ .dashboard-active-filters { display: flex; gap: 4px 16px; flex-wrap: wrap; font-size: 12px; color: var(--muted); margin: 10px 0; overflow-wrap: anywhere; }
3233
+ .dashboard-saved-content { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 8px 0 20px; }
3234
+ .dashboard-saved-content .empty { padding: 0; }
3235
+ .dashboard .saved-view-form { align-content: start; }
3236
+ .dashboard .saved-view-list { margin: 0; }
3237
+ .dashboard .saved-view-item { min-width: 0; }
3238
+ .artifact-overview { align-items: flex-start; }
3239
+ .artifact-details { min-width: 0; }
3240
+ .artifact-details > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
3241
+ .artifact-details > .meta-strip { padding: 12px 0 0; max-width: 100%; overflow-wrap: anywhere; }
3242
+ .dashboard-results { margin: 18px 0 10px; display: flex; justify-content: space-between; }
3243
+ .dashboard-results > span + span { color: var(--faint); font-size: 12px; }
3244
+ .dashboard .row-main strong { white-space: normal; overflow-wrap: anywhere; font-size: 15px; line-height: 1.45; font-weight: 600; }
3245
+ .dashboard .row-agent, .dashboard .row-formats { display: grid; gap: 2px; min-width: 0; font-size: 12px; color: var(--muted); }
3246
+ .dashboard .row-formats { display: flex; flex-wrap: wrap; gap: 4px 8px; }
3247
+ .dashboard .row-formats .badge { border: 0; padding: 0; background: transparent; color: var(--muted); font: inherit; border-radius: 0; }
3248
+ .dashboard .artifact-row .tag { border: 0; background: transparent; padding: 0; border-radius: 0; font-family: var(--sans); font-size: 12px; }
3249
+ .dashboard .artifact-row .tag + .tag::before { content: "·"; margin-right: 6px; color: var(--faint); }
3250
+ .dashboard .tags { min-width: 0; overflow-wrap: anywhere; }
3251
+ .dashboard .row-agent .publisher-cell { font-size: 12px; color: var(--faint); }
3252
+ .dashboard .artifact-row { grid-template-columns: minmax(180px, 1fr) minmax(100px, 150px) minmax(100px, 145px) minmax(80px, 150px); min-height: 76px; padding: 14px 16px; gap: 16px; }
3253
+ @media (min-width: 1100px) {
3254
+ body:has(> .workspace-header) { padding-left: 196px; }
3255
+ .workspace-header { position: fixed; inset: 0 auto 0 0; width: 196px; padding: 24px 12px; border-bottom: 0; border-right: 1px solid var(--line); display: block; overflow-y: auto; z-index: 20; }
3256
+ .workspace-brand { padding: 0 10px 26px; }
3257
+ .workspace-desktop { display: block; }
3258
+ .workspace-mobile { display: none; }
3259
+ }
3260
+ @media (max-width: 1099px) {
3261
+ .dashboard-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
3262
+ }
3263
+ @media (max-width: 899px) {
3264
+ .dashboard .artifact-row { grid-template-columns: auto minmax(0, 1fr); gap: 8px 12px; }
3265
+ .dashboard .row-main, .dashboard .tags { grid-column: 1 / -1; }
3266
+ .dashboard .row-agent { max-width: 42vw; }
3267
+ .dashboard .row-agent, .dashboard .row-formats { display: flex; gap: 8px; flex-wrap: wrap; }
3268
+ .dashboard .row-agent .publisher-cell { max-width: 100%; }
3269
+ .dashboard .tags { gap: 6px; }
3270
+ }
3271
+ @media (max-width: 640px) {
3272
+ .workspace-header { padding: 12px 16px; }
3273
+ .workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { min-height: 44px; }
3274
+ .dashboard-filter-grid, .dashboard-saved-content { grid-template-columns: 1fr; }
3275
+ .dashboard-heading h1 { font-size: 22px; }
3276
+ .dashboard-heading nav { gap: 12px; font-size: 13px; }
3277
+ .dashboard-search-row { gap: 8px; }
3278
+ .dashboard-search-row button { padding: 8px 12px; }
3279
+ .dashboard-advanced > summary, .dashboard-saved-views > summary { min-height: 44px; }
3280
+ .dashboard .artifact-row { padding: 14px; }
3281
+ .dashboard .row-main strong { font-size: 14px; }
3282
+ }
3189
3283
  </style>
3190
3284
  </head>
3191
3285
  <body>
@@ -3418,13 +3512,25 @@ function languageSwitcher(view) {
3418
3512
  return `<span class="language-switcher">${english}${korean}</span>`;
3419
3513
  }
3420
3514
 
3421
- function authNav(user, view) {
3422
- if (!user) {
3423
- return `<a href="/login">Sign in</a>`;
3424
- }
3425
- return `${user.role === "admin" ? `<a href="/admin/users">Users</a><a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>` : ""}<a href="/account">Account</a><a href="${view.href("/inbox")}">${view.text("inbox.title")}</a>`;
3515
+ function workspaceHeader(user, view) {
3516
+ const pathname = new URL(view.currentPath, "http://artifacty.local").pathname;
3517
+ const link = (href, key, active = pathname === href, className = "") => `<a href="${view.href(href)}"${active ? ' aria-current="page"' : ""}${className ? ` class="${className}"` : ""}>${view.text(key)}</a>`;
3518
+ const artifactActive = pathname === "/" || pathname.startsWith("/artifacts/");
3519
+ const adminLinks = user?.role === "admin" ? `<details class="workspace-dropdown"${pathname.startsWith("/admin/") ? " open" : ""}><summary${pathname.startsWith("/admin/") ? ' class="is-current"' : ""}>${view.text("nav.administration")}</summary><div class="workspace-dropdown-content">
3520
+ ${link("/admin/access", "access.title")}${link("/admin/users", "nav.users")}${link("/admin/backup", "nav.backup")}${link("/admin/retention", "retention.navLabel")}${link("/admin/webhooks", "nav.webhooks")}${link("/admin/update", "update.title")}<span class="workspace-version">Artifacty v${escapeHtml(APP_VERSION)}</span>
3521
+ </div></details>` : "";
3522
+ const account = user ? `<details class="workspace-dropdown"${pathname.startsWith("/account") ? " open" : ""}><summary${pathname.startsWith("/account") ? ' class="is-current"' : ""}>${view.text("nav.account")}</summary><div class="workspace-dropdown-content">
3523
+ <span class="workspace-identity">${escapeHtml(user.name || user.email || "")}</span>${link("/account", "nav.accountTokens")}${link("/account/password", "nav.password")}
3524
+ <form method="post" action="/logout" class="nav-form">${localeInput(view.locale)}<button type="submit">${view.text("nav.signOut")}</button></form>
3525
+ </div></details>` : user === null ? link("/login", "nav.signIn") : "";
3526
+ const links = `<div class="workspace-primary">${link("/", "nav.artifacts", artifactActive)}${user ? link("/inbox", "inbox.title") : ""}</div><div class="workspace-actions">${link("/new", "nav.new", pathname === "/new", "workspace-create")}${link("/import", "nav.import")}${adminLinks}${account}${link("/docs/api", "nav.apiDocs")}${languageSwitcher(view)}</div>`;
3527
+ return `<a class="workspace-skip" href="#main-content">${view.text("nav.skip")}</a><header class="workspace-header"><a class="workspace-brand" href="${view.href("/")}">${brandMark()}<span>artifacty</span></a>
3528
+ <nav class="workspace-desktop" aria-label="${view.attr("dashboard.navigation")}">${links}</nav>
3529
+ <details class="workspace-mobile"><summary>${view.text("dashboard.more")}</summary><nav aria-label="${view.attr("dashboard.navigation")}">${links}</nav></details>
3530
+ </header>`;
3426
3531
  }
3427
3532
 
3533
+
3428
3534
  export function escapeHtml(value) {
3429
3535
  return String(value)
3430
3536
  .replaceAll("&", "&amp;")