@graview/core 0.1.15 → 0.1.17

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.
Files changed (216) hide show
  1. package/README.md +23 -5
  2. package/dist/app.d.ts +8 -8
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/arrangement.d.ts +2 -2
  5. package/dist/arrangement.d.ts.map +1 -1
  6. package/dist/arrangement.js +5 -5
  7. package/dist/arranging.js +3 -3
  8. package/dist/capabilities.js +1 -1
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/city.d.ts +5 -5
  11. package/dist/city.js +3 -3
  12. package/dist/cli/check/beginnings.js +2 -2
  13. package/dist/cli/check/brand.d.ts.map +1 -1
  14. package/dist/cli/check/brand.js +15 -9
  15. package/dist/cli/check/brand.js.map +1 -1
  16. package/dist/cli/check/city.js +1 -1
  17. package/dist/cli/check/context.js +1 -1
  18. package/dist/cli/check/relations.js +2 -2
  19. package/dist/cli/check/relations.js.map +1 -1
  20. package/dist/cli/check/settings.js +8 -8
  21. package/dist/cli/check/settings.js.map +1 -1
  22. package/dist/cli/check/views.d.ts +1 -1
  23. package/dist/cli/check/views.js +1 -1
  24. package/dist/cli/create.d.ts +1 -1
  25. package/dist/cli/create.js +5 -5
  26. package/dist/cli/describe.d.ts +1 -1
  27. package/dist/cli/describe.js +2 -2
  28. package/dist/cli/describe.js.map +1 -1
  29. package/dist/cli/index.d.ts +1 -1
  30. package/dist/columns.d.ts +1 -1
  31. package/dist/columns.js +1 -1
  32. package/dist/compiled.d.ts.map +1 -1
  33. package/dist/compiled.js +6 -2
  34. package/dist/compiled.js.map +1 -1
  35. package/dist/conformance/fixtures.d.ts.map +1 -1
  36. package/dist/conformance/fixtures.js +194 -0
  37. package/dist/conformance/fixtures.js.map +1 -1
  38. package/dist/describe.d.ts +1 -1
  39. package/dist/describe.d.ts.map +1 -1
  40. package/dist/document/brand-check.d.ts +10 -0
  41. package/dist/document/brand-check.d.ts.map +1 -0
  42. package/dist/document/brand-check.js +33 -0
  43. package/dist/document/brand-check.js.map +1 -0
  44. package/dist/document/brand.d.ts +10 -0
  45. package/dist/document/brand.d.ts.map +1 -0
  46. package/dist/document/brand.js +49 -0
  47. package/dist/document/brand.js.map +1 -0
  48. package/dist/document/compile-checked.d.ts.map +1 -1
  49. package/dist/document/compile-checked.js +6 -2
  50. package/dist/document/compile-checked.js.map +1 -1
  51. package/dist/document/compile.d.ts +6 -0
  52. package/dist/document/compile.d.ts.map +1 -1
  53. package/dist/document/compile.js +4 -13
  54. package/dist/document/compile.js.map +1 -1
  55. package/dist/document/compiled.d.ts.map +1 -1
  56. package/dist/document/compiled.js +12 -4
  57. package/dist/document/compiled.js.map +1 -1
  58. package/dist/document/describe-place.d.ts +16 -0
  59. package/dist/document/describe-place.d.ts.map +1 -1
  60. package/dist/document/describe-place.js +14 -1
  61. package/dist/document/describe-place.js.map +1 -1
  62. package/dist/document/diff.d.ts +1 -1
  63. package/dist/document/diff.d.ts.map +1 -1
  64. package/dist/document/diff.js +52 -7
  65. package/dist/document/diff.js.map +1 -1
  66. package/dist/document/edit.d.ts +7 -2
  67. package/dist/document/edit.d.ts.map +1 -1
  68. package/dist/document/edit.js +188 -48
  69. package/dist/document/edit.js.map +1 -1
  70. package/dist/document/expr/analyze.d.ts +2 -2
  71. package/dist/document/expr/analyze.js +2 -2
  72. package/dist/document/expr/print.js +1 -1
  73. package/dist/document/index.d.ts +3 -1
  74. package/dist/document/index.d.ts.map +1 -1
  75. package/dist/document/index.js +1 -0
  76. package/dist/document/index.js.map +1 -1
  77. package/dist/document/quote.d.ts +5 -0
  78. package/dist/document/quote.d.ts.map +1 -0
  79. package/dist/document/quote.js +17 -0
  80. package/dist/document/quote.js.map +1 -0
  81. package/dist/document/respell.d.ts +20 -0
  82. package/dist/document/respell.d.ts.map +1 -0
  83. package/dist/document/respell.js +66 -0
  84. package/dist/document/respell.js.map +1 -0
  85. package/dist/document/rules.d.ts +2 -2
  86. package/dist/document/rules.d.ts.map +1 -1
  87. package/dist/document/rules.js +2 -2
  88. package/dist/document/rules.js.map +1 -1
  89. package/dist/document/schema.d.ts +47 -2
  90. package/dist/document/schema.d.ts.map +1 -1
  91. package/dist/document/schema.js +27 -3
  92. package/dist/document/schema.js.map +1 -1
  93. package/dist/document/thumbnail.d.ts +9 -0
  94. package/dist/document/thumbnail.d.ts.map +1 -1
  95. package/dist/document/thumbnail.js +74 -12
  96. package/dist/document/thumbnail.js.map +1 -1
  97. package/dist/document/to-document.js +5 -5
  98. package/dist/document/to-document.js.map +1 -1
  99. package/dist/document/upgrade.d.ts +3 -1
  100. package/dist/document/upgrade.d.ts.map +1 -1
  101. package/dist/document/upgrade.js +6 -3
  102. package/dist/document/upgrade.js.map +1 -1
  103. package/dist/graph/graph.js +3 -3
  104. package/dist/graph/primitives.d.ts +2 -2
  105. package/dist/graph/primitives.js +2 -2
  106. package/dist/index.d.ts +12 -5
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +9 -5
  109. package/dist/index.js.map +1 -1
  110. package/dist/invariants/engine.js +1 -1
  111. package/dist/invariants/types.d.ts +3 -3
  112. package/dist/invariants/types.d.ts.map +1 -1
  113. package/dist/labels.d.ts +1 -1
  114. package/dist/modules.d.ts +1 -1
  115. package/dist/modules.js +1 -1
  116. package/dist/mutations/define-mutation.js +4 -4
  117. package/dist/mutations/define-mutation.js.map +1 -1
  118. package/dist/mutations/derive-edits.js +3 -3
  119. package/dist/mutations/node-ref.js +1 -1
  120. package/dist/mutations/types.d.ts +1 -1
  121. package/dist/mutations/words.js +3 -3
  122. package/dist/ops/types.d.ts +1 -1
  123. package/dist/ops/withheld.d.ts +3 -3
  124. package/dist/permissions/policy.d.ts +1 -1
  125. package/dist/permissions/policy.js +3 -3
  126. package/dist/permissions/types.d.ts +2 -2
  127. package/dist/places.d.ts +19 -2
  128. package/dist/places.d.ts.map +1 -1
  129. package/dist/places.js +41 -4
  130. package/dist/places.js.map +1 -1
  131. package/dist/scaffold/domain.js +3 -3
  132. package/dist/scaffold/index.d.ts +1 -1
  133. package/dist/scaffold/index.d.ts.map +1 -1
  134. package/dist/scaffold/names.d.ts +1 -1
  135. package/dist/scaffold/names.js +1 -1
  136. package/dist/scaffold/names.js.map +1 -1
  137. package/dist/scaffold/project.d.ts.map +1 -1
  138. package/dist/scaffold/project.js +4 -1
  139. package/dist/scaffold/project.js.map +1 -1
  140. package/dist/scene-districts.d.ts +1 -1
  141. package/dist/schema/define-node.d.ts +4 -4
  142. package/dist/schema/define-node.d.ts.map +1 -1
  143. package/dist/schema/define-node.js +8 -8
  144. package/dist/schema/define-node.js.map +1 -1
  145. package/dist/schema/figures.d.ts +1 -1
  146. package/dist/schema/figures.js +7 -7
  147. package/dist/schema/figures.js.map +1 -1
  148. package/dist/schema/types.d.ts +1 -1
  149. package/dist/search.d.ts +13 -1
  150. package/dist/search.d.ts.map +1 -1
  151. package/dist/search.js +33 -14
  152. package/dist/search.js.map +1 -1
  153. package/dist/seen.d.ts +2 -2
  154. package/dist/seen.js +12 -12
  155. package/dist/seen.js.map +1 -1
  156. package/dist/settings.d.ts +1 -1
  157. package/dist/settings.js +3 -3
  158. package/dist/settings.js.map +1 -1
  159. package/dist/store.d.ts +2 -2
  160. package/dist/store.d.ts.map +1 -1
  161. package/dist/store.js +8 -8
  162. package/dist/store.js.map +1 -1
  163. package/dist/sync/google-calendar.js +1 -1
  164. package/dist/sync/types.d.ts +2 -2
  165. package/dist/testing.d.ts +1 -1
  166. package/dist/testing.d.ts.map +1 -1
  167. package/dist/testing.js +2 -2
  168. package/dist/testing.js.map +1 -1
  169. package/dist/theme/accent.d.ts +37 -0
  170. package/dist/theme/accent.d.ts.map +1 -0
  171. package/dist/theme/accent.js +138 -0
  172. package/dist/theme/accent.js.map +1 -0
  173. package/dist/theme/contrast.d.ts +7 -7
  174. package/dist/theme/contrast.d.ts.map +1 -1
  175. package/dist/theme/contrast.js +14 -14
  176. package/dist/theme/contrast.js.map +1 -1
  177. package/dist/theme/derive.d.ts +4 -4
  178. package/dist/theme/derive.d.ts.map +1 -1
  179. package/dist/theme/derive.js +21 -22
  180. package/dist/theme/derive.js.map +1 -1
  181. package/dist/theme/fonts.d.ts +19 -0
  182. package/dist/theme/fonts.d.ts.map +1 -0
  183. package/dist/theme/fonts.js +74 -0
  184. package/dist/theme/fonts.js.map +1 -0
  185. package/dist/theme/identity.d.ts +83 -0
  186. package/dist/theme/identity.d.ts.map +1 -0
  187. package/dist/theme/identity.js +128 -0
  188. package/dist/theme/identity.js.map +1 -0
  189. package/dist/theme/kit.d.ts +9 -9
  190. package/dist/theme/kit.d.ts.map +1 -1
  191. package/dist/theme/kit.js +15 -15
  192. package/dist/theme/kit.js.map +1 -1
  193. package/dist/theme/layers.d.ts +2 -2
  194. package/dist/theme/layers.d.ts.map +1 -1
  195. package/dist/theme/layers.js +2 -2
  196. package/dist/theme/layers.js.map +1 -1
  197. package/dist/theme/look.d.ts +27 -7
  198. package/dist/theme/look.d.ts.map +1 -1
  199. package/dist/theme/look.js +30 -6
  200. package/dist/theme/look.js.map +1 -1
  201. package/dist/theme/marks.d.ts +28 -0
  202. package/dist/theme/marks.d.ts.map +1 -0
  203. package/dist/theme/marks.js +237 -0
  204. package/dist/theme/marks.js.map +1 -0
  205. package/dist/theme/palettes.d.ts +15 -7
  206. package/dist/theme/palettes.d.ts.map +1 -1
  207. package/dist/theme/palettes.js +61 -53
  208. package/dist/theme/palettes.js.map +1 -1
  209. package/dist/theme/types.d.ts +38 -6
  210. package/dist/theme/types.d.ts.map +1 -1
  211. package/dist/theme/types.js.map +1 -1
  212. package/dist/validate-graph.js +2 -2
  213. package/dist/version.d.ts +1 -1
  214. package/dist/version.js +1 -1
  215. package/dist/watched.js +8 -8
  216. package/package.json +1 -1
@@ -11,6 +11,11 @@ import { placeSlug } from "../views/types.js";
11
11
  import { computedOf, validateComputed } from "./computed.js";
12
12
  import { parseTemplate, templateBraces, TemplateError } from "./template-parse.js";
13
13
  import { validateViews, VIEW_SLOTS } from "./views.js";
14
+ import { brandFindings } from "./brand-check.js";
15
+ import { canonicalize } from "./canonical.js";
16
+ import { quotedAtTheEnd } from "./quote.js";
17
+ import { respellDocument } from "./respell.js";
18
+ import { accentProblem } from "../theme/accent.js";
14
19
  /** The ops, for tool schemas and the guide. */
15
20
  export const EDIT_OPS = [
16
21
  "add-kind",
@@ -32,6 +37,8 @@ export const EDIT_OPS = [
32
37
  "add-rule",
33
38
  "remove-rule",
34
39
  "set-brand",
40
+ "set-name",
41
+ "set-description",
35
42
  "set-label",
36
43
  "set-describe",
37
44
  "set-view",
@@ -74,11 +81,20 @@ const SHAPES = {
74
81
  .object({
75
82
  op: z.literal("set-brand"),
76
83
  accent: z.union([z.string(), z.null()]).optional(),
77
- name: z.string().min(1).max(60).optional(),
84
+ name: z.union([z.string().min(1).max(60), z.null()]).optional(),
78
85
  currency: z.union([z.string(), z.null()]).optional(),
79
86
  locale: z.union([z.string(), z.null()]).optional(),
87
+ // The rest of the brand (FR-124, FR-125): each as the document holds it, null to clear it.
88
+ logo: z.union([z.string().min(1), z.object({ src: z.string().min(1), alt: z.string().min(1).max(120).optional() }).strict(), z.null()]).optional(),
89
+ favicon: z.union([z.string().min(1), z.null()]).optional(),
90
+ typography: z.union([z.object({ display: z.union([z.string().min(1).max(200), z.null()]).optional(), body: z.union([z.string().min(1).max(200), z.null()]).optional(), mono: z.union([z.string().min(1).max(200), z.null()]).optional() }).strict(), z.null()]).optional(),
91
+ shape: z.union([z.object({ radius: z.union([z.number().min(0).max(32), z.null()]).optional(), density: z.union([z.number().min(0.75).max(1.5), z.null()]).optional() }).strict(), z.null()]).optional(),
92
+ accents: z.union([z.record(z.string(), z.union([z.number().min(0).max(360), z.null()])), z.null()]).optional(),
93
+ scheme: z.union([z.enum(["light", "dark", "auto"]), z.null()]).optional(),
80
94
  })
81
95
  .strict(),
96
+ "set-name": z.object({ op: z.literal("set-name"), name: z.string().min(1).max(80) }).strict(),
97
+ "set-description": z.object({ op: z.literal("set-description"), description: z.union([z.string().min(1).max(500), z.null()]) }).strict(),
82
98
  "set-label": z.object({ op: z.literal("set-label"), kind: kindName, field: fieldName.optional(), label: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
83
99
  "set-describe": z.object({ op: z.literal("set-describe"), kind: kindName, describe: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
84
100
  // A kind's slot; the front page (`slot: "home"`, no kind); or a blocks lens's blocks, by its title (FR-84).
@@ -104,6 +120,10 @@ const SHAPES = {
104
120
  order: z.union([z.array(kindName).max(40), z.null()]).optional(),
105
121
  hide: z.union([z.array(kindName).max(40), z.null()]).optional(),
106
122
  first: z.union([z.string().min(1).max(80), z.null()]).optional(),
123
+ scene: z.union([z.string().min(1).max(40), z.null()]).optional(),
124
+ pages: z.union([z.string().min(1).max(40), z.null()]).optional(),
125
+ // The scene's word as a chat or a tool written before 0.1.17 sends it (FR-132's key, RESPELLED as `scene`).
126
+ overview: z.union([z.string().min(1).max(40), z.null()]).optional(),
107
127
  })
108
128
  .strict(),
109
129
  "set-computed": z
@@ -395,6 +415,7 @@ function setTargets(act, doc) {
395
415
  // ── the edits ────────────────────────────────────────────────────────────────
396
416
  class Editor {
397
417
  doc;
418
+ options;
398
419
  said = [];
399
420
  /** The agent tools this change renames or reshapes: a model that listed them before calls what is gone. */
400
421
  toolsMoved = new Set();
@@ -404,8 +425,9 @@ class Editor {
404
425
  kindOrigin = new Map();
405
426
  fieldOrigin = new Map();
406
427
  edgeOrigin = new Map();
407
- constructor(doc, base) {
428
+ constructor(doc, base, options = {}) {
408
429
  this.doc = doc;
430
+ this.options = options;
409
431
  for (const [kind, spec] of Object.entries(base.kinds)) {
410
432
  this.kindOrigin.set(kind, kind);
411
433
  for (const f of Object.keys(spec.fields))
@@ -417,6 +439,128 @@ class Editor {
417
439
  fail(i, path, message, fix) {
418
440
  this.findings.push(error("edit", `edits.${i}${path ? `.${path}` : ""}`, message, fix));
419
441
  }
442
+ /**
443
+ * THE BRAND, KEY BY KEY (FR-100, FR-124, FR-125): what an edit names is
444
+ * set, null clears it, and what it does not name stays as it was. Each
445
+ * is judged as `graview check` judges it — a mark that could act or load,
446
+ * a face off the list — and an accent that does not read as given is
447
+ * refused with the pair, the ratio and a shade that would (FR-126).
448
+ */
449
+ setBrand(i, e) {
450
+ const brand = clone(this.doc.brand ?? {});
451
+ const at = `edits.${i}`;
452
+ // An edit that names nothing to change is refused, never applied with nothing said.
453
+ const named = Object.keys(e).filter((key) => key !== "op" && e[key] !== undefined);
454
+ if (named.length === 0)
455
+ return this.fail(i, "", "a set-brand edit names at least one key of the brand to set, or null to clear one", '{"op": "set-brand", "accent": "#0f6e5c"}');
456
+ const emptyPart = ["typography", "shape", "accents"].find((key) => isObject(e[key]) && Object.keys(e[key]).length === 0);
457
+ if (emptyPart)
458
+ return this.fail(i, emptyPart, `${emptyPart} names no part to set; name one, or give null to clear all of it`);
459
+ const judged = brandFindings({ ...(typeof e.logo === "string" || isObject(e.logo) ? { logo: e.logo } : {}), ...(typeof e.favicon === "string" ? { favicon: e.favicon } : {}), ...(isObject(e.typography) ? { typography: Object.fromEntries(Object.entries(e.typography).filter(([, v]) => typeof v === "string")) } : {}) }, { at, ...(this.options.fonts ? { fonts: this.options.fonts } : {}) });
460
+ if (judged.length > 0)
461
+ return void this.findings.push(...judged);
462
+ if (e.accent === null) {
463
+ delete brand["accent"];
464
+ this.said.push("The app goes back to Graview's colors.");
465
+ }
466
+ else if (e.accent !== undefined) {
467
+ if (!/^#[0-9a-fA-F]{6}$/.test(e.accent))
468
+ return this.fail(i, "accent", 'an accent is a color like "#c2577a"');
469
+ const refused = accentProblem(e.accent);
470
+ if (refused)
471
+ return this.fail(i, "accent", refused.sentence, refused.suggestion ? `{"op": "set-brand", "accent": "${refused.suggestion}"}` : "pick a color of another hue");
472
+ brand["accent"] = e.accent;
473
+ this.said.push(`The app's accent color becomes ${e.accent}.`);
474
+ }
475
+ if (e.currency === null) {
476
+ delete brand["currency"];
477
+ this.said.push("Money is said with no currency.");
478
+ }
479
+ else if (e.currency !== undefined) {
480
+ if (!/^[A-Z]{3}$/.test(e.currency))
481
+ return this.fail(i, "currency", 'a currency is its three-letter code, like "USD" or "EUR"');
482
+ brand["currency"] = e.currency;
483
+ this.said.push(`Money is said in ${e.currency}.`);
484
+ }
485
+ if (e.locale === null)
486
+ delete brand["locale"];
487
+ else if (e.locale !== undefined) {
488
+ if (!/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(e.locale))
489
+ return this.fail(i, "locale", 'a locale is a language tag, like "en-US" or "de-DE"');
490
+ brand["locale"] = e.locale;
491
+ this.said.push(`Money is written for ${e.locale}.`);
492
+ }
493
+ if (e.name === null) {
494
+ delete brand["name"];
495
+ this.said.push("The wordmark says the app's name.");
496
+ }
497
+ else if (e.name !== undefined) {
498
+ brand["name"] = e.name;
499
+ this.said.push(`The app's wordmark says ${e.name}.`);
500
+ }
501
+ for (const [key, what] of [["logo", "logo"], ["favicon", "page icon"]]) {
502
+ if (e[key] === undefined)
503
+ continue;
504
+ const was = brand[key];
505
+ if (e[key] === null)
506
+ delete brand[key];
507
+ else
508
+ brand[key] = e[key];
509
+ const src = (mark) => (typeof mark === "string" ? mark : isObject(mark) ? mark["src"] : undefined);
510
+ const alt = (mark) => (isObject(mark) && typeof mark["alt"] === "string" ? mark["alt"] : undefined);
511
+ if (e[key] === null)
512
+ this.said.push(`The app has no ${what}.`);
513
+ else if (was === undefined)
514
+ this.said.push(`The app gets a ${what}.`);
515
+ else if (canonicalize(was) === canonicalize(e[key]))
516
+ this.said.push(`The ${what} stays as it was.`);
517
+ else if (src(was) === src(e[key]))
518
+ this.said.push(`The ${what} is said as ${quotedAtTheEnd(alt(e[key]) ?? brand["name"] ?? this.doc.name)}`);
519
+ else
520
+ this.said.push(`The ${what} changes.`);
521
+ }
522
+ const merge = (key, say, cleared) => {
523
+ if (e[key] === undefined)
524
+ return;
525
+ if (e[key] === null) {
526
+ delete brand[key];
527
+ this.said.push(cleared);
528
+ return;
529
+ }
530
+ const into = { ...(brand[key] ?? {}) };
531
+ for (const [part, value] of Object.entries(e[key])) {
532
+ if (value === null)
533
+ delete into[part];
534
+ else
535
+ into[part] = value;
536
+ this.said.push(say(part, value));
537
+ }
538
+ if (Object.keys(into).length === 0)
539
+ delete brand[key];
540
+ else
541
+ brand[key] = into;
542
+ };
543
+ const ROLE = { display: "Headings", body: "Body text", mono: "Code" };
544
+ merge("typography", (role, value) => (value === null ? `${ROLE[role]} go${role === "display" ? "" : "es"} back to Graview's face.` : `${ROLE[role]} ${role === "display" ? "are" : "is"} now set in ${value}.`), "Words go back to Graview's faces.");
545
+ merge("shape", (part, value) => (part === "radius" ? (value === null ? "Corners go back to Graview's own." : `Corners are now ${value}px.`) : value === null ? "Spacing goes back to Graview's own." : `Spacing is now ${value} times Graview's own.`), "Corners and spacing go back to Graview's own.");
546
+ if (isObject(e.accents)) {
547
+ const unknown = Object.keys(e.accents).find((kind) => !this.doc.kinds[kind]);
548
+ if (unknown)
549
+ return this.fail(i, "accents", `"${unknown}" is not a kind this app has; it has ${Object.keys(this.doc.kinds).join(", ")}`);
550
+ }
551
+ merge("accents", (kind, hue) => (hue === null ? `${cap(kind)} goes back to its own hue.` : `${cap(kind)} is drawn at hue ${hue}°.`), "Every kind goes back to its own hue.");
552
+ if (e.scheme !== undefined) {
553
+ if (e.scheme === null || e.scheme === "auto")
554
+ delete brand["scheme"];
555
+ else
556
+ brand["scheme"] = e.scheme;
557
+ this.said.push(e.scheme === "light" || e.scheme === "dark" ? `The app opens ${e.scheme} when the reader has not chosen.` : "The app follows the reader's system for light and dark.");
558
+ }
559
+ if (Object.keys(brand).length === 0)
560
+ delete this.doc.brand;
561
+ else
562
+ this.doc.brand = brand;
563
+ }
420
564
  kind(i, kind) {
421
565
  const spec = this.doc.kinds[kind];
422
566
  if (!spec)
@@ -532,46 +676,23 @@ class Editor {
532
676
  this.said.push(`The rule "${title}" is removed.`);
533
677
  return;
534
678
  }
535
- case "set-brand": {
536
- // The colours and the money (FR-100) are set apart: what an edit does not name stays as it was.
537
- const brand = { ...(this.doc.brand ?? {}) };
538
- if (e.accent === null) {
539
- delete brand["accent"];
540
- this.said.push("The app goes back to Graview's colours.");
541
- }
542
- else if (e.accent !== undefined) {
543
- if (!/^#[0-9a-fA-F]{6}$/.test(e.accent))
544
- return this.fail(i, "accent", 'an accent is a colour like "#c2577a"');
545
- brand["accent"] = e.accent;
546
- this.said.push(`The app's accent colour becomes ${e.accent}.`);
547
- }
548
- if (e.currency === null) {
549
- delete brand["currency"];
550
- this.said.push("Money is said with no currency.");
551
- }
552
- else if (e.currency !== undefined) {
553
- if (!/^[A-Z]{3}$/.test(e.currency))
554
- return this.fail(i, "currency", 'a currency is its three-letter code, like "USD" or "EUR"');
555
- brand["currency"] = e.currency;
556
- this.said.push(`Money is said in ${e.currency}.`);
557
- }
558
- if (e.locale === null)
559
- delete brand["locale"];
560
- else if (e.locale !== undefined) {
561
- if (!/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(e.locale))
562
- return this.fail(i, "locale", 'a locale is a language tag, like "en-US" or "de-DE"');
563
- brand["locale"] = e.locale;
564
- this.said.push(`Money is written for ${e.locale}.`);
565
- }
566
- if (e.name) {
567
- brand["name"] = e.name;
568
- this.said.push(`The app's wordmark says ${e.name}.`);
569
- }
570
- // A brand that holds only a name holds nothing to apply.
571
- if (Object.keys(brand).every((key) => key === "name"))
572
- delete this.doc.brand;
679
+ case "set-brand":
680
+ return this.setBrand(i, e);
681
+ case "set-name": {
682
+ const already = this.doc.name === e.name;
683
+ this.doc.name = e.name;
684
+ this.said.push(already ? `The app is already called ${quotedAtTheEnd(e.name)}` : `The app is now called ${quotedAtTheEnd(e.name)}`);
685
+ return;
686
+ }
687
+ case "set-description": {
688
+ const already = (this.doc.description ?? null) === e.description;
689
+ if (e.description === null)
690
+ delete this.doc.description;
573
691
  else
574
- this.doc.brand = brand;
692
+ this.doc.description = e.description;
693
+ this.said.push(e.description === null
694
+ ? already ? "The app already has no line under its name." : "The app has no line under its name."
695
+ : already ? `The line under the app's name already reads ${quotedAtTheEnd(e.description)}` : `The line under the app's name reads ${quotedAtTheEnd(e.description)}`);
575
696
  return;
576
697
  }
577
698
  case "set-label": {
@@ -822,9 +943,12 @@ class Editor {
822
943
  const said = this.removeLensAt(index);
823
944
  this.said.push(`${cap(said.replace(/ \(the app opens at its home again\)$/, ""))} is removed${said.endsWith("again)") ? "; the app opens at its home again" : ""}.`);
824
945
  }
825
- arrangePages(i, e) {
826
- if (e.order === undefined && e.hide === undefined && e.first === undefined)
827
- return this.fail(i, "", 'arrange-pages says at least one of "order", "hide" or "first"');
946
+ arrangePages(i, given) {
947
+ // `overview` is the scene's word as 0.1.15 and 0.1.16 took it (FR-134): read as `scene`, which wins when both are said.
948
+ const { overview, ...rest } = given;
949
+ const e = rest.scene === undefined && overview !== undefined ? { ...rest, scene: overview } : rest;
950
+ if (e.order === undefined && e.hide === undefined && e.first === undefined && e.scene === undefined && e.pages === undefined)
951
+ return this.fail(i, "", 'arrange-pages says at least one of "order", "hide", "first", "scene" or "pages"');
828
952
  const kinds = Object.keys(this.doc.kinds);
829
953
  for (const part of ["order", "hide"]) {
830
954
  for (const [n, kind] of (e[part] ?? []).entries()) {
@@ -873,6 +997,20 @@ class Editor {
873
997
  said.push(`the app opens on "${e.first}"`);
874
998
  }
875
999
  }
1000
+ // What the bar's switch calls the two faces (FR-137); the scene's address stays /places/overview.
1001
+ for (const [face, word] of [["scene", "Scene"], ["pages", "Pages"]]) {
1002
+ const asked = e[face];
1003
+ if (asked === undefined)
1004
+ continue;
1005
+ if (asked === null || asked.trim() === "" || asked.trim() === word) {
1006
+ delete pages[face];
1007
+ said.push(`the switch calls the ${face} ${word}`);
1008
+ }
1009
+ else {
1010
+ pages[face] = asked.trim();
1011
+ said.push(`the switch calls the ${face} "${pages[face]}"`);
1012
+ }
1013
+ }
876
1014
  if (Object.keys(pages).length === 0)
877
1015
  delete this.doc.pages;
878
1016
  this.said.push(`${cap(listOf(said))}.`);
@@ -2277,15 +2415,17 @@ function pruneBlock(doc, b, ctx, r, kind) {
2277
2415
  * `edits.<i>.…`) and no document. The result is NOT compiled here: the caller
2278
2416
  * previews it like any other proposed document.
2279
2417
  */
2280
- export function editDocument(document, edits) {
2418
+ export function editDocument(document, edits, options = {}) {
2281
2419
  if (!Array.isArray(edits))
2282
2420
  return { ok: false, findings: [error("edit", "edits", "edits are a list, like [{\"op\": \"add-field\", …}]")] };
2283
- // No edits is no change: the document as it was, and nothing said.
2421
+ // No edits is no change: the document as it was, in the current spelling (FR-134), and nothing said.
2284
2422
  if (edits.length === 0)
2285
- return { ok: true, document: clone(document), said: [], fills: [] };
2423
+ return { ok: true, document: clone(respellDocument(document).document), said: [], fills: [] };
2286
2424
  if (edits.length > MAX_EDITS)
2287
2425
  return { ok: false, findings: [error("edit", "edits", `at most ${MAX_EDITS} edits at once`)] };
2288
- const editor = new Editor(clone(document), document);
2426
+ // Edited as this build spells it, so what the edit writes says each key's current name (FR-134).
2427
+ const current = respellDocument(document).document;
2428
+ const editor = new Editor(clone(current), current, options);
2289
2429
  edits.forEach((raw, i) => {
2290
2430
  if (editor.findings.length > 0)
2291
2431
  return;