@valbuild/shared 0.151.0 → 0.152.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.152.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1015](https://github.com/valbuild/val/pull/1015) [`08a3977`](https://github.com/valbuild/val/commit/08a39779dbe43702a72ae57cbf8cf29b14e64672) Thanks [@freekh](https://github.com/freekh)! - The Sketchpad covers several pages, builds what it shows, and can be linked to the content.
8
+
9
+ - **Route dividers.** A sketchpad is no longer about one page. A `--- /about` line starts the part about a page, so one sketch can cover a page and the pages it links to. Press a route to see that page beside the sketch, in the Studio's own live preview, which follows edits as they are made. A divider's menu changes its page, or adds a new one from a page type the site has (`/blog/[slug]`). A route the site has no page type for is allowed too, and is marked as needing code.
10
+ - **Start from a page.** An empty sketchpad offers to start from a page. The assistant writes that page into it, images and all, and changes nothing.
11
+ - **Build.** The button that sent the sketchpad to the assistant is now **Build**. It sends the sketchpad as a card in the chat (press it to open the sketchpad), with a request to change the pages to match the sketch. It is disabled while the sketch is empty.
12
+ - **Images that exist.** A box shows an image from a gallery, from the site's content, or one uploaded to the chat, and the picker offers exactly those, with search and Upload (which puts the file in the chat first). A box naming any other image says so. When the assistant writes a sketch with an image that is not in the project, the write is refused. Images are named by their content path (`/public/val/hero.webp`) and shown where the site serves them, including uploads that are not published yet.
13
+ - **Block types.** A box's kind is a combobox of the kinds of section the site's pages have, read from the schema. Any other word is allowed, and is marked as a new type that needs code.
14
+ - **Drag.** Blocks, rows, columns and images can be dragged to a new place. Columns share a row equally, so the width controls are gone.
15
+ - **Highlights.** The assistant can point at blocks, in a colour and with a note (for example, the ones that need code), with `highlight_sketchpad_blocks`. The highlights can be cleared, by the person or by the assistant.
16
+ - **Linked text (a try-out).** A heading or paragraph can be linked to a plain text field with `{field: <source path>}`. Typing in it then changes that field, as typing in the field would. The block shows what it is linked to and has an **Unlink** button. When the field was changed elsewhere, the block says so and offers the field's text. A sketchpad the assistant starts from a page links its text this way.
17
+ - **Converting images.** The assistant can convert an image in the chat to WebP, or scale it down, with `convert_session_image`, using the same encoder as the Studio's uploads.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#1013](https://github.com/valbuild/val/pull/1013) [`3bb76d7`](https://github.com/valbuild/val/commit/3bb76d74d82b499aa15023838d63018a70fd7ce7) Thanks [@freekh](https://github.com/freekh)! - A save that the content service fails to take no longer looks like a successful one.
22
+
23
+ Before, when the content service answered a save with a server error (or could not be reached), the Studio treated the edit as invalid and dropped it: the field went back to its old value, and the status bar kept saying "All changes saved". Now:
24
+
25
+ - **Your edit is kept.** The field keeps what you typed.
26
+ - **The Studio says the save failed.** The status bar shows "Could not save" (never "All changes saved" while a save has failed or is still pending), and a notice says which service failed, with the service's own message under **Details**.
27
+ - **Try again** sends the same save again. The Studio also retries by itself in the background, so if you leave the tab open your changes are saved once the service is back.
28
+
29
+ Pressing Publish while a save has failed now says what failed, in the service's words, with a **Save again** button, instead of "Check the connection and try again".
30
+
31
+ Saves that the service refuses because of your plan, and saves that conflict with someone else's change, work as before.
32
+
33
+ Error messages from the content service also stop blaming your network. "Could not get your changes. It is most likely due to a network issue. Check your network connection" was shown even when the service had answered with a 500. These messages now say what actually happened (the service answered with an error, refused the request, or could not be reached from your site's server) and name the service. The status code and the service's response are kept as details for whoever needs to look into it. Publish failures now also show the service's own message, where they used to say "Unknown error".
34
+
3
35
  ## 0.151.0
4
36
 
5
37
  ### Patch Changes
@@ -1265,6 +1265,22 @@ export declare const Api: {
1265
1265
  }, z.core.$strip>]>;
1266
1266
  headVersion: z.ZodOptional<z.ZodNumber>;
1267
1267
  }, z.core.$strip>;
1268
+ }, z.core.$strip>, z.ZodObject<{
1269
+ status: z.ZodLiteral<502>;
1270
+ json: z.ZodObject<{
1271
+ type: z.ZodLiteral<"service-error">;
1272
+ message: z.ZodString;
1273
+ details: z.ZodOptional<z.ZodString>;
1274
+ newPatchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
1275
+ parentRef: z.ZodUnion<readonly [z.ZodObject<{
1276
+ type: z.ZodLiteral<"head">;
1277
+ headBaseSha: z.ZodString;
1278
+ }, z.core.$strip>, z.ZodObject<{
1279
+ type: z.ZodLiteral<"patch">;
1280
+ patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
1281
+ }, z.core.$strip>]>;
1282
+ headVersion: z.ZodOptional<z.ZodNumber>;
1283
+ }, z.core.$strip>;
1268
1284
  }, z.core.$strip>, z.ZodObject<{
1269
1285
  status: z.ZodLiteral<200>;
1270
1286
  json: z.ZodObject<{
@@ -1315,6 +1331,7 @@ export declare const Api: {
1315
1331
  error: z.ZodObject<{
1316
1332
  message: z.ZodString;
1317
1333
  }, z.core.$strip>;
1334
+ details: z.ZodOptional<z.ZodString>;
1318
1335
  }, z.core.$strip>;
1319
1336
  }, z.core.$strip>, z.ZodObject<{
1320
1337
  status: z.ZodLiteral<200>;
@@ -1,2 +1,2 @@
1
- export declare const toolNames: readonly ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad"];
1
+ export declare const toolNames: readonly ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "convert_session_image", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad", "highlight_sketchpad_blocks", "clear_sketchpad_highlights"];
2
2
  export type ToolName = (typeof toolNames)[number];
@@ -2,18 +2,24 @@
2
2
  * A page sketch: a page written as indented text, with four primitives.
3
3
  *
4
4
  * ```
5
+ * --- /about a route: what follows is the page at /about
5
6
  * # Our people text (a heading: #, ## or ###)
6
7
  * We are 40 people in Oslo. text (a paragraph)
8
+ * ## Hi {field: /x.val.ts?p="t"} text linked to a content field: typing
9
+ * in it writes the field
7
10
  * [image: Team photo] placeholder: [kind] or [kind: label]
8
11
  * [hero: Roof](image: /a.jpg) a placeholder with an image (or video) in it
9
12
  * icon hero M3 5h18v14H3z how every [hero] box is drawn: SVG path data
10
13
  * row its children side by side
11
- * col 2 its children stacked; 2 shares of the row
12
- * ## Design
14
+ * col its children stacked; columns share a row
15
+ * ## Design equally
13
16
  * col
14
17
  * [button: Join us]
15
18
  * ```
16
19
  *
20
+ * A sketchpad is not one page's: a route divider starts the part about a
21
+ * page, so one sketch can cover a page and the two it links to.
22
+ *
17
23
  * It is a scratch pad, not a layout language: it says WHAT is on a page and
18
24
  * roughly WHERE, so an assistant and a person can agree on a page's content
19
25
  * before anyone worries about how it looks. The person never sees this text,
@@ -31,17 +37,23 @@
31
37
  * image. Media put in a box is written into the text too, so the assistant
32
38
  * that builds the page reads it exactly as the person left it.
33
39
  */
34
- export type SketchNode = SketchRow | SketchCol | SketchText | SketchPlaceholder;
40
+ export type SketchNode = SketchRoute | SketchRow | SketchCol | SketchText | SketchPlaceholder;
41
+ /** The start of the part about one page. Only at the top level. */
42
+ export type SketchRoute = {
43
+ type: "route";
44
+ line: number;
45
+ /** The page's route as the site serves it: `/about`, `/blog/[slug]`. */
46
+ route: string;
47
+ };
35
48
  export type SketchRow = {
36
49
  type: "row";
37
50
  line: number;
38
51
  children: SketchNode[];
39
52
  };
53
+ /** Columns share their row equally: a sketch says what, not how wide. */
40
54
  export type SketchCol = {
41
55
  type: "col";
42
56
  line: number;
43
- /** Shares of the enclosing row this column takes. */
44
- weight: number;
45
57
  children: SketchNode[];
46
58
  };
47
59
  export type SketchText = {
@@ -50,6 +62,13 @@ export type SketchText = {
50
62
  /** 0 is a paragraph; 1-3 are headings. */
51
63
  level: 0 | 1 | 2 | 3;
52
64
  text: string;
65
+ /**
66
+ * The content field this text is linked to, as a source path
67
+ * (`/content/home.val.ts?p="hero"."title"`): typing in the block writes
68
+ * the field. Written `{field: <source path>}` at the end of the line. One
69
+ * block per field: a second link to the same field is not one.
70
+ */
71
+ field?: string;
53
72
  };
54
73
  export type SketchPlaceholder = {
55
74
  type: "placeholder";
@@ -58,10 +77,18 @@ export type SketchPlaceholder = {
58
77
  label: string | null;
59
78
  media: SketchMedia | null;
60
79
  };
80
+ /**
81
+ * `src` names a file the project has, never a guess: its `path` exactly as
82
+ * the content has it (`/public/val/hero.webp`, or its remote URL), or an
83
+ * image uploaded to the assistant's session, `session:<key>`. Which files
84
+ * those are is the Studio's to check; the text only holds the name.
85
+ */
61
86
  export type SketchMedia = {
62
87
  type: "image" | "video";
63
88
  src: string;
64
89
  };
90
+ /** The prefix of a `src` that is an image in the assistant's session. */
91
+ export declare const SESSION_MEDIA_PREFIX = "session:";
65
92
  export type SketchError = {
66
93
  /** 1-based, like an editor's gutter. */
67
94
  line: number;
@@ -82,7 +109,7 @@ export declare function parseSketch(source: string): ParsedSketch;
82
109
  * The line a text block is written as. Line breaks become spaces, and a
83
110
  * paragraph that would read as something else is kept as text with `\`.
84
111
  */
85
- export declare function formatSketchText(level: SketchText["level"], text: string): string;
112
+ export declare function formatSketchText(level: SketchText["level"], text: string, field?: string): string;
86
113
  /** The line a placeholder is written as. */
87
114
  export declare function formatSketchBox(placeholder: Pick<SketchPlaceholder, "kind" | "label" | "media">): string;
88
115
  /**
@@ -95,6 +122,14 @@ export declare function formatSketchBox(placeholder: Pick<SketchPlaceholder, "ki
95
122
  * it is not on the page.
96
123
  */
97
124
  export declare function formatSketch(blocks: SketchNode[], icons: Record<string, string>): string;
125
+ /**
126
+ * The text of every block linked to a field, by field: what the Studio
127
+ * writes to the content when a linked block changes. The first block wins,
128
+ * as `formatSketch` keeps only its link.
129
+ */
130
+ export declare function sketchFieldTexts(blocks: SketchNode[]): Map<string, string>;
131
+ /** The pages a sketch is about, in the order it gives them. */
132
+ export declare function sketchRoutes(blocks: SketchNode[]): string[];
98
133
  /**
99
134
  * What a list names a sketch by: its first top-level heading, the biggest
100
135
  * one first. Null for a sketch with no heading, which a list then names by
@@ -3086,6 +3086,26 @@ var Api = {
3086
3086
  parentRef: ParentRef,
3087
3087
  headVersion: zod.z.number().optional()
3088
3088
  })
3089
+ }),
3090
+ /*
3091
+ * The content service failed to take a patch: it answered with a
3092
+ * server error, or could not be reached. Not the 400 above either,
3093
+ * because the patches are fine -- the Studio keeps them and offers
3094
+ * to send them again. `message` is for the editor and `details` (the
3095
+ * status line and the service's own words) for whoever they ask.
3096
+ *
3097
+ * As with the 403, the patches BEFORE the failed one were saved.
3098
+ */
3099
+ zod.z.object({
3100
+ status: zod.z.literal(502),
3101
+ json: zod.z.object({
3102
+ type: zod.z.literal("service-error"),
3103
+ message: zod.z.string(),
3104
+ details: zod.z.string().optional(),
3105
+ newPatchIds: zod.z.array(PatchId),
3106
+ parentRef: ParentRef,
3107
+ headVersion: zod.z.number().optional()
3108
+ })
3089
3109
  }), zod.z.object({
3090
3110
  status: zod.z.literal(200),
3091
3111
  json: zod.z.object({
@@ -3121,7 +3141,9 @@ var Api = {
3121
3141
  status: zod.z.literal(500),
3122
3142
  json: zod.z.object({
3123
3143
  message: zod.z.string(),
3124
- error: GenericError
3144
+ error: GenericError,
3145
+ /** What the content service said: its status line and words. */
3146
+ details: zod.z.string().optional()
3125
3147
  })
3126
3148
  }), zod.z.object({
3127
3149
  status: zod.z.literal(200),
@@ -8239,24 +8261,30 @@ function emptyRecord(schema, context) {
8239
8261
  }));
8240
8262
  }
8241
8263
 
8242
- var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad"];
8264
+ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "convert_session_image", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad", "highlight_sketchpad_blocks", "clear_sketchpad_highlights"];
8243
8265
 
8244
8266
  /**
8245
8267
  * A page sketch: a page written as indented text, with four primitives.
8246
8268
  *
8247
8269
  * ```
8270
+ * --- /about a route: what follows is the page at /about
8248
8271
  * # Our people text (a heading: #, ## or ###)
8249
8272
  * We are 40 people in Oslo. text (a paragraph)
8273
+ * ## Hi {field: /x.val.ts?p="t"} text linked to a content field: typing
8274
+ * in it writes the field
8250
8275
  * [image: Team photo] placeholder: [kind] or [kind: label]
8251
8276
  * [hero: Roof](image: /a.jpg) a placeholder with an image (or video) in it
8252
8277
  * icon hero M3 5h18v14H3z how every [hero] box is drawn: SVG path data
8253
8278
  * row its children side by side
8254
- * col 2 its children stacked; 2 shares of the row
8255
- * ## Design
8279
+ * col its children stacked; columns share a row
8280
+ * ## Design equally
8256
8281
  * col
8257
8282
  * [button: Join us]
8258
8283
  * ```
8259
8284
  *
8285
+ * A sketchpad is not one page's: a route divider starts the part about a
8286
+ * page, so one sketch can cover a page and the two it links to.
8287
+ *
8260
8288
  * It is a scratch pad, not a layout language: it says WHAT is on a page and
8261
8289
  * roughly WHERE, so an assistant and a person can agree on a page's content
8262
8290
  * before anyone worries about how it looks. The person never sees this text,
@@ -8275,7 +8303,21 @@ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_con
8275
8303
  * that builds the page reads it exactly as the person left it.
8276
8304
  */
8277
8305
 
8306
+ /** The start of the part about one page. Only at the top level. */
8307
+
8308
+ /** Columns share their row equally: a sketch says what, not how wide. */
8309
+
8310
+ /**
8311
+ * `src` names a file the project has, never a guess: its `path` exactly as
8312
+ * the content has it (`/public/val/hero.webp`, or its remote URL), or an
8313
+ * image uploaded to the assistant's session, `session:<key>`. Which files
8314
+ * those are is the Studio's to check; the text only holds the name.
8315
+ */
8316
+
8317
+ /** The prefix of a `src` that is an image in the assistant's session. */
8318
+ var SESSION_MEDIA_PREFIX = "session:";
8278
8319
  var CONTAINER = /^(row|col)(?:\s+(\d+))?$/;
8320
+ var ROUTE = /^---(?:\s+(.*))?$/;
8279
8321
  var PLACEHOLDER = /^\[([^\]:]*)(?::([^\]]*))?\](?:\((image|video):\s*([^\s)]+)\s*\))?$/;
8280
8322
  var HAS_MEDIA = /^\[[^\]]*\]\(/;
8281
8323
  var ICON = /^icon\s+(\S+)\s+([Mm][\d\s.-].*)$/;
@@ -8318,6 +8360,32 @@ function parseSketch(source) {
8318
8360
  while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
8319
8361
  stack.pop();
8320
8362
  }
8363
+ var route = ROUTE.exec(content);
8364
+ if (route) {
8365
+ var _route$;
8366
+ var _path = ((_route$ = route[1]) !== null && _route$ !== void 0 ? _route$ : "").trim();
8367
+ if (!_path.startsWith("/") || /\s/.test(_path)) {
8368
+ errors.push({
8369
+ line: line,
8370
+ message: 'A route divider names one page by its route, like "--- /about".'
8371
+ });
8372
+ }
8373
+ if (indent > 0) {
8374
+ errors.push({
8375
+ line: line,
8376
+ message: "A route divider starts a page, so it goes at the top level, not indented."
8377
+ });
8378
+ }
8379
+ // It closes everything open: what follows is another page.
8380
+ stack.length = 1;
8381
+ stack[0].lastIndent = 0;
8382
+ blocks.push({
8383
+ type: "route",
8384
+ line: line,
8385
+ route: _path
8386
+ });
8387
+ return;
8388
+ }
8321
8389
  var parent = stack[stack.length - 1];
8322
8390
  var node = parseContent(content, line, errors);
8323
8391
 
@@ -8359,7 +8427,27 @@ function parseSketch(source) {
8359
8427
  errors: errors
8360
8428
  };
8361
8429
  }
8362
- function parseContent(content, line, errors) {
8430
+
8431
+ /** The link to a content field at the end of a text line. */
8432
+ var FIELD = / \{field: (\/[^{}]*)\}$/;
8433
+ function parseContent(whole, line, errors) {
8434
+ var link = FIELD.exec(whole);
8435
+ if (link) {
8436
+ var node = parseLine(whole.slice(0, link.index), line, errors);
8437
+ if (node.type !== "text") {
8438
+ errors.push({
8439
+ line: line,
8440
+ message: "Only a heading or a paragraph is linked to a field: put {field: ...} at the end of one."
8441
+ });
8442
+ return node;
8443
+ }
8444
+ return createForOfIteratorHelper._objectSpread2(createForOfIteratorHelper._objectSpread2({}, node), {}, {
8445
+ field: link[1].trim()
8446
+ });
8447
+ }
8448
+ return parseLine(whole, line, errors);
8449
+ }
8450
+ function parseLine(content, line, errors) {
8363
8451
  // A leading backslash keeps a line that looks like a keyword as text.
8364
8452
  if (content.startsWith("\\")) {
8365
8453
  return {
@@ -8384,17 +8472,15 @@ function parseContent(content, line, errors) {
8384
8472
  children: []
8385
8473
  };
8386
8474
  }
8387
- var weight = container[2] === undefined ? 1 : Number(container[2]);
8388
- if (weight === 0) {
8475
+ if (container[2] !== undefined) {
8389
8476
  errors.push({
8390
8477
  line: line,
8391
- message: "A col needs a weight of 1 or more."
8478
+ message: 'Columns share a row equally, so a col takes no number: write "col".'
8392
8479
  });
8393
8480
  }
8394
8481
  return {
8395
8482
  type: "col",
8396
8483
  line: line,
8397
- weight: Math.max(weight, 1),
8398
8484
  children: []
8399
8485
  };
8400
8486
  }
@@ -8468,13 +8554,19 @@ function parseContent(content, line, errors) {
8468
8554
  * The line a text block is written as. Line breaks become spaces, and a
8469
8555
  * paragraph that would read as something else is kept as text with `\`.
8470
8556
  */
8471
- function formatSketchText(level, text) {
8557
+ function formatSketchText(level, text, field) {
8472
8558
  var flat = text.replace(/\s+/g, " ").trim();
8559
+ var path = field === null || field === void 0 ? void 0 : field.replace(/\s+/g, " ").trim();
8560
+ // A source path, and one the line can be read back with: no braces.
8561
+ var link = path && /^\/[^{}]*$/.test(path) ? " {field: ".concat(path, "}") : "";
8473
8562
  if (level > 0) {
8474
- return "#".repeat(level) + " " + flat;
8563
+ return "#".repeat(level) + " " + flat + link;
8475
8564
  }
8476
- var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(flat) || HEADING.test(flat) || ICON.test(flat);
8477
- return ambiguous ? "\\" + flat : flat;
8565
+ // Read as the whole line will be, link and all: `icon x M` is text, and
8566
+ // `icon x M {field: ...}` would be an icon.
8567
+ var whole = flat + link;
8568
+ var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(whole) || ROUTE.test(whole) || HEADING.test(whole) || ICON.test(whole);
8569
+ return (ambiguous ? "\\" + flat : flat) + link;
8478
8570
  }
8479
8571
 
8480
8572
  /** The line a placeholder is written as. */
@@ -8499,17 +8591,26 @@ function formatSketchBox(placeholder) {
8499
8591
  */
8500
8592
  function formatSketch(blocks, icons) {
8501
8593
  var lines = [];
8594
+ // One block per field: the first keeps the link.
8595
+ var linked = new Set();
8502
8596
  var _walk = function walk(node, depth, inRow) {
8503
8597
  var indent = " ".repeat(depth);
8504
8598
  switch (node.type) {
8505
8599
  case "text":
8506
8600
  if (node.text) {
8507
- lines.push(indent + formatSketchText(node.level, node.text));
8601
+ var field = node.field && !linked.has(node.field) ? node.field : undefined;
8602
+ if (field) {
8603
+ linked.add(field);
8604
+ }
8605
+ lines.push(indent + formatSketchText(node.level, node.text, field));
8508
8606
  }
8509
8607
  return;
8510
8608
  case "placeholder":
8511
8609
  lines.push(indent + formatSketchBox(node));
8512
8610
  return;
8611
+ case "route":
8612
+ lines.push("--- ".concat(node.route));
8613
+ return;
8513
8614
  case "row":
8514
8615
  lines.push(indent + "row");
8515
8616
  if (node.children.length === 0) {
@@ -8526,7 +8627,7 @@ function formatSketch(blocks, icons) {
8526
8627
  });
8527
8628
  return;
8528
8629
  }
8529
- lines.push(indent + (node.weight === 1 ? "col" : "col ".concat(node.weight)));
8630
+ lines.push(indent + "col");
8530
8631
  node.children.forEach(function (child) {
8531
8632
  return _walk(child, depth + 1, false);
8532
8633
  });
@@ -8546,6 +8647,33 @@ function formatSketch(blocks, icons) {
8546
8647
  return all.length > 0 ? all.join("\n") + "\n" : "";
8547
8648
  }
8548
8649
 
8650
+ /**
8651
+ * The text of every block linked to a field, by field: what the Studio
8652
+ * writes to the content when a linked block changes. The first block wins,
8653
+ * as `formatSketch` keeps only its link.
8654
+ */
8655
+ function sketchFieldTexts(blocks) {
8656
+ var texts = new Map();
8657
+ var _walk2 = function walk(node) {
8658
+ if (node.type === "text") {
8659
+ if (node.field && node.text && !texts.has(node.field)) {
8660
+ texts.set(node.field, node.text.replace(/\s+/g, " ").trim());
8661
+ }
8662
+ } else if (node.type === "row" || node.type === "col") {
8663
+ node.children.forEach(_walk2);
8664
+ }
8665
+ };
8666
+ blocks.forEach(_walk2);
8667
+ return texts;
8668
+ }
8669
+
8670
+ /** The pages a sketch is about, in the order it gives them. */
8671
+ function sketchRoutes(blocks) {
8672
+ return blocks.flatMap(function (block) {
8673
+ return block.type === "route" && block.route !== "" ? [block.route] : [];
8674
+ });
8675
+ }
8676
+
8549
8677
  /**
8550
8678
  * What a list names a sketch by: its first top-level heading, the biggest
8551
8679
  * one first. Null for a sketch with no heading, which a list then names by
@@ -11829,6 +11957,7 @@ exports.RestoreVerdict = RestoreVerdict;
11829
11957
  exports.RichTextOptions = RichTextOptions;
11830
11958
  exports.SCHEMA_SOURCE_FIXES = SCHEMA_SOURCE_FIXES;
11831
11959
  exports.SELF_CHECK_VERSION = SELF_CHECK_VERSION;
11960
+ exports.SESSION_MEDIA_PREFIX = SESSION_MEDIA_PREFIX;
11832
11961
  exports.SKETCHPAD_MAX_TEXT_LENGTH = SKETCHPAD_MAX_TEXT_LENGTH;
11833
11962
  exports.STAGED_SCHEMA_PREFIX = STAGED_SCHEMA_PREFIX;
11834
11963
  exports.STUDIO_FIXES = STUDIO_FIXES;
@@ -11980,6 +12109,8 @@ exports.selfCheck = selfCheck;
11980
12109
  exports.shouldEncodeWithWasm = shouldEncodeWithWasm;
11981
12110
  exports.sideFiles = sideFiles;
11982
12111
  exports.sideSource = sideSource;
12112
+ exports.sketchFieldTexts = sketchFieldTexts;
12113
+ exports.sketchRoutes = sketchRoutes;
11983
12114
  exports.sketchTitle = sketchTitle;
11984
12115
  exports.sourcePathOfChild = sourcePathOfChild;
11985
12116
  exports.storeWork = storeWork;
@@ -3086,6 +3086,26 @@ var Api = {
3086
3086
  parentRef: ParentRef,
3087
3087
  headVersion: zod.z.number().optional()
3088
3088
  })
3089
+ }),
3090
+ /*
3091
+ * The content service failed to take a patch: it answered with a
3092
+ * server error, or could not be reached. Not the 400 above either,
3093
+ * because the patches are fine -- the Studio keeps them and offers
3094
+ * to send them again. `message` is for the editor and `details` (the
3095
+ * status line and the service's own words) for whoever they ask.
3096
+ *
3097
+ * As with the 403, the patches BEFORE the failed one were saved.
3098
+ */
3099
+ zod.z.object({
3100
+ status: zod.z.literal(502),
3101
+ json: zod.z.object({
3102
+ type: zod.z.literal("service-error"),
3103
+ message: zod.z.string(),
3104
+ details: zod.z.string().optional(),
3105
+ newPatchIds: zod.z.array(PatchId),
3106
+ parentRef: ParentRef,
3107
+ headVersion: zod.z.number().optional()
3108
+ })
3089
3109
  }), zod.z.object({
3090
3110
  status: zod.z.literal(200),
3091
3111
  json: zod.z.object({
@@ -3121,7 +3141,9 @@ var Api = {
3121
3141
  status: zod.z.literal(500),
3122
3142
  json: zod.z.object({
3123
3143
  message: zod.z.string(),
3124
- error: GenericError
3144
+ error: GenericError,
3145
+ /** What the content service said: its status line and words. */
3146
+ details: zod.z.string().optional()
3125
3147
  })
3126
3148
  }), zod.z.object({
3127
3149
  status: zod.z.literal(200),
@@ -8239,24 +8261,30 @@ function emptyRecord(schema, context) {
8239
8261
  }));
8240
8262
  }
8241
8263
 
8242
- var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad"];
8264
+ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "convert_session_image", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad", "highlight_sketchpad_blocks", "clear_sketchpad_highlights"];
8243
8265
 
8244
8266
  /**
8245
8267
  * A page sketch: a page written as indented text, with four primitives.
8246
8268
  *
8247
8269
  * ```
8270
+ * --- /about a route: what follows is the page at /about
8248
8271
  * # Our people text (a heading: #, ## or ###)
8249
8272
  * We are 40 people in Oslo. text (a paragraph)
8273
+ * ## Hi {field: /x.val.ts?p="t"} text linked to a content field: typing
8274
+ * in it writes the field
8250
8275
  * [image: Team photo] placeholder: [kind] or [kind: label]
8251
8276
  * [hero: Roof](image: /a.jpg) a placeholder with an image (or video) in it
8252
8277
  * icon hero M3 5h18v14H3z how every [hero] box is drawn: SVG path data
8253
8278
  * row its children side by side
8254
- * col 2 its children stacked; 2 shares of the row
8255
- * ## Design
8279
+ * col its children stacked; columns share a row
8280
+ * ## Design equally
8256
8281
  * col
8257
8282
  * [button: Join us]
8258
8283
  * ```
8259
8284
  *
8285
+ * A sketchpad is not one page's: a route divider starts the part about a
8286
+ * page, so one sketch can cover a page and the two it links to.
8287
+ *
8260
8288
  * It is a scratch pad, not a layout language: it says WHAT is on a page and
8261
8289
  * roughly WHERE, so an assistant and a person can agree on a page's content
8262
8290
  * before anyone worries about how it looks. The person never sees this text,
@@ -8275,7 +8303,21 @@ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_con
8275
8303
  * that builds the page reads it exactly as the person left it.
8276
8304
  */
8277
8305
 
8306
+ /** The start of the part about one page. Only at the top level. */
8307
+
8308
+ /** Columns share their row equally: a sketch says what, not how wide. */
8309
+
8310
+ /**
8311
+ * `src` names a file the project has, never a guess: its `path` exactly as
8312
+ * the content has it (`/public/val/hero.webp`, or its remote URL), or an
8313
+ * image uploaded to the assistant's session, `session:<key>`. Which files
8314
+ * those are is the Studio's to check; the text only holds the name.
8315
+ */
8316
+
8317
+ /** The prefix of a `src` that is an image in the assistant's session. */
8318
+ var SESSION_MEDIA_PREFIX = "session:";
8278
8319
  var CONTAINER = /^(row|col)(?:\s+(\d+))?$/;
8320
+ var ROUTE = /^---(?:\s+(.*))?$/;
8279
8321
  var PLACEHOLDER = /^\[([^\]:]*)(?::([^\]]*))?\](?:\((image|video):\s*([^\s)]+)\s*\))?$/;
8280
8322
  var HAS_MEDIA = /^\[[^\]]*\]\(/;
8281
8323
  var ICON = /^icon\s+(\S+)\s+([Mm][\d\s.-].*)$/;
@@ -8318,6 +8360,32 @@ function parseSketch(source) {
8318
8360
  while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
8319
8361
  stack.pop();
8320
8362
  }
8363
+ var route = ROUTE.exec(content);
8364
+ if (route) {
8365
+ var _route$;
8366
+ var _path = ((_route$ = route[1]) !== null && _route$ !== void 0 ? _route$ : "").trim();
8367
+ if (!_path.startsWith("/") || /\s/.test(_path)) {
8368
+ errors.push({
8369
+ line: line,
8370
+ message: 'A route divider names one page by its route, like "--- /about".'
8371
+ });
8372
+ }
8373
+ if (indent > 0) {
8374
+ errors.push({
8375
+ line: line,
8376
+ message: "A route divider starts a page, so it goes at the top level, not indented."
8377
+ });
8378
+ }
8379
+ // It closes everything open: what follows is another page.
8380
+ stack.length = 1;
8381
+ stack[0].lastIndent = 0;
8382
+ blocks.push({
8383
+ type: "route",
8384
+ line: line,
8385
+ route: _path
8386
+ });
8387
+ return;
8388
+ }
8321
8389
  var parent = stack[stack.length - 1];
8322
8390
  var node = parseContent(content, line, errors);
8323
8391
 
@@ -8359,7 +8427,27 @@ function parseSketch(source) {
8359
8427
  errors: errors
8360
8428
  };
8361
8429
  }
8362
- function parseContent(content, line, errors) {
8430
+
8431
+ /** The link to a content field at the end of a text line. */
8432
+ var FIELD = / \{field: (\/[^{}]*)\}$/;
8433
+ function parseContent(whole, line, errors) {
8434
+ var link = FIELD.exec(whole);
8435
+ if (link) {
8436
+ var node = parseLine(whole.slice(0, link.index), line, errors);
8437
+ if (node.type !== "text") {
8438
+ errors.push({
8439
+ line: line,
8440
+ message: "Only a heading or a paragraph is linked to a field: put {field: ...} at the end of one."
8441
+ });
8442
+ return node;
8443
+ }
8444
+ return createForOfIteratorHelper._objectSpread2(createForOfIteratorHelper._objectSpread2({}, node), {}, {
8445
+ field: link[1].trim()
8446
+ });
8447
+ }
8448
+ return parseLine(whole, line, errors);
8449
+ }
8450
+ function parseLine(content, line, errors) {
8363
8451
  // A leading backslash keeps a line that looks like a keyword as text.
8364
8452
  if (content.startsWith("\\")) {
8365
8453
  return {
@@ -8384,17 +8472,15 @@ function parseContent(content, line, errors) {
8384
8472
  children: []
8385
8473
  };
8386
8474
  }
8387
- var weight = container[2] === undefined ? 1 : Number(container[2]);
8388
- if (weight === 0) {
8475
+ if (container[2] !== undefined) {
8389
8476
  errors.push({
8390
8477
  line: line,
8391
- message: "A col needs a weight of 1 or more."
8478
+ message: 'Columns share a row equally, so a col takes no number: write "col".'
8392
8479
  });
8393
8480
  }
8394
8481
  return {
8395
8482
  type: "col",
8396
8483
  line: line,
8397
- weight: Math.max(weight, 1),
8398
8484
  children: []
8399
8485
  };
8400
8486
  }
@@ -8468,13 +8554,19 @@ function parseContent(content, line, errors) {
8468
8554
  * The line a text block is written as. Line breaks become spaces, and a
8469
8555
  * paragraph that would read as something else is kept as text with `\`.
8470
8556
  */
8471
- function formatSketchText(level, text) {
8557
+ function formatSketchText(level, text, field) {
8472
8558
  var flat = text.replace(/\s+/g, " ").trim();
8559
+ var path = field === null || field === void 0 ? void 0 : field.replace(/\s+/g, " ").trim();
8560
+ // A source path, and one the line can be read back with: no braces.
8561
+ var link = path && /^\/[^{}]*$/.test(path) ? " {field: ".concat(path, "}") : "";
8473
8562
  if (level > 0) {
8474
- return "#".repeat(level) + " " + flat;
8563
+ return "#".repeat(level) + " " + flat + link;
8475
8564
  }
8476
- var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(flat) || HEADING.test(flat) || ICON.test(flat);
8477
- return ambiguous ? "\\" + flat : flat;
8565
+ // Read as the whole line will be, link and all: `icon x M` is text, and
8566
+ // `icon x M {field: ...}` would be an icon.
8567
+ var whole = flat + link;
8568
+ var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(whole) || ROUTE.test(whole) || HEADING.test(whole) || ICON.test(whole);
8569
+ return (ambiguous ? "\\" + flat : flat) + link;
8478
8570
  }
8479
8571
 
8480
8572
  /** The line a placeholder is written as. */
@@ -8499,17 +8591,26 @@ function formatSketchBox(placeholder) {
8499
8591
  */
8500
8592
  function formatSketch(blocks, icons) {
8501
8593
  var lines = [];
8594
+ // One block per field: the first keeps the link.
8595
+ var linked = new Set();
8502
8596
  var _walk = function walk(node, depth, inRow) {
8503
8597
  var indent = " ".repeat(depth);
8504
8598
  switch (node.type) {
8505
8599
  case "text":
8506
8600
  if (node.text) {
8507
- lines.push(indent + formatSketchText(node.level, node.text));
8601
+ var field = node.field && !linked.has(node.field) ? node.field : undefined;
8602
+ if (field) {
8603
+ linked.add(field);
8604
+ }
8605
+ lines.push(indent + formatSketchText(node.level, node.text, field));
8508
8606
  }
8509
8607
  return;
8510
8608
  case "placeholder":
8511
8609
  lines.push(indent + formatSketchBox(node));
8512
8610
  return;
8611
+ case "route":
8612
+ lines.push("--- ".concat(node.route));
8613
+ return;
8513
8614
  case "row":
8514
8615
  lines.push(indent + "row");
8515
8616
  if (node.children.length === 0) {
@@ -8526,7 +8627,7 @@ function formatSketch(blocks, icons) {
8526
8627
  });
8527
8628
  return;
8528
8629
  }
8529
- lines.push(indent + (node.weight === 1 ? "col" : "col ".concat(node.weight)));
8630
+ lines.push(indent + "col");
8530
8631
  node.children.forEach(function (child) {
8531
8632
  return _walk(child, depth + 1, false);
8532
8633
  });
@@ -8546,6 +8647,33 @@ function formatSketch(blocks, icons) {
8546
8647
  return all.length > 0 ? all.join("\n") + "\n" : "";
8547
8648
  }
8548
8649
 
8650
+ /**
8651
+ * The text of every block linked to a field, by field: what the Studio
8652
+ * writes to the content when a linked block changes. The first block wins,
8653
+ * as `formatSketch` keeps only its link.
8654
+ */
8655
+ function sketchFieldTexts(blocks) {
8656
+ var texts = new Map();
8657
+ var _walk2 = function walk(node) {
8658
+ if (node.type === "text") {
8659
+ if (node.field && node.text && !texts.has(node.field)) {
8660
+ texts.set(node.field, node.text.replace(/\s+/g, " ").trim());
8661
+ }
8662
+ } else if (node.type === "row" || node.type === "col") {
8663
+ node.children.forEach(_walk2);
8664
+ }
8665
+ };
8666
+ blocks.forEach(_walk2);
8667
+ return texts;
8668
+ }
8669
+
8670
+ /** The pages a sketch is about, in the order it gives them. */
8671
+ function sketchRoutes(blocks) {
8672
+ return blocks.flatMap(function (block) {
8673
+ return block.type === "route" && block.route !== "" ? [block.route] : [];
8674
+ });
8675
+ }
8676
+
8549
8677
  /**
8550
8678
  * What a list names a sketch by: its first top-level heading, the biggest
8551
8679
  * one first. Null for a sketch with no heading, which a list then names by
@@ -11829,6 +11957,7 @@ exports.RestoreVerdict = RestoreVerdict;
11829
11957
  exports.RichTextOptions = RichTextOptions;
11830
11958
  exports.SCHEMA_SOURCE_FIXES = SCHEMA_SOURCE_FIXES;
11831
11959
  exports.SELF_CHECK_VERSION = SELF_CHECK_VERSION;
11960
+ exports.SESSION_MEDIA_PREFIX = SESSION_MEDIA_PREFIX;
11832
11961
  exports.SKETCHPAD_MAX_TEXT_LENGTH = SKETCHPAD_MAX_TEXT_LENGTH;
11833
11962
  exports.STAGED_SCHEMA_PREFIX = STAGED_SCHEMA_PREFIX;
11834
11963
  exports.STUDIO_FIXES = STUDIO_FIXES;
@@ -11980,6 +12109,8 @@ exports.selfCheck = selfCheck;
11980
12109
  exports.shouldEncodeWithWasm = shouldEncodeWithWasm;
11981
12110
  exports.sideFiles = sideFiles;
11982
12111
  exports.sideSource = sideSource;
12112
+ exports.sketchFieldTexts = sketchFieldTexts;
12113
+ exports.sketchRoutes = sketchRoutes;
11983
12114
  exports.sketchTitle = sketchTitle;
11984
12115
  exports.sourcePathOfChild = sourcePathOfChild;
11985
12116
  exports.storeWork = storeWork;
@@ -3079,6 +3079,26 @@ var Api = {
3079
3079
  parentRef: ParentRef,
3080
3080
  headVersion: z.number().optional()
3081
3081
  })
3082
+ }),
3083
+ /*
3084
+ * The content service failed to take a patch: it answered with a
3085
+ * server error, or could not be reached. Not the 400 above either,
3086
+ * because the patches are fine -- the Studio keeps them and offers
3087
+ * to send them again. `message` is for the editor and `details` (the
3088
+ * status line and the service's own words) for whoever they ask.
3089
+ *
3090
+ * As with the 403, the patches BEFORE the failed one were saved.
3091
+ */
3092
+ z.object({
3093
+ status: z.literal(502),
3094
+ json: z.object({
3095
+ type: z.literal("service-error"),
3096
+ message: z.string(),
3097
+ details: z.string().optional(),
3098
+ newPatchIds: z.array(PatchId),
3099
+ parentRef: ParentRef,
3100
+ headVersion: z.number().optional()
3101
+ })
3082
3102
  }), z.object({
3083
3103
  status: z.literal(200),
3084
3104
  json: z.object({
@@ -3114,7 +3134,9 @@ var Api = {
3114
3134
  status: z.literal(500),
3115
3135
  json: z.object({
3116
3136
  message: z.string(),
3117
- error: GenericError
3137
+ error: GenericError,
3138
+ /** What the content service said: its status line and words. */
3139
+ details: z.string().optional()
3118
3140
  })
3119
3141
  }), z.object({
3120
3142
  status: z.literal(200),
@@ -8232,24 +8254,30 @@ function emptyRecord(schema, context) {
8232
8254
  }));
8233
8255
  }
8234
8256
 
8235
- var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad"];
8257
+ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "convert_session_image", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad", "highlight_sketchpad_blocks", "clear_sketchpad_highlights"];
8236
8258
 
8237
8259
  /**
8238
8260
  * A page sketch: a page written as indented text, with four primitives.
8239
8261
  *
8240
8262
  * ```
8263
+ * --- /about a route: what follows is the page at /about
8241
8264
  * # Our people text (a heading: #, ## or ###)
8242
8265
  * We are 40 people in Oslo. text (a paragraph)
8266
+ * ## Hi {field: /x.val.ts?p="t"} text linked to a content field: typing
8267
+ * in it writes the field
8243
8268
  * [image: Team photo] placeholder: [kind] or [kind: label]
8244
8269
  * [hero: Roof](image: /a.jpg) a placeholder with an image (or video) in it
8245
8270
  * icon hero M3 5h18v14H3z how every [hero] box is drawn: SVG path data
8246
8271
  * row its children side by side
8247
- * col 2 its children stacked; 2 shares of the row
8248
- * ## Design
8272
+ * col its children stacked; columns share a row
8273
+ * ## Design equally
8249
8274
  * col
8250
8275
  * [button: Join us]
8251
8276
  * ```
8252
8277
  *
8278
+ * A sketchpad is not one page's: a route divider starts the part about a
8279
+ * page, so one sketch can cover a page and the two it links to.
8280
+ *
8253
8281
  * It is a scratch pad, not a layout language: it says WHAT is on a page and
8254
8282
  * roughly WHERE, so an assistant and a person can agree on a page's content
8255
8283
  * before anyone worries about how it looks. The person never sees this text,
@@ -8268,7 +8296,21 @@ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_con
8268
8296
  * that builds the page reads it exactly as the person left it.
8269
8297
  */
8270
8298
 
8299
+ /** The start of the part about one page. Only at the top level. */
8300
+
8301
+ /** Columns share their row equally: a sketch says what, not how wide. */
8302
+
8303
+ /**
8304
+ * `src` names a file the project has, never a guess: its `path` exactly as
8305
+ * the content has it (`/public/val/hero.webp`, or its remote URL), or an
8306
+ * image uploaded to the assistant's session, `session:<key>`. Which files
8307
+ * those are is the Studio's to check; the text only holds the name.
8308
+ */
8309
+
8310
+ /** The prefix of a `src` that is an image in the assistant's session. */
8311
+ var SESSION_MEDIA_PREFIX = "session:";
8271
8312
  var CONTAINER = /^(row|col)(?:\s+(\d+))?$/;
8313
+ var ROUTE = /^---(?:\s+(.*))?$/;
8272
8314
  var PLACEHOLDER = /^\[([^\]:]*)(?::([^\]]*))?\](?:\((image|video):\s*([^\s)]+)\s*\))?$/;
8273
8315
  var HAS_MEDIA = /^\[[^\]]*\]\(/;
8274
8316
  var ICON = /^icon\s+(\S+)\s+([Mm][\d\s.-].*)$/;
@@ -8311,6 +8353,32 @@ function parseSketch(source) {
8311
8353
  while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
8312
8354
  stack.pop();
8313
8355
  }
8356
+ var route = ROUTE.exec(content);
8357
+ if (route) {
8358
+ var _route$;
8359
+ var _path = ((_route$ = route[1]) !== null && _route$ !== void 0 ? _route$ : "").trim();
8360
+ if (!_path.startsWith("/") || /\s/.test(_path)) {
8361
+ errors.push({
8362
+ line: line,
8363
+ message: 'A route divider names one page by its route, like "--- /about".'
8364
+ });
8365
+ }
8366
+ if (indent > 0) {
8367
+ errors.push({
8368
+ line: line,
8369
+ message: "A route divider starts a page, so it goes at the top level, not indented."
8370
+ });
8371
+ }
8372
+ // It closes everything open: what follows is another page.
8373
+ stack.length = 1;
8374
+ stack[0].lastIndent = 0;
8375
+ blocks.push({
8376
+ type: "route",
8377
+ line: line,
8378
+ route: _path
8379
+ });
8380
+ return;
8381
+ }
8314
8382
  var parent = stack[stack.length - 1];
8315
8383
  var node = parseContent(content, line, errors);
8316
8384
 
@@ -8352,7 +8420,27 @@ function parseSketch(source) {
8352
8420
  errors: errors
8353
8421
  };
8354
8422
  }
8355
- function parseContent(content, line, errors) {
8423
+
8424
+ /** The link to a content field at the end of a text line. */
8425
+ var FIELD = / \{field: (\/[^{}]*)\}$/;
8426
+ function parseContent(whole, line, errors) {
8427
+ var link = FIELD.exec(whole);
8428
+ if (link) {
8429
+ var node = parseLine(whole.slice(0, link.index), line, errors);
8430
+ if (node.type !== "text") {
8431
+ errors.push({
8432
+ line: line,
8433
+ message: "Only a heading or a paragraph is linked to a field: put {field: ...} at the end of one."
8434
+ });
8435
+ return node;
8436
+ }
8437
+ return _objectSpread2(_objectSpread2({}, node), {}, {
8438
+ field: link[1].trim()
8439
+ });
8440
+ }
8441
+ return parseLine(whole, line, errors);
8442
+ }
8443
+ function parseLine(content, line, errors) {
8356
8444
  // A leading backslash keeps a line that looks like a keyword as text.
8357
8445
  if (content.startsWith("\\")) {
8358
8446
  return {
@@ -8377,17 +8465,15 @@ function parseContent(content, line, errors) {
8377
8465
  children: []
8378
8466
  };
8379
8467
  }
8380
- var weight = container[2] === undefined ? 1 : Number(container[2]);
8381
- if (weight === 0) {
8468
+ if (container[2] !== undefined) {
8382
8469
  errors.push({
8383
8470
  line: line,
8384
- message: "A col needs a weight of 1 or more."
8471
+ message: 'Columns share a row equally, so a col takes no number: write "col".'
8385
8472
  });
8386
8473
  }
8387
8474
  return {
8388
8475
  type: "col",
8389
8476
  line: line,
8390
- weight: Math.max(weight, 1),
8391
8477
  children: []
8392
8478
  };
8393
8479
  }
@@ -8461,13 +8547,19 @@ function parseContent(content, line, errors) {
8461
8547
  * The line a text block is written as. Line breaks become spaces, and a
8462
8548
  * paragraph that would read as something else is kept as text with `\`.
8463
8549
  */
8464
- function formatSketchText(level, text) {
8550
+ function formatSketchText(level, text, field) {
8465
8551
  var flat = text.replace(/\s+/g, " ").trim();
8552
+ var path = field === null || field === void 0 ? void 0 : field.replace(/\s+/g, " ").trim();
8553
+ // A source path, and one the line can be read back with: no braces.
8554
+ var link = path && /^\/[^{}]*$/.test(path) ? " {field: ".concat(path, "}") : "";
8466
8555
  if (level > 0) {
8467
- return "#".repeat(level) + " " + flat;
8556
+ return "#".repeat(level) + " " + flat + link;
8468
8557
  }
8469
- var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(flat) || HEADING.test(flat) || ICON.test(flat);
8470
- return ambiguous ? "\\" + flat : flat;
8558
+ // Read as the whole line will be, link and all: `icon x M` is text, and
8559
+ // `icon x M {field: ...}` would be an icon.
8560
+ var whole = flat + link;
8561
+ var ambiguous = flat.startsWith("\\") || flat.startsWith("[") || CONTAINER.test(whole) || ROUTE.test(whole) || HEADING.test(whole) || ICON.test(whole);
8562
+ return (ambiguous ? "\\" + flat : flat) + link;
8471
8563
  }
8472
8564
 
8473
8565
  /** The line a placeholder is written as. */
@@ -8492,17 +8584,26 @@ function formatSketchBox(placeholder) {
8492
8584
  */
8493
8585
  function formatSketch(blocks, icons) {
8494
8586
  var lines = [];
8587
+ // One block per field: the first keeps the link.
8588
+ var linked = new Set();
8495
8589
  var _walk = function walk(node, depth, inRow) {
8496
8590
  var indent = " ".repeat(depth);
8497
8591
  switch (node.type) {
8498
8592
  case "text":
8499
8593
  if (node.text) {
8500
- lines.push(indent + formatSketchText(node.level, node.text));
8594
+ var field = node.field && !linked.has(node.field) ? node.field : undefined;
8595
+ if (field) {
8596
+ linked.add(field);
8597
+ }
8598
+ lines.push(indent + formatSketchText(node.level, node.text, field));
8501
8599
  }
8502
8600
  return;
8503
8601
  case "placeholder":
8504
8602
  lines.push(indent + formatSketchBox(node));
8505
8603
  return;
8604
+ case "route":
8605
+ lines.push("--- ".concat(node.route));
8606
+ return;
8506
8607
  case "row":
8507
8608
  lines.push(indent + "row");
8508
8609
  if (node.children.length === 0) {
@@ -8519,7 +8620,7 @@ function formatSketch(blocks, icons) {
8519
8620
  });
8520
8621
  return;
8521
8622
  }
8522
- lines.push(indent + (node.weight === 1 ? "col" : "col ".concat(node.weight)));
8623
+ lines.push(indent + "col");
8523
8624
  node.children.forEach(function (child) {
8524
8625
  return _walk(child, depth + 1, false);
8525
8626
  });
@@ -8539,6 +8640,33 @@ function formatSketch(blocks, icons) {
8539
8640
  return all.length > 0 ? all.join("\n") + "\n" : "";
8540
8641
  }
8541
8642
 
8643
+ /**
8644
+ * The text of every block linked to a field, by field: what the Studio
8645
+ * writes to the content when a linked block changes. The first block wins,
8646
+ * as `formatSketch` keeps only its link.
8647
+ */
8648
+ function sketchFieldTexts(blocks) {
8649
+ var texts = new Map();
8650
+ var _walk2 = function walk(node) {
8651
+ if (node.type === "text") {
8652
+ if (node.field && node.text && !texts.has(node.field)) {
8653
+ texts.set(node.field, node.text.replace(/\s+/g, " ").trim());
8654
+ }
8655
+ } else if (node.type === "row" || node.type === "col") {
8656
+ node.children.forEach(_walk2);
8657
+ }
8658
+ };
8659
+ blocks.forEach(_walk2);
8660
+ return texts;
8661
+ }
8662
+
8663
+ /** The pages a sketch is about, in the order it gives them. */
8664
+ function sketchRoutes(blocks) {
8665
+ return blocks.flatMap(function (block) {
8666
+ return block.type === "route" && block.route !== "" ? [block.route] : [];
8667
+ });
8668
+ }
8669
+
8542
8670
  /**
8543
8671
  * What a list names a sketch by: its first top-level heading, the biggest
8544
8672
  * one first. Null for a sketch with no heading, which a list then names by
@@ -11707,4 +11835,4 @@ var messageOf = function messageOf(error) {
11707
11835
  return error instanceof Error ? error.message : String(error);
11708
11836
  };
11709
11837
 
11710
- export { AA_LARGE, AA_TEXT, AIToolCallSummaries, AnalyticsBeacon, Api, BRAND_CONTRAST_PAIRS, BinaryFileRef, ChainFold, CodeOptions, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, EMPTY_CHAIN, ENCODE_DEFAULTS, ENCODE_MIME_TYPE_OF, ENGINE, FileOptions, HistoricalCommit, HistoricalModule, HistoricalPatchSet, HistoryError, ImageEncodeOption, ImageOptions, JSONValue, JSON_ENTRIES_BATCH_MAX, LOCAL_FILE_IN_REMOTE_PROJECT_HINT, LOCAL_FILE_IN_REMOTE_PROJECT_WARNING, ModuleComparison, ModuleVersionSchema, NEVER_ENCODED, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchId, PatchJSON, PatchSets, PlanRefusalError, ProjectVersionSchema, RAMP_STEPS, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RestoreVerdict, RichTextOptions, SCHEMA_SOURCE_FIXES, SELF_CHECK_VERSION, SKETCHPAD_MAX_TEXT_LENGTH, STAGED_SCHEMA_PREFIX, STUDIO_FIXES, SerializedArraySchema, SerializedBooleanSchema, SerializedCodeSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedDiscriminatedUnionSchema, SerializedEnumSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedLocaleSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedSettingsSchema, SerializedStringSchema, SerializedValViewSchema, SerializedVideoSchema, SharedValConfig, Sketchpad, SketchpadSummary, VAL_CLIENT_HEADER, VAL_ENABLE_COOKIE_NAME, VAL_GREEN_RAMP, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_STUDIO_HEADER, ValCommit, ValDeployment, VideoOptions, VideoStreamOption, WARNING_FIXES, WASM_ENCODED_MIME_TYPES, accentRamp, applyChain, applyPatchRecord, baseModuleContent, baseVersionOf, buildDuplicatePatch, buildEmptyAtPathPatch, buildFileRefValue, buildImageGalleryPatch, buildRemoveImageGalleryEntryPatch, buildSearchIndex, bundleTier, chainHeadOf, chainOf, checkCompatibility, checkSketch, childSource, chooseEncoded, classifyJsonValuesOp, concatModulePath, contrastRatio, createRegExpFromPattern, createSearchIndex, createValClient, declaredLocales, describeContainerAtPath, emptyOf, engineWork, entryAddress, entryState, enumLocaleTag, expandJsonValuesRootOp, expandJsonValuesRootOpForKey, explainIncompatible, extendChain, fallbackMimeType, filterBlockingValidationErrors, filterRoutesByPatterns, findSimilar, fitWithin, flattenRichText, forEachRichTextImage, formatSketch, formatSketchBox, formatSketchText, getErrorMessageFromUnknownJson, getFilenameFromRef, getInlineImgInfo, getKeyOfRecordAt, getRefParts, getRoutesOf, getRoutesWithModulePaths, getSourceAt, getValDemo, hasLocaleRouteParam, indexModule, isInsidePatchSetPath, isJsonObject, isJsonValuesRootOp, isLocaleSchema, isMimeTypeAccepted, isRemoteSchema, isRepairedOnSave, isSchemaSourceFixError, isSkippedSource, isStudioFix, isValidationWarning, isWarningFix, joinSharedChanges, keyOfRecordPath, levenshtein, localeAt, localeMeantBy, localeOfModuleRoot, localeOfRouteKey, localeParamOfRouteKey, localeScopeAt, memoryTier, moduleKey, newestCommitSha, parseModuleKey, parseSketch, partitionValidationErrors, pathOfModuleKey, performSearch, proposalCustomProperties, rebaseContentOp, relativeLuminance, relocateForLocale, remirrorToRichTextSource, removeModule, resolveEncodeSettings, resolveEntry, resolvePairGround, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, resolveSerializedSchemaAtPath, richTextImageSchema, richTextToRemirror, safeParsePatch, sameEntryVersion, sameModuleVersion, schemaErrorsOf, schemaTypesOfPath, schemaVersionOf, selectionClosure, selfCheck, shouldEncodeWithWasm, sideFiles, sideSource, sketchTitle, sourcePathOfChild, storeWork, themeCustomProperties, tiered, toolNames, traverseSchemaSource, urlOf, valClientHeaders, valModulesEntryFile, valModulesEntryPath, validateRoutePatterns, versionHashCount, wholeEntryKey, withExtension };
11838
+ export { AA_LARGE, AA_TEXT, AIToolCallSummaries, AnalyticsBeacon, Api, BRAND_CONTRAST_PAIRS, BinaryFileRef, ChainFold, CodeOptions, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, EMPTY_CHAIN, ENCODE_DEFAULTS, ENCODE_MIME_TYPE_OF, ENGINE, FileOptions, HistoricalCommit, HistoricalModule, HistoricalPatchSet, HistoryError, ImageEncodeOption, ImageOptions, JSONValue, JSON_ENTRIES_BATCH_MAX, LOCAL_FILE_IN_REMOTE_PROJECT_HINT, LOCAL_FILE_IN_REMOTE_PROJECT_WARNING, ModuleComparison, ModuleVersionSchema, NEVER_ENCODED, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchId, PatchJSON, PatchSets, PlanRefusalError, ProjectVersionSchema, RAMP_STEPS, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RestoreVerdict, RichTextOptions, SCHEMA_SOURCE_FIXES, SELF_CHECK_VERSION, SESSION_MEDIA_PREFIX, SKETCHPAD_MAX_TEXT_LENGTH, STAGED_SCHEMA_PREFIX, STUDIO_FIXES, SerializedArraySchema, SerializedBooleanSchema, SerializedCodeSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedDiscriminatedUnionSchema, SerializedEnumSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedLocaleSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedSettingsSchema, SerializedStringSchema, SerializedValViewSchema, SerializedVideoSchema, SharedValConfig, Sketchpad, SketchpadSummary, VAL_CLIENT_HEADER, VAL_ENABLE_COOKIE_NAME, VAL_GREEN_RAMP, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_STUDIO_HEADER, ValCommit, ValDeployment, VideoOptions, VideoStreamOption, WARNING_FIXES, WASM_ENCODED_MIME_TYPES, accentRamp, applyChain, applyPatchRecord, baseModuleContent, baseVersionOf, buildDuplicatePatch, buildEmptyAtPathPatch, buildFileRefValue, buildImageGalleryPatch, buildRemoveImageGalleryEntryPatch, buildSearchIndex, bundleTier, chainHeadOf, chainOf, checkCompatibility, checkSketch, childSource, chooseEncoded, classifyJsonValuesOp, concatModulePath, contrastRatio, createRegExpFromPattern, createSearchIndex, createValClient, declaredLocales, describeContainerAtPath, emptyOf, engineWork, entryAddress, entryState, enumLocaleTag, expandJsonValuesRootOp, expandJsonValuesRootOpForKey, explainIncompatible, extendChain, fallbackMimeType, filterBlockingValidationErrors, filterRoutesByPatterns, findSimilar, fitWithin, flattenRichText, forEachRichTextImage, formatSketch, formatSketchBox, formatSketchText, getErrorMessageFromUnknownJson, getFilenameFromRef, getInlineImgInfo, getKeyOfRecordAt, getRefParts, getRoutesOf, getRoutesWithModulePaths, getSourceAt, getValDemo, hasLocaleRouteParam, indexModule, isInsidePatchSetPath, isJsonObject, isJsonValuesRootOp, isLocaleSchema, isMimeTypeAccepted, isRemoteSchema, isRepairedOnSave, isSchemaSourceFixError, isSkippedSource, isStudioFix, isValidationWarning, isWarningFix, joinSharedChanges, keyOfRecordPath, levenshtein, localeAt, localeMeantBy, localeOfModuleRoot, localeOfRouteKey, localeParamOfRouteKey, localeScopeAt, memoryTier, moduleKey, newestCommitSha, parseModuleKey, parseSketch, partitionValidationErrors, pathOfModuleKey, performSearch, proposalCustomProperties, rebaseContentOp, relativeLuminance, relocateForLocale, remirrorToRichTextSource, removeModule, resolveEncodeSettings, resolveEntry, resolvePairGround, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, resolveSerializedSchemaAtPath, richTextImageSchema, richTextToRemirror, safeParsePatch, sameEntryVersion, sameModuleVersion, schemaErrorsOf, schemaTypesOfPath, schemaVersionOf, selectionClosure, selfCheck, shouldEncodeWithWasm, sideFiles, sideSource, sketchFieldTexts, sketchRoutes, sketchTitle, sourcePathOfChild, storeWork, themeCustomProperties, tiered, toolNames, traverseSchemaSource, urlOf, valClientHeaders, valModulesEntryFile, valModulesEntryPath, validateRoutePatterns, versionHashCount, wholeEntryKey, withExtension };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valbuild/shared",
3
- "version": "0.151.0",
3
+ "version": "0.152.0",
4
4
  "private": false,
5
5
  "description": "Val shared types and utilities",
6
6
  "repository": {