@graview/core 0.1.41 → 0.1.42

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 (104) hide show
  1. package/dist/capabilities.js +1 -1
  2. package/dist/capabilities.js.map +1 -1
  3. package/dist/cli/check/brand.d.ts.map +1 -1
  4. package/dist/cli/check/brand.js +39 -2
  5. package/dist/cli/check/brand.js.map +1 -1
  6. package/dist/cli/document-schema.d.ts.map +1 -1
  7. package/dist/cli/document-schema.js +8 -5
  8. package/dist/cli/document-schema.js.map +1 -1
  9. package/dist/document/brand-check.d.ts +2 -0
  10. package/dist/document/brand-check.d.ts.map +1 -1
  11. package/dist/document/brand-check.js +8 -1
  12. package/dist/document/brand-check.js.map +1 -1
  13. package/dist/document/brand.d.ts.map +1 -1
  14. package/dist/document/brand.js +3 -2
  15. package/dist/document/brand.js.map +1 -1
  16. package/dist/document/connection-spec.d.ts +2 -2
  17. package/dist/document/connection-spec.d.ts.map +1 -1
  18. package/dist/document/connection-spec.js +40 -34
  19. package/dist/document/connection-spec.js.map +1 -1
  20. package/dist/document/describe-place.d.ts.map +1 -1
  21. package/dist/document/describe-place.js +3 -2
  22. package/dist/document/describe-place.js.map +1 -1
  23. package/dist/document/diff.d.ts.map +1 -1
  24. package/dist/document/diff.js +7 -2
  25. package/dist/document/diff.js.map +1 -1
  26. package/dist/document/edit.d.ts.map +1 -1
  27. package/dist/document/edit.js +150 -128
  28. package/dist/document/edit.js.map +1 -1
  29. package/dist/document/graview-template.d.ts +97 -43
  30. package/dist/document/graview-template.d.ts.map +1 -1
  31. package/dist/document/graview-template.js +77 -66
  32. package/dist/document/graview-template.js.map +1 -1
  33. package/dist/document/logo.d.ts +12 -0
  34. package/dist/document/logo.d.ts.map +1 -0
  35. package/dist/document/logo.js +36 -0
  36. package/dist/document/logo.js.map +1 -0
  37. package/dist/document/schema.d.ts +51 -57
  38. package/dist/document/schema.d.ts.map +1 -1
  39. package/dist/document/schema.js +372 -338
  40. package/dist/document/schema.js.map +1 -1
  41. package/dist/document.schema.json +30 -2
  42. package/dist/history.d.ts +32 -3
  43. package/dist/history.d.ts.map +1 -1
  44. package/dist/history.js.map +1 -1
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/installation.d.ts +1 -1
  50. package/dist/installation.d.ts.map +1 -1
  51. package/dist/installation.js +1 -1
  52. package/dist/installation.js.map +1 -1
  53. package/dist/mutations/node-ref.d.ts +1 -1
  54. package/dist/mutations/node-ref.d.ts.map +1 -1
  55. package/dist/mutations/node-ref.js +1 -1
  56. package/dist/mutations/node-ref.js.map +1 -1
  57. package/dist/ops/revisions.d.ts +19 -4
  58. package/dist/ops/revisions.d.ts.map +1 -1
  59. package/dist/ops/revisions.js +23 -8
  60. package/dist/ops/revisions.js.map +1 -1
  61. package/dist/past.d.ts +15 -0
  62. package/dist/past.d.ts.map +1 -1
  63. package/dist/past.js +148 -18
  64. package/dist/past.js.map +1 -1
  65. package/dist/schema/app-link-field.d.ts +1 -1
  66. package/dist/schema/app-link-field.d.ts.map +1 -1
  67. package/dist/schema/app-link-field.js +1 -1
  68. package/dist/schema/app-link-field.js.map +1 -1
  69. package/dist/schema/file-field.d.ts +1 -1
  70. package/dist/schema/file-field.d.ts.map +1 -1
  71. package/dist/schema/file-field.js +1 -1
  72. package/dist/schema/file-field.js.map +1 -1
  73. package/dist/schema/linked-field.d.ts +1 -1
  74. package/dist/schema/linked-field.d.ts.map +1 -1
  75. package/dist/schema/linked-field.js +1 -1
  76. package/dist/schema/linked-field.js.map +1 -1
  77. package/dist/schema/markdown-field.d.ts +1 -1
  78. package/dist/schema/markdown-field.d.ts.map +1 -1
  79. package/dist/schema/markdown-field.js +1 -1
  80. package/dist/schema/markdown-field.js.map +1 -1
  81. package/dist/schema/repeat-field.d.ts +1 -1
  82. package/dist/schema/repeat-field.d.ts.map +1 -1
  83. package/dist/schema/repeat-field.js +1 -1
  84. package/dist/schema/repeat-field.js.map +1 -1
  85. package/dist/schema/typed-field-builders.d.ts +1 -1
  86. package/dist/schema/typed-field-builders.d.ts.map +1 -1
  87. package/dist/schema/typed-field-builders.js +1 -1
  88. package/dist/schema/typed-field-builders.js.map +1 -1
  89. package/dist/temporal/effectivity.d.ts +1 -1
  90. package/dist/temporal/effectivity.d.ts.map +1 -1
  91. package/dist/temporal/effectivity.js +1 -1
  92. package/dist/temporal/effectivity.js.map +1 -1
  93. package/dist/testing.js +1 -1
  94. package/dist/testing.js.map +1 -1
  95. package/dist/theme/marks.d.ts +13 -2
  96. package/dist/theme/marks.d.ts.map +1 -1
  97. package/dist/theme/marks.js +16 -0
  98. package/dist/theme/marks.js.map +1 -1
  99. package/dist/theme/types.d.ts +17 -2
  100. package/dist/theme/types.d.ts.map +1 -1
  101. package/dist/theme/types.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { DERIVED_ACTS, derivedActOf, derivedActsOf, givenWhenNeverArchived, givenWhenNeverTidied, RELATION_ACTS, relationActName, relationActsOf } from "../mutations/derived-names.js";
2
2
  import { documentRelations, givesRelationAct, renamedRelationAct } from "./relation-acts.js";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
5
5
  import { printExpr } from "./expr/print.js";
6
6
  import { error } from "./findings.js";
@@ -24,6 +24,7 @@ import { computedOf, validateComputed } from "./computed.js";
24
24
  import { parseTemplate, templateBraces, TemplateError } from "./template-parse.js";
25
25
  import { validateViews, VIEW_SLOTS } from "./views.js";
26
26
  import { brandFindings, emojiIconSentence } from "./brand-check.js";
27
+ import { darkLogoSentence, logoParts } from "./logo.js";
27
28
  import { emojiOf } from "../theme/marks.js";
28
29
  import { canonicalize } from "./canonical.js";
29
30
  import { quotedAtTheEnd } from "./quote.js";
@@ -83,128 +84,145 @@ export const EDIT_OPS = [
83
84
  const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
84
85
  const MAX_EDITS = 50;
85
86
  // ── shapes ─────────────────────────────────────────────────────────────────
86
- const kindName = z.string().regex(NAME, 'kind names are lower-case words joined by hyphens, like "guest-list"');
87
- const listKey = z.string().min(1).max(120);
88
- const listDirection = z.enum(["asc", "desc"]).optional();
89
- const listSortShape = z.union([listKey, z.object({ by: listKey, direction: listDirection, then: z.union([listKey, z.object({ by: listKey, direction: listDirection }).strict()]).optional() }).strict()]);
90
- const listGroupShape = z.union([listKey, z.object({ by: listKey, order: z.array(z.string().min(1).max(80)).max(40).optional(), closed: z.array(z.string().min(1).max(80)).max(40).optional() }).strict()]);
91
- const fieldName = z.string().regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate"');
92
- const edgeName = z.string().regex(EDGE_NAME, 'relation names are one camelCase word, like "tendedBy"');
93
- const actName = z.string().regex(NAME, 'act names are lower-case words joined by hyphens, like "mark-paid"');
94
- const lensTitle = z.string().min(1).max(80);
95
- const SHAPES = {
96
- "add-kind": z.object({ op: z.literal("add-kind"), kind: kindName, act: z.boolean().optional() }).passthrough(),
97
- "rename-kind": z.object({ op: z.literal("rename-kind"), kind: kindName, to: kindName, noun: z.string().min(1).max(40).optional(), plural: z.string().min(1).max(40).optional() }).strict(),
98
- "remove-kind": z.object({ op: z.literal("remove-kind"), kind: kindName }).strict(),
99
- "add-field": z.object({ op: z.literal("add-field"), kind: kindName, field: fieldName, fill: z.unknown().optional(), spec: z.record(z.string(), z.unknown()).optional() }).passthrough(),
100
- "rename-field": z.object({ op: z.literal("rename-field"), kind: kindName, field: fieldName, to: fieldName }).strict(),
101
- "retype-field": z.object({ op: z.literal("retype-field"), kind: kindName, field: fieldName, type: z.enum(FIELD_TYPES), options: z.array(z.string().min(1).max(80)).min(1).max(100).optional(), of: z.enum(["string", "number", "date", "image", "video", "file"]).optional(), format: z.enum(["money", "percent", "duration", "markdown"]).optional() }).strict(),
102
- "set-options": z.object({ op: z.literal("set-options"), kind: kindName, field: fieldName, add: z.array(z.string().min(1).max(80)).optional(), remove: z.array(z.string().min(1).max(80)).optional() }).strict(),
103
- "set-format": z.object({ op: z.literal("set-format"), kind: kindName, field: fieldName, format: z.union([z.enum(["money", "percent", "duration", "markdown"]), z.null()]).optional(), unit: z.union([z.string().min(1).max(12), z.null()]).optional() }).strict(),
104
- "set-range": z.object({ op: z.literal("set-range"), kind: kindName, field: fieldName, min: z.union([z.number(), z.null()]).optional(), max: z.union([z.number(), z.null()]).optional(), step: z.union([z.number().positive(), z.null()]).optional() }).strict(),
105
- "set-required": z.object({ op: z.literal("set-required"), kind: kindName, field: fieldName, required: z.boolean() }).strict(),
106
- "set-default": z.object({ op: z.literal("set-default"), kind: kindName, field: fieldName, default: z.unknown() }).strict(),
107
- "set-repeats": z.object({ op: z.literal("set-repeats"), kind: kindName, field: fieldName, repeats: z.boolean() }).strict(),
108
- "remove-field": z.object({ op: z.literal("remove-field"), kind: kindName, field: fieldName }).strict(),
109
- "add-relation": z.object({ op: z.literal("add-relation"), kind: kindName, relation: edgeName }).passthrough(),
110
- "rename-relation": z.object({ op: z.literal("rename-relation"), kind: kindName, relation: edgeName, to: edgeName }).strict(),
111
- "remove-relation": z.object({ op: z.literal("remove-relation"), kind: kindName, relation: edgeName }).strict(),
112
- "add-act": z.object({ op: z.literal("add-act"), act: actName, replace: z.boolean().optional() }).passthrough(),
113
- "remove-act": z.object({ op: z.literal("remove-act"), act: actName }).strict(),
114
- "add-rule": z.object({ op: z.literal("add-rule"), rule: actName, replace: z.boolean().optional() }).passthrough(),
115
- "remove-rule": z.object({ op: z.literal("remove-rule"), rule: actName }).strict(),
116
- "set-brand": z
117
- .object({
118
- op: z.literal("set-brand"),
119
- accent: z.union([z.string(), z.null()]).optional(),
120
- name: z.union([z.string().min(1).max(60), z.null()]).optional(),
121
- currency: z.union([z.string(), z.null()]).optional(),
122
- locale: z.union([z.string(), z.null()]).optional(),
123
- // An IANA time zone its instants are said in (Roadmap f-app-time-zone).
124
- timeZone: z.union([z.string().min(1).max(64), z.null()]).optional(),
125
- // The rest of the brand (FR-124, FR-125): each as the document holds it, null to clear it.
126
- 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(),
127
- favicon: z.union([z.string().min(1), z.object({ emoji: z.string() }).strict(), z.null()]).optional(),
128
- 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(),
129
- 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(),
130
- accents: z.union([z.record(z.string(), z.union([z.number().min(0).max(360), z.null()])), z.null()]).optional(),
131
- scheme: z.union([z.enum(["light", "dark", "auto"]), z.null()]).optional(),
132
- })
133
- .strict(),
134
- "set-name": z.object({ op: z.literal("set-name"), name: z.string().min(1).max(80) }).strict(),
135
- "set-description": z.object({ op: z.literal("set-description"), description: z.union([z.string().min(1).max(500), z.null()]) }).strict(),
136
- "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(),
137
- "set-describe": z.object({ op: z.literal("set-describe"), kind: kindName, describe: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
138
- // A kind's slot; the front page (`slot: "home"`, no kind); or a blocks lens's blocks, by its title (FR-84).
139
- "set-view": z.object({ op: z.literal("set-view"), kind: kindName.optional(), slot: z.enum([...VIEW_SLOTS, "home"]).optional(), lens: lensTitle.optional(), blocks: z.union([z.array(z.unknown()).min(1), z.null()]) }).strict(),
140
- "set-glance": z.object({ op: z.literal("set-glance"), kind: kindName, fields: z.array(fieldName).max(20) }).strict(),
141
- // How a record's page orders and groups its facts (FR-148): these first, then each group under its title; none of either takes the choice away.
142
- "set-page-fields": z
143
- .object({
144
- op: z.literal("set-page-fields"),
145
- kind: kindName,
146
- fields: z.array(fieldName).max(40),
147
- groups: z.union([z.array(z.object({ title: z.string().min(1).max(60), fields: z.array(fieldName).min(1).max(40) }).strict()).max(12), z.null()]).optional(),
148
- })
149
- .strict(),
150
- "add-lens": z
151
- .object({
152
- op: z.literal("add-lens"),
153
- title: lensTitle,
154
- lens: z.string().min(1).max(40).optional(),
155
- on: kindName.optional(),
156
- bindings: z.record(z.string(), z.unknown()).optional(),
157
- options: z.record(z.string(), z.unknown()).optional(),
158
- // False: the place's page leaves off the forms that begin its kind.
159
- create: z.boolean().optional(),
160
- at: z.number().int().min(0).optional(),
161
- replace: lensTitle.optional(),
162
- })
163
- .strict(),
164
- "remove-lens": z.object({ op: z.literal("remove-lens"), title: lensTitle, on: kindName.optional() }).strict(),
165
- // A lens kept off the bar's places, the page list and the home's gallery, still reached by its address, a link or navigate.
166
- "set-lens-hidden": z.object({ op: z.literal("set-lens-hidden"), title: lensTitle, on: kindName.optional(), hidden: z.boolean() }).strict(),
167
- "set-home": z.object({ op: z.literal("set-home"), blocks: z.union([z.array(z.unknown()).min(1), z.null()]) }).strict(),
168
- "arrange-pages": z
169
- .object({
170
- op: z.literal("arrange-pages"),
171
- order: z.union([z.array(kindName).max(40), z.null()]).optional(),
172
- hide: z.union([z.array(kindName).max(40), z.null()]).optional(),
173
- first: z.union([z.string().min(1).max(80), z.null()]).optional(),
174
- primary: z.union([z.array(z.string().min(1).max(80)).max(40), z.null()]).optional(),
175
- scene: z.union([z.string().min(1).max(40), z.null()]).optional(),
176
- pages: z.union([z.string().min(1).max(40), z.null()]).optional(),
177
- // What a kind's list offers to filter by: `{ "request": ["status", "became.stage"] }`; null or [] gives a kind back its derived filters.
178
- filters: z.record(kindName, z.union([z.array(z.string().min(1).max(120)).max(40), z.null()])).optional(),
179
- // The order a kind's list opens in: `{ "feature": { "by": "score", "direction": "desc", "then": "label" } }` or a key; null gives it back its own.
180
- sort: z.record(kindName, z.union([listSortShape, z.null()])).optional(),
181
- // How a kind's list opens grouped: `{ "request": { "by": "status", "order": [...], "closed": ["done"] } }` or a field; null opens it ungrouped.
182
- group: z.record(kindName, z.union([listGroupShape, z.null()])).optional(),
183
- // The scene's word as a chat or a tool written before 0.1.17 sends it (FR-132's key, RESPELLED as `scene`).
184
- overview: z.union([z.string().min(1).max(40), z.null()]).optional(),
185
- })
186
- .strict(),
187
- "set-computed": z
188
- .object({
189
- op: z.literal("set-computed"),
190
- kind: kindName,
191
- name: fieldName,
192
- expr: z.union([z.string().min(1).max(2000), z.null()]).optional(),
193
- label: z.union([z.string().min(1).max(60), z.null()]).optional(),
194
- description: z.union([z.string().min(1).max(500), z.null()]).optional(),
195
- })
196
- .strict(),
197
- // Who the people are, and who may do and see what (FR-170): a grant and a sight as the document holds one.
198
- "set-people": z.object({ op: z.literal("set-people"), kind: kindName, people: z.boolean() }).strict(),
199
- // Whether a kind's records are archived and merged, or only kept and removed (FR-173).
200
- "set-archives": z.object({ op: z.literal("set-archives"), kind: kindName, archives: z.boolean() }).strict(),
201
- // What the app's own actor may do on its own to a kind, or to one relation of it (`@graview/core/tidying`).
202
- "set-tidying": z.object({ op: z.literal("set-tidying"), kind: kindName, relation: z.optional(z.string()), tidying: z.enum(["never", "no-links", "allowed"]) }).strict(),
203
- "add-grant": z.object({ op: z.literal("add-grant") }).passthrough(),
204
- "remove-grant": z.object({ op: z.literal("remove-grant") }).passthrough(),
205
- "add-sight": z.object({ op: z.literal("add-sight") }).passthrough(),
206
- "remove-sight": z.object({ op: z.literal("remove-sight") }).passthrough(),
207
- };
87
+ /*
88
+ * Each op's shape, built the first time an edit is read rather than when the
89
+ * module loads: a Worker or a page that imports the document format and
90
+ * never edits one does not build sixty zod schemas to get there.
91
+ */
92
+ function buildShapes() {
93
+ const kindName = z.string().regex(NAME, 'kind names are lower-case words joined by hyphens, like "guest-list"');
94
+ const listKey = z.string().min(1).max(120);
95
+ const listDirection = z.enum(["asc", "desc"]).optional();
96
+ const listSortShape = z.union([listKey, z.object({ by: listKey, direction: listDirection, then: z.union([listKey, z.object({ by: listKey, direction: listDirection }).strict()]).optional() }).strict()]);
97
+ const listGroupShape = z.union([listKey, z.object({ by: listKey, order: z.array(z.string().min(1).max(80)).max(40).optional(), closed: z.array(z.string().min(1).max(80)).max(40).optional() }).strict()]);
98
+ const fieldName = z.string().regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate"');
99
+ const edgeName = z.string().regex(EDGE_NAME, 'relation names are one camelCase word, like "tendedBy"');
100
+ const actName = z.string().regex(NAME, 'act names are lower-case words joined by hyphens, like "mark-paid"');
101
+ const lensTitle = z.string().min(1).max(80);
102
+ return {
103
+ "add-kind": z.object({ op: z.literal("add-kind"), kind: kindName, act: z.boolean().optional() }).passthrough(),
104
+ "rename-kind": z.object({ op: z.literal("rename-kind"), kind: kindName, to: kindName, noun: z.string().min(1).max(40).optional(), plural: z.string().min(1).max(40).optional() }).strict(),
105
+ "remove-kind": z.object({ op: z.literal("remove-kind"), kind: kindName }).strict(),
106
+ "add-field": z.object({ op: z.literal("add-field"), kind: kindName, field: fieldName, fill: z.unknown().optional(), spec: z.record(z.string(), z.unknown()).optional() }).passthrough(),
107
+ "rename-field": z.object({ op: z.literal("rename-field"), kind: kindName, field: fieldName, to: fieldName }).strict(),
108
+ "retype-field": z.object({ op: z.literal("retype-field"), kind: kindName, field: fieldName, type: z.enum(FIELD_TYPES), options: z.array(z.string().min(1).max(80)).min(1).max(100).optional(), of: z.enum(["string", "number", "date", "image", "video", "file"]).optional(), format: z.enum(["money", "percent", "duration", "markdown"]).optional() }).strict(),
109
+ "set-options": z.object({ op: z.literal("set-options"), kind: kindName, field: fieldName, add: z.array(z.string().min(1).max(80)).optional(), remove: z.array(z.string().min(1).max(80)).optional() }).strict(),
110
+ "set-format": z.object({ op: z.literal("set-format"), kind: kindName, field: fieldName, format: z.union([z.enum(["money", "percent", "duration", "markdown"]), z.null()]).optional(), unit: z.union([z.string().min(1).max(12), z.null()]).optional() }).strict(),
111
+ "set-range": z.object({ op: z.literal("set-range"), kind: kindName, field: fieldName, min: z.union([z.number(), z.null()]).optional(), max: z.union([z.number(), z.null()]).optional(), step: z.union([z.number().positive(), z.null()]).optional() }).strict(),
112
+ "set-required": z.object({ op: z.literal("set-required"), kind: kindName, field: fieldName, required: z.boolean() }).strict(),
113
+ "set-default": z.object({ op: z.literal("set-default"), kind: kindName, field: fieldName, default: z.unknown() }).strict(),
114
+ "set-repeats": z.object({ op: z.literal("set-repeats"), kind: kindName, field: fieldName, repeats: z.boolean() }).strict(),
115
+ "remove-field": z.object({ op: z.literal("remove-field"), kind: kindName, field: fieldName }).strict(),
116
+ "add-relation": z.object({ op: z.literal("add-relation"), kind: kindName, relation: edgeName }).passthrough(),
117
+ "rename-relation": z.object({ op: z.literal("rename-relation"), kind: kindName, relation: edgeName, to: edgeName }).strict(),
118
+ "remove-relation": z.object({ op: z.literal("remove-relation"), kind: kindName, relation: edgeName }).strict(),
119
+ "add-act": z.object({ op: z.literal("add-act"), act: actName, replace: z.boolean().optional() }).passthrough(),
120
+ "remove-act": z.object({ op: z.literal("remove-act"), act: actName }).strict(),
121
+ "add-rule": z.object({ op: z.literal("add-rule"), rule: actName, replace: z.boolean().optional() }).passthrough(),
122
+ "remove-rule": z.object({ op: z.literal("remove-rule"), rule: actName }).strict(),
123
+ "set-brand": z
124
+ .object({
125
+ op: z.literal("set-brand"),
126
+ accent: z.union([z.string(), z.null()]).optional(),
127
+ name: z.union([z.string().min(1).max(60), z.null()]).optional(),
128
+ currency: z.union([z.string(), z.null()]).optional(),
129
+ locale: z.union([z.string(), z.null()]).optional(),
130
+ // An IANA time zone its instants are said in (Roadmap f-app-time-zone).
131
+ timeZone: z.union([z.string().min(1).max(64), z.null()]).optional(),
132
+ // The rest of the brand (FR-124, FR-125): each as the document holds it, null to clear it.
133
+ logo: z
134
+ .union([
135
+ z.string().min(1),
136
+ z.object({ src: z.string().min(1), alt: z.string().min(1).max(120).optional() }).strict(),
137
+ // One for each scheme; with no dark one, `darkFallback: "invert"` asks for the light one inverted in the dark.
138
+ z.object({ light: z.string().min(1), dark: z.string().min(1).optional(), alt: z.string().min(1).max(120).optional(), darkFallback: z.literal("invert").optional() }).strict(),
139
+ z.null(),
140
+ ])
141
+ .optional(),
142
+ favicon: z.union([z.string().min(1), z.object({ emoji: z.string() }).strict(), z.null()]).optional(),
143
+ 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(),
144
+ 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(),
145
+ accents: z.union([z.record(z.string(), z.union([z.number().min(0).max(360), z.null()])), z.null()]).optional(),
146
+ scheme: z.union([z.enum(["light", "dark", "auto"]), z.null()]).optional(),
147
+ })
148
+ .strict(),
149
+ "set-name": z.object({ op: z.literal("set-name"), name: z.string().min(1).max(80) }).strict(),
150
+ "set-description": z.object({ op: z.literal("set-description"), description: z.union([z.string().min(1).max(500), z.null()]) }).strict(),
151
+ "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(),
152
+ "set-describe": z.object({ op: z.literal("set-describe"), kind: kindName, describe: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
153
+ // A kind's slot; the front page (`slot: "home"`, no kind); or a blocks lens's blocks, by its title (FR-84).
154
+ "set-view": z.object({ op: z.literal("set-view"), kind: kindName.optional(), slot: z.enum([...VIEW_SLOTS, "home"]).optional(), lens: lensTitle.optional(), blocks: z.union([z.array(z.unknown()).min(1), z.null()]) }).strict(),
155
+ "set-glance": z.object({ op: z.literal("set-glance"), kind: kindName, fields: z.array(fieldName).max(20) }).strict(),
156
+ // How a record's page orders and groups its facts (FR-148): these first, then each group under its title; none of either takes the choice away.
157
+ "set-page-fields": z
158
+ .object({
159
+ op: z.literal("set-page-fields"),
160
+ kind: kindName,
161
+ fields: z.array(fieldName).max(40),
162
+ groups: z.union([z.array(z.object({ title: z.string().min(1).max(60), fields: z.array(fieldName).min(1).max(40) }).strict()).max(12), z.null()]).optional(),
163
+ })
164
+ .strict(),
165
+ "add-lens": z
166
+ .object({
167
+ op: z.literal("add-lens"),
168
+ title: lensTitle,
169
+ lens: z.string().min(1).max(40).optional(),
170
+ on: kindName.optional(),
171
+ bindings: z.record(z.string(), z.unknown()).optional(),
172
+ options: z.record(z.string(), z.unknown()).optional(),
173
+ // False: the place's page leaves off the forms that begin its kind.
174
+ create: z.boolean().optional(),
175
+ at: z.number().int().min(0).optional(),
176
+ replace: lensTitle.optional(),
177
+ })
178
+ .strict(),
179
+ "remove-lens": z.object({ op: z.literal("remove-lens"), title: lensTitle, on: kindName.optional() }).strict(),
180
+ // A lens kept off the bar's places, the page list and the home's gallery, still reached by its address, a link or navigate.
181
+ "set-lens-hidden": z.object({ op: z.literal("set-lens-hidden"), title: lensTitle, on: kindName.optional(), hidden: z.boolean() }).strict(),
182
+ "set-home": z.object({ op: z.literal("set-home"), blocks: z.union([z.array(z.unknown()).min(1), z.null()]) }).strict(),
183
+ "arrange-pages": z
184
+ .object({
185
+ op: z.literal("arrange-pages"),
186
+ order: z.union([z.array(kindName).max(40), z.null()]).optional(),
187
+ hide: z.union([z.array(kindName).max(40), z.null()]).optional(),
188
+ first: z.union([z.string().min(1).max(80), z.null()]).optional(),
189
+ primary: z.union([z.array(z.string().min(1).max(80)).max(40), z.null()]).optional(),
190
+ scene: z.union([z.string().min(1).max(40), z.null()]).optional(),
191
+ pages: z.union([z.string().min(1).max(40), z.null()]).optional(),
192
+ // What a kind's list offers to filter by: `{ "request": ["status", "became.stage"] }`; null or [] gives a kind back its derived filters.
193
+ filters: z.record(kindName, z.union([z.array(z.string().min(1).max(120)).max(40), z.null()])).optional(),
194
+ // The order a kind's list opens in: `{ "feature": { "by": "score", "direction": "desc", "then": "label" } }` or a key; null gives it back its own.
195
+ sort: z.record(kindName, z.union([listSortShape, z.null()])).optional(),
196
+ // How a kind's list opens grouped: `{ "request": { "by": "status", "order": [...], "closed": ["done"] } }` or a field; null opens it ungrouped.
197
+ group: z.record(kindName, z.union([listGroupShape, z.null()])).optional(),
198
+ // The scene's word as a chat or a tool written before 0.1.17 sends it (FR-132's key, RESPELLED as `scene`).
199
+ overview: z.union([z.string().min(1).max(40), z.null()]).optional(),
200
+ })
201
+ .strict(),
202
+ "set-computed": z
203
+ .object({
204
+ op: z.literal("set-computed"),
205
+ kind: kindName,
206
+ name: fieldName,
207
+ expr: z.union([z.string().min(1).max(2000), z.null()]).optional(),
208
+ label: z.union([z.string().min(1).max(60), z.null()]).optional(),
209
+ description: z.union([z.string().min(1).max(500), z.null()]).optional(),
210
+ })
211
+ .strict(),
212
+ // Who the people are, and who may do and see what (FR-170): a grant and a sight as the document holds one.
213
+ "set-people": z.object({ op: z.literal("set-people"), kind: kindName, people: z.boolean() }).strict(),
214
+ // Whether a kind's records are archived and merged, or only kept and removed (FR-173).
215
+ "set-archives": z.object({ op: z.literal("set-archives"), kind: kindName, archives: z.boolean() }).strict(),
216
+ // What the app's own actor may do on its own to a kind, or to one relation of it (`@graview/core/tidying`).
217
+ "set-tidying": z.object({ op: z.literal("set-tidying"), kind: kindName, relation: z.optional(z.string()), tidying: z.enum(["never", "no-links", "allowed"]) }).strict(),
218
+ "add-grant": z.object({ op: z.literal("add-grant") }).passthrough(),
219
+ "remove-grant": z.object({ op: z.literal("remove-grant") }).passthrough(),
220
+ "add-sight": z.object({ op: z.literal("add-sight") }).passthrough(),
221
+ "remove-sight": z.object({ op: z.literal("remove-sight") }).passthrough(),
222
+ };
223
+ }
224
+ let shapes;
225
+ const opShapes = () => (shapes ??= buildShapes());
208
226
  const NONE = new Set();
209
227
  const clone = (v) => structuredClone(v);
210
228
  const asArray = (v) => [v ?? []].flat();
@@ -588,8 +606,10 @@ class Editor {
588
606
  delete brand[key];
589
607
  else
590
608
  brand[key] = e[key];
591
- const src = (mark) => (typeof mark === "string" ? mark : isObject(mark) ? mark["src"] : undefined);
609
+ // A logo's light one is its `src` or its `light`; its dark scheme is said on its own (logo.ts).
610
+ const src = (mark) => (key === "logo" ? logoParts(mark)?.light : typeof mark === "string" ? mark : isObject(mark) ? mark["src"] : undefined);
592
611
  const alt = (mark) => (isObject(mark) && typeof mark["alt"] === "string" ? mark["alt"] : undefined);
612
+ const dark = key === "logo" && e[key] !== null ? darkLogoSentence(was, e[key]) : undefined;
593
613
  if (e[key] === null)
594
614
  this.said.push(`The app has no ${what}.`);
595
615
  else if (key === "favicon" && emojiOf(e[key]) !== undefined)
@@ -598,10 +618,12 @@ class Editor {
598
618
  this.said.push(`The app gets a ${what}.`);
599
619
  else if (canonicalize(was) === canonicalize(e[key]))
600
620
  this.said.push(`The ${what} stays as it was.`);
601
- else if (src(was) === src(e[key]))
602
- this.said.push(`The ${what} is said as ${quotedAtTheEnd(alt(e[key]) ?? brand["name"] ?? this.doc.name)}`);
603
- else
621
+ else if (src(was) !== src(e[key]))
604
622
  this.said.push(`The ${what} changes.`);
623
+ else if (alt(was) !== alt(e[key]) || !dark)
624
+ this.said.push(`The ${what} is said as ${quotedAtTheEnd(alt(e[key]) ?? brand["name"] ?? this.doc.name)}`);
625
+ if (dark)
626
+ this.said.push(dark);
605
627
  }
606
628
  const merge = (key, say, cleared) => {
607
629
  if (e[key] === undefined)
@@ -687,7 +709,7 @@ class Editor {
687
709
  this.fail(i, "op", `${op === undefined ? "an edit names its op" : `"${String(op)}" is not an edit Graview knows`}`, `one of ${EDIT_OPS.join(", ")}`);
688
710
  return;
689
711
  }
690
- const parsed = SHAPES[op].safeParse(edit);
712
+ const parsed = opShapes()[op].safeParse(edit);
691
713
  if (!parsed.success) {
692
714
  this.refuse(i, parsed.error.issues, op);
693
715
  return;