@plitzi/cli 0.38.8 → 0.38.9

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";
@@ -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
@@ -2660,7 +2660,7 @@ What you leave behind is the next reader's problem — the user's, or the next a
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
 
@@ -3226,7 +3226,10 @@ var spaceFiles = (answers) => {
3226
3226
  [r$6]: authorScript()
3227
3227
  };
3228
3228
  if (answers.template === "blank") return {
3229
- [a$6]: emptySpaceSource({ name: answers.name }),
3229
+ ...emptyTemplateFiles({
3230
+ name: answers.name,
3231
+ dir: i$6
3232
+ }),
3230
3233
  [r$6]: authorScript(),
3231
3234
  ...answers.mode === "client" ? { "public/data/.gitkeep": "" } : {}
3232
3235
  };
@@ -4425,9 +4428,9 @@ var __exportAll = (all, no_symbols) => {
4425
4428
  var e$42 = "[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*";
4426
4429
  var t$63 = `(?:${e$42}|\\d+)`;
4427
4430
  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})`;
4431
+ var r$36 = `${`${e$42}(?:${n$47})*(?:\\??\\.${t$63}(?:${n$47})*)*`}(?:\\s*\\?\\?\\s*[^|{}]+?)?(?:\\s*\\|\\s*[a-zA-Z_]+(?:\\([^)]*\\))?)*`;
4432
+ var i$28 = `\\{\\{\\s*${r$36}\\s*\\}\\}`;
4433
+ var a$24 = `(?:${`\\{\\{\\{\\s*${r$36}\\s*\\}\\}\\}`}|${i$28})`;
4431
4434
  new RegExp(a$24, "g");
4432
4435
  RegExp(`^${a$24}$`);
4433
4436
  var e$41 = ["smooth", "auto"];
@@ -4437,8 +4440,8 @@ var t$61 = [
4437
4440
  "end",
4438
4441
  "nearest"
4439
4442
  ];
4440
- var s$21 = "onScroll";
4441
- var r$35 = {
4443
+ var s$22 = "onScroll";
4444
+ var r$34 = {
4442
4445
  type: "select",
4443
4446
  description: "How it moves: \"smooth\" glides there, \"auto\" jumps.",
4444
4447
  default: "smooth",
@@ -4516,7 +4519,7 @@ var a$22 = {
4516
4519
  description: "How far down, the same way; negative goes up.",
4517
4520
  default: ""
4518
4521
  },
4519
- behavior: r$35
4522
+ behavior: r$34
4520
4523
  }
4521
4524
  },
4522
4525
  scrollTo: {
@@ -4534,7 +4537,7 @@ var a$22 = {
4534
4537
  description: "Where down, the same way. Empty stays.",
4535
4538
  default: ""
4536
4539
  },
4537
- behavior: r$35
4540
+ behavior: r$34
4538
4541
  }
4539
4542
  },
4540
4543
  scrollIntoView: {
@@ -4554,7 +4557,7 @@ var a$22 = {
4554
4557
  default: "nearest",
4555
4558
  options: [...t$61]
4556
4559
  },
4557
- behavior: r$35
4560
+ behavior: r$34
4558
4561
  }
4559
4562
  }
4560
4563
  };
@@ -4958,7 +4961,7 @@ var n$44 = {
4958
4961
  "border-bottom-left-radius": "4px",
4959
4962
  "border-bottom-right-radius": "4px"
4960
4963
  };
4961
- var r$34 = t$60()({
4964
+ var r$33 = t$60()({
4962
4965
  type: "button",
4963
4966
  attributeValues: {
4964
4967
  subType: e$39()([
@@ -5608,7 +5611,7 @@ var n$39 = {
5608
5611
  "letter-spacing": "inherit",
5609
5612
  cursor: "pointer"
5610
5613
  };
5611
- var r$33 = t$60()({
5614
+ var r$32 = t$60()({
5612
5615
  type: "themeToggle",
5613
5616
  attributeValues: { subType: e$39()(["switch", "segmented"]) },
5614
5617
  triggers: { onThemeChange: {
@@ -5806,7 +5809,7 @@ var n$37 = {
5806
5809
  outline: "none",
5807
5810
  position: "relative"
5808
5811
  };
5809
- var r$32 = {
5812
+ var r$31 = {
5810
5813
  ...n$37,
5811
5814
  "line-height": "20px"
5812
5815
  };
@@ -5828,7 +5831,7 @@ var o$23 = {
5828
5831
  color: "red",
5829
5832
  "margin-top": "4px"
5830
5833
  };
5831
- var s$19 = {
5834
+ var s$20 = {
5832
5835
  color: "red",
5833
5836
  "margin-left": "4px"
5834
5837
  };
@@ -5847,7 +5850,7 @@ var l$13 = {
5847
5850
  "padding-left": "0px",
5848
5851
  display: "block",
5849
5852
  ...c$18,
5850
- "background-color": "inherit",
5853
+ "background-color": "transparent",
5851
5854
  color: "inherit",
5852
5855
  "line-height": "inherit",
5853
5856
  "font-family": "inherit",
@@ -5868,7 +5871,7 @@ var u$13 = {
5868
5871
  cursor: "inherit",
5869
5872
  "box-shadow": "none"
5870
5873
  };
5871
- var d$13 = {
5874
+ var d$12 = {
5872
5875
  height: "100%",
5873
5876
  "margin-top": "0px",
5874
5877
  "margin-right": "0px",
@@ -5888,11 +5891,11 @@ var d$13 = {
5888
5891
  "font-weight": "inherit",
5889
5892
  cursor: "pointer"
5890
5893
  };
5891
- var f$11 = {
5894
+ var f$10 = {
5892
5895
  ...n$37,
5893
5896
  cursor: "pointer"
5894
5897
  };
5895
- var p$10 = {
5898
+ var p$11 = {
5896
5899
  height: "20px",
5897
5900
  width: "36px",
5898
5901
  "margin-top": "0px",
@@ -5922,7 +5925,7 @@ var p$10 = {
5922
5925
  appearance: "none",
5923
5926
  cursor: "pointer"
5924
5927
  };
5925
- var m$6 = (e, t, { field: n, icon: r } = {}) => ({
5928
+ var m$7 = (e, t, { field: n, icon: r } = {}) => ({
5926
5929
  name: e,
5927
5930
  displayMode: "desktop",
5928
5931
  style: {
@@ -5931,7 +5934,7 @@ var m$6 = (e, t, { field: n, icon: r } = {}) => ({
5931
5934
  ...n ? { field: { default: n } } : {},
5932
5935
  ...r ? { icon: { default: r } } : {},
5933
5936
  label: { default: a$21 },
5934
- requiredMark: { default: s$19 },
5937
+ requiredMark: { default: s$20 },
5935
5938
  error: { default: o$23 }
5936
5939
  }
5937
5940
  });
@@ -5977,7 +5980,7 @@ var g$4 = t$60()({
5977
5980
  autoFocus: !1,
5978
5981
  disabled: !1,
5979
5982
  options: [],
5980
- required: !0,
5983
+ required: !1,
5981
5984
  requiredMessage: "",
5982
5985
  minLength: 0,
5983
5986
  minLengthMessage: "",
@@ -6029,26 +6032,26 @@ var g$4 = t$60()({
6029
6032
  displayMode: "desktop",
6030
6033
  style: { base: { default: {} } },
6031
6034
  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, {
6035
+ hidden: m$7("Form Control Hidden", {}),
6036
+ text: m$7("Form Control Text", r$31, h$4),
6037
+ number: m$7("Form Control Number", r$31, h$4),
6038
+ email: m$7("Form Control Email", r$31, h$4),
6039
+ password: m$7("Form Control Password", r$31, {
6037
6040
  ...h$4,
6038
- icon: d$13
6041
+ icon: d$12
6039
6042
  }),
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,
6043
+ search: m$7("Form Control Search", r$31, h$4),
6044
+ url: m$7("Form Control URL", r$31, h$4),
6045
+ tel: m$7("Form Control Phone", r$31, h$4),
6046
+ date: m$7("Form Control Date", r$31, h$4),
6047
+ time: m$7("Form Control Time", r$31, h$4),
6048
+ color: m$7("Form Control Color", r$31, h$4),
6049
+ textarea: m$7("Form Control Textarea", {
6050
+ ...r$31,
6048
6051
  color: "inherit",
6049
6052
  resize: "vertical"
6050
6053
  }),
6051
- select: m$6("Form Control Select", f$11, { field: u$13 }),
6054
+ select: m$7("Form Control Select", f$10, { field: u$13 }),
6052
6055
  checkbox: {
6053
6056
  name: "Form Control Checkbox",
6054
6057
  displayMode: "desktop",
@@ -6061,7 +6064,7 @@ var g$4 = t$60()({
6061
6064
  "margin-right": "4px"
6062
6065
  } },
6063
6066
  label: { default: i$25 },
6064
- requiredMark: { default: s$19 },
6067
+ requiredMark: { default: s$20 },
6065
6068
  error: { default: o$23 }
6066
6069
  }
6067
6070
  },
@@ -6070,12 +6073,12 @@ var g$4 = t$60()({
6070
6073
  displayMode: "desktop",
6071
6074
  style: {
6072
6075
  base: { default: {} },
6073
- input: { default: p$10 },
6076
+ input: { default: p$11 },
6074
6077
  label: { default: {
6075
6078
  ...i$25,
6076
6079
  "align-items": "center"
6077
6080
  } },
6078
- requiredMark: { default: s$19 },
6081
+ requiredMark: { default: s$20 },
6079
6082
  error: { default: o$23 }
6080
6083
  }
6081
6084
  }
@@ -6677,7 +6680,7 @@ var n$34 = t$60()({
6677
6680
  definition: {
6678
6681
  label: "Api Container",
6679
6682
  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.",
6683
+ 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
6684
  items: [],
6682
6685
  bindings: {},
6683
6686
  styleSelectors: { base: "" },
@@ -6976,7 +6979,7 @@ var t$42 = t$60()({
6976
6979
  settings: {}
6977
6980
  }
6978
6981
  });
6979
- var r$31 = t$60()({
6982
+ var r$30 = t$60()({
6980
6983
  type: "container",
6981
6984
  attributeValues: {
6982
6985
  subType: e$39()([
@@ -7093,11 +7096,11 @@ var B$2 = {
7093
7096
  ApiContainer: n$34,
7094
7097
  BlockHtml: t$59,
7095
7098
  BlockJsx: t$58,
7096
- Button: r$34,
7099
+ Button: r$33,
7097
7100
  Carousel: t$43,
7098
7101
  CarouselTrack: t$42,
7099
7102
  Channel: n$33,
7100
- Container: r$31,
7103
+ Container: r$30,
7101
7104
  Custom: t$57,
7102
7105
  DialogContainer: t$60()({
7103
7106
  type: "dialogContainer",
@@ -7851,7 +7854,7 @@ var B$2 = {
7851
7854
  }
7852
7855
  }),
7853
7856
  Text: t$51,
7854
- ThemeToggle: r$33,
7857
+ ThemeToggle: r$32,
7855
7858
  Video: t$44
7856
7859
  };
7857
7860
  /** Every declaration, read through the part of its shape this catalog uses. */
@@ -8370,6 +8373,16 @@ var AUTHORING_CODES = {
8370
8373
  means: "a `runtime: 'server'` element inside a component: the page server resolves a page's and its layouts', never a component's",
8371
8374
  fix: "put the provider on the page, around the instance, and hand the component its rows as a prop"
8372
8375
  },
8376
+ "not-found-in-browser": {
8377
+ kind: "refused",
8378
+ means: "`notFound` on a provider asked from the browser — its answer arrives after the page was sent with its status",
8379
+ fix: "`runtime: 'server'` on the provider, or `visible` on the page's \"not found\" part instead"
8380
+ },
8381
+ "not-found-not-a-template": {
8382
+ kind: "refused",
8383
+ means: "`notFound` that is not one `{{ expression }}` against the answer — never `true`, so never a 404",
8384
+ fix: "`notFound: '{{ source.found == false }}'`"
8385
+ },
8373
8386
  "server-data-in-browser": {
8374
8387
  kind: "refused",
8375
8388
  means: "a provider asking for the project's own data (`/data/…`, read by its server from `src/data/` and never served) from the browser",
@@ -8855,6 +8868,11 @@ var AUTHORING_CODES = {
8855
8868
  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
8869
  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
8870
  },
8871
+ "not-found-page": {
8872
+ kind: "suggested",
8873
+ 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",
8874
+ fix: "a page whose slug is `'*'`: `{ id: 'not-found', name: 'Not found', slug: '*', layout: …, body: [...] }`"
8875
+ },
8858
8876
  "unused-component": {
8859
8877
  kind: "suggested",
8860
8878
  means: "a component no page, layout or other component places",
@@ -9154,6 +9172,8 @@ var t$30 = {
9154
9172
  TEXT_INDENT: "text-indent",
9155
9173
  TEXT_TRANSFORM: "text-transform",
9156
9174
  DIRECTION: "direction",
9175
+ WRITING_MODE: "writing-mode",
9176
+ TEXT_ORIENTATION: "text-orientation",
9157
9177
  WHITE_SPACE: "white-space",
9158
9178
  TEXT_WRAP: "text-wrap",
9159
9179
  WORD_BREAK: "word-break",
@@ -9178,7 +9198,7 @@ Object.values(t$30).map((e) => ({
9178
9198
  path: e,
9179
9199
  label: e
9180
9200
  }));
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;
9201
+ 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;
9182
9202
  [...e$38];
9183
9203
  /** Both binding forms as one list. The map form targets attributes, which is what nearly every binding does. */
9184
9204
  var toBindingSpecs = (bind) => Array.isArray(bind) ? bind : Object.entries(bind).map(([to, source]) => ({
@@ -9517,11 +9537,11 @@ var e$27 = {
9517
9537
  "motion-safe": "@media (prefers-reduced-motion: no-preference)"
9518
9538
  };
9519
9539
  var t$23 = Object.keys(e$27);
9520
- var r$19 = (t) => Object.hasOwn(e$27, t);
9540
+ var r$18 = (t) => Object.hasOwn(e$27, t);
9521
9541
  var i$17 = String.raw`\d+(?:\.\d+)?(?:px|rem|em|ch)`;
9522
9542
  var a$15 = String.raw`\((?:min|max)-width:\s*${i$17}\)`;
9523
9543
  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) => {
9544
+ var s$15 = (e) => {
9525
9545
  let t = o$17.exec(e.trim());
9526
9546
  if (t?.groups) return {
9527
9547
  name: t.groups.name,
@@ -9529,8 +9549,8 @@ var s$14 = (e) => {
9529
9549
  };
9530
9550
  };
9531
9551
  var c$12 = (e) => {
9532
- if (r$19(e)) return e;
9533
- let t = s$14(e);
9552
+ if (r$18(e)) return e;
9553
+ let t = s$15(e);
9534
9554
  if (t) return t.name ? `container ${t.name} ${t.query}` : `container ${t.query}`;
9535
9555
  };
9536
9556
  var e$26 = [
@@ -9573,7 +9593,7 @@ var o$16 = [
9573
9593
  "line-height",
9574
9594
  "vertical-align"
9575
9595
  ];
9576
- var s$13 = [
9596
+ var s$14 = [
9577
9597
  "font-family",
9578
9598
  "font-size",
9579
9599
  "font-style",
@@ -9600,16 +9620,16 @@ var l$8 = {
9600
9620
  "color",
9601
9621
  "content",
9602
9622
  "direction",
9603
- ...s$13
9623
+ ...s$14
9604
9624
  ],
9605
9625
  placeholder: [
9606
9626
  ...o$16,
9607
- ...s$13,
9627
+ ...s$14,
9608
9628
  ...c$11
9609
9629
  ],
9610
9630
  "first-line": [
9611
9631
  ...o$16,
9612
- ...s$13,
9632
+ ...s$14,
9613
9633
  ...c$11
9614
9634
  ]
9615
9635
  };
@@ -9617,7 +9637,7 @@ var u$7 = (e, t) => {
9617
9637
  let n = l$8[e];
9618
9638
  return !n || t.startsWith("--") || n.includes(t);
9619
9639
  };
9620
- var d$8 = /* @__PURE__ */ new Set([
9640
+ var d$7 = /* @__PURE__ */ new Set([
9621
9641
  "none",
9622
9642
  "normal",
9623
9643
  "open-quote",
@@ -9629,8 +9649,8 @@ var d$8 = /* @__PURE__ */ new Set([
9629
9649
  "unset",
9630
9650
  "revert"
9631
9651
  ]);
9632
- var f$6 = /^(?:counter|counters|attr|url|var|image-set|linear-gradient|radial-gradient|conic-gradient)\(/i;
9633
- var p$7 = (e) => {
9652
+ var f$5 = /^(?:counter|counters|attr|url|var|image-set|linear-gradient|radial-gradient|conic-gradient)\(/i;
9653
+ var p$8 = (e) => {
9634
9654
  let t = [], n = "", r = "", i = 0;
9635
9655
  for (let a of e) {
9636
9656
  if (r) {
@@ -9648,9 +9668,9 @@ var p$7 = (e) => {
9648
9668
  }
9649
9669
  return n && t.push(n), r || i !== 0 ? void 0 : t;
9650
9670
  };
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;
9671
+ var m$4 = (e) => {
9672
+ let t = p$8(e.trim());
9673
+ return t?.length ? t.every((e) => /^(["']).*\1$/s.test(e) || d$7.has(e.toLowerCase()) || f$5.test(e) && e.endsWith(")")) : !1;
9654
9674
  };
9655
9675
  var e$25 = [
9656
9676
  "hover",
@@ -9670,7 +9690,7 @@ var e$25 = [
9670
9690
  "even"
9671
9691
  ];
9672
9692
  var t$21 = "plitzi-component--hidden";
9673
- var r$17 = {
9693
+ var r$16 = {
9674
9694
  odd: 0,
9675
9695
  even: 1,
9676
9696
  first: 2,
@@ -9687,7 +9707,7 @@ var r$17 = {
9687
9707
  disabled: 13,
9688
9708
  hidden: 14
9689
9709
  };
9690
- var i$15 = (e) => Object.hasOwn(r$17, e);
9710
+ var i$15 = (e) => Object.hasOwn(r$16, e);
9691
9711
  var l$7 = {
9692
9712
  current: ":is([aria-current]:not([aria-current=\"false\"]),[aria-pressed=\"true\"],[aria-selected=\"true\"],[role=\"tabpanel\"]:not([hidden]))",
9693
9713
  expanded: "[aria-expanded=\"true\"]",
@@ -9698,7 +9718,7 @@ var l$7 = {
9698
9718
  even: ":nth-child(even)"
9699
9719
  };
9700
9720
  var u$6 = (e) => (i$15(e) ? l$7[e] : void 0) ?? `:${e}`;
9701
- var p$6 = (e) => e === ">";
9721
+ var p$7 = (e) => e === ">";
9702
9722
  var n$16 = (e) => {
9703
9723
  let t = typeof e == "string" && e.trim() !== "" ? Number(e) : e;
9704
9724
  return typeof t == "number" && Number.isInteger(t) && t >= 250 ? t : void 0;
@@ -9880,7 +9900,7 @@ var onInterval = (ms) => {
9880
9900
  * and hide the arrow at the end already reached: `[named('row', onScroll()), setState({ key: 'atEnd', type:
9881
9901
  * 'boolean', value: '{{ row.atEnd }}' })]`.
9882
9902
  */
9883
- var onScroll = () => on(s$21);
9903
+ var onScroll = () => on(s$22);
9884
9904
  var onPageLoad = () => on("onPageLoad");
9885
9905
  /**
9886
9906
  * The end of a server action this element started — the trigger a `detached` run needs, because that step returns
@@ -12094,7 +12114,7 @@ var toStates = (states) => {
12094
12114
  };
12095
12115
  var CLASS_NAME = /^-?[_a-zA-Z][\w-]*$/;
12096
12116
  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.`);
12117
+ 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
12118
  return [name, toBlocks({
12099
12119
  css: ancestor.css,
12100
12120
  states: ancestor.states,
@@ -12132,7 +12152,7 @@ var assertPseudo = (pseudo, part) => {
12132
12152
  for (const rules of ruleSets) for (const [property, value] of Object.entries(rules)) {
12133
12153
  if (property === "content") {
12134
12154
  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.`);
12155
+ 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
12156
  continue;
12137
12157
  }
12138
12158
  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.`);
@@ -14305,6 +14325,33 @@ var footerRow = styles("footer-row", { css: {
14305
14325
  gap: "8px"
14306
14326
  }
14307
14327
  } });
14328
+ container({
14329
+ id: "not-found-main",
14330
+ subType: "main",
14331
+ class: [shell$1, heroBand],
14332
+ children: [
14333
+ text$1("404", {
14334
+ id: "not-found-kicker",
14335
+ class: kicker
14336
+ }),
14337
+ heading("Page not found", {
14338
+ id: "not-found-title",
14339
+ subType: "h1",
14340
+ class: sectionTitle
14341
+ }),
14342
+ paragraph({
14343
+ id: "not-found-lede",
14344
+ class: bodyText,
14345
+ content: "There is nothing at this address — it may have moved, or the link has a typo."
14346
+ }),
14347
+ link({
14348
+ id: "not-found-home",
14349
+ href: "/",
14350
+ class: button,
14351
+ content: "Back to the home page"
14352
+ })
14353
+ ]
14354
+ });
14308
14355
  /**
14309
14356
  * The space, declared: this file is the page, and it assembles the rest — the palette in `tokens.ts`, the classes in
14310
14357
  * `theme.ts`, what the page says in `content.ts`. Everything else — element ids, style selector names, the
@@ -14614,6 +14661,17 @@ container({
14614
14661
  })]
14615
14662
  })]
14616
14663
  });
14664
+ heading("Page not found", {
14665
+ id: "not-found-title",
14666
+ subType: "h1"
14667
+ }), paragraph({
14668
+ id: "not-found-lede",
14669
+ content: "There is nothing at this address."
14670
+ }), link({
14671
+ id: "not-found-home",
14672
+ href: "/",
14673
+ content: "Back to the home page"
14674
+ });
14617
14675
  /**
14618
14676
  * The space, declared — and nothing in it yet but what every space starts from: tokens for both themes, a layout the
14619
14677
  * pages render in, and one page.
@@ -15211,6 +15269,14 @@ var builtPluginTypes = (folder) => existsSync(folder) ? readdirSync(folder, { wi
15211
15269
  return [entry.name, ...e$32(pluginSchema) ? Object.keys(pluginSchema) : []];
15212
15270
  }) : [];
15213
15271
  /**
15272
+ * The stylesheets of the plugins under `folder` — every `.css` in a plugin's folder, at any depth — as their text, for
15273
+ * `authorSpace`'s `stylesheets`: the tokens a plugin's own CSS reads are tokens the space uses.
15274
+ */
15275
+ var pluginStylesheets = (folder) => existsSync(folder) ? readdirSync(folder, {
15276
+ recursive: true,
15277
+ withFileTypes: true
15278
+ }).filter((entry) => entry.isFile() && entry.name.endsWith(".css")).map((entry) => path.join(entry.parentPath, entry.name)).sort().map((file) => readFileSync(file, "utf-8")) : [];
15279
+ /**
15214
15280
  * `projectAuthoring` of the project at `root`, held to the same check: for a tool that works on a project from another
15215
15281
  * folder — the CLI, run anywhere inside one. A project's own code calls `projectAuthoring()`.
15216
15282
  */
@@ -15221,6 +15287,7 @@ var projectAuthoringAt = async (root) => {
15221
15287
  return {
15222
15288
  plugins: await pluginDeclarations(path.join(at, o$1$1)),
15223
15289
  pluginTypes: builtPluginTypes(path.join(at, u$2)),
15290
+ stylesheets: pluginStylesheets(path.join(at, o$1$1)),
15224
15291
  ...existsSync(serverData) ? { serverData: projectData(serverData) } : {},
15225
15292
  data: publicData(path.join(at, p$1))
15226
15293
  };
@@ -18285,11 +18352,43 @@ var move$1 = async (elementId, options) => {
18285
18352
  ].join("\n"));
18286
18353
  };
18287
18354
  //#endregion
18355
+ //#region src/commands/projectSettings.ts
18356
+ /**
18357
+ * What the project's processes start with: what its `.env` sets, under what the environment already has — as its
18358
+ * scripts read it (`--env-file-if-exists=.env`). Read, never loaded into this process's own environment.
18359
+ */
18360
+ var projectSettings = async (root) => {
18361
+ const text = await fs.readFile(path.join(root, ".env"), "utf-8").catch(() => void 0);
18362
+ const parsed = text === void 0 ? {} : parseEnv(text);
18363
+ return {
18364
+ file: text !== void 0,
18365
+ value: (name) => process.env[name] ?? parsed[name] ?? ""
18366
+ };
18367
+ };
18368
+ //#endregion
18288
18369
  //#region src/commands/check.ts
18289
- /** Where `--as` reads the account's password: the environment, never the command line, which a shell keeps. */
18370
+ /** The account `verify` signs in as for the pages that send a visitor to sign in: the project's, in `.env`. */
18371
+ var CHECK_USER_ENV = "PLITZI_CHECK_USER";
18372
+ /** Where the account's password is read: `.env` or the environment, never the command line, which a shell keeps. */
18290
18373
  var CHECK_PASSWORD_ENV = "PLITZI_CHECK_PASSWORD";
18291
18374
  /** The project's sign-in route: where `createServer({ auth })` mounts its flows unless told otherwise. */
18292
18375
  var SIGN_IN_PATH = "/auth/login";
18376
+ /**
18377
+ * The account a check signs in as: the one named, or the project's own (`PLITZI_CHECK_USER`) — `undefined` when
18378
+ * neither is — with its password from `PLITZI_CHECK_PASSWORD`, each read from the environment over `.env`, as the
18379
+ * project's scripts read them.
18380
+ */
18381
+ var checkAccount = async (root, username) => {
18382
+ const settings = await projectSettings(root);
18383
+ const named = username ?? settings.value("PLITZI_CHECK_USER");
18384
+ if (!named) return;
18385
+ const password = settings.value(CHECK_PASSWORD_ENV);
18386
+ 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` };
18387
+ return {
18388
+ username: named,
18389
+ password
18390
+ };
18391
+ };
18293
18392
  /** The space the project declares, authored. Only for one written here. */
18294
18393
  var projectSpace = async (root) => {
18295
18394
  const project = await loadProjectSpace(root);
@@ -18310,12 +18409,25 @@ var projectSpace = async (root) => {
18310
18409
  * (`/p/:slug`), so the one meant is read off the list rather than guessed from letters.
18311
18410
  */
18312
18411
  var noPageMessage = (handles, pathname) => `no page of the space answers at ${pathname}: its pages are ${Object.values(handles.pages).map((page) => page.path).join(", ")}`;
18412
+ /**
18413
+ * The space's "not found" page: a page whose slug is `*`, the page of every address under its folder that no other
18414
+ * page answers. Not every path ending in `*` — `update/*` is a page of its own that answers a whole subtree.
18415
+ */
18416
+ var isNotFoundPage = (page) => page.slug === "*";
18417
+ /** A path whose last segment is `*`: it answers every address under it. */
18418
+ var isSplat = (path) => path.endsWith("/*");
18419
+ /** Where `verify` looks at a page for an address nothing answers: an address under its folder no page has. */
18420
+ var NOT_FOUND_PROBE = "plitzi-check-not-found";
18421
+ /** 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
18422
  var pageFor = (handles, pathname) => {
18314
18423
  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;
18424
+ const answers = (path) => {
18425
+ const splat = isSplat(path);
18426
+ const segments = (splat ? path.slice(0, -2) : path.replace(/\/$/, "")).split("/");
18427
+ return (splat ? wanted.length > segments.length : segments.length === wanted.length) && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
18428
+ };
18429
+ const pages = Object.values(handles.pages);
18430
+ 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
18431
  };
18320
18432
  /** The elements a piece of HTML carries, by the id each is marked with. */
18321
18433
  var markedIn = (html) => new Set([...html.matchAll(/data-plitzi-el="([^"]+)"/g)].map(([, id]) => id));
@@ -18708,17 +18820,23 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18708
18820
  } });
18709
18821
  if (!signedIn.ok()) return unchecked({
18710
18822
  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}`,
18823
+ 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} in .env`,
18712
18824
  width
18713
18825
  });
18714
18826
  }
18715
18827
  const consoleErrors = [];
18716
18828
  const failedRequests = [];
18717
18829
  page.on("pageerror", (error) => consoleErrors.push(error.message));
18830
+ const address = `${origin}${pathname}`;
18718
18831
  page.on("console", (message) => {
18719
- if (message.type() === "error") consoleErrors.push(message.text());
18832
+ if (message.type() === "error" && message.location().url !== address) consoleErrors.push(message.text());
18720
18833
  });
18834
+ let sentWith;
18721
18835
  page.on("response", (response) => {
18836
+ if (sentWith === void 0 && response.url() === address) {
18837
+ sentWith = response.status();
18838
+ return;
18839
+ }
18722
18840
  if (response.status() >= 400 && response.url().startsWith(origin)) failedRequests.push(`${String(response.status())} ${response.url().slice(origin.length)}`);
18723
18841
  });
18724
18842
  if (handles && !pageFor(handles, pathname)) return {
@@ -18743,7 +18861,7 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18743
18861
  if (withoutTrailingSlash(landed) !== withoutTrailingSlash(new URL(`${origin}${pathname}`).pathname)) return {
18744
18862
  ...unchecked({
18745
18863
  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}`,
18864
+ 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
18865
  width
18748
18866
  }),
18749
18867
  consoleErrors,
@@ -18773,8 +18891,18 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18773
18891
  ...flow.on === void 0 ? {} : { elementId: flow.on },
18774
18892
  width
18775
18893
  }));
18894
+ const statusIssues = (handles && pageId ? isNotFoundPage(handles.pages[pageId]) : false) && sentWith !== 404 ? [{
18895
+ code: "page-status",
18896
+ 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`,
18897
+ width
18898
+ }] : sentWith !== void 0 && sentWith >= 400 && sentWith !== 404 ? [{
18899
+ code: "page-status",
18900
+ message: `${pathname} was sent with status ${String(sentWith)}`,
18901
+ width
18902
+ }] : [];
18776
18903
  const issues = [
18777
18904
  ...noPage,
18905
+ ...statusIssues,
18778
18906
  ...report.issues.map((issue) => ({
18779
18907
  ...issue,
18780
18908
  width
@@ -18803,7 +18931,8 @@ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked,
18803
18931
  ...!asked.state || devTools.sources === void 0 ? {} : { sources: devTools.sources },
18804
18932
  ...devTools.element === void 0 ? {} : { element: devTools.element },
18805
18933
  ...styled === void 0 ? {} : { styles: styled },
18806
- ...clicked === void 0 ? {} : { clicked }
18934
+ ...clicked === void 0 ? {} : { clicked },
18935
+ ...sentWith === 404 ? { notFound: true } : {}
18807
18936
  };
18808
18937
  };
18809
18938
  /** What the page holds, when it was asked for: one compact line each, the shape of a source rather than its rows. */
@@ -18859,6 +18988,7 @@ var clickedText = (clicked) => {
18859
18988
  var reportText$2 = (report, asked) => {
18860
18989
  const head = `${report.path} at ${String(report.width)} px`;
18861
18990
  const held = [
18991
+ ...report.notFound ? [" · sent with status 404: the page for an address that shows nothing"] : [],
18862
18992
  ...listsText(report.lists),
18863
18993
  ...browserOnlyText(report),
18864
18994
  ...heldText(report, asked),
@@ -18879,27 +19009,27 @@ var reportText$2 = (report, asked) => {
18879
19009
  ].join("\n");
18880
19010
  };
18881
19011
  var withoutTrailingSlash = (path) => path.length > 1 ? path.replace(/\/+$/, "") : path;
18882
- var check = async (route, options) => {
19012
+ var check = async (routes, options) => {
18883
19013
  const project = await projectHere("whose page to check");
18884
19014
  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 …`);
19015
+ const account = options.as === void 0 ? void 0 : await checkAccount(project.root, options.as);
19016
+ if (account && "problem" in account) {
19017
+ fail(`--as ${account.problem}.`);
18888
19018
  return;
18889
19019
  }
18890
19020
  if (options.fill && !options.click) {
18891
19021
  fail("--fill fills fields for a --click: with nothing clicked, nothing they hold is sent anywhere.");
18892
19022
  return;
18893
19023
  }
18894
- const account = options.as && password ? {
18895
- username: options.as,
18896
- password
18897
- } : void 0;
19024
+ if (options.click && routes.length > 1) {
19025
+ fail(`--click clicks one element of one page: ${String(routes.length)} were named.`);
19026
+ return;
19027
+ }
18898
19028
  const asked = {
18899
19029
  state: Boolean(options.state),
18900
19030
  ...options.element ? { element: options.element } : {}
18901
19031
  };
18902
- const reports = await checkRoutes(project, [route ?? "/"], {
19032
+ const reports = await checkRoutes(project, routes.length > 0 ? routes : ["/"], {
18903
19033
  widths: options.width ?? [1440, 390],
18904
19034
  asked,
18905
19035
  ssr: Boolean(options.ssr),
@@ -18942,7 +19072,7 @@ var checkRoutes = async (project, routes, options) => {
18942
19072
  var staticPaths = async (root) => {
18943
19073
  const authored = await projectSpace(root);
18944
19074
  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)));
19075
+ return 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)));
18946
19076
  };
18947
19077
  //#endregion
18948
19078
  //#region src/commands/functions.ts
@@ -23510,6 +23640,82 @@ var compareWith = async (browser, taken, target, view, regions, base) => {
23510
23640
  diff: `${base}-diff.png`
23511
23641
  };
23512
23642
  };
23643
+ /**
23644
+ * 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
23645
+ * self-contained — no import, nothing it closes over.
23646
+ */
23647
+ function contactSheet(cells) {
23648
+ const CELL_WIDTH = 360;
23649
+ const LABEL = 24;
23650
+ const GAP = 12;
23651
+ const load = (src) => new Promise((resolve, reject) => {
23652
+ const image = new Image();
23653
+ image.onload = () => resolve(image);
23654
+ image.onerror = () => reject(/* @__PURE__ */ new Error("a picture of the sheet did not load"));
23655
+ image.src = src;
23656
+ });
23657
+ return Promise.all(cells.map((cell) => load(cell.src))).then((images) => {
23658
+ const columns = Math.min(4, images.length);
23659
+ const heightOf = (image) => Math.round(image.naturalHeight * CELL_WIDTH / image.naturalWidth);
23660
+ const cellHeight = Math.max(...images.map(heightOf));
23661
+ const rows = Math.ceil(images.length / columns);
23662
+ const canvas = document.createElement("canvas");
23663
+ canvas.width = GAP + columns * 372;
23664
+ canvas.height = GAP + rows * (LABEL + cellHeight + GAP);
23665
+ const context = canvas.getContext("2d");
23666
+ if (!context) throw new Error("no canvas to draw the sheet on");
23667
+ context.fillStyle = "#ffffff";
23668
+ context.fillRect(0, 0, canvas.width, canvas.height);
23669
+ context.font = "13px ui-monospace, monospace";
23670
+ images.forEach((image, index) => {
23671
+ const x = GAP + index % columns * 372;
23672
+ const y = GAP + Math.floor(index / columns) * (LABEL + cellHeight + GAP);
23673
+ context.fillStyle = "#16181d";
23674
+ context.fillText(cells[index].label, x, y + 16);
23675
+ context.drawImage(image, x, y + LABEL, CELL_WIDTH, heightOf(image));
23676
+ context.strokeStyle = "#d0d4dc";
23677
+ context.strokeRect(x + .5, y + LABEL + .5, 359, heightOf(image) - 1);
23678
+ });
23679
+ return canvas.toDataURL("image/png");
23680
+ });
23681
+ }
23682
+ /**
23683
+ * Each element clicked in turn, and the page pictured after it — `frames` pictures `every` ms apart, or one. What a
23684
+ * click changed is measured against the picture before it: nothing at all is said, as a click that did nothing.
23685
+ */
23686
+ var clickThrough = async (taken, how, clicks, { frames, every }) => {
23687
+ const moments = [{
23688
+ label: "before",
23689
+ png: taken.png
23690
+ }];
23691
+ const clicked = [];
23692
+ for (const target of clicks) {
23693
+ const element = taken.page.locator(selectorOf(target)).first();
23694
+ if (await element.count() === 0) return { problem: `${target} is not on the page: --click takes an element's name or a CSS selector.` };
23695
+ const before = moments[moments.length - 1].png;
23696
+ try {
23697
+ await element.click({ timeout: 3e3 });
23698
+ } catch (error) {
23699
+ return { problem: `${target} could not be clicked — hidden, covered or disabled: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
23700
+ }
23701
+ for (let frame = 1; frame <= Math.max(1, frames); frame += 1) {
23702
+ await taken.page.waitForTimeout(every);
23703
+ moments.push({
23704
+ label: `${target} +${String(frame * every)} ms`,
23705
+ png: await capture(taken.page, how)
23706
+ });
23707
+ }
23708
+ const after = moments[moments.length - 1].png;
23709
+ clicked.push({
23710
+ target,
23711
+ changed: (await comparePictures(taken.page, dataUrl(before), dataUrl(after))).changed
23712
+ });
23713
+ }
23714
+ return {
23715
+ moments,
23716
+ clicked
23717
+ };
23718
+ };
23513
23719
  var shot = async (route, options) => {
23514
23720
  const project = await projectHere("whose page to picture");
23515
23721
  if (!project) return;
@@ -23523,6 +23729,17 @@ var shot = async (route, options) => {
23523
23729
  fail(browser.problem);
23524
23730
  return;
23525
23731
  }
23732
+ const clicks = options.click ?? [];
23733
+ if (options.compare && clicks.length > 0) {
23734
+ await browser.close();
23735
+ fail("`--compare` sets the page beside another site as it loads: leave out --click, or picture the click on its own.");
23736
+ return;
23737
+ }
23738
+ if (options.sheet && clicks.length === 0 && (options.frames ?? 0) <= 1) {
23739
+ await browser.close();
23740
+ fail("`--sheet` lays out the pictures a --click or --frames takes: with neither, there is one picture and no sheet.");
23741
+ return;
23742
+ }
23526
23743
  if (options.compare && (options.clip || options.scrollTo || options.viewport)) {
23527
23744
  await browser.close();
23528
23745
  fail("`--compare` sets the whole page beside the other site, section by section: leave out --clip, --scroll-to and --viewport.");
@@ -23539,7 +23756,7 @@ var shot = async (route, options) => {
23539
23756
  ...options.clip ? { clip: options.clip } : {},
23540
23757
  ...options.scrollTo ? { scrollTo: options.scrollTo } : {},
23541
23758
  ...options.viewport ? { viewport: true } : {},
23542
- still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1
23759
+ still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1 && clicks.length === 0
23543
23760
  };
23544
23761
  const how = framing(view);
23545
23762
  const taken = await pictureOf(browser, `${where.origin}${pathname}`, view, where.origin);
@@ -23571,13 +23788,29 @@ var shot = async (route, options) => {
23571
23788
  report.compare = compared;
23572
23789
  }
23573
23790
  const frames = options.frames ?? 0;
23574
- if (frames > 1) {
23575
- const every = options.every ?? 500;
23791
+ const every = options.every ?? 500;
23792
+ let moments = [];
23793
+ if (clicks.length > 0) {
23794
+ const played = await clickThrough(taken, how, clicks, {
23795
+ frames,
23796
+ every
23797
+ });
23798
+ if ("problem" in played) {
23799
+ fail(played.problem);
23800
+ return;
23801
+ }
23802
+ moments = played.moments;
23803
+ report.clicked = played.clicked;
23804
+ } else if (frames > 1) {
23576
23805
  const pictures = [taken.png];
23577
23806
  for (let frame = 1; frame < frames; frame += 1) {
23578
23807
  await taken.page.waitForTimeout(every);
23579
23808
  pictures.push(await capture(taken.page, how));
23580
23809
  }
23810
+ moments = pictures.map((png, frame) => ({
23811
+ label: frame === 0 ? "before" : `+${String(frame * every)} ms`,
23812
+ png
23813
+ }));
23581
23814
  const diffs = [];
23582
23815
  for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), { regions }));
23583
23816
  const whole = how.kind === "element" ? options.clip ?? "element" : "viewport";
@@ -23592,6 +23825,24 @@ var shot = async (route, options) => {
23592
23825
  still: moved.filter((region) => region.changed <= MOVING_PERCENT).map((region) => region.name)
23593
23826
  };
23594
23827
  }
23828
+ if (moments.length > 1 && options.sheet) {
23829
+ const sheet = await taken.page.evaluate(contactSheet, moments.map((moment) => ({
23830
+ label: moment.label,
23831
+ src: dataUrl(moment.png)
23832
+ })));
23833
+ report.sheet = `${base}-sheet.png`;
23834
+ await writeFile(report.sheet, Buffer.from(sheet.replace(/^data:image\/png;base64,/, ""), "base64"));
23835
+ } else if (moments.length > 1 && clicks.length > 0) {
23836
+ report.pictures = [];
23837
+ for (const [index, moment] of moments.slice(1).entries()) {
23838
+ const file = `${base}-${String(index + 1)}.png`;
23839
+ await writeFile(file, moment.png);
23840
+ report.pictures.push({
23841
+ label: moment.label,
23842
+ out: file
23843
+ });
23844
+ }
23845
+ }
23595
23846
  if (options.json) {
23596
23847
  console.log(JSON.stringify(report));
23597
23848
  return;
@@ -23600,6 +23851,9 @@ var shot = async (route, options) => {
23600
23851
  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
23852
  if (report.unloaded) console.log(chalk.yellow(`${String(report.unloaded)} picture(s) had not loaded when the page was taken`));
23602
23853
  if (report.compare) console.log(compareText(report.compare).join("\n"));
23854
+ 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`));
23855
+ if (report.sheet) console.log(`sheet: ${report.sheet}`);
23856
+ for (const picture of report.pictures ?? []) console.log(`${picture.label}: ${picture.out}`);
23603
23857
  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
23858
  } finally {
23605
23859
  await browser.close();
@@ -23664,8 +23918,10 @@ var scriptsOf = async (root) => {
23664
23918
  const scripts = e$3(parsed) && e$3(parsed.scripts) ? parsed.scripts : {};
23665
23919
  return Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string"));
23666
23920
  };
23921
+ /** A page the browser was sent away from — one for signed-in visitors, sent to sign in. */
23922
+ var redirected = (report) => report.issues.some((issue) => issue.code === "redirected");
23667
23923
  /** 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");
23924
+ var unchecked = (report) => redirected(report) || report.issues.some((issue) => issue.code === "no-answer");
23669
23925
  var verify = async (options) => {
23670
23926
  const project = await projectHere("to verify");
23671
23927
  if (!project) return;
@@ -23703,29 +23959,53 @@ var verify = async (options) => {
23703
23959
  } : pages));
23704
23960
  if (failed) process.exitCode = 1;
23705
23961
  };
23962
+ var NOT_STARTED = "start it (npm start) and verify again, or --no-pages to leave them";
23706
23963
  var pagesChecked = async (project) => {
23707
23964
  const paths = await staticPaths(project.root);
23708
23965
  if ("problem" in paths) return paths;
23709
- const reports = await checkRoutes(project, paths, {
23966
+ const options = {
23710
23967
  widths: WIDTHS,
23711
23968
  asked: { state: false },
23712
23969
  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))]
23719
23970
  };
23971
+ const reports = await checkRoutes(project, paths, options);
23972
+ if ("problem" in reports) return { problem: `${reports.problem} — ${NOT_STARTED}` };
23973
+ const sentAway = [...new Set(reports.filter(redirected).map((report) => report.path))];
23974
+ if (sentAway.length === 0) return summary(reports);
23975
+ const account = await checkAccount(project.root);
23976
+ if (!account || "problem" in account) return summary(reports, account ?? { none: true });
23977
+ const signedIn = await checkRoutes(project, sentAway, {
23978
+ ...options,
23979
+ account
23980
+ });
23981
+ if ("problem" in signedIn) return { problem: `${signedIn.problem} — ${NOT_STARTED}` };
23982
+ return summary([...reports.filter((report) => !sentAway.includes(report.path)), ...signedIn], {
23983
+ as: account.username,
23984
+ checks: signedIn.filter((report) => !unchecked(report)).length
23985
+ });
23986
+ };
23987
+ var summary = (reports, signedIn) => ({
23988
+ passing: reports.filter((report) => report.ok).length,
23989
+ failing: reports.filter((report) => !report.ok && !unchecked(report)),
23990
+ unchecked: [...new Set(reports.filter(unchecked).map((report) => report.path))],
23991
+ ...signedIn ? { signedIn } : {}
23992
+ });
23993
+ /** Why the pages left are not checked, and what checks them. */
23994
+ var uncheckedText = (pages) => {
23995
+ const list = pages.unchecked.join(", ");
23996
+ const signedIn = pages.signedIn;
23997
+ 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`;
23998
+ if ("problem" in signedIn) return `- not checked — sent elsewhere (a page for signed-in visitors) or not answered: ${list} — ${signedIn.problem}`;
23999
+ return `- not checked, even signed in as ${signedIn.as}: ${list} — that account may not see them, or nothing answered`;
23720
24000
  };
23721
24001
  var verifyText = (results, pages) => [...results.flatMap((result) => result.ok ? [chalk.green(`✓ ${result.name}`)] : [
23722
24002
  chalk.red(`✗ ${result.name}`),
23723
24003
  ...(result.said ?? []).map((line) => ` ${line}`),
23724
24004
  ...result.fix ? [` → ${result.fix}`] : []
23725
24005
  ]), ...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`),
24006
+ pages.failing.length === 0 ? chalk.green(`✓ pages — ${String(pages.passing)} checks at ${WIDTHS.join(" and ")} px${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
24007
  ...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>`)] : []
24008
+ ...pages.unchecked.length > 0 ? [chalk.yellow(uncheckedText(pages))] : []
23729
24009
  ]].join("\n");
23730
24010
  //#endregion
23731
24011
  //#region src/doctor/projectModules.ts
@@ -24066,17 +24346,8 @@ var minimumSecret = async (root) => {
24066
24346
  const minimum = e$3(actions) ? actions.MIN_SIGNING_SECRET_LENGTH : void 0;
24067
24347
  return typeof minimum === "number" ? minimum : void 0;
24068
24348
  };
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
24349
  var envChecks = async ({ root, answers }) => {
24079
- const settings = await settingsOf(root);
24350
+ const settings = await projectSettings(root);
24080
24351
  const where = settings.file ? ".env" : "the environment (there is no .env)";
24081
24352
  const findings = [];
24082
24353
  if (answers.mode === "client") {
@@ -24682,6 +24953,13 @@ var NODE_SCRIPTS = [
24682
24953
  "start:dev",
24683
24954
  "author"
24684
24955
  ];
24956
+ /** The variables a script sets before its command — `NODE_ENV=production node …` — by name. */
24957
+ var assignmentsOf = (command) => Object.fromEntries([...(/^((?:\w+=\S*\s+)*)/.exec(command)?.[1] ?? "").matchAll(/(\w+)=(\S*)/g)].map(([, name, value]) => [name, value]));
24958
+ /** A script's words when its command runs Node — after any variable it sets first — and none when it runs anything else. */
24959
+ var nodeWords = (command) => {
24960
+ const words = (command ?? "").replace(/^(?:\w+=\S*\s+)*/, "").split(/\s+/);
24961
+ return words[0] === "node" ? words : [];
24962
+ };
24685
24963
  /**
24686
24964
  * What a script that runs Node needs on disk: the file it starts, and every folder `--watch-path` names — Node stops at
24687
24965
  * start when one of them is not there, which is why each watched folder keeps a `.gitkeep`.
@@ -24691,7 +24969,7 @@ var scriptTargetChecks = async ({ root, manifest }) => {
24691
24969
  const findings = [];
24692
24970
  for (const name of NODE_SCRIPTS) {
24693
24971
  const command = scripts[name];
24694
- const words = command && /^node\s/.test(command) ? command.split(/\s+/).slice(1) : [];
24972
+ const words = nodeWords(command).slice(1);
24695
24973
  const watched = words.flatMap((word) => /^--watch-path=(.+)$/.exec(word)?.[1] ?? []);
24696
24974
  const started = words.filter((word) => !word.startsWith("-")).at(-1);
24697
24975
  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 +25005,12 @@ var scriptEnvChecks = ({ answers, manifest }) => {
24727
25005
  if (answers.mode !== "server") return [];
24728
25006
  const scripts = stringsOf(manifest.scripts);
24729
25007
  return SERVER_SCRIPTS.flatMap((name) => {
24730
- const words = /^node\s/.test(scripts[name] ?? "") ? scripts[name].split(/\s+/) : [];
25008
+ const words = nodeWords(scripts[name]);
24731
25009
  if (words.length === 0) return [];
25010
+ 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.", {
25011
+ file: "package.json",
25012
+ fix: `NODE_ENV=production before node in start:prod — ${UPGRADE_PACKAGES} rewrites a script the CLI wrote.`
25013
+ })];
24732
25014
  const flagged = words.some((word) => ENV_FLAG.test(word));
24733
25015
  const preloaded = words.some((word, index) => word === `--import=${ENV_PRELOAD}` || word === "--import" && words[index + 1] === ENV_PRELOAD);
24734
25016
  const watched = words.some((word) => word.startsWith("--watch"));
@@ -25452,7 +25734,9 @@ var previousLines = (previous) => previous.length ? [
25452
25734
  ...previous.map((url) => ` - ${url}`),
25453
25735
  "Read each (a claude.ai artifact: open it with your artifact tool, never by fetching the URL). Number the new",
25454
25736
  "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."
25737
+ "fixed, partly or still open — and how you saw it, on the versions above.",
25738
+ "A link can stop opening — an artifact deleted, or no longer shared. Say which one did not, ask the person for",
25739
+ "the highest id it had, and leave `changed` to the reports you could read."
25456
25740
  ] : ["No earlier report: number the findings from PZ-1."];
25457
25741
  var feedbackBrief = (facts, file, previous) => [
25458
25742
  "A report to Plitzi, for the developer to send as a link.",
@@ -26749,7 +27033,7 @@ var reportText = (report) => {
26749
27033
  }
26750
27034
  for (const [title, findings] of groups) lines.push(...fileText(title, findings));
26751
27035
  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))];
27036
+ const suggested = [...new Set(report.findings.filter((each) => each.origin === "authoring" && each.file !== void 0).map((each) => each.code))];
26753
27037
  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
27038
  const summary = [
26755
27039
  plural(counts.error, "error"),
@@ -26901,14 +27185,14 @@ program.command("login").description("Sign in, in your browser. The session is k
26901
27185
  program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
26902
27186
  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
27187
  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));
27188
+ 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
27189
  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
27190
  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
27191
  1440,
26908
27192
  768,
26909
27193
  390
26910
27194
  ])).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));
27195
+ 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("--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("--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));
26912
27196
  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
27197
  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
27198
  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));