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 +82 -37
- package/package.json +1 -1
- package/skills/rshow/SKILL.md +7 -5
- package/skills/rshow/reference/cli.md +8 -6
- package/skills/rshow/reference/dashboard.md +5 -5
- package/skills/rshow/reference/page-contract.md +2 -1
- package/skills/rshow/reference/pages.md +7 -7
- package/skills/rshow/reference/poster.md +5 -5
- package/skills/rshow/reference/project-page.md +5 -5
- package/skills/rshow/reference/reports.md +5 -5
- package/skills/rshow/reference/sketch.md +5 -5
- package/skills/rshow/reference/terminal.md +5 -5
- package/skills/rshow-slides/SKILL.md +8 -10
- package/worker/index.js +118 -46
- package/worker/migrations/0005_version_counters.sql +3 -0
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.
|
|
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> --
|
|
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
|
-
|
|
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>/
|
|
1105
|
-
theme, then from the built-in themes on disk,
|
|
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,
|
|
1116
|
-
as <name>@<n>; when
|
|
1117
|
-
|
|
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
|
|
1178
|
-
when the folder changes; --no-reload turns the
|
|
1179
|
-
another theme comes from the built-in themes
|
|
1180
|
-
from config, which --offline turns off. 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]
|
|
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 { ...
|
|
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 :
|
|
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(
|
|
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
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
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 (
|
|
22717
|
-
|
|
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
|
|
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
|
|
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
package/skills/rshow/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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>/`
|
|
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`
|
|
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@
|
|
34
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
35
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
36
|
-
<script src="/_themes/default@
|
|
37
|
-
<script defer src="/_themes/default@
|
|
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
|
|
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
|
-
|
|
31
|
-
Every link
|
|
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@
|
|
41
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
42
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
43
|
-
<script src="/_themes/default@
|
|
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@
|
|
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@
|
|
41
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
42
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
43
|
-
<script src="/_themes/default@
|
|
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@
|
|
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@
|
|
42
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
43
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
44
|
-
<script src="/_themes/default@
|
|
45
|
-
<script defer src="/_themes/default@
|
|
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@
|
|
39
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
40
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
41
|
-
<script src="/_themes/default@
|
|
42
|
-
<script defer src="/_themes/default@
|
|
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@
|
|
27
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
28
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
29
|
-
<script src="/_themes/default@
|
|
30
|
-
<script defer src="/_themes/default@
|
|
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@
|
|
30
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
31
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
32
|
-
<script src="/_themes/default@
|
|
33
|
-
<script defer src="/_themes/default@
|
|
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
|
-
|
|
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@
|
|
46
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
47
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
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@
|
|
50
|
-
<script defer src="/_themes/default@
|
|
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@
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
3340
|
-
|
|
3341
|
-
|
|
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,
|
|
3491
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
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
|
|
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@
|
|
6522
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
6523
|
-
<link rel="stylesheet" href="/_themes/default@
|
|
6524
|
-
<script src="/_themes/default@
|
|
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,
|
|
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
|
|
6747
|
-
if (!
|
|
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 =
|
|
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,
|
|
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();
|