rshow 0.0.7 → 0.0.9

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/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();
@@ -3271,21 +3277,18 @@ __name(etagMatches, "etagMatches");
3271
3277
 
3272
3278
  // src/cache.ts
3273
3279
  var CACHE_ORIGIN = "https://rshow.internal";
3274
- var ACCESS_TTL_MS = 15e3;
3275
- var ACCESS_MAX_ENTRIES = 500;
3276
3280
  var MANIFEST_MAX_ENTRIES = 200;
3277
3281
  var VERSION_MAX_ENTRIES = 500;
3278
- var ACCESS_CACHE_CONTROL = "max-age=15";
3282
+ var ACCESS_CACHE_CONTROL = "max-age=2";
3279
3283
  var MANIFEST_CACHE_CONTROL = "public, max-age=31536000, immutable";
3280
3284
  var VERSION_CACHE_CONTROL = "public, max-age=31536000, immutable";
3281
3285
  var JSON_CONTENT_TYPE = "application/json";
3282
3286
  var BLOB_CACHE_CONTROL = "public, max-age=31536000, immutable";
3283
- var ARTIFACT_COLUMNS2 = "slug, title, owner, mode, pinned, latest, project, created_at, updated_at";
3287
+ var ARTIFACT_COLUMNS2 = "slug, title, owner, mode, pinned, latest, last_n, version_gen, project, created_at, updated_at";
3284
3288
  var VERSION_COLUMNS = "slug, n, label, manifest_key, files, bytes, created_by, created_at";
3285
- var accessMemory = /* @__PURE__ */ new Map();
3286
3289
  var manifestMemory = /* @__PURE__ */ new Map();
3287
3290
  var versionMemory = /* @__PURE__ */ new Map();
3288
- var versionNumbers = /* @__PURE__ */ new Map();
3291
+ var versionKeys = /* @__PURE__ */ new Map();
3289
3292
  var trackedKeys = /* @__PURE__ */ new Set();
3290
3293
  function accessCacheKey(slug) {
3291
3294
  return `${CACHE_ORIGIN}/access/${slug}`;
@@ -3299,24 +3302,14 @@ function blobCacheKey(hash) {
3299
3302
  return `${CACHE_ORIGIN}/blob/${hash}`;
3300
3303
  }
3301
3304
  __name(blobCacheKey, "blobCacheKey");
3302
- function versionCacheKey(slug, n) {
3303
- return `${CACHE_ORIGIN}/version/${slug}/${n}`;
3305
+ function versionCacheKey(slug, n, gen) {
3306
+ return `${CACHE_ORIGIN}/version/${slug}/${n}/${gen}`;
3304
3307
  }
3305
3308
  __name(versionCacheKey, "versionCacheKey");
3306
- function versionMemoryKey(slug, n) {
3307
- return `${slug}/${n}`;
3309
+ function versionMemoryKey(slug, n, gen) {
3310
+ return `${slug}/${n}/${gen}`;
3308
3311
  }
3309
3312
  __name(versionMemoryKey, "versionMemoryKey");
3310
- function rememberAccess(slug, value, expires) {
3311
- accessMemory.delete(slug);
3312
- accessMemory.set(slug, { value, expires });
3313
- while (accessMemory.size > ACCESS_MAX_ENTRIES) {
3314
- const oldest = accessMemory.keys().next().value;
3315
- if (oldest === void 0) break;
3316
- accessMemory.delete(oldest);
3317
- }
3318
- }
3319
- __name(rememberAccess, "rememberAccess");
3320
3313
  function rememberManifest(key, value) {
3321
3314
  manifestMemory.delete(key);
3322
3315
  manifestMemory.set(key, value);
@@ -3327,8 +3320,8 @@ function rememberManifest(key, value) {
3327
3320
  }
3328
3321
  }
3329
3322
  __name(rememberManifest, "rememberManifest");
3330
- function rememberVersion(slug, n, row) {
3331
- const key = versionMemoryKey(slug, n);
3323
+ function rememberVersion(slug, n, gen, row) {
3324
+ const key = versionMemoryKey(slug, n, gen);
3332
3325
  versionMemory.delete(key);
3333
3326
  versionMemory.set(key, row);
3334
3327
  while (versionMemory.size > VERSION_MAX_ENTRIES) {
@@ -3336,9 +3329,9 @@ function rememberVersion(slug, n, row) {
3336
3329
  if (oldest === void 0) break;
3337
3330
  versionMemory.delete(oldest);
3338
3331
  }
3339
- const numbers = versionNumbers.get(slug) ?? /* @__PURE__ */ new Set();
3340
- numbers.add(n);
3341
- versionNumbers.set(slug, numbers);
3332
+ const keys = versionKeys.get(slug) ?? /* @__PURE__ */ new Set();
3333
+ keys.add(versionCacheKey(slug, n, gen));
3334
+ versionKeys.set(slug, keys);
3342
3335
  }
3343
3336
  __name(rememberVersion, "rememberVersion");
3344
3337
  function decodeVersion(text) {
@@ -3447,30 +3440,18 @@ async function loadAccess(env, slug) {
3447
3440
  }
3448
3441
  __name(loadAccess, "loadAccess");
3449
3442
  async function accessFor(env, slug) {
3450
- const now = Date.now();
3451
- const memory = accessMemory.get(slug);
3452
- if (memory && memory.expires > now) {
3453
- rememberAccess(slug, memory.value, memory.expires);
3454
- return memory.value;
3455
- }
3456
- accessMemory.delete(slug);
3457
3443
  const key = accessCacheKey(slug);
3458
3444
  const cached = await cacheGet(key);
3459
3445
  if (cached) {
3460
3446
  const decoded = decodeAccess(await cached.text());
3461
- if (decoded.ok) {
3462
- rememberAccess(slug, decoded.value, now + ACCESS_TTL_MS);
3463
- return decoded.value;
3464
- }
3447
+ if (decoded.ok) return decoded.value;
3465
3448
  }
3466
3449
  const value = await loadAccess(env, slug);
3467
- rememberAccess(slug, value, now + ACCESS_TTL_MS);
3468
3450
  await cachePut(key, JSON.stringify(value), ACCESS_CACHE_CONTROL);
3469
3451
  return value;
3470
3452
  }
3471
3453
  __name(accessFor, "accessFor");
3472
3454
  async function invalidateAccess(slug) {
3473
- accessMemory.delete(slug);
3474
3455
  const key = accessCacheKey(slug);
3475
3456
  trackedKeys.delete(key);
3476
3457
  try {
@@ -3487,34 +3468,38 @@ async function invalidateProjectAccess(env, project) {
3487
3468
  }
3488
3469
  }
3489
3470
  __name(invalidateProjectAccess, "invalidateProjectAccess");
3490
- async function versionFor(env, slug, n) {
3491
- const memory = versionMemory.get(versionMemoryKey(slug, n));
3471
+ async function versionFor(env, artifact, n) {
3472
+ const gen = artifact.version_gen;
3473
+ const memory = versionMemory.get(versionMemoryKey(artifact.slug, n, gen));
3492
3474
  if (memory) {
3493
- rememberVersion(slug, n, memory);
3475
+ rememberVersion(artifact.slug, n, gen, memory);
3494
3476
  return memory;
3495
3477
  }
3496
- const cached = await cacheGet(versionCacheKey(slug, n));
3478
+ const key = versionCacheKey(artifact.slug, n, gen);
3479
+ const cached = await cacheGet(key);
3497
3480
  if (cached) {
3498
3481
  const row2 = decodeVersion(await cached.text());
3499
3482
  if (row2) {
3500
- rememberVersion(slug, n, row2);
3483
+ rememberVersion(artifact.slug, n, gen, row2);
3501
3484
  return row2;
3502
3485
  }
3503
3486
  }
3504
- const row = await getVersion(env, slug, n);
3487
+ const row = await getVersion(env, artifact.slug, n);
3505
3488
  if (!row) return null;
3506
- rememberVersion(slug, n, row);
3507
- await cachePut(versionCacheKey(slug, n), JSON.stringify(row), VERSION_CACHE_CONTROL);
3489
+ rememberVersion(artifact.slug, n, gen, row);
3490
+ await cachePut(key, JSON.stringify(row), VERSION_CACHE_CONTROL);
3508
3491
  return row;
3509
3492
  }
3510
3493
  __name(versionFor, "versionFor");
3511
3494
  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);
3495
+ const prefix = `${slug}/`;
3496
+ for (const key of [...versionMemory.keys()]) {
3497
+ if (key.startsWith(prefix)) versionMemory.delete(key);
3498
+ }
3499
+ const keys = versionKeys.get(slug);
3500
+ versionKeys.delete(slug);
3501
+ if (!keys) return;
3502
+ for (const key of keys) {
3518
3503
  trackedKeys.delete(key);
3519
3504
  try {
3520
3505
  await caches.default.delete(key);
@@ -3796,7 +3781,7 @@ async function storeVersion(env, artifact, input2) {
3796
3781
  env.DB.prepare(
3797
3782
  "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
3783
  ).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(
3784
+ env.DB.prepare("UPDATE artifacts SET latest = ?2, last_n = ?2, updated_at = ?3 WHERE slug = ?1 AND latest IS ?4").bind(
3800
3785
  artifact.slug,
3801
3786
  input2.n,
3802
3787
  input2.now,
@@ -3841,6 +3826,52 @@ function registerVersions(app) {
3841
3826
  if (!manifest) return jsonError("no such version", 404);
3842
3827
  return json(toVersionDetail(c.env, access.artifact.slug, version, manifest));
3843
3828
  });
3829
+ app.delete("/api/artifacts/:slug/versions/:n", async (c) => {
3830
+ const email = c.get("identity").email;
3831
+ const access = await loadAccessible(c.env, c.req.param("slug"), email);
3832
+ if (!access) return jsonError("not found", 404);
3833
+ if (!canManage(access.artifact, access.shares, actorOf(c), access.project)) {
3834
+ return jsonError("owner access is required", 403);
3835
+ }
3836
+ const raw2 = c.req.param("n");
3837
+ if (!/^[1-9][0-9]{0,8}$/.test(raw2)) return jsonError("no such version", 404);
3838
+ const n = Number(raw2);
3839
+ const slug = access.artifact.slug;
3840
+ if (slug.startsWith(THEME_PREFIX) && c.req.query("force") !== "1") {
3841
+ return jsonError("pages link to theme versions; pass force to delete it", 409);
3842
+ }
3843
+ const version = await getVersion(c.env, slug, n);
3844
+ if (!version) return jsonError("no such version", 404);
3845
+ if (access.artifact.pinned === n) {
3846
+ return jsonError(`version ${n} is pinned; pin another version first`, 409);
3847
+ }
3848
+ if (await countVersions(c.env, slug) <= 1) {
3849
+ return jsonError("this is the only version; delete the artifact instead", 409);
3850
+ }
3851
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3852
+ let deletedChanges = 0;
3853
+ try {
3854
+ const [deleted] = await c.env.DB.batch([
3855
+ c.env.DB.prepare(
3856
+ "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"
3857
+ ).bind(slug, n),
3858
+ c.env.DB.prepare(
3859
+ "UPDATE artifacts SET latest = (SELECT MAX(n) FROM versions WHERE slug = ?1), version_gen = version_gen + 1, updated_at = ?2 WHERE slug = ?1"
3860
+ ).bind(slug, now)
3861
+ ]);
3862
+ deletedChanges = deleted?.meta?.changes ?? 0;
3863
+ } catch {
3864
+ return jsonError("could not delete the version", 500);
3865
+ }
3866
+ if (deletedChanges !== 1) return jsonError("another change came first, retry", 409);
3867
+ await discardManifest(c.env, version.manifest_key);
3868
+ await invalidateVersions(slug);
3869
+ await invalidateAccess(slug);
3870
+ const updated = await getArtifact(c.env, slug);
3871
+ if (!updated) return jsonError("not found", 404);
3872
+ const tags = await getArtifactTags(c.env, slug);
3873
+ return json(toArtifact(c.env, updated, access.role, tags));
3874
+ });
3844
3875
  app.post("/api/artifacts/:slug/versions", async (c) => {
3845
3876
  const body = await readBody(c);
3846
3877
  if (!body.ok) return body.response;
@@ -3882,7 +3913,7 @@ function registerVersions(app) {
3882
3913
  }
3883
3914
  const label = typeof body.body.label === "string" ? body.body.label.trim().slice(0, 200) : "";
3884
3915
  const base = access.artifact.latest;
3885
- const n = (base ?? 0) + 1;
3916
+ const n = Math.max(access.artifact.last_n, base ?? 0) + 1;
3886
3917
  const now = (/* @__PURE__ */ new Date()).toISOString();
3887
3918
  const stored = await storeVersion(c.env, access.artifact, {
3888
3919
  label,
@@ -3980,10 +4011,11 @@ function registerArtifactCreate(app) {
3980
4011
  const existing = await getArtifact(c.env, slug);
3981
4012
  if (existing) return jsonError("slug is already taken", 409);
3982
4013
  const now = (/* @__PURE__ */ new Date()).toISOString();
4014
+ let counters;
3983
4015
  try {
3984
- await c.env.DB.prepare(
3985
- "INSERT INTO artifacts (slug, title, owner, mode, pinned, latest, project, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, NULL, NULL, ?5, ?6, ?6)"
3986
- ).bind(slug, title2, email, MODE.private, project, now).run();
4016
+ counters = await c.env.DB.prepare(
4017
+ "INSERT INTO artifacts (slug, title, owner, mode, pinned, latest, last_n, version_gen, project, created_at, updated_at) SELECT ?1, ?2, ?3, ?4, NULL, NULL, COALESCE((SELECT last_n FROM retired_slugs WHERE slug = ?1), 0), COALESCE((SELECT version_gen FROM retired_slugs WHERE slug = ?1), 0), ?5, ?6, ?6 RETURNING last_n, version_gen"
4018
+ ).bind(slug, title2, email, MODE.private, project, now).first();
3987
4019
  } catch {
3988
4020
  return jsonError("slug is already taken", 409);
3989
4021
  }
@@ -3999,6 +4031,8 @@ function registerArtifactCreate(app) {
3999
4031
  mode: MODE.private,
4000
4032
  pinned: null,
4001
4033
  latest: null,
4034
+ last_n: counters?.last_n ?? 0,
4035
+ version_gen: counters?.version_gen ?? 0,
4002
4036
  project,
4003
4037
  created_at: now,
4004
4038
  updated_at: now
@@ -4138,6 +4172,9 @@ function registerArtifactItem(app) {
4138
4172
  if (!canManage(access.artifact, access.shares, actorOf(c), access.project)) {
4139
4173
  return jsonError("owner access is required", 403);
4140
4174
  }
4175
+ if (access.artifact.slug.startsWith(THEME_PREFIX) && c.req.query("force") !== "1") {
4176
+ return jsonError("pages link to this theme; pass force to delete it", 409);
4177
+ }
4141
4178
  const prefix = `manifests/${access.artifact.slug}/`;
4142
4179
  const keys = [];
4143
4180
  let cursor;
@@ -4149,6 +4186,9 @@ function registerArtifactItem(app) {
4149
4186
  if (keys.length > 0) await c.env.BUCKET.delete(keys);
4150
4187
  await deleteArtifactData(c.env, access.artifact.slug);
4151
4188
  await c.env.DB.batch([
4189
+ c.env.DB.prepare(
4190
+ "INSERT INTO retired_slugs (slug, last_n, version_gen) SELECT slug, MAX(last_n, COALESCE(latest, 0)), version_gen + 1 FROM artifacts WHERE slug = ?1 ON CONFLICT(slug) DO UPDATE SET last_n = excluded.last_n, version_gen = excluded.version_gen"
4191
+ ).bind(access.artifact.slug),
4152
4192
  c.env.DB.prepare("DELETE FROM page_data WHERE slug = ?1").bind(access.artifact.slug),
4153
4193
  c.env.DB.prepare("DELETE FROM artifact_tags WHERE slug = ?1").bind(access.artifact.slug),
4154
4194
  c.env.DB.prepare("DELETE FROM versions WHERE slug = ?1").bind(access.artifact.slug),
@@ -4498,7 +4538,7 @@ __name(registerApi, "registerApi");
4498
4538
 
4499
4539
  // src/static.generated.ts
4500
4540
  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';
4541
+ 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
4542
  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
4543
 
4504
4544
  // src/static.ts
@@ -5403,6 +5443,7 @@ function VersionRowView(props) {
5403
5443
  const version = props.version;
5404
5444
  const served = props.artifact.served === version.n;
5405
5445
  const pinned = props.artifact.pinned === version.n;
5446
+ const deletable = props.owner && !props.artifact.slug.startsWith(THEME_PREFIX) && !pinned && props.count > 1;
5406
5447
  return /* @__PURE__ */ jsxDEV("li", { class: served ? "version-row is-served" : "version-row", children: [
5407
5448
  /* @__PURE__ */ jsxDEV("span", { class: "version-n mono", children: [
5408
5449
  "v",
@@ -5423,7 +5464,8 @@ function VersionRowView(props) {
5423
5464
  ] }),
5424
5465
  served ? /* @__PURE__ */ jsxDEV("span", { class: "pill pill-live", children: pinned ? "Pinned, live" : "Live" }) : null,
5425
5466
  /* @__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
5467
+ props.owner && !served ? /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn-link", "data-action": "pin", "data-n": version.n, children: "Pin here" }) : null,
5468
+ deletable ? /* @__PURE__ */ jsxDEV("button", { type: "button", class: "btn-link", "data-action": "delete-version", "data-n": version.n, children: "Delete" }) : null
5427
5469
  ] });
5428
5470
  }
5429
5471
  __name(VersionRowView, "VersionRowView");
@@ -5557,7 +5599,7 @@ function artifactPage(env, email, artifact, versions, shares, project, editableP
5557
5599
  /* @__PURE__ */ jsxDEV("h2", { children: "Versions" }),
5558
5600
  /* @__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
5601
  ] }),
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 })) }),
5602
+ 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
5603
  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
5604
  ]
5563
5605
  }
@@ -5627,7 +5669,7 @@ function artifactPage(env, email, artifact, versions, shares, project, editableP
5627
5669
  ] }) : null,
5628
5670
  owner ? /* @__PURE__ */ jsxDEV("div", { class: "danger-zone", children: [
5629
5671
  /* @__PURE__ */ jsxDEV("h2", { children: "Danger zone" }),
5630
- /* @__PURE__ */ jsxDEV(
5672
+ 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
5673
  "button",
5632
5674
  {
5633
5675
  type: "button",
@@ -6503,7 +6545,6 @@ __name(loginRedirect, "loginRedirect");
6503
6545
 
6504
6546
  // src/project.ts
6505
6547
  var THEME_SLUG = `${THEME_PREFIX}default`;
6506
- var THEME_VERSION = 3;
6507
6548
  var MODE_WORDS = {
6508
6549
  private: "Private",
6509
6550
  people: "People",
@@ -6513,15 +6554,15 @@ var LIST_STYLE = ".rshow-project{max-width:44rem}.rshow-project h1{margin-bottom
6513
6554
  async function defaultThemeReady(env) {
6514
6555
  const artifact = await env.DB.prepare("SELECT mode FROM artifacts WHERE slug = ?1").bind(THEME_SLUG).first();
6515
6556
  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();
6557
+ const version = await env.DB.prepare("SELECT n FROM versions WHERE slug = ?1 LIMIT 1").bind(THEME_SLUG).first();
6517
6558
  return version !== null;
6518
6559
  }
6519
6560
  __name(defaultThemeReady, "defaultThemeReady");
6520
6561
  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>`;
6562
+ const links = theme ? `<link rel="stylesheet" href="/_themes/default@latest/fonts.css">
6563
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
6564
+ <link rel="stylesheet" href="/_themes/default@latest/page.css">
6565
+ <script src="/_themes/default@latest/mode.js"><\/script>` : `<style>${MINIMAL_STYLE}</style>`;
6525
6566
  const descriptionHtml = description === "" ? "" : `
6526
6567
  <p class="rshow-desc">${escapeHtml(description)}</p>`;
6527
6568
  const listHtml = artifacts.length === 0 ? `
@@ -6593,7 +6634,8 @@ var CACHE_CONTROL2 = {
6593
6634
  publicLive: "public, max-age=0, s-maxage=15, must-revalidate",
6594
6635
  publicVersion: "public, max-age=31536000, immutable, s-maxage=300",
6595
6636
  theme: "public, max-age=31536000, immutable",
6596
- themeJson: "public, max-age=60"
6637
+ themeJson: "public, max-age=60",
6638
+ themeLatest: "public, max-age=60"
6597
6639
  };
6598
6640
  function viewerCsp(env) {
6599
6641
  return [
@@ -6719,7 +6761,7 @@ async function handleViewer(c) {
6719
6761
  if (identity.email === null) return loginRedirect(pathname, url.search);
6720
6762
  return forbiddenPage(identity.email);
6721
6763
  }
6722
- const version = versionN === null ? access.version : access.version && access.version.n === versionN ? access.version : await versionFor(env, slug, versionN);
6764
+ const version = versionN === null ? access.version : access.version && access.version.n === versionN ? access.version : await versionFor(env, artifact, versionN);
6723
6765
  if (!version) return notFoundPage("This artifact has no such version yet.");
6724
6766
  const relative = relativePath(parts);
6725
6767
  if (relative === null) return notFoundPage();
@@ -6743,11 +6785,11 @@ async function handleTheme(c, rest, url) {
6743
6785
  if (info) {
6744
6786
  if (rest.length !== 1) return withoutBodyOnHead(c, jsonError("not found", 404));
6745
6787
  const name2 = info[1];
6746
- const latest = await accessFor(env, `${THEME_PREFIX}${name2}`);
6747
- if (!latest || latest.artifact.mode !== MODE.public || !latest.version) {
6788
+ const latest2 = await accessFor(env, `${THEME_PREFIX}${name2}`);
6789
+ if (!latest2 || latest2.artifact.mode !== MODE.public || !latest2.version) {
6748
6790
  return withoutBodyOnHead(c, jsonError("not found", 404));
6749
6791
  }
6750
- const version2 = latest.version.n;
6792
+ const version2 = latest2.version.n;
6751
6793
  const reply = { name: name2, version: version2, url: `/${THEMES_SEGMENT}/${name2}@${version2}/` };
6752
6794
  return withoutBodyOnHead(
6753
6795
  c,
@@ -6757,6 +6799,15 @@ async function handleTheme(c, rest, url) {
6757
6799
  })
6758
6800
  );
6759
6801
  }
6802
+ const latest = THEME_LATEST_PATTERN.exec(segment);
6803
+ if (latest) {
6804
+ const name2 = latest[1];
6805
+ const access2 = await accessFor(env, `${THEME_PREFIX}${name2}`);
6806
+ if (!access2 || access2.artifact.mode !== MODE.public || !access2.version) return notFoundPage();
6807
+ const path = rest.slice(1).map(encodeURIComponent).join("/");
6808
+ const location = `/${THEMES_SEGMENT}/${name2}@${access2.version.n}/${path}${url.search}`;
6809
+ return withoutBodyOnHead(c, redirect(location, 302, CACHE_CONTROL2.themeLatest));
6810
+ }
6760
6811
  const match2 = THEME_VERSION_PATTERN.exec(segment);
6761
6812
  if (!match2) return notFoundPage();
6762
6813
  const name = match2[1];
@@ -6767,7 +6818,7 @@ async function handleTheme(c, rest, url) {
6767
6818
  if (!access || access.artifact.mode !== MODE.public) return notFoundPage();
6768
6819
  const parts = rest.slice(1);
6769
6820
  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);
6821
+ const version = access.version && access.version.n === n ? access.version : await versionFor(env, access.artifact, n);
6771
6822
  if (!version) return notFoundPage("This theme has no such version yet.");
6772
6823
  const relative = relativePath(parts);
6773
6824
  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);
@@ -0,0 +1,5 @@
1
+ CREATE TABLE retired_slugs (
2
+ slug TEXT PRIMARY KEY,
3
+ last_n INTEGER NOT NULL,
4
+ version_gen INTEGER NOT NULL
5
+ );