makersclaw 0.6.0 → 0.7.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 +48 -8
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +31 -4
- package/src/install.mjs +263 -21
- package/src/registry.mjs +78 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +810 -79
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/check/run.mjs +1438 -41
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +170 -51
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +119 -29
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +9 -3
- package/templates/next/files/schema.sql +0 -22
package/src/install.mjs
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
// result and prints nothing: src/cli.mjs says it, as text or as --json.
|
|
3
3
|
//
|
|
4
4
|
// Two files record what an app holds:
|
|
5
|
-
// makersclaw.json the template and version it was laid from,
|
|
6
|
-
// block with the version it came from
|
|
5
|
+
// makersclaw.json the template and version it was laid from, each
|
|
6
|
+
// block with the version it came from, where its
|
|
7
|
+
// tables live (`tables`: "own" or "public") and which
|
|
8
|
+
// fixed files it runs on (`mode`: "server" or
|
|
9
|
+
// "browser"). The rest of its keys are the app's
|
|
10
|
+
// (`brain`: the platform's declaration; `moves`: a
|
|
11
|
+
// store package's move map); every command keeps
|
|
12
|
+
// them. `tables` is never "moved": that is the
|
|
13
|
+
// platform's mode, and no command writes it
|
|
7
14
|
// check/pins.json sha256 of every fixed file, the template's and the
|
|
8
15
|
// blocks' — `npm run check` refuses one that differs
|
|
9
16
|
// and a third says which package versions it installs:
|
|
@@ -21,7 +28,7 @@ import { join } from "node:path";
|
|
|
21
28
|
import { hashFile, readJson, removeFile, sha256, walk, writeFile, writeJson } from "./fsutil.mjs";
|
|
22
29
|
import { LOCKFILE, LockError, deriveLock, parseLock } from "./lockfile.mjs";
|
|
23
30
|
import { NPM_LOCKFILE, PNPM_VERSION, installDeps } from "./pm.mjs";
|
|
24
|
-
import { BLOCK_DIR, loadBlocks, loadItems, loadTemplate, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
|
|
31
|
+
import { BASE_MODE, BLOCK_DIR, loadBlocks, loadItems, loadTemplate, modeFiles, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
|
|
25
32
|
|
|
26
33
|
export const MANIFEST = "makersclaw.json";
|
|
27
34
|
export const PINS = "check/pins.json";
|
|
@@ -68,6 +75,7 @@ function readState(dir) {
|
|
|
68
75
|
|
|
69
76
|
function requireManifest(dir) {
|
|
70
77
|
const state = readState(dir);
|
|
78
|
+
if (state.manifest?.tables === MOVED) throw movedManifest();
|
|
71
79
|
if (!state.manifest) {
|
|
72
80
|
const hasApp = existsSync(join(dir, "package.json"));
|
|
73
81
|
throw new CliError(
|
|
@@ -80,6 +88,142 @@ function requireManifest(dir) {
|
|
|
80
88
|
return state;
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** Where an app's tables live: "own", its own schema (app_<name>), or
|
|
92
|
+
* "public", the workspace schema every app shared before 0.7.0. */
|
|
93
|
+
const TABLES = ["own", "public"];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The platform's third mode (claw's MIGRATE-SPEC M17 and §10): a legacy
|
|
97
|
+
* app whose tables the workspace's move put in its own schema, while its
|
|
98
|
+
* code still names public. The platform holds it (the app's blueprint,
|
|
99
|
+
* claw.app_roles); the app's makersclaw.json keeps "public" (or no key)
|
|
100
|
+
* until its rebuild writes "own". So no command writes "moved": `init
|
|
101
|
+
* --tables moved` is refused, and so is every command over a manifest
|
|
102
|
+
* that says it, rather than write it back.
|
|
103
|
+
*/
|
|
104
|
+
const MOVED = "moved";
|
|
105
|
+
const MOVED_WHY = "moved is the platform's mode, never a manifest's";
|
|
106
|
+
function movedManifest() {
|
|
107
|
+
return new CliError(
|
|
108
|
+
`${MANIFEST} says tables "moved": ${MOVED_WHY}`,
|
|
109
|
+
`set "tables" to "public" (or take the key out, for an app laid before 0.7.0): a moved app's code still names public, and its rebuild sets "own"`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A makersclaw.json a lay wrote. Every lay since 0.2.0 records the
|
|
115
|
+
* `version` it laid, so a manifest without one was written by someone
|
|
116
|
+
* else first: the platform writes its `brain` declaration into a
|
|
117
|
+
* work-first app's new manifest before that app's first `init`
|
|
118
|
+
* (APPS-SPEC 4.2). Such a manifest's keys are kept like any other's, but
|
|
119
|
+
* it is no record of an app from before 0.7.0.
|
|
120
|
+
*/
|
|
121
|
+
function writtenByLay(manifest) {
|
|
122
|
+
return Boolean(manifest) && Object.hasOwn(manifest, "version");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The `tables` init writes. It never flips an app: an app's tables move
|
|
127
|
+
* from public to their own schema only with its workspace (the platform's
|
|
128
|
+
* move), never because a refresh ran `init --force`. So an app's
|
|
129
|
+
* makersclaw.json keeps its value — an absent key in a manifest a lay
|
|
130
|
+
* wrote is an app from before 0.7.0, on public — and a `--tables` that
|
|
131
|
+
* says otherwise is refused. Where no lay wrote a makersclaw.json (there
|
|
132
|
+
* is none, or it holds only the platform's keys) and it names no tables,
|
|
133
|
+
* there is no record to keep: a new app is "own" unless told, and an app
|
|
134
|
+
* laid before makersclaw kept one is "public" unless told (the platform,
|
|
135
|
+
* which holds the truth, always tells).
|
|
136
|
+
*/
|
|
137
|
+
function tablesFor(dir, before, asked) {
|
|
138
|
+
if (asked === MOVED) throw new CliError(`--tables moved: ${MOVED_WHY}`, `leave --tables out: a moved app keeps its makersclaw.json's "public" until its rebuild sets "own"`);
|
|
139
|
+
if (before.manifest?.tables === MOVED) throw movedManifest();
|
|
140
|
+
if (asked !== undefined && !TABLES.includes(asked)) {
|
|
141
|
+
throw new CliError(`--tables takes ${TABLES.join(" or ")}, not "${asked}"`, "own: the app's tables in its own schema (every new app) · public: an app from before 0.7.0");
|
|
142
|
+
}
|
|
143
|
+
const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "tables");
|
|
144
|
+
if (!named && !writtenByLay(before.manifest)) return asked ?? (existsSync(join(dir, "package.json")) ? "public" : "own");
|
|
145
|
+
const had = named ? before.manifest.tables : "public";
|
|
146
|
+
if (!TABLES.includes(had)) {
|
|
147
|
+
throw new CliError(`${MANIFEST} says tables ${JSON.stringify(had)}; an app's tables are ${TABLES.join(" or ")}`, `set "tables" in ${MANIFEST} to the one the platform holds for this app`);
|
|
148
|
+
}
|
|
149
|
+
if (asked !== undefined && asked !== had) {
|
|
150
|
+
throw new CliError(
|
|
151
|
+
(named ? `this app's tables are ${had}, not ${asked}` : `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0), not ${asked}`) +
|
|
152
|
+
(had === "public" ? ": an app's tables move from public to their own schema only with its workspace" : ": an app on its own schema never goes back to public"),
|
|
153
|
+
`leave --tables out, or pass --tables ${had}` + (named ? "" : `; if its tables are already in its own schema, put "tables": "own" back in ${MANIFEST}`),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return had;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Which fixed files an app runs on: "server", a Next server in the
|
|
160
|
+
* workspace (files/ as it is; an absent key, every app before 0.7.0), or
|
|
161
|
+
* "browser", code that runs in the member's browser: files the platform
|
|
162
|
+
* serves, reading and writing through its data door (template.json
|
|
163
|
+
* `modes.browser`). */
|
|
164
|
+
const MODES = [BASE_MODE, "browser"];
|
|
165
|
+
|
|
166
|
+
/** The app's mode, from its makersclaw.json. Anything but the two is
|
|
167
|
+
* refused: which fixed files to lay depends on it. */
|
|
168
|
+
function modeOf(manifest) {
|
|
169
|
+
const mode = manifest && Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
|
|
170
|
+
if (!MODES.includes(mode)) {
|
|
171
|
+
throw new CliError(`${MANIFEST} says mode ${JSON.stringify(mode)}; an app's mode is ${MODES.join(" or ")}`, `makersclaw mode <${MODES.join("|")}> sets the one the platform holds for this app`);
|
|
172
|
+
}
|
|
173
|
+
return mode;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The `mode` init writes. Like `tables`, init never changes an app's: a
|
|
178
|
+
* refresh is claw's `init --force` over the app it has, and a flip there
|
|
179
|
+
* would turn a server app into a browser one whose Server Actions then fail
|
|
180
|
+
* its export. So a manifest that names a mode keeps it, whoever wrote it,
|
|
181
|
+
* and so does one a lay wrote (absent: server); a --mode that differs is
|
|
182
|
+
* refused, and `makersclaw mode` is the one switch. A new manifest takes
|
|
183
|
+
* --mode's value, and with none no key: an absent mode is server, the
|
|
184
|
+
* CLI's own default, because outside Makersclaw no door answers a browser
|
|
185
|
+
* app. { mode, key }: the app's mode, and the value to write (undefined:
|
|
186
|
+
* the key stays as it is).
|
|
187
|
+
*/
|
|
188
|
+
function modeFor(before, asked) {
|
|
189
|
+
if (asked !== undefined && !MODES.includes(asked)) {
|
|
190
|
+
throw new CliError(`--mode takes ${MODES.join(" or ")}, not "${asked}"`, "server: a Next server in the workspace (the default) · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
|
|
191
|
+
}
|
|
192
|
+
const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "mode");
|
|
193
|
+
if (!named && !writtenByLay(before.manifest)) return { mode: asked ?? BASE_MODE, key: asked };
|
|
194
|
+
const had = modeOf(before.manifest);
|
|
195
|
+
if (asked !== undefined && asked !== had) {
|
|
196
|
+
throw new CliError(
|
|
197
|
+
`this app is a ${had} app, not ${asked}: an app changes mode only with makersclaw mode`,
|
|
198
|
+
`leave --mode out, or run makersclaw mode ${asked}: it lays that mode's fixed files and says what the app's own code must change`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return { mode: had, key: undefined };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** A browser app's tables are its own: the door serves an app's own schema
|
|
205
|
+
* and nothing else, so a browser app on public would read nothing. `how`
|
|
206
|
+
* says where the pair came from, for the refusal's words. */
|
|
207
|
+
function requireOwnTables(mode, tables, how) {
|
|
208
|
+
if (mode !== "browser" || tables === "own") return;
|
|
209
|
+
const why = "a browser app reads and writes only through the platform's data door, which serves an app's own schema and nothing else";
|
|
210
|
+
if (how === "flags") throw new CliError(`a browser app's tables are own, not ${tables}: ${why}`, "leave --tables out: --mode browser writes tables own");
|
|
211
|
+
if (how === "switch") {
|
|
212
|
+
throw new CliError(
|
|
213
|
+
`${tables === undefined ? `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0)` : `this app's tables are ${tables}`}, not own: ${why}`,
|
|
214
|
+
"an app's tables move to their own schema only with its workspace (the platform's move); until then it stays a server app",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw new CliError(`${MANIFEST} says mode browser and tables ${tables}: ${why}`, `if its tables are in its own schema, set "tables": "own"; if not, makersclaw mode server lays it as a server app`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The files the template lays in this mode (registry modeFiles). */
|
|
221
|
+
function modeFilesOrFail(tpl, mode) {
|
|
222
|
+
const files = modeFiles(tpl, mode);
|
|
223
|
+
if (!files) throw new CliError(`template ${tpl.name} has no ${mode} mode in makersclaw ${version()}`, `its modes: ${[BASE_MODE, ...Object.keys(tpl.modes ?? {})].join(", ")}`);
|
|
224
|
+
return files;
|
|
225
|
+
}
|
|
226
|
+
|
|
83
227
|
function loadTemplateOrFail(name) {
|
|
84
228
|
const tpl = loadTemplate(name);
|
|
85
229
|
if (!tpl) throw new CliError(`this makersclaw has no template "${name}"`, `templates: ${templateNames().join(", ")}`);
|
|
@@ -149,11 +293,12 @@ function writeLock(dir, cut, written) {
|
|
|
149
293
|
|
|
150
294
|
const lockSummary = (cut) => (cut ? { covered: cut.covered.length, carried: cut.carried, left: cut.left } : null);
|
|
151
295
|
|
|
152
|
-
/** Paths a template may remove when a newer version stops shipping them
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
296
|
+
/** Paths a template may remove when a newer version stops shipping them,
|
|
297
|
+
* or the app's mode stops laying them: its fixed files (the base's, so
|
|
298
|
+
* the ones a mode drops too), the ones it names as retired (middleware.js
|
|
299
|
+
* since 0.6.0: Next 16 fails a build that has it beside proxy.js), and
|
|
300
|
+
* the kit, parts and block folders. Anything else a pins.json names is
|
|
301
|
+
* left alone — it is not ours to delete. */
|
|
157
302
|
function ownedPath(path, tpl) {
|
|
158
303
|
if (/^components\/(?:ui|parts|blocks)\//.test(path) || /^lib\/[^/]+\.js$/.test(path)) return true;
|
|
159
304
|
if ((tpl.retired ?? []).includes(path)) return true;
|
|
@@ -162,12 +307,24 @@ function ownedPath(path, tpl) {
|
|
|
162
307
|
|
|
163
308
|
/**
|
|
164
309
|
* Lay a template — and the blocks the app already has — over `dir`: the
|
|
165
|
-
* one routine behind init, init --force and
|
|
166
|
-
* are replaced; starters are laid only when missing; package.json is
|
|
310
|
+
* one routine behind init, init --force, update and mode. Fixed files and
|
|
311
|
+
* docs are replaced; starters are laid only when missing; package.json is
|
|
167
312
|
* merged; blocks come again from this version; what an older version
|
|
168
|
-
* pinned and this one no longer ships is removed.
|
|
313
|
+
* pinned and this one no longer ships is removed. makersclaw.json is
|
|
314
|
+
* rewritten over what it held: `template`, `version` and `blocks` are
|
|
315
|
+
* this lay's, `tables` is init's (tablesFor; update leaves it as it was),
|
|
316
|
+
* `mode` is a new manifest's init's or `makersclaw mode`'s (modeFor), and
|
|
317
|
+
* every other key — the platform's `brain` declaration, one a later
|
|
318
|
+
* version adds — survives a refresh and an update. Until 0.7.0 it was
|
|
319
|
+
* rewritten as exactly those three, and a declaration was lost.
|
|
320
|
+
*
|
|
321
|
+
* The files are the app's mode's (modeFiles): a browser app gets the
|
|
322
|
+
* browser overlay's copy of a fixed file where there is one, and none of
|
|
323
|
+
* the files browser mode drops. The pins follow: a file the mode does not
|
|
324
|
+
* lay is not pinned, and one that was pinned is removed as a retired one
|
|
325
|
+
* is, so a switch of mode leaves only the new mode's fixed files.
|
|
169
326
|
*/
|
|
170
|
-
function lay(dir, tpl, before) {
|
|
327
|
+
function lay(dir, tpl, before, { tables, mode } = {}) {
|
|
171
328
|
const v = version();
|
|
172
329
|
const written = [];
|
|
173
330
|
const kept = [];
|
|
@@ -177,7 +334,13 @@ function lay(dir, tpl, before) {
|
|
|
177
334
|
const pins = {};
|
|
178
335
|
const fresh = !existsSync(join(dir, "package.json"));
|
|
179
336
|
|
|
180
|
-
|
|
337
|
+
// Decided before a file is written: a refused mode leaves the app as it was.
|
|
338
|
+
const keys = { ...(tables ? { tables } : {}), ...(mode ? { mode } : {}) };
|
|
339
|
+
const appMode = modeOf({ ...before.manifest, ...keys });
|
|
340
|
+
requireOwnTables(appMode, keys.tables ?? before.manifest?.tables ?? "public", "manifest");
|
|
341
|
+
const files = modeFilesOrFail(tpl, appMode);
|
|
342
|
+
|
|
343
|
+
for (const f of files) {
|
|
181
344
|
const to = join(dir, f.path);
|
|
182
345
|
if (f.class === "starter" && existsSync(to)) {
|
|
183
346
|
kept.push(f.path);
|
|
@@ -259,12 +422,24 @@ function lay(dir, tpl, before) {
|
|
|
259
422
|
removed.push(p);
|
|
260
423
|
}
|
|
261
424
|
}
|
|
425
|
+
// A file the mode leaves out that no pin names is not the template's to
|
|
426
|
+
// delete (the app wrote it, or a hand laid it), but it is in the way:
|
|
427
|
+
// under output: "export" Next ignores a proxy.js, and lib/db.js is pg.
|
|
428
|
+
const laid = new Set(files.map((f) => f.path));
|
|
429
|
+
for (const f of tpl.files) {
|
|
430
|
+
if (f.class !== "fixed" || laid.has(f.path) || f.path in before.pins || !existsSync(join(dir, f.path))) continue;
|
|
431
|
+
notes.push(`${f.path}: ${appMode} mode lays none, and this one is not pinned — left alone; delete it unless the app's own code needs it`);
|
|
432
|
+
}
|
|
262
433
|
|
|
263
434
|
writeJson(join(dir, PINS), sortKeys(pins));
|
|
264
|
-
|
|
435
|
+
const manifest = { ...before.manifest, template: tpl.name, version: v, blocks: sortKeys(blocks), ...keys };
|
|
436
|
+
writeJson(join(dir, MANIFEST), manifest);
|
|
265
437
|
return {
|
|
266
438
|
template: tpl.name,
|
|
267
439
|
version: v,
|
|
440
|
+
// An absent key is an app from before 0.7.0: its tables are in public.
|
|
441
|
+
tables: manifest.tables ?? "public",
|
|
442
|
+
mode: appMode,
|
|
268
443
|
from: before.manifest?.version ?? null,
|
|
269
444
|
written,
|
|
270
445
|
kept,
|
|
@@ -278,14 +453,14 @@ function lay(dir, tpl, before) {
|
|
|
278
453
|
};
|
|
279
454
|
}
|
|
280
455
|
|
|
281
|
-
/** `init [dir] [-t next] [--force]` */
|
|
282
|
-
export function init(dir, { template = "next", force = false } = {}) {
|
|
456
|
+
/** `init [dir] [-t next] [--force] [--tables own|public] [--mode browser|server]` */
|
|
457
|
+
export function init(dir, { template = "next", force = false, tables, mode: askedMode } = {}) {
|
|
283
458
|
const tpl = loadTemplateOrFail(template);
|
|
284
459
|
const before = readState(dir);
|
|
285
460
|
if (existsSync(join(dir, "package.json")) && !force) {
|
|
286
461
|
throw new CliError(
|
|
287
462
|
`${dir} already holds an app`,
|
|
288
|
-
before.manifest
|
|
463
|
+
writtenByLay(before.manifest)
|
|
289
464
|
? "makersclaw update lays this version over it"
|
|
290
465
|
: "makersclaw init --force lays the template over it: fixed files and docs are replaced, your own files stay",
|
|
291
466
|
);
|
|
@@ -293,7 +468,13 @@ export function init(dir, { template = "next", force = false } = {}) {
|
|
|
293
468
|
if (before.manifest?.template && before.manifest.template !== template) {
|
|
294
469
|
throw new CliError(`this app was laid from template "${before.manifest.template}", not "${template}"`, `makersclaw init --force -t ${before.manifest.template}`);
|
|
295
470
|
}
|
|
296
|
-
|
|
471
|
+
// Decided before a file is written: a refused --tables or --mode leaves
|
|
472
|
+
// the app as it was. A new browser app's tables are own unless --tables
|
|
473
|
+
// says otherwise, and then it is refused.
|
|
474
|
+
const m = modeFor(before, askedMode);
|
|
475
|
+
const t = tablesFor(dir, before, tables ?? (m.key === "browser" ? "own" : undefined));
|
|
476
|
+
if (tables !== undefined) requireOwnTables(m.mode, t, "flags");
|
|
477
|
+
return lay(dir, tpl, before, { tables: t, mode: m.key });
|
|
297
478
|
}
|
|
298
479
|
|
|
299
480
|
/** `update [--major] [--adopt]` — this version's fixed files, docs and
|
|
@@ -330,6 +511,57 @@ export function update(dir, { major = false, adopt = false } = {}) {
|
|
|
330
511
|
return lay(dir, tpl, before);
|
|
331
512
|
}
|
|
332
513
|
|
|
514
|
+
/** What an app's own code must change when it moves to a mode. The
|
|
515
|
+
* template lays the fixed files; the surfaces are the app's, and move in
|
|
516
|
+
* the same build (claw's STATIC-SPEC §3, "What the Builder is told"). */
|
|
517
|
+
export const MODE_CHANGES = {
|
|
518
|
+
browser: [
|
|
519
|
+
'every surface a client component ("use client") that reads through the lib/data hooks (useRows, useRow, useCount, useCountBy, useActivity) and writes through lib/actions; a hook compares its options on every render (a time in filter or since to the minute), so anything else worked out per render (a Date.now() number) is worked out once, with useMemo',
|
|
520
|
+
"view state (?row=) from useSearchParams(): a browser app's page is a file built once and has no request, so a surface gets no searchParams",
|
|
521
|
+
"a write of more than one row is a function in the app's schema ((args jsonb) returns jsonb), called with runAction and named in references/actions.md",
|
|
522
|
+
'no server code of its own: no "use server" module, no route.js under app/, no proxy.js or middleware.js, no import of next/headers, next/cache, next/server, pg or lib/db',
|
|
523
|
+
"no export const dynamic, revalidate, fetchCache, runtime or dynamicParams in the app's own files",
|
|
524
|
+
"no process.env in the app's own code but CLAW_BASE_PATH, CLAW_PRODUCT and NEXT_PUBLIC_*: a prerendered page prints what it reads into a file anyone who opens the app can fetch",
|
|
525
|
+
],
|
|
526
|
+
server: [
|
|
527
|
+
"lib/data.js is the server's again (pg, next/headers): a client surface takes its hooks from lib/door.js, or becomes a server component that calls loadRows",
|
|
528
|
+
"lib/actions.js is Server Actions again: a client surface calls them as it did",
|
|
529
|
+
"server code the app needs (a route handler, a Server Action of its own) may come back",
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* `mode <browser|server>` — the one switch of an app's mode. It sets
|
|
535
|
+
* `mode` in makersclaw.json, lays that mode's fixed files, removes the
|
|
536
|
+
* other mode's (as update removes a retired file), and says what the
|
|
537
|
+
* app's own code must change. Browser needs `tables: "own"`: the door
|
|
538
|
+
* serves only an app's own schema. The app must be on this version, as
|
|
539
|
+
* for add: the switch lays this version's fixed files.
|
|
540
|
+
*/
|
|
541
|
+
export function mode(dir, to) {
|
|
542
|
+
if (to === undefined) throw new CliError("name a mode: browser or server", "makersclaw mode browser · makersclaw mode server");
|
|
543
|
+
if (!MODES.includes(to)) throw new CliError(`an app's mode is ${MODES.join(" or ")}, not "${to}"`, "server: a Next server in the workspace · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
|
|
544
|
+
const before = requireManifest(dir);
|
|
545
|
+
const { manifest } = before;
|
|
546
|
+
const v = version();
|
|
547
|
+
if (manifest.version !== v) {
|
|
548
|
+
throw new CliError(
|
|
549
|
+
`this app is on makersclaw ${manifest.version}; this is ${v}, and a switch of mode lays this version's fixed files`,
|
|
550
|
+
newer(manifest.version, v)
|
|
551
|
+
? `npx makersclaw@${manifest.version} mode ${to}`
|
|
552
|
+
: compatible(manifest.version, v)
|
|
553
|
+
? `makersclaw update first (${manifest.version} → ${v} is compatible), then mode ${to}`
|
|
554
|
+
: `makersclaw update --major first (makersclaw diff shows what changes), then mode ${to}`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const tpl = loadTemplateOrFail(manifest.template);
|
|
558
|
+
// Not checked: a mode this CLI cannot read is what the switch mends.
|
|
559
|
+
const was = Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
|
|
560
|
+
requireOwnTables(to, manifest.tables, "switch");
|
|
561
|
+
const r = lay(dir, tpl, before, { mode: to });
|
|
562
|
+
return { ...r, was, changed: was !== to, change: MODE_CHANGES[to] };
|
|
563
|
+
}
|
|
564
|
+
|
|
333
565
|
function importFor(item) {
|
|
334
566
|
if (item.type === "block") {
|
|
335
567
|
const names = item.exports?.length ? item.exports.join(", ") : "…";
|
|
@@ -356,6 +588,9 @@ export function add(dir, names) {
|
|
|
356
588
|
}
|
|
357
589
|
const tpl = loadTemplateOrFail(manifest.template);
|
|
358
590
|
const items = loadItems(tpl);
|
|
591
|
+
// A kit component is restored from the app's mode's copy of its file.
|
|
592
|
+
const appMode = modeOf(manifest);
|
|
593
|
+
const laid = new Map(modeFilesOrFail(tpl, appMode).map((f) => [f.path, f.from]));
|
|
359
594
|
const order = [];
|
|
360
595
|
const seen = new Set();
|
|
361
596
|
const visit = (name, by) => {
|
|
@@ -380,7 +615,9 @@ export function add(dir, names) {
|
|
|
380
615
|
const files = [];
|
|
381
616
|
let changed = 0;
|
|
382
617
|
for (const f of item.files) {
|
|
383
|
-
const
|
|
618
|
+
const from = item.type === "block" ? f.from : laid.get(f.to);
|
|
619
|
+
if (!from) throw new CliError(`${item.name}: ${f.to} is not laid in ${appMode} mode`);
|
|
620
|
+
const bytes = readFileSync(from);
|
|
384
621
|
const hash = sha256(bytes);
|
|
385
622
|
if (hashFile(join(dir, f.to)) !== hash) {
|
|
386
623
|
writeFile(join(dir, f.to), bytes);
|
|
@@ -490,6 +727,8 @@ export function diff(dir) {
|
|
|
490
727
|
const { manifest, pins } = requireManifest(dir);
|
|
491
728
|
const tpl = loadTemplateOrFail(manifest.template);
|
|
492
729
|
const v = version();
|
|
730
|
+
const appMode = modeOf(manifest);
|
|
731
|
+
const files = modeFilesOrFail(tpl, appMode);
|
|
493
732
|
const entries = [];
|
|
494
733
|
const statusOf = (path, have, want, pinned) => {
|
|
495
734
|
if (have === want) return null;
|
|
@@ -498,12 +737,14 @@ export function diff(dir) {
|
|
|
498
737
|
return "changed";
|
|
499
738
|
};
|
|
500
739
|
|
|
501
|
-
|
|
740
|
+
// The app's mode's files: update lays those, and removes a pinned file
|
|
741
|
+
// the mode does not lay ("dropped").
|
|
742
|
+
for (const f of files) {
|
|
502
743
|
if (f.class !== "fixed" && f.class !== "docs") continue;
|
|
503
744
|
const status = statusOf(f.path, hashFile(join(dir, f.path)), sha256(readFileSync(f.from)), f.class === "fixed" ? pins[f.path] : null);
|
|
504
745
|
if (status) entries.push({ item: "template", path: f.path, status });
|
|
505
746
|
}
|
|
506
|
-
const shipped = new Set(
|
|
747
|
+
const shipped = new Set(files.map((f) => f.path));
|
|
507
748
|
for (const p of Object.keys(pins)) {
|
|
508
749
|
if (!p.startsWith(`${BLOCK_DIR}/`) && !shipped.has(p) && ownedPath(p, tpl)) entries.push({ item: "template", path: p, status: "dropped" });
|
|
509
750
|
}
|
|
@@ -541,6 +782,7 @@ export function diff(dir) {
|
|
|
541
782
|
from: manifest.version,
|
|
542
783
|
to: v,
|
|
543
784
|
compatible: compatible(manifest.version, v) && !newer(manifest.version, v),
|
|
785
|
+
mode: appMode,
|
|
544
786
|
entries,
|
|
545
787
|
package: pkg,
|
|
546
788
|
blocks: manifest.blocks ?? {},
|
package/src/registry.mjs
CHANGED
|
@@ -56,6 +56,82 @@ export const FILE_CLASSES = ["fixed", "docs", "starter", "merged"];
|
|
|
56
56
|
// Next 16 renamed proxy.js). `update` removes them from an app that has
|
|
57
57
|
// them pinned; nothing lays them.
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The mode files/ is: a Next server in the workspace. Every other mode is
|
|
61
|
+
* files/ with some fixed files replaced and some left out (template.json
|
|
62
|
+
* `modes`), and an app says which in makersclaw.json `mode` (absent: this
|
|
63
|
+
* one). Browser mode (0.7.0, claw's STATIC-SPEC §3): the app's code runs
|
|
64
|
+
* in the member's browser, from files the platform serves, and every read
|
|
65
|
+
* and write goes through the platform's data door. So it lays no
|
|
66
|
+
* proxy.js, no /claw-alive, no pg (lib/db.js) and no server reads of the
|
|
67
|
+
* brain (lib/brain-server.js).
|
|
68
|
+
*/
|
|
69
|
+
export const BASE_MODE = "server";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A mode's overlay and drop list, checked against the base's files. Pure:
|
|
73
|
+
* names in, the first thing wrong thrown, nothing returned. `base` is the
|
|
74
|
+
* base's files ({ path, class }), `overlay` the paths under the mode's
|
|
75
|
+
* overlay folder, `drop` the base paths the mode leaves out.
|
|
76
|
+
* - No file is both dropped and overlaid: a mode lays a file or leaves
|
|
77
|
+
* it out, never both.
|
|
78
|
+
* - Each overlay file is a fixed file of the base. The overlay replaces
|
|
79
|
+
* what the check pins (and update lays again); it adds no file, and
|
|
80
|
+
* never a starter or a doc, which are the app's or read only.
|
|
81
|
+
* - Each dropped file is a fixed file of the base: a drop the base does
|
|
82
|
+
* not ship drops nothing, without a word, and only a pinned file is
|
|
83
|
+
* the template's to remove from an app.
|
|
84
|
+
*/
|
|
85
|
+
export function checkMode(template, mode, { base, overlay, drop }) {
|
|
86
|
+
const fixed = new Set(base.filter((f) => f.class === "fixed").map((f) => f.path));
|
|
87
|
+
const dropped = new Set(drop);
|
|
88
|
+
for (const path of overlay) {
|
|
89
|
+
if (dropped.has(path)) throw new Error(`template ${template}: mode ${mode} both drops and overlays ${path} — a mode lays a file or leaves it out, not both`);
|
|
90
|
+
if (!fixed.has(path)) {
|
|
91
|
+
throw new Error(`template ${template}: mode ${mode} overlays ${path}, which is not a fixed file of files/ — an overlay replaces a fixed file and adds none`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const path of drop) {
|
|
95
|
+
if (!fixed.has(path)) throw new Error(`template ${template}: mode ${mode} drops ${path}, which is not a fixed file of files/ — it would drop nothing`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A mode's overlay folder: beside files/, a plain name. */
|
|
100
|
+
const OVERLAY = /^[a-z]+$/;
|
|
101
|
+
|
|
102
|
+
function loadModes(name, dir, meta, base) {
|
|
103
|
+
const modes = {};
|
|
104
|
+
for (const [mode, spec] of Object.entries(meta.modes ?? {})) {
|
|
105
|
+
if (!NAME.test(mode) || mode === BASE_MODE) throw new Error(`template ${name}: mode "${mode}" — ${BASE_MODE} is files/ itself, and a mode is named lowercase and dashed`);
|
|
106
|
+
const folder = spec?.overlay;
|
|
107
|
+
if (typeof folder !== "string" || !OVERLAY.test(folder) || folder === "files") {
|
|
108
|
+
throw new Error(`template ${name}: mode ${mode} names its overlay ${JSON.stringify(folder)} — a plain folder name beside files/`);
|
|
109
|
+
}
|
|
110
|
+
const root = join(dir, folder);
|
|
111
|
+
// A folder that is not there is an empty overlay: the mode only leaves
|
|
112
|
+
// files out. Browser mode's own files come after its drop list does.
|
|
113
|
+
const overlay = existsSync(root) ? walk(root) : [];
|
|
114
|
+
const drop = [...(spec.drop ?? [])];
|
|
115
|
+
checkMode(name, mode, { base, overlay, drop });
|
|
116
|
+
modes[mode] = { root, drop, files: overlay.map((path) => ({ path, class: "fixed", from: join(root, path) })) };
|
|
117
|
+
}
|
|
118
|
+
return modes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The files a template lays in a mode: the base's, less the mode's drops,
|
|
123
|
+
* with the mode's copy in place of the base's where its overlay has one.
|
|
124
|
+
* The base mode is files/ as it is. null for a mode the template has not.
|
|
125
|
+
*/
|
|
126
|
+
export function modeFiles(template, mode = BASE_MODE) {
|
|
127
|
+
if (mode === BASE_MODE) return template.files;
|
|
128
|
+
const m = template.modes?.[mode];
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
const over = new Map(m.files.map((f) => [f.path, f]));
|
|
131
|
+
const drop = new Set(m.drop);
|
|
132
|
+
return template.files.filter((f) => !drop.has(f.path)).map((f) => over.get(f.path) ?? f);
|
|
133
|
+
}
|
|
134
|
+
|
|
59
135
|
export function loadTemplate(name) {
|
|
60
136
|
const dir = join(TEMPLATES, name);
|
|
61
137
|
const meta = readJson(join(dir, "template.json"));
|
|
@@ -94,7 +170,8 @@ export function loadTemplate(name) {
|
|
|
94
170
|
// The release lock (0.6.0 on): beside files/, never laid as it is. Each
|
|
95
171
|
// app's pnpm-lock.yaml is cut from it (src/lockfile.mjs).
|
|
96
172
|
const lock = existsSync(join(dir, RELEASE_LOCK)) ? join(dir, RELEASE_LOCK) : null;
|
|
97
|
-
|
|
173
|
+
const modes = loadModes(name, dir, meta, files);
|
|
174
|
+
return { name, title: meta.title ?? name, description: meta.description ?? "", root, files, components, retired, lock, modes };
|
|
98
175
|
}
|
|
99
176
|
|
|
100
177
|
/** The release lock's file, in templates/<name>/: made by `npm run lock`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { claw } from "../lib/claw";
|
|
5
|
+
import { dataChanged, whenLanded } from "../lib/door";
|
|
6
|
+
import { changeFeed, refreshLoop } from "./frame";
|
|
7
|
+
|
|
8
|
+
// A browser app's auto-refresh (makersclaw.json "mode": "browser"). Rows
|
|
9
|
+
// change under this page from outside it: the agent writes them while
|
|
10
|
+
// the founder watches, and so do jobs and other members. The page is
|
|
11
|
+
// files, so there is nothing to re-render from a server: the hooks that
|
|
12
|
+
// read the rows (lib/data's useRows and the rest, lib/brain.js's) read
|
|
13
|
+
// again, and only when the data door says something changed.
|
|
14
|
+
//
|
|
15
|
+
// The rules are app/frame.js's (refreshLoop, and the reasons with each),
|
|
16
|
+
// the server app's every one:
|
|
17
|
+
//
|
|
18
|
+
// - only while the tab is visible AND the App tab is on screen: no timer
|
|
19
|
+
// at all otherwise, and coming back asks at once;
|
|
20
|
+
// - only while someone is there: after 30 minutes with no key, pointer,
|
|
21
|
+
// touch, wheel or focus in the app it stops, and the next sign asks
|
|
22
|
+
// at once;
|
|
23
|
+
// - only while the founder is not typing and no dialog is open
|
|
24
|
+
// (isEditing), and never while a surface has opted out with
|
|
25
|
+
// data-claw-refresh="off" on its root;
|
|
26
|
+
// - one tick at a time, the next timed once the last has landed: the
|
|
27
|
+
// interval plus what the last one took.
|
|
28
|
+
//
|
|
29
|
+
// What a tick does is this mode's own (changeFeed):
|
|
30
|
+
//
|
|
31
|
+
// - it asks GET _claw/data/changes?since=<cursor>. The first answer
|
|
32
|
+
// only sets the cursor and the stamp of the app's own tables; after
|
|
33
|
+
// it, a brain change or a different stamp is a change;
|
|
34
|
+
// - a change is dataChanged(), every hook on the page reading again,
|
|
35
|
+
// and the tick lands when whenLanded() says they all have their new
|
|
36
|
+
// rows. A read that fails keeps the rows it had;
|
|
37
|
+
// - never router.refresh(): under an export it fetches the same
|
|
38
|
+
// prebuilt payload. Never /claw-alive: nothing in the machine answers
|
|
39
|
+
// for a browser app, and the door is the platform's.
|
|
40
|
+
//
|
|
41
|
+
// The feed's first answer is asked as the page mounts, not at the first
|
|
42
|
+
// tick. That answer only sets where the feed is, so a change between it
|
|
43
|
+
// and the tick would never count: asked five seconds late, it would hide
|
|
44
|
+
// every change made while the page opened. This sits ahead of the Shell
|
|
45
|
+
// in the layout and React runs effects in that order, so the ask goes
|
|
46
|
+
// out before the surfaces' first reads: a change made in between is read
|
|
47
|
+
// twice, not missed.
|
|
48
|
+
export default function AutoRefresh() {
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const feed = changeFeed((path, signal) => claw(path, { signal }));
|
|
51
|
+
feed.probe();
|
|
52
|
+
const loop = refreshLoop({
|
|
53
|
+
doc: document,
|
|
54
|
+
probe: feed.probe,
|
|
55
|
+
refresh: () => {
|
|
56
|
+
feed.covered();
|
|
57
|
+
dataChanged();
|
|
58
|
+
return whenLanded();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
loop.stop();
|
|
63
|
+
feed.stop();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import { Examples } from "../components/parts/fixture";
|
|
3
|
+
import { surfaceNav } from "../surfaces";
|
|
4
|
+
import AutoRefresh from "./auto-refresh";
|
|
5
|
+
import ErrorRelay from "./error-relay";
|
|
6
|
+
import EscapeRelay from "./escape-relay";
|
|
7
|
+
import Shell from "./shell";
|
|
8
|
+
|
|
9
|
+
// FIXED — the platform agent does not edit this file. Design lives in
|
|
10
|
+
// app/shell.jsx and the surfaces; colour lives nowhere in the app.
|
|
11
|
+
//
|
|
12
|
+
// A browser app's layout (makersclaw.json "mode": "browser"). The app is
|
|
13
|
+
// files: `next build` prerenders every page once, with `output: "export"`
|
|
14
|
+
// (next.config.mjs), and the platform serves what it wrote. So nothing
|
|
15
|
+
// here reads the request. There is no request: no headers(), no
|
|
16
|
+
// cookies(), and no `dynamic` export, which an export refuses anyway.
|
|
17
|
+
// Everything the server layout decides per request is decided here at
|
|
18
|
+
// build, or in the browser.
|
|
19
|
+
export const metadata = { title: process.env.CLAW_PRODUCT || "Artifact" };
|
|
20
|
+
|
|
21
|
+
// The theme, decided in the browser before the body paints. It follows
|
|
22
|
+
// the desk: the OS window opens the app with ?theme=light|dark, and the
|
|
23
|
+
// product proxy keeps that query through its redirect and sets the
|
|
24
|
+
// claw_theme cookie (Path=/, readable here) on the same answer, so every
|
|
25
|
+
// later page reads the cookie. Dark when neither says: the desk is dark,
|
|
26
|
+
// and so is the data-theme the build writes.
|
|
27
|
+
//
|
|
28
|
+
// Why there is no flash: an inline script in <head> runs while the HTML
|
|
29
|
+
// is parsed, before anything paints, and _claw/theme.css after it still
|
|
30
|
+
// blocks the first paint. suppressHydrationWarning on <html> covers the
|
|
31
|
+
// one attribute React then finds changed. Anything that throws (a
|
|
32
|
+
// browser with cookies off) leaves the build's dark.
|
|
33
|
+
//
|
|
34
|
+
// A plain string, not a function turned into one: what runs is exactly
|
|
35
|
+
// what test/browser.test.mjs runs against a fake document.
|
|
36
|
+
const THEME = `(function () {
|
|
37
|
+
try {
|
|
38
|
+
var d = document.documentElement;
|
|
39
|
+
var q = new URLSearchParams(location.search).get("theme");
|
|
40
|
+
var m = document.cookie.match(/(?:^|; )claw_theme=(light|dark)(?:;|$)/);
|
|
41
|
+
d.dataset.theme = q === "light" || q === "dark" ? q : m ? m[1] : "dark";
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
})();`;
|
|
44
|
+
|
|
45
|
+
// <html> carries what the browser-side code reads: the base path for
|
|
46
|
+
// lib/claw.js, the product name, the mode, the examples decision and
|
|
47
|
+
// data-theme.
|
|
48
|
+
//
|
|
49
|
+
// The palette follows the WORKSPACE: <head> links `_claw/theme.css`, the
|
|
50
|
+
// preset picked in System Settings → Appearance, served by the platform
|
|
51
|
+
// with the page's own cookie as identity. app/globals.css holds the same
|
|
52
|
+
// preset as defaults for a run with nothing serving it (a local `next
|
|
53
|
+
// dev`, where the link 404s harmlessly). The href is prefixed by hand:
|
|
54
|
+
// Next's basePath applies to next/link and assets, not to a literal
|
|
55
|
+
// <link>.
|
|
56
|
+
//
|
|
57
|
+
// Examples (components/parts/fixture.jsx) are off inside a workspace, by
|
|
58
|
+
// the server layout's own expression, read here at build: the platform
|
|
59
|
+
// builds every browser app with its CLAW_ARTIFACT_ID, and its publish
|
|
60
|
+
// refuses an export that says data-claw-examples="1". The store's own
|
|
61
|
+
// loop builds without one, and shows its examples. The title and
|
|
62
|
+
// data-claw-product are the name at build: a rename reaches them at the
|
|
63
|
+
// next ship.
|
|
64
|
+
export default function RootLayout({ children }) {
|
|
65
|
+
const basePath = process.env.CLAW_BASE_PATH || "";
|
|
66
|
+
const name = process.env.CLAW_PRODUCT || "Artifact";
|
|
67
|
+
const examples = process.env.CLAW_EXAMPLES === "1" || !process.env.CLAW_ARTIFACT_ID;
|
|
68
|
+
return (
|
|
69
|
+
<html
|
|
70
|
+
lang="en"
|
|
71
|
+
data-claw-base-path={basePath}
|
|
72
|
+
data-claw-product={name}
|
|
73
|
+
data-claw-mode="browser"
|
|
74
|
+
data-claw-examples={examples ? "1" : "0"}
|
|
75
|
+
data-theme="dark"
|
|
76
|
+
suppressHydrationWarning
|
|
77
|
+
>
|
|
78
|
+
<head>
|
|
79
|
+
<script dangerouslySetInnerHTML={{ __html: THEME }} />
|
|
80
|
+
<link rel="stylesheet" href={`${basePath}/_claw/theme.css`} />
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<AutoRefresh />
|
|
84
|
+
<ErrorRelay />
|
|
85
|
+
<EscapeRelay />
|
|
86
|
+
<Examples allowed={examples}>
|
|
87
|
+
<Shell name={name} surfaces={surfaceNav()}>
|
|
88
|
+
{children}
|
|
89
|
+
</Shell>
|
|
90
|
+
</Examples>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
|
93
|
+
);
|
|
94
|
+
}
|