@dcl-regenesislabs/artifacts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -0
- package/cli/artifacts.mjs +706 -0
- package/package.json +48 -0
- package/shared/artifact-id.js +175 -0
- package/templates/artifact/assets/dcl.css +279 -0
- package/templates/artifact/assets/dcl.js +62 -0
- package/templates/artifact/index.html +161 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dcl-regenesislabs/artifacts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Publish static artifacts to artifacts.dclregenesislabs.xyz — sign in with a work email, push a folder, get a link back",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dcl-artifacts": "cli/artifacts.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/",
|
|
11
|
+
"shared/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/dcl-regenesislabs/ia-generated-artifacts.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://artifacts.dclregenesislabs.xyz",
|
|
22
|
+
"bugs": "https://github.com/dcl-regenesislabs/ia-generated-artifacts/issues",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "wrangler dev",
|
|
28
|
+
"deploy": "wrangler deploy",
|
|
29
|
+
"tail": "wrangler tail",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"push": "node ./cli/artifacts.mjs push",
|
|
32
|
+
"smoke": "./scripts/smoke.sh",
|
|
33
|
+
"skill:install": "./scripts/install-skill.sh",
|
|
34
|
+
"guide": "node ./scripts/build-guide.mjs",
|
|
35
|
+
"dist": "node ./scripts/build-dist.mjs",
|
|
36
|
+
"predev": "node ./scripts/build-dist.mjs",
|
|
37
|
+
"predeploy": "node ./scripts/build-dist.mjs"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@cloudflare/workers-types": "^5.20260831.1",
|
|
41
|
+
"typescript": "^5.6.0",
|
|
42
|
+
"wrangler": "^4.56.0"
|
|
43
|
+
},
|
|
44
|
+
"allowScripts": {
|
|
45
|
+
"esbuild@0.28.1": true,
|
|
46
|
+
"workerd@1.20260831.1": true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for artifact ids, version hashes and manifests.
|
|
3
|
+
*
|
|
4
|
+
* Imported by BOTH the Worker (TypeScript, bundled by wrangler) and the Node
|
|
5
|
+
* CLI, so the client and the server can never disagree about what an id or a
|
|
6
|
+
* manifest looks like.
|
|
7
|
+
*
|
|
8
|
+
* An artifact id is a random UUID v4 minted by whoever creates the artifact.
|
|
9
|
+
* It never changes; new pushes to the same id become new *versions*. Each
|
|
10
|
+
* version is identified inside the artifact by a content hash of its file
|
|
11
|
+
* list, which is what makes re-pushing identical bytes a no-op.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const MANIFEST_VERSION = 2;
|
|
15
|
+
export const MAX_PATH_LEN = 512;
|
|
16
|
+
|
|
17
|
+
/** Artifact id: UUID v4, lowercase. */
|
|
18
|
+
export const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
19
|
+
/** Ids from before versioning existed — only so they can still be deleted. */
|
|
20
|
+
export const LEGACY_ID_RE = /^[0-9a-f]{16}$/;
|
|
21
|
+
/** `{id}@{n}` pins one version in a URL. */
|
|
22
|
+
export const VERSION_SUFFIX_RE = /^@([1-9][0-9]{0,5})$/;
|
|
23
|
+
|
|
24
|
+
/** Paths are POSIX-style, relative, no `.`/`..` segments, no leading slash. */
|
|
25
|
+
export const PATH_RE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))[^\0]+$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A top-level path starting with `__` is reserved for the host: `__ui/...`
|
|
29
|
+
* is the in-page API, and the old layout kept its records under such names.
|
|
30
|
+
*/
|
|
31
|
+
export const RESERVED_PREFIX = "__";
|
|
32
|
+
|
|
33
|
+
/** Public share aliases: 16 random bytes, base64url, no padding. */
|
|
34
|
+
export const ALIAS_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
35
|
+
|
|
36
|
+
export function newId() {
|
|
37
|
+
return crypto.randomUUID();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `n` random bytes as unpadded base64url. Share aliases, login codes, upload
|
|
42
|
+
* tokens and the PKCE verifier all come from here, so there is one place to
|
|
43
|
+
* look at when asking how much entropy any of them has.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} n
|
|
46
|
+
*/
|
|
47
|
+
export function randomB64url(n) {
|
|
48
|
+
return b64url(crypto.getRandomValues(new Uint8Array(n)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {ArrayBuffer | Uint8Array} buf */
|
|
52
|
+
export function b64url(buf) {
|
|
53
|
+
const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
54
|
+
let binary = "";
|
|
55
|
+
for (const b of view) binary += String.fromCharCode(b);
|
|
56
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The PKCE challenge, computed identically on both sides of the login so the
|
|
61
|
+
* two can never disagree about the encoding.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} value
|
|
64
|
+
* @returns {Promise<string>}
|
|
65
|
+
*/
|
|
66
|
+
export async function sha256B64url(value) {
|
|
67
|
+
return b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The short code shown both in the terminal and on the login confirm page,
|
|
72
|
+
* derived from the PKCE challenge that both sides already hold. It is not a
|
|
73
|
+
* secret — it is something the person has to *match*, so a page someone else
|
|
74
|
+
* mailed them has nothing to match against and the right move is obvious.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} challenge
|
|
77
|
+
* @returns {Promise<string>} e.g. `K7QM-3F`
|
|
78
|
+
*/
|
|
79
|
+
export async function userCode(challenge) {
|
|
80
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge));
|
|
81
|
+
const bytes = new Uint8Array(digest);
|
|
82
|
+
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no O/0, no I/1
|
|
83
|
+
let out = "";
|
|
84
|
+
for (let i = 0; i < 6; i++) out += alphabet[bytes[i] % alphabet.length];
|
|
85
|
+
return `${out.slice(0, 4)}-${out.slice(4)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Generates a share alias with 128 bits of entropy. */
|
|
89
|
+
export function newAlias() {
|
|
90
|
+
return randomB64url(16);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Accepts a bare id or any URL on the host that contains one
|
|
95
|
+
* (`https://host/{id}`, `https://host/{id}@3/x.png`) and returns the id.
|
|
96
|
+
* @param {string} input
|
|
97
|
+
* @returns {string | null}
|
|
98
|
+
*/
|
|
99
|
+
export function parseId(input) {
|
|
100
|
+
const s = String(input ?? "").trim();
|
|
101
|
+
if (ID_RE.test(s) || LEGACY_ID_RE.test(s)) return s.toLowerCase();
|
|
102
|
+
const m = /^(?:https?:\/\/[^/]+)?\/?([0-9a-f-]{36}|[0-9a-f]{16})(?:@\d+)?(?:[/?#]|$)/i.exec(s);
|
|
103
|
+
if (!m) return null;
|
|
104
|
+
const id = m[1].toLowerCase();
|
|
105
|
+
return ID_RE.test(id) || LEGACY_ID_RE.test(id) ? id : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {{ path: string, size: number, sha256: string, contentType?: string }} ArtifactFile
|
|
110
|
+
* @typedef {{ files: ArtifactFile[], name?: string }} FileList
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Deterministic byte string that the version hash covers: paths and digests
|
|
115
|
+
* only, sorted, so file order and metadata never change the hash.
|
|
116
|
+
* @param {FileList} manifest
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function canonicalize(manifest) {
|
|
120
|
+
const files = [...manifest.files]
|
|
121
|
+
.map((f) => ({ path: f.path, sha256: String(f.sha256).toLowerCase() }))
|
|
122
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
123
|
+
return JSON.stringify({ v: MANIFEST_VERSION, files });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Content hash of a version's file list.
|
|
128
|
+
* @param {FileList} manifest
|
|
129
|
+
* @returns {Promise<string>} 32 lowercase hex chars
|
|
130
|
+
*/
|
|
131
|
+
export async function contentHash(manifest) {
|
|
132
|
+
const bytes = new TextEncoder().encode(canonicalize(manifest));
|
|
133
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
134
|
+
return hex(digest).slice(0, 32);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {ArrayBuffer | Uint8Array} buf */
|
|
138
|
+
export function hex(buf) {
|
|
139
|
+
const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
140
|
+
let out = "";
|
|
141
|
+
for (const b of view) out += b.toString(16).padStart(2, "0");
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate a file list received over the wire. Returns an error string, or null when valid.
|
|
147
|
+
* @param {unknown} manifest
|
|
148
|
+
* @returns {string | null}
|
|
149
|
+
*/
|
|
150
|
+
export function validateManifest(manifest) {
|
|
151
|
+
if (!manifest || typeof manifest !== "object") return "manifest must be an object";
|
|
152
|
+
const m = /** @type {FileList} */ (manifest);
|
|
153
|
+
if (!Array.isArray(m.files)) return "manifest.files must be an array";
|
|
154
|
+
if (m.files.length === 0) return "manifest.files must not be empty";
|
|
155
|
+
if (m.files.length > 2000) return "too many files (max 2000)";
|
|
156
|
+
if (m.name !== undefined && typeof m.name !== "string") return "manifest.name must be a string";
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (const f of m.files) {
|
|
159
|
+
if (!f || typeof f !== "object") return "each file must be an object";
|
|
160
|
+
if (typeof f.path !== "string" || !PATH_RE.test(f.path)) return `invalid path: ${JSON.stringify(f?.path)}`;
|
|
161
|
+
if (f.path.length > MAX_PATH_LEN) return `path too long: ${f.path}`;
|
|
162
|
+
if (isReservedPath(f.path)) return `reserved path: ${f.path} (top-level names may not start with ${RESERVED_PREFIX})`;
|
|
163
|
+
if (seen.has(f.path)) return `duplicate path: ${f.path}`;
|
|
164
|
+
seen.add(f.path);
|
|
165
|
+
if (typeof f.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(f.sha256)) return `invalid sha256 for ${f.path}`;
|
|
166
|
+
if (!Number.isInteger(f.size) || f.size < 0) return `invalid size for ${f.path}`;
|
|
167
|
+
if (f.contentType !== undefined && typeof f.contentType !== "string") return `invalid contentType for ${f.path}`;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** True for top-level names the host keeps for itself. */
|
|
173
|
+
export function isReservedPath(path) {
|
|
174
|
+
return path.startsWith(RESERVED_PREFIX);
|
|
175
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/* dcl.css — the house style for prose pages on artifacts.dclregenesislabs.xyz.
|
|
2
|
+
*
|
|
3
|
+
* Palette, type, radius and dark mode are the Regenesis house definitions, kept in step with
|
|
4
|
+
* .claude/skills/dcl-artifacts/references/regenesis-artifact-style.md, which is authoritative.
|
|
5
|
+
* This file is the PROSE layout of that one visual language; the guide is the report layout.
|
|
6
|
+
*
|
|
7
|
+
* Tokens first, components after. Per page you normally touch ONE thing:
|
|
8
|
+
* --accent (and --accent-soft to match). Leave the rest alone so every
|
|
9
|
+
* artifact reads as one family.
|
|
10
|
+
*
|
|
11
|
+
* Theme has three states on <html>: no attribute (follows the OS),
|
|
12
|
+
* data-theme="light", data-theme="dark". dcl.js drives the toggle and
|
|
13
|
+
* remembers the choice for the whole host.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--ground: #f9f9f7;
|
|
18
|
+
--surface: #fcfcfb;
|
|
19
|
+
--surface-2: #f1f0ea;
|
|
20
|
+
--ink: #0b0b0b;
|
|
21
|
+
--muted: #52514e;
|
|
22
|
+
--hairline: #e1e0d9;
|
|
23
|
+
--accent: #FF2D55;
|
|
24
|
+
--accent-soft: #ffe8ed;
|
|
25
|
+
--ok: #0ca30c; --ok-soft: #e6f6e6;
|
|
26
|
+
--warn: #fab219; --warn-soft: #fdf3e0;
|
|
27
|
+
--bad: #FF2D55; --bad-soft: #ffe8ed;
|
|
28
|
+
--info: #2a78d6; --info-soft: #e6eff9;
|
|
29
|
+
--pad: rgba(120, 130, 150, .06);
|
|
30
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
31
|
+
--font-mono: "Inter", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
32
|
+
--page-width: 920px;
|
|
33
|
+
--radius: 14px;
|
|
34
|
+
color-scheme: light;
|
|
35
|
+
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root:not([data-theme="light"]) {
|
|
38
|
+
--ground: #0d0d0d; --surface: #1a1a19; --surface-2: #222220;
|
|
39
|
+
--ink: #ffffff; --muted: #c3c2b7; --hairline: #2c2c2a;
|
|
40
|
+
--accent: #FF2D55; --accent-soft: #3a1620;
|
|
41
|
+
--ok: #0ca30c; --ok-soft: #10240f; --warn: #fab219; --warn-soft: #2a2110;
|
|
42
|
+
--bad: #FF2D55; --bad-soft: #2a1219; --info: #3987e5; --info-soft: #10203a;
|
|
43
|
+
--pad: rgba(120, 130, 150, .05);
|
|
44
|
+
color-scheme: dark;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
:root[data-theme="dark"] {
|
|
48
|
+
--ground: #0d0d0d; --surface: #1a1a19; --surface-2: #222220;
|
|
49
|
+
--ink: #ffffff; --muted: #c3c2b7; --hairline: #2c2c2a;
|
|
50
|
+
--accent: #FF2D55; --accent-soft: #3a1620;
|
|
51
|
+
--ok: #0ca30c; --ok-soft: #10240f; --warn: #fab219; --warn-soft: #2a2110;
|
|
52
|
+
--bad: #FF2D55; --bad-soft: #2a1219; --info: #3987e5; --info-soft: #10203a;
|
|
53
|
+
--pad: rgba(120, 130, 150, .05);
|
|
54
|
+
color-scheme: dark;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ---------- base ---------- */
|
|
58
|
+
* { box-sizing: border-box; }
|
|
59
|
+
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
|
|
60
|
+
/* the host adds a 44px sticky bar on team links; keep anchored headings clear of it */
|
|
61
|
+
[id] { scroll-margin-top: 60px; }
|
|
62
|
+
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
|
|
63
|
+
body {
|
|
64
|
+
margin: 0; background: var(--ground); color: var(--ink);
|
|
65
|
+
font-family: var(--font-sans); font-size: 16px; line-height: 1.55;
|
|
66
|
+
-webkit-font-smoothing: antialiased;
|
|
67
|
+
}
|
|
68
|
+
a { color: var(--accent); }
|
|
69
|
+
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
70
|
+
b, strong { font-weight: 600; }
|
|
71
|
+
p { margin: 0 0 12px; max-width: 78ch; }
|
|
72
|
+
ul, ol { margin: 0 0 12px; padding-left: 22px; max-width: 78ch; }
|
|
73
|
+
li { margin: 4px 0; }
|
|
74
|
+
li::marker { color: var(--muted); }
|
|
75
|
+
hr { border: 0; border-top: 1px solid var(--hairline); margin: 28px 0; }
|
|
76
|
+
img, svg, video { max-width: 100%; height: auto; }
|
|
77
|
+
|
|
78
|
+
.wrap { width: 100%; max-width: var(--page-width); margin: 0 auto; padding: 0 clamp(14px, 2.4vw, 40px) 90px; }
|
|
79
|
+
.wrap.narrow { --page: 860px; }
|
|
80
|
+
.wrap.wide { --page: 1580px; }
|
|
81
|
+
|
|
82
|
+
code { font-family: var(--font-mono); font-size: .86em; background: var(--surface-2); padding: 1px 6px; border-radius: 5px; }
|
|
83
|
+
pre {
|
|
84
|
+
margin: 10px 0 14px; overflow-x: auto; background: var(--surface); border: 1px solid var(--hairline);
|
|
85
|
+
border-radius: 10px; padding: 12px 16px; font-family: var(--font-mono); font-size: .8rem; line-height: 1.7; color: var(--ink);
|
|
86
|
+
}
|
|
87
|
+
/* dcl.js wraps every pre in .codeblock and adds the Copy button */
|
|
88
|
+
.codeblock { position: relative; }
|
|
89
|
+
.codeblock > pre { padding-right: 100px; }
|
|
90
|
+
/* fade the text out under the button instead of letting it run beneath it */
|
|
91
|
+
.codeblock::after {
|
|
92
|
+
content: ""; position: absolute; top: 1px; right: 1px; bottom: 1px; width: 96px; pointer-events: none;
|
|
93
|
+
border-radius: 0 10px 10px 0; background: linear-gradient(to right, transparent, var(--surface) 38%);
|
|
94
|
+
}
|
|
95
|
+
.codeblock > .copy {
|
|
96
|
+
z-index: 1;
|
|
97
|
+
position: absolute; top: 8px; right: 8px; font: 500 11px/1 var(--font-mono); letter-spacing: .04em; text-transform: uppercase;
|
|
98
|
+
color: var(--muted); background: var(--surface-2); border: 1px solid var(--hairline); border-radius: 6px; padding: 5px 8px; cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
.codeblock > .copy:hover { color: var(--ink); border-color: var(--muted); }
|
|
101
|
+
.codeblock > .copy.done { color: var(--ok); border-color: var(--ok); }
|
|
102
|
+
pre code { background: none; padding: 0; font-size: inherit; }
|
|
103
|
+
pre .c { color: var(--muted); } /* comment */
|
|
104
|
+
pre .k { color: var(--accent); } /* keyword / emphasis */
|
|
105
|
+
pre .o { color: var(--ok); } /* output */
|
|
106
|
+
kbd { font-family: var(--font-mono); font-size: .8em; border: 1px solid var(--hairline); border-bottom-width: 2px; border-radius: 5px; padding: 1px 6px; background: var(--surface); }
|
|
107
|
+
|
|
108
|
+
.mono { font-family: var(--font-mono); }
|
|
109
|
+
.muted { color: var(--muted); }
|
|
110
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
111
|
+
.small { font-size: .9rem; }
|
|
112
|
+
|
|
113
|
+
/* ---------- header ---------- */
|
|
114
|
+
header { padding: 34px 0 20px; border-bottom: 1px solid var(--hairline); margin-bottom: 22px; }
|
|
115
|
+
.top { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
|
|
116
|
+
.top > div { min-width: 0; flex: 1 1 520px; }
|
|
117
|
+
.eyebrow {
|
|
118
|
+
font-family: var(--font-mono); font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
|
|
119
|
+
color: var(--accent); margin: 0 0 12px; font-weight: 500;
|
|
120
|
+
}
|
|
121
|
+
h1 {
|
|
122
|
+
font-size: clamp(1.55rem, 2.4vw, 2.2rem); font-weight: 700; letter-spacing: -.02em;
|
|
123
|
+
margin: 0 0 12px; text-wrap: balance; line-height: 1.12;
|
|
124
|
+
}
|
|
125
|
+
.lede { margin: 0; max-width: 88ch; color: var(--muted); font-size: 1rem; }
|
|
126
|
+
.lede b { color: var(--ink); }
|
|
127
|
+
.toggle {
|
|
128
|
+
font-family: var(--font-mono); font-size: .78rem; color: var(--muted); background: var(--surface);
|
|
129
|
+
border: 1px solid var(--hairline); border-radius: 8px; padding: 8px 12px; cursor: pointer; white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
.toggle:hover { color: var(--ink); border-color: var(--accent); }
|
|
132
|
+
.toc { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
|
|
133
|
+
.toc a {
|
|
134
|
+
font-family: var(--font-mono); font-size: .75rem; text-decoration: none; color: var(--muted);
|
|
135
|
+
background: var(--surface); border: 1px solid var(--hairline); border-radius: 999px; padding: 5px 11px;
|
|
136
|
+
}
|
|
137
|
+
.toc a:hover { color: var(--ink); border-color: var(--accent); }
|
|
138
|
+
|
|
139
|
+
/* ---------- sections ---------- */
|
|
140
|
+
h2 { font-size: 1.22rem; font-weight: 700; margin: 38px 0 6px; letter-spacing: -.01em; scroll-margin-top: 16px; }
|
|
141
|
+
h3 { font-size: 1rem; font-weight: 600; margin: 22px 0 6px; }
|
|
142
|
+
.sub { color: var(--muted); margin: 0 0 14px; max-width: 92ch; font-size: .95rem; }
|
|
143
|
+
|
|
144
|
+
/* ---------- callouts ---------- */
|
|
145
|
+
.callout {
|
|
146
|
+
background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--hairline));
|
|
147
|
+
border-radius: var(--radius); padding: 16px 18px; margin: 14px 0;
|
|
148
|
+
}
|
|
149
|
+
.callout p { margin: 0 0 8px; font-size: .94rem; max-width: 100ch; }
|
|
150
|
+
.callout p:last-child, .callout ul:last-child { margin-bottom: 0; }
|
|
151
|
+
.callout.ok { background: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 40%, var(--hairline)); }
|
|
152
|
+
.callout.warn { background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 45%, var(--hairline)); }
|
|
153
|
+
.callout.bad { background: var(--bad-soft); border-color: color-mix(in srgb, var(--bad) 40%, var(--hairline)); }
|
|
154
|
+
.callout.info { background: var(--info-soft); border-color: color-mix(in srgb, var(--info) 40%, var(--hairline)); }
|
|
155
|
+
/* a quieter variant: surface background, colored left rail */
|
|
156
|
+
.note {
|
|
157
|
+
background: var(--surface); border: 1px solid var(--hairline); border-left: 4px solid var(--accent);
|
|
158
|
+
border-radius: var(--radius); padding: 14px 18px; margin: 12px 0;
|
|
159
|
+
}
|
|
160
|
+
.note p { margin: 0 0 8px; font-size: .93rem; max-width: 100ch; }
|
|
161
|
+
.note p:last-child { margin: 0; }
|
|
162
|
+
.note.ok { border-left-color: var(--ok); }
|
|
163
|
+
.note.warn { border-left-color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, var(--hairline)); border-left-color: var(--warn); }
|
|
164
|
+
.note.bad { border-left-color: var(--bad); }
|
|
165
|
+
|
|
166
|
+
/* ---------- tables ---------- */
|
|
167
|
+
.tblwrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--hairline); border-radius: 14px; margin: 0 0 14px; }
|
|
168
|
+
.tblwrap table { width: 100%; border-collapse: collapse; font-size: .9rem; }
|
|
169
|
+
.tblwrap th {
|
|
170
|
+
font-family: var(--font-mono); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
|
|
171
|
+
text-align: left; padding: 12px 18px 8px; border-bottom: 1px solid var(--hairline); font-weight: 500;
|
|
172
|
+
}
|
|
173
|
+
.tblwrap td { padding: 11px 18px; border-bottom: 1px solid var(--hairline); vertical-align: top; }
|
|
174
|
+
.tblwrap tr:last-child td { border-bottom: 0; }
|
|
175
|
+
.tblwrap td.n { font-family: var(--font-mono); font-weight: 600; white-space: nowrap; }
|
|
176
|
+
.tblwrap td.t { white-space: nowrap; }
|
|
177
|
+
.tblwrap td.src { font-family: var(--font-mono); font-size: .79rem; color: var(--muted); }
|
|
178
|
+
.tblwrap td.src b { color: var(--ink); font-weight: 500; }
|
|
179
|
+
.tblwrap td.r { text-align: right; font-variant-numeric: tabular-nums; }
|
|
180
|
+
.yes { color: var(--ok); font-weight: 600; white-space: nowrap; }
|
|
181
|
+
.no { color: var(--muted); font-weight: 600; white-space: nowrap; }
|
|
182
|
+
.chg { color: var(--warn); font-weight: 600; white-space: nowrap; }
|
|
183
|
+
|
|
184
|
+
/* key / value list */
|
|
185
|
+
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; margin: 0 0 14px; font-size: .93rem; max-width: 100ch; }
|
|
186
|
+
.kv dt { font-family: var(--font-mono); font-size: .8rem; color: var(--muted); padding-top: 2px; }
|
|
187
|
+
.kv dd { margin: 0; }
|
|
188
|
+
|
|
189
|
+
/* ---------- cards ---------- */
|
|
190
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 0 0 14px; }
|
|
191
|
+
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin: 0 0 14px; }
|
|
192
|
+
@media (max-width: 900px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
|
|
193
|
+
.card {
|
|
194
|
+
background: var(--surface); border: 1px solid var(--hairline); border-radius: 14px; padding: 18px 20px;
|
|
195
|
+
border-left: 4px solid var(--layer, var(--accent)); min-width: 0;
|
|
196
|
+
}
|
|
197
|
+
.card.ok { --layer: var(--ok); } .card.warn { --layer: var(--warn); } .card.bad { --layer: var(--bad); }
|
|
198
|
+
.card.info { --layer: var(--info); } .card.plain { --layer: var(--hairline); }
|
|
199
|
+
.card-h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
|
|
200
|
+
.card-h .name { font-family: var(--font-mono); font-size: 1rem; font-weight: 600; color: var(--ink); }
|
|
201
|
+
.card .covers { font-size: .86rem; color: var(--muted); margin: 2px 0 12px; }
|
|
202
|
+
.card p { font-size: .93rem; }
|
|
203
|
+
.card ul { margin: 0; padding-left: 0; list-style: none; }
|
|
204
|
+
.card li { margin: 9px 0; font-size: .9rem; padding-left: 16px; position: relative; }
|
|
205
|
+
.card li::before {
|
|
206
|
+
content: ""; position: absolute; left: 0; top: .55em; width: 6px; height: 6px; border-radius: 50%;
|
|
207
|
+
background: var(--layer, var(--accent));
|
|
208
|
+
}
|
|
209
|
+
.badge {
|
|
210
|
+
font-family: var(--font-mono); font-size: .68rem; letter-spacing: .04em; text-transform: uppercase;
|
|
211
|
+
color: var(--layer, var(--accent)); border: 1px solid var(--layer, var(--accent)); border-radius: 999px; padding: 2px 8px;
|
|
212
|
+
}
|
|
213
|
+
.lang { font-family: var(--font-mono); font-size: .72rem; color: var(--muted); margin-left: auto; }
|
|
214
|
+
|
|
215
|
+
/* big-number tiles — only when the numbers are the point */
|
|
216
|
+
.stat { background: var(--surface); border: 1px solid var(--hairline); border-radius: 14px; padding: 16px 18px; }
|
|
217
|
+
.stat .v { font-family: var(--font-mono); font-size: 1.8rem; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
|
218
|
+
.stat .l { font-size: .8rem; color: var(--muted); margin-top: 6px; }
|
|
219
|
+
|
|
220
|
+
/* ---------- inline labels ---------- */
|
|
221
|
+
.tag, .pill {
|
|
222
|
+
display: inline-block; font-family: var(--font-mono); font-size: .66rem; text-transform: uppercase; letter-spacing: .04em;
|
|
223
|
+
border-radius: 999px; padding: 2px 9px; border: 1px solid var(--hairline); color: var(--muted); white-space: nowrap; vertical-align: middle;
|
|
224
|
+
}
|
|
225
|
+
.tag.accent, .pill.accent { color: var(--accent); border-color: var(--accent); }
|
|
226
|
+
.tag.ok, .pill.ok { color: var(--ok); border-color: var(--ok); }
|
|
227
|
+
.tag.warn, .pill.warn { color: var(--warn); border-color: var(--warn); }
|
|
228
|
+
.tag.bad, .pill.bad { color: var(--bad); border-color: var(--bad); }
|
|
229
|
+
.tag.info, .pill.info { color: var(--info); border-color: var(--info); }
|
|
230
|
+
.pill { border: 0; font-weight: 600; }
|
|
231
|
+
.pill.accent { background: var(--accent-soft); } .pill.ok { background: var(--ok-soft); }
|
|
232
|
+
.pill.warn { background: var(--warn-soft); } .pill.bad { background: var(--bad-soft); } .pill.info { background: var(--info-soft); }
|
|
233
|
+
|
|
234
|
+
/* ---------- ordered sequence (only when order carries meaning) ---------- */
|
|
235
|
+
ol.phases { counter-reset: step; list-style: none; padding: 0; margin: 0 0 14px; display: grid; gap: 12px; max-width: none; }
|
|
236
|
+
ol.phases li { background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--radius); padding: 16px 18px 16px 56px; position: relative; margin: 0; }
|
|
237
|
+
ol.phases li::before {
|
|
238
|
+
counter-increment: step; content: counter(step); position: absolute; left: 16px; top: 16px; width: 26px; height: 26px;
|
|
239
|
+
border-radius: 50%; background: var(--accent); color: var(--surface); font-family: var(--font-mono); font-size: .82rem; font-weight: 600;
|
|
240
|
+
display: flex; align-items: center; justify-content: center;
|
|
241
|
+
}
|
|
242
|
+
ol.phases b { font-weight: 600; }
|
|
243
|
+
ol.phases p { margin: 4px 0 0; color: var(--muted); font-size: .9rem; }
|
|
244
|
+
ol.phases pre { margin: 8px 0 0; }
|
|
245
|
+
|
|
246
|
+
/* ---------- before / after code columns with diff rows ---------- */
|
|
247
|
+
.path { font-family: var(--font-mono); font-size: .79rem; color: var(--muted); margin: 10px 2px 6px; }
|
|
248
|
+
.path b { color: var(--ink); font-weight: 600; }
|
|
249
|
+
.fn { color: var(--accent); }
|
|
250
|
+
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: start; margin-bottom: 14px; }
|
|
251
|
+
@media (max-width: 920px) { .split { grid-template-columns: 1fr; } }
|
|
252
|
+
.col { border: 1px solid var(--hairline); border-radius: 11px; overflow: hidden; background: var(--surface); min-width: 0; }
|
|
253
|
+
.colhead { padding: 7px 14px; background: var(--surface-2); border-bottom: 1px solid var(--hairline); font-family: var(--font-mono); font-size: .74rem; text-transform: uppercase; letter-spacing: .05em; }
|
|
254
|
+
.colhead.before { color: var(--bad); } .colhead.after { color: var(--ok); }
|
|
255
|
+
.code { overflow-x: auto; padding: 6px 0; font-family: var(--font-mono); font-size: .8rem; line-height: 1.6; }
|
|
256
|
+
.row { display: flex; white-space: pre; min-width: max-content; width: 100%; }
|
|
257
|
+
.row .num { flex: 0 0 auto; width: 3.4em; text-align: right; padding: 0 10px 0 6px; user-select: none; color: var(--muted); opacity: .55; font-variant-numeric: tabular-nums; }
|
|
258
|
+
.row .sign { flex: 0 0 auto; width: 1.5ch; text-align: center; user-select: none; color: var(--muted); }
|
|
259
|
+
.row .src { flex: 1 1 auto; padding-right: 16px; }
|
|
260
|
+
.row.del { background: var(--bad-soft); box-shadow: inset 2px 0 0 var(--bad); } .row.del .sign { color: var(--bad); }
|
|
261
|
+
.row.add { background: var(--ok-soft); box-shadow: inset 2px 0 0 var(--ok); } .row.add .sign { color: var(--ok); }
|
|
262
|
+
.row.pad { background: var(--pad); }
|
|
263
|
+
|
|
264
|
+
/* ---------- checklist ---------- */
|
|
265
|
+
/* <input type="checkbox" data-persist="key"> remembers its state on the host,
|
|
266
|
+
for everyone who opens the page. The key is the identity: keep it stable
|
|
267
|
+
across versions. Public viewers see the state but cannot change it. */
|
|
268
|
+
ul.checklist { list-style: none; padding: 0; margin: 0 0 14px; max-width: 78ch; background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--radius); }
|
|
269
|
+
ul.checklist li { margin: 0; border-top: 1px solid var(--hairline); }
|
|
270
|
+
ul.checklist li:first-child { border-top: 0; }
|
|
271
|
+
ul.checklist label { display: flex; gap: 12px; align-items: flex-start; padding: 10px 14px; cursor: pointer; }
|
|
272
|
+
ul.checklist input { flex: none; width: 17px; height: 17px; margin: 3px 0 0; accent-color: var(--accent); cursor: pointer; }
|
|
273
|
+
ul.checklist label:has(input:checked) { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--hairline); }
|
|
274
|
+
ul.checklist .hint { display: block; font-size: .85rem; color: var(--muted); text-decoration: none; }
|
|
275
|
+
|
|
276
|
+
/* ---------- footer ---------- */
|
|
277
|
+
footer { margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--hairline); color: var(--muted); font-size: .85rem; max-width: 104ch; }
|
|
278
|
+
footer b { color: var(--ink); }
|
|
279
|
+
footer p { margin: 0 0 10px; }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// dcl.js — behaviour for pages on artifacts.dclregenesislabs.xyz. Load it in
|
|
2
|
+
// <head>, before the body paints.
|
|
3
|
+
//
|
|
4
|
+
// 1. Theme. Three states on <html>: no attribute (follow the OS), "light",
|
|
5
|
+
// "dark". Any element with data-theme-toggle switches it; the choice is
|
|
6
|
+
// stored once for the whole host, so every artifact follows it.
|
|
7
|
+
// 2. A Copy button on every <pre>, so commands and snippets can be copied
|
|
8
|
+
// without selecting text.
|
|
9
|
+
(function () {
|
|
10
|
+
var root = document.documentElement;
|
|
11
|
+
var KEY = "dcl-artifacts-theme";
|
|
12
|
+
function apply(t) {
|
|
13
|
+
if (t === "dark" || t === "light") root.setAttribute("data-theme", t);
|
|
14
|
+
else root.removeAttribute("data-theme");
|
|
15
|
+
}
|
|
16
|
+
try { apply(localStorage.getItem(KEY)); } catch (e) { /* storage blocked: follow the OS */ }
|
|
17
|
+
document.addEventListener("click", function (ev) {
|
|
18
|
+
var btn = ev.target.closest("[data-theme-toggle]");
|
|
19
|
+
if (!btn) return;
|
|
20
|
+
var dark = root.getAttribute("data-theme") === "dark" ||
|
|
21
|
+
(!root.getAttribute("data-theme") && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
22
|
+
var next = dark ? "light" : "dark";
|
|
23
|
+
apply(next);
|
|
24
|
+
try { localStorage.setItem(KEY, next); } catch (e) { /* fine, just not remembered */ }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function copyButtons() {
|
|
28
|
+
Array.prototype.forEach.call(document.querySelectorAll("pre"), function (pre) {
|
|
29
|
+
if (pre.parentNode && pre.parentNode.classList && pre.parentNode.classList.contains("codeblock")) return;
|
|
30
|
+
var wrap = document.createElement("div");
|
|
31
|
+
wrap.className = "codeblock";
|
|
32
|
+
pre.parentNode.insertBefore(wrap, pre);
|
|
33
|
+
wrap.appendChild(pre);
|
|
34
|
+
var btn = document.createElement("button");
|
|
35
|
+
btn.type = "button";
|
|
36
|
+
btn.className = "copy";
|
|
37
|
+
btn.textContent = "Copy";
|
|
38
|
+
btn.setAttribute("aria-label", "Copy to clipboard");
|
|
39
|
+
btn.addEventListener("click", function () {
|
|
40
|
+
var text = pre.textContent.replace(/\n$/, "");
|
|
41
|
+
var done = function () {
|
|
42
|
+
btn.textContent = "Copied";
|
|
43
|
+
btn.classList.add("done");
|
|
44
|
+
setTimeout(function () { btn.textContent = "Copy"; btn.classList.remove("done"); }, 1600);
|
|
45
|
+
};
|
|
46
|
+
var fallback = function () {
|
|
47
|
+
var range = document.createRange();
|
|
48
|
+
range.selectNodeContents(pre);
|
|
49
|
+
var sel = window.getSelection();
|
|
50
|
+
sel.removeAllRanges();
|
|
51
|
+
sel.addRange(range);
|
|
52
|
+
try { document.execCommand("copy"); done(); } catch (e) { /* nothing more we can do */ }
|
|
53
|
+
};
|
|
54
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, fallback);
|
|
55
|
+
else fallback();
|
|
56
|
+
});
|
|
57
|
+
wrap.appendChild(btn);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", copyButtons);
|
|
61
|
+
else copyButtons();
|
|
62
|
+
})();
|