@augurworks/augur 0.15.1 → 0.15.2
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/agents/README.md +2 -2
- package/agents/drafts.md +0 -6
- package/agents/identity.md +1 -1
- package/changelog.md +5 -0
- package/package.json +16 -16
- package/scripts/connect.mjs +1 -1
- package/scripts/lib/draft.mjs +2 -10
- package/scripts/no-tenant-globals.mjs +0 -16
- package/scripts/open.mjs +3 -6
- package/seed/README.md +1 -1
- package/seed/start-here/prototypes/connect-your-terminal/index.html +5 -5
- package/src/_worker.js +20 -141
- package/src/seed-pack.mjs +2 -2
- package/src/galleries.mjs +0 -400
package/agents/README.md
CHANGED
|
@@ -21,7 +21,7 @@ prototypes needs a **publish token**, and an agent gets one by device pairing, n
|
|
|
21
21
|
asking anyone for a password:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
npx augur connect --origin https://<the workspace>
|
|
24
|
+
npx @augurworks/augur connect --origin https://<the workspace>
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
It prints one line to relay — *ask the owner of this workspace to open `<link>` and
|
|
@@ -31,7 +31,7 @@ signed in to and types the code; the token lands in `~/.config/augur/tokens.json
|
|
|
31
31
|
it finds no token, so inside a workspace tree nothing has to be done first. On a workspace
|
|
32
32
|
that serves drafts (`/.well-known/augur.json` says `drafts.enabled`), the everyday verbs
|
|
33
33
|
are `augur open` and `augur land` instead — see [drafts.md](./drafts.md). To get the
|
|
34
|
-
tree from a hostname, pair first, then `npx augur clone --space <id>` — it reads the
|
|
34
|
+
tree from a hostname, pair first, then `npx @augurworks/augur clone --space <id>` — it reads the
|
|
35
35
|
origin from the pairing.
|
|
36
36
|
|
|
37
37
|
Not on npm yet? The engine clone sits next to every workspace that publishes:
|
package/agents/drafts.md
CHANGED
|
@@ -16,12 +16,6 @@ augur sync # only when land was refused: fold main
|
|
|
16
16
|
That is it. There is no commit, no push, no build. The URL `land` prints is what you hand
|
|
17
17
|
the person you are working for — never a localhost, never a `file://` path.
|
|
18
18
|
|
|
19
|
-
A prototype that does not exist yet: `augur open --new <opportunity>/<name>` gives you an
|
|
20
|
-
empty folder and an empty draft; write its `index.html` and land. The same works for a
|
|
21
|
-
library demo (`components/<name>`, `base/…`, `patterns/…`, `pages/…`) and for the
|
|
22
|
-
workspace's design system (`skills/<prefix>-ui`), which is one unit like any other.
|
|
23
|
-
Without `--new`, a name that does not exist is refused rather than guessed at.
|
|
24
|
-
|
|
25
19
|
## What a draft is
|
|
26
20
|
|
|
27
21
|
Your own live copy of one prototype, at its own address: the prototype's URL with `@` and
|
package/agents/identity.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Getting in, if you are an agent
|
|
4
4
|
|
|
5
5
|
You do not need a login. Publishing needs a **publish token**, and the way to get one
|
|
6
|
-
is device pairing: `npx augur connect --origin <the workspace>` prints a link and a
|
|
6
|
+
is device pairing: `npx @augurworks/augur connect --origin <the workspace>` prints a link and a
|
|
7
7
|
code, the owner enters the code in a browser they are already signed in to, and the
|
|
8
8
|
token lands on this machine. Nobody's password is asked for, typed or stored. The
|
|
9
9
|
instance says the same at `GET /llms.txt`. The full shape is in [README.md](./README.md)
|
package/changelog.md
CHANGED
|
@@ -20,6 +20,11 @@ The date is the day the update went live. The page works out "Today / 3 days ago
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
## 2026-09-06 — Augur installs from npm
|
|
24
|
+
Your agent can now fetch the Augur tool with one line, `npx @augurworks/augur`, on any
|
|
25
|
+
machine with Node. Nothing to clone first: the workspace's own front door prints that
|
|
26
|
+
line, and the rest is the code you approve in your browser.
|
|
27
|
+
|
|
23
28
|
## 2026-09-03 — Your agent finds the door on its own
|
|
24
29
|
Point an agent at a workspace address and it now learns how to get in without
|
|
25
30
|
asking you for a password: the site tells it to run `augur connect`, you approve a
|
package/package.json
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augurworks/augur",
|
|
3
|
-
"version": "0.15.
|
|
4
|
-
"description": "A prototype and research repository for product teams. Real, clickable prototypes and the design system they are built from, on one site with login, comments and live boards on top. Underneath it is all git and static HTML.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/andratwiro/augur.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/andratwiro/augur#readme",
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
},
|
|
14
|
-
"type": "module",
|
|
15
|
-
"bin": {
|
|
16
|
-
"augur": "scripts/cli.mjs"
|
|
17
|
-
},
|
|
3
|
+
"version": "0.15.2",
|
|
18
4
|
"files": [
|
|
19
5
|
"scripts/",
|
|
20
6
|
"build.js",
|
|
@@ -31,6 +17,12 @@
|
|
|
31
17
|
"TRADEMARKS.md",
|
|
32
18
|
"changelog.md"
|
|
33
19
|
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"description": "A prototype and research repository for product teams. Real, clickable prototypes and the design system they are built from, on one site with login, comments and live boards on top. Underneath it is all git and static HTML.",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"augur": "scripts/cli.mjs"
|
|
25
|
+
},
|
|
34
26
|
"scripts": {
|
|
35
27
|
"test": "node --test \"test/*.test.mjs\"",
|
|
36
28
|
"smoke": "node scripts/smoke-invite.mjs",
|
|
@@ -53,5 +45,13 @@
|
|
|
53
45
|
},
|
|
54
46
|
"devDependencies": {
|
|
55
47
|
"playwright": "^1.60.0"
|
|
56
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/andratwiro/augur.git"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/andratwiro/augur#readme"
|
|
57
57
|
}
|
package/scripts/connect.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* transcript. This asks for neither: it prints a code, you type that code into a browser
|
|
8
8
|
* that already has your session, and the token approval mints comes back here.
|
|
9
9
|
*
|
|
10
|
-
* npx augur connect [--origin https://your.site]
|
|
10
|
+
* npx @augurworks/augur connect [--origin https://your.site]
|
|
11
11
|
*
|
|
12
12
|
* `augur login` stays for CI and scripts, where there is no browser to type into.
|
|
13
13
|
*
|
package/scripts/lib/draft.mjs
CHANGED
|
@@ -150,19 +150,11 @@ async function materialise(client, unit, table, dir) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
async function doOpenImpl({ client, unit, dir, origin, space, session, now
|
|
153
|
+
async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
|
|
154
154
|
if (fs.existsSync(dir) && fs.readdirSync(dir).length) return { ok: false, error: "folder-not-empty", dir };
|
|
155
155
|
const createdFolder = !fs.existsSync(dir);
|
|
156
156
|
const o = await client.open({ unit });
|
|
157
157
|
if (o.status) return { ok: false, ...o };
|
|
158
|
-
// A unit with no files is one that does not exist yet. Creating one is a decision the
|
|
159
|
-
// caller states with `isNew`; without it, a typo would quietly open an empty draft on a
|
|
160
|
-
// prototype that was never there. Either mismatch hands the draft straight back.
|
|
161
|
-
const exists = Object.keys(o.table || {}).length > 0;
|
|
162
|
-
if (exists !== !isNew) {
|
|
163
|
-
try { await client.discard({ unit, draftId: o.draftId }); } catch (e) { /* best-effort */ }
|
|
164
|
-
return { ok: false, error: exists ? "unit-exists" : "unknown-unit", unit };
|
|
165
|
-
}
|
|
166
158
|
// From here the server-side draft exists, so any failure below must both undo what we
|
|
167
159
|
// wrote to disk and tell the server to drop the orphan — otherwise a retry finds a
|
|
168
160
|
// half-materialised folder (`folder-not-empty`) and the draft it opened is never freed.
|
|
@@ -185,7 +177,7 @@ async function doOpenImpl({ client, unit, dir, origin, space, session, now, isNe
|
|
|
185
177
|
throw err;
|
|
186
178
|
}
|
|
187
179
|
const others = (o.presence || []).filter((d) => d.id !== o.draftId);
|
|
188
|
-
return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others
|
|
180
|
+
return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others };
|
|
189
181
|
}
|
|
190
182
|
|
|
191
183
|
async function doSaveImpl({ client, dir, baseRevision, baseTable }) {
|
|
@@ -224,7 +224,6 @@ const ALLOWLIST = {
|
|
|
224
224
|
"mcpHostAllowlist",
|
|
225
225
|
"CANVAS_REGISTRY",
|
|
226
226
|
"PITI_REMARKS",
|
|
227
|
-
"DERIVED", // the two store reads a derived gallery needs, per manifest version — the status baseline and the design-system catalog
|
|
228
227
|
"ROSTER_OVERLAY",
|
|
229
228
|
"FREEZE_STATE",
|
|
230
229
|
"SUSPENSION_STATE",
|
|
@@ -282,21 +281,6 @@ const ALLOWLIST = {
|
|
|
282
281
|
},
|
|
283
282
|
},
|
|
284
283
|
|
|
285
|
-
// The derived pages: pure renderers over the live store. Every table here is a fact
|
|
286
|
-
// about the engine's vocabulary — tier names, status words and glyphs, the emoji pool a
|
|
287
|
-
// card's leading glyph is picked from — the same for every workspace.
|
|
288
|
-
"src/galleries.mjs": {
|
|
289
|
-
frozen: [
|
|
290
|
-
"TIERS", // the four library tiers whose demos are units
|
|
291
|
-
"TIER_TITLE", // their titles
|
|
292
|
-
"TIER_COPY", // their hints and empty states
|
|
293
|
-
"STATUS_META", // status word → label + class
|
|
294
|
-
"STATUS_ICONS", // status word → glyph
|
|
295
|
-
"STATUS_RANK", // the card order a status implies
|
|
296
|
-
"EMOJI_POOL", // the leading-emoji pool, picked by slug hash
|
|
297
|
-
],
|
|
298
|
-
},
|
|
299
|
-
|
|
300
284
|
"src/bundle-keys.mjs": {
|
|
301
285
|
frozen: [
|
|
302
286
|
"BUNDLE_TENANCY", // which bundle-store families carry a workspace segment; one word per family is the revert, and a deploy-wide fact — moved here from the worker so the workspace object shares the key shape
|
package/scripts/open.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// augur open <prototype> [--
|
|
2
|
+
// augur open <prototype> [--dir <folder>] [--session <label>]
|
|
3
3
|
//
|
|
4
4
|
// Open one prototype into a folder of its own, as a draft that is live at once at its own
|
|
5
5
|
// address. Prints who else is drafting it. The folder holds only that prototype's files and
|
|
@@ -35,18 +35,15 @@ const session = process.env.AUGUR_SESSION || opt("--session") || `session-${proc
|
|
|
35
35
|
const dir = path.resolve(opt("--dir") || unit.split("/").filter(Boolean).pop());
|
|
36
36
|
|
|
37
37
|
const client = unitClient({ origin, token, space, session });
|
|
38
|
-
const
|
|
39
|
-
const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString(), isNew });
|
|
38
|
+
const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString() });
|
|
40
39
|
if (!r.ok) {
|
|
41
40
|
if (r.error === "folder-not-empty") die(`${r.dir} is not empty — pick another folder with --dir.`);
|
|
42
|
-
if (r.error === "unknown-unit") die(`${unit} does not exist here. To create it: \`augur open --new ${unit.replace(/^\/|\/$/g, "")}\`.`);
|
|
43
|
-
if (r.error === "unit-exists") die(`${unit} exists already — open it without --new.`);
|
|
44
41
|
if (r.error === "units-not-configured") die("this instance does not serve drafts yet (no unit store bound).");
|
|
45
42
|
if (r.error === "bad-unit" && r.reason === "reserved-folder") die(`${unit} sits under a folder the engine reserves — a prototype lives at <opportunity>/<prototype>.`);
|
|
46
43
|
if (r.error === "bad-unit" && r.reason === "not-a-prototype-folder") die(`${unit} is not a prototype folder — name one as <opportunity>/<prototype>.`);
|
|
47
44
|
die(`could not open: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}`);
|
|
48
45
|
}
|
|
49
|
-
log(
|
|
46
|
+
log(`draft ${r.draftId} on ${unit} — ${r.files} file(s) in ${dir}`);
|
|
50
47
|
// The agent tool's hooks, installed for this machine the first time a draft is opened
|
|
51
48
|
// here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
|
|
52
49
|
if (!process.env.AUGUR_NO_ADAPTERS) {
|
package/seed/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Three things are substituted on the way in:
|
|
|
59
59
|
|
|
60
60
|
| What | Where | Substituted with |
|
|
61
61
|
| --- | --- | --- |
|
|
62
|
-
| The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
|
|
62
|
+
| The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx @augurworks/augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
|
|
63
63
|
| Comment timestamps | `at` in `threads.json` | the provisioning time, so day-one threads do not read as months old |
|
|
64
64
|
| File timestamps | `editedAt` on every file in the manifest | the same provisioning time, all of them |
|
|
65
65
|
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
<h2 class="s-h2">Run this where your agent runs</h2>
|
|
92
92
|
<p class="s-prose">In the folder you want this workspace to live in, run:</p>
|
|
93
93
|
<div class="s-cmd">
|
|
94
|
-
<code class="s-cmd__text" id="cmd-connect">npx augur connect your-workspace</code>
|
|
94
|
+
<code class="s-cmd__text" id="cmd-connect">npx @augurworks/augur connect your-workspace</code>
|
|
95
95
|
<button type="button" class="s-cmd__btn" data-s-copy="#cmd-connect">Copy</button>
|
|
96
96
|
</div>
|
|
97
97
|
<p class="s-prose" style="margin-top: var(--starter-s4)">A browser tab opens and asks you to
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
<div>
|
|
106
106
|
<p class="ask__why">Paste this into your agent instead of running the command yourself.</p>
|
|
107
107
|
<div class="s-cmd s-cmd--paste">
|
|
108
|
-
<code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
|
|
108
|
+
<code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
|
|
109
109
|
<button type="button" class="s-cmd__btn" data-s-copy="#ask-agent">Copy</button>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
@@ -311,7 +311,7 @@
|
|
|
311
311
|
<p class="s-prose">Last one. Ask the agent to connect, in plain language — it runs the
|
|
312
312
|
command for you:</p>
|
|
313
313
|
<div class="s-cmd s-cmd--paste">
|
|
314
|
-
<code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
|
|
314
|
+
<code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
|
|
315
315
|
<button type="button" class="s-cmd__btn" data-s-copy="#ask-5-do">Copy</button>
|
|
316
316
|
</div>
|
|
317
317
|
<p class="s-prose">Approve the tab that opens. The terminal now has a key of its own, so
|
|
@@ -369,7 +369,7 @@
|
|
|
369
369
|
var host = location.hostname || "";
|
|
370
370
|
var label = host.split(".")[0];
|
|
371
371
|
var named = host.indexOf(".") > 0 && label && label !== "localhost" && !/^\d+$/.test(label);
|
|
372
|
-
return named ? "npx augur connect --origin https://" + host : "npx augur connect your-workspace";
|
|
372
|
+
return named ? "npx @augurworks/augur connect --origin https://" + host : "npx @augurworks/augur connect your-workspace";
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
var cmd = connectCommand();
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
if (cmdEl) cmdEl.textContent = cmd;
|
|
378
378
|
["ask-agent", "ask-5-do"].forEach(function (id) {
|
|
379
379
|
var el = document.getElementById(id);
|
|
380
|
-
if (el) el.textContent = el.textContent.replace(/npx augur connect [\w-]+/, cmd);
|
|
380
|
+
if (el) el.textContent = el.textContent.replace(/npx @augurworks/augur connect [\w-]+/, cmd);
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
/* ── Which path you are on ────────────────────────────────────────────────
|
package/src/_worker.js
CHANGED
|
@@ -104,10 +104,6 @@ import { PURGED_AUTHOR, purgeThreads, idCollisions } from "./purge.mjs";
|
|
|
104
104
|
import { authoredUnits, unitOfPath, unitPaths } from "./publish-units.mjs";
|
|
105
105
|
import { composePublish, forkLanded } from "./publish-compose.mjs";
|
|
106
106
|
import { normUnit, splitDraftPath, unitTable, draftAddress, DRAFT_ID_RE } from "./unit-core.mjs";
|
|
107
|
-
import {
|
|
108
|
-
siteModel, derivedPathKind, renderRootIndex, renderOpportunityIndex, renderPlaygroundIndex,
|
|
109
|
-
renderTierIndex, renderComponentsIndex, searchIndex, catalogFrom,
|
|
110
|
-
} from "./galleries.mjs";
|
|
111
107
|
// `F-fork-verb`. Fork as a deliberate verb — one unit aliased to a new path, zero bytes
|
|
112
108
|
// moved — plus the rule that keeps a fork's lineage and owner alive across every later
|
|
113
109
|
// publish. Both are pure, and both are the CLI's too if it ever needs them.
|
|
@@ -4337,7 +4333,7 @@ function resolveBundlePath(manifests, pathname) {
|
|
|
4337
4333
|
// against. It is the first argument for the same reason `loadTenantContext` takes it
|
|
4338
4334
|
// first: everything below reads one workspace's store, and a function that had to guess
|
|
4339
4335
|
// would resolve a path against whatever the isolate happened to hold.
|
|
4340
|
-
async function assetFetch(tenantId, env, request
|
|
4336
|
+
async function assetFetch(tenantId, env, request) {
|
|
4341
4337
|
if (!bundleMode(env)) return env.ASSETS.fetch(request);
|
|
4342
4338
|
const url = new URL(request.url);
|
|
4343
4339
|
// ── a draft address: the draft's table, not the manifest ─────────────────────────
|
|
@@ -4356,22 +4352,6 @@ async function assetFetch(tenantId, env, request, opts = {}) {
|
|
|
4356
4352
|
return blobResponse(env, request, f);
|
|
4357
4353
|
}
|
|
4358
4354
|
const manifests = await loadManifests(tenantId, env);
|
|
4359
|
-
// ── a design-system draft, across the site (`?ds=<draft>`, §5) ──────────────────
|
|
4360
|
-
// A path under the workspace's declared skill folder resolves from THAT draft's table
|
|
4361
|
-
// when the request carries one, so a shared change can be looked at on every prototype
|
|
4362
|
-
// before it lands. Only the skill's own paths: a prototype's files never come from it,
|
|
4363
|
-
// and a draft that does not hold the file falls through to main.
|
|
4364
|
-
if (opts.dsDraft && DRAFT_ID_RE.test(opts.dsDraft) && decoded) {
|
|
4365
|
-
const skill = Object.values(manifests)
|
|
4366
|
-
.flatMap((m) => ((m && m.routing) || {}).publicSkillPrefixes || [])
|
|
4367
|
-
.map(normUnit).find((p) => p && decoded.startsWith(p));
|
|
4368
|
-
const stub = skill ? unitStub(env, tenantId, skill) : null;
|
|
4369
|
-
if (stub) {
|
|
4370
|
-
const dr = await unitCall(stub, `/draft/${opts.dsDraft}`, null, "GET");
|
|
4371
|
-
const f = dr.status === 200 && dr.body.table ? dr.body.table[decoded] : null;
|
|
4372
|
-
if (f) return blobResponse(env, request, f);
|
|
4373
|
-
}
|
|
4374
|
-
}
|
|
4375
4355
|
const r = resolveBundlePath(manifests, url.pathname);
|
|
4376
4356
|
if (r.redirect) return Response.redirect(new URL(r.redirect + url.search, url).toString(), 308);
|
|
4377
4357
|
if (r.miss) return new Response("Not Found", { status: 404 });
|
|
@@ -5049,23 +5029,21 @@ const UNIT_CT_RE = /^[\w.+-]+\/[\w.+-]+(; ?charset=[\w-]+)?$/i;
|
|
|
5049
5029
|
// to anonymous visitors, from a draft on a folder nobody edits.
|
|
5050
5030
|
const isNewUnitPath = (unit) => unit.split("/").filter(Boolean).length === 2;
|
|
5051
5031
|
// Top-level folders a NEW unit may not be created under. A landing replaces a unit's
|
|
5052
|
-
// folder wholesale, so
|
|
5053
|
-
//
|
|
5054
|
-
// are
|
|
5055
|
-
//
|
|
5056
|
-
//
|
|
5057
|
-
//
|
|
5058
|
-
//
|
|
5059
|
-
// manifest ALREADY declares is never held to this: the manifest is the authority.
|
|
5032
|
+
// folder wholesale, so a draft opened on `/components/button/` or `/skills/x-ui/` would
|
|
5033
|
+
// have replaced the design system's or the gallery's own files with whatever one draft
|
|
5034
|
+
// held. These are the folders a site builds from and every prototype reads: the design
|
|
5035
|
+
// system, the gallery pages, the changelog, the search index, engine chrome (`_`, `__`).
|
|
5036
|
+
// Stateless and the same on every workspace — an opportunity folder is anything else,
|
|
5037
|
+
// `/playground/<name>/` included. A unit the manifest ALREADY declares is never held to
|
|
5038
|
+
// this: the manifest is the authority on what exists.
|
|
5060
5039
|
const RESERVED_UNIT_FOLDERS = Object.freeze([
|
|
5061
|
-
"
|
|
5040
|
+
"base", "components", "pages", "patterns", "skills", "tokens", "fonts",
|
|
5041
|
+
"admin", "changelog", "search",
|
|
5062
5042
|
]);
|
|
5063
5043
|
const isReservedUnitFolder = (unit) => {
|
|
5064
5044
|
const first = unit.split("/").filter(Boolean)[0] || "";
|
|
5065
5045
|
return first.startsWith("_") || RESERVED_UNIT_FOLDERS.includes(first);
|
|
5066
5046
|
};
|
|
5067
|
-
/** The one unit allowed under `skills/`: the design system the workspace declares. */
|
|
5068
|
-
const isDeclaredSkillUnit = (unit, tctx) => ((tctx && tctx.PUBLIC_SKILL_PREFIXES) || []).some((p) => normUnit(p) === unit);
|
|
5069
5047
|
const shortText = (s, n) => String(s == null ? "" : s).slice(0, n);
|
|
5070
5048
|
// `tctx.SPACES` is a routing field, filled from the live manifests by `loadTenantContext`
|
|
5071
5049
|
// on the request's normal path — which this route also goes through, since `handleRequest`
|
|
@@ -5162,11 +5140,7 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
|
|
|
5162
5140
|
// A unit the manifest already declares keeps the entry it has, spelling and all: the
|
|
5163
5141
|
// set union that replaced it added a second, normalized entry beside one written
|
|
5164
5142
|
// without its trailing slash, and two entries for one unit is two things to prune.
|
|
5165
|
-
|
|
5166
|
-
// surface is the rendered-asset rule `publicSkillPrefixes` already spells, and a whole
|
|
5167
|
-
// skill folder opened to the gate would expose the documents that rule keeps gated.
|
|
5168
|
-
const skillUnit = ((cur.routing || {}).publicSkillPrefixes || []).some((p) => normUnit(p) === unit);
|
|
5169
|
-
routing.publicPrefixes = authoredUnits(cur).has(unit) || skillUnit
|
|
5143
|
+
routing.publicPrefixes = authoredUnits(cur).has(unit)
|
|
5170
5144
|
? [...(routing.publicPrefixes || [])]
|
|
5171
5145
|
: [...(routing.publicPrefixes || []), unit];
|
|
5172
5146
|
routing.unitSources = { ...(routing.unitSources || {}), [unit]: { sha: null, dirty: false, landed: true, by: who.personId, at: now } };
|
|
@@ -5323,7 +5297,7 @@ async function unitApi(tctx, request, url, env) {
|
|
|
5323
5297
|
// reached, so a folder that is not one is never given a draft to be adopted from later.
|
|
5324
5298
|
// A unit the space ALREADY publishes is a unit whatever its shape (the manifest is the
|
|
5325
5299
|
// authority on what exists); anything else has to be the shape a new one is created in.
|
|
5326
|
-
if (!authoredUnits(live).has(unit)
|
|
5300
|
+
if (!authoredUnits(live).has(unit)) {
|
|
5327
5301
|
if (!isNewUnitPath(unit)) return jsonResponse({ error: "bad-unit", reason: "not-a-prototype-folder" }, 400);
|
|
5328
5302
|
if (isReservedUnitFolder(unit)) return jsonResponse({ error: "bad-unit", reason: "reserved-folder" }, 400);
|
|
5329
5303
|
}
|
|
@@ -5975,7 +5949,7 @@ async function publishApi(tctx, request, url, env) {
|
|
|
5975
5949
|
clientProtocol,
|
|
5976
5950
|
minProtocol: tctx.MIN_CLIENT_PROTOCOL,
|
|
5977
5951
|
protocol: PUBLISH_PROTOCOL,
|
|
5978
|
-
upgrade: "npx augur@latest",
|
|
5952
|
+
upgrade: "npx @augurworks/augur@latest",
|
|
5979
5953
|
}, 426);
|
|
5980
5954
|
}
|
|
5981
5955
|
// Prune any publicPrefixes entry THIS manifest declares without backing it with
|
|
@@ -7264,94 +7238,6 @@ async function composeChrome(tctx, res, url) {
|
|
|
7264
7238
|
return new Response(html, { status: res.status, statusText: res.statusText, headers });
|
|
7265
7239
|
}
|
|
7266
7240
|
|
|
7267
|
-
// ---- Derived pages (drafts that land, §6.4) -------------------------------------
|
|
7268
|
-
// Where drafts are served, the gallery, each opportunity's index, the playground, the
|
|
7269
|
-
// library tiers and the finder's index are RENDERED HERE from the live store, so a landing
|
|
7270
|
-
// is on them at once and no client build ships them. The stored copies a publish once
|
|
7271
|
-
// baked are ignored on this path — they cannot know about a landing. Where drafts are not
|
|
7272
|
-
// served, nothing here runs and the stored pages serve exactly as before.
|
|
7273
|
-
//
|
|
7274
|
-
// Two store reads feed a render besides the manifest — the status baseline
|
|
7275
|
-
// (`/prototype-status.json`) and the design-system catalog (`/registry.json`) — cached per
|
|
7276
|
-
// manifest version; the status OVERLAY is read fresh every time, one get, as the status
|
|
7277
|
-
// route itself reads it. Per-request work after that is string rendering.
|
|
7278
|
-
const DERIVED = tenantCache("derived");
|
|
7279
|
-
const DERIVED_PATH_RE = /^\/(?:[^/]+\/?(?:index\.html)?|index\.html|__search\.json)?$/;
|
|
7280
|
-
async function derivedInputs(tctx, env, spaceId, manifest) {
|
|
7281
|
-
const slot = DERIVED.entry(tctx.tenantId, () => ({ key: null, baseline: {}, catalog: {} }));
|
|
7282
|
-
const key = `${spaceId}:${manifest.version}`;
|
|
7283
|
-
if (slot.key === key) return slot;
|
|
7284
|
-
const readJson = async (p) => {
|
|
7285
|
-
const f = (manifest.files || {})[p];
|
|
7286
|
-
if (!f || !env.BUNDLES) return null;
|
|
7287
|
-
try {
|
|
7288
|
-
const obj = await env.BUNDLES.get("blobs/" + f.h);
|
|
7289
|
-
return obj ? JSON.parse(await obj.text()) : null;
|
|
7290
|
-
} catch (e) { return null; }
|
|
7291
|
-
};
|
|
7292
|
-
const base = await readJson("/prototype-status.json");
|
|
7293
|
-
const baseline = {};
|
|
7294
|
-
for (const [k, v] of Object.entries(base && typeof base === "object" ? base : {})) {
|
|
7295
|
-
if (!k.startsWith("_") && typeof v === "string") baseline[k] = v;
|
|
7296
|
-
}
|
|
7297
|
-
slot.baseline = baseline;
|
|
7298
|
-
slot.catalog = catalogFrom(await readJson("/registry.json"));
|
|
7299
|
-
slot.key = key;
|
|
7300
|
-
return slot;
|
|
7301
|
-
}
|
|
7302
|
-
/** A derived page for this path, or null when the path is not one (or drafts are not served here). */
|
|
7303
|
-
async function derivedPage(tctx, env, url) {
|
|
7304
|
-
if (!draftsServedHere(env) || !DERIVED_PATH_RE.test(url.pathname)) return null;
|
|
7305
|
-
const manifests = await loadManifests(tctx.tenantId, env);
|
|
7306
|
-
const spaceId = defaultSpaceIdFromCtx(tctx) || defaultSpaceIdFromManifests(manifests);
|
|
7307
|
-
const manifest = spaceId ? manifests[spaceId] : null;
|
|
7308
|
-
if (!manifest || !manifest.files) return null;
|
|
7309
|
-
const inputs = await derivedInputs(tctx, env, spaceId, manifest);
|
|
7310
|
-
const store = overlayFor(env, tctx);
|
|
7311
|
-
let statuses = {};
|
|
7312
|
-
try { statuses = store ? (await store.read("statuses")) || {} : {}; } catch (e) { statuses = {}; }
|
|
7313
|
-
// A recorded author resolves to a face through the roster; an id nobody answers to
|
|
7314
|
-
// (an ex-member, the `live` adoption) is nobody's face rather than a blank chip.
|
|
7315
|
-
const people = (id) => { const f = personFace(tctx.USERS, id); return f.name ? { id, ...f } : null; };
|
|
7316
|
-
const now = Date.now();
|
|
7317
|
-
const model = siteModel({ manifest, statuses, baseline: inputs.baseline, people, now });
|
|
7318
|
-
const kind = derivedPathKind(url.pathname, model);
|
|
7319
|
-
if (!kind) return null;
|
|
7320
|
-
const sp = (tctx.SPACES || []).find((s) => s.id === spaceId) || {};
|
|
7321
|
-
const ctx = { spaces: tctx.SPACES || [], activeSpace: spaceId, chrome: tctx.CHROME_POINTER || null, projectsLabel: sp.projectsLabel || "", now };
|
|
7322
|
-
if (kind.kind === "search") return jsonResponse(searchIndex(model, ctx), 200, { "Cache-Control": "no-cache" });
|
|
7323
|
-
if (kind.kind === "opportunity" && !kind.slash) return Response.redirect(new URL(`/${encodeURIComponent(kind.name)}/${url.search}`, url).toString(), 308);
|
|
7324
|
-
const html = kind.kind === "root" ? renderRootIndex(model, ctx)
|
|
7325
|
-
: kind.kind === "opportunity" ? renderOpportunityIndex(model, kind.name, ctx)
|
|
7326
|
-
: kind.kind === "playground" ? renderPlaygroundIndex(model, ctx)
|
|
7327
|
-
: kind.kind === "tier" ? renderTierIndex(model, kind.tier, ctx)
|
|
7328
|
-
: kind.kind === "components" ? renderComponentsIndex(model, inputs.catalog, ctx)
|
|
7329
|
-
: null;
|
|
7330
|
-
if (html == null) return null;
|
|
7331
|
-
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": ASSET_REVALIDATE } });
|
|
7332
|
-
}
|
|
7333
|
-
|
|
7334
|
-
// ---- The design-system overlay cookie (`?ds=`) --------------------------------
|
|
7335
|
-
// `?ds=<draft>` on any page names a design-system draft to look through; the cookie keeps
|
|
7336
|
-
// it while the person navigates; `?ds=` with nothing drops it. A malformed id is nobody's
|
|
7337
|
-
// draft and touches no cookie. Members only — the branch that reads this is past the gate.
|
|
7338
|
-
const DS_COOKIE = "augur_ds";
|
|
7339
|
-
function dsOverlay(request, url) {
|
|
7340
|
-
if (url.searchParams.has("ds")) {
|
|
7341
|
-
const v = url.searchParams.get("ds") || "";
|
|
7342
|
-
if (v === "") return { draft: null, setCookie: `${DS_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; Secure; HttpOnly` };
|
|
7343
|
-
if (!DRAFT_ID_RE.test(v)) return { draft: null, setCookie: null };
|
|
7344
|
-
return { draft: v, setCookie: `${DS_COOKIE}=${v}; Path=/; SameSite=Lax; Secure; HttpOnly` };
|
|
7345
|
-
}
|
|
7346
|
-
const c = cookieValue(request.headers.get("Cookie") || "", DS_COOKIE);
|
|
7347
|
-
return { draft: c && DRAFT_ID_RE.test(c) ? c : null, setCookie: null };
|
|
7348
|
-
}
|
|
7349
|
-
function withSetCookie(res, cookie) {
|
|
7350
|
-
const out = new Response(res.body, res);
|
|
7351
|
-
out.headers.append("Set-Cookie", cookie);
|
|
7352
|
-
return out;
|
|
7353
|
-
}
|
|
7354
|
-
|
|
7355
7241
|
// ---- The draft bar (drafts that land, §5) -------------------------------------
|
|
7356
7242
|
// A prototype is raw HTML from a space and carries no chrome; a draft has no build step
|
|
7357
7243
|
// that could bake one in. So a member looking at a unit's page — main or a draft address —
|
|
@@ -9149,7 +9035,7 @@ function doorFacts(tctx, url, env) {
|
|
|
9149
9035
|
origin: url.origin,
|
|
9150
9036
|
engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
|
|
9151
9037
|
pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
|
|
9152
|
-
connect: pairing ? `npx augur connect --origin ${url.origin}` : null,
|
|
9038
|
+
connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
|
|
9153
9039
|
docs: DOOR_DOCS,
|
|
9154
9040
|
// Where drafts are served, a prototype is changed by opening it, not by shipping a
|
|
9155
9041
|
// tree — and the door is where a cold agent learns which of the two this instance is.
|
|
@@ -9170,7 +9056,7 @@ function doorText(f) {
|
|
|
9170
9056
|
+ `It prints a link and a code. The owner of this workspace opens the link in a\n`
|
|
9171
9057
|
+ `browser they are already signed in to and enters the code. The token lands on\n`
|
|
9172
9058
|
+ `that machine, and \`augur publish\` / \`augur ship\` use it from then on. With no\n`
|
|
9173
|
-
+ `source tree yet, \`npx augur clone --space ${f.workspace}\` then fetches one (it reads\n`
|
|
9059
|
+
+ `source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches one (it reads\n`
|
|
9174
9060
|
+ `the origin from the pairing).\n\n`
|
|
9175
9061
|
+ `Not on npm yet? The engine clone sits next to every workspace that publishes:\n\n`
|
|
9176
9062
|
+ ` node <engine>/scripts/cli.mjs connect --origin ${f.origin}\n\n`
|
|
@@ -12547,20 +12433,13 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12547
12433
|
// Past the gate (or nothing gates the site) → serve. A 404 gets one more chance
|
|
12548
12434
|
// as a created canvas (a KV-registered board with no repo file — see canvasesApi).
|
|
12549
12435
|
if (authed) {
|
|
12550
|
-
|
|
12551
|
-
const ds = dsOverlay(request, url);
|
|
12552
|
-
const stamp = (res) => (ds.setCookie ? withSetCookie(res, ds.setCookie) : res);
|
|
12553
|
-
// Where drafts are served, the gallery and its indexes are derived from the live
|
|
12554
|
-
// store rather than read from it — a landing is on them at once.
|
|
12555
|
-
const derived = await derivedPage(tctx, env, url);
|
|
12556
|
-
if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env));
|
|
12557
|
-
const asset = await assetFetch(tctx.tenantId, env, request, { dsDraft: ds.draft });
|
|
12436
|
+
const asset = await assetFetch(tctx.tenantId, env, request);
|
|
12558
12437
|
if (asset.status === 404) {
|
|
12559
12438
|
const virt = await virtualCanvas(tctx, request, env, url);
|
|
12560
|
-
if (virt) return
|
|
12561
|
-
return
|
|
12439
|
+
if (virt) return virt;
|
|
12440
|
+
return notFoundResponse(tctx);
|
|
12562
12441
|
}
|
|
12563
|
-
return
|
|
12442
|
+
return serveContent(tctx, asset, url, me, env);
|
|
12564
12443
|
}
|
|
12565
12444
|
|
|
12566
12445
|
// Created canvas boards are public like published prototypes — same obscure
|
|
@@ -12645,7 +12524,7 @@ export const __testables = Object.freeze({
|
|
|
12645
12524
|
doorFacts, doorText, wantsMachineDoor, gateResponse, DOOR_DOCS, DOOR_WELL_KNOWN,
|
|
12646
12525
|
resumeAfterDormancy,
|
|
12647
12526
|
PITI_VIEW_KEY, PITI_REMARKS_KEY,
|
|
12648
|
-
publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot,
|
|
12527
|
+
publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, isEngineChrome, publishRefusalBody, splitDraftPath,
|
|
12649
12528
|
adminStorageApi,
|
|
12650
12529
|
adminCustomDomainApi,
|
|
12651
12530
|
isPrefixBacked, backedPublicPrefixes,
|
package/src/seed-pack.mjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
//
|
|
45
45
|
// ONE SUBSTITUTION. connect-your-terminal ships with an empty `CONNECT_COMMAND` slot and
|
|
46
46
|
// derives a command from the URL it is served on. Provisioning fills the slot with the
|
|
47
|
-
// workspace's REAL command — `npx augur connect --origin https://<label><suffix>` — so the
|
|
47
|
+
// workspace's REAL command — `npx @augurworks/augur connect --origin https://<label><suffix>` — so the
|
|
48
48
|
// page is exact rather than merely not wrong. The filled page hashes differently, so it is
|
|
49
49
|
// the one blob that is per-workspace rather than shared; everything else dedups.
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ export const SEED_CONNECT_SLOT = 'var CONNECT_COMMAND = "";';
|
|
|
60
60
|
/** The one-line command a person runs to pair a terminal with their workspace. */
|
|
61
61
|
export function connectCommandFor(origin) {
|
|
62
62
|
const o = String(origin || "").replace(/\/+$/, "");
|
|
63
|
-
return o ? `npx augur connect --origin ${o}` : "";
|
|
63
|
+
return o ? `npx @augurworks/augur connect --origin ${o}` : "";
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
package/src/galleries.mjs
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
// galleries.mjs — the pages a workspace DERIVES, rendered at serve time from the live store.
|
|
2
|
-
//
|
|
3
|
-
// docs/drafts-that-land.md §6.4. The gallery, each opportunity's index, the playground, the
|
|
4
|
-
// library tiers and the search index used to be baked by a client's build and shipped with a
|
|
5
|
-
// publish, so a landing could not appear on them until somebody published a tree. Here they
|
|
6
|
-
// are a pure function of what the store holds: the live manifest (units, files, per-file
|
|
7
|
-
// provenance), the status overlay, the roster (ids → faces) and the design-system catalog.
|
|
8
|
-
// The worker calls this where drafts are served; the markup keeps the card contract the
|
|
9
|
-
// chrome bundle's scripts read (class names and data-* attributes), so pins, renames, status
|
|
10
|
-
// clicks, currency, faces and the finder keep working on a derived page exactly as they did
|
|
11
|
-
// on a baked one.
|
|
12
|
-
//
|
|
13
|
-
// ⚠️ NO NODE IMPORTS, NO I/O. The worker runs this per request.
|
|
14
|
-
import { renderAppChrome, renderSpaceContextScript, CHROME_MARK_START, CHROME_MARK_END, UI_VERSION, escAttr, titleCase, fmtDate, relTime } from "./chrome/appchrome.mjs";
|
|
15
|
-
import { authoredUnits } from "./publish-units.mjs";
|
|
16
|
-
import { unitProvenance, unitKey, STATUS_LABELS } from "./currency.mjs";
|
|
17
|
-
|
|
18
|
-
export const TIERS = Object.freeze(["base", "components", "patterns", "pages"]);
|
|
19
|
-
const TIER_TITLE = Object.freeze({ base: "Base", components: "Components", patterns: "Patterns", pages: "Pages" });
|
|
20
|
-
|
|
21
|
-
const STATUS_META = Object.freeze({
|
|
22
|
-
"in-progress": { label: STATUS_LABELS["in-progress"], cls: "is-wip" },
|
|
23
|
-
"dev-ready": { label: STATUS_LABELS["dev-ready"], cls: "is-ready" },
|
|
24
|
-
ignore: { label: STATUS_LABELS.ignore, cls: "is-ignore" },
|
|
25
|
-
});
|
|
26
|
-
const STATUS_ICONS = Object.freeze({
|
|
27
|
-
"dev-ready": '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="9" fill="#17935a"/><path d="M5.8 10.4l2.7 2.7 5.7-6" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
28
|
-
"in-progress": '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8" fill="none" stroke="#1c1c22" stroke-width="2.2"/><path d="M10 2.8a7.2 7.2 0 0 1 0 14.4z" fill="#1c1c22"/></svg>',
|
|
29
|
-
ignore: '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8" fill="none" stroke="#aeb3bd" stroke-width="2.2"/><line x1="6.4" y1="10" x2="13.6" y2="10" stroke="#aeb3bd" stroke-width="2.2" stroke-linecap="round"/></svg>',
|
|
30
|
-
});
|
|
31
|
-
const STATUS_RANK = Object.freeze({ "dev-ready": 0, "in-progress": 1, ignore: 2 });
|
|
32
|
-
const IC_STAR = `<svg class="pin-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></svg>`;
|
|
33
|
-
// Spelled out rather than built with `ic()`: the module-scope lint reads a call initializer
|
|
34
|
-
// as state it cannot prove pure, and a literal costs nothing.
|
|
35
|
-
const IC_PLUS = `<svg class="gvic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`;
|
|
36
|
-
const EMOJI_POOL = Object.freeze(["🗳️", "🏛️", "📊", "🧭", "🛰️", "🧩", "🪧", "🌳", "🚲", "📣", "🗺️", "🧪", "💡", "🔭", "🪟", "🧱", "🎛️", "🛣️", "🧰", "📐", "🧮", "🗂️", "🔔", "🏘️", "🌍", "💬", "📝", "🚏", "🏙️", "🌿", "🎚️", "🧷"]);
|
|
37
|
-
|
|
38
|
-
const dec = (s) => { try { return decodeURIComponent(String(s)); } catch (e) { return String(s); } };
|
|
39
|
-
const enc = (s) => encodeURIComponent(s);
|
|
40
|
-
const plural = (n, word) => `${n} ${n === 1 ? word : word.endsWith("y") ? word.slice(0, -1) + "ies" : word + "s"}`;
|
|
41
|
-
function protoEmoji(slug) {
|
|
42
|
-
let h = 0;
|
|
43
|
-
for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
|
|
44
|
-
return EMOJI_POOL[h % EMOJI_POOL.length];
|
|
45
|
-
}
|
|
46
|
-
const protoName = (slug) => `${protoEmoji(slug)} ${titleCase(slug)}`;
|
|
47
|
-
|
|
48
|
-
// ── the site model ───────────────────────────────────────────────────────────
|
|
49
|
-
/** Which list a unit belongs to, from its path alone. */
|
|
50
|
-
export function unitHome(unit) {
|
|
51
|
-
const segs = dec(unit).split("/").filter(Boolean);
|
|
52
|
-
if (segs.length !== 2) return null;
|
|
53
|
-
if (segs[0] === "playground") return { kind: "playground", name: segs[1] };
|
|
54
|
-
if (TIERS.includes(segs[0])) return { kind: "tier", tier: segs[0], name: segs[1] };
|
|
55
|
-
if (segs[0].startsWith("_") || segs[0] === "skills") return null;
|
|
56
|
-
return { kind: "opportunity", opp: segs[0], name: segs[1] };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function unitEntry(manifest, unit, home, { statuses, baseline, people, now }) {
|
|
60
|
-
const files = manifest.files || {};
|
|
61
|
-
const prefix = dec(unit);
|
|
62
|
-
const inUnit = Object.keys(files).filter((p) => dec(p).startsWith(prefix));
|
|
63
|
-
const rel = (p) => dec(p).slice(prefix.length);
|
|
64
|
-
const hasIndex = inUnit.some((p) => rel(p) === "index.html");
|
|
65
|
-
const firstHtml = inUnit.map(rel).filter((r) => !r.includes("/") && r.endsWith(".html")).sort()[0] || null;
|
|
66
|
-
const file = hasIndex ? `${unit}index.html` : firstHtml ? `${unit}${enc(firstHtml)}` : null;
|
|
67
|
-
const { editedAt, by } = unitProvenance(manifest, unit);
|
|
68
|
-
const key = unitKey(unit);
|
|
69
|
-
const status = (statuses && Object.prototype.hasOwnProperty.call(statuses, key) && statuses[key])
|
|
70
|
-
|| (baseline && baseline[key]) || null;
|
|
71
|
-
const ids = [...new Set(inUnit.map((p) => files[p] && files[p].by).filter(Boolean))];
|
|
72
|
-
const editors = ids.map((id) => people(id)).filter(Boolean);
|
|
73
|
-
return {
|
|
74
|
-
name: home.name, unit, href: unit, file,
|
|
75
|
-
poster: inUnit.some((p) => rel(p) === "preview.webp"),
|
|
76
|
-
editedAt: editedAt || null,
|
|
77
|
-
mtimeMs: editedAt ? Date.parse(editedAt) : 0,
|
|
78
|
-
status, editors, by,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const byRecency = (a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name);
|
|
83
|
-
const byStatusThenRecency = (a, b) => {
|
|
84
|
-
const ra = a.status in STATUS_RANK ? STATUS_RANK[a.status] : 2;
|
|
85
|
-
const rb = b.status in STATUS_RANK ? STATUS_RANK[b.status] : 2;
|
|
86
|
-
return ra - rb || byRecency(a, b);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Everything the derived pages need, from the live manifest and the overlays.
|
|
91
|
-
* `people(id) → {id, name, initials, color} | null` resolves a recorded author id.
|
|
92
|
-
*/
|
|
93
|
-
export function siteModel({ manifest, statuses = {}, baseline = {}, people = () => null, now = Date.now() }) {
|
|
94
|
-
const opps = new Map(), playground = [], tiers = { base: [], components: [], patterns: [], pages: [] };
|
|
95
|
-
const units = [];
|
|
96
|
-
for (const unit of authoredUnits(manifest || {})) {
|
|
97
|
-
const home = unitHome(unit);
|
|
98
|
-
if (!home) continue;
|
|
99
|
-
const e = unitEntry(manifest, unit, home, { statuses, baseline, people, now });
|
|
100
|
-
units.push({ ...e, home });
|
|
101
|
-
if (home.kind === "playground") { playground.push({ ...e, status: e.status || "in-progress" }); continue; }
|
|
102
|
-
if (home.kind === "tier") { tiers[home.tier].push(e); continue; }
|
|
103
|
-
if (!opps.has(home.opp)) opps.set(home.opp, { name: home.opp, prototypes: [] });
|
|
104
|
-
opps.get(home.opp).prototypes.push(e);
|
|
105
|
-
}
|
|
106
|
-
const opportunities = [...opps.values()].map((o) => {
|
|
107
|
-
o.prototypes.sort(byStatusThenRecency);
|
|
108
|
-
const seen = new Map();
|
|
109
|
-
for (const p of o.prototypes) for (const u of p.editors) if (!seen.has(u.id)) seen.set(u.id, u);
|
|
110
|
-
return { ...o, people: [...seen.values()], mtimeMs: Math.max(0, ...o.prototypes.map((p) => p.mtimeMs)) };
|
|
111
|
-
}).sort(byRecency);
|
|
112
|
-
playground.sort(byStatusThenRecency);
|
|
113
|
-
for (const t of TIERS) tiers[t].sort(byRecency);
|
|
114
|
-
return { opportunities, playground, tiers, hasPlayground: playground.length > 0, units };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** What a request path names on the derived surface, or null for anything else. */
|
|
118
|
-
export function derivedPathKind(pathname, model) {
|
|
119
|
-
const p = dec(pathname);
|
|
120
|
-
if (p === "/" || p === "/index.html") return { kind: "root" };
|
|
121
|
-
if (p === "/__search.json") return { kind: "search" };
|
|
122
|
-
const m = /^\/([^/]+)\/?(index\.html)?$/.exec(p);
|
|
123
|
-
if (!m) return null;
|
|
124
|
-
const seg = m[1];
|
|
125
|
-
if (seg === "playground") return model.hasPlayground || p.endsWith("/") ? { kind: "playground" } : null;
|
|
126
|
-
if (TIERS.includes(seg)) return { kind: seg === "components" ? "components" : "tier", tier: seg };
|
|
127
|
-
if (model.opportunities.some((o) => o.name === seg)) return { kind: "opportunity", name: seg, slash: p.endsWith("/") || p.endsWith("index.html") };
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── markup ───────────────────────────────────────────────────────────────────
|
|
132
|
-
const media = (href, hasPoster) => hasPoster
|
|
133
|
-
? `<img class="preview-img" src="${href}preview.webp" alt="" aria-hidden="true" loading="lazy" decoding="async" width="768" height="480" />`
|
|
134
|
-
: `<div class="preview-ph" aria-hidden="true"></div>`;
|
|
135
|
-
const preview = (href, hasPoster) => `<div class="preview${hasPoster ? "" : " preview--ph"}">${media(href, hasPoster)}</div>`;
|
|
136
|
-
const filterEmpty = () => `<p class="filter-empty" data-filter-empty hidden>No matches.</p>`;
|
|
137
|
-
const pinStar = (key, href) => `<button type="button" class="pin-btn" data-pin-key="${key}" data-pin-href="${href}" aria-pressed="false" aria-label="Pin to sidebar" title="Pin to sidebar">${IC_STAR}</button>`;
|
|
138
|
-
const newCanvasBtn = (dir) => `<button type="button" class="folderbar__new" data-new-canvas="${dir}" hidden>${IC_PLUS}New canvas</button>`;
|
|
139
|
-
const emptyState = (...paras) => paras.map((p) => `<p class="empty">${p}</p>`).join("");
|
|
140
|
-
const emptyHead = (title, keepOnMobile = false) =>
|
|
141
|
-
`<header class="folderbar${keepOnMobile ? " folderbar--keep" : ""}"><h1 class="folderbar__title">${title}</h1><span class="folderbar__count">0</span><span class="folderbar__rule"></span></header>`;
|
|
142
|
-
const ghostLine = (w) => `<span class="ghost-line" style="width:${w}"></span>`;
|
|
143
|
-
const ghosts = (inner) => `<div class="ghosts" aria-hidden="true">${inner}</div>`;
|
|
144
|
-
const ghostFolderGrid = (n = 6) => ghosts(`<div class="opp-grid">${Array.from({ length: n }, () => `<div class="card-opp"><div class="preview preview--pending"></div><div class="proto-meta"><div class="proto-text">${ghostLine("58%")}${ghostLine("30%")}</div></div></div>`).join("")}</div>`);
|
|
145
|
-
const ghostCardGrid = (n = 8) => ghosts(`<div class="page-grid">${Array.from({ length: n }, () => `<div class="card-proto is-pending"><div class="preview preview--pending"></div><div class="proto-meta">${ghostLine("56%")}</div></div>`).join("")}</div>`);
|
|
146
|
-
const ghostCompTable = (n = 4) => ghosts(`<table class="comp-table"><thead><tr><th>Preview</th><th>Component</th><th>What it is</th><th class="comp-status">Status</th></tr></thead><tbody>${Array.from({ length: n }, () => `<tr><td><span class="ghost-thumb"></span></td><td><div class="comp-name">${ghostLine("110px")}</div><div class="comp-badges"><span class="ghost-chip" style="width:54px"></span><span class="ghost-chip" style="width:40px"></span></div></td><td><div class="comp-desc">${ghostLine("100%")}${ghostLine("62%")}</div></td><td class="comp-status"><span class="ghost-dot"></span></td></tr>`).join("")}</tbody></table>`);
|
|
147
|
-
|
|
148
|
-
function faceChip(u, cls) {
|
|
149
|
-
const ini = escAttr((u.initials || (u.name || "?").slice(0, 2)).toUpperCase());
|
|
150
|
-
const label = escAttr(u.name || "Someone");
|
|
151
|
-
return `<span class="${cls}" style="background-color:${u.color || "#4f46e5"}" data-person="${escAttr(u.id)}" title="${label}" aria-label="${label}">${ini}</span>`;
|
|
152
|
-
}
|
|
153
|
-
function facePile(people, cap = 5) {
|
|
154
|
-
if (!people || !people.length) return "";
|
|
155
|
-
const shown = people.slice(0, cap), extra = people.length - shown.length;
|
|
156
|
-
const chips = shown.map((u) => faceChip(u, "proto-editor opp-face")).join("");
|
|
157
|
-
const more = extra > 0 ? `<span class="proto-editor opp-face opp-face--more" title="+${extra} more">+${extra}</span>` : "";
|
|
158
|
-
return `<span class="opp-people" role="group" aria-label="Contributors">${chips}${more}</span>`;
|
|
159
|
-
}
|
|
160
|
-
function currencyLine(key, p, now) {
|
|
161
|
-
const cur = STATUS_META[p.status] ? p.status : "ignore";
|
|
162
|
-
const word = cur === "ignore" ? "" : `<span class="proto-state ${STATUS_META[cur].cls}">${STATUS_META[cur].label}</span> · `;
|
|
163
|
-
const when = p.mtimeMs ? `<span class="proto-when" title="${fmtDate(p.mtimeMs)}">${relTime(p.mtimeMs, now)}</span>` : `<span class="proto-when"></span>`;
|
|
164
|
-
return `<div class="proto-date" data-currency="${escAttr(key)}">${word}${when}</div>`;
|
|
165
|
-
}
|
|
166
|
-
function statusChip(status, key) {
|
|
167
|
-
const cur = STATUS_META[status] ? status : "ignore";
|
|
168
|
-
const aria = `Status: ${STATUS_META[cur].label}. Click to change.`;
|
|
169
|
-
return `<button type="button" class="status-chip ${STATUS_META[cur].cls}" data-status-key="${key}" data-status="${cur}" aria-label="${aria}" title="${aria}">${STATUS_ICONS[cur]}</button>`;
|
|
170
|
-
}
|
|
171
|
-
const compStatusChip = (name) => {
|
|
172
|
-
const aria = "Validation: In progress. Click to mark reviewed.";
|
|
173
|
-
return `<button type="button" class="status-chip is-wip" data-comp-status-key="components/${name}" data-status="in-progress" aria-label="${aria}" title="${aria}">${STATUS_ICONS["in-progress"]}</button>`;
|
|
174
|
-
};
|
|
175
|
-
function metaBadges(meta) {
|
|
176
|
-
const layer = meta.layer ? `<span class="cbadge cbadge--layer-${escAttr(meta.layer)}">${escAttr(meta.layer)}</span>` : "";
|
|
177
|
-
const surf = meta.surface ? `<span class="cbadge cbadge--surf">${escAttr(meta.surface)}</span>` : "";
|
|
178
|
-
const cat = meta.category ? `<span class="cbadge cbadge--cat">${escAttr(meta.category)}</span>` : "";
|
|
179
|
-
const stat = meta.status ? `<span class="cbadge cbadge--st-${escAttr(meta.status)}">${escAttr(meta.status)}</span>` : "";
|
|
180
|
-
return `<div class="comp-badges">${layer}${surf}${cat}${stat}</div>`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** The page skeleton the build used to emit, with the CURRENT chrome. */
|
|
184
|
-
export function shell({ title, body, activeTab = "prototypes", wrapClass = "", ctx }) {
|
|
185
|
-
const state = { spaces: ctx.spaces || [], activeSpace: ctx.activeSpace || "", opportunities: [], hasPlayground: !!ctx.hasPlayground };
|
|
186
|
-
const css = ctx.chrome && ctx.chrome.css ? `/${ctx.chrome.css}` : "";
|
|
187
|
-
const js = ctx.chrome && ctx.chrome.js ? `/${ctx.chrome.js}` : "";
|
|
188
|
-
return `<!doctype html>
|
|
189
|
-
<html lang="en">
|
|
190
|
-
<head>
|
|
191
|
-
<meta charset="utf-8" />
|
|
192
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
193
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
194
|
-
<title>${escAttr(title)}</title>
|
|
195
|
-
<link rel="icon" type="image/png" href="/augur-mark.png?v=${UI_VERSION}" />
|
|
196
|
-
<link rel="apple-touch-icon" href="/augur-mark.png?v=${UI_VERSION}" />
|
|
197
|
-
<link rel="manifest" href="/manifest.webmanifest" />
|
|
198
|
-
<meta name="theme-color" content="#2C2150" />
|
|
199
|
-
<link rel="preload" href="/fonts/inter-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin />
|
|
200
|
-
${js ? ` <link rel="preload" href="${js}" as="script" />\n` : ""}${css ? ` <link rel="stylesheet" href="${css}" />\n` : ""}</head>
|
|
201
|
-
<body>
|
|
202
|
-
${CHROME_MARK_START(state.activeSpace, activeTab, state.hasPlayground)}${renderAppChrome(activeTab, state, {})}${CHROME_MARK_END}
|
|
203
|
-
<div class="wrap${wrapClass ? " " + wrapClass : ""}">
|
|
204
|
-
${body}
|
|
205
|
-
</div>
|
|
206
|
-
<script>${renderSpaceContextScript(state)}
|
|
207
|
-
</script>
|
|
208
|
-
${js ? ` <script defer src="${js}"></script>\n` : ""} <script type="speculationrules">${JSON.stringify({ prefetch: [{ where: { and: [{ href_matches: "/*" }, { not: { href_matches: "/__*" } }] }, eagerness: "moderate" }] })}</script>
|
|
209
|
-
</body>
|
|
210
|
-
</html>
|
|
211
|
-
`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const folderbar = (title, count, extra = "") => `<header class="folderbar"><h1 class="folderbar__title">${title}</h1><span class="folderbar__count">${count}</span><span class="folderbar__rule"></span>${extra}</header>`;
|
|
215
|
-
|
|
216
|
-
export function renderRootIndex(model, ctx) {
|
|
217
|
-
const label = ctx.projectsLabel || "Projects";
|
|
218
|
-
if (!model.opportunities.length) {
|
|
219
|
-
return shell({ title: "Augur", wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
|
|
220
|
-
body: emptyHead(label) + ghostFolderGrid() + emptyState("Ask your agent for a clickable prototype and it shows up here, ready to send.") });
|
|
221
|
-
}
|
|
222
|
-
const cards = model.opportunities.map((opp) => {
|
|
223
|
-
const oppPath = `${enc(opp.name)}/`;
|
|
224
|
-
const cover = opp.prototypes[0];
|
|
225
|
-
const coverSrc = cover ? cover.href : "";
|
|
226
|
-
return `
|
|
227
|
-
<div class="card-opp" data-fitem data-fkey="${titleCase(opp.name)}">
|
|
228
|
-
<a class="card-cover-link" href="${oppPath}" aria-label="Open ${titleCase(opp.name)}"></a>
|
|
229
|
-
${preview(coverSrc, cover && cover.poster)}
|
|
230
|
-
<div class="opp-meta">
|
|
231
|
-
<div class="opp-name-row"><div class="proto-name">${titleCase(opp.name)}</div>${facePile(opp.people)}</div>
|
|
232
|
-
<div class="proto-date">${plural(opp.prototypes.length, "prototype")} · <span class="proto-when" data-currency-folder="${escAttr(opp.name)}" title="${opp.mtimeMs ? fmtDate(opp.mtimeMs) : ""}">${opp.mtimeMs ? relTime(opp.mtimeMs, ctx.now) : ""}</span></div>
|
|
233
|
-
</div>
|
|
234
|
-
</div>`;
|
|
235
|
-
}).join("");
|
|
236
|
-
return shell({ title: "Augur", wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
|
|
237
|
-
body: `${folderbar(label, model.opportunities.length)}<div data-fgroup><div class="opp-grid">${cards}</div></div>${filterEmpty()}` });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function renderOpportunityIndex(model, oppName, ctx) {
|
|
241
|
-
const opp = model.opportunities.find((o) => o.name === oppName);
|
|
242
|
-
if (!opp) return null;
|
|
243
|
-
const label = (ctx.projectsLabel || "Projects").toLowerCase();
|
|
244
|
-
const cards = opp.prototypes.map((p) => {
|
|
245
|
-
const download = p.file ? `<button type="button" data-dl="${p.file}" data-dlname="${enc(p.name)}.html" aria-label="Download HTML" hidden></button>` : "";
|
|
246
|
-
const pinKey = `/${enc(opp.name)}/${enc(p.name)}/`;
|
|
247
|
-
const dname = protoName(p.name);
|
|
248
|
-
const key = `${opp.name}/${p.name}`;
|
|
249
|
-
return `
|
|
250
|
-
<div class="card-proto" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${key}" data-default-name="${dname}" data-del-space="${escAttr(ctx.activeSpace || "")}" data-del-path="${opp.name}/prototypes/${p.name}">
|
|
251
|
-
<div class="preview">
|
|
252
|
-
${media(p.href, p.poster)}
|
|
253
|
-
<a class="preview-link" href="${p.href}" aria-label="Open ${titleCase(p.name)}"></a>
|
|
254
|
-
<div class="preview-actions">
|
|
255
|
-
${download}
|
|
256
|
-
${pinStar(pinKey, pinKey)}
|
|
257
|
-
</div>
|
|
258
|
-
${statusChip(p.status, key)}
|
|
259
|
-
</div>
|
|
260
|
-
<div class="proto-meta">
|
|
261
|
-
<div class="proto-text">
|
|
262
|
-
<div class="proto-name">${dname}</div>
|
|
263
|
-
${currencyLine(key, p, ctx.now)}
|
|
264
|
-
</div>
|
|
265
|
-
${facePile(p.editors)}
|
|
266
|
-
</div>
|
|
267
|
-
</div>`;
|
|
268
|
-
}).join("");
|
|
269
|
-
const up = `<a class="folderbar__up" href="/" aria-label="All ${label}" title="All ${label}"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></a>`;
|
|
270
|
-
return shell({ title: titleCase(opp.name), activeTab: opp.name, wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
|
|
271
|
-
body: `<header class="folderbar">${up}<h1 class="folderbar__title">${titleCase(opp.name)}</h1><span class="folderbar__count">${opp.prototypes.length}</span><span class="folderbar__rule"></span>${newCanvasBtn(`/${opp.name}/`)}</header><div data-fgroup><div class="page-grid is-3up">${cards}</div></div>${filterEmpty()}` });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function renderPlaygroundIndex(model, ctx) {
|
|
275
|
-
const c = { ...ctx, hasPlayground: model.hasPlayground };
|
|
276
|
-
if (!model.playground.length) {
|
|
277
|
-
return shell({ title: "Playground", activeTab: "playground", wrapClass: "wrap--wide", ctx: c,
|
|
278
|
-
body: emptyHead("Playground") + ghostFolderGrid() + emptyState("Ask your agent for something quick that doesn't need a project.") });
|
|
279
|
-
}
|
|
280
|
-
const cards = model.playground.map((p) => {
|
|
281
|
-
const folder = `${enc(p.name)}/`;
|
|
282
|
-
const pinKey = `/playground/${enc(p.name)}/`;
|
|
283
|
-
const dname = protoName(p.name);
|
|
284
|
-
const key = `playground/${p.name}`;
|
|
285
|
-
return `
|
|
286
|
-
<div class="card-opp" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${key}" data-default-name="${dname}" data-del-space="${escAttr(ctx.activeSpace || "")}" data-del-path="playground/${p.name}">
|
|
287
|
-
<a class="card-cover-link" href="${folder}" aria-label="Open ${titleCase(p.name)}"></a>
|
|
288
|
-
<div class="preview">
|
|
289
|
-
${media(p.href, p.poster)}
|
|
290
|
-
${statusChip(p.status, key)}
|
|
291
|
-
</div>
|
|
292
|
-
<div class="preview-actions">${pinStar(pinKey, pinKey)}</div>
|
|
293
|
-
<div class="proto-meta">
|
|
294
|
-
<div class="proto-text">
|
|
295
|
-
<div class="proto-name">${dname}</div>
|
|
296
|
-
${currencyLine(key, p, ctx.now)}
|
|
297
|
-
</div>
|
|
298
|
-
${facePile(p.editors)}
|
|
299
|
-
</div>
|
|
300
|
-
</div>`;
|
|
301
|
-
}).join("");
|
|
302
|
-
return shell({ title: "Playground", activeTab: "playground", wrapClass: "wrap--wide", ctx: c,
|
|
303
|
-
body: `${folderbar("Playground", model.playground.length, newCanvasBtn("/playground/"))}<div data-fgroup><div class="opp-grid">${cards}</div></div>${filterEmpty()}` });
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const TIER_COPY = Object.freeze({
|
|
307
|
-
base: { addHint: 'The atoms every screen here borrows: buttons, inputs, cards, badges, modal, icons. Components and Patterns are built out of these, and all of them wear <a href="/tokens/">Tokens</a>.', empty: "Ask your agent for your buttons, inputs and cards, one page each." },
|
|
308
|
-
patterns: { addHint: "Layouts that keep coming back: several Components arranged the way real screens arrange them again and again.", empty: 'Build a few <a href="/pages/">Pages</a> first, then ask your agent to pull out what repeats.' },
|
|
309
|
-
pages: { addHint: "", empty: "Ask your agent to build a whole screen out of your design system." },
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
export function renderTierIndex(model, tier, ctx) {
|
|
313
|
-
if (!TIER_COPY[tier]) return null;
|
|
314
|
-
const items = model.tiers[tier] || [];
|
|
315
|
-
const title = TIER_TITLE[tier];
|
|
316
|
-
const c = { ...ctx, hasPlayground: model.hasPlayground };
|
|
317
|
-
if (!items.length) {
|
|
318
|
-
return shell({ title, activeTab: tier, wrapClass: "wrap--wide", ctx: c, body: emptyHead(title, true) + ghostCardGrid() + emptyState(TIER_COPY[tier].empty) });
|
|
319
|
-
}
|
|
320
|
-
const card = (p) => `
|
|
321
|
-
<div class="card-proto" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${tier}/${p.name}" data-default-name="${titleCase(p.name)}">
|
|
322
|
-
<div class="preview">
|
|
323
|
-
${media(p.href, p.poster)}
|
|
324
|
-
<a class="preview-link" href="${p.href}" aria-label="Open ${titleCase(p.name)}"></a>
|
|
325
|
-
</div>
|
|
326
|
-
<div class="proto-meta">
|
|
327
|
-
<div class="proto-name">${titleCase(p.name)}</div>
|
|
328
|
-
</div>
|
|
329
|
-
</div>`;
|
|
330
|
-
const hint = TIER_COPY[tier].addHint ? `<p class="tier-hint">${TIER_COPY[tier].addHint}</p>` : "";
|
|
331
|
-
return shell({ title, activeTab: tier, wrapClass: "wrap--wide", ctx: c,
|
|
332
|
-
body: `${folderbar(title, items.length)}${hint}<div class="page-grid">${items.map(card).join("")}</div>${filterEmpty()}` });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** The catalog a components table reads: registry.json → {slug → {name, classes, desc, meta}}. */
|
|
336
|
-
export function catalogFrom(registry) {
|
|
337
|
-
const out = {};
|
|
338
|
-
for (const it of (registry && registry.items) || []) {
|
|
339
|
-
if (!it || !it.name || it.type === "page") continue;
|
|
340
|
-
const fams = it.classes || (it.class ? [it.class] : []);
|
|
341
|
-
out[it.name] = {
|
|
342
|
-
name: it.label || titleCase(it.name),
|
|
343
|
-
classes: (it.cssClasses || fams).map((f) => "." + f).join(" / "),
|
|
344
|
-
desc: it.description || "",
|
|
345
|
-
meta: it.meta || null,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return out;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function renderComponentsIndex(model, catalog, ctx) {
|
|
352
|
-
const items = model.tiers.components || [];
|
|
353
|
-
const c = { ...ctx, hasPlayground: model.hasPlayground };
|
|
354
|
-
if (!items.length) {
|
|
355
|
-
return shell({ title: "Components", activeTab: "components", wrapClass: "wrap--wide", ctx: c,
|
|
356
|
-
body: emptyHead("Components", true) + ghostCompTable() + emptyState("Ask your agent for a component you keep rebuilding, like a search field.") });
|
|
357
|
-
}
|
|
358
|
-
const rows = items.map((it) => {
|
|
359
|
-
const blurb = (catalog && catalog[it.name]) || { name: "", classes: "", desc: "", meta: null };
|
|
360
|
-
const dname = blurb.name || titleCase(it.name);
|
|
361
|
-
const classes = blurb.classes ? `<code>${escAttr(blurb.classes)}</code>` : "";
|
|
362
|
-
const badges = blurb.meta ? metaBadges(blurb.meta) : "";
|
|
363
|
-
const tags = blurb.meta && blurb.meta.tags && blurb.meta.tags.length ? `<div class="comp-tags">${blurb.meta.tags.map((t) => `<span>#${escAttr(t)}</span>`).join("")}</div>` : "";
|
|
364
|
-
const metaKey = blurb.meta ? [blurb.meta.surface, blurb.meta.category, blurb.meta.status, ...(blurb.meta.tags || [])].filter(Boolean).join(" ") : "";
|
|
365
|
-
const fkey = `${dname} ${blurb.classes} ${blurb.desc} ${metaKey}`.replace(/<[^>]+>/g, " ").replace(/"/g, "");
|
|
366
|
-
return `
|
|
367
|
-
<tr data-fitem data-fkey="${escAttr(fkey)}" data-rename-key="components/${it.name}" data-default-name="${escAttr(dname)}">
|
|
368
|
-
<td>
|
|
369
|
-
<a class="comp-thumb" href="${it.href}" aria-label="Open ${escAttr(dname)}">
|
|
370
|
-
${media(it.href, it.poster)}
|
|
371
|
-
</a>
|
|
372
|
-
</td>
|
|
373
|
-
<td><div class="comp-name"><span class="proto-name">${escAttr(dname)}</span>${classes}</div>${badges}${tags}</td>
|
|
374
|
-
<td><div class="comp-desc" data-desc-key="components/${it.name}#desc">${escAttr(blurb.desc)}</div></td>
|
|
375
|
-
<td class="comp-status">${compStatusChip(it.name)}</td>
|
|
376
|
-
</tr>`;
|
|
377
|
-
}).join("");
|
|
378
|
-
return shell({ title: "Components", activeTab: "components", wrapClass: "wrap--wide", ctx: c,
|
|
379
|
-
body: `${folderbar("Components", items.length)}<table class="comp-table">
|
|
380
|
-
<thead><tr><th>Preview</th><th>Component</th><th>What it is</th><th class="comp-status">Status</th></tr></thead>
|
|
381
|
-
<tbody>${rows}</tbody>
|
|
382
|
-
</table>${filterEmpty()}` });
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** The rail's finder index — every navigable thing, the shape the chrome bundle reads. */
|
|
386
|
-
export function searchIndex(model, ctx) {
|
|
387
|
-
const idx = [{ t: ctx.projectsLabel || "Projects", y: "Index", u: "/" }];
|
|
388
|
-
if (model.hasPlayground) idx.push({ t: "Playground", y: "Index", u: "/playground/" });
|
|
389
|
-
for (const t of ["pages", "components", "base", "patterns"]) if (model.tiers[t].length) idx.push({ t: TIER_TITLE[t], y: "Index", u: `/${t}/` });
|
|
390
|
-
const thumb = (p) => (p.poster ? { th: `${p.href}preview.webp` } : {});
|
|
391
|
-
for (const opp of model.opportunities) {
|
|
392
|
-
const cover = opp.prototypes[0];
|
|
393
|
-
idx.push({ t: titleCase(opp.name), y: "Folder", u: `/${enc(opp.name)}/`, ...(cover ? thumb(cover) : {}) });
|
|
394
|
-
for (const p of opp.prototypes) idx.push({ t: titleCase(p.name), y: "Prototype", g: titleCase(opp.name), u: p.href, k: `${opp.name}/${p.name}`, ...thumb(p) });
|
|
395
|
-
}
|
|
396
|
-
for (const p of model.playground) idx.push({ t: titleCase(p.name), y: "Playground", u: p.href, k: `playground/${p.name}`, ...thumb(p) });
|
|
397
|
-
for (const p of model.tiers.pages) idx.push({ t: titleCase(p.name), y: "Page", u: p.href, k: `pages/${p.name}`, ...thumb(p) });
|
|
398
|
-
for (const p of model.tiers.components) idx.push({ t: titleCase(p.name), y: "Component", u: p.href, k: `components/${p.name}`, ...thumb(p) });
|
|
399
|
-
return idx;
|
|
400
|
-
}
|