@plitzi/cli 0.38.8 → 0.38.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { createHash, randomBytes } from "node:crypto";
11
11
  import { createServer } from "node:http";
12
12
  import tls from "node:tls";
13
13
  import readline from "node:readline/promises";
14
- import { EXPLAIN_KINDS, PAGE_SPEC_FIELDS, PAGE_TEXT_LIMIT, actionSpecFromEntry, actionToSource, authorSpace, blankTemplateFiles, catalogTemplateFiles, closest, comparePictures, compareTexts, darkScheme, dataIssues, elementTriggers, elementTypeNames, emptySpaceSource, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, isSuggestionCode, loadImages, locateClasses, locateElements, openPage, pageRegions, pageTexts, planFixes, readDevTools, refusalOf, unrollPage } from "@plitzi/sdk-authoring";
14
+ import { EXPLAIN_KINDS, PAGE_SPEC_FIELDS, PAGE_TEXT_LIMIT, actionSpecFromEntry, actionToSource, authorSpace, blankTemplateFiles, catalogTemplateFiles, closest, comparePictures, compareTexts, darkScheme, dataIssues, elementTriggers, elementTypeNames, emptyTemplateFiles, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, isSuggestionCode, loadImages, locateClasses, locateElements, openPage, pageRegions, pageTexts, planFixes, readDevTools, refusalOf, unrollPage } from "@plitzi/sdk-authoring";
15
15
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
16
16
  import { parseEnv, promisify } from "node:util";
17
17
  import { lookup } from "node:dns/promises";
@@ -1354,7 +1354,7 @@ var n$3 = [
1354
1354
  ".otf"
1355
1355
  ];
1356
1356
  var r$5 = () => Object.fromEntries(n$3.map((e) => [e, "dataurl"]));
1357
- var i$5 = /\?(raw|inline)$/;
1357
+ var i$4 = /\?(raw|inline)$/;
1358
1358
  var a$5 = {
1359
1359
  ".wasm": "application/wasm",
1360
1360
  ".js": "text/javascript",
@@ -1371,8 +1371,8 @@ var a$5 = {
1371
1371
  var o$4 = () => ({
1372
1372
  name: "plitzi-import-queries",
1373
1373
  setup: (n) => {
1374
- n.onResolve({ filter: i$5 }, async (e) => {
1375
- let t = i$5.exec(e.path)?.[1] ?? "raw", r = await n.resolve(e.path.replace(i$5, ""), {
1374
+ n.onResolve({ filter: i$4 }, async (e) => {
1375
+ let t = i$4.exec(e.path)?.[1] ?? "raw", r = await n.resolve(e.path.replace(i$4, ""), {
1376
1376
  kind: e.kind,
1377
1377
  importer: e.importer,
1378
1378
  resolveDir: e.resolveDir
@@ -2418,7 +2418,7 @@ var projectScripts = (answers) => {
2418
2418
  * runs what `build` emitted and carries no TypeScript at all.
2419
2419
  */
2420
2420
  build: "tsc -p tsconfig.build.json",
2421
- "start:prod": nodeWithEnv(`${v$3}/main.js`)
2421
+ "start:prod": `NODE_ENV=production ${nodeWithEnv(`${v$3}/main.js`)}`
2422
2422
  } : {
2423
2423
  start: "vite",
2424
2424
  build: "vite build",
@@ -2512,7 +2512,7 @@ var YARN_IGNORES$1 = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases
2512
2512
  * the CLI records about it, and is committed — a clone without it could not pull, push or upgrade.
2513
2513
  */
2514
2514
  var gitignore$1 = ({ mode, packageManager }) => `node_modules\n${v$3}\n.env\ntmp\n${mode === "server" ? `${y$2}\n` : ""}${packageManager === "yarn" ? YARN_IGNORES$1 : ""}`;
2515
- var startLine = ({ mode, packageManager, source }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`${x$3}\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on a save to the server's code; a save to a plugin swaps it in the open page${source === "local" ? ", and a save to the space reloads it" : ""}. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
2515
+ var startLine = ({ mode, packageManager, source }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`${x$3}\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on a save to the server's code; a save to a plugin swaps it in the open page${source === "local" ? ", and a save to the space reloads it" : ""}. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\`, which sets \`NODE_ENV=production\` itself: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
2516
2516
  var spaceSection = (answers) => {
2517
2517
  if (answers.source === "cloud") return `## Where the space comes from
2518
2518
 
@@ -2634,7 +2634,7 @@ ${commands.join("\n")}
2634
2634
  - **Settings are ${code(".env")}**, never committed; ${code(".env.example")} names them, committed — a new one goes in both. ${settingsNote}
2635
2635
  ${dataNote}
2636
2636
  - **${code("public/")} is on the internet.** Every file in it is served to anyone who asks for it, as it is, the moment the project is deployed — no sign-in, no check. Never put in it a secret, a key, a ${code(".env")}, a private document, a database dump, or data only some visitors may read: that goes through a server action or a provider that checks who is asking.
2637
- ${serverNotes}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — and with ${code("--click <id>")}, what clicking one element changed, or that nothing did (${code("--fill <id>=<value>")} first, for a form) — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves; ${code("--compare <url>")} against another site: by section, and each text measured). ${run("visual")} runs the checks as tests.
2637
+ ${serverNotes}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — and with ${code("--click <id>")}, what clicking one element changed, or that nothing did (${code("--fill <id>=<value>")} first, for a form) — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves, ${code("--click <id|selector> --sheet")} for an interaction; ${code("--compare <url>")} against another site: by section, and each text measured). ${run("visual")} runs the checks as tests.
2638
2638
  - **What the page holds, in text:** ${code(`${runCommand(answers.packageManager, "check")} -- /products --state --element <id>`)} adds its state, every source by name and one element (what it reads, its own state, whether it is on screen); every check already lists the flows that failed. Read it instead of guessing from classes in the DOM.
2639
2639
 
2640
2640
  ## Do not read
@@ -2656,11 +2656,11 @@ ${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi plugin add")
2656
2656
 
2657
2657
  What you leave behind is the next reader's problem — the user's, or the next agent's. Before calling a change done:
2658
2658
 
2659
- - **Nothing unused.** Delete what you made and no longer use — a file, a page, a component, a class, a token, a data file, an import, a plugin folder. No commented-out code, no ${code("console.log")} left from debugging, no copy of a file kept "just in case": git keeps the history.
2659
+ - **Nothing unused.** Delete what you made and no longer use — a file, a page, a component, a class, a token, a data file, an import, a plugin folder. A folder's ${code(".gitkeep")} keeps it in git while it is empty; once the folder holds files it does nothing, and ${code("doctor")} and ${code("upgrade")} ask for it no more. No commented-out code, no ${code("console.log")} left from debugging, no copy of a file kept "just in case": git keeps the history.
2660
2660
  - **Scratch goes in ${code(`tmp/`)}, or nowhere.** A one-off script, a dump, a picture to look at — never at the root or beside the source, where it reads as part of the project.
2661
2661
  - **One of everything.** A look used twice is a class; a value used twice is a token; a block used twice is a component, and rows of data are one list. Change it where it is defined, and rename everywhere when you rename.
2662
2662
  - **Files a reader can find.** One part per file, named after what it is, in the folder of its kind — the shape ${code("src/space/")} already has. Do not start a parallel layout of your own.
2663
- - **Leave it passing.** ${run("verify")} runs it all and prints only what fails: ${local ? `${run("author")} with zero warnings, ${run("lint:space")} clean, ` : ""}${run("typecheck")}, ${run("lint")} and the format clean, and every page whole (${run("check")}, with the server up).
2663
+ - **Leave it passing.** ${run("verify")} runs it all and prints only what fails: ${local ? `${run("author")} with zero warnings, ${run("lint:space")} clean, ` : ""}${run("typecheck")}, ${run("lint")} and the format clean, and every page whole (${run("check")}, with the server up) — those for signed-in visitors too, once ${code(".env")} names an account to check them as: ${code("PLITZI_CHECK_USER")} and ${code("PLITZI_CHECK_PASSWORD")}.
2664
2664
 
2665
2665
  ## The rules that go wrong most
2666
2666
 
@@ -2908,6 +2908,8 @@ var serverOptionsModule = (local) => `import type { ProjectServerOptions } from
2908
2908
  *
2909
2909
  * - \`images: { domains: ['images.example.com'] }\` — pictures from those hosts resized here, with \`sharp\` installed.
2910
2910
  ${local ? ACTION_OPTIONS_DOC : ""} * - \`rsc: { elementTimeoutMs }\` — how long a section resolved on the server is waited for: 5 s by default.
2911
+ * - \`devMode\` — the development build, the dev tools, a public action's trace: on unless \`NODE_ENV\` is \`production\`.
2912
+ * \`devMode: false\` keeps them off wherever the server is started, \`NODE_ENV\` or not. \`devReload\` follows it.
2911
2913
  */
2912
2914
  export const serverOptions: ProjectServerOptions = {};
2913
2915
  `;
@@ -3226,7 +3228,10 @@ var spaceFiles = (answers) => {
3226
3228
  [r$6]: authorScript()
3227
3229
  };
3228
3230
  if (answers.template === "blank") return {
3229
- [a$6]: emptySpaceSource({ name: answers.name }),
3231
+ ...emptyTemplateFiles({
3232
+ name: answers.name,
3233
+ dir: i$6
3234
+ }),
3230
3235
  [r$6]: authorScript(),
3231
3236
  ...answers.mode === "client" ? { "public/data/.gitkeep": "" } : {}
3232
3237
  };
@@ -4422,27 +4427,27 @@ var __exportAll = (all, no_symbols) => {
4422
4427
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
4423
4428
  return target;
4424
4429
  };
4425
- var e$42 = "[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*";
4426
- var t$63 = `(?:${e$42}|\\d+)`;
4427
- var n$47 = `\\[\\s*(?:\\d+|'[^']*'|"[^"]*"|${e$42}(?:\\.${t$63})*)\\s*\\]`;
4428
- var r$37 = `${`${e$42}(?:${n$47})*(?:\\??\\.${t$63}(?:${n$47})*)*`}(?:\\s*\\?\\?\\s*[^|{}]+?)?(?:\\s*\\|\\s*[a-zA-Z_]+(?:\\([^)]*\\))?)*`;
4429
- var i$28 = `\\{\\{\\s*${r$37}\\s*\\}\\}`;
4430
- var a$24 = `(?:${`\\{\\{\\{\\s*${r$37}\\s*\\}\\}\\}`}|${i$28})`;
4430
+ var e$43 = "[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*";
4431
+ var t$63 = `(?:${e$43}|\\d+)`;
4432
+ var n$47 = `\\[\\s*(?:\\d+|'[^']*'|"[^"]*"|${e$43}(?:\\.${t$63})*)\\s*\\]`;
4433
+ var r$36 = `${`${e$43}(?:${n$47})*(?:\\??\\.${t$63}(?:${n$47})*)*`}(?:\\s*\\?\\?\\s*[^|{}]+?)?(?:\\s*\\|\\s*[a-zA-Z_]+(?:\\([^)]*\\))?)*`;
4434
+ var i$29 = `\\{\\{\\s*${r$36}\\s*\\}\\}`;
4435
+ var a$24 = `(?:${`\\{\\{\\{\\s*${r$36}\\s*\\}\\}\\}`}|${i$29})`;
4431
4436
  new RegExp(a$24, "g");
4432
4437
  RegExp(`^${a$24}$`);
4433
- var e$41 = ["smooth", "auto"];
4438
+ var e$42 = ["smooth", "auto"];
4434
4439
  var t$61 = [
4435
4440
  "start",
4436
4441
  "center",
4437
4442
  "end",
4438
4443
  "nearest"
4439
4444
  ];
4440
- var s$21 = "onScroll";
4441
- var r$35 = {
4445
+ var s$22 = "onScroll";
4446
+ var r$34 = {
4442
4447
  type: "select",
4443
4448
  description: "How it moves: \"smooth\" glides there, \"auto\" jumps.",
4444
4449
  default: "smooth",
4445
- options: [...e$41]
4450
+ options: [...e$42]
4446
4451
  };
4447
4452
  var a$22 = {
4448
4453
  setState: {
@@ -4516,7 +4521,7 @@ var a$22 = {
4516
4521
  description: "How far down, the same way; negative goes up.",
4517
4522
  default: ""
4518
4523
  },
4519
- behavior: r$35
4524
+ behavior: r$34
4520
4525
  }
4521
4526
  },
4522
4527
  scrollTo: {
@@ -4534,7 +4539,7 @@ var a$22 = {
4534
4539
  description: "Where down, the same way. Empty stays.",
4535
4540
  default: ""
4536
4541
  },
4537
- behavior: r$35
4542
+ behavior: r$34
4538
4543
  }
4539
4544
  },
4540
4545
  scrollIntoView: {
@@ -4554,11 +4559,11 @@ var a$22 = {
4554
4559
  default: "nearest",
4555
4560
  options: [...t$61]
4556
4561
  },
4557
- behavior: r$35
4562
+ behavior: r$34
4558
4563
  }
4559
4564
  }
4560
4565
  };
4561
- var e$40 = {
4566
+ var e$41 = {
4562
4567
  onLoad: {
4563
4568
  action: "onLoad",
4564
4569
  title: "On Load",
@@ -4728,7 +4733,7 @@ var e$40 = {
4728
4733
  params: {}
4729
4734
  }
4730
4735
  };
4731
- var e$39 = () => (e) => e;
4736
+ var e$40 = () => (e) => e;
4732
4737
  var t$60 = () => (e) => e;
4733
4738
  var t$59 = t$60()({
4734
4739
  type: "blockHtml",
@@ -4958,16 +4963,16 @@ var n$44 = {
4958
4963
  "border-bottom-left-radius": "4px",
4959
4964
  "border-bottom-right-radius": "4px"
4960
4965
  };
4961
- var r$34 = t$60()({
4966
+ var r$33 = t$60()({
4962
4967
  type: "button",
4963
4968
  attributeValues: {
4964
- subType: e$39()([
4969
+ subType: e$40()([
4965
4970
  "button",
4966
4971
  "submit",
4967
4972
  "reset"
4968
4973
  ]),
4969
- contentPlacement: e$39()(["before", "after"]),
4970
- iconPlacement: e$39()(["before", "after"])
4974
+ contentPlacement: e$40()(["before", "after"]),
4975
+ iconPlacement: e$40()(["before", "after"])
4971
4976
  },
4972
4977
  content: {
4973
4978
  attributes: {
@@ -5035,7 +5040,7 @@ var r$34 = t$60()({
5035
5040
  });
5036
5041
  var n$43 = t$60()({
5037
5042
  type: "dropdown",
5038
- attributeValues: { popupPlacement: e$39()([
5043
+ attributeValues: { popupPlacement: e$40()([
5039
5044
  "left",
5040
5045
  "right",
5041
5046
  "top",
@@ -5166,7 +5171,7 @@ var t$54 = t$60()({
5166
5171
  });
5167
5172
  var n$42 = t$60()({
5168
5173
  type: "heading",
5169
- attributeValues: { subType: e$39()([
5174
+ attributeValues: { subType: e$40()([
5170
5175
  "h1",
5171
5176
  "h2",
5172
5177
  "h3",
@@ -5288,20 +5293,20 @@ var n$42 = t$60()({
5288
5293
  var n$41 = t$60()({
5289
5294
  type: "link",
5290
5295
  attributeValues: {
5291
- mode: e$39()([
5296
+ mode: e$40()([
5292
5297
  "page",
5293
5298
  "internal",
5294
5299
  "external"
5295
5300
  ]),
5296
- target: e$39()([
5301
+ target: e$40()([
5297
5302
  "self",
5298
5303
  "blank",
5299
5304
  "parent",
5300
5305
  "top"
5301
5306
  ]),
5302
- contentPlacement: e$39()(["before", "after"]),
5303
- iconPlacement: e$39()(["before", "after"]),
5304
- current: e$39()(["page", "section"])
5307
+ contentPlacement: e$40()(["before", "after"]),
5308
+ iconPlacement: e$40()(["before", "after"]),
5309
+ current: e$40()(["page", "section"])
5305
5310
  },
5306
5311
  content: {
5307
5312
  attributes: {
@@ -5473,7 +5478,7 @@ var t$52 = t$60()({
5473
5478
  });
5474
5479
  var n$40 = t$60()({
5475
5480
  type: "richText",
5476
- attributeValues: { format: e$39()([
5481
+ attributeValues: { format: e$40()([
5477
5482
  "html",
5478
5483
  "markdown",
5479
5484
  "text"
@@ -5608,9 +5613,9 @@ var n$39 = {
5608
5613
  "letter-spacing": "inherit",
5609
5614
  cursor: "pointer"
5610
5615
  };
5611
- var r$33 = t$60()({
5616
+ var r$32 = t$60()({
5612
5617
  type: "themeToggle",
5613
- attributeValues: { subType: e$39()(["switch", "segmented"]) },
5618
+ attributeValues: { subType: e$40()(["switch", "segmented"]) },
5614
5619
  triggers: { onThemeChange: {
5615
5620
  action: "onThemeChange",
5616
5621
  title: "On Theme Change",
@@ -5676,7 +5681,7 @@ var r$33 = t$60()({
5676
5681
  });
5677
5682
  var n$38 = t$60()({
5678
5683
  type: "form",
5679
- attributeValues: { method: e$39()(["get", "post"]) },
5684
+ attributeValues: { method: e$40()(["get", "post"]) },
5680
5685
  sourceType: "apiContainer",
5681
5686
  triggers: { onSubmit: {
5682
5687
  action: "onSubmit",
@@ -5806,11 +5811,11 @@ var n$37 = {
5806
5811
  outline: "none",
5807
5812
  position: "relative"
5808
5813
  };
5809
- var r$32 = {
5814
+ var r$31 = {
5810
5815
  ...n$37,
5811
5816
  "line-height": "20px"
5812
5817
  };
5813
- var i$25 = {
5818
+ var i$26 = {
5814
5819
  display: "flex",
5815
5820
  cursor: "pointer",
5816
5821
  "font-weight": "600",
@@ -5820,7 +5825,7 @@ var i$25 = {
5820
5825
  "user-select": "none"
5821
5826
  };
5822
5827
  var a$21 = {
5823
- ...i$25,
5828
+ ...i$26,
5824
5829
  "margin-bottom": "4px"
5825
5830
  };
5826
5831
  var o$23 = {
@@ -5828,7 +5833,7 @@ var o$23 = {
5828
5833
  color: "red",
5829
5834
  "margin-top": "4px"
5830
5835
  };
5831
- var s$19 = {
5836
+ var s$20 = {
5832
5837
  color: "red",
5833
5838
  "margin-left": "4px"
5834
5839
  };
@@ -5847,14 +5852,14 @@ var l$13 = {
5847
5852
  "padding-left": "0px",
5848
5853
  display: "block",
5849
5854
  ...c$18,
5850
- "background-color": "inherit",
5855
+ "background-color": "transparent",
5851
5856
  color: "inherit",
5852
5857
  "line-height": "inherit",
5853
5858
  "font-family": "inherit",
5854
5859
  "font-size": "inherit",
5855
5860
  "box-shadow": "none"
5856
5861
  };
5857
- var u$13 = {
5862
+ var u$14 = {
5858
5863
  height: "100%",
5859
5864
  width: "100%",
5860
5865
  display: "block",
@@ -5868,7 +5873,7 @@ var u$13 = {
5868
5873
  cursor: "inherit",
5869
5874
  "box-shadow": "none"
5870
5875
  };
5871
- var d$13 = {
5876
+ var d$12 = {
5872
5877
  height: "100%",
5873
5878
  "margin-top": "0px",
5874
5879
  "margin-right": "0px",
@@ -5888,11 +5893,11 @@ var d$13 = {
5888
5893
  "font-weight": "inherit",
5889
5894
  cursor: "pointer"
5890
5895
  };
5891
- var f$11 = {
5896
+ var f$10 = {
5892
5897
  ...n$37,
5893
5898
  cursor: "pointer"
5894
5899
  };
5895
- var p$10 = {
5900
+ var p$11 = {
5896
5901
  height: "20px",
5897
5902
  width: "36px",
5898
5903
  "margin-top": "0px",
@@ -5922,7 +5927,7 @@ var p$10 = {
5922
5927
  appearance: "none",
5923
5928
  cursor: "pointer"
5924
5929
  };
5925
- var m$6 = (e, t, { field: n, icon: r } = {}) => ({
5930
+ var m$7 = (e, t, { field: n, icon: r } = {}) => ({
5926
5931
  name: e,
5927
5932
  displayMode: "desktop",
5928
5933
  style: {
@@ -5931,14 +5936,14 @@ var m$6 = (e, t, { field: n, icon: r } = {}) => ({
5931
5936
  ...n ? { field: { default: n } } : {},
5932
5937
  ...r ? { icon: { default: r } } : {},
5933
5938
  label: { default: a$21 },
5934
- requiredMark: { default: s$19 },
5939
+ requiredMark: { default: s$20 },
5935
5940
  error: { default: o$23 }
5936
5941
  }
5937
5942
  });
5938
5943
  var h$4 = { field: l$13 };
5939
5944
  var g$4 = t$60()({
5940
5945
  type: "formControl",
5941
- attributeValues: { subType: e$39()([
5946
+ attributeValues: { subType: e$40()([
5942
5947
  "text",
5943
5948
  "number",
5944
5949
  "email",
@@ -5977,7 +5982,7 @@ var g$4 = t$60()({
5977
5982
  autoFocus: !1,
5978
5983
  disabled: !1,
5979
5984
  options: [],
5980
- required: !0,
5985
+ required: !1,
5981
5986
  requiredMessage: "",
5982
5987
  minLength: 0,
5983
5988
  minLengthMessage: "",
@@ -5993,7 +5998,7 @@ var g$4 = t$60()({
5993
5998
  definition: {
5994
5999
  label: "Form Control",
5995
6000
  type: "formControl",
5996
- description: "A single labelled input (text/select/checkbox/… per its subType) inside a form; captures one field of user input. A select offers its `options`: `[{ label, value }, …]` — the text shown, and what the field takes. A `switch` is a checkbox announced and drawn as an on/off switch (`role=\"switch\"`), for a setting that applies at once; both hold `true`/`false`, and a `defaultValue` of `\"true\"` starts them on. Its slots: `label`, `requiredMark` (the `*` that ends the label of a required field), `input` (the box a typed field or a select is drawn in — a textarea, checkbox or switch is its own box), `field` (the `<input>` or `<select>` inside that box), `icon` (the show/hide button of a password) and `error`. The thumb of a switch is no element a class reaches: the class on its `input` slot sets `--plitzi-switch-thumb` (off), `--plitzi-switch-thumb-checked` (on, white by default) and `--plitzi-switch-thumb-shadow`.",
6001
+ description: "A single labelled input (text/select/checkbox/… per its subType) inside a form; captures one field of user input. Optional unless `required: true`, as an HTML field is: a `requiredMessage` alone requires nothing. A select offers its `options`: `[{ label, value }, …]` — the text shown, and what the field takes. A `switch` is a checkbox announced and drawn as an on/off switch (`role=\"switch\"`), for a setting that applies at once; both hold `true`/`false`, and a `defaultValue` of `\"true\"` starts them on. Its slots: `label`, `requiredMark` (the `*` that ends the label of a required field), `input` (the box a typed field or a select is drawn in — a textarea, checkbox or switch is its own box), `field` (the `<input>` or `<select>` inside that box), `icon` (the show/hide button of a password) and `error`. The box is ringed (`2px solid currentColor`) while the field inside has keyboard focus: restyle it with a `focus-within` state on the `input` slot — the box never takes `focus` itself. The thumb of a switch is no element a class reaches: the class on its `input` slot sets `--plitzi-switch-thumb` (off), `--plitzi-switch-thumb-checked` (on, white by default) and `--plitzi-switch-thumb-shadow`.",
5997
6002
  bindings: {},
5998
6003
  styleSelectors: {
5999
6004
  base: "",
@@ -6029,26 +6034,26 @@ var g$4 = t$60()({
6029
6034
  displayMode: "desktop",
6030
6035
  style: { base: { default: {} } },
6031
6036
  subTypes: {
6032
- hidden: m$6("Form Control Hidden", {}),
6033
- text: m$6("Form Control Text", r$32, h$4),
6034
- number: m$6("Form Control Number", r$32, h$4),
6035
- email: m$6("Form Control Email", r$32, h$4),
6036
- password: m$6("Form Control Password", r$32, {
6037
+ hidden: m$7("Form Control Hidden", {}),
6038
+ text: m$7("Form Control Text", r$31, h$4),
6039
+ number: m$7("Form Control Number", r$31, h$4),
6040
+ email: m$7("Form Control Email", r$31, h$4),
6041
+ password: m$7("Form Control Password", r$31, {
6037
6042
  ...h$4,
6038
- icon: d$13
6043
+ icon: d$12
6039
6044
  }),
6040
- search: m$6("Form Control Search", r$32, h$4),
6041
- url: m$6("Form Control URL", r$32, h$4),
6042
- tel: m$6("Form Control Phone", r$32, h$4),
6043
- date: m$6("Form Control Date", r$32, h$4),
6044
- time: m$6("Form Control Time", r$32, h$4),
6045
- color: m$6("Form Control Color", r$32, h$4),
6046
- textarea: m$6("Form Control Textarea", {
6047
- ...r$32,
6045
+ search: m$7("Form Control Search", r$31, h$4),
6046
+ url: m$7("Form Control URL", r$31, h$4),
6047
+ tel: m$7("Form Control Phone", r$31, h$4),
6048
+ date: m$7("Form Control Date", r$31, h$4),
6049
+ time: m$7("Form Control Time", r$31, h$4),
6050
+ color: m$7("Form Control Color", r$31, h$4),
6051
+ textarea: m$7("Form Control Textarea", {
6052
+ ...r$31,
6048
6053
  color: "inherit",
6049
6054
  resize: "vertical"
6050
6055
  }),
6051
- select: m$6("Form Control Select", f$11, { field: u$13 }),
6056
+ select: m$7("Form Control Select", f$10, { field: u$14 }),
6052
6057
  checkbox: {
6053
6058
  name: "Form Control Checkbox",
6054
6059
  displayMode: "desktop",
@@ -6060,8 +6065,8 @@ var g$4 = t$60()({
6060
6065
  "margin-left": "0px",
6061
6066
  "margin-right": "4px"
6062
6067
  } },
6063
- label: { default: i$25 },
6064
- requiredMark: { default: s$19 },
6068
+ label: { default: i$26 },
6069
+ requiredMark: { default: s$20 },
6065
6070
  error: { default: o$23 }
6066
6071
  }
6067
6072
  },
@@ -6070,12 +6075,12 @@ var g$4 = t$60()({
6070
6075
  displayMode: "desktop",
6071
6076
  style: {
6072
6077
  base: { default: {} },
6073
- input: { default: p$10 },
6078
+ input: { default: p$11 },
6074
6079
  label: { default: {
6075
- ...i$25,
6080
+ ...i$26,
6076
6081
  "align-items": "center"
6077
6082
  } },
6078
- requiredMark: { default: s$19 },
6083
+ requiredMark: { default: s$20 },
6079
6084
  error: { default: o$23 }
6080
6085
  }
6081
6086
  }
@@ -6086,7 +6091,7 @@ var g$4 = t$60()({
6086
6091
  });
6087
6092
  var n$36 = t$60()({
6088
6093
  type: "layoutContainer",
6089
- attributeValues: { subType: e$39()([
6094
+ attributeValues: { subType: e$40()([
6090
6095
  "div",
6091
6096
  "header",
6092
6097
  "footer",
@@ -6218,6 +6223,11 @@ var t$49 = t$60()({
6218
6223
  });
6219
6224
  var t$48 = t$60()({
6220
6225
  type: "page",
6226
+ serverTemplates: [
6227
+ "seoPageTitle",
6228
+ "seoPageDescription",
6229
+ "notFound"
6230
+ ],
6221
6231
  triggers: { onPageLoad: {
6222
6232
  action: "onPageLoad",
6223
6233
  title: "On Page Load",
@@ -6385,12 +6395,12 @@ var t$46 = t$60()({
6385
6395
  var n$35 = t$60()({
6386
6396
  type: "image",
6387
6397
  attributeValues: {
6388
- fetchPriority: e$39()([
6398
+ fetchPriority: e$40()([
6389
6399
  "high",
6390
6400
  "low",
6391
6401
  "auto"
6392
6402
  ]),
6393
- loadMode: e$39()([
6403
+ loadMode: e$40()([
6394
6404
  "auto",
6395
6405
  "eager",
6396
6406
  "lazy"
@@ -6539,7 +6549,7 @@ var t$44 = t$60()({
6539
6549
  var n$34 = t$60()({
6540
6550
  type: "apiContainer",
6541
6551
  attributeValues: {
6542
- subType: e$39()([
6552
+ subType: e$40()([
6543
6553
  "",
6544
6554
  "div",
6545
6555
  "header",
@@ -6552,25 +6562,26 @@ var n$34 = t$60()({
6552
6562
  "address",
6553
6563
  "figure"
6554
6564
  ]),
6555
- method: e$39()([
6565
+ method: e$40()([
6556
6566
  "get",
6557
6567
  "post",
6558
6568
  "put",
6559
6569
  "delete",
6560
6570
  "patch"
6561
6571
  ]),
6562
- credentials: e$39()([
6572
+ credentials: e$40()([
6563
6573
  "include",
6564
6574
  "omit",
6565
6575
  "same-origin"
6566
6576
  ]),
6567
- pagination: e$39()([
6577
+ pagination: e$40()([
6568
6578
  "none",
6569
6579
  "url",
6570
6580
  "append"
6571
6581
  ])
6572
6582
  },
6573
6583
  sourceType: "apiContainer",
6584
+ serverTemplates: ["notFound"],
6574
6585
  triggers: {
6575
6586
  onApiError: {
6576
6587
  action: "onApiError",
@@ -6677,7 +6688,7 @@ var n$34 = t$60()({
6677
6688
  definition: {
6678
6689
  label: "Api Container",
6679
6690
  type: "apiContainer",
6680
- description: "Fetches data from a backend HTTP API (its `query`/`method`/`credentials`) and exposes the response as a data source ITS DESCENDANTS bind to (source `apiContainer_<id>.data`; only elements inside it can consume it). This is how you get backend data into the frontend. Its `mockData` prop is builder-only sample data — the published runtime fetches the real `query`, so always set a real query for production. With `cache: true` a browser request is kept for `staleTime` seconds (default 30) and shared with every provider asking the same thing; it is off unless set. A flow refreshes it with `performQuery` or the global `invalidateQueries` step, and `refreshSeconds` makes it ask again on its own every N seconds (either runtime; paused while the tab is hidden) — the way to keep a queue, feed or status board current without a plugin. Its `subType` (container tag) is empty by default, and then it renders NO element of its own: its children lay out directly in its parent, and any class, variant or style binding on it applies to nothing. To style the provider itself give it a tag (`subType: \"div\"`, `section`, …); otherwise style its parent or a child.",
6691
+ description: "Fetches data from a backend HTTP API (its `query`/`method`/`credentials`) and exposes the response as a data source ITS DESCENDANTS bind to (source `apiContainer_<id>.data`; only elements inside it can consume it). This is how you get backend data into the frontend. Its `mockData` prop is builder-only sample data — the published runtime fetches the real `query`, so always set a real query for production. With `cache: true` a browser request is kept for `staleTime` seconds (default 30) and shared with every provider asking the same thing; it is off unless set. A flow refreshes it with `performQuery` or the global `invalidateQueries` step, and `refreshSeconds` makes it ask again on its own every N seconds (either runtime; paused while the tab is hidden) — the way to keep a queue, feed or status board current without a plugin. A server provider (`runtime: \"server\"`) whose answer means the address shows nothing — a record not found — says so in `notFound`, a template against its answer (`{{ source.found == false }}`): the page is sent with status 404, rendered as written. Its `subType` (container tag) is empty by default, and then it renders NO element of its own: its children lay out directly in its parent, and any class, variant or style binding on it applies to nothing. To style the provider itself give it a tag (`subType: \"div\"`, `section`, …); otherwise style its parent or a child.",
6681
6692
  items: [],
6682
6693
  bindings: {},
6683
6694
  styleSelectors: { base: "" },
@@ -6712,7 +6723,7 @@ var n$34 = t$60()({
6712
6723
  });
6713
6724
  var n$33 = t$60()({
6714
6725
  type: "channel",
6715
- attributeValues: { subType: e$39()([
6726
+ attributeValues: { subType: e$40()([
6716
6727
  "",
6717
6728
  "div",
6718
6729
  "section",
@@ -6832,6 +6843,49 @@ var n$33 = t$60()({
6832
6843
  }
6833
6844
  });
6834
6845
  var t$43 = t$60()({
6846
+ type: "carouselTrack",
6847
+ ancestorType: "carousel",
6848
+ content: {
6849
+ attributes: {},
6850
+ definition: {
6851
+ label: "Carousel Track",
6852
+ type: "carouselTrack",
6853
+ description: "Where a carousel shows its slides: what is inside is one slide, rendered once per item.",
6854
+ items: [],
6855
+ bindings: {},
6856
+ styleSelectors: { base: "" },
6857
+ initialState: { visibility: !0 }
6858
+ },
6859
+ builder: {
6860
+ canDelete: !0,
6861
+ canSelect: !0,
6862
+ canDragDrop: !0,
6863
+ canMove: !0,
6864
+ canSnippet: !1,
6865
+ itemsAllowed: [],
6866
+ itemsNotAllowed: []
6867
+ },
6868
+ market: {
6869
+ category: "structure",
6870
+ owner: "Plitzi",
6871
+ verified: !0,
6872
+ license: "MIT",
6873
+ website: "https://plitzi.com",
6874
+ backgroundColor: "#4422ee",
6875
+ icon: "fa-solid fa-film"
6876
+ },
6877
+ defaultStyle: {
6878
+ name: "Carousel Track",
6879
+ displayMode: "desktop",
6880
+ style: { base: { default: {
6881
+ position: "relative",
6882
+ overflow: "hidden"
6883
+ } } }
6884
+ },
6885
+ settings: {}
6886
+ }
6887
+ });
6888
+ var t$42 = t$60()({
6835
6889
  type: "carousel",
6836
6890
  sourceType: "carousel",
6837
6891
  triggers: { onChange: {
@@ -6933,53 +6987,10 @@ var t$43 = t$60()({
6933
6987
  },
6934
6988
  initialItems: ["carouselTrack"]
6935
6989
  });
6936
- var t$42 = t$60()({
6937
- type: "carouselTrack",
6938
- ancestorType: "carousel",
6939
- content: {
6940
- attributes: {},
6941
- definition: {
6942
- label: "Carousel Track",
6943
- type: "carouselTrack",
6944
- description: "Where a carousel shows its slides: what is inside is one slide, rendered once per item.",
6945
- items: [],
6946
- bindings: {},
6947
- styleSelectors: { base: "" },
6948
- initialState: { visibility: !0 }
6949
- },
6950
- builder: {
6951
- canDelete: !0,
6952
- canSelect: !0,
6953
- canDragDrop: !0,
6954
- canMove: !0,
6955
- canSnippet: !1,
6956
- itemsAllowed: [],
6957
- itemsNotAllowed: []
6958
- },
6959
- market: {
6960
- category: "structure",
6961
- owner: "Plitzi",
6962
- verified: !0,
6963
- license: "MIT",
6964
- website: "https://plitzi.com",
6965
- backgroundColor: "#4422ee",
6966
- icon: "fa-solid fa-film"
6967
- },
6968
- defaultStyle: {
6969
- name: "Carousel Track",
6970
- displayMode: "desktop",
6971
- style: { base: { default: {
6972
- position: "relative",
6973
- overflow: "hidden"
6974
- } } }
6975
- },
6976
- settings: {}
6977
- }
6978
- });
6979
- var r$31 = t$60()({
6990
+ var r$30 = t$60()({
6980
6991
  type: "container",
6981
6992
  attributeValues: {
6982
- subType: e$39()([
6993
+ subType: e$40()([
6983
6994
  "div",
6984
6995
  "header",
6985
6996
  "footer",
@@ -7003,7 +7014,7 @@ var r$31 = t$60()({
7003
7014
  "p",
7004
7015
  "span"
7005
7016
  ]),
7006
- live: e$39()([
7017
+ live: e$40()([
7007
7018
  "",
7008
7019
  "polite",
7009
7020
  "assertive"
@@ -7093,11 +7104,11 @@ var B$2 = {
7093
7104
  ApiContainer: n$34,
7094
7105
  BlockHtml: t$59,
7095
7106
  BlockJsx: t$58,
7096
- Button: r$34,
7097
- Carousel: t$43,
7098
- CarouselTrack: t$42,
7107
+ Button: r$33,
7108
+ Carousel: t$42,
7109
+ CarouselTrack: t$43,
7099
7110
  Channel: n$33,
7100
- Container: r$31,
7111
+ Container: r$30,
7101
7112
  Custom: t$57,
7102
7113
  DialogContainer: t$60()({
7103
7114
  type: "dialogContainer",
@@ -7228,8 +7239,7 @@ var B$2 = {
7228
7239
  top: "0",
7229
7240
  position: "absolute",
7230
7241
  "z-index": "210",
7231
- "background-color": "black",
7232
- opacity: "0.5"
7242
+ "background-color": "rgb(0 0 0 / 50%)"
7233
7243
  } },
7234
7244
  rootContainer: { default: {
7235
7245
  display: "flex",
@@ -7300,8 +7310,8 @@ var B$2 = {
7300
7310
  List: t$60()({
7301
7311
  type: "list",
7302
7312
  attributeValues: {
7303
- subType: e$39()(["ul", "ol"]),
7304
- source: e$39()(["none", "controlled"])
7313
+ subType: e$40()(["ul", "ol"]),
7314
+ source: e$40()(["none", "controlled"])
7305
7315
  },
7306
7316
  sourceType: "list",
7307
7317
  content: {
@@ -7464,7 +7474,7 @@ var B$2 = {
7464
7474
  definition: {
7465
7475
  label: "Modal Container",
7466
7476
  type: "modalContainer",
7467
- description: "A modal overlay container opened and closed through interactions; use for dialogs over the page.",
7477
+ description: "A modal overlay container opened and closed through interactions; use for dialogs over the page. The element itself is fixed over the whole viewport, and holds two layers side by side — neither inside the other: `backgroundContainer`, the dim layer, `rgb(0 0 0 / 50%)` (its `background-color` is the whole dim), and `rootContainer`, the dialog, centred by `top: 50%`, `left: 50%` and `transform: translate3d(-50%, -50%, 0)` — to sit it near the top, set `top` and `transform: translateX(-50%)`; flex alignment on the dim layer moves nothing. Inside the dialog, top to bottom: `headerContainer` (with `headerTitle` and `headerCloseButton`) and `bodyContainer`.",
7468
7478
  items: [],
7469
7479
  bindings: {},
7470
7480
  loadStrategy: "lazy",
@@ -7516,8 +7526,7 @@ var B$2 = {
7516
7526
  top: "0",
7517
7527
  position: "absolute",
7518
7528
  "z-index": "210",
7519
- "background-color": "black",
7520
- opacity: "0.5"
7529
+ "background-color": "rgb(0 0 0 / 50%)"
7521
7530
  } },
7522
7531
  rootContainer: { default: {
7523
7532
  display: "flex",
@@ -7601,8 +7610,8 @@ var B$2 = {
7601
7610
  Pagination: t$60()({
7602
7611
  type: "pagination",
7603
7612
  attributeValues: {
7604
- mode: e$39()(["pages", "loadMore"]),
7605
- target: e$39()(["url", "interaction"])
7613
+ mode: e$40()(["pages", "loadMore"]),
7614
+ target: e$40()(["url", "interaction"])
7606
7615
  },
7607
7616
  triggers: { onPageChange: {
7608
7617
  action: "onPageChange",
@@ -7851,9 +7860,10 @@ var B$2 = {
7851
7860
  }
7852
7861
  }),
7853
7862
  Text: t$51,
7854
- ThemeToggle: r$33,
7863
+ ThemeToggle: r$32,
7855
7864
  Video: t$44
7856
7865
  };
7866
+ Object.fromEntries(Object.values(B$2).flatMap((e) => "serverTemplates" in e ? [[e.type, e.serverTemplates]] : []));
7857
7867
  /** Every declaration, read through the part of its shape this catalog uses. */
7858
7868
  var declarations$1 = B$2;
7859
7869
  /** Keyed by the schema `type`, which is what a document and every catalog address a type by. */
@@ -7864,7 +7874,7 @@ var elementCatalog = Object.fromEntries(Object.values(declarations$1).map((decla
7864
7874
  }]));
7865
7875
  Object.keys(elementCatalog);
7866
7876
  Object.fromEntries(Object.values(declarations$1).filter((declaration) => declaration.sourceType).map((declaration) => [declaration.type, declaration.sourceType]));
7867
- Object.fromEntries(Object.values(declarations$1).map((declaration) => [declaration.type, [...Object.keys(e$40), ...Object.keys(declaration.triggers ?? {})]]));
7877
+ Object.fromEntries(Object.values(declarations$1).map((declaration) => [declaration.type, [...Object.keys(e$41), ...Object.keys(declaration.triggers ?? {})]]));
7868
7878
  /** The triggers only some types fire, by action name — for a step builder that wants the title the builder shows. */
7869
7879
  var typeTriggerDefinitions = Object.fromEntries(Object.values(declarations$1).flatMap((declaration) => Object.entries(declaration.triggers ?? {})));
7870
7880
  Object.fromEntries(Object.values(declarations$1).flatMap((declaration) => Object.entries(declaration.callbacks ?? {})));
@@ -7965,6 +7975,11 @@ var AUTHORING_CODES = {
7965
7975
  means: "a page that sends visitors it is not for to a page the space does not have",
7966
7976
  fix: "a page's id or slug (`''` is the home page), or a full URL"
7967
7977
  },
7978
+ "page-template": {
7979
+ kind: "refused",
7980
+ means: "a template in a page's `seoTitle`, `seoDescription` or `notFound` that reads what is not there when the server answers — anything but the page's `runtime: 'server'` providers and `navigation` — or cannot be read",
7981
+ fix: "read the record from a provider with `runtime: 'server'` on the page or its layout — `'{{ apiContainer_post.title }} — Blog'` — or words of its own"
7982
+ },
7968
7983
  "folder-undeclared": {
7969
7984
  kind: "refused",
7970
7985
  means: "a page, a layout or a folder filed in a folder the space does not declare",
@@ -8140,6 +8155,11 @@ var AUTHORING_CODES = {
8140
8155
  means: "an ancestor condition keyed by something that is neither a class name nor `'>'` (the parent)",
8141
8156
  fix: "`[card.name]` for a `styles()` declaration, or `'>'` for the parent whatever it wears"
8142
8157
  },
8158
+ "focus-on-field-box": {
8159
+ kind: "warned",
8160
+ means: "a `focus` or `focus-visible` state on a text field's or a select's `input` slot — the box the field is drawn in, which never takes focus, so no ring is ever shown",
8161
+ fix: "`'focus-within'` on the same class, or the state on the `field` slot"
8162
+ },
8143
8163
  "heading-level-overridden": {
8144
8164
  kind: "warned",
8145
8165
  means: "a markdown's or richText's `heading` slot and a level's (`heading3`) both setting one property, the general class written later in the stylesheet — the level's value is lost",
@@ -8370,6 +8390,16 @@ var AUTHORING_CODES = {
8370
8390
  means: "a `runtime: 'server'` element inside a component: the page server resolves a page's and its layouts', never a component's",
8371
8391
  fix: "put the provider on the page, around the instance, and hand the component its rows as a prop"
8372
8392
  },
8393
+ "not-found-in-browser": {
8394
+ kind: "refused",
8395
+ means: "`notFound` on a provider asked from the browser — its answer arrives after the page was sent with its status",
8396
+ fix: "`runtime: 'server'` on the provider, or `visible` on the page's \"not found\" part instead"
8397
+ },
8398
+ "not-found-not-a-template": {
8399
+ kind: "refused",
8400
+ means: "`notFound`, a provider's or a page's, that is not one `{{ expression }}` — never `true`, so never a 404",
8401
+ fix: "`notFound: '{{ source.found == false }}'`"
8402
+ },
8373
8403
  "server-data-in-browser": {
8374
8404
  kind: "refused",
8375
8405
  means: "a provider asking for the project's own data (`/data/…`, read by its server from `src/data/` and never served) from the browser",
@@ -8390,6 +8420,11 @@ var AUTHORING_CODES = {
8390
8420
  means: "a control in a form with no `name` — its value never reaches `values`",
8391
8421
  fix: "`formControl({ name: 'email', … })`"
8392
8422
  },
8423
+ "required-message-unused": {
8424
+ kind: "warned",
8425
+ means: "a `requiredMessage` on a field nothing requires — fields are optional unless `required: true`, so it is never shown and an empty answer is sent",
8426
+ fix: "`required: true` on the field, or no message"
8427
+ },
8393
8428
  "form-control-name-taken": {
8394
8429
  kind: "warned",
8395
8430
  means: "two controls in one form with one name — one overwrites the other",
@@ -8855,6 +8890,11 @@ var AUTHORING_CODES = {
8855
8890
  means: "a colour written out in a class where the space has a token of that value — it stays put in the dark scheme",
8856
8891
  fix: "`var(--token)` when it should follow the scheme; a token of one value of its own when it must stay the same in both"
8857
8892
  },
8893
+ "not-found-page": {
8894
+ kind: "suggested",
8895
+ means: "a space with no page of its own for an address no other page answers — a plain one was added, in the home page’s layout, sent with status 404",
8896
+ fix: "a page whose slug is `'*'`: `{ id: 'not-found', name: 'Not found', slug: '*', layout: …, body: [...] }`"
8897
+ },
8858
8898
  "unused-component": {
8859
8899
  kind: "suggested",
8860
8900
  means: "a component no page, layout or other component places",
@@ -8965,7 +9005,7 @@ var sourceRow = (listSource, items) => {
8965
9005
  index: pathTo(`${listSource}.index`, 0)
8966
9006
  };
8967
9007
  };
8968
- var e$38 = [
9008
+ var e$39 = [
8969
9009
  "variables",
8970
9010
  "navigation",
8971
9011
  "auth",
@@ -8975,7 +9015,7 @@ var e$38 = [
8975
9015
  "flags",
8976
9016
  "computed"
8977
9017
  ];
8978
- e$38.filter((e) => e !== "state" && e !== "computed");
9018
+ e$39.filter((e) => e !== "state" && e !== "computed");
8979
9019
  var t$30 = {
8980
9020
  DISPLAY: "display",
8981
9021
  FLEX_WRAP: "flex-wrap",
@@ -9154,6 +9194,8 @@ var t$30 = {
9154
9194
  TEXT_INDENT: "text-indent",
9155
9195
  TEXT_TRANSFORM: "text-transform",
9156
9196
  DIRECTION: "direction",
9197
+ WRITING_MODE: "writing-mode",
9198
+ TEXT_ORIENTATION: "text-orientation",
9157
9199
  WHITE_SPACE: "white-space",
9158
9200
  TEXT_WRAP: "text-wrap",
9159
9201
  WORD_BREAK: "word-break",
@@ -9178,8 +9220,8 @@ Object.values(t$30).map((e) => ({
9178
9220
  path: e,
9179
9221
  label: e
9180
9222
  }));
9181
- t$30.DISPLAY, t$30.FLEX_WRAP, t$30.FLEX_DIRECTION, t$30.ALIGN_ITEMS, t$30.JUSTIFY_ITEMS, t$30.JUSTIFY_CONTENT, t$30.ALIGN_CONTENT, t$30.ROW_GAP, t$30.COLUMN_GAP, t$30.GRID_ROW_GAP, t$30.GRID_COLUMN_GAP, t$30.GRID_TEMPLATE_AREAS, t$30.GRID_TEMPLATE_COLUMNS, t$30.GRID_TEMPLATE_ROWS, t$30.GRID_AUTO_FLOW, t$30.GRID_AUTO_ROWS, t$30.GRID_AUTO_COLUMNS, t$30.GRID_ROW_START, t$30.GRID_COLUMN_START, t$30.GRID_ROW_END, t$30.GRID_COLUMN_END, t$30.ALIGN_SELF, t$30.JUSTIFY_SELF, t$30.ORDER, t$30.FLEX_GROW, t$30.FLEX_SHRINK, t$30.FLEX_BASIS, t$30.LIST_STYLE, t$30.LIST_STYLE_TYPE, t$30.LIST_STYLE_POSITION, t$30.LIST_STYLE_IMAGE, t$30.BACKGROUND_COLOR, t$30.BACKGROUND_IMAGE, t$30.BACKGROUND_POSITION, t$30.BACKGROUND_SIZE, t$30.BACKGROUND_CLIP, t$30.BACKGROUND_ORIGIN, t$30.BACKGROUND_REPEAT, t$30.BACKGROUND_ATTACHMENT, t$30.BACKGROUND_BLEND_MODE, t$30.MASK_IMAGE, t$30.MASK_SIZE, t$30.MASK_POSITION, t$30.MASK_REPEAT, t$30.MASK_COMPOSITE, t$30.WEBKIT_MASK_IMAGE, t$30.BORDER_TOP_STYLE, t$30.BORDER_TOP_WIDTH, t$30.BORDER_TOP_COLOR, t$30.BORDER_BOTTOM_STYLE, t$30.BORDER_BOTTOM_WIDTH, t$30.BORDER_BOTTOM_COLOR, t$30.BORDER_LEFT_STYLE, t$30.BORDER_LEFT_WIDTH, t$30.BORDER_LEFT_COLOR, t$30.BORDER_RIGHT_STYLE, t$30.BORDER_RIGHT_WIDTH, t$30.BORDER_RIGHT_COLOR, t$30.BORDER_TOP_LEFT_RADIUS, t$30.BORDER_TOP_RIGHT_RADIUS, t$30.BORDER_BOTTOM_LEFT_RADIUS, t$30.BORDER_BOTTOM_RIGHT_RADIUS, t$30.BORDER_COLLAPSE, t$30.BORDER_SPACING, t$30.TABLE_LAYOUT, t$30.OPACITY, t$30.VISIBILITY, t$30.CURSOR, t$30.TRANSITION, t$30.TRANSITION_PROPERTY, t$30.TRANSITION_DURATION, t$30.TRANSITION_TIMING_FUNCTION, t$30.TRANSITION_DELAY, t$30.TRANSITION_BEHAVIOR, t$30.ANIMATION, t$30.ANIMATION_NAME, t$30.ANIMATION_DURATION, t$30.ANIMATION_TIMING_FUNCTION, t$30.ANIMATION_DELAY, t$30.ANIMATION_ITERATION_COUNT, t$30.ANIMATION_DIRECTION, t$30.ANIMATION_FILL_MODE, t$30.ANIMATION_PLAY_STATE, t$30.ANIMATION_TIMELINE, t$30.ANIMATION_RANGE, t$30.BOX_SHADOW, t$30.FILTER, t$30.BACKDROP_FILTER, t$30.MIX_BLEND_MODE, t$30.ISOLATION, t$30.CLIP_PATH, t$30.TRANSFORM, t$30.TRANSFORM_ORIGIN, t$30.TRANSLATE, t$30.ROTATE, t$30.SCALE, t$30.TRANSFORM_STYLE, t$30.PERSPECTIVE, t$30.PERSPECTIVE_ORIGIN, t$30.BACKFACE_VISIBILITY, t$30.WILL_CHANGE, t$30.OUTLINE_WIDTH, t$30.OUTLINE_STYLE, t$30.OUTLINE_COLOR, t$30.OUTLINE_OFFSET, t$30.POINTER_EVENTS, t$30.USER_SELECT, t$30.TOUCH_ACTION, t$30.RESIZE, t$30.APPEARANCE, t$30.SCROLL_BEHAVIOR, t$30.OVERSCROLL_BEHAVIOR, t$30.SCROLL_SNAP_TYPE, t$30.SCROLL_SNAP_ALIGN, t$30.SCROLL_SNAP_STOP, t$30.SCROLL_PADDING_TOP, t$30.SCROLL_MARGIN_TOP, t$30.ACCENT_COLOR, t$30.CARET_COLOR, t$30.COLOR_SCHEME, t$30.POSITION, t$30.FLOAT, t$30.CLEAR, t$30.Z_INDEX, t$30.TOP, t$30.BOTTOM, t$30.LEFT, t$30.RIGHT, t$30.BOX_SIZING, t$30.ASPECT_RATIO, t$30.WIDTH, t$30.HEIGHT, t$30.MIN_WIDTH, t$30.MIN_HEIGHT, t$30.MAX_WIDTH, t$30.MAX_HEIGHT, t$30.OVERFLOW, t$30.OVERFLOW_X, t$30.OVERFLOW_Y, t$30.SCROLLBAR_WIDTH, t$30.SCROLLBAR_COLOR, t$30.CONTAINER_TYPE, t$30.CONTENT, t$30.CONTAINER_NAME, t$30.OBJECT_FIT, t$30.OBJECT_POSITION, t$30.COLUMN_WIDTH, t$30.COLUMN_COUNT, t$30.MARGIN_TOP, t$30.MARGIN_BOTTOM, t$30.MARGIN_LEFT, t$30.MARGIN_RIGHT, t$30.PADDING_TOP, t$30.PADDING_BOTTOM, t$30.PADDING_LEFT, t$30.PADDING_RIGHT, t$30.FONT_FAMILY, t$30.FONT_WEIGHT, t$30.FONT_SIZE, t$30.LINE_HEIGHT, t$30.COLOR, t$30.TEXT_ALIGN, t$30.FONT_STYLE, t$30.FONT_VARIANT, t$30.FONT_VARIANT_NUMERIC, t$30.FONT_FEATURE_SETTINGS, t$30.TEXT_DECORATION, t$30.TEXT_DECORATION_LINE, t$30.TEXT_DECORATION_COLOR, t$30.TEXT_DECORATION_STYLE, t$30.TEXT_DECORATION_THICKNESS, t$30.TEXT_UNDERLINE_OFFSET, t$30.LETTER_SPACING, t$30.WORD_SPACING, t$30.TEXT_INDENT, t$30.TEXT_TRANSFORM, t$30.DIRECTION, t$30.WHITE_SPACE, t$30.TEXT_WRAP, t$30.WORD_BREAK, t$30.OVERFLOW_WRAP, t$30.HYPHENS, t$30.VERTICAL_ALIGN, t$30.TEXT_OVERFLOW, t$30.TEXT_SHADOW, t$30.LINE_CLAMP, t$30.WEBKIT_LINE_CLAMP, t$30.WEBKIT_BOX_ORIENT, t$30.WEBKIT_TEXT_FILL_COLOR, t$30.WEBKIT_BACKGROUND_CLIP, t$30.BOX_DECORATION_BREAK, t$30.WEBKIT_BOX_DECORATION_BREAK, t$30.FILL, t$30.STROKE, t$30.STROKE_WIDTH;
9182
- [...e$38];
9223
+ t$30.DISPLAY, t$30.FLEX_WRAP, t$30.FLEX_DIRECTION, t$30.ALIGN_ITEMS, t$30.JUSTIFY_ITEMS, t$30.JUSTIFY_CONTENT, t$30.ALIGN_CONTENT, t$30.ROW_GAP, t$30.COLUMN_GAP, t$30.GRID_ROW_GAP, t$30.GRID_COLUMN_GAP, t$30.GRID_TEMPLATE_AREAS, t$30.GRID_TEMPLATE_COLUMNS, t$30.GRID_TEMPLATE_ROWS, t$30.GRID_AUTO_FLOW, t$30.GRID_AUTO_ROWS, t$30.GRID_AUTO_COLUMNS, t$30.GRID_ROW_START, t$30.GRID_COLUMN_START, t$30.GRID_ROW_END, t$30.GRID_COLUMN_END, t$30.ALIGN_SELF, t$30.JUSTIFY_SELF, t$30.ORDER, t$30.FLEX_GROW, t$30.FLEX_SHRINK, t$30.FLEX_BASIS, t$30.LIST_STYLE, t$30.LIST_STYLE_TYPE, t$30.LIST_STYLE_POSITION, t$30.LIST_STYLE_IMAGE, t$30.BACKGROUND_COLOR, t$30.BACKGROUND_IMAGE, t$30.BACKGROUND_POSITION, t$30.BACKGROUND_SIZE, t$30.BACKGROUND_CLIP, t$30.BACKGROUND_ORIGIN, t$30.BACKGROUND_REPEAT, t$30.BACKGROUND_ATTACHMENT, t$30.BACKGROUND_BLEND_MODE, t$30.MASK_IMAGE, t$30.MASK_SIZE, t$30.MASK_POSITION, t$30.MASK_REPEAT, t$30.MASK_COMPOSITE, t$30.WEBKIT_MASK_IMAGE, t$30.BORDER_TOP_STYLE, t$30.BORDER_TOP_WIDTH, t$30.BORDER_TOP_COLOR, t$30.BORDER_BOTTOM_STYLE, t$30.BORDER_BOTTOM_WIDTH, t$30.BORDER_BOTTOM_COLOR, t$30.BORDER_LEFT_STYLE, t$30.BORDER_LEFT_WIDTH, t$30.BORDER_LEFT_COLOR, t$30.BORDER_RIGHT_STYLE, t$30.BORDER_RIGHT_WIDTH, t$30.BORDER_RIGHT_COLOR, t$30.BORDER_TOP_LEFT_RADIUS, t$30.BORDER_TOP_RIGHT_RADIUS, t$30.BORDER_BOTTOM_LEFT_RADIUS, t$30.BORDER_BOTTOM_RIGHT_RADIUS, t$30.BORDER_COLLAPSE, t$30.BORDER_SPACING, t$30.TABLE_LAYOUT, t$30.OPACITY, t$30.VISIBILITY, t$30.CURSOR, t$30.TRANSITION, t$30.TRANSITION_PROPERTY, t$30.TRANSITION_DURATION, t$30.TRANSITION_TIMING_FUNCTION, t$30.TRANSITION_DELAY, t$30.TRANSITION_BEHAVIOR, t$30.ANIMATION, t$30.ANIMATION_NAME, t$30.ANIMATION_DURATION, t$30.ANIMATION_TIMING_FUNCTION, t$30.ANIMATION_DELAY, t$30.ANIMATION_ITERATION_COUNT, t$30.ANIMATION_DIRECTION, t$30.ANIMATION_FILL_MODE, t$30.ANIMATION_PLAY_STATE, t$30.ANIMATION_TIMELINE, t$30.ANIMATION_RANGE, t$30.BOX_SHADOW, t$30.FILTER, t$30.BACKDROP_FILTER, t$30.MIX_BLEND_MODE, t$30.ISOLATION, t$30.CLIP_PATH, t$30.TRANSFORM, t$30.TRANSFORM_ORIGIN, t$30.TRANSLATE, t$30.ROTATE, t$30.SCALE, t$30.TRANSFORM_STYLE, t$30.PERSPECTIVE, t$30.PERSPECTIVE_ORIGIN, t$30.BACKFACE_VISIBILITY, t$30.WILL_CHANGE, t$30.OUTLINE_WIDTH, t$30.OUTLINE_STYLE, t$30.OUTLINE_COLOR, t$30.OUTLINE_OFFSET, t$30.POINTER_EVENTS, t$30.USER_SELECT, t$30.TOUCH_ACTION, t$30.RESIZE, t$30.APPEARANCE, t$30.SCROLL_BEHAVIOR, t$30.OVERSCROLL_BEHAVIOR, t$30.SCROLL_SNAP_TYPE, t$30.SCROLL_SNAP_ALIGN, t$30.SCROLL_SNAP_STOP, t$30.SCROLL_PADDING_TOP, t$30.SCROLL_MARGIN_TOP, t$30.ACCENT_COLOR, t$30.CARET_COLOR, t$30.COLOR_SCHEME, t$30.POSITION, t$30.FLOAT, t$30.CLEAR, t$30.Z_INDEX, t$30.TOP, t$30.BOTTOM, t$30.LEFT, t$30.RIGHT, t$30.BOX_SIZING, t$30.ASPECT_RATIO, t$30.WIDTH, t$30.HEIGHT, t$30.MIN_WIDTH, t$30.MIN_HEIGHT, t$30.MAX_WIDTH, t$30.MAX_HEIGHT, t$30.OVERFLOW, t$30.OVERFLOW_X, t$30.OVERFLOW_Y, t$30.SCROLLBAR_WIDTH, t$30.SCROLLBAR_COLOR, t$30.CONTAINER_TYPE, t$30.CONTENT, t$30.CONTAINER_NAME, t$30.OBJECT_FIT, t$30.OBJECT_POSITION, t$30.COLUMN_WIDTH, t$30.COLUMN_COUNT, t$30.MARGIN_TOP, t$30.MARGIN_BOTTOM, t$30.MARGIN_LEFT, t$30.MARGIN_RIGHT, t$30.PADDING_TOP, t$30.PADDING_BOTTOM, t$30.PADDING_LEFT, t$30.PADDING_RIGHT, t$30.FONT_FAMILY, t$30.FONT_WEIGHT, t$30.FONT_SIZE, t$30.LINE_HEIGHT, t$30.COLOR, t$30.TEXT_ALIGN, t$30.FONT_STYLE, t$30.FONT_VARIANT, t$30.FONT_VARIANT_NUMERIC, t$30.FONT_FEATURE_SETTINGS, t$30.TEXT_DECORATION, t$30.TEXT_DECORATION_LINE, t$30.TEXT_DECORATION_COLOR, t$30.TEXT_DECORATION_STYLE, t$30.TEXT_DECORATION_THICKNESS, t$30.TEXT_UNDERLINE_OFFSET, t$30.LETTER_SPACING, t$30.WORD_SPACING, t$30.TEXT_INDENT, t$30.TEXT_TRANSFORM, t$30.DIRECTION, t$30.WRITING_MODE, t$30.TEXT_ORIENTATION, t$30.WHITE_SPACE, t$30.TEXT_WRAP, t$30.WORD_BREAK, t$30.OVERFLOW_WRAP, t$30.HYPHENS, t$30.VERTICAL_ALIGN, t$30.TEXT_OVERFLOW, t$30.TEXT_SHADOW, t$30.LINE_CLAMP, t$30.WEBKIT_LINE_CLAMP, t$30.WEBKIT_BOX_ORIENT, t$30.WEBKIT_TEXT_FILL_COLOR, t$30.WEBKIT_BACKGROUND_CLIP, t$30.BOX_DECORATION_BREAK, t$30.WEBKIT_BOX_DECORATION_BREAK, t$30.FILL, t$30.STROKE, t$30.STROKE_WIDTH;
9224
+ [...e$39];
9183
9225
  /** Both binding forms as one list. The map form targets attributes, which is what nearly every binding does. */
9184
9226
  var toBindingSpecs = (bind) => Array.isArray(bind) ? bind : Object.entries(bind).map(([to, source]) => ({
9185
9227
  to,
@@ -9491,20 +9533,20 @@ var tabContainerItem = defineElement(B$2.TabContainerItem);
9491
9533
  var text$1 = defineElement(B$2.Text);
9492
9534
  var themeToggle = defineElement(B$2.ThemeToggle);
9493
9535
  var video = defineElement(B$2.Video);
9494
- var e$32 = (e) => typeof e == "object" && !!e && !Array.isArray(e);
9536
+ var e$33 = (e) => typeof e == "object" && !!e && !Array.isArray(e);
9495
9537
  var STEP_TYPES = [
9496
9538
  "trigger",
9497
9539
  "globalCallback",
9498
9540
  "callback",
9499
9541
  "utility"
9500
9542
  ];
9501
- var e$30 = {
9543
+ var e$31 = {
9502
9544
  desktop: 1024,
9503
9545
  tablet: 768,
9504
9546
  mobile: 0
9505
9547
  };
9506
- var t$26 = (t) => e$30[t] === 0 ? "0" : `${String(e$30[t] / 16)}rem`;
9507
- var e$29 = [
9548
+ var t$26 = (t) => e$31[t] === 0 ? "0" : `${String(e$31[t] / 16)}rem`;
9549
+ var e$30 = [
9508
9550
  "desktop",
9509
9551
  "tablet",
9510
9552
  "mobile"
@@ -9512,16 +9554,16 @@ var e$29 = [
9512
9554
  Object.values(/* @__PURE__ */ function(e) {
9513
9555
  return e.COLOR = "color", e.SPACING = "spacing", e.SHADOW = "shadow", e.CUSTOM = "custom", e;
9514
9556
  }({}));
9515
- var e$27 = {
9557
+ var e$28 = {
9516
9558
  "motion-reduce": "@media (prefers-reduced-motion: reduce)",
9517
9559
  "motion-safe": "@media (prefers-reduced-motion: no-preference)"
9518
9560
  };
9519
- var t$23 = Object.keys(e$27);
9520
- var r$19 = (t) => Object.hasOwn(e$27, t);
9521
- var i$17 = String.raw`\d+(?:\.\d+)?(?:px|rem|em|ch)`;
9522
- var a$15 = String.raw`\((?:min|max)-width:\s*${i$17}\)`;
9561
+ var t$23 = Object.keys(e$28);
9562
+ var r$18 = (t) => Object.hasOwn(e$28, t);
9563
+ var i$18 = String.raw`\d+(?:\.\d+)?(?:px|rem|em|ch)`;
9564
+ var a$15 = String.raw`\((?:min|max)-width:\s*${i$18}\)`;
9523
9565
  var o$17 = new RegExp(String.raw`^container(?:\s+(?<name>-?[_a-zA-Z][\w-]*))?\s+(?<query>${a$15}(?:\s+and\s+${a$15})?)$`);
9524
- var s$14 = (e) => {
9566
+ var s$15 = (e) => {
9525
9567
  let t = o$17.exec(e.trim());
9526
9568
  if (t?.groups) return {
9527
9569
  name: t.groups.name,
@@ -9529,11 +9571,11 @@ var s$14 = (e) => {
9529
9571
  };
9530
9572
  };
9531
9573
  var c$12 = (e) => {
9532
- if (r$19(e)) return e;
9533
- let t = s$14(e);
9574
+ if (r$18(e)) return e;
9575
+ let t = s$15(e);
9534
9576
  if (t) return t.name ? `container ${t.name} ${t.query}` : `container ${t.query}`;
9535
9577
  };
9536
- var e$26 = [
9578
+ var e$27 = [
9537
9579
  "before",
9538
9580
  "after",
9539
9581
  "marker",
@@ -9552,7 +9594,7 @@ var t$22 = {
9552
9594
  selection: "::selection"
9553
9595
  };
9554
9596
  var n$19 = (e) => Object.hasOwn(t$22, e);
9555
- var i$16 = [
9597
+ var i$17 = [
9556
9598
  "before",
9557
9599
  "after",
9558
9600
  "marker"
@@ -9573,7 +9615,7 @@ var o$16 = [
9573
9615
  "line-height",
9574
9616
  "vertical-align"
9575
9617
  ];
9576
- var s$13 = [
9618
+ var s$14 = [
9577
9619
  "font-family",
9578
9620
  "font-size",
9579
9621
  "font-style",
@@ -9600,24 +9642,24 @@ var l$8 = {
9600
9642
  "color",
9601
9643
  "content",
9602
9644
  "direction",
9603
- ...s$13
9645
+ ...s$14
9604
9646
  ],
9605
9647
  placeholder: [
9606
9648
  ...o$16,
9607
- ...s$13,
9649
+ ...s$14,
9608
9650
  ...c$11
9609
9651
  ],
9610
9652
  "first-line": [
9611
9653
  ...o$16,
9612
- ...s$13,
9654
+ ...s$14,
9613
9655
  ...c$11
9614
9656
  ]
9615
9657
  };
9616
- var u$7 = (e, t) => {
9658
+ var u$8 = (e, t) => {
9617
9659
  let n = l$8[e];
9618
9660
  return !n || t.startsWith("--") || n.includes(t);
9619
9661
  };
9620
- var d$8 = /* @__PURE__ */ new Set([
9662
+ var d$7 = /* @__PURE__ */ new Set([
9621
9663
  "none",
9622
9664
  "normal",
9623
9665
  "open-quote",
@@ -9629,8 +9671,8 @@ var d$8 = /* @__PURE__ */ new Set([
9629
9671
  "unset",
9630
9672
  "revert"
9631
9673
  ]);
9632
- var f$6 = /^(?:counter|counters|attr|url|var|image-set|linear-gradient|radial-gradient|conic-gradient)\(/i;
9633
- var p$7 = (e) => {
9674
+ var f$5 = /^(?:counter|counters|attr|url|var|image-set|linear-gradient|radial-gradient|conic-gradient)\(/i;
9675
+ var p$8 = (e) => {
9634
9676
  let t = [], n = "", r = "", i = 0;
9635
9677
  for (let a of e) {
9636
9678
  if (r) {
@@ -9648,11 +9690,11 @@ var p$7 = (e) => {
9648
9690
  }
9649
9691
  return n && t.push(n), r || i !== 0 ? void 0 : t;
9650
9692
  };
9651
- var m$3 = (e) => {
9652
- let t = p$7(e.trim());
9653
- return t?.length ? t.every((e) => /^(["']).*\1$/s.test(e) || d$8.has(e.toLowerCase()) || f$6.test(e) && e.endsWith(")")) : !1;
9693
+ var m$4 = (e) => {
9694
+ let t = p$8(e.trim());
9695
+ return t?.length ? t.every((e) => /^(["']).*\1$/s.test(e) || d$7.has(e.toLowerCase()) || f$5.test(e) && e.endsWith(")")) : !1;
9654
9696
  };
9655
- var e$25 = [
9697
+ var e$26 = [
9656
9698
  "hover",
9657
9699
  "focus",
9658
9700
  "focus-visible",
@@ -9670,7 +9712,7 @@ var e$25 = [
9670
9712
  "even"
9671
9713
  ];
9672
9714
  var t$21 = "plitzi-component--hidden";
9673
- var r$17 = {
9715
+ var r$16 = {
9674
9716
  odd: 0,
9675
9717
  even: 1,
9676
9718
  first: 2,
@@ -9687,7 +9729,7 @@ var r$17 = {
9687
9729
  disabled: 13,
9688
9730
  hidden: 14
9689
9731
  };
9690
- var i$15 = (e) => Object.hasOwn(r$17, e);
9732
+ var i$16 = (e) => Object.hasOwn(r$16, e);
9691
9733
  var l$7 = {
9692
9734
  current: ":is([aria-current]:not([aria-current=\"false\"]),[aria-pressed=\"true\"],[aria-selected=\"true\"],[role=\"tabpanel\"]:not([hidden]))",
9693
9735
  expanded: "[aria-expanded=\"true\"]",
@@ -9697,8 +9739,8 @@ var l$7 = {
9697
9739
  odd: ":nth-child(odd)",
9698
9740
  even: ":nth-child(even)"
9699
9741
  };
9700
- var u$6 = (e) => (i$15(e) ? l$7[e] : void 0) ?? `:${e}`;
9701
- var p$6 = (e) => e === ">";
9742
+ var u$7 = (e) => (i$16(e) ? l$7[e] : void 0) ?? `:${e}`;
9743
+ var p$7 = (e) => e === ">";
9702
9744
  var n$16 = (e) => {
9703
9745
  let t = typeof e == "string" && e.trim() !== "" ? Number(e) : e;
9704
9746
  return typeof t == "number" && Number.isInteger(t) && t >= 250 ? t : void 0;
@@ -9733,7 +9775,7 @@ var r$13 = {
9733
9775
  del: "delete",
9734
9776
  return: "enter"
9735
9777
  };
9736
- var i$12 = /* @__PURE__ */ new Set([
9778
+ var i$13 = /* @__PURE__ */ new Set([
9737
9779
  "escape",
9738
9780
  "enter",
9739
9781
  "tab",
@@ -9766,7 +9808,7 @@ var o$11 = (e, t) => {
9766
9808
  }
9767
9809
  if (u.length !== 1) return { problem: `"${e.trim()}" names ${u.length ? `${u.length} keys (${u.join(", ")})` : "no key, only modifiers"}: a shortcut is modifiers and ONE key, like "shift+f"` };
9768
9810
  let [d] = u;
9769
- return d.length !== 1 && !i$12.has(d) ? { problem: `"${d}" in "${e.trim()}" is not a key: use one character ("f", "?", "+") or a key's name (${[...i$12].slice(0, 15).join(", ")}, f1…f12)` } : { combo: a$10(l, d) };
9811
+ return d.length !== 1 && !i$13.has(d) ? { problem: `"${d}" in "${e.trim()}" is not a key: use one character ("f", "?", "+") or a key's name (${[...i$13].slice(0, 15).join(", ")}, f1…f12)` } : { combo: a$10(l, d) };
9770
9812
  };
9771
9813
  var s$9 = (e, t = !1) => {
9772
9814
  let n = e.split(",").map((e) => o$11(e, t));
@@ -9819,7 +9861,7 @@ var steps_exports$1 = /* @__PURE__ */ __exportAll({
9819
9861
  * the builders useless for exactly the flows that matter most.
9820
9862
  */
9821
9863
  var builtinTrigger = (trigger) => {
9822
- if (Object.hasOwn(e$40, trigger)) return e$40[trigger];
9864
+ if (Object.hasOwn(e$41, trigger)) return e$41[trigger];
9823
9865
  return Object.hasOwn(typeTriggerDefinitions, trigger) ? typeTriggerDefinitions[trigger] : void 0;
9824
9866
  };
9825
9867
  var on = (trigger, params = {}) => {
@@ -9880,7 +9922,7 @@ var onInterval = (ms) => {
9880
9922
  * and hide the arrow at the end already reached: `[named('row', onScroll()), setState({ key: 'atEnd', type:
9881
9923
  * 'boolean', value: '{{ row.atEnd }}' })]`.
9882
9924
  */
9883
- var onScroll = () => on(s$21);
9925
+ var onScroll = () => on(s$22);
9884
9926
  var onPageLoad = () => on("onPageLoad");
9885
9927
  /**
9886
9928
  * The end of a server action this element started — the trigger a `detached` run needs, because that step returns
@@ -10130,7 +10172,7 @@ var carouselGoTo = (target, index) => carouselStep(target, "goTo", "Go To Slide"
10130
10172
  var carouselPlay = (target) => carouselStep(target, "play", "Play");
10131
10173
  /** The carousel `target` stops moving by itself until it is told to play. */
10132
10174
  var carouselPause = (target) => carouselStep(target, "pause", "Pause");
10133
- var e$21 = {
10175
+ var e$22 = {
10134
10176
  origin: "Requests to the same site",
10135
10177
  all: "All cached requests",
10136
10178
  elements: "Only these containers",
@@ -10142,7 +10184,7 @@ var t$16 = (t, n = () => !0) => ({
10142
10184
  description: "What to refresh once the write succeeded, so the page shows the new data: the requests to the same site, every provider on the page, only the api containers named in `invalidateElements`, or none for a step that only reads. Server-driven containers are refreshed too, asking the server past its cache. Containers on screen ask again at once; the rest when they are shown.",
10143
10185
  default: t[0],
10144
10186
  options: [...t],
10145
- optionLabels: Object.fromEntries(t.map((t) => [t, e$21[t]])),
10187
+ optionLabels: Object.fromEntries(t.map((t) => [t, e$22[t]])),
10146
10188
  label: "Refresh cached requests",
10147
10189
  canBind: !1,
10148
10190
  when: n
@@ -10222,7 +10264,7 @@ var t$15 = {
10222
10264
  preview: { cancelled: "" }
10223
10265
  }
10224
10266
  };
10225
- var e$20 = {
10267
+ var e$21 = {
10226
10268
  login: {
10227
10269
  source: "auth",
10228
10270
  title: "Auth Login",
@@ -10308,7 +10350,7 @@ var e$20 = {
10308
10350
  params: {}
10309
10351
  }
10310
10352
  };
10311
- var e$19 = { hostAction: {
10353
+ var e$20 = { hostAction: {
10312
10354
  source: "host",
10313
10355
  title: "Host Action",
10314
10356
  strictParams: !0,
@@ -10325,7 +10367,7 @@ var e$19 = { hostAction: {
10325
10367
  }
10326
10368
  }
10327
10369
  } };
10328
- var e$18 = { navigate: {
10370
+ var e$19 = { navigate: {
10329
10371
  source: "navigation",
10330
10372
  title: "Navigate",
10331
10373
  strictParams: !0,
@@ -10352,7 +10394,7 @@ var e$18 = { navigate: {
10352
10394
  }
10353
10395
  }
10354
10396
  } };
10355
- var e$17 = { invalidateQueries: {
10397
+ var e$18 = { invalidateQueries: {
10356
10398
  source: "queries",
10357
10399
  title: "Invalidate Queries",
10358
10400
  strictParams: !0,
@@ -10585,11 +10627,11 @@ var BUILTIN_GLOBAL_CALLBACKS = {
10585
10627
  default: ""
10586
10628
  } }
10587
10629
  },
10588
- ...e$19,
10589
- ...e$18,
10590
10630
  ...e$20,
10631
+ ...e$19,
10632
+ ...e$21,
10591
10633
  ...t$15,
10592
- ...e$17
10634
+ ...e$18
10593
10635
  };
10594
10636
  var steps_exports = /* @__PURE__ */ __exportAll({
10595
10637
  SET_STATE_TYPES: () => SET_STATE_TYPES,
@@ -10828,10 +10870,11 @@ var t$14 = (e) => {
10828
10870
  var t$13 = "[A-Za-z_][A-Za-z0-9_]*";
10829
10871
  RegExp(`^${t$13}$`);
10830
10872
  RegExp(`^!?${t$13}$`);
10831
- var e$8 = "/data/";
10832
- var e$6 = (e) => typeof e == "string" && /\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}/.test(e);
10833
- var i$4 = (e) => `value|round(0, '${e}')`;
10834
- i$4("floor"), i$4("ceil");
10873
+ var e$9 = "/data/";
10874
+ var e$7 = (e) => typeof e == "string" && /\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}/.test(e);
10875
+ var i$5 = (e) => `value|round(0, '${e}')`;
10876
+ i$5("floor"), i$5("ceil");
10877
+ new Set(Object.entries(g$4.content.defaultStyle.subTypes).filter(([, variant]) => "field" in variant.style).map(([subType]) => subType));
10835
10878
  /** A container with no tag of its own named: the row `list-row-not-li` makes an `<li>` without asking. */
10836
10879
  var isPlainContainer = (element) => element.definition.type === "container" && (element.attributes.subType === void 0 || element.attributes.subType === "div");
10837
10880
  new Set(STEP_TYPES);
@@ -10990,7 +11033,7 @@ Array.from({ length: 9 }, (e, t) => ({
10990
11033
  Object.entries(AUTHORING_CODES).filter(([, entry]) => entry.kind === "suggested").map(([code]) => code);
10991
11034
  var URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
10992
11035
  /** A page target that is really a URL: the one reading of `mailto:`/`https:` a page-mode link can never mean. */
10993
- var isUrlTarget = (target) => typeof target === "string" && !e$6(target) && URL_SCHEME.test(target);
11036
+ var isUrlTarget = (target) => typeof target === "string" && !e$7(target) && URL_SCHEME.test(target);
10994
11037
  var isRoot = (element) => !element.definition.parentId;
10995
11038
  var stepsOf$1 = (element) => Object.values(element.definition.interactions ?? {});
10996
11039
  /**
@@ -11001,7 +11044,7 @@ var settleUnknownKeys = (record, known, what, on, report, defaults = {}) => {
11001
11044
  for (const key of Object.keys(record)) {
11002
11045
  if (known.includes(key)) continue;
11003
11046
  const wrapped = record[key];
11004
- if (e$32(wrapped) && Object.keys(wrapped).length > 0 && Object.keys(wrapped).every((inner) => known.includes(inner))) {
11047
+ if (e$33(wrapped) && Object.keys(wrapped).length > 0 && Object.keys(wrapped).every((inner) => known.includes(inner))) {
11005
11048
  const inners = Object.keys(wrapped);
11006
11049
  if (inners.every((inner) => record[inner] === void 0 || record[inner] === defaults[inner])) {
11007
11050
  Object.assign(record, wrapped);
@@ -11282,7 +11325,7 @@ t$4.map((e) => {
11282
11325
  fieldOf: (e) => t.get(e)
11283
11326
  };
11284
11327
  });
11285
- Object.fromEntries(e$29.map((e) => [e, t$26(e)]));
11328
+ Object.fromEntries(e$30.map((e) => [e, t$26(e)]));
11286
11329
  /** Every kebab-case CSS property Plitzi's style engine understands. Values written to a definition must use these
11287
11330
  * exact keys — camelCase or unknown keys are rejected. */
11288
11331
  var cssProperties = Array.from(new Set(Object.values(t$30))).sort();
@@ -12067,7 +12110,7 @@ var byProperty = (rules) => {
12067
12110
  return responsive;
12068
12111
  };
12069
12112
  /** The states a selector can react to — the closed list the style editor offers, read from where it is declared. */
12070
- var STYLE_STATES = e$25;
12113
+ var STYLE_STATES = e$26;
12071
12114
  var STYLE_STATE_SET = new Set(STYLE_STATES);
12072
12115
  var RULE_SET_KEYS = /* @__PURE__ */ new Set([
12073
12116
  "css",
@@ -12094,7 +12137,7 @@ var toStates = (states) => {
12094
12137
  };
12095
12138
  var CLASS_NAME = /^-?[_a-zA-Z][\w-]*$/;
12096
12139
  var toAncestors = (ancestors) => Object.entries(ancestors ?? {}).map(([name, ancestor]) => {
12097
- if (!p$6(name) && !CLASS_NAME.test(name)) throw new AuthoringError("ancestor-not-class", `The ancestor "${name}" is not a class name. An ancestor condition is keyed by a class that ancestor wears — \`[card.name]\` for a \`styles()\` declaration — or by \`'>'\` for the parent, whatever it wears.`);
12140
+ if (!p$7(name) && !CLASS_NAME.test(name)) throw new AuthoringError("ancestor-not-class", `The ancestor "${name}" is not a class name. An ancestor condition is keyed by a class that ancestor wears — \`[card.name]\` for a \`styles()\` declaration — or by \`'>'\` for the parent, whatever it wears.`);
12098
12141
  return [name, toBlocks({
12099
12142
  css: ancestor.css,
12100
12143
  states: ancestor.states,
@@ -12131,11 +12174,11 @@ var assertPseudo = (pseudo, part) => {
12131
12174
  const ruleSets = partRuleSets(part);
12132
12175
  for (const rules of ruleSets) for (const [property, value] of Object.entries(rules)) {
12133
12176
  if (property === "content") {
12134
- if (!i$16.includes(pseudo)) throw new AuthoringError("style-pseudo-content", `\`content\` on \`::${pseudo}\` draws nothing: only ${i$16.map((name) => `\`::${name}\``).join(", ")} take one.`);
12135
- if (!m$3(String(value))) throw new AuthoringError("style-pseudo-content", `\`content: ${String(value)}\` on \`::${pseudo}\` is not text CSS can read, so nothing is drawn. Text goes in quotes inside the string — \`content: '"${String(value).replace(/"/g, "\\\"")}"'\` — and an empty box is \`'""'\`; \`counter()\`, \`attr()\` and \`url()\` are written as they are.`);
12177
+ if (!i$17.includes(pseudo)) throw new AuthoringError("style-pseudo-content", `\`content\` on \`::${pseudo}\` draws nothing: only ${i$17.map((name) => `\`::${name}\``).join(", ")} take one.`);
12178
+ if (!m$4(String(value))) throw new AuthoringError("style-pseudo-content", `\`content: ${String(value)}\` on \`::${pseudo}\` is not text CSS can read, so nothing is drawn. Text goes in quotes inside the string — \`content: '"${String(value).replace(/"/g, "\\\"")}"'\` — and an empty box is \`'""'\`; \`counter()\`, \`attr()\` and \`url()\` are written as they are.`);
12136
12179
  continue;
12137
12180
  }
12138
- if (!u$7(pseudo, property)) throw new AuthoringError("style-pseudo-property", `\`${property}\` on \`::${pseudo}\` is dropped by every browser — \`::${pseudo}\` honours ${(l$8[pseudo] ?? []).map((name) => `\`${name}\``).join(", ")}. Style the element itself for the rest.`);
12181
+ if (!u$8(pseudo, property)) throw new AuthoringError("style-pseudo-property", `\`${property}\` on \`::${pseudo}\` is dropped by every browser — \`::${pseudo}\` honours ${(l$8[pseudo] ?? []).map((name) => `\`${name}\``).join(", ")}. Style the element itself for the rest.`);
12139
12182
  }
12140
12183
  if (a$14.includes(pseudo) && !ruleSets.some((rules) => Object.hasOwn(rules, "content"))) throw new AuthoringError("style-pseudo-content", `\`::${pseudo}\` has no \`content\`, so the browser draws nothing for it. Give it one — \`content: '""'\` for an empty box — in its rules or in a state's.`);
12141
12184
  };
@@ -12143,7 +12186,7 @@ var toPseudos = (pseudos) => {
12143
12186
  const parts = /* @__PURE__ */ new Map();
12144
12187
  for (const [name, spec] of Object.entries(pseudos ?? {})) {
12145
12188
  const bare = name.replace(/^:+/, "");
12146
- if (!n$19(name)) throw new AuthoringError("style-pseudo-unknown", `Unknown pseudo-element "${name}"${n$19(bare) ? ` — write it without the colons, "${bare}"` : didYouMean(bare, e$26)}. A class dresses ${e$26.join(", ")}.`);
12189
+ if (!n$19(name)) throw new AuthoringError("style-pseudo-unknown", `Unknown pseudo-element "${name}"${n$19(bare) ? ` — write it without the colons, "${bare}"` : didYouMean(bare, e$27)}. A class dresses ${e$27.join(", ")}.`);
12147
12190
  const part = partOf(spec);
12148
12191
  const rules = {
12149
12192
  base: toResponsive(part.css),
@@ -13769,7 +13812,7 @@ var createTw = ({ colors = {} } = {}) => (classes) => {
13769
13812
  return rules;
13770
13813
  };
13771
13814
  createTw();
13772
- [...new Set(STYLE_STATES.filter((state) => state !== "hidden"))].filter((state) => u$6(state) !== `:${state}`);
13815
+ [...new Set(STYLE_STATES.filter((state) => state !== "hidden"))].filter((state) => u$7(state) !== `:${state}`);
13773
13816
  new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await element styles".split(" "));
13774
13817
  new Set(STYLE_STATES);
13775
13818
  new Set([
@@ -14305,6 +14348,33 @@ var footerRow = styles("footer-row", { css: {
14305
14348
  gap: "8px"
14306
14349
  }
14307
14350
  } });
14351
+ container({
14352
+ id: "not-found-main",
14353
+ subType: "main",
14354
+ class: [shell$1, heroBand],
14355
+ children: [
14356
+ text$1("404", {
14357
+ id: "not-found-kicker",
14358
+ class: kicker
14359
+ }),
14360
+ heading("Page not found", {
14361
+ id: "not-found-title",
14362
+ subType: "h1",
14363
+ class: sectionTitle
14364
+ }),
14365
+ paragraph({
14366
+ id: "not-found-lede",
14367
+ class: bodyText,
14368
+ content: "There is nothing at this address — it may have moved, or the link has a typo."
14369
+ }),
14370
+ link({
14371
+ id: "not-found-home",
14372
+ href: "/",
14373
+ class: button,
14374
+ content: "Back to the home page"
14375
+ })
14376
+ ]
14377
+ });
14308
14378
  /**
14309
14379
  * The space, declared: this file is the page, and it assembles the rest — the palette in `tokens.ts`, the classes in
14310
14380
  * `theme.ts`, what the page says in `content.ts`. Everything else — element ids, style selector names, the
@@ -14614,6 +14684,17 @@ container({
14614
14684
  })]
14615
14685
  })]
14616
14686
  });
14687
+ heading("Page not found", {
14688
+ id: "not-found-title",
14689
+ subType: "h1"
14690
+ }), paragraph({
14691
+ id: "not-found-lede",
14692
+ content: "There is nothing at this address."
14693
+ }), link({
14694
+ id: "not-found-home",
14695
+ href: "/",
14696
+ content: "Back to the home page"
14697
+ });
14617
14698
  /**
14618
14699
  * The space, declared — and nothing in it yet but what every space starts from: tokens for both themes, a layout the
14619
14700
  * pages render in, and one page.
@@ -14914,7 +14995,7 @@ var V$1 = (e) => {
14914
14995
  for (let n of e.filter((e) => x$2.test(e))) t.set(M$1(n), [...t.get(M$1(n)) ?? [], n]);
14915
14996
  return [...t].flatMap(([e, t]) => t.slice(1).map((n) => y$1("plugin-type-taken", `${o$1$1}/${n}/`, `${o$1$1}/${t[0]}/ and ${o$1$1}/${n}/ are both the plugin ${e}: the server registers a folder by its name with a small first letter, and keeps one.`, `Put them in one folder, or rename one: git mv ${o$1$1}/${n} ${o$1$1}/<AnotherName>`)));
14916
14997
  };
14917
- var H$1 = (t) => e$32(t.assets) ? Object.values(t.assets).flatMap((t) => e$32(t) && typeof t.src == "string" && typeof t.type == "string" ? [{
14998
+ var H$1 = (t) => e$33(t.assets) ? Object.values(t.assets).flatMap((t) => e$33(t) && typeof t.src == "string" && typeof t.type == "string" ? [{
14918
14999
  src: t.src,
14919
15000
  type: t.type,
14920
15001
  isMain: t.isMain === !0
@@ -14928,7 +15009,7 @@ var U$1 = (t, n) => {
14928
15009
  } catch (e) {
14929
15010
  return { problem: y$1("vendor-manifest-invalid", i, `${i} is not JSON (${e instanceof Error ? e.message : String(e)}), so ${n} does not run.`, a) };
14930
15011
  }
14931
- if (!e$32(o)) return { problem: y$1("vendor-manifest-invalid", i, `${i} is not a manifest, so ${n} does not run.`, a) };
15012
+ if (!e$33(o)) return { problem: y$1("vendor-manifest-invalid", i, `${i} is not a manifest, so ${n} does not run.`, a) };
14932
15013
  let s = H$1(o), l = s.find((e) => e.type === "script" && e.isMain) ?? s.find((e) => e.type === "script");
14933
15014
  if (!l) return { problem: y$1("vendor-script-missing", i, `${i} names no script to run, so ${n} does not run.`, a) };
14934
15015
  if (!C$1(t, path.posix.join(r, l.src), "file")) return { problem: y$1("vendor-script-missing", i, `${i} names ${l.src} as the script to run, and ${r}/${l.src} is not there, so ${n} does not run.`, a) };
@@ -15052,7 +15133,7 @@ var ce$1 = (e, t) => {
15052
15133
  var Z$1 = (t) => {
15053
15134
  try {
15054
15135
  let n = JSON.parse(readFileSync(path.join(t, "package.json"), "utf-8"));
15055
- return e$32(n) && [n.dependencies, n.devDependencies].some((t) => e$32(t) && "@plitzi/sdk-server" in t);
15136
+ return e$33(n) && [n.dependencies, n.devDependencies].some((t) => e$33(t) && "@plitzi/sdk-server" in t);
15056
15137
  } catch {
15057
15138
  return !1;
15058
15139
  }
@@ -15175,7 +15256,7 @@ var publicData = (folder) => fileReader(folder, "/");
15175
15256
  * never serves (`dataDir`) — for `authorSpace`'s `serverData`: bindings are held to the file, and a provider reading
15176
15257
  * one from the browser is refused.
15177
15258
  */
15178
- var projectData = (folder) => fileReader(folder, e$8);
15259
+ var projectData = (folder) => fileReader(folder, e$9);
15179
15260
  /**
15180
15261
  * The declaration of every plugin under `folder` — `src/plugins/<Name>/declaration.ts`, its default export — for
15181
15262
  * `authorSpace`'s `plugins`: what each fires, answers and reads. Found by folder, as the server finds the plugins
@@ -15197,7 +15278,7 @@ var pluginDeclarations = async (folder) => {
15197
15278
  var manifestOf$1 = (file) => {
15198
15279
  try {
15199
15280
  const manifest = JSON.parse(readFileSync(file, "utf-8"));
15200
- return e$32(manifest) ? manifest : {};
15281
+ return e$33(manifest) ? manifest : {};
15201
15282
  } catch {
15202
15283
  return {};
15203
15284
  }
@@ -15208,9 +15289,17 @@ var manifestOf$1 = (file) => {
15208
15289
  */
15209
15290
  var builtPluginTypes = (folder) => existsSync(folder) ? readdirSync(folder, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
15210
15291
  const { pluginSchema } = manifestOf$1(path.join(folder, entry.name, d$1));
15211
- return [entry.name, ...e$32(pluginSchema) ? Object.keys(pluginSchema) : []];
15292
+ return [entry.name, ...e$33(pluginSchema) ? Object.keys(pluginSchema) : []];
15212
15293
  }) : [];
15213
15294
  /**
15295
+ * The stylesheets of the plugins under `folder` — every `.css` in a plugin's folder, at any depth — as their text, for
15296
+ * `authorSpace`'s `stylesheets`: the tokens a plugin's own CSS reads are tokens the space uses.
15297
+ */
15298
+ var pluginStylesheets = (folder) => existsSync(folder) ? readdirSync(folder, {
15299
+ recursive: true,
15300
+ withFileTypes: true
15301
+ }).filter((entry) => entry.isFile() && entry.name.endsWith(".css")).map((entry) => path.join(entry.parentPath, entry.name)).sort().map((file) => readFileSync(file, "utf-8")) : [];
15302
+ /**
15214
15303
  * `projectAuthoring` of the project at `root`, held to the same check: for a tool that works on a project from another
15215
15304
  * folder — the CLI, run anywhere inside one. A project's own code calls `projectAuthoring()`.
15216
15305
  */
@@ -15221,6 +15310,7 @@ var projectAuthoringAt = async (root) => {
15221
15310
  return {
15222
15311
  plugins: await pluginDeclarations(path.join(at, o$1$1)),
15223
15312
  pluginTypes: builtPluginTypes(path.join(at, u$2)),
15313
+ stylesheets: pluginStylesheets(path.join(at, o$1$1)),
15224
15314
  ...existsSync(serverData) ? { serverData: projectData(serverData) } : {},
15225
15315
  data: publicData(path.join(at, p$1))
15226
15316
  };
@@ -15268,7 +15358,7 @@ var importProjectModule = async (file, root) => {
15268
15358
  * What the module exports as `space`, taken as a declaration when it has the shape of one. `authorSpace` checks the
15269
15359
  * rest of it, field by field, and says what is wrong — so this only has to tell a space from anything else.
15270
15360
  */
15271
- var isSpaceSpec = (value) => e$32(value) && typeof value.name === "string" && typeof value.permanentUrl === "string" && Array.isArray(value.pages);
15361
+ var isSpaceSpec = (value) => e$33(value) && typeof value.name === "string" && typeof value.permanentUrl === "string" && Array.isArray(value.pages);
15272
15362
  /**
15273
15363
  * `projectSpace` of the project at `root`, held to the same checks: for a tool that works on a project from another
15274
15364
  * folder — the CLI, run anywhere inside one. A project's own code calls `projectSpace()` or `authorProjectSpace()`.
@@ -15276,7 +15366,7 @@ var isSpaceSpec = (value) => e$32(value) && typeof value.name === "string" && ty
15276
15366
  var projectSpaceAt = async (root) => {
15277
15367
  const authoring = await projectAuthoringAt(root);
15278
15368
  const module = await importProjectModule(l$2(root, a$1, process.argv[1]), root);
15279
- const space = e$32(module) ? module.space : void 0;
15369
+ const space = e$33(module) ? module.space : void 0;
15280
15370
  if (!isSpaceSpec(space)) throw new ProjectSpaceError(a$1);
15281
15371
  return {
15282
15372
  space,
@@ -16227,7 +16317,8 @@ var unifiedDiff = (file, before, after, context = 2) => {
16227
16317
  * is left to the author, said with the command that would take the registry's.
16228
16318
  * - `skills`: `.claude/skills/plitzi-*` from the packages installed, each replaced whole.
16229
16319
  * - `renames`: a name a version renamed, still written in the source (`canTemplate`), at its file and line — and with
16230
- * `--write`, renamed.
16320
+ * `--write`, renamed. With it, what a version changed that the source may count on — a `formControl` written
16321
+ * before fields became optional — at its file and line, for a project last upgraded before: said, never written.
16231
16322
  *
16232
16323
  * Shown by default; `--write` makes it. What the space is — its pages, plugins, data — is the project's, never named.
16233
16324
  *
@@ -16288,6 +16379,42 @@ var renamesIn = (file, text) => RENAMES.filter((rename) => !rename.from || impor
16288
16379
  since: rename.since,
16289
16380
  what: rename.what
16290
16381
  }] : []));
16382
+ /** Before this version a `formControl` was required unless it said `required: false`; from it, optional unless `true`. */
16383
+ var OPTIONAL_FIELDS_SINCE = "0.38.9";
16384
+ /** The text of the call whose `(` is at `open`, up to its `)` — strings skipped, so a `)` inside one closes nothing. */
16385
+ var callArguments = (text, open) => {
16386
+ let depth = 0;
16387
+ let quote;
16388
+ for (let at = open; at < text.length; at++) {
16389
+ const char = text[at];
16390
+ if (quote && char === "\\") {
16391
+ at++;
16392
+ continue;
16393
+ }
16394
+ if (quote) {
16395
+ quote = char === quote ? void 0 : quote;
16396
+ continue;
16397
+ }
16398
+ if (/['"`]/.test(char)) quote = char;
16399
+ else if (char === "(") depth++;
16400
+ else if (char === ")" && --depth === 0) return text.slice(open + 1, at);
16401
+ }
16402
+ return text.slice(open + 1);
16403
+ };
16404
+ /**
16405
+ * A `formControl(…)` that writes no `required`: one that, written before fields became optional, was required, and now
16406
+ * takes an empty answer. Said, never written — whether a field must be answered is the space's to say. Found as written
16407
+ * in the call: one built by a helper of the project's own is the helper's to look at.
16408
+ */
16409
+ var optionalFieldsIn = (file, text) => [...text.matchAll(/\bformControl\(/g)].flatMap((match) => {
16410
+ const open = match.index + match[0].length - 1;
16411
+ return /\brequired\s*:/.test(callArguments(text, open)) ? [] : [{
16412
+ file,
16413
+ line: text.slice(0, match.index).split("\n").length,
16414
+ since: OPTIONAL_FIELDS_SINCE,
16415
+ what: "a formControl with no `required` is optional now — write `required: true` if it must be answered"
16416
+ }];
16417
+ });
16291
16418
  /** The lowest version a range allows, as numbers: `^0.38.2` → [0, 38, 2]. */
16292
16419
  var floorOf$1 = (range) => {
16293
16420
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(range);
@@ -16384,8 +16511,20 @@ var readText$1 = async (file) => {
16384
16511
  return;
16385
16512
  }
16386
16513
  };
16514
+ var holdsFiles = async (folder) => {
16515
+ try {
16516
+ return (await fs.readdir(folder)).length > 0;
16517
+ } catch {
16518
+ return false;
16519
+ }
16520
+ };
16387
16521
  var planFiles = async (root, files, recorded, take) => Promise.all(Object.entries(files).map(async ([file, ours]) => {
16388
16522
  const yours = await readText$1(path.join(root, file));
16523
+ if (yours === void 0 && path.basename(file) === ".gitkeep" && await holdsFiles(path.join(root, file, ".."))) return {
16524
+ file,
16525
+ status: "current",
16526
+ ours
16527
+ };
16389
16528
  if (yours === void 0) return {
16390
16529
  file,
16391
16530
  status: "added",
@@ -16544,6 +16683,10 @@ var reportText$3 = (report, install) => {
16544
16683
  lines.push(report.renames.length === 0 ? chalk.green("renames: none written") : "renames:");
16545
16684
  lines.push(...report.renames.map((found) => chalk.yellow(` ${report.write ? "~" : "!"} ${found.file}:${String(found.line)} ${found.name} → ${found.to} (${found.since}: ${found.what})`)));
16546
16685
  }
16686
+ if (report.changes && report.changes.length > 0) {
16687
+ lines.push("changes the source may count on (look, then decide — never written):");
16688
+ lines.push(...report.changes.map((found) => chalk.yellow(` ! ${found.file}:${String(found.line)} (${found.since}) ${found.what}`)));
16689
+ }
16547
16690
  return lines.join("\n");
16548
16691
  };
16549
16692
  var upgrade = async (parts, options) => {
@@ -16640,8 +16783,11 @@ var upgrade = async (parts, options) => {
16640
16783
  }
16641
16784
  if (wanted.has("renames")) {
16642
16785
  const found = [];
16786
+ const changes = [];
16787
+ const before = record === void 0 || isBelow(record.cli, OPTIONAL_FIELDS_SINCE);
16643
16788
  for (const file of await sourceFiles(root)) {
16644
16789
  const text = await readText$1(path.join(root, file));
16790
+ if (before && text !== void 0) changes.push(...optionalFieldsIn(file, text));
16645
16791
  const inFile = text === void 0 ? [] : renamesIn(file, text);
16646
16792
  if (write && text !== void 0 && inFile.length > 0) {
16647
16793
  const renamed = [...new Set(inFile.map((entry) => entry.name))].reduce((result, name) => result.replace(wordOf(name), RENAMES.find((rename) => rename.name === name)?.to ?? name), text);
@@ -16650,6 +16796,7 @@ var upgrade = async (parts, options) => {
16650
16796
  found.push(...inFile);
16651
16797
  }
16652
16798
  report.renames = found;
16799
+ report.changes = changes;
16653
16800
  }
16654
16801
  console.log(options.json ? JSON.stringify(report) : reportText$3(report, installCommand(manager)));
16655
16802
  if (report.packages?.install === "failed") {
@@ -18285,20 +18432,69 @@ var move$1 = async (elementId, options) => {
18285
18432
  ].join("\n"));
18286
18433
  };
18287
18434
  //#endregion
18435
+ //#region src/commands/projectSettings.ts
18436
+ /**
18437
+ * What the project's processes start with: what its `.env` sets, under what the environment already has — as its
18438
+ * scripts read it (`--env-file-if-exists=.env`). Read, never loaded into this process's own environment.
18439
+ */
18440
+ var projectSettings = async (root) => {
18441
+ const text = await fs.readFile(path.join(root, ".env"), "utf-8").catch(() => void 0);
18442
+ const parsed = text === void 0 ? {} : parseEnv(text);
18443
+ return {
18444
+ file: text !== void 0,
18445
+ value: (name) => process.env[name] ?? parsed[name] ?? ""
18446
+ };
18447
+ };
18448
+ //#endregion
18288
18449
  //#region src/commands/check.ts
18289
- /** Where `--as` reads the account's password: the environment, never the command line, which a shell keeps. */
18450
+ /** The account `verify` signs in as for the pages that send a visitor to sign in: the project's, in `.env`. */
18451
+ var CHECK_USER_ENV = "PLITZI_CHECK_USER";
18452
+ /** Where the account's password is read: `.env` or the environment, never the command line, which a shell keeps. */
18290
18453
  var CHECK_PASSWORD_ENV = "PLITZI_CHECK_PASSWORD";
18291
18454
  /** The project's sign-in route: where `createServer({ auth })` mounts its flows unless told otherwise. */
18292
18455
  var SIGN_IN_PATH = "/auth/login";
18456
+ /**
18457
+ * The account a check signs in as: the one named, or the project's own (`PLITZI_CHECK_USER`) — `undefined` when
18458
+ * neither is — with its password from `PLITZI_CHECK_PASSWORD`, each read from the environment over `.env`, as the
18459
+ * project's scripts read them.
18460
+ */
18461
+ var checkAccount = async (root, username) => {
18462
+ const settings = await projectSettings(root);
18463
+ const named = username ?? settings.value("PLITZI_CHECK_USER");
18464
+ if (!named) return;
18465
+ const password = settings.value(CHECK_PASSWORD_ENV);
18466
+ if (!password) return { problem: `${named} signs in with the password in ${CHECK_PASSWORD_ENV}: set it in .env — never on the command line, which a shell keeps` };
18467
+ return {
18468
+ username: named,
18469
+ password
18470
+ };
18471
+ };
18472
+ /** Signs the page's browser in as the account, through the server's `/auth` routes: what went wrong, or nothing. */
18473
+ var signInAs = async (page, origin, account) => {
18474
+ const answer = await page.request.post(`${origin}${SIGN_IN_PATH}`, { data: {
18475
+ username: account.username,
18476
+ password: account.password
18477
+ } });
18478
+ if (answer.ok()) return;
18479
+ return answer.status() === 404 ? `nothing answers at ${SIGN_IN_PATH}: --as signs in through the routes createServer({ auth }) mounts there` : `${account.username} was not signed in (${String(answer.status())}): ${(await answer.text()).slice(0, 200)} — check ${CHECK_PASSWORD_ENV} in .env`;
18480
+ };
18481
+ var withoutTrailingSlash = (path) => path.length > 1 ? path.replace(/\/+$/, "") : path;
18482
+ /** Where the page landed, when that is not the path it was opened at — sent to the sign-in, most often. */
18483
+ var sentElsewhere = (page, pathname) => {
18484
+ const landed = new URL(page.url()).pathname;
18485
+ const asked = new URL(pathname, page.url()).pathname;
18486
+ return withoutTrailingSlash(landed) === withoutTrailingSlash(asked) ? void 0 : landed;
18487
+ };
18293
18488
  /** The space the project declares, authored. Only for one written here. */
18294
18489
  var projectSpace = async (root) => {
18295
18490
  const project = await loadProjectSpace(root);
18296
18491
  if ("problem" in project) return { problem: `${project.problem} There is nothing to check the page against.` };
18297
18492
  try {
18298
- const { handles, schema } = authorSpace(project.space, project.authoring);
18493
+ const { handles, schema, style } = authorSpace(project.space, project.authoring);
18299
18494
  return {
18300
18495
  handles,
18301
- schema
18496
+ schema,
18497
+ schemes: SCHEMES.filter((scheme) => style.theme.schemes.includes(scheme))
18302
18498
  };
18303
18499
  } catch (error) {
18304
18500
  return { problem: `The space does not author — npm run author says why: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
@@ -18310,12 +18506,25 @@ var projectSpace = async (root) => {
18310
18506
  * (`/p/:slug`), so the one meant is read off the list rather than guessed from letters.
18311
18507
  */
18312
18508
  var noPageMessage = (handles, pathname) => `no page of the space answers at ${pathname}: its pages are ${Object.values(handles.pages).map((page) => page.path).join(", ")}`;
18509
+ /**
18510
+ * The space's "not found" page: a page whose slug is `*`, the page of every address under its folder that no other
18511
+ * page answers. Not every path ending in `*` — `update/*` is a page of its own that answers a whole subtree.
18512
+ */
18513
+ var isNotFoundPage = (page) => page.slug === "*";
18514
+ /** A path whose last segment is `*`: it answers every address under it. */
18515
+ var isSplat = (path) => path.endsWith("/*");
18516
+ /** Where `verify` looks at a page for an address nothing answers: an address under its folder no page has. */
18517
+ var NOT_FOUND_PROBE = "plitzi-check-not-found";
18518
+ /** The page an address is answered by — as the router picks: every page with a path of its own first, then the "not found" pages, the deepest folder's before the space's. */
18313
18519
  var pageFor = (handles, pathname) => {
18314
18520
  const wanted = pathname.replace(/\/$/, "").split("/");
18315
- return Object.values(handles.pages).find((page) => {
18316
- const segments = page.path.replace(/\/$/, "").split("/");
18317
- return segments.length === wanted.length && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
18318
- })?.id;
18521
+ const answers = (path) => {
18522
+ const splat = isSplat(path);
18523
+ const segments = (splat ? path.slice(0, -2) : path.replace(/\/$/, "")).split("/");
18524
+ return (splat ? wanted.length > segments.length : segments.length === wanted.length) && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
18525
+ };
18526
+ const pages = Object.values(handles.pages);
18527
+ return [...pages.filter((page) => !isNotFoundPage(page)), ...pages.filter(isNotFoundPage).sort((a, b) => b.path.length - a.path.length)].find((page) => answers(page.path))?.id;
18319
18528
  };
18320
18529
  /** The elements a piece of HTML carries, by the id each is marked with. */
18321
18530
  var markedIn = (html) => new Set([...html.matchAll(/data-plitzi-el="([^"]+)"/g)].map(([, id]) => id));
@@ -18701,24 +18910,25 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18701
18910
  devTools: false,
18702
18911
  lists: {}
18703
18912
  });
18704
- if (account) {
18705
- const signedIn = await page.request.post(`${origin}${SIGN_IN_PATH}`, { data: {
18706
- username: account.username,
18707
- password: account.password
18708
- } });
18709
- if (!signedIn.ok()) return unchecked({
18710
- code: "not-signed-in",
18711
- message: signedIn.status() === 404 ? `nothing answers at ${SIGN_IN_PATH}: --as signs in through the routes createServer({ auth }) mounts there` : `${account.username} was not signed in (${String(signedIn.status())}): ${(await signedIn.text()).slice(0, 200)} — check ${CHECK_PASSWORD_ENV}`,
18712
- width
18713
- });
18714
- }
18913
+ const notSignedIn = account && await signInAs(page, origin, account);
18914
+ if (notSignedIn) return unchecked({
18915
+ code: "not-signed-in",
18916
+ message: notSignedIn,
18917
+ width
18918
+ });
18715
18919
  const consoleErrors = [];
18716
18920
  const failedRequests = [];
18717
18921
  page.on("pageerror", (error) => consoleErrors.push(error.message));
18922
+ const address = `${origin}${pathname}`;
18718
18923
  page.on("console", (message) => {
18719
- if (message.type() === "error") consoleErrors.push(message.text());
18924
+ if (message.type() === "error" && message.location().url !== address) consoleErrors.push(message.text());
18720
18925
  });
18926
+ let sentWith;
18721
18927
  page.on("response", (response) => {
18928
+ if (sentWith === void 0 && response.url() === address) {
18929
+ sentWith = response.status();
18930
+ return;
18931
+ }
18722
18932
  if (response.status() >= 400 && response.url().startsWith(origin)) failedRequests.push(`${String(response.status())} ${response.url().slice(origin.length)}`);
18723
18933
  });
18724
18934
  if (handles && !pageFor(handles, pathname)) return {
@@ -18739,11 +18949,11 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18739
18949
  consoleErrors,
18740
18950
  failedRequests
18741
18951
  };
18742
- const landed = new URL(page.url()).pathname;
18743
- if (withoutTrailingSlash(landed) !== withoutTrailingSlash(new URL(`${origin}${pathname}`).pathname)) return {
18952
+ const landed = sentElsewhere(page, pathname);
18953
+ if (landed) return {
18744
18954
  ...unchecked({
18745
18955
  code: "redirected",
18746
- message: account ? `${pathname} sent the browser to ${landed}, signed in as ${account.username}: that account may not see it` : `${pathname} sent the browser to ${landed} — what a page for signed-in visitors does: \`plitzi page check ${pathname} --as <username>\` signs in first, the password in ${CHECK_PASSWORD_ENV}`,
18956
+ message: account ? `${pathname} sent the browser to ${landed}, signed in as ${account.username}: that account may not see it` : `${pathname} sent the browser to ${landed} — what a page for signed-in visitors does: \`plitzi page check ${pathname} --as <username>\` signs in first, the password in ${CHECK_PASSWORD_ENV} in .env`,
18747
18957
  width
18748
18958
  }),
18749
18959
  consoleErrors,
@@ -18773,8 +18983,18 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18773
18983
  ...flow.on === void 0 ? {} : { elementId: flow.on },
18774
18984
  width
18775
18985
  }));
18986
+ const statusIssues = (handles && pageId ? isNotFoundPage(handles.pages[pageId]) : false) && sentWith !== 404 ? [{
18987
+ code: "page-status",
18988
+ message: `${pathname} shows the page for an address nothing answers, sent with status ${String(sentWith)}: a crawler reads it as a page that exists — the server should send it with 404`,
18989
+ width
18990
+ }] : sentWith !== void 0 && sentWith >= 400 && sentWith !== 404 ? [{
18991
+ code: "page-status",
18992
+ message: `${pathname} was sent with status ${String(sentWith)}`,
18993
+ width
18994
+ }] : [];
18776
18995
  const issues = [
18777
18996
  ...noPage,
18997
+ ...statusIssues,
18778
18998
  ...report.issues.map((issue) => ({
18779
18999
  ...issue,
18780
19000
  width
@@ -18803,7 +19023,8 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18803
19023
  ...!asked.state || devTools.sources === void 0 ? {} : { sources: devTools.sources },
18804
19024
  ...devTools.element === void 0 ? {} : { element: devTools.element },
18805
19025
  ...styled === void 0 ? {} : { styles: styled },
18806
- ...clicked === void 0 ? {} : { clicked }
19026
+ ...clicked === void 0 ? {} : { clicked },
19027
+ ...sentWith === 404 ? { notFound: true } : {}
18807
19028
  };
18808
19029
  };
18809
19030
  /** What the page holds, when it was asked for: one compact line each, the shape of a source rather than its rows. */
@@ -18857,8 +19078,9 @@ var clickedText = (clicked) => {
18857
19078
  ];
18858
19079
  };
18859
19080
  var reportText$2 = (report, asked) => {
18860
- const head = `${report.path} at ${String(report.width)} px`;
19081
+ const head = `${report.path} at ${String(report.width)} px${report.scheme ? `, ${report.scheme}` : ""}`;
18861
19082
  const held = [
19083
+ ...report.notFound ? [" · sent with status 404: the page for an address that shows nothing"] : [],
18862
19084
  ...listsText(report.lists),
18863
19085
  ...browserOnlyText(report),
18864
19086
  ...heldText(report, asked),
@@ -18878,28 +19100,27 @@ var reportText$2 = (report, asked) => {
18878
19100
  ...held
18879
19101
  ].join("\n");
18880
19102
  };
18881
- var withoutTrailingSlash = (path) => path.length > 1 ? path.replace(/\/+$/, "") : path;
18882
- var check = async (route, options) => {
19103
+ var check = async (routes, options) => {
18883
19104
  const project = await projectHere("whose page to check");
18884
19105
  if (!project) return;
18885
- const password = process.env[CHECK_PASSWORD_ENV];
18886
- if (options.as && !password) {
18887
- fail(`--as ${options.as} signs in with the password in ${CHECK_PASSWORD_ENV}: set it — ${CHECK_PASSWORD_ENV}=… npx plitzi page check …`);
19106
+ const account = options.as === void 0 ? void 0 : await checkAccount(project.root, options.as);
19107
+ if (account && "problem" in account) {
19108
+ fail(`--as ${account.problem}.`);
18888
19109
  return;
18889
19110
  }
18890
19111
  if (options.fill && !options.click) {
18891
19112
  fail("--fill fills fields for a --click: with nothing clicked, nothing they hold is sent anywhere.");
18892
19113
  return;
18893
19114
  }
18894
- const account = options.as && password ? {
18895
- username: options.as,
18896
- password
18897
- } : void 0;
19115
+ if (options.click && routes.length > 1) {
19116
+ fail(`--click clicks one element of one page: ${String(routes.length)} were named.`);
19117
+ return;
19118
+ }
18898
19119
  const asked = {
18899
19120
  state: Boolean(options.state),
18900
19121
  ...options.element ? { element: options.element } : {}
18901
19122
  };
18902
- const reports = await checkRoutes(project, [route ?? "/"], {
19123
+ const reports = await checkRoutes(project, routes.length > 0 ? routes : ["/"], {
18903
19124
  widths: options.width ?? [1440, 390],
18904
19125
  asked,
18905
19126
  ssr: Boolean(options.ssr),
@@ -18932,17 +19153,26 @@ var checkRoutes = async (project, routes, options) => {
18932
19153
  if ("problem" in browser) return browser;
18933
19154
  try {
18934
19155
  const reports = [];
18935
- for (const route of routes) for (const width of options.widths) reports.push(await checkAt(browser, where.origin, route, width, options.scheme, authored, options.asked, options.ssr, options.account, options.click));
19156
+ for (const route of routes) for (const width of options.widths) {
19157
+ const report = await checkAt(browser, where.origin, route, width, options.scheme, authored, options.asked, options.ssr, options.account, options.click);
19158
+ reports.push({
19159
+ ...report,
19160
+ ...options.scheme ? { scheme: options.scheme } : {}
19161
+ });
19162
+ }
18936
19163
  return reports;
18937
19164
  } finally {
18938
19165
  await browser.close();
18939
19166
  }
18940
19167
  };
18941
- /** The paths of the space's pages that take no parameter — the ones a check can open as they are. */
19168
+ /** What a whole-project check opens: every page with no parameter in its path, in every theme the space can show. */
18942
19169
  var staticPaths = async (root) => {
18943
19170
  const authored = await projectSpace(root);
18944
19171
  if ("problem" in authored) return authored;
18945
- return Object.values(authored.handles.pages).map((page) => page.path).filter((pathname) => pathname.split("/").every((segment) => !segment.startsWith(":") && !/\{\{.*\}\}/.test(segment)));
19172
+ return {
19173
+ paths: Object.values(authored.handles.pages).map((page) => isNotFoundPage(page) ? `${page.path.slice(0, -1)}${NOT_FOUND_PROBE}` : page.path).filter((pathname) => pathname.split("/").every((segment) => !segment.startsWith(":") && !/\{\{.*\}\}/.test(segment))),
19174
+ schemes: authored.schemes
19175
+ };
18946
19176
  };
18947
19177
  //#endregion
18948
19178
  //#region src/commands/functions.ts
@@ -21056,12 +21286,12 @@ var measure = async (browser, url, width, scheme, read = []) => {
21056
21286
  screen: await page.screenshot({ fullPage: true })
21057
21287
  };
21058
21288
  };
21059
- var count$2 = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
21289
+ var count$3 = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
21060
21290
  var summaryText = (summary, out, stale) => [
21061
21291
  chalk.green(`Imported ${summary.url} into ${out}`),
21062
- ` tokens ${count$2(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count$2(summary.shadows, "shadow")}, ${count$2(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
21063
- ` outline ${count$2(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
21064
- ...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count$2(list.rows, "row")})`).join(", ")}`] : [],
21292
+ ` tokens ${count$3(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count$3(summary.shadows, "shadow")}, ${count$3(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
21293
+ ` outline ${count$3(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
21294
+ ...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count$3(list.rows, "row")})`).join(", ")}`] : [],
21065
21295
  ` pictures ${String(summary.pictures)} in assets.json`,
21066
21296
  ...stale.length > 0 ? [chalk.yellow(` earlier ${stale.join(", ")} ${stale.length === 1 ? "was" : "were"} there before and not written this time: delete what is stale`)] : [],
21067
21297
  ` next ${path.join(out, "IMPORT.md")} says what was not carried over`
@@ -22279,7 +22509,7 @@ var projectConnectors = async (root) => {
22279
22509
  manifest: value.manifest
22280
22510
  }] : []);
22281
22511
  };
22282
- var count$1 = (entries, noun) => entries ? `${String(entries.length)} ${noun}${entries.length === 1 ? "" : "s"}` : `the space’s ${noun}s as they are`;
22512
+ var count$2 = (entries, noun) => entries ? `${String(entries.length)} ${noun}${entries.length === 1 ? "" : "s"}` : `the space’s ${noun}s as they are`;
22283
22513
  /**
22284
22514
  * What of the space would not reach Plitzi with it, said before it is sent — never left to be found on a page that
22285
22515
  * renders empty there:
@@ -22364,7 +22594,7 @@ var pushSpaceOf = async (root, connection, space, { base, force, downloads }) =>
22364
22594
  outcome: "unchanged",
22365
22595
  draft: data.draft
22366
22596
  };
22367
- console.log(chalk.green(`The space is ${space.name}’s draft now — its pages and styles, ${count$1(sent.actions, "action")}, ${count$1(sent.connectors, "connector")}.`) + chalk.dim(" Any builder open on it shows it now."));
22597
+ console.log(chalk.green(`The space is ${space.name}’s draft now — its pages and styles, ${count$2(sent.actions, "action")}, ${count$2(sent.connectors, "connector")}.`) + chalk.dim(" Any builder open on it shows it now."));
22368
22598
  return {
22369
22599
  outcome: "pushed",
22370
22600
  draft: data.draft
@@ -23392,7 +23622,107 @@ var _ = `[data-motion-enter], [data-motion-stagger] > *, [data-motion-loop] { an
23392
23622
  "[data-motion-on=\"scroll\"][data-motion-enter], [data-motion-on=\"scroll\"][data-motion-stagger] > * { animation-timeline: auto, auto !important; }"
23393
23623
  ].join("\n");
23394
23624
  //#endregion
23625
+ //#region src/commands/shotSteps.ts
23626
+ var STEP_NAMES = [
23627
+ "click",
23628
+ "type",
23629
+ "press",
23630
+ "wait",
23631
+ "wait-for",
23632
+ "shot",
23633
+ "frames"
23634
+ ];
23635
+ /** The longest a script may wait in one step: a page that needs more is waiting for something else. */
23636
+ var MAX_WAIT_MS = 3e4;
23637
+ var unquoted = (text) => text.replace(/^(["'])(.*)\1$/s, "$2");
23638
+ var count$1 = (value, at, max) => {
23639
+ const parsed = Number(value);
23640
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= max ? parsed : { problem: `${at}: "${value ?? ""}" is not a whole number from 1 to ${String(max)}` };
23641
+ };
23642
+ /** A script, read into its steps — or what is wrong with it, said by its step. */
23643
+ var parseSteps = (script, every) => {
23644
+ const lines = script.split(/[;\n]/).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
23645
+ if (lines.length === 0) return { problem: "--steps has no step: click, type, press, wait, wait-for, shot or frames, separated by \";\"" };
23646
+ const steps = [];
23647
+ for (const [index, line] of lines.entries()) {
23648
+ const at = `step ${String(index + 1)} ("${line}")`;
23649
+ const [name, ...rest] = line.split(/\s+/);
23650
+ const argument = unquoted(rest.join(" "));
23651
+ switch (name) {
23652
+ case "click":
23653
+ case "wait-for":
23654
+ if (!argument) return { problem: `${at}: ${name} needs an element — its name or a CSS selector` };
23655
+ steps.push({
23656
+ kind: name,
23657
+ target: argument
23658
+ });
23659
+ break;
23660
+ case "type": {
23661
+ const [target, ...words] = rest;
23662
+ if (!target || words.length === 0) return { problem: `${at}: type needs an element and the text — type <element> <text>` };
23663
+ steps.push({
23664
+ kind: "type",
23665
+ target,
23666
+ text: unquoted(words.join(" "))
23667
+ });
23668
+ break;
23669
+ }
23670
+ case "press":
23671
+ if (!argument) return { problem: `${at}: press needs a key — Enter, Escape, ArrowDown…` };
23672
+ steps.push({
23673
+ kind: "press",
23674
+ key: argument
23675
+ });
23676
+ break;
23677
+ case "wait": {
23678
+ const ms = count$1(rest[0], at, MAX_WAIT_MS);
23679
+ if (typeof ms !== "number") return ms;
23680
+ steps.push({
23681
+ kind: "wait",
23682
+ ms
23683
+ });
23684
+ break;
23685
+ }
23686
+ case "shot":
23687
+ steps.push({
23688
+ kind: "shot",
23689
+ ...argument ? { label: argument } : {}
23690
+ });
23691
+ break;
23692
+ case "frames": {
23693
+ const frames = count$1(rest[0], at, 60);
23694
+ if (typeof frames !== "number") return frames;
23695
+ const apart = rest.length < 2 ? every : count$1(rest[1], at, MAX_WAIT_MS);
23696
+ if (typeof apart !== "number") return apart;
23697
+ steps.push({
23698
+ kind: "frames",
23699
+ count: frames,
23700
+ every: apart
23701
+ });
23702
+ break;
23703
+ }
23704
+ default: return { problem: `${at}: "${name}" is not a step — the steps are ${STEP_NAMES.join(", ")}` };
23705
+ }
23706
+ }
23707
+ return steps;
23708
+ };
23709
+ //#endregion
23395
23710
  //#region src/commands/shot.ts
23711
+ /**
23712
+ * `plitzi page shot`: a picture of one page of the project — and, asked, the numbers that say what a picture would.
23713
+ *
23714
+ * plitzi page shot /about --width 390 --scheme dark
23715
+ * plitzi page shot / --compare https://example.com --width 1440 # beside another site: what differs, and how
23716
+ * plitzi page shot / --frames 4 --every 500 # what moves: a marquee, an autoplay
23717
+ * plitzi page shot / --clip pricing-table # one element, scrolled to wherever it is
23718
+ * plitzi page shot / --scroll-to faq --viewport --frames 6 # the screen at that section, as it animates
23719
+ *
23720
+ * The project's server has to be running (`npm start`). `--json` answers in one object, for a tool or an agent. The
23721
+ * dev tools' badge is never in the picture; a picture of the whole page has every lazy image loaded and shows every
23722
+ * arrival that waits for the scroll as it ends — taken without scrolling, they would be holes where the sections are.
23723
+ */
23724
+ /** When a run's first picture is taken: once the page settles, or as soon as its HTML is in. */
23725
+ var SHOT_MOMENTS = ["settled", "load"];
23396
23726
  /** A region moves when more than this share of it changed between two frames: below is anti-aliasing and carets. */
23397
23727
  var MOVING_PERCENT = .5;
23398
23728
  /** How long a whole page waits for its pictures before it is taken anyway. */
@@ -23403,8 +23733,8 @@ var TEXTS_SHOWN = 12;
23403
23733
  var DRIFT_PX = 2;
23404
23734
  /** An arrival that waits for the scroll and has not had it: one waiting to be seen, or one tied to the scroll. */
23405
23735
  var WAITING_FOR_SCROLL = "[data-motion-on=\"view\"]:not([data-motion-seen]), [data-motion-on=\"scroll\"]";
23406
- /** A selector, or an element's name — what `data-plitzi-el` carries. */
23407
- var selectorOf = (target) => /^[#.[]/.test(target) ? target : `[data-plitzi-el="${target}"]`;
23736
+ /** A selector — CSS, or Playwright's own (`text=Saved`, `role=button[name="Publish"]`) — or an element's name. */
23737
+ var selectorOf = (target) => /^([#.[]|(text|role|css|xpath)=)/.test(target) ? target : `[data-plitzi-el="${target}"]`;
23408
23738
  var framing = (view) => {
23409
23739
  if (view.clip) return {
23410
23740
  kind: "element",
@@ -23429,7 +23759,10 @@ var pictureOf = async (browser, url, view, origin) => {
23429
23759
  colorScheme: view.scheme ?? "light",
23430
23760
  reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
23431
23761
  });
23432
- if (!await openPage(page, url)) return { problem: `Nothing answers at ${url}.` };
23762
+ const notSignedIn = origin && view.account && await signInAs(page, origin, view.account);
23763
+ if (notSignedIn) return { problem: `--as ${notSignedIn}.` };
23764
+ if (!(view.fromLoad ? await page.goto(url, { waitUntil: "domcontentloaded" }).catch(() => null) : await openPage(page, url))) return { problem: `Nothing answers at ${url}.${origin ? " Start the project first: npm start" : ""}` };
23765
+ const sentTo = origin ? sentElsewhere(page, new URL(url).pathname) : void 0;
23433
23766
  if (view.waitFor) {
23434
23767
  if (!await page.waitForSelector(selectorOf(view.waitFor), { timeout: 1e4 }).catch(() => null)) return { problem: `${view.waitFor} did not appear on ${url} within 10 s.` };
23435
23768
  }
@@ -23446,7 +23779,8 @@ var pictureOf = async (browser, url, view, origin) => {
23446
23779
  png: await capture(page, framing(view)),
23447
23780
  scheme: await paintedScheme(page, view.scheme ?? "light"),
23448
23781
  settled,
23449
- unloaded: pictures?.missing ?? 0
23782
+ unloaded: pictures?.missing ?? 0,
23783
+ ...sentTo ? { sentTo } : {}
23450
23784
  };
23451
23785
  };
23452
23786
  var shiftText = (shift) => shift !== void 0 && Math.abs(shift) > DRIFT_PX ? ` (${shift > 0 ? "+" : ""}${String(shift)}px there)` : "";
@@ -23510,6 +23844,141 @@ var compareWith = async (browser, taken, target, view, regions, base) => {
23510
23844
  diff: `${base}-diff.png`
23511
23845
  };
23512
23846
  };
23847
+ /**
23848
+ * The pictures on one sheet, each under what it shows, four to a row and each 360 px wide: runs in the page, so it is
23849
+ * self-contained — no import, nothing it closes over.
23850
+ */
23851
+ function contactSheet(cells) {
23852
+ const CELL_WIDTH = 360;
23853
+ const LABEL = 24;
23854
+ const GAP = 12;
23855
+ const load = (src) => new Promise((resolve, reject) => {
23856
+ const image = new Image();
23857
+ image.onload = () => resolve(image);
23858
+ image.onerror = () => reject(/* @__PURE__ */ new Error("a picture of the sheet did not load"));
23859
+ image.src = src;
23860
+ });
23861
+ return Promise.all(cells.map((cell) => load(cell.src))).then((images) => {
23862
+ const columns = Math.min(4, images.length);
23863
+ const heightOf = (image) => Math.round(image.naturalHeight * CELL_WIDTH / image.naturalWidth);
23864
+ const cellHeight = Math.max(...images.map(heightOf));
23865
+ const rows = Math.ceil(images.length / columns);
23866
+ const canvas = document.createElement("canvas");
23867
+ canvas.width = GAP + columns * 372;
23868
+ canvas.height = GAP + rows * (LABEL + cellHeight + GAP);
23869
+ const context = canvas.getContext("2d");
23870
+ if (!context) throw new Error("no canvas to draw the sheet on");
23871
+ context.fillStyle = "#ffffff";
23872
+ context.fillRect(0, 0, canvas.width, canvas.height);
23873
+ context.font = "13px ui-monospace, monospace";
23874
+ images.forEach((image, index) => {
23875
+ const x = GAP + index % columns * 372;
23876
+ const y = GAP + Math.floor(index / columns) * (LABEL + cellHeight + GAP);
23877
+ context.fillStyle = "#16181d";
23878
+ context.fillText(cells[index].label, x, y + 16);
23879
+ context.drawImage(image, x, y + LABEL, CELL_WIDTH, heightOf(image));
23880
+ context.strokeStyle = "#d0d4dc";
23881
+ context.strokeRect(x + .5, y + LABEL + .5, 359, heightOf(image) - 1);
23882
+ });
23883
+ return canvas.toDataURL("image/png");
23884
+ });
23885
+ }
23886
+ /**
23887
+ * Each element clicked in turn, and the page pictured after it — `frames` pictures `every` ms apart, or one. What a
23888
+ * click changed is measured against the picture before it: nothing at all is said, as a click that did nothing.
23889
+ */
23890
+ var clickThrough = async (taken, how, clicks, { frames, every }) => {
23891
+ const moments = [{
23892
+ label: "before",
23893
+ png: taken.png
23894
+ }];
23895
+ const clicked = [];
23896
+ for (const target of clicks) {
23897
+ const element = taken.page.locator(selectorOf(target)).first();
23898
+ if (await element.count() === 0) return { problem: `${target} is not on the page: --click takes an element's name or a CSS selector.` };
23899
+ const before = moments[moments.length - 1].png;
23900
+ try {
23901
+ await element.click({ timeout: 3e3 });
23902
+ } catch (error) {
23903
+ return { problem: `${target} could not be clicked — hidden, covered or disabled: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
23904
+ }
23905
+ for (let frame = 1; frame <= Math.max(1, frames); frame += 1) {
23906
+ await taken.page.waitForTimeout(every);
23907
+ moments.push({
23908
+ label: `${target} +${String(frame * every)} ms`,
23909
+ png: await capture(taken.page, how)
23910
+ });
23911
+ }
23912
+ const after = moments[moments.length - 1].png;
23913
+ clicked.push({
23914
+ target,
23915
+ changed: (await comparePictures(taken.page, dataUrl(before), dataUrl(after))).changed
23916
+ });
23917
+ }
23918
+ return {
23919
+ moments,
23920
+ clicked
23921
+ };
23922
+ };
23923
+ /** What a step is, as its label says it. */
23924
+ var stepText = (step) => {
23925
+ switch (step.kind) {
23926
+ case "click":
23927
+ case "wait-for": return `${step.kind} ${step.target}`;
23928
+ case "type": return `type ${step.target}`;
23929
+ case "press": return `press ${step.key}`;
23930
+ case "wait": return `wait ${String(step.ms)} ms`;
23931
+ case "shot": return step.label ?? "shot";
23932
+ case "frames": return `frames ${String(step.count)}`;
23933
+ }
23934
+ };
23935
+ /**
23936
+ * A `--steps` script played on the page: what each step does, and every picture the script asks for — each labelled with
23937
+ * its step and the time since the first, and measured against the one before it, so "nothing changed" is said.
23938
+ */
23939
+ var playSteps = async (taken, how, steps) => {
23940
+ const { page } = taken;
23941
+ const started = Date.now();
23942
+ const moments = [{
23943
+ label: "before",
23944
+ png: taken.png
23945
+ }];
23946
+ const changes = [];
23947
+ const picture = async (label) => {
23948
+ const before = moments[moments.length - 1].png;
23949
+ const png = await capture(page, how);
23950
+ const at = `${label} · +${String(Date.now() - started)} ms`;
23951
+ moments.push({
23952
+ label: at,
23953
+ png
23954
+ });
23955
+ changes.push({
23956
+ label: at,
23957
+ changed: (await comparePictures(page, dataUrl(before), dataUrl(png))).changed
23958
+ });
23959
+ };
23960
+ for (const [index, step] of steps.entries()) try {
23961
+ if (step.kind === "click" || step.kind === "type") {
23962
+ const element = page.locator(selectorOf(step.target)).first();
23963
+ if (await element.count() === 0) return { problem: `Step ${String(index + 1)}: ${step.target} is not on the page — its name or a CSS selector.` };
23964
+ await element.click({ timeout: 3e3 });
23965
+ if (step.kind === "type") await page.keyboard.type(step.text);
23966
+ } else if (step.kind === "press") await page.keyboard.press(step.key);
23967
+ else if (step.kind === "wait") await page.waitForTimeout(step.ms);
23968
+ else if (step.kind === "wait-for") await page.waitForSelector(selectorOf(step.target), { timeout: 1e4 });
23969
+ else if (step.kind === "shot") await picture(step.label ?? `step ${String(index + 1)}`);
23970
+ else for (let frame = 1; frame <= step.count; frame += 1) {
23971
+ if (frame > 1) await page.waitForTimeout(step.every);
23972
+ await picture(`frame ${String(frame)}/${String(step.count)}`);
23973
+ }
23974
+ } catch (error) {
23975
+ return { problem: `Step ${String(index + 1)} (${stepText(step)}) did not happen: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
23976
+ }
23977
+ return {
23978
+ moments,
23979
+ changes
23980
+ };
23981
+ };
23513
23982
  var shot = async (route, options) => {
23514
23983
  const project = await projectHere("whose page to picture");
23515
23984
  if (!project) return;
@@ -23518,11 +23987,55 @@ var shot = async (route, options) => {
23518
23987
  fail(where.problem);
23519
23988
  return;
23520
23989
  }
23990
+ const account = options.as === void 0 ? void 0 : await checkAccount(project.root, options.as);
23991
+ if (account && "problem" in account) {
23992
+ fail(`--as ${account.problem}.`);
23993
+ return;
23994
+ }
23521
23995
  const browser = await launchBrowser(project.root);
23522
23996
  if ("problem" in browser) {
23523
23997
  fail(browser.problem);
23524
23998
  return;
23525
23999
  }
24000
+ const clicks = options.click ?? [];
24001
+ const script = options.steps?.startsWith("@") ? await readFile(path.resolve(options.steps.slice(1)), "utf-8").catch(() => void 0) : options.steps;
24002
+ if (options.steps !== void 0 && script === void 0) {
24003
+ await browser.close();
24004
+ fail(`${options.steps.slice(1)} cannot be read: --steps @<file> names a file of steps.`);
24005
+ return;
24006
+ }
24007
+ const steps = script === void 0 ? void 0 : parseSteps(script, options.every ?? 500);
24008
+ if (steps && "problem" in steps) {
24009
+ await browser.close();
24010
+ fail(`--steps: ${steps.problem}.`);
24011
+ return;
24012
+ }
24013
+ if (steps && (clicks.length > 0 || options.compare || (options.frames ?? 0) > 1)) {
24014
+ await browser.close();
24015
+ fail("`--steps` is the whole interaction — its own clicks and frames: leave out --click, --frames and --compare.");
24016
+ return;
24017
+ }
24018
+ if (options.compare && clicks.length > 0) {
24019
+ await browser.close();
24020
+ fail("`--compare` sets the page beside another site as it loads: leave out --click, or picture the click on its own.");
24021
+ return;
24022
+ }
24023
+ if (options.sheet && clicks.length === 0 && (options.frames ?? 0) <= 1 && options.steps === void 0) {
24024
+ await browser.close();
24025
+ fail("`--sheet` lays out the pictures a --click, --frames or --steps takes: with none of them, there is one picture and no sheet.");
24026
+ return;
24027
+ }
24028
+ const fromLoad = options.from === "load";
24029
+ if (fromLoad && ((options.frames ?? 0) <= 1 || clicks.length > 0 || options.compare)) {
24030
+ await browser.close();
24031
+ fail("`--from load` starts --frames as the HTML arrives, for what plays while the page loads: give it --frames, and leave out --click and --compare.");
24032
+ return;
24033
+ }
24034
+ if (fromLoad && !options.clip && !options.scrollTo && !options.viewport) {
24035
+ await browser.close();
24036
+ fail("`--from load` pictures the screen as the page arrives, never the whole page — unrolling it waits for it to settle: add --viewport or --clip.");
24037
+ return;
24038
+ }
23526
24039
  if (options.compare && (options.clip || options.scrollTo || options.viewport)) {
23527
24040
  await browser.close();
23528
24041
  fail("`--compare` sets the whole page beside the other site, section by section: leave out --clip, --scroll-to and --viewport.");
@@ -23539,12 +24052,14 @@ var shot = async (route, options) => {
23539
24052
  ...options.clip ? { clip: options.clip } : {},
23540
24053
  ...options.scrollTo ? { scrollTo: options.scrollTo } : {},
23541
24054
  ...options.viewport ? { viewport: true } : {},
23542
- still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1
24055
+ ...account ? { account } : {},
24056
+ ...fromLoad ? { fromLoad } : {},
24057
+ still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1 && clicks.length === 0
23543
24058
  };
23544
24059
  const how = framing(view);
23545
24060
  const taken = await pictureOf(browser, `${where.origin}${pathname}`, view, where.origin);
23546
24061
  if ("problem" in taken) {
23547
- fail(`${taken.problem} Start the project first: npm start`);
24062
+ fail(taken.problem);
23548
24063
  return;
23549
24064
  }
23550
24065
  const name = pathname === "/" ? "home" : pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");
@@ -23559,7 +24074,8 @@ var shot = async (route, options) => {
23559
24074
  scheme: taken.scheme,
23560
24075
  out,
23561
24076
  ...taken.settled > 0 ? { settled: taken.settled } : {},
23562
- ...taken.unloaded > 0 ? { unloaded: taken.unloaded } : {}
24077
+ ...taken.unloaded > 0 ? { unloaded: taken.unloaded } : {},
24078
+ ...taken.sentTo ? { sentTo: taken.sentTo } : {}
23563
24079
  };
23564
24080
  if (options.compare) {
23565
24081
  const other = new URL(options.compare);
@@ -23571,13 +24087,37 @@ var shot = async (route, options) => {
23571
24087
  report.compare = compared;
23572
24088
  }
23573
24089
  const frames = options.frames ?? 0;
23574
- if (frames > 1) {
23575
- const every = options.every ?? 500;
24090
+ const every = options.every ?? 500;
24091
+ let moments = [];
24092
+ if (steps) {
24093
+ const played = await playSteps(taken, how, steps);
24094
+ if ("problem" in played) {
24095
+ fail(played.problem);
24096
+ return;
24097
+ }
24098
+ moments = played.moments;
24099
+ report.steps = played.changes;
24100
+ } else if (clicks.length > 0) {
24101
+ const played = await clickThrough(taken, how, clicks, {
24102
+ frames,
24103
+ every
24104
+ });
24105
+ if ("problem" in played) {
24106
+ fail(played.problem);
24107
+ return;
24108
+ }
24109
+ moments = played.moments;
24110
+ report.clicked = played.clicked;
24111
+ } else if (frames > 1) {
23576
24112
  const pictures = [taken.png];
23577
24113
  for (let frame = 1; frame < frames; frame += 1) {
23578
24114
  await taken.page.waitForTimeout(every);
23579
24115
  pictures.push(await capture(taken.page, how));
23580
24116
  }
24117
+ moments = pictures.map((png, frame) => ({
24118
+ label: frame === 0 ? fromLoad ? "HTML in" : "before" : `+${String(frame * every)} ms`,
24119
+ png
24120
+ }));
23581
24121
  const diffs = [];
23582
24122
  for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), { regions }));
23583
24123
  const whole = how.kind === "element" ? options.clip ?? "element" : "viewport";
@@ -23592,14 +24132,37 @@ var shot = async (route, options) => {
23592
24132
  still: moved.filter((region) => region.changed <= MOVING_PERCENT).map((region) => region.name)
23593
24133
  };
23594
24134
  }
24135
+ if (moments.length > 1 && options.sheet) {
24136
+ const sheet = await taken.page.evaluate(contactSheet, moments.map((moment) => ({
24137
+ label: moment.label,
24138
+ src: dataUrl(moment.png)
24139
+ })));
24140
+ report.sheet = `${base}-sheet.png`;
24141
+ await writeFile(report.sheet, Buffer.from(sheet.replace(/^data:image\/png;base64,/, ""), "base64"));
24142
+ } else if (moments.length > 1 && (clicks.length > 0 || steps)) {
24143
+ report.pictures = [];
24144
+ for (const [index, moment] of moments.slice(1).entries()) {
24145
+ const file = `${base}-${String(index + 1)}.png`;
24146
+ await writeFile(file, moment.png);
24147
+ report.pictures.push({
24148
+ label: moment.label,
24149
+ out: file
24150
+ });
24151
+ }
24152
+ }
23595
24153
  if (options.json) {
23596
24154
  console.log(JSON.stringify(report));
23597
24155
  return;
23598
24156
  }
23599
24157
  console.log(out);
24158
+ if (report.sentTo) console.log(chalk.yellow(account ? `${pathname} sent the browser to ${report.sentTo}, signed in as ${account.username}: the picture is of ${report.sentTo} — that account may not see ${pathname}` : `${pathname} sent the browser to ${report.sentTo}: the picture is of ${report.sentTo}. A page for signed-in visitors? \`--as <username>\` signs in first, the password in ${CHECK_PASSWORD_ENV} in .env`));
23600
24159
  if (report.settled) console.log(chalk.dim(`${String(report.settled)} arrival${report.settled === 1 ? "" : "s"} waiting for the scroll shown as they end — --scroll-to <id> --viewport --frames 4 shows one arriving`));
23601
24160
  if (report.unloaded) console.log(chalk.yellow(`${String(report.unloaded)} picture(s) had not loaded when the page was taken`));
23602
24161
  if (report.compare) console.log(compareText(report.compare).join("\n"));
24162
+ for (const { label, changed } of report.steps ?? []) console.log(changed > 0 ? `${label}: ${String(changed)}% of the picture changed` : chalk.yellow(`${label}: nothing changed since the picture before`));
24163
+ for (const { target, changed } of report.clicked ?? []) console.log(changed > 0 ? `clicked ${target}: ${String(changed)}% of the picture changed` : chalk.yellow(`clicked ${target}: nothing changed — the click did nothing to see`));
24164
+ if (report.sheet) console.log(`sheet: ${report.sheet}`);
24165
+ for (const picture of report.pictures ?? []) console.log(`${picture.label}: ${picture.out}`);
23603
24166
  if (report.frames) console.log(report.frames.moving.length > 0 ? `moving over ${String(report.frames.count)} frames: ${regionsText(report.frames.moving)}` : `nothing moved over ${String(report.frames.count)} frames, ${String(report.frames.every)} ms apart`);
23604
24167
  } finally {
23605
24168
  await browser.close();
@@ -23664,8 +24227,10 @@ var scriptsOf = async (root) => {
23664
24227
  const scripts = e$3(parsed) && e$3(parsed.scripts) ? parsed.scripts : {};
23665
24228
  return Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string"));
23666
24229
  };
24230
+ /** A page the browser was sent away from — one for signed-in visitors, sent to sign in. */
24231
+ var redirected = (report) => report.issues.some((issue) => issue.code === "redirected");
23667
24232
  /** A page the browser was sent away from, or that nothing answered: not looked at, so neither passing nor failing. */
23668
- var unchecked = (report) => report.issues.some((issue) => issue.code === "redirected" || issue.code === "no-answer");
24233
+ var unchecked = (report) => redirected(report) || report.issues.some((issue) => issue.code === "no-answer");
23669
24234
  var verify = async (options) => {
23670
24235
  const project = await projectHere("to verify");
23671
24236
  if (!project) return;
@@ -23703,29 +24268,62 @@ var verify = async (options) => {
23703
24268
  } : pages));
23704
24269
  if (failed) process.exitCode = 1;
23705
24270
  };
24271
+ var NOT_STARTED = "start it (npm start) and verify again, or --no-pages to leave them";
23706
24272
  var pagesChecked = async (project) => {
23707
- const paths = await staticPaths(project.root);
23708
- if ("problem" in paths) return paths;
23709
- const reports = await checkRoutes(project, paths, {
23710
- widths: WIDTHS,
23711
- asked: { state: false },
23712
- ssr: false
23713
- });
23714
- if ("problem" in reports) return { problem: `${reports.problem} — start it (npm start) and verify again, or --no-pages to leave them` };
23715
- return {
23716
- passing: reports.filter((report) => report.ok).length,
23717
- failing: reports.filter((report) => !report.ok && !unchecked(report)),
23718
- unchecked: [...new Set(reports.filter(unchecked).map((report) => report.path))]
24273
+ const found = await staticPaths(project.root);
24274
+ if ("problem" in found) return found;
24275
+ const { paths, schemes } = found;
24276
+ const inEveryTheme = async (routes, account) => {
24277
+ const all = [];
24278
+ for (const scheme of schemes) {
24279
+ const reports = await checkRoutes(project, routes, {
24280
+ widths: WIDTHS,
24281
+ asked: { state: false },
24282
+ ssr: false,
24283
+ scheme,
24284
+ ...account ? { account } : {}
24285
+ });
24286
+ if ("problem" in reports) return { problem: `${reports.problem} — ${NOT_STARTED}` };
24287
+ all.push(...reports);
24288
+ }
24289
+ return all;
23719
24290
  };
24291
+ const reports = await inEveryTheme(paths);
24292
+ if ("problem" in reports) return reports;
24293
+ const sentAway = [...new Set(reports.filter(redirected).map((report) => report.path))];
24294
+ if (sentAway.length === 0) return summary(schemes, reports);
24295
+ const account = await checkAccount(project.root);
24296
+ if (!account || "problem" in account) return summary(schemes, reports, account ?? { none: true });
24297
+ const signedIn = await inEveryTheme(sentAway, account);
24298
+ if ("problem" in signedIn) return signedIn;
24299
+ return summary(schemes, [...reports.filter((report) => !sentAway.includes(report.path)), ...signedIn], {
24300
+ as: account.username,
24301
+ checks: signedIn.filter((report) => !unchecked(report)).length
24302
+ });
24303
+ };
24304
+ var summary = (schemes, reports, signedIn) => ({
24305
+ schemes,
24306
+ passing: reports.filter((report) => report.ok).length,
24307
+ failing: reports.filter((report) => !report.ok && !unchecked(report)),
24308
+ unchecked: [...new Set(reports.filter(unchecked).map((report) => report.path))],
24309
+ ...signedIn ? { signedIn } : {}
24310
+ });
24311
+ /** Why the pages left are not checked, and what checks them. */
24312
+ var uncheckedText = (pages) => {
24313
+ const list = pages.unchecked.join(", ");
24314
+ const signedIn = pages.signedIn;
24315
+ if (!signedIn || "none" in signedIn) return `- not checked — sent elsewhere (a page for signed-in visitors) or not answered: ${list} — name an account of the project in .env, ${CHECK_USER_ENV} and ${CHECK_PASSWORD_ENV}, and verify signs in for them`;
24316
+ if ("problem" in signedIn) return `- not checked — sent elsewhere (a page for signed-in visitors) or not answered: ${list} — ${signedIn.problem}`;
24317
+ return `- not checked, even signed in as ${signedIn.as}: ${list} — that account may not see them, or nothing answered`;
23720
24318
  };
23721
24319
  var verifyText = (results, pages) => [...results.flatMap((result) => result.ok ? [chalk.green(`✓ ${result.name}`)] : [
23722
24320
  chalk.red(`✗ ${result.name}`),
23723
24321
  ...(result.said ?? []).map((line) => ` ${line}`),
23724
24322
  ...result.fix ? [` → ${result.fix}`] : []
23725
24323
  ]), ...pages === void 0 ? [chalk.yellow("- pages not checked: --no-pages")] : "waiting" in pages ? [chalk.yellow(`- not run until ${pages.after} passes: ${pages.waiting.join(", ")} (--keep-going runs them anyway)`)] : "problem" in pages ? [chalk.red(`✗ pages — ${pages.problem}`)] : [
23726
- pages.failing.length === 0 ? chalk.green(`✓ pages — ${String(pages.passing)} checks at ${WIDTHS.join(" and ")} px`) : chalk.red(`✗ pages — ${String(pages.failing.length)} of ${String(pages.passing + pages.failing.length)} checks`),
24324
+ pages.failing.length === 0 ? chalk.green(`✓ pages — ${String(pages.passing)} checks at ${WIDTHS.join(" and ")} px, ${pages.schemes.join(" and ")}${pages.signedIn && "as" in pages.signedIn ? `, ${String(pages.signedIn.checks)} of them signed in as ${pages.signedIn.as}` : ""}`) : chalk.red(`✗ pages — ${String(pages.failing.length)} of ${String(pages.passing + pages.failing.length)} checks`),
23727
24325
  ...pages.failing.map((report) => reportText$2(report, { state: false })),
23728
- ...pages.unchecked.length > 0 ? [chalk.yellow(`- not checked — sent elsewhere (a page for signed-in visitors) or not answered: ${pages.unchecked.join(", ")} — plitzi page check <path> --as <username>`)] : []
24326
+ ...pages.unchecked.length > 0 ? [chalk.yellow(uncheckedText(pages))] : []
23729
24327
  ]].join("\n");
23730
24328
  //#endregion
23731
24329
  //#region src/doctor/projectModules.ts
@@ -24066,17 +24664,8 @@ var minimumSecret = async (root) => {
24066
24664
  const minimum = e$3(actions) ? actions.MIN_SIGNING_SECRET_LENGTH : void 0;
24067
24665
  return typeof minimum === "number" ? minimum : void 0;
24068
24666
  };
24069
- /** What the process starts with: what `.env` sets, under what the environment already has — as the server reads it. */
24070
- var settingsOf = async (root) => {
24071
- const text = await readOptional(path.join(root, ".env"));
24072
- const parsed = text === void 0 ? {} : parseEnv(text);
24073
- return {
24074
- file: text !== void 0,
24075
- value: (name) => process.env[name] ?? parsed[name] ?? ""
24076
- };
24077
- };
24078
24667
  var envChecks = async ({ root, answers }) => {
24079
- const settings = await settingsOf(root);
24668
+ const settings = await projectSettings(root);
24080
24669
  const where = settings.file ? ".env" : "the environment (there is no .env)";
24081
24670
  const findings = [];
24082
24671
  if (answers.mode === "client") {
@@ -24682,6 +25271,13 @@ var NODE_SCRIPTS = [
24682
25271
  "start:dev",
24683
25272
  "author"
24684
25273
  ];
25274
+ /** The variables a script sets before its command — `NODE_ENV=production node …` — by name. */
25275
+ var assignmentsOf = (command) => Object.fromEntries([...(/^((?:\w+=\S*\s+)*)/.exec(command)?.[1] ?? "").matchAll(/(\w+)=(\S*)/g)].map(([, name, value]) => [name, value]));
25276
+ /** A script's words when its command runs Node — after any variable it sets first — and none when it runs anything else. */
25277
+ var nodeWords = (command) => {
25278
+ const words = (command ?? "").replace(/^(?:\w+=\S*\s+)*/, "").split(/\s+/);
25279
+ return words[0] === "node" ? words : [];
25280
+ };
24685
25281
  /**
24686
25282
  * What a script that runs Node needs on disk: the file it starts, and every folder `--watch-path` names — Node stops at
24687
25283
  * start when one of them is not there, which is why each watched folder keeps a `.gitkeep`.
@@ -24691,7 +25287,7 @@ var scriptTargetChecks = async ({ root, manifest }) => {
24691
25287
  const findings = [];
24692
25288
  for (const name of NODE_SCRIPTS) {
24693
25289
  const command = scripts[name];
24694
- const words = command && /^node\s/.test(command) ? command.split(/\s+/).slice(1) : [];
25290
+ const words = nodeWords(command).slice(1);
24695
25291
  const watched = words.flatMap((word) => /^--watch-path=(.+)$/.exec(word)?.[1] ?? []);
24696
25292
  const started = words.filter((word) => !word.startsWith("-")).at(-1);
24697
25293
  for (const folder of watched) if (!await fs.stat(path.join(root, folder)).then(() => true, () => false)) findings.push(say$3.error("watch-path-missing", `The script ${name} watches ${folder}, which is not there: Node stops before it starts.`, {
@@ -24727,8 +25323,12 @@ var scriptEnvChecks = ({ answers, manifest }) => {
24727
25323
  if (answers.mode !== "server") return [];
24728
25324
  const scripts = stringsOf(manifest.scripts);
24729
25325
  return SERVER_SCRIPTS.flatMap((name) => {
24730
- const words = /^node\s/.test(scripts[name] ?? "") ? scripts[name].split(/\s+/) : [];
25326
+ const words = nodeWords(scripts[name]);
24731
25327
  if (words.length === 0) return [];
25328
+ if (name === "start:prod" && assignmentsOf(scripts[name]).NODE_ENV !== "production") return [say$3.warning("start-prod-not-production", "The script start:prod does not set NODE_ENV=production: wherever it runs without it, the server is a development one — its dev tools on, and the trace of every action in what a visitor is answered.", {
25329
+ file: "package.json",
25330
+ fix: `NODE_ENV=production before node in start:prod — ${UPGRADE_PACKAGES} rewrites a script the CLI wrote.`
25331
+ })];
24732
25332
  const flagged = words.some((word) => ENV_FLAG.test(word));
24733
25333
  const preloaded = words.some((word, index) => word === `--import=${ENV_PRELOAD}` || word === "--import" && words[index + 1] === ENV_PRELOAD);
24734
25334
  const watched = words.some((word) => word.startsWith("--watch"));
@@ -25452,7 +26052,9 @@ var previousLines = (previous) => previous.length ? [
25452
26052
  ...previous.map((url) => ` - ${url}`),
25453
26053
  "Read each (a claude.ai artifact: open it with your artifact tool, never by fetching the URL). Number the new",
25454
26054
  "findings after the highest id there, and fill `changed` with every earlier finding this project touched:",
25455
- "fixed, partly or still open — and how you saw it, on the versions above."
26055
+ "fixed, partly or still open — and how you saw it, on the versions above.",
26056
+ "A link can stop opening — an artifact deleted, or no longer shared. Say which one did not, ask the person for",
26057
+ "the highest id it had, and leave `changed` to the reports you could read."
25456
26058
  ] : ["No earlier report: number the findings from PZ-1."];
25457
26059
  var feedbackBrief = (facts, file, previous) => [
25458
26060
  "A report to Plitzi, for the developer to send as a link.",
@@ -26749,7 +27351,7 @@ var reportText = (report) => {
26749
27351
  }
26750
27352
  for (const [title, findings] of groups) lines.push(...fileText(title, findings));
26751
27353
  if (report.findings.length > 0) lines.push(chalk.dim(`What each means and what to write instead: the docs of each code — ${docsOf(report)}.`));
26752
- const suggested = [...new Set(report.findings.filter((each) => each.origin === "authoring").map((each) => each.code))];
27354
+ const suggested = [...new Set(report.findings.filter((each) => each.origin === "authoring" && each.file !== void 0).map((each) => each.code))];
26753
27355
  if (suggested.length > 0) lines.push(chalk.dim(`Authoring's suggestions (${suggested.join(", ")}) are quieted on the element they are about — \`quiet: ['${suggested[0]}']\` — not by a \`plitzi-lint-disable\` comment, which silences only lint's own codes.`));
26754
27356
  const summary = [
26755
27357
  plural(counts.error, "error"),
@@ -26901,14 +27503,14 @@ program.command("login").description("Sign in, in your browser. The session is k
26901
27503
  program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
26902
27504
  program.command("whoami").description("Who the CLI is signed in as, and the space it works in").option("--json", "One object: { api, user, space }, for a tool or an agent").option(...API_OPTION).action((options) => whoami(options));
26903
27505
  spaceGroup.command("use").description("Choose the space to work in, in your browser. One at a time: it replaces the one chosen before").option(...API_OPTION).action((options) => space$2(options));
26904
- pageGroup.command("check").argument("[path]", "The page: /about. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--ssr", "Also the HTML the server sent against the hydrated page: what a server provider sent late").option("--click <id>", "Click one element once the page settles, and say what it changed — or that nothing did").addOption(new Option("--fill <id=value>", "Fill a field before the click, as a visitor types in it — once per field; a box takes true or false").argParser(fill)).option("--as <username>", "Sign in first as this account, through the server’s /auth routes — the password from PLITZI_CHECK_PASSWORD").option("--json", "One object per width, for a tool or an agent").action((path, options) => check(path, options));
27506
+ pageGroup.command("check").argument("[paths...]", "The pages: /about /writers, in one browser. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--ssr", "Also the HTML the server sent against the hydrated page: what a server provider sent late").option("--click <id>", "Click one element once the page settles, and say what it changed — or that nothing did").addOption(new Option("--fill <id=value>", "Fill a field before the click, as a visitor types in it — once per field; a box takes true or false").argParser(fill)).option("--as <username>", "Sign in first as this account, through the server’s /auth routes — the password from PLITZI_CHECK_PASSWORD, in .env").option("--json", "One object per page and width, for a tool or an agent").action((paths, options) => check(paths, options));
26905
27507
  spaceGroup.command("fix").description("What authoring would fix in the space, as edits to your source: shown as a diff; with --write written, formatted and checked").option("--write", "Write the edits — kept only when the space authors with each fix gone and no problem added").option("--json", "The plan as one object, for a tool or an agent").action((options) => fix(options));
26906
27508
  pageGroup.command("import").argument("<url>", "The page to start from: https://example.com/pricing").description("A page of a site you verified, as a place to start writing from: its tokens, the outline of its blocks per breakpoint, its lists as JSON — never its words").option("-o, --out <dir>", "Where it is written, inside the project", "src/imported").addOption(widthsOption("The widths it is measured at: the widest is desktop, then tablet and mobile", [
26907
27509
  1440,
26908
27510
  768,
26909
27511
  390
26910
27512
  ])).option("-f, --force", "Write over what --out already holds").option("--account", "A site not served from this machine: ask your Plitzi account whether one of your spaces verified its domain (signs in)").option("--json", "One object, for a tool or an agent").option(...API_OPTION).option(...DRY_RUN_OPTION).action((url, options) => importPage(url, options));
26911
- pageGroup.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page unless --viewport, --scroll-to or --clip)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. tmp/shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--clip <element>", "Only this element — its name or a CSS selector — scrolled to wherever it is").option("--scroll-to <element>", "The page scrolled until this element is in view; the picture is the screen there").option("--viewport", "Only what fits the viewport, not the whole page").option("--reduced-motion", "As a visitor who asked for less motion").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
27513
+ pageGroup.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page unless --viewport, --scroll-to or --clip)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. tmp/shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).addOption(new Option("--from <moment>", "When --frames start: once the page settles, or as soon as its HTML is in (load) — what plays as it arrives").choices([...SHOT_MOMENTS]).default("settled")).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--clip <element>", "Only this element — its name or a CSS selector — scrolled to wherever it is").option("--scroll-to <element>", "The page scrolled until this element is in view; the picture is the screen there").option("--viewport", "Only what fits the viewport, not the whole page").option("--click <element...>", "Click these in order once the page settles — each its name or a CSS selector, inside a plugin too — picturing what each changed (--frames pictures --every ms apart after each)").option("--sheet", "Every picture taken — before, after each click, each frame — on one contact sheet").option("--steps <script>", "An interaction, step by step: \"click <el>; type <el> <text>; press <key>; wait <ms>; wait-for <el>; shot [label]; frames <n> [ms]\" — or @file with one per line").option("--reduced-motion", "As a visitor who asked for less motion").option("--as <username>", "Sign in first as this account, through the server’s /auth routes — the password from PLITZI_CHECK_PASSWORD, in .env").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
26912
27514
  program.command("verify").description("Whether the project is left passing: author, lints, types, format and every page, only the problems said").option("--no-pages", "Leave the pages out — said as such").option("--keep-going", "Run every check even after one fails — by default the first failure ends the run").option("--json", "One object, for a tool or an agent").action((options) => verify(options));
26913
27515
  elementGroup.command("where").argument("<query>", "An element’s id, a class it wears, or words it shows: hero-cta, navLink, \"Get started\"").description("Where the project’s code writes an element: the file, the line and the call itself").addOption(new Option("--by <reading>", "Read the query one way only, not the first of id, class and words that matches").choices(WHERE_READINGS)).option("--json", "One object, for a tool or an agent").action((query, options) => where(query, options));
26914
27516
  elementGroup.command("edit").argument("<id>", "The element, by its id: hero-cta").description("Change an element’s attributes in the call that writes it, then check the space still authors").option("--set <key=value...>", "An attribute to write: content=\"Start free\", required=true").option("--remove <key...>", "An attribute to take out").option("--every", "The call writes other elements too, and the change is for every one").option("--json", "One object, for a tool or an agent").action((id, options) => edit(id, options));