rshow 0.0.7 → 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.
package/dist/main.js CHANGED
@@ -38,7 +38,7 @@ import process from "node:process";
38
38
  // src/version.ts
39
39
  import { readFileSync } from "node:fs";
40
40
  import { fileURLToPath } from "node:url";
41
- var VERSION = true ? "0.0.7" : versionFromPackage();
41
+ var VERSION = true ? "0.0.8" : versionFromPackage();
42
42
 
43
43
  // src/binary.ts
44
44
  var ASSET_MANIFEST = "assets.json";
@@ -382,8 +382,12 @@ var Client = class {
382
382
  async patchArtifact(slug, patch) {
383
383
  return (await this.send("PATCH", `/api/artifacts/${encodeURIComponent(slug)}`, { json: patch })).body;
384
384
  }
385
- async deleteArtifact(slug) {
386
- await this.send("DELETE", `/api/artifacts/${encodeURIComponent(slug)}`);
385
+ async deleteArtifact(slug, force = false) {
386
+ await this.send("DELETE", `/api/artifacts/${encodeURIComponent(slug)}${force ? "?force=1" : ""}`);
387
+ }
388
+ async deleteVersion(slug, n, force = false) {
389
+ const pathname = `/api/artifacts/${encodeURIComponent(slug)}/versions/${n}${force ? "?force=1" : ""}`;
390
+ return (await this.send("DELETE", pathname)).body;
387
391
  }
388
392
  async putShare(slug, email, role) {
389
393
  const pathname = `/api/artifacts/${encodeURIComponent(slug)}/shares/${encodeURIComponent(email)}`;
@@ -444,6 +448,7 @@ var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
444
448
  var TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
445
449
  var THEME_NAME_PATTERN = /^[a-z0-9-]{1,40}$/;
446
450
  var THEME_VERSION_PATTERN = /^([a-z0-9-]{1,40})@([0-9]{1,9})$/;
451
+ var THEME_LATEST_PATTERN = /^([a-z0-9-]{1,40})@latest$/;
447
452
  var DATA_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
448
453
  var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
449
454
  var TOKEN_PREFIX = "rsh_";
@@ -991,7 +996,8 @@ commands:
991
996
  show <slug> artifact, versions and shares
992
997
  share <slug> set mode or change shares
993
998
  pin <slug> <n|live> serve version n, or live for the newest
994
- delete <slug> --yes delete an artifact
999
+ delete <slug> [--version n] [--force] --yes
1000
+ delete an artifact, or one version of it
995
1001
  login [--url u] save the API token for this account
996
1002
  logout [--url u] remove the saved token for a server
997
1003
  completion <shell> print a completion script for bash, zsh or fish
@@ -1041,8 +1047,10 @@ usage error. --offline skips the theme version lookup on the art host.
1041
1047
  errors:
1042
1048
  no index.html at the root, unless theme.json makes the folder a theme
1043
1049
  a local href, src, poster, srcset, CSS url() or @import with no file behind it
1044
- a root-relative reference other than /_themes/<name>@<n>/<path>
1045
- a /_themes/<name>@<n>/ path whose version the art host does not have
1050
+ a root-relative reference other than /_themes/<name>@<n>/<path> or
1051
+ /_themes/<name>@latest/<path>
1052
+ a /_themes/<name>@<n>/ path whose version the art host does not have, or an
1053
+ @latest whose theme it does not have
1046
1054
  a script from a host other than cdnjs.cloudflare.com, cdn.jsdelivr.net,
1047
1055
  unpkg.com, or one of those without an exact version (@1.2.3, /1.2.3/ on cdnjs)
1048
1056
  a stylesheet from a host other than fonts.googleapis.com, cdn.jsdelivr.net,
@@ -1101,9 +1109,9 @@ Style warnings:
1101
1109
  var PREVIEW_USAGE = `usage: rshow preview <dir> [--theme <dir>] [--offline] [--no-reload] [--port 4321]
1102
1110
 
1103
1111
  Serves <dir> on 127.0.0.1: index.html for a folder, content types for files.
1104
- Works offline: /_themes/<name>@<n>/ is served from --theme when it names that
1105
- theme, then from the built-in themes on disk, and only then proxied to the art
1106
- URL from config, which --offline turns off.
1112
+ Works offline: /_themes/<name>@<n>/ and /_themes/<name>@latest/ are served
1113
+ from --theme when it names that theme, then from the built-in themes on disk,
1114
+ and only then proxied to the art URL from config, which --offline turns off.
1107
1115
  The page is reloaded on its own when a file changes; --no-reload turns that off.
1108
1116
  Scripts that use <artifact>/_data/<key> get a local store in that same session.`;
1109
1117
  var CONFIG_USAGE = `usage: rshow config [dir] [--offline]
@@ -1112,9 +1120,10 @@ Prints the merged config as JSON: ~/.config/rshow/config.json first, then
1112
1120
  <dir>/rshow.json (default: the current folder). RSHOW_URL overrides url and
1113
1121
  RSHOW_ART overrides art. <dir>/rshow.json also takes project and tags; later
1114
1122
  files win. Run rshow init to write url and art.
1115
- A theme with no version, or @latest, is looked up on the art host and printed
1116
- as <name>@<n>; when that fails a warning goes to stderr. --offline skips it, and
1117
- so does a config with no art host.
1123
+ A theme with no version, or @latest, prints as <name>@latest, and with the
1124
+ network its lookup adds theme_resolved as <name>@<n>; when the lookup fails a
1125
+ warning goes to stderr and the key is left out. --offline prints the loaded
1126
+ config as it is, and so does a config with no art host.
1118
1127
  The token is RSHOW_TOKEN, else the token rshow login saved in the macOS
1119
1128
  Keychain, the Linux keyring or a file with mode 600.`;
1120
1129
  var INIT_USAGE = `usage: rshow init --url <admin-url> --art <viewer-url> [--theme <name>] [--force]
@@ -1174,11 +1183,11 @@ var THEME_PREVIEW_USAGE = `usage: rshow theme preview <dir> [--offline] [--no-re
1174
1183
 
1175
1184
  Builds the theme folder into a temporary folder, serves that on 127.0.0.1 and
1176
1185
  prints the URL of index.html, the preview page that shows every layout. The
1177
- built folder answers /_themes/<name>@<n>/ and is rebuilt, and the page reloaded,
1178
- when the folder changes; --no-reload turns the reload off. A /_themes/ path for
1179
- another theme comes from the built-in themes on disk, then from the art URL
1180
- from config, which --offline turns off. The temporary folder goes when the
1181
- server stops.`;
1186
+ built folder answers /_themes/<name>@<n>/ and /_themes/<name>@latest/ and is
1187
+ rebuilt, and the page reloaded, when the folder changes; --no-reload turns the
1188
+ reload off. A /_themes/ path for another theme comes from the built-in themes
1189
+ on disk, then from the art URL from config, which --offline turns off. The
1190
+ temporary folder goes when the server stops.`;
1182
1191
  var THEME_PUBLISH_USAGE = `usage: rshow theme publish <dir> [--label l] [--build cmd] [--no-check] [--force]
1183
1192
 
1184
1193
  <dir> needs theme.json with a name. The folder is published as theme-<name>,
@@ -1190,7 +1199,8 @@ built on top of its base theme, and the built folder is what gets published.
1190
1199
  When the newest version already holds the built folder's paths and hashes, it
1191
1200
  prints unchanged: theme-<name> is already v<n> and publishes nothing.
1192
1201
  --force publishes a new version anyway.
1193
- Prints /_themes/<name>@<n>/.`;
1202
+ Prints /_themes/<name>@<n>/. Pages link /_themes/<name>@latest/ by default, so
1203
+ they follow every version you publish here.`;
1194
1204
  var THEME_LIST_USAGE = `usage: rshow theme list
1195
1205
 
1196
1206
  Lists artifacts whose slug starts with theme- and their served version.`;
@@ -1258,9 +1268,13 @@ var SHARE_USAGE = `usage: rshow share <slug> [--mode private|people|public] [--a
1258
1268
  var PIN_USAGE = `usage: rshow pin <slug> <n|live>
1259
1269
 
1260
1270
  Pins version n, or live to serve the newest version again.`;
1261
- var DELETE_USAGE = `usage: rshow delete <slug> --yes
1271
+ var DELETE_USAGE = `usage: rshow delete <slug> [--version n] [--force] --yes
1262
1272
 
1263
- Deletes the artifact and needs --yes.`;
1273
+ Deletes the artifact, or with --version n only that version, and needs --yes.
1274
+ A theme or a theme version also needs --force, because pages link to it.
1275
+ A pinned version and the only version of an artifact cannot be deleted.
1276
+ A public version can stay visible for up to 5 minutes after the delete, and a
1277
+ theme file for longer.`;
1264
1278
  var LOGIN_USAGE = `usage: rshow login [--url u]
1265
1279
 
1266
1280
  Reads the token from stdin, checks it with the API, then saves it in the macOS
@@ -15984,7 +15998,7 @@ var EMOJI = new RegExp("\\p{Emoji_Presentation}", "gu");
15984
15998
  var COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?)\([^()]*\)/gi;
15985
15999
  var GOOGLE_URL = /fonts\.googleapis\.com\/[^"'\s)>]*/gi;
15986
16000
  var GOOGLE_FAMILY = /[?&]family=([^&"'\s]+)/gi;
15987
- var THEME_LINK = /\/_themes\/[a-z0-9][a-z0-9-]*@[0-9]+\//i;
16001
+ var THEME_LINK = /\/_themes\/[a-z0-9][a-z0-9-]*@(?:[0-9]+|latest)\//i;
15988
16002
  function stylePathSkipped(rel2) {
15989
16003
  const parts2 = rel2.toLowerCase().split("/");
15990
16004
  const name = parts2[parts2.length - 1] ?? "";
@@ -16372,7 +16386,7 @@ var VERSION_AT = /@[0-9]+\.[0-9]+\.[0-9]+/;
16372
16386
  var VERSION_PATH = /\/[0-9]+\.[0-9]+\.[0-9]+\//;
16373
16387
  var EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
16374
16388
  var IGNORED = /^(?:#|data:|blob:|mailto:|tel:|javascript:)/i;
16375
- var THEME_REF = /^\/_themes\/([a-z0-9][a-z0-9-]*)@([0-9]+)\//;
16389
+ var THEME_REF = /^\/_themes\/([a-z0-9][a-z0-9-]*)@(latest|[0-9]+)\//;
16376
16390
  var TAG_PATTERN2 = /<([a-zA-Z][a-zA-Z0-9:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
16377
16391
  var ATTR_PATTERN = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
16378
16392
  var CSS_URL = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)'"\s]*))\s*\)/gi;
@@ -16498,7 +16512,7 @@ function resolveRef(scan, file, raw, line) {
16498
16512
  file,
16499
16513
  line,
16500
16514
  "error",
16501
- `root-relative reference ${value} (only /_themes/<name>@<n>/ paths may start with a slash)`
16515
+ `root-relative reference ${value} (only /_themes/<name>@<n>/ or /_themes/<name>@latest/ paths may start with a slash)`
16502
16516
  );
16503
16517
  return;
16504
16518
  }
@@ -18093,7 +18107,7 @@ async function handleTheme(req, res, session, raw, pathname, method) {
18093
18107
  const rest = pathname.slice(PROXY_PREFIX.length);
18094
18108
  const slash = rest.indexOf("/");
18095
18109
  const segment = slash < 0 ? rest : rest.slice(0, slash);
18096
- const match = THEME_VERSION_PATTERN.exec(segment);
18110
+ const match = THEME_VERSION_PATTERN.exec(segment) ?? THEME_LATEST_PATTERN.exec(segment);
18097
18111
  if (match === null) {
18098
18112
  if (session.art !== "" && !session.offline) {
18099
18113
  await proxy(session.art, raw, method, res);
@@ -18488,15 +18502,16 @@ async function resolveTheme2(config) {
18488
18502
  const theme = config.theme ?? "default";
18489
18503
  const ref = themeRefOf(theme);
18490
18504
  if (ref.version !== void 0 && ref.version !== "latest") return config;
18505
+ const latest = { ...config, theme: `${ref.name}@latest` };
18491
18506
  if (config.art === void 0) {
18492
18507
  warn("no art host configured; skipping theme version lookup");
18493
- return config;
18508
+ return config.theme === void 0 ? config : latest;
18494
18509
  }
18495
18510
  try {
18496
- return { ...config, theme: await resolvedTheme(config.art, theme) };
18511
+ return { ...latest, theme_resolved: await resolvedTheme(config.art, theme) };
18497
18512
  } catch (err) {
18498
18513
  warn(`cannot resolve theme ${ref.name}: ${messageOf8(err)}`);
18499
- return config.theme === void 0 ? config : { ...config, theme: ref.name };
18514
+ return config.theme === void 0 ? config : latest;
18500
18515
  }
18501
18516
  }
18502
18517
  async function run4(argv, ctx) {
@@ -19473,13 +19488,35 @@ async function run12(argv, ctx) {
19473
19488
 
19474
19489
  // src/commands/delete.ts
19475
19490
  async function run13(argv, ctx) {
19476
- const args = parseCommandArgs(argv, { yes: { type: "boolean" } }, 1, 1, DELETE_USAGE);
19491
+ const args = parseCommandArgs(
19492
+ argv,
19493
+ { version: { type: "string" }, force: { type: "boolean" }, yes: { type: "boolean" } },
19494
+ 1,
19495
+ 1,
19496
+ DELETE_USAGE
19497
+ );
19477
19498
  const slug = validateSlug(args.positionals[0]);
19499
+ const raw = stringValue(args, "version");
19500
+ let version2 = null;
19501
+ if (raw !== void 0) {
19502
+ if (!/^[0-9]+$/.test(raw) || Number(raw) < 1) {
19503
+ throw new UsageError(`expected a version number, got: ${raw}`);
19504
+ }
19505
+ version2 = Number(raw);
19506
+ }
19478
19507
  if (!boolValue(args, "yes")) throw new UsageError(`delete needs --yes (try: ${usageLine(DELETE_USAGE)})`);
19479
19508
  const api = await ctx.api();
19480
- await api.deleteArtifact(slug);
19481
- ctx.out.data(null);
19482
- ctx.out.line(`deleted: ${slug}`);
19509
+ const force = boolValue(args, "force");
19510
+ if (version2 === null) {
19511
+ await api.deleteArtifact(slug, force);
19512
+ ctx.out.data(null);
19513
+ ctx.out.line(`deleted: ${slug}`);
19514
+ return;
19515
+ }
19516
+ const artifact = await api.deleteVersion(slug, version2, force);
19517
+ ctx.out.data(artifact);
19518
+ ctx.out.line(`deleted: ${slug} v${version2}`);
19519
+ ctx.out.line(`served: ${artifact?.served === null || artifact?.served === void 0 ? "-" : artifact.served}`);
19483
19520
  }
19484
19521
 
19485
19522
  // src/commands/project.ts
@@ -22713,8 +22750,15 @@ function listLines(usage) {
22713
22750
  if (start < 0) return [];
22714
22751
  const out = [];
22715
22752
  for (const line of lines.slice(start)) {
22716
- if (!/^\s{2}\S/.test(line)) break;
22717
- out.push(line);
22753
+ if (/^\s{2}\S/.test(line)) {
22754
+ out.push(line);
22755
+ continue;
22756
+ }
22757
+ if (out.length > 0 && /^\s{3,}\S/.test(line)) {
22758
+ out[out.length - 1] = `${out[out.length - 1].trimEnd()} ${line.trim()}`;
22759
+ continue;
22760
+ }
22761
+ break;
22718
22762
  }
22719
22763
  return out;
22720
22764
  }
@@ -23483,10 +23527,12 @@ function fail(err) {
23483
23527
  async function main2(argv) {
23484
23528
  const wantsJson = argv.includes("--json");
23485
23529
  const args = argv.filter((arg) => arg !== "--json");
23486
- const wantsVersion = args.includes("--version") || args.includes("-v");
23487
23530
  const wantsHelp = args.includes("--help") || args.includes("-h");
23488
- const name = args.find((arg) => !arg.startsWith("-"));
23531
+ const commandAt = args.findIndex((arg) => !arg.startsWith("-"));
23532
+ const name = commandAt < 0 ? void 0 : args[commandAt];
23489
23533
  const spec = name === void 0 ? void 0 : COMMANDS[name];
23534
+ const versionAt = args.findIndex((arg) => arg === "--version" || arg === "-v");
23535
+ const wantsVersion = versionAt >= 0 && (commandAt < 0 || versionAt < commandAt);
23490
23536
  if (wantsVersion) {
23491
23537
  const suffix = await isBinary() ? " (binary)" : "";
23492
23538
  process23.stdout.write(`rshow ${VERSION}${suffix}
@@ -23506,8 +23552,7 @@ async function main2(argv) {
23506
23552
  if (spec === void 0) {
23507
23553
  throw new UsageError(`unknown command: ${name} (try: rshow --help)`);
23508
23554
  }
23509
- const index = args.indexOf(name);
23510
- const rest = [...args.slice(0, index), ...args.slice(index + 1)];
23555
+ const rest = [...args.slice(0, commandAt), ...args.slice(commandAt + 1)];
23511
23556
  const configs = /* @__PURE__ */ new Map();
23512
23557
  const clients = /* @__PURE__ */ new Map();
23513
23558
  const keyOf = (dir) => dir === void 0 ? "" : path26.resolve(dir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rshow",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Publish static artifacts to rshow",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rslib.github.io/rshow",
@@ -20,7 +20,7 @@ Copy this checklist into the reply and tick it as you go.
20
20
 
21
21
  ```text
22
22
  - [ ] shape picked and outline or headings agreed
23
- - [ ] page written, theme linked at a pinned version
23
+ - [ ] page written, theme linked at `@latest`
24
24
  - [ ] rshow check <dir> exits 0
25
25
  - [ ] rshow preview <dir> looked at 360px wide and at desktop width
26
26
  - [ ] audience confirmed with the person
@@ -30,7 +30,7 @@ Copy this checklist into the reply and tick it as you go.
30
30
  ```
31
31
 
32
32
  1. Pick the shape and say it back in one sentence, such as a page, a report, a poster, a project page, a dashboard, a mockup, a sketch, a terminal page or a prerendered Astro or Vite site.
33
- 2. Write `index.html` in its own folder, link the theme at a pinned version and keep every other path relative.
33
+ 2. Write `index.html` in its own folder, link the theme at `@latest` and keep every other path relative.
34
34
  3. Run `rshow check <dir>`.
35
35
  4. Fix every error, then run `rshow check` again until it exits 0.
36
36
  5. Run `rshow preview <dir>` and open the printed `http://127.0.0.1:4321` link.
@@ -46,7 +46,10 @@ Copy this checklist into the reply and tick it as you go.
46
46
  - Every path inside the artifact is relative, with no leading slash.
47
47
  The only exceptions are `/_themes/...` and `/_data/...`.
48
48
  - Link the theme, never copy it.
49
- The stylesheet links and `mode.js` in the head all carry the same `<name>@<n>` version.
49
+ The stylesheet links and `mode.js` in the head all carry the same `<name>@latest`.
50
+ - Link `@latest`, so a theme fix reaches the page with no republish.
51
+ Write a number only when the person asks to freeze a page, such as a talk on the day it is given.
52
+ A page frozen on a number needs 8 or newer for math.
50
53
  - The theme is the base, not a cage.
51
54
  A page may carry one inline `<style>` block with its own classes and its own layouts when the content needs them.
52
55
  Own CSS takes colour, type and space from the theme tokens with `var(--...)`, so light, dark, print and a theme swap keep working.
@@ -63,7 +66,6 @@ Copy this checklist into the reply and tick it as you go.
63
66
  - A report links `report.css` and `report.js` in place of `page.css`, and `mode.js` still goes in the head.
64
67
  Read [reports](reference/reports.md) before you write one.
65
68
  - Math is LaTeX in the text of an element with the class `math`, and the page links `math.css` and `math.js` for it.
66
- Math needs theme version 8 or newer, so a page on an older version moves up with `rshow config <dir>`.
67
69
  A `div.math` is display math and any other element is inline.
68
70
  A `$` in the prose is a price and is never read as math, so `$5` stays text.
69
71
  Read [reports](reference/reports.md) for the markup an equation takes.
@@ -154,4 +156,4 @@ The short list above is a hard rule.
154
156
  Write the LaTeX in an element with the class `math` and link `math.css` and `math.js`.
155
157
  - Never create a second artifact with a `-v2` slug.
156
158
  Publish the same slug again.
157
- - Never delete an artifact or drop a share without a yes from the person.
159
+ - Never delete a version, a theme or an artifact, and never drop a share, unless the person asks for it.
@@ -25,7 +25,7 @@ This file is the whole surface, both config files and the exit codes.
25
25
  | `rshow check <file-or-dir>` | apply the page contract to the files a publish would upload |
26
26
  | `rshow preview <dir>` | serve a folder on `127.0.0.1` for a look, offline |
27
27
  | `rshow publish <file-or-dir>` | upload a folder, or one file as `index.html`, as a new version |
28
- | `rshow config [dir] [--offline]` | print the merged config as JSON, with `theme` resolved to its current version, such as `default@3` |
28
+ | `rshow config [dir] [--offline]` | print the merged config as JSON, with `theme` as `<name>@latest` and, with the network, `theme_resolved` as the version it resolves to |
29
29
  | `rshow init --url <admin-url> --art <viewer-url>` | write the global config file, with `--theme` and `--force` |
30
30
  | `rshow doctor [dir]` | check node, config, token, api, art, theme and skills, one `ok\|warn\|fail` line each |
31
31
  | `rshow list` | list artifacts you own or that are shared with you, with `--project`, `--tag` and `--q` filters |
@@ -36,7 +36,7 @@ This file is the whole surface, both config files and the exit codes.
36
36
  | `rshow tag <slug>` | add or remove tags on an artifact |
37
37
  | `rshow handoff <slug>@v<n>` | write a design handoff folder from a published design |
38
38
  | `rshow compare --against <dir>` | compare an implementation with that handoff folder |
39
- | `rshow delete <slug> --yes` | delete an artifact |
39
+ | `rshow delete <slug> [--version n] [--force] --yes` | delete an artifact, or one version of it |
40
40
  | `rshow theme new <name>` | write a preset folder that extends a built-in theme |
41
41
  | `rshow theme check <dir>` | check a theme or preset folder against the token contract |
42
42
  | `rshow theme preview <dir>` | build the theme, then serve its preview page on `127.0.0.1` |
@@ -77,7 +77,7 @@ They are never errors, because a person may have a reason.
77
77
  `--no-style` skips all of them, and `--json` prints the findings as data.
78
78
 
79
79
  `--offline` skips every request the check would make, so it works on a plane and it never waits on the host.
80
- A published page that links `/_themes/<name>@<n>/` needs the network to confirm that version exists, so a `--offline` run reports those links as unchecked rather than as a pass.
80
+ A published page that links `/_themes/<name>@<n>/` or `/_themes/<name>@latest/` needs the network to confirm the theme exists, so a `--offline` run reports those links as unchecked rather than as a pass.
81
81
 
82
82
  `rshow publish` runs the check first and stops while there is an error, so a page that fails the contract does not go live.
83
83
  `--no-check` skips that step.
@@ -90,7 +90,7 @@ rshow preview <dir> [--theme <dir>] [--offline] [--no-reload] [--port 4321]
90
90
  ```
91
91
 
92
92
  It serves `<dir>` on `127.0.0.1`, prints `http://127.0.0.1:<port>` and keeps running until you stop it.
93
- It works offline: `/_themes/<name>@<n>/` comes from `--theme` when that folder is the theme, then from the built-in theme on disk, and only then from the art URL in the config, which `--offline` turns off.
93
+ It works offline: `/_themes/<name>@<n>/` and `/_themes/<name>@latest/` come from `--theme` when that folder is the theme, then from the built-in theme on disk, and only then from the art URL in the config, which `--offline` turns off.
94
94
  A page that uses `<artifact>/_data/<key>` gets a local store for the session, and the page reloads by itself when a file changes, which `--no-reload` turns off.
95
95
 
96
96
  ## publish
@@ -174,8 +174,10 @@ Set the tags and the project in `rshow.json` when they are a fact about the fold
174
174
  `rshow theme publish <dir> [--label l] [--build cmd]` needs a `theme.json` with a `name`, publishes the folder as `theme-<name>`, sets it public and prints `/_themes/<name>@<n>/`.
175
175
  `rshow theme list` prints the theme artifacts and the version each one serves.
176
176
  `rshow show <slug>` prints the artifact, its versions, its shares, its pin and its URLs.
177
- `rshow delete <slug> --yes` deletes the artifact and every version.
177
+ `rshow delete <slug> --yes` deletes the artifact and every version, and `--version <n>` deletes only that version.
178
178
  It needs `--yes`, and it needs a yes from the person.
179
+ A theme or a theme version also needs `--force`, because pages link to it.
180
+ A public version can stay visible for up to 5 minutes after the delete, and a theme file for longer, so tell the person this when they delete to take something down.
179
181
 
180
182
  A preset is a small folder whose `theme.json` carries `"extends": "default"` and only token and font overrides.
181
183
  `rshow theme new <name> [--dir <parent>] [--from default]` writes it with both modes pre-filled with the base values of `--bg`, `--fg`, `--muted`, `--accent` and `--layer-01`, and prints the path plus `next: rshow theme check <dir>`.
@@ -238,7 +240,7 @@ Later files win, and the environment beats both.
238
240
  | `art` | the art base the preview proxies `/_themes/` to, set by `rshow init` or `RSHOW_ART`; without it a theme lookup is skipped with a warning |
239
241
  | `slug` | the artifact to publish to, overridden by `--slug` |
240
242
  | `title` | the human title, overridden by `--title` |
241
- | `theme` | the theme a page should link: `<name>`, `<name>@latest` or a fixed `<name>@<n>`. `rshow config` prints it as `<name>@<n>` and you apply it by hand |
243
+ | `theme` | the theme a page should link: `<name>`, `<name>@latest` or a fixed `<name>@<n>`. Agents link `<name>@latest` and `rshow config` keeps it that way, with `theme_resolved` beside it when the lookup works |
242
244
  | `tokens` | token overrides recorded and printed by `rshow config`. No command writes them into a page, so apply them by hand |
243
245
  | `build` | the command `publish` runs in the folder first |
244
246
  | `out` | the folder under `<dir>` to publish, default `dist` |
@@ -30,11 +30,11 @@ A dashboard that nobody filters is a report with extra steps.
30
30
  <meta charset="utf-8">
31
31
  <meta name="viewport" content="width=device-width, initial-scale=1">
32
32
  <title>Write-ahead log, results per run</title>
33
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
34
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
35
- <link rel="stylesheet" href="/_themes/default@3/dashboard.css">
36
- <script src="/_themes/default@3/mode.js"></script>
37
- <script defer src="/_themes/default@3/dashboard.js"></script>
33
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
34
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
35
+ <link rel="stylesheet" href="/_themes/default@latest/dashboard.css">
36
+ <script src="/_themes/default@latest/mode.js"></script>
37
+ <script defer src="/_themes/default@latest/dashboard.js"></script>
38
38
  </head>
39
39
  <body>
40
40
  <div class="dashboard" data-src="results.csv" data-facet="workload"
@@ -8,7 +8,7 @@ It reads the files a publish would upload, so for a prerendered site it reads th
8
8
 
9
9
  - One `index.html` at the root of the folder you publish.
10
10
  - Every other path inside the artifact is relative and has no leading slash, such as `chart.svg`.
11
- - The two root-relative prefixes are `/_themes/<name>@<n>/` and `/_data/`, and both point at the host on purpose.
11
+ - The root-relative prefixes are `/_themes/<name>@latest/`, `/_themes/<name>@<n>/` and `/_data/`, and all point at the host on purpose.
12
12
  - At most 2000 files and 100 MiB per version, and at most 25 MiB per file.
13
13
  - No theme file in the artifact.
14
14
  No `deck.css`, no `page.css`, no font file, no `assets/` copy of a theme.
@@ -34,6 +34,7 @@ The theme files under `/_themes/` count as `'self'`, so a linked theme needs no
34
34
  ## CDN builds, pinned
35
35
 
36
36
  Pin the exact version in the URL and never write `@latest` or a moving tag, which `rshow check` counts as an error.
37
+ A theme link is the one moving link that is allowed, so `/_themes/<name>@latest/` is right and a number is for a frozen page.
37
38
 
38
39
  | Library | Pinned URL |
39
40
  |---|---|
@@ -27,8 +27,8 @@ A single file published on its own becomes `index.html` and nothing else is uplo
27
27
 
28
28
  ## The skeleton
29
29
 
30
- Replace `<name>@<n>` with the `theme` value that `rshow config <dir>` prints, which the server resolves to the current version. Use `default@3` only when `rshow config` warns that it cannot resolve the theme.
31
- Every link carries the same version on purpose, because a mixed version renders somewhere between two designs.
30
+ Link the theme at `@latest`, so a theme fix reaches the page with no republish. Write a number only when the person asks to freeze a page, such as a talk on the day it is given. A page frozen on a number needs 8 or newer for math.
31
+ Every link names the same theme on purpose, and all of them use `@latest` or all use the same number, because a mix of two versions renders somewhere between two designs.
32
32
 
33
33
  ```html
34
34
  <!doctype html>
@@ -37,10 +37,10 @@ Every link carries the same version on purpose, because a mixed version renders
37
37
  <meta charset="utf-8">
38
38
  <meta name="viewport" content="width=device-width, initial-scale=1">
39
39
  <title>Q3 latency report</title>
40
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
41
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
42
- <link rel="stylesheet" href="/_themes/default@3/page.css">
43
- <script src="/_themes/default@3/mode.js"></script>
40
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
41
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
42
+ <link rel="stylesheet" href="/_themes/default@latest/page.css">
43
+ <script src="/_themes/default@latest/mode.js"></script>
44
44
  <style>:root{--accent: #0f62fe;}</style>
45
45
  </head>
46
46
  <body>
@@ -164,7 +164,7 @@ Put the same thing in `rshow.json` when it should not be retyped:
164
164
  {
165
165
  "slug": "status",
166
166
  "title": "Cluster status",
167
- "theme": "default@3",
167
+ "theme": "default@latest",
168
168
  "build": "npm run build",
169
169
  "out": "dist"
170
170
  }
@@ -37,12 +37,12 @@ The status line beside the buttons always names the width and the height, so the
37
37
  <meta charset="utf-8">
38
38
  <meta name="viewport" content="width=device-width, initial-scale=1">
39
39
  <title>Immutable artifacts for reproducible figures</title>
40
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
41
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
42
- <link rel="stylesheet" href="/_themes/default@3/poster.css">
43
- <script src="/_themes/default@3/mode.js"></script>
40
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
41
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
42
+ <link rel="stylesheet" href="/_themes/default@latest/poster.css">
43
+ <script src="/_themes/default@latest/mode.js"></script>
44
44
  <script defer src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.js"></script>
45
- <script defer src="/_themes/default@3/poster.js"></script>
45
+ <script defer src="/_themes/default@latest/poster.js"></script>
46
46
  </head>
47
47
  <body>
48
48
 
@@ -38,11 +38,11 @@ Link `fonts.css`, `tokens.css` and `project.css`, then `mode.js` in the head and
38
38
  <meta charset="utf-8">
39
39
  <meta name="viewport" content="width=device-width, initial-scale=1">
40
40
  <title>Immutable artifacts for reproducible figures</title>
41
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
42
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
43
- <link rel="stylesheet" href="/_themes/default@3/project.css">
44
- <script src="/_themes/default@3/mode.js"></script>
45
- <script defer src="/_themes/default@3/project.js"></script>
41
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
42
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
43
+ <link rel="stylesheet" href="/_themes/default@latest/project.css">
44
+ <script src="/_themes/default@latest/mode.js"></script>
45
+ <script defer src="/_themes/default@latest/project.js"></script>
46
46
  </head>
47
47
  <body>
48
48
  <div class="project">
@@ -35,11 +35,11 @@ Link `fonts.css`, `tokens.css` and `report.css`, then `mode.js` in the head and
35
35
  <meta charset="utf-8">
36
36
  <meta name="viewport" content="width=device-width, initial-scale=1">
37
37
  <title>Publishing a folder as an immutable artifact</title>
38
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
39
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
40
- <link rel="stylesheet" href="/_themes/default@3/report.css">
41
- <script src="/_themes/default@3/mode.js"></script>
42
- <script defer src="/_themes/default@3/report.js"></script>
38
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
39
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
40
+ <link rel="stylesheet" href="/_themes/default@latest/report.css">
41
+ <script src="/_themes/default@latest/mode.js"></script>
42
+ <script defer src="/_themes/default@latest/report.js"></script>
43
43
  </head>
44
44
  <body>
45
45
  <div class="report">
@@ -23,11 +23,11 @@ A sketch is also the honest shape for a workshop, where a tidy drawing would end
23
23
  ## Markup
24
24
 
25
25
  ```html
26
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
27
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
28
- <link rel="stylesheet" href="/_themes/default@3/sketch.css">
29
- <script src="/_themes/default@3/mode.js"></script>
30
- <script defer src="/_themes/default@3/sketch.js"></script>
26
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
27
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
28
+ <link rel="stylesheet" href="/_themes/default@latest/sketch.css">
29
+ <script src="/_themes/default@latest/mode.js"></script>
30
+ <script defer src="/_themes/default@latest/sketch.js"></script>
31
31
  ...
32
32
  <div class="sketch" data-src="drawing.excalidraw" data-store="sketch-main"></div>
33
33
  ```
@@ -26,11 +26,11 @@ The viewer and the TUI kit are the theme's, and both are guided kits: the frame
26
26
  ## The viewer
27
27
 
28
28
  ```html
29
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
30
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
31
- <link rel="stylesheet" href="/_themes/default@3/terminal.css">
32
- <script src="/_themes/default@3/mode.js"></script>
33
- <script defer src="/_themes/default@3/terminal.js"></script>
29
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
30
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
31
+ <link rel="stylesheet" href="/_themes/default@latest/terminal.css">
32
+ <script src="/_themes/default@latest/mode.js"></script>
33
+ <script defer src="/_themes/default@latest/terminal.js"></script>
34
34
  ...
35
35
  <figure class="terminal" data-mode="ansi" data-src="capture.ans"
36
36
  data-cols="100" data-rows="30" data-title="procview" data-palette="default"></figure>
@@ -32,8 +32,7 @@ A topic on its own is not a slide.
32
32
 
33
33
  ## 2. Skeleton
34
34
 
35
- Replace `<name>@<n>` with the `theme` value that `rshow config <dir>` prints, which the server resolves to the current version. Use `default@3` only when `rshow config` warns that it cannot resolve the theme.
36
- Math needs theme version 8 or newer, so a page on an older version moves up with `rshow config <dir>`.
35
+ Link the theme at `@latest`, so a theme fix reaches the deck with no republish. Write a number only when the person asks to freeze a deck, such as a talk on the day it is given. A deck frozen on a number needs 8 or newer for math.
37
36
 
38
37
  ```html
39
38
  <!doctype html>
@@ -42,12 +41,12 @@ Math needs theme version 8 or newer, so a page on an older version moves up with
42
41
  <meta charset="utf-8">
43
42
  <meta name="viewport" content="width=device-width, initial-scale=1">
44
43
  <title>Talk title</title>
45
- <link rel="stylesheet" href="/_themes/default@3/fonts.css">
46
- <link rel="stylesheet" href="/_themes/default@3/tokens.css">
47
- <link rel="stylesheet" href="/_themes/default@3/deck.css">
44
+ <link rel="stylesheet" href="/_themes/default@latest/fonts.css">
45
+ <link rel="stylesheet" href="/_themes/default@latest/tokens.css">
46
+ <link rel="stylesheet" href="/_themes/default@latest/deck.css">
48
47
  <!-- a deck that writes math links these two, and math.js goes before deck.js -->
49
- <link rel="stylesheet" href="/_themes/default@3/math.css">
50
- <script defer src="/_themes/default@3/math.js"></script>
48
+ <link rel="stylesheet" href="/_themes/default@latest/math.css">
49
+ <script defer src="/_themes/default@latest/math.js"></script>
51
50
  </head>
52
51
  <body>
53
52
  <main class="deck">
@@ -69,13 +68,12 @@ Math needs theme version 8 or newer, so a page on an older version moves up with
69
68
  </section>
70
69
 
71
70
  </main>
72
- <script src="/_themes/default@3/deck.js"></script>
71
+ <script src="/_themes/default@latest/deck.js"></script>
73
72
  </body>
74
73
  </html>
75
74
  ```
76
75
 
77
- The three stylesheet links and the script carry the same `<n>` on purpose, because a mixed version renders somewhere between two designs.
78
- A fallback to `default@3` keeps the page readable but has no math, so a deck on that version links neither `math.css` nor `math.js`, and the `math.js` warning of `rshow check` is the sign to move up.
76
+ The three stylesheet links and the script carry `@latest` on purpose, so a theme fix reaches every deck on the next open.
79
77
  The deck needs no `mode.js`, because `deck.js` reads the same key and `t` does the same work.
80
78
  Override a token in one `:root` block after the theme links when the deck needs its own accent, with no limit on the declarations.
81
79
  An inline `<style>` block after the theme links may also carry a layout of the deck's own, as section 3 says.
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);