rshow 0.0.6 → 0.0.8

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.
Files changed (44) hide show
  1. package/dist/main.js +15002 -668
  2. package/package.json +2 -1
  3. package/skills/rshow/SKILL.md +13 -4
  4. package/skills/rshow/reference/cli.md +8 -6
  5. package/skills/rshow/reference/dashboard.md +5 -5
  6. package/skills/rshow/reference/page-contract.md +2 -1
  7. package/skills/rshow/reference/pages.md +7 -7
  8. package/skills/rshow/reference/poster.md +5 -5
  9. package/skills/rshow/reference/project-page.md +5 -5
  10. package/skills/rshow/reference/reports.md +10 -6
  11. package/skills/rshow/reference/sketch.md +5 -5
  12. package/skills/rshow/reference/terminal.md +5 -5
  13. package/skills/rshow-slides/SKILL.md +11 -6
  14. package/skills/rshow-slides/reference/layouts.md +15 -0
  15. package/themes/default/deck.js +86 -4
  16. package/themes/default/example/index.html +19 -2
  17. package/themes/default/example/report.html +17 -10
  18. package/themes/default/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  19. package/themes/default/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  20. package/themes/default/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  21. package/themes/default/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  22. package/themes/default/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  23. package/themes/default/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  24. package/themes/default/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  25. package/themes/default/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  26. package/themes/default/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  27. package/themes/default/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  28. package/themes/default/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  29. package/themes/default/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  30. package/themes/default/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  31. package/themes/default/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  32. package/themes/default/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  33. package/themes/default/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  34. package/themes/default/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  35. package/themes/default/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  36. package/themes/default/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  37. package/themes/default/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  38. package/themes/default/fonts/katex/LICENSE +21 -0
  39. package/themes/default/math.css +46 -0
  40. package/themes/default/math.js +58 -0
  41. package/themes/default/report.css +15 -6
  42. package/themes/default/theme.json +7 -0
  43. package/worker/index.js +118 -46
  44. package/worker/migrations/0005_version_counters.sql +3 -0
@@ -388,24 +388,30 @@ tbody tr:last-child td {
388
388
  font-variant-numeric: tabular-nums;
389
389
  }
390
390
 
391
- /* An equation is one line of mono with its number at the right end of the text
392
- column, so the number lines up with the right edge of the prose. The box
393
- itself stays in the body face, which is what makes --measure resolve to the
394
- same width here as in a paragraph. */
391
+ /* An equation is a .math box in KaTeX's own face with its number at the right
392
+ end of the text column, so the number lines up with the right edge of the
393
+ prose. A tall equation centres its number instead of sticking to the first
394
+ line. The box itself stays in the body face, which is what makes --measure
395
+ resolve to the same width here as in a paragraph. */
395
396
  .equation {
396
397
  display: grid;
397
398
  grid-template-columns: minmax(0, 1fr) auto;
398
- align-items: baseline;
399
+ align-items: center;
399
400
  gap: var(--space-05);
400
401
  margin: var(--space-06) 0;
401
402
  line-height: 1.5;
402
403
  }
403
404
 
404
- .equation > span:not(.eq-num) {
405
+ .equation > span:not(.eq-num):not(.math) {
405
406
  font-family: var(--font-mono);
406
407
  font-size: 0.95em;
407
408
  }
408
409
 
410
+ .equation > .math {
411
+ margin: 0;
412
+ min-width: 0;
413
+ }
414
+
409
415
  .eq-num {
410
416
  color: var(--muted);
411
417
  font-variant-numeric: tabular-nums;
@@ -539,6 +545,9 @@ tbody tr:last-child td {
539
545
 
540
546
  .refs li {
541
547
  margin: 0 0 var(--space-04);
548
+ /* A cited address is longer than a phone is wide and has no space to break
549
+ at, so it wraps mid path rather than widening the page. */
550
+ overflow-wrap: anywhere;
542
551
  }
543
552
 
544
553
  .refs li::marker {
@@ -55,6 +55,13 @@
55
55
  "example": "example/tui.html"
56
56
  }
57
57
  },
58
+ "addons": {
59
+ "math": {
60
+ "css": ["math.css"],
61
+ "js": ["math.js"],
62
+ "example": "example/report.html"
63
+ }
64
+ },
58
65
  "tokens": {
59
66
  "--font-sans": "\"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", sans-serif",
60
67
  "--font-mono": "\"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
package/worker/index.js CHANGED
@@ -2703,6 +2703,7 @@ var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
2703
2703
  var TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
2704
2704
  var THEME_NAME_PATTERN = /^[a-z0-9-]{1,40}$/;
2705
2705
  var THEME_VERSION_PATTERN = /^([a-z0-9-]{1,40})@([0-9]{1,9})$/;
2706
+ var THEME_LATEST_PATTERN = /^([a-z0-9-]{1,40})@latest$/;
2706
2707
  var THEME_JSON_PATTERN = /^([a-z0-9-]{1,40})\.json$/;
2707
2708
  var DATA_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
2708
2709
  var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -3002,13 +3003,13 @@ function jsonError(message2, status) {
3002
3003
  return json({ error: message2 }, status);
3003
3004
  }
3004
3005
  __name(jsonError, "jsonError");
3005
- function redirect(location, status = 301) {
3006
- return new Response(null, { status, headers: { location, "cache-control": "no-store" } });
3006
+ function redirect(location, status = 301, cacheControl = "no-store") {
3007
+ return new Response(null, { status, headers: { location, "cache-control": cacheControl } });
3007
3008
  }
3008
3009
  __name(redirect, "redirect");
3009
3010
 
3010
3011
  // src/db.ts
3011
- var ARTIFACT_COLUMNS = "slug, title, owner, mode, pinned, latest, project, created_at, updated_at";
3012
+ var ARTIFACT_COLUMNS = "slug, title, owner, mode, pinned, latest, last_n, version_gen, project, created_at, updated_at";
3012
3013
  var PROJECT_COLUMNS = "slug, title, description, owner, created_at, updated_at";
3013
3014
  async function getArtifact(env, slug) {
3014
3015
  return await env.DB.prepare(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE slug = ?1`).bind(slug).first();
@@ -3048,6 +3049,11 @@ async function listVersions(env, slug) {
3048
3049
  return result.results ?? [];
3049
3050
  }
3050
3051
  __name(listVersions, "listVersions");
3052
+ async function countVersions(env, slug) {
3053
+ const row = await env.DB.prepare("SELECT COUNT(*) AS total FROM versions WHERE slug = ?1").bind(slug).first();
3054
+ return row?.total ?? 0;
3055
+ }
3056
+ __name(countVersions, "countVersions");
3051
3057
  async function sharesForEmail(env, email) {
3052
3058
  const result = await env.DB.prepare("SELECT slug, email, role FROM shares WHERE email = ?1").bind(email).all();
3053
3059
  const map = /* @__PURE__ */ new Map();
@@ -3280,12 +3286,12 @@ var MANIFEST_CACHE_CONTROL = "public, max-age=31536000, immutable";
3280
3286
  var VERSION_CACHE_CONTROL = "public, max-age=31536000, immutable";
3281
3287
  var JSON_CONTENT_TYPE = "application/json";
3282
3288
  var BLOB_CACHE_CONTROL = "public, max-age=31536000, immutable";
3283
- var ARTIFACT_COLUMNS2 = "slug, title, owner, mode, pinned, latest, project, created_at, updated_at";
3289
+ var ARTIFACT_COLUMNS2 = "slug, title, owner, mode, pinned, latest, last_n, version_gen, project, created_at, updated_at";
3284
3290
  var VERSION_COLUMNS = "slug, n, label, manifest_key, files, bytes, created_by, created_at";
3285
3291
  var accessMemory = /* @__PURE__ */ new Map();
3286
3292
  var manifestMemory = /* @__PURE__ */ new Map();
3287
3293
  var versionMemory = /* @__PURE__ */ new Map();
3288
- var versionNumbers = /* @__PURE__ */ new Map();
3294
+ var versionKeys = /* @__PURE__ */ new Map();
3289
3295
  var trackedKeys = /* @__PURE__ */ new Set();
3290
3296
  function accessCacheKey(slug) {
3291
3297
  return `${CACHE_ORIGIN}/access/${slug}`;
@@ -3299,12 +3305,12 @@ function blobCacheKey(hash) {
3299
3305
  return `${CACHE_ORIGIN}/blob/${hash}`;
3300
3306
  }
3301
3307
  __name(blobCacheKey, "blobCacheKey");
3302
- function versionCacheKey(slug, n) {
3303
- return `${CACHE_ORIGIN}/version/${slug}/${n}`;
3308
+ function versionCacheKey(slug, n, gen) {
3309
+ return `${CACHE_ORIGIN}/version/${slug}/${n}/${gen}`;
3304
3310
  }
3305
3311
  __name(versionCacheKey, "versionCacheKey");
3306
- function versionMemoryKey(slug, n) {
3307
- return `${slug}/${n}`;
3312
+ function versionMemoryKey(slug, n, gen) {
3313
+ return `${slug}/${n}/${gen}`;
3308
3314
  }
3309
3315
  __name(versionMemoryKey, "versionMemoryKey");
3310
3316
  function rememberAccess(slug, value, expires) {
@@ -3327,8 +3333,8 @@ function rememberManifest(key, value) {
3327
3333
  }
3328
3334
  }
3329
3335
  __name(rememberManifest, "rememberManifest");
3330
- function rememberVersion(slug, n, row) {
3331
- const key = versionMemoryKey(slug, n);
3336
+ function rememberVersion(slug, n, gen, row) {
3337
+ const key = versionMemoryKey(slug, n, gen);
3332
3338
  versionMemory.delete(key);
3333
3339
  versionMemory.set(key, row);
3334
3340
  while (versionMemory.size > VERSION_MAX_ENTRIES) {
@@ -3336,9 +3342,9 @@ function rememberVersion(slug, n, row) {
3336
3342
  if (oldest === void 0) break;
3337
3343
  versionMemory.delete(oldest);
3338
3344
  }
3339
- const numbers = versionNumbers.get(slug) ?? /* @__PURE__ */ new Set();
3340
- numbers.add(n);
3341
- versionNumbers.set(slug, numbers);
3345
+ const keys = versionKeys.get(slug) ?? /* @__PURE__ */ new Set();
3346
+ keys.add(versionCacheKey(slug, n, gen));
3347
+ versionKeys.set(slug, keys);
3342
3348
  }
3343
3349
  __name(rememberVersion, "rememberVersion");
3344
3350
  function decodeVersion(text) {
@@ -3487,34 +3493,38 @@ async function invalidateProjectAccess(env, project) {
3487
3493
  }
3488
3494
  }
3489
3495
  __name(invalidateProjectAccess, "invalidateProjectAccess");
3490
- async function versionFor(env, slug, n) {
3491
- const memory = versionMemory.get(versionMemoryKey(slug, n));
3496
+ async function versionFor(env, artifact, n) {
3497
+ const gen = artifact.version_gen;
3498
+ const memory = versionMemory.get(versionMemoryKey(artifact.slug, n, gen));
3492
3499
  if (memory) {
3493
- rememberVersion(slug, n, memory);
3500
+ rememberVersion(artifact.slug, n, gen, memory);
3494
3501
  return memory;
3495
3502
  }
3496
- const cached = await cacheGet(versionCacheKey(slug, n));
3503
+ const key = versionCacheKey(artifact.slug, n, gen);
3504
+ const cached = await cacheGet(key);
3497
3505
  if (cached) {
3498
3506
  const row2 = decodeVersion(await cached.text());
3499
3507
  if (row2) {
3500
- rememberVersion(slug, n, row2);
3508
+ rememberVersion(artifact.slug, n, gen, row2);
3501
3509
  return row2;
3502
3510
  }
3503
3511
  }
3504
- const row = await getVersion(env, slug, n);
3512
+ const row = await getVersion(env, artifact.slug, n);
3505
3513
  if (!row) return null;
3506
- rememberVersion(slug, n, row);
3507
- await cachePut(versionCacheKey(slug, n), JSON.stringify(row), VERSION_CACHE_CONTROL);
3514
+ rememberVersion(artifact.slug, n, gen, row);
3515
+ await cachePut(key, JSON.stringify(row), VERSION_CACHE_CONTROL);
3508
3516
  return row;
3509
3517
  }
3510
3518
  __name(versionFor, "versionFor");
3511
3519
  async function invalidateVersions(slug) {
3512
- const numbers = versionNumbers.get(slug);
3513
- versionNumbers.delete(slug);
3514
- if (!numbers) return;
3515
- for (const n of numbers) {
3516
- versionMemory.delete(versionMemoryKey(slug, n));
3517
- const key = versionCacheKey(slug, n);
3520
+ const prefix = `${slug}/`;
3521
+ for (const key of [...versionMemory.keys()]) {
3522
+ if (key.startsWith(prefix)) versionMemory.delete(key);
3523
+ }
3524
+ const keys = versionKeys.get(slug);
3525
+ versionKeys.delete(slug);
3526
+ if (!keys) return;
3527
+ for (const key of keys) {
3518
3528
  trackedKeys.delete(key);
3519
3529
  try {
3520
3530
  await caches.default.delete(key);
@@ -3796,7 +3806,7 @@ async function storeVersion(env, artifact, input2) {
3796
3806
  env.DB.prepare(
3797
3807
  "INSERT INTO versions (slug, n, label, manifest_key, files, bytes, created_by, created_at) SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 WHERE (SELECT latest FROM artifacts WHERE slug = ?1) IS ?9"
3798
3808
  ).bind(artifact.slug, input2.n, input2.label, key, input2.files, input2.bytes, input2.createdBy, input2.now, input2.base),
3799
- env.DB.prepare("UPDATE artifacts SET latest = ?2, updated_at = ?3 WHERE slug = ?1 AND latest IS ?4").bind(
3809
+ env.DB.prepare("UPDATE artifacts SET latest = ?2, last_n = ?2, updated_at = ?3 WHERE slug = ?1 AND latest IS ?4").bind(
3800
3810
  artifact.slug,
3801
3811
  input2.n,
3802
3812
  input2.now,
@@ -3841,6 +3851,52 @@ function registerVersions(app) {
3841
3851
  if (!manifest) return jsonError("no such version", 404);
3842
3852
  return json(toVersionDetail(c.env, access.artifact.slug, version, manifest));
3843
3853
  });
3854
+ app.delete("/api/artifacts/:slug/versions/:n", async (c) => {
3855
+ const email = c.get("identity").email;
3856
+ const access = await loadAccessible(c.env, c.req.param("slug"), email);
3857
+ if (!access) return jsonError("not found", 404);
3858
+ if (!canManage(access.artifact, access.shares, actorOf(c), access.project)) {
3859
+ return jsonError("owner access is required", 403);
3860
+ }
3861
+ const raw2 = c.req.param("n");
3862
+ if (!/^[1-9][0-9]{0,8}$/.test(raw2)) return jsonError("no such version", 404);
3863
+ const n = Number(raw2);
3864
+ const slug = access.artifact.slug;
3865
+ if (slug.startsWith(THEME_PREFIX) && c.req.query("force") !== "1") {
3866
+ return jsonError("pages link to theme versions; pass force to delete it", 409);
3867
+ }
3868
+ const version = await getVersion(c.env, slug, n);
3869
+ if (!version) return jsonError("no such version", 404);
3870
+ if (access.artifact.pinned === n) {
3871
+ return jsonError(`version ${n} is pinned; pin another version first`, 409);
3872
+ }
3873
+ if (await countVersions(c.env, slug) <= 1) {
3874
+ return jsonError("this is the only version; delete the artifact instead", 409);
3875
+ }
3876
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3877
+ let deletedChanges = 0;
3878
+ try {
3879
+ const [deleted] = await c.env.DB.batch([
3880
+ c.env.DB.prepare(
3881
+ "DELETE FROM versions WHERE slug = ?1 AND n = ?2 AND (SELECT pinned FROM artifacts WHERE slug = ?1) IS NOT ?2 AND (SELECT COUNT(*) FROM versions WHERE slug = ?1) > 1"
3882
+ ).bind(slug, n),
3883
+ c.env.DB.prepare(
3884
+ "UPDATE artifacts SET latest = (SELECT MAX(n) FROM versions WHERE slug = ?1), version_gen = version_gen + 1, updated_at = ?2 WHERE slug = ?1"
3885
+ ).bind(slug, now)
3886
+ ]);
3887
+ deletedChanges = deleted?.meta?.changes ?? 0;
3888
+ } catch {
3889
+ return jsonError("could not delete the version", 500);
3890
+ }
3891
+ if (deletedChanges !== 1) return jsonError("another change came first, retry", 409);
3892
+ await discardManifest(c.env, version.manifest_key);
3893
+ await invalidateVersions(slug);
3894
+ await invalidateAccess(slug);
3895
+ const updated = await getArtifact(c.env, slug);
3896
+ if (!updated) return jsonError("not found", 404);
3897
+ const tags = await getArtifactTags(c.env, slug);
3898
+ return json(toArtifact(c.env, updated, access.role, tags));
3899
+ });
3844
3900
  app.post("/api/artifacts/:slug/versions", async (c) => {
3845
3901
  const body = await readBody(c);
3846
3902
  if (!body.ok) return body.response;
@@ -3882,7 +3938,7 @@ function registerVersions(app) {
3882
3938
  }
3883
3939
  const label = typeof body.body.label === "string" ? body.body.label.trim().slice(0, 200) : "";
3884
3940
  const base = access.artifact.latest;
3885
- const n = (base ?? 0) + 1;
3941
+ const n = Math.max(access.artifact.last_n, base ?? 0) + 1;
3886
3942
  const now = (/* @__PURE__ */ new Date()).toISOString();
3887
3943
  const stored = await storeVersion(c.env, access.artifact, {
3888
3944
  label,
@@ -3999,6 +4055,8 @@ function registerArtifactCreate(app) {
3999
4055
  mode: MODE.private,
4000
4056
  pinned: null,
4001
4057
  latest: null,
4058
+ last_n: 0,
4059
+ version_gen: 0,
4002
4060
  project,
4003
4061
  created_at: now,
4004
4062
  updated_at: now
@@ -4138,6 +4196,9 @@ function registerArtifactItem(app) {
4138
4196
  if (!canManage(access.artifact, access.shares, actorOf(c), access.project)) {
4139
4197
  return jsonError("owner access is required", 403);
4140
4198
  }
4199
+ if (access.artifact.slug.startsWith(THEME_PREFIX) && c.req.query("force") !== "1") {
4200
+ return jsonError("pages link to this theme; pass force to delete it", 409);
4201
+ }
4141
4202
  const prefix = `manifests/${access.artifact.slug}/`;
4142
4203
  const keys = [];
4143
4204
  let cursor;
@@ -4498,7 +4559,7 @@ __name(registerApi, "registerApi");
4498
4559
 
4499
4560
  // src/static.generated.ts
4500
4561
  var APP_CSS = '/* IBM Carbon Design System v11, g10 theme (light) and g100 theme (dark) */\n:root{--bg:#f4f4f4;--layer-01:#ffffff;--layer-02:#f4f4f4;--layer-hover:#e8e8e8;--layer-active:#d1d1d1;--border-subtle:#e0e0e0;--border-strong:#8d8d8d;--text-primary:#161616;--text-secondary:#525252;--link:#0f62fe;--link-hover:#0043ce;--focus:#0f62fe;--support-error:#da1e28;--support-success:#24a148;--error-bg:#fff1f1;--tag-gray:#e0e0e0;--tag-blue:#d0e2ff;--tag-blue-ink:#0043ce;--tag-teal:#9ef0f0;--tag-teal-ink:#004144;--btn-primary:#0f62fe;--btn-primary-hover:#0043ce;--btn-primary-ink:#ffffff;--btn-secondary:#393939;--btn-secondary-hover:#4c4c4c;--tag-live-bg:#a7f0ba;--tag-live-ink:#044317;--code-bg:#161616;--code-ink:#f4f4f4;--shell:#161616;--shell-fg:#f4f4f4;--shell-muted:#c6c6c6;--shell-field:#393939;--nav-selected:#e8e8e8;--segment-on-bg:#161616;--segment-on-fg:#ffffff;--font-sans:"IBM Plex Sans",system-ui,-apple-system,sans-serif;--font-mono:"IBM Plex Mono",ui-monospace,monospace}\n@media (prefers-color-scheme: dark){:root:not([data-mode="light"]){--bg:#161616;--layer-01:#262626;--layer-02:#393939;--layer-hover:#333333;--layer-active:#525252;--border-subtle:#393939;--border-strong:#6f6f6f;--text-primary:#f4f4f4;--text-secondary:#c6c6c6;--link:#78a9ff;--link-hover:#a6c8ff;--focus:#ffffff;--support-error:#fa4d56;--support-success:#42be65;--error-bg:#2d0709;--tag-gray:#393939;--tag-blue:#002d9c;--tag-blue-ink:#d0e2ff;--tag-teal:#004144;--tag-teal-ink:#9ef0f0;--btn-primary:#0f62fe;--btn-primary-hover:#0353e9;--btn-primary-ink:#ffffff;--btn-secondary:#6f6f6f;--btn-secondary-hover:#525252;--tag-live-bg:#044317;--tag-live-ink:#a7f0ba;--code-bg:#262626;--code-ink:#f4f4f4;--shell:#161616;--shell-fg:#f4f4f4;--shell-muted:#c6c6c6;--shell-field:#262626;--nav-selected:#393939;--segment-on-bg:#f4f4f4;--segment-on-fg:#161616}}\n:root[data-mode="dark"]{--bg:#161616;--layer-01:#262626;--layer-02:#393939;--layer-hover:#333333;--layer-active:#525252;--border-subtle:#393939;--border-strong:#6f6f6f;--text-primary:#f4f4f4;--text-secondary:#c6c6c6;--link:#78a9ff;--link-hover:#a6c8ff;--focus:#ffffff;--support-error:#fa4d56;--support-success:#42be65;--error-bg:#2d0709;--tag-gray:#393939;--tag-blue:#002d9c;--tag-blue-ink:#d0e2ff;--tag-teal:#004144;--tag-teal-ink:#9ef0f0;--btn-primary:#0f62fe;--btn-primary-hover:#0353e9;--btn-primary-ink:#ffffff;--btn-secondary:#6f6f6f;--btn-secondary-hover:#525252;--tag-live-bg:#044317;--tag-live-ink:#a7f0ba;--code-bg:#262626;--code-ink:#f4f4f4;--shell:#161616;--shell-fg:#f4f4f4;--shell-muted:#c6c6c6;--shell-field:#262626;--nav-selected:#393939;--segment-on-bg:#f4f4f4;--segment-on-fg:#161616}\n*{box-sizing:border-box}\nhtml,body{margin:0;padding:0}\nbody{background:var(--bg);color:var(--text-primary);font:400 14px/1.43 var(--font-sans);-webkit-font-smoothing:antialiased}\na{color:var(--link);text-decoration:none}\na:hover{color:var(--link-hover);text-decoration:underline}\na:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible,.segment:focus-within{outline:2px solid var(--focus);outline-offset:-2px}\nh1,h2,h3,p{margin:0}\nh1{font-size:28px;font-weight:400;line-height:1.29;letter-spacing:0}\nh2{font-size:16px;font-weight:600;line-height:1.375;letter-spacing:0}\n.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.muted{color:var(--text-secondary)}\n.mono{font-family:var(--font-mono);font-size:12px;line-height:1.33;overflow-wrap:anywhere}\n.empty{padding:24px 16px;margin:0;color:var(--text-secondary)}\n.app{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);color:var(--text-primary)}\n.topbar{height:48px;flex:0 0 auto;display:flex;align-items:center;gap:16px;padding:0 16px;background:var(--shell);color:var(--shell-fg)}\n.brand{display:inline-flex;align-items:baseline;gap:8px;color:var(--shell-fg);font-size:14px;font-weight:600;white-space:nowrap}\n.brand:hover{color:var(--shell-fg);text-decoration:none}\n.brand-sub{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--shell-muted)}\n.search{flex:1 1 240px;min-width:0;max-width:520px;height:32px;display:flex;align-items:center;gap:8px;padding:0 12px;background:var(--shell-field);color:var(--shell-fg);margin-left:8px}\n.search-icon{flex:0 0 auto;color:var(--shell-muted);font-size:16px;line-height:1}\n.search input{flex:1;min-width:0;height:32px;padding:0;border:0;background:transparent;color:var(--shell-fg);font:400 14px/1.29 var(--font-sans)}\n.search input::placeholder{color:var(--shell-muted)}\n.search input:focus{outline:2px solid var(--focus);outline-offset:-2px}\n.top-spacer{flex:1}\n.topbar-user{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:12px;color:var(--shell-muted)}\nbutton.mode-toggle{height:48px;padding:0 12px;display:inline-flex;align-items:center;gap:8px;color:var(--shell-fg);background:transparent;border:0;border-radius:0;white-space:nowrap}\nbutton.mode-toggle:hover{background:var(--shell-field);color:var(--shell-fg)}\n.shell{flex:1;display:flex;align-items:stretch;min-height:0}\n.sidenav{flex:0 0 240px;width:240px;padding:16px 0;background:var(--layer-01);border-right:1px solid var(--border-subtle)}\n.nav-item{display:flex;align-items:center;gap:8px;min-height:40px;padding:8px 16px;border-left:3px solid transparent;color:var(--text-primary);font-size:14px;line-height:1.29}\n.nav-item:hover{background:var(--layer-hover);color:var(--text-primary);text-decoration:none}\n.nav-item.is-active{background:var(--nav-selected);border-left-color:var(--link);font-weight:600}\n.nav-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.nav-count{margin-left:auto;color:var(--text-secondary);font-weight:400;font-family:var(--font-mono);font-size:12px}\n.nav-heading{margin:24px 16px 8px;font-size:12px;line-height:1.33;color:var(--text-secondary);letter-spacing:.32px}\n.main{flex:999 1 640px;min-width:0;padding:32px}\n.page{width:100%;max-width:1376px;margin:0 auto;padding:24px 32px 48px}\n.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:24px}\n.detail-head{align-items:flex-start}\n.detail-head h1,.signin-card h1{font-size:32px;font-weight:300;line-height:1.25}\n.title-block{min-width:0;flex:1 1 480px;overflow-wrap:anywhere}\n.page-sub{margin:4px 0 0;color:var(--text-secondary);max-width:80ch}\n.project-description{margin-top:12px;color:var(--text-primary);font-size:16px;line-height:1.5}\n.meta-line{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:8px 0 0;color:var(--text-secondary);font-size:14px;overflow-wrap:anywhere}\n.meta-line a{color:var(--text-primary)}\n.meta-line a.mono:hover{color:var(--link);text-decoration:none}\n.meta-line .pill{vertical-align:middle}\n.breadcrumbs{margin:0 0 12px;font-size:14px;line-height:1.29;color:var(--text-secondary);overflow-wrap:anywhere}\n.breadcrumbs a{color:var(--link)}\n.crumb-sep{margin:0 4px;color:var(--text-secondary)}\n.button-row{display:flex;align-items:center;gap:1px;flex-wrap:wrap}\n.btn{display:inline-flex;align-items:center;justify-content:center;height:48px;padding:0 16px;border:1px solid transparent;border-radius:0;background:var(--btn-secondary);color:#fff;font:400 14px/1.29 var(--font-sans);white-space:nowrap;cursor:pointer}\n.btn:hover{background:var(--btn-secondary-hover);color:#fff;text-decoration:none}\n.btn-primary{background:var(--btn-primary);color:var(--btn-primary-ink)}\n.btn-primary:hover{background:var(--btn-primary-hover);color:var(--btn-primary-ink)}\n.btn-tertiary{background:transparent;border-color:var(--border-strong);color:var(--text-primary)}\n.btn-tertiary:hover{background:var(--layer-hover);color:var(--text-primary)}\n.btn-danger{background:transparent;border-color:var(--support-error);color:var(--support-error)}\n.btn-danger:hover{background:var(--error-bg);color:var(--support-error)}\n.btn-sm{height:40px;padding:0 12px}\n.btn-link{display:inline-flex;align-items:center;height:32px;padding:0 8px;border:0;background:transparent;color:var(--link);font:400 14px/1.29 var(--font-sans);cursor:pointer}\n.btn-link:hover{background:var(--layer-hover);color:var(--link-hover);text-decoration:none}\nbutton{height:40px;padding:0 16px;border:1px solid transparent;border-radius:0;background:var(--btn-secondary);color:#fff;font:400 14px/1.29 var(--font-sans);cursor:pointer}\nbutton:hover{background:var(--btn-secondary-hover)}\nbutton.primary{background:var(--btn-primary);color:var(--btn-primary-ink)}\nbutton.primary:hover{background:var(--btn-primary-hover)}\nbutton.small{height:40px;padding:0 12px;border-color:transparent;background:transparent;color:var(--link)}\nbutton.small:hover{background:var(--layer-hover);color:var(--link-hover)}\nbutton.small.danger{color:var(--support-error)}\nbutton.danger{background:transparent;border-color:transparent;color:var(--support-error)}\nbutton.danger:hover{background:var(--error-bg);color:var(--support-error)}\n.icon-btn{width:40px;height:40px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;color:var(--text-secondary);font-size:20px;line-height:1}\n.icon-btn:hover{background:var(--layer-hover);color:var(--text-primary)}\n.tagbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:16px;min-height:32px}\n.tagbar-label{margin-right:4px;color:var(--text-secondary);font-size:14px}\n.tagbar-spacer{flex:1}\n.tag-chip{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:16px;border:0;background:var(--tag-gray);color:var(--text-primary);font:400 12px/1.33 var(--font-sans);text-decoration:none;white-space:nowrap;cursor:pointer}\n.tag-chip:hover{background:var(--layer-active);color:var(--text-primary);text-decoration:none}\n.tag-chip.is-on{background:var(--btn-primary);color:var(--btn-primary-ink)}\n.tag-count{font-family:var(--font-mono);opacity:.72}\n.tag-chip-static{cursor:default;padding-right:6px}\n.tag-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-height:32px}\n.tag-remove{width:20px;height:20px;padding:0;display:inline-flex;align-items:center;justify-content:center;border:0;background:transparent;color:inherit;font-size:16px;line-height:1}\n.tag-remove:hover{background:var(--layer-active);color:var(--text-primary)}\n.sharing-filter{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.inline-label{display:inline-flex;align-items:center;gap:8px;color:var(--text-secondary);font-size:14px;white-space:nowrap}\ninput,select,textarea{height:40px;padding:0 12px;border:0;border-bottom:1px solid var(--border-strong);border-radius:0;background:var(--layer-02);color:var(--text-primary);font:400 14px/1.29 var(--font-sans)}\nselect{padding-right:32px;cursor:pointer}\ntextarea{height:auto;min-height:80px;padding:8px 12px;resize:vertical}\ninput::placeholder,textarea::placeholder{color:var(--text-secondary)}\ninput:focus,select:focus,textarea:focus{outline:2px solid var(--focus);outline-offset:-2px}\ninput:disabled,select:disabled,textarea:disabled{color:var(--text-secondary);cursor:not-allowed}\n.field-row{display:flex;align-items:stretch;gap:1px;flex-wrap:wrap}\n.field-row input,.field-row select{flex:1 1 160px;min-width:0}\n.field-row .btn{height:40px}\n.field-label{display:block;margin-bottom:8px;color:var(--text-secondary);font-size:12px;line-height:1.33;letter-spacing:.32px}\n.field-hint{color:var(--text-secondary);font-size:12px;line-height:1.33}\n.form-stack{display:flex;flex-direction:column;gap:24px}\n.form-field{display:flex;flex-direction:column;gap:8px;max-width:640px}\n.form-field input,.form-field textarea{width:100%}\n.form-card{max-width:720px}\n.form-actions{display:flex;gap:1px;flex-wrap:wrap}\n.detail-block{display:flex;flex-direction:column;gap:8px;margin-bottom:24px}\n.detail-block:last-child{margin-bottom:0}\n.tag-form{margin-top:12px;max-width:420px}\n.table-box{width:100%;overflow-x:auto;background:var(--layer-01);border-radius:0}\n.data-table{width:100%;border-collapse:collapse;font-size:14px;line-height:1.29}\n.gallery-table{min-width:860px}\n.project-table{min-width:620px}\n.token-table{min-width:640px}\n.data-table th,.data-table td{height:48px;padding:0 16px;text-align:left;vertical-align:middle;white-space:nowrap;border-bottom:1px solid var(--border-subtle)}\n.data-table th{background:var(--border-subtle);color:var(--text-primary);font-weight:600}\n.data-table tbody tr:hover td{background:var(--layer-hover)}\n.data-table tr:last-child td{border-bottom:0}\n.cell-name{min-width:220px}\n.cell-name .row-title{display:block;color:var(--text-primary);font-weight:400;max-width:360px;overflow:hidden;text-overflow:ellipsis}\n.cell-name .row-title:hover{color:var(--link);text-decoration:none}\n.cell-name .slug-link{display:block;margin-top:2px;color:var(--text-secondary)}\n.cell-name .slug-link:hover{color:var(--link);text-decoration:none}\n.empty-row td{height:auto;padding:32px 16px;white-space:normal;color:var(--text-secondary);text-align:center}\n.pill{display:inline-flex;align-items:center;height:24px;padding:0 8px;border-radius:12px;background:var(--tag-gray);color:var(--text-primary);font-size:12px;font-weight:400;line-height:1;white-space:nowrap}\n.pill-private{background:var(--tag-gray);color:var(--text-primary)}\n.pill-people{background:var(--tag-blue);color:var(--tag-blue-ink)}\n.pill-public{background:var(--tag-teal);color:var(--tag-teal-ink)}\n.pill-live{background:var(--tag-live-bg);color:var(--tag-live-ink)}\na.pill:hover{text-decoration:none;color:var(--text-primary)}\n.panel{margin-bottom:24px;background:var(--layer-01);border-radius:0}\n.panel-head{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;min-height:48px;padding:8px 16px;border-bottom:1px solid var(--border-subtle)}\n.panel-head h2{font-size:16px;font-weight:600}\n.panel-head .muted{font-size:14px}\n.panel-body{padding:16px}\n.detail-layout{display:flex;align-items:flex-start;gap:24px;flex-wrap:wrap}\n.detail-main{flex:999 1 640px;min-width:0}\n.detail-side{flex:1 1 380px;min-width:0;display:flex;flex-direction:column;gap:20px;padding:16px;background:var(--layer-01)}\n.detail-side h2{font-size:16px;font-weight:600}\n.side-text{margin:4px 0 0;color:var(--text-secondary);line-height:1.43}\n.side-hint{margin:4px 0 0;color:var(--text-secondary);font-size:12px;line-height:1.33}\n.side-section{display:flex;flex-direction:column;gap:12px}\n.preview-picker{display:inline-flex;align-items:center;gap:8px}\n.preview-body{padding:16px}\n.preview-frame{display:block;width:100%;aspect-ratio:16/9;border:1px solid var(--border-subtle);background:#ffffff}\n.preview-empty{display:flex;align-items:center;justify-content:center;min-height:200px;padding:24px;border:1px solid var(--border-subtle);background:var(--layer-02);color:var(--text-secondary);text-align:center}\n.version-list{list-style:none;margin:0;padding:0}\n.version-row{display:flex;align-items:center;gap:16px;flex-wrap:wrap;min-height:56px;padding:8px 16px;border-bottom:1px solid var(--border-subtle)}\n.version-row:last-child{border-bottom:0}\n.version-row.is-served{background:var(--layer-02);border-left:3px solid var(--support-success);padding-left:13px}\n.version-n{flex:0 0 36px}\n.version-main{flex:1 1 220px;min-width:0}\n.version-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.version-meta{color:var(--text-secondary);font-size:12px;line-height:1.33}\n.version-actions{padding:12px 16px;border-top:1px solid var(--border-subtle)}\n.segmented{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin:0;background:var(--border-strong)}\n.segment{position:relative;display:flex;align-items:center;justify-content:center;height:40px;background:var(--layer-01);color:var(--text-primary);font-size:14px;cursor:pointer}\n.segment:hover{background:var(--layer-hover)}\n.segment.is-on{background:var(--segment-on-bg);color:var(--segment-on-fg)}\n.segment-input{position:absolute;width:1px;height:1px;padding:0;margin:0;overflow:hidden;clip:rect(0,0,0,0);border:0;opacity:0}\n.people-list{list-style:none;margin:0;padding:0}\n.person-row{display:flex;align-items:center;gap:8px;min-height:48px;padding:4px 0;border-bottom:1px solid var(--border-subtle)}\n.person-row:last-child{border-bottom:0}\n.person-email{flex:1;min-width:0;overflow-wrap:anywhere}\n.person-role{color:var(--text-secondary);font-size:12px;white-space:nowrap}\n.info-box{padding:12px 16px;background:var(--layer-02)}\n.info-title{font-weight:600}\n.info-text{margin-top:4px;color:var(--text-secondary);font-size:14px}\n.info-box a{display:inline-block;margin-top:8px}\n.danger-zone{padding-top:16px;border-top:1px solid var(--border-subtle)}\n.danger-zone h2{margin-bottom:8px;font-size:14px;font-weight:600;color:var(--support-error)}\n.theme-list{list-style:none;margin:0;padding:0}\n.theme-row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:48px;padding:0 16px;border-bottom:1px solid var(--border-subtle)}\n.theme-row:last-child{border-bottom:0}\n.token-box{margin-top:16px;padding:16px;background:var(--code-bg);color:var(--code-ink)}\n.token-box code{display:block;margin-top:8px;font-family:var(--font-mono);font-size:12px;word-break:break-all}\n.token-box .muted{color:var(--code-ink);opacity:.78}\n.token-error{margin-top:12px;color:var(--support-error)}\n.signin{display:flex;align-items:flex-start;justify-content:center;padding-top:80px}\n.signin-card{width:100%;max-width:560px}\n.signin-card .btn{margin-top:8px}\n@media(max-width:900px){\n.shell{display:block}\n.sidenav{width:auto;display:flex;flex-wrap:nowrap;align-items:stretch;overflow-x:auto;overflow-y:hidden;padding:0;border-right:0;border-bottom:1px solid var(--border-subtle);scrollbar-width:thin}\n.nav-item{width:auto;flex:0 0 auto;min-height:48px;padding:0 16px;border-left:0;border-bottom:3px solid transparent}\n.nav-item.is-active{border-left-color:transparent;border-bottom-color:var(--link)}\n.nav-heading{display:none}\n.data-table{min-width:0}\n.data-table th,.data-table td{white-space:normal;overflow-wrap:anywhere}\n.data-table th{overflow-wrap:normal}\n.main{padding:24px 16px 48px}\n.page{padding:16px 16px 40px}\n.detail-side{flex-basis:100%}\n}\n@media(max-width:620px){\n.topbar{height:auto;min-height:48px;flex-wrap:wrap;gap:8px;padding:8px 16px}\n.search{order:3;flex:1 1 100%;max-width:none;height:40px;margin-left:0}\n.search input{height:40px}\n.brand-sub{display:none}\n.topbar-user{max-width:100%}\nbutton.mode-toggle{height:40px}\nh1,.detail-head h1,.signin-card h1{font-size:24px}\n.page-head,.detail-head{align-items:flex-start}\n.button-row{width:100%}\n.button-row .btn{flex:1 1 auto}\n.field-row input,.field-row select{flex:1 1 140px}\n.panel-head{align-items:flex-start}\n.version-row{gap:8px}\n.preview-frame{aspect-ratio:4/3}\n.data-table,.data-table tbody,.data-table tr,.data-table td{display:block;width:100%;min-width:0}\n.data-table thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.data-table tr{padding:12px 16px;border-bottom:1px solid var(--border-subtle);background:var(--layer-01)}\n.data-table tr:hover{background:var(--layer-hover)}\n.data-table tbody tr:hover td{background:transparent}\n.data-table tr:last-child{border-bottom:0}\n.data-table td,.data-table th{height:auto;padding:0;border:0;text-align:left;white-space:normal;overflow-wrap:anywhere;background:transparent}\n.data-table tr>td:first-child{padding-bottom:8px}\n.data-table td[data-label]{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;min-height:32px;padding:2px 0}\n.data-table td[data-label]::before{content:attr(data-label) ": ";flex:0 0 auto;min-width:96px;color:var(--text-secondary);font-family:var(--font-sans);font-weight:400;font-size:12px;line-height:1.33}\n.data-table .btn-link{height:40px;padding:0}\n.data-table .icon-btn{height:40px}\n.data-table .empty-row td{padding:24px 0;text-align:center;color:var(--text-secondary)}\n.cell-name .row-title{max-width:none;overflow:visible;white-space:normal}\n}\n';
4501
- var APP_JS = '"use strict";\n(function () {\n function api(method, path, body) {\n var init = { method: method, credentials: "same-origin", headers: {} };\n if (method !== "GET") init.headers["Content-Type"] = "application/json";\n if (body !== undefined) init.body = JSON.stringify(body);\n return fetch(path, init).then(function (res) {\n if (res.status === 204) return null;\n return res.text().then(function (text) {\n var data = null;\n if (text) { try { data = JSON.parse(text); } catch (err) { data = null; } }\n if (!res.ok) {\n var msg = data && data.error ? data.error : "Request failed with status " + res.status;\n throw new Error(msg);\n }\n return data;\n });\n });\n }\n\n function fail(err) {\n window.alert(err && err.message ? err.message : String(err));\n }\n\n function reload() {\n window.location.reload();\n }\n\n function copyText(value) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(value).catch(fail);\n return;\n }\n var input = document.createElement("textarea");\n input.value = value;\n document.body.appendChild(input);\n input.select();\n try { document.execCommand("copy"); } catch (err) { fail(err); }\n document.body.removeChild(input);\n }\n\n function propsOf(el) {\n var raw = el.getAttribute("data-props");\n if (!raw) return {};\n try { return JSON.parse(raw) || {}; } catch (err) { return {}; }\n }\n\n function closest(event, selector) {\n var target = event.target;\n if (!target || !target.closest) return null;\n return target.closest(selector);\n }\n\n function flash(el, text) {\n var label = el.textContent;\n el.textContent = text;\n window.setTimeout(function () { el.textContent = label; }, 1200);\n }\n\n function field(form, name) {\n var el = form.querySelector("[name=" + name + "]");\n return el && typeof el.value === "string" ? el.value : "";\n }\n\n var islands = {\n copy: function (root, props) {\n root.addEventListener("click", function (event) {\n event.preventDefault();\n copyText(String(props.text || ""));\n flash(root, "Copied");\n });\n },\n delete: function (root, props) {\n root.addEventListener("click", function (event) {\n event.preventDefault();\n if (!window.confirm("Delete this artifact, all versions and all shares?")) return;\n api("DELETE", "/api/artifacts/" + encodeURIComponent(props.slug)).then(function () {\n window.location.assign("/");\n }, fail);\n });\n },\n versions: function (root, props) {\n root.addEventListener("click", function (event) {\n var el = closest(event, "[data-action]");\n if (!el || !root.contains(el)) return;\n var action = el.getAttribute("data-action");\n if (action !== "pin" && action !== "set-live" && action !== "newest") return;\n event.preventDefault();\n if (action === "newest") {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { pinned: null }).then(reload, fail);\n } else {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n pinned: Number(el.getAttribute("data-n"))\n }).then(reload, fail);\n }\n });\n },\n share: function (root, props) {\n root.addEventListener("click", function (event) {\n var removed = closest(event, "[data-action=remove-share]");\n if (removed && root.contains(removed)) {\n event.preventDefault();\n api(\n "DELETE",\n "/api/artifacts/" + encodeURIComponent(props.slug) + "/shares/" + encodeURIComponent(removed.getAttribute("data-email"))\n ).then(reload, fail);\n return;\n }\n var tag = closest(event, "[data-action=remove-tag]");\n if (tag && root.contains(tag)) {\n event.preventDefault();\n var tags = Array.isArray(props.tags) ? props.tags.filter(function (value) {\n return value !== tag.getAttribute("data-tag");\n }) : [];\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { tags: tags }).then(reload, fail);\n }\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n var kind = form.getAttribute("data-form");\n if (!kind) return;\n event.preventDefault();\n if (kind === "mode") {\n var checked = form.querySelector("[name=mode]:checked") || form.querySelector("[name=mode]");\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n mode: checked ? checked.value : "private"\n }).then(reload, fail);\n } else if (kind === "title") {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n title: field(form, "title").trim()\n }).then(reload, fail);\n } else if (kind === "share") {\n var email = field(form, "email").trim();\n if (email === "") return;\n api(\n "PUT",\n "/api/artifacts/" + encodeURIComponent(props.slug) + "/shares/" + encodeURIComponent(email),\n { role: field(form, "role") }\n ).then(reload, fail);\n } else if (kind === "tags") {\n var tagInput = field(form, "tag").trim().toLowerCase();\n if (tagInput === "") return;\n var next = Array.isArray(props.tags) ? props.tags.slice() : [];\n if (next.indexOf(tagInput) === -1) next.push(tagInput);\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { tags: next }).then(reload, fail);\n } else if (kind === "project") {\n var project = field(form, "project");\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n project: project === "" ? null : project\n }).then(reload, fail);\n }\n });\n },\n project: function (root, props) {\n root.addEventListener("click", function (event) {\n var el = closest(event, "[data-action=remove-member]");\n if (!el || !root.contains(el)) return;\n event.preventDefault();\n api(\n "DELETE",\n "/api/projects/" + encodeURIComponent(props.slug) + "/members/" + encodeURIComponent(el.getAttribute("data-email"))\n ).then(reload, fail);\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n var kind = form.getAttribute("data-form");\n if (!kind) return;\n event.preventDefault();\n if (kind === "project-new") {\n var slug = field(form, "slug").trim().toLowerCase();\n if (slug === "") return;\n var body = { slug: slug };\n var title = field(form, "title").trim();\n var description = field(form, "description").trim();\n if (title !== "") body.title = title;\n if (description !== "") body.description = description;\n api("POST", "/api/projects", body).then(function (data) {\n window.location.assign("/p/" + encodeURIComponent(data.slug));\n }, fail);\n } else if (kind === "description") {\n api("PATCH", "/api/projects/" + encodeURIComponent(props.slug), {\n description: field(form, "description").trim()\n }).then(reload, fail);\n } else if (kind === "add-artifact") {\n var artifact = field(form, "artifact");\n if (artifact === "") return;\n api("PATCH", "/api/artifacts/" + encodeURIComponent(artifact), { project: props.slug }).then(reload, fail);\n } else if (kind === "member") {\n var email = field(form, "email").trim();\n if (email === "") return;\n api(\n "PUT",\n "/api/projects/" + encodeURIComponent(props.slug) + "/members/" + encodeURIComponent(email),\n { role: field(form, "role") }\n ).then(reload, fail);\n }\n });\n },\n tokens: function (root) {\n root.addEventListener("click", function (event) {\n var copy = closest(event, "[data-action=copy]");\n if (copy && root.contains(copy)) {\n event.preventDefault();\n copyText(copy.getAttribute("data-copy") || "");\n flash(copy, "Copied");\n return;\n }\n var revoke = closest(event, "[data-action=revoke], [data-action=revoke-token]");\n if (!revoke || !root.contains(revoke)) return;\n event.preventDefault();\n if (!window.confirm("Revoke this token? The CLI using it will stop working.")) return;\n api("DELETE", "/api/tokens/" + encodeURIComponent(revoke.getAttribute("data-id"))).then(function () {\n var row = revoke.closest("tr");\n if (row) row.remove();\n }, fail);\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n if (form.getAttribute("data-form") !== "token") return;\n event.preventDefault();\n var name = field(form, "name").trim();\n var scope = field(form, "scope");\n api("POST", "/api/tokens", { name: name, scope: scope }).then(function (data) {\n var box = document.getElementById("token-box");\n var code = document.getElementById("token-value");\n var error = document.getElementById("token-error");\n if (box && code) {\n code.textContent = data.token;\n box.hidden = false;\n }\n if (error) error.hidden = true;\n var body = document.getElementById("token-rows");\n if (body) {\n var tr = document.createElement("tr");\n var cells = [data.name, data.scope.charAt(0).toUpperCase() + data.scope.slice(1), String(data.created_at).slice(0, 10), "Never"];\n var labels = [null, "Scope", "Created", "Last used"];\n for (var i = 0; i < cells.length; i += 1) {\n var td = document.createElement("td");\n td.textContent = cells[i];\n if (labels[i]) td.setAttribute("data-label", labels[i]);\n tr.appendChild(td);\n }\n var last = document.createElement("td");\n last.setAttribute("data-label", "Actions");\n var button = document.createElement("button");\n button.type = "button";\n button.className = "btn-link";\n button.setAttribute("data-action", "revoke");\n button.setAttribute("data-id", data.id);\n button.textContent = "Revoke";\n last.appendChild(button);\n tr.appendChild(last);\n body.appendChild(tr);\n }\n form.reset();\n }, function (err) {\n var box = document.getElementById("token-error");\n if (box) {\n box.textContent = err && err.message ? err.message : String(err);\n box.hidden = false;\n return;\n }\n fail(err);\n });\n });\n }\n };\n\n var MODE_KEY = "rshow-mode";\n var MODES = ["system", "light", "dark"];\n var MODE_LABELS = { system: "System", light: "Light", dark: "Dark" };\n\n function normalizeMode(value) {\n return value === "light" || value === "dark" ? value : "system";\n }\n\n function readMode() {\n try { return normalizeMode(window.localStorage.getItem(MODE_KEY)); } catch (err) { return "system"; }\n }\n\n function applyMode(mode) {\n var root = document.documentElement;\n if (mode === "light" || mode === "dark") root.dataset.mode = mode;\n else delete root.dataset.mode;\n }\n\n function saveMode(mode) {\n try { window.localStorage.setItem(MODE_KEY, mode); } catch (err) { /* storage may be blocked */ }\n applyMode(mode);\n }\n\n function paintMode(toggle) {\n var mode = readMode();\n toggle.textContent = MODE_LABELS[mode];\n toggle.setAttribute("aria-label", "Color mode: " + MODE_LABELS[mode] + ". Activate to cycle System, Light, Dark.");\n }\n\n var modeToggle = document.getElementById("mode-toggle");\n if (modeToggle) {\n paintMode(modeToggle);\n modeToggle.addEventListener("click", function () {\n var next = MODES[(MODES.indexOf(readMode()) + 1) % MODES.length];\n saveMode(next);\n paintMode(modeToggle);\n });\n }\n\n document.addEventListener("change", function (event) {\n var el = event.target;\n if (!el || !el.matches || !el.matches("[data-submit-on-change]")) return;\n if (el.form) el.form.requestSubmit();\n });\n\n var navBar = document.querySelector(".sidenav");\n var activeNav = navBar ? navBar.querySelector(".nav-item.is-active") : null;\n if (navBar && activeNav && navBar.scrollWidth > navBar.clientWidth) {\n var delta = activeNav.getBoundingClientRect().left - navBar.getBoundingClientRect().left;\n if (delta < 0 || delta + activeNav.offsetWidth > navBar.clientWidth) {\n navBar.scrollLeft = Math.max(0, navBar.scrollLeft + delta - 16);\n }\n }\n\n var nodes = document.querySelectorAll("[data-island]");\n for (var i = 0; i < nodes.length; i += 1) {\n var kind = nodes[i].getAttribute("data-island");\n var mount = islands[kind];\n if (mount) mount(nodes[i], propsOf(nodes[i]));\n }\n})();\n';
4562
+ var APP_JS = '"use strict";\n(function () {\n function api(method, path, body) {\n var init = { method: method, credentials: "same-origin", headers: {} };\n if (method !== "GET") init.headers["Content-Type"] = "application/json";\n if (body !== undefined) init.body = JSON.stringify(body);\n return fetch(path, init).then(function (res) {\n if (res.status === 204) return null;\n return res.text().then(function (text) {\n var data = null;\n if (text) { try { data = JSON.parse(text); } catch (err) { data = null; } }\n if (!res.ok) {\n var msg = data && data.error ? data.error : "Request failed with status " + res.status;\n throw new Error(msg);\n }\n return data;\n });\n });\n }\n\n function fail(err) {\n window.alert(err && err.message ? err.message : String(err));\n }\n\n function reload() {\n window.location.reload();\n }\n\n function copyText(value) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(value).catch(fail);\n return;\n }\n var input = document.createElement("textarea");\n input.value = value;\n document.body.appendChild(input);\n input.select();\n try { document.execCommand("copy"); } catch (err) { fail(err); }\n document.body.removeChild(input);\n }\n\n function propsOf(el) {\n var raw = el.getAttribute("data-props");\n if (!raw) return {};\n try { return JSON.parse(raw) || {}; } catch (err) { return {}; }\n }\n\n function closest(event, selector) {\n var target = event.target;\n if (!target || !target.closest) return null;\n return target.closest(selector);\n }\n\n function flash(el, text) {\n var label = el.textContent;\n el.textContent = text;\n window.setTimeout(function () { el.textContent = label; }, 1200);\n }\n\n function field(form, name) {\n var el = form.querySelector("[name=" + name + "]");\n return el && typeof el.value === "string" ? el.value : "";\n }\n\n var islands = {\n copy: function (root, props) {\n root.addEventListener("click", function (event) {\n event.preventDefault();\n copyText(String(props.text || ""));\n flash(root, "Copied");\n });\n },\n delete: function (root, props) {\n root.addEventListener("click", function (event) {\n event.preventDefault();\n if (!window.confirm("Delete this artifact, all versions and all shares?")) return;\n api("DELETE", "/api/artifacts/" + encodeURIComponent(props.slug)).then(function () {\n window.location.assign("/");\n }, fail);\n });\n },\n versions: function (root, props) {\n root.addEventListener("click", function (event) {\n var el = closest(event, "[data-action]");\n if (!el || !root.contains(el)) return;\n var action = el.getAttribute("data-action");\n if (action !== "pin" && action !== "set-live" && action !== "newest" && action !== "delete-version") return;\n event.preventDefault();\n if (action === "newest") {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { pinned: null }).then(reload, fail);\n } else if (action === "delete-version") {\n var version = Number(el.getAttribute("data-n"));\n if (!window.confirm("Delete v" + version + "? This cannot be undone.")) return;\n api("DELETE", "/api/artifacts/" + encodeURIComponent(props.slug) + "/versions/" + version).then(reload, fail);\n } else {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n pinned: Number(el.getAttribute("data-n"))\n }).then(reload, fail);\n }\n });\n },\n share: function (root, props) {\n root.addEventListener("click", function (event) {\n var removed = closest(event, "[data-action=remove-share]");\n if (removed && root.contains(removed)) {\n event.preventDefault();\n api(\n "DELETE",\n "/api/artifacts/" + encodeURIComponent(props.slug) + "/shares/" + encodeURIComponent(removed.getAttribute("data-email"))\n ).then(reload, fail);\n return;\n }\n var tag = closest(event, "[data-action=remove-tag]");\n if (tag && root.contains(tag)) {\n event.preventDefault();\n var tags = Array.isArray(props.tags) ? props.tags.filter(function (value) {\n return value !== tag.getAttribute("data-tag");\n }) : [];\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { tags: tags }).then(reload, fail);\n }\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n var kind = form.getAttribute("data-form");\n if (!kind) return;\n event.preventDefault();\n if (kind === "mode") {\n var checked = form.querySelector("[name=mode]:checked") || form.querySelector("[name=mode]");\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n mode: checked ? checked.value : "private"\n }).then(reload, fail);\n } else if (kind === "title") {\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n title: field(form, "title").trim()\n }).then(reload, fail);\n } else if (kind === "share") {\n var email = field(form, "email").trim();\n if (email === "") return;\n api(\n "PUT",\n "/api/artifacts/" + encodeURIComponent(props.slug) + "/shares/" + encodeURIComponent(email),\n { role: field(form, "role") }\n ).then(reload, fail);\n } else if (kind === "tags") {\n var tagInput = field(form, "tag").trim().toLowerCase();\n if (tagInput === "") return;\n var next = Array.isArray(props.tags) ? props.tags.slice() : [];\n if (next.indexOf(tagInput) === -1) next.push(tagInput);\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), { tags: next }).then(reload, fail);\n } else if (kind === "project") {\n var project = field(form, "project");\n api("PATCH", "/api/artifacts/" + encodeURIComponent(props.slug), {\n project: project === "" ? null : project\n }).then(reload, fail);\n }\n });\n },\n project: function (root, props) {\n root.addEventListener("click", function (event) {\n var el = closest(event, "[data-action=remove-member]");\n if (!el || !root.contains(el)) return;\n event.preventDefault();\n api(\n "DELETE",\n "/api/projects/" + encodeURIComponent(props.slug) + "/members/" + encodeURIComponent(el.getAttribute("data-email"))\n ).then(reload, fail);\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n var kind = form.getAttribute("data-form");\n if (!kind) return;\n event.preventDefault();\n if (kind === "project-new") {\n var slug = field(form, "slug").trim().toLowerCase();\n if (slug === "") return;\n var body = { slug: slug };\n var title = field(form, "title").trim();\n var description = field(form, "description").trim();\n if (title !== "") body.title = title;\n if (description !== "") body.description = description;\n api("POST", "/api/projects", body).then(function (data) {\n window.location.assign("/p/" + encodeURIComponent(data.slug));\n }, fail);\n } else if (kind === "description") {\n api("PATCH", "/api/projects/" + encodeURIComponent(props.slug), {\n description: field(form, "description").trim()\n }).then(reload, fail);\n } else if (kind === "add-artifact") {\n var artifact = field(form, "artifact");\n if (artifact === "") return;\n api("PATCH", "/api/artifacts/" + encodeURIComponent(artifact), { project: props.slug }).then(reload, fail);\n } else if (kind === "member") {\n var email = field(form, "email").trim();\n if (email === "") return;\n api(\n "PUT",\n "/api/projects/" + encodeURIComponent(props.slug) + "/members/" + encodeURIComponent(email),\n { role: field(form, "role") }\n ).then(reload, fail);\n }\n });\n },\n tokens: function (root) {\n root.addEventListener("click", function (event) {\n var copy = closest(event, "[data-action=copy]");\n if (copy && root.contains(copy)) {\n event.preventDefault();\n copyText(copy.getAttribute("data-copy") || "");\n flash(copy, "Copied");\n return;\n }\n var revoke = closest(event, "[data-action=revoke], [data-action=revoke-token]");\n if (!revoke || !root.contains(revoke)) return;\n event.preventDefault();\n if (!window.confirm("Revoke this token? The CLI using it will stop working.")) return;\n api("DELETE", "/api/tokens/" + encodeURIComponent(revoke.getAttribute("data-id"))).then(function () {\n var row = revoke.closest("tr");\n if (row) row.remove();\n }, fail);\n });\n root.addEventListener("submit", function (event) {\n var form = event.target;\n if (form.getAttribute("data-form") !== "token") return;\n event.preventDefault();\n var name = field(form, "name").trim();\n var scope = field(form, "scope");\n api("POST", "/api/tokens", { name: name, scope: scope }).then(function (data) {\n var box = document.getElementById("token-box");\n var code = document.getElementById("token-value");\n var error = document.getElementById("token-error");\n if (box && code) {\n code.textContent = data.token;\n box.hidden = false;\n }\n if (error) error.hidden = true;\n var body = document.getElementById("token-rows");\n if (body) {\n var tr = document.createElement("tr");\n var cells = [data.name, data.scope.charAt(0).toUpperCase() + data.scope.slice(1), String(data.created_at).slice(0, 10), "Never"];\n var labels = [null, "Scope", "Created", "Last used"];\n for (var i = 0; i < cells.length; i += 1) {\n var td = document.createElement("td");\n td.textContent = cells[i];\n if (labels[i]) td.setAttribute("data-label", labels[i]);\n tr.appendChild(td);\n }\n var last = document.createElement("td");\n last.setAttribute("data-label", "Actions");\n var button = document.createElement("button");\n button.type = "button";\n button.className = "btn-link";\n button.setAttribute("data-action", "revoke");\n button.setAttribute("data-id", data.id);\n button.textContent = "Revoke";\n last.appendChild(button);\n tr.appendChild(last);\n body.appendChild(tr);\n }\n form.reset();\n }, function (err) {\n var box = document.getElementById("token-error");\n if (box) {\n box.textContent = err && err.message ? err.message : String(err);\n box.hidden = false;\n return;\n }\n fail(err);\n });\n });\n }\n };\n\n var MODE_KEY = "rshow-mode";\n var MODES = ["system", "light", "dark"];\n var MODE_LABELS = { system: "System", light: "Light", dark: "Dark" };\n\n function normalizeMode(value) {\n return value === "light" || value === "dark" ? value : "system";\n }\n\n function readMode() {\n try { return normalizeMode(window.localStorage.getItem(MODE_KEY)); } catch (err) { return "system"; }\n }\n\n function applyMode(mode) {\n var root = document.documentElement;\n if (mode === "light" || mode === "dark") root.dataset.mode = mode;\n else delete root.dataset.mode;\n }\n\n function saveMode(mode) {\n try { window.localStorage.setItem(MODE_KEY, mode); } catch (err) { /* storage may be blocked */ }\n applyMode(mode);\n }\n\n function paintMode(toggle) {\n var mode = readMode();\n toggle.textContent = MODE_LABELS[mode];\n toggle.setAttribute("aria-label", "Color mode: " + MODE_LABELS[mode] + ". Activate to cycle System, Light, Dark.");\n }\n\n var modeToggle = document.getElementById("mode-toggle");\n if (modeToggle) {\n paintMode(modeToggle);\n modeToggle.addEventListener("click", function () {\n var next = MODES[(MODES.indexOf(readMode()) + 1) % MODES.length];\n saveMode(next);\n paintMode(modeToggle);\n });\n }\n\n document.addEventListener("change", function (event) {\n var el = event.target;\n if (!el || !el.matches || !el.matches("[data-submit-on-change]")) return;\n if (el.form) el.form.requestSubmit();\n });\n\n var navBar = document.querySelector(".sidenav");\n var activeNav = navBar ? navBar.querySelector(".nav-item.is-active") : null;\n if (navBar && activeNav && navBar.scrollWidth > navBar.clientWidth) {\n var delta = activeNav.getBoundingClientRect().left - navBar.getBoundingClientRect().left;\n if (delta < 0 || delta + activeNav.offsetWidth > navBar.clientWidth) {\n navBar.scrollLeft = Math.max(0, navBar.scrollLeft + delta - 16);\n }\n }\n\n var nodes = document.querySelectorAll("[data-island]");\n for (var i = 0; i < nodes.length; i += 1) {\n var kind = nodes[i].getAttribute("data-island");\n var mount = islands[kind];\n if (mount) mount(nodes[i], propsOf(nodes[i]));\n }\n})();\n';
4502
4563
  var MODE_JS = '"use strict";\n(function () {\n var KEY = "rshow-mode";\n var root = document.documentElement;\n var mode = null;\n try { mode = window.localStorage.getItem(KEY); } catch (err) { mode = null; }\n if (mode === "light" || mode === "dark") root.dataset.mode = mode;\n else delete root.dataset.mode;\n})();\n';
4503
4564
 
4504
4565
  // src/static.ts
@@ -5403,6 +5464,7 @@ function VersionRowView(props) {
5403
5464
  const version = props.version;
5404
5465
  const served = props.artifact.served === version.n;
5405
5466
  const pinned = props.artifact.pinned === version.n;
5467
+ const deletable = props.owner && !props.artifact.slug.startsWith(THEME_PREFIX) && !pinned && props.count > 1;
5406
5468
  return /* @__PURE__ */ jsxDEV("li", { class: served ? "version-row is-served" : "version-row", children: [
5407
5469
  /* @__PURE__ */ jsxDEV("span", { class: "version-n mono", children: [
5408
5470
  "v",
@@ -5423,7 +5485,8 @@ function VersionRowView(props) {
5423
5485
  ] }),
5424
5486
  served ? /* @__PURE__ */ jsxDEV("span", { class: "pill pill-live", children: pinned ? "Pinned, live" : "Live" }) : null,
5425
5487
  /* @__PURE__ */ jsxDEV("a", { class: "btn-link", href: `/a/${props.artifact.slug}?preview=${version.n}`, children: "Preview" }),
5426
- props.owner && !served ? /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn-link", "data-action": "pin", "data-n": version.n, children: "Pin here" }) : null
5488
+ props.owner && !served ? /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn-link", "data-action": "pin", "data-n": version.n, children: "Pin here" }) : null,
5489
+ deletable ? /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn-link", "data-action": "delete-version", "data-n": version.n, children: "Delete" }) : null
5427
5490
  ] });
5428
5491
  }
5429
5492
  __name(VersionRowView, "VersionRowView");
@@ -5557,7 +5620,7 @@ function artifactPage(env, email, artifact, versions, shares, project, editableP
5557
5620
  /* @__PURE__ */ jsxDEV("h2", { children: "Versions" }),
5558
5621
  /* @__PURE__ */ jsxDEV("span", { class: "muted", children: artifact.pinned === null ? "The link follows the newest version." : `Pinned to v${artifact.pinned}. New publishes wait behind it.` })
5559
5622
  ] }),
5560
- versions.length === 0 ? /* @__PURE__ */ jsxDEV("p", { class: "empty", children: "No versions yet." }) : /* @__PURE__ */ jsxDEV("ul", { class: "version-list", children: versions.map((version) => /* @__PURE__ */ jsxDEV(VersionRowView, { artifact, version, owner })) }),
5623
+ versions.length === 0 ? /* @__PURE__ */ jsxDEV("p", { class: "empty", children: "No versions yet." }) : /* @__PURE__ */ jsxDEV("ul", { class: "version-list", children: versions.map((version) => /* @__PURE__ */ jsxDEV(VersionRowView, { artifact, version, owner, count: versions.length })) }),
5561
5624
  artifact.pinned !== null ? /* @__PURE__ */ jsxDEV("div", { class: "version-actions", children: /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn btn-tertiary", "data-action": "newest", children: "Follow the newest version again" }) }) : null
5562
5625
  ]
5563
5626
  }
@@ -5627,7 +5690,7 @@ function artifactPage(env, email, artifact, versions, shares, project, editableP
5627
5690
  ] }) : null,
5628
5691
  owner ? /* @__PURE__ */ jsxDEV("div", { class: "danger-zone", children: [
5629
5692
  /* @__PURE__ */ jsxDEV("h2", { children: "Danger zone" }),
5630
- /* @__PURE__ */ jsxDEV(
5693
+ artifact.slug.startsWith(THEME_PREFIX) ? /* @__PURE__ */ jsxDEV("p", { class: "side-text", children: "Pages link to this theme. Delete it with rshow delete --force." }) : /* @__PURE__ */ jsxDEV(
5631
5694
  "button",
5632
5695
  {
5633
5696
  type: "button",
@@ -6503,7 +6566,6 @@ __name(loginRedirect, "loginRedirect");
6503
6566
 
6504
6567
  // src/project.ts
6505
6568
  var THEME_SLUG = `${THEME_PREFIX}default`;
6506
- var THEME_VERSION = 3;
6507
6569
  var MODE_WORDS = {
6508
6570
  private: "Private",
6509
6571
  people: "People",
@@ -6513,15 +6575,15 @@ var LIST_STYLE = ".rshow-project{max-width:44rem}.rshow-project h1{margin-bottom
6513
6575
  async function defaultThemeReady(env) {
6514
6576
  const artifact = await env.DB.prepare("SELECT mode FROM artifacts WHERE slug = ?1").bind(THEME_SLUG).first();
6515
6577
  if (!artifact || artifact.mode !== MODE.public) return false;
6516
- const version = await env.DB.prepare("SELECT n FROM versions WHERE slug = ?1 AND n = ?2").bind(THEME_SLUG, THEME_VERSION).first();
6578
+ const version = await env.DB.prepare("SELECT n FROM versions WHERE slug = ?1 LIMIT 1").bind(THEME_SLUG).first();
6517
6579
  return version !== null;
6518
6580
  }
6519
6581
  __name(defaultThemeReady, "defaultThemeReady");
6520
6582
  function groupPage(title2, description, artifacts, theme) {
6521
- const links = theme ? `<link rel="stylesheet" href="/_themes/default@3/fonts.css">
6522
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
6523
- <link rel="stylesheet" href="/_themes/default@3/page.css">
6524
- <script src="/_themes/default@3/mode.js"><\/script>` : `<style>${MINIMAL_STYLE}</style>`;
6583
+ const links = theme ? `<link rel="stylesheet" href="/_themes/default@latest/fonts.css">
6584
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
6585
+ <link rel="stylesheet" href="/_themes/default@latest/page.css">
6586
+ <script src="/_themes/default@latest/mode.js"><\/script>` : `<style>${MINIMAL_STYLE}</style>`;
6525
6587
  const descriptionHtml = description === "" ? "" : `
6526
6588
  <p class="rshow-desc">${escapeHtml(description)}</p>`;
6527
6589
  const listHtml = artifacts.length === 0 ? `
@@ -6593,7 +6655,8 @@ var CACHE_CONTROL2 = {
6593
6655
  publicLive: "public, max-age=0, s-maxage=15, must-revalidate",
6594
6656
  publicVersion: "public, max-age=31536000, immutable, s-maxage=300",
6595
6657
  theme: "public, max-age=31536000, immutable",
6596
- themeJson: "public, max-age=60"
6658
+ themeJson: "public, max-age=60",
6659
+ themeLatest: "public, max-age=60"
6597
6660
  };
6598
6661
  function viewerCsp(env) {
6599
6662
  return [
@@ -6719,7 +6782,7 @@ async function handleViewer(c) {
6719
6782
  if (identity.email === null) return loginRedirect(pathname, url.search);
6720
6783
  return forbiddenPage(identity.email);
6721
6784
  }
6722
- const version = versionN === null ? access.version : access.version && access.version.n === versionN ? access.version : await versionFor(env, slug, versionN);
6785
+ const version = versionN === null ? access.version : access.version && access.version.n === versionN ? access.version : await versionFor(env, artifact, versionN);
6723
6786
  if (!version) return notFoundPage("This artifact has no such version yet.");
6724
6787
  const relative = relativePath(parts);
6725
6788
  if (relative === null) return notFoundPage();
@@ -6743,11 +6806,11 @@ async function handleTheme(c, rest, url) {
6743
6806
  if (info) {
6744
6807
  if (rest.length !== 1) return withoutBodyOnHead(c, jsonError("not found", 404));
6745
6808
  const name2 = info[1];
6746
- const latest = await accessFor(env, `${THEME_PREFIX}${name2}`);
6747
- if (!latest || latest.artifact.mode !== MODE.public || !latest.version) {
6809
+ const latest2 = await accessFor(env, `${THEME_PREFIX}${name2}`);
6810
+ if (!latest2 || latest2.artifact.mode !== MODE.public || !latest2.version) {
6748
6811
  return withoutBodyOnHead(c, jsonError("not found", 404));
6749
6812
  }
6750
- const version2 = latest.version.n;
6813
+ const version2 = latest2.version.n;
6751
6814
  const reply = { name: name2, version: version2, url: `/${THEMES_SEGMENT}/${name2}@${version2}/` };
6752
6815
  return withoutBodyOnHead(
6753
6816
  c,
@@ -6757,6 +6820,15 @@ async function handleTheme(c, rest, url) {
6757
6820
  })
6758
6821
  );
6759
6822
  }
6823
+ const latest = THEME_LATEST_PATTERN.exec(segment);
6824
+ if (latest) {
6825
+ const name2 = latest[1];
6826
+ const access2 = await accessFor(env, `${THEME_PREFIX}${name2}`);
6827
+ if (!access2 || access2.artifact.mode !== MODE.public || !access2.version) return notFoundPage();
6828
+ const path = rest.slice(1).map(encodeURIComponent).join("/");
6829
+ const location = `/${THEMES_SEGMENT}/${name2}@${access2.version.n}/${path}${url.search}`;
6830
+ return withoutBodyOnHead(c, redirect(location, 302, CACHE_CONTROL2.themeLatest));
6831
+ }
6760
6832
  const match2 = THEME_VERSION_PATTERN.exec(segment);
6761
6833
  if (!match2) return notFoundPage();
6762
6834
  const name = match2[1];
@@ -6767,7 +6839,7 @@ async function handleTheme(c, rest, url) {
6767
6839
  if (!access || access.artifact.mode !== MODE.public) return notFoundPage();
6768
6840
  const parts = rest.slice(1);
6769
6841
  if (parts.length === 0) return redirect(`/${THEMES_SEGMENT}/${segment}/${url.search}`, 301);
6770
- const version = access.version && access.version.n === n ? access.version : await versionFor(env, slug, n);
6842
+ const version = access.version && access.version.n === n ? access.version : await versionFor(env, access.artifact, n);
6771
6843
  if (!version) return notFoundPage("This theme has no such version yet.");
6772
6844
  const relative = relativePath(parts);
6773
6845
  if (relative === null) return notFoundPage();
@@ -0,0 +1,3 @@
1
+ ALTER TABLE artifacts ADD COLUMN last_n INTEGER NOT NULL DEFAULT 0;
2
+ ALTER TABLE artifacts ADD COLUMN version_gen INTEGER NOT NULL DEFAULT 0;
3
+ UPDATE artifacts SET last_n = COALESCE(latest, 0);