@valbuild/core 0.129.0 → 0.130.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/declarations/src/index.d.ts +1 -1
  3. package/dist/declarations/src/preview.d.ts +65 -8
  4. package/dist/declarations/src/render.d.ts +26 -0
  5. package/dist/declarations/src/schema/array.d.ts +8 -5
  6. package/dist/declarations/src/schema/boolean.d.ts +8 -6
  7. package/dist/declarations/src/schema/code.d.ts +8 -6
  8. package/dist/declarations/src/schema/color.d.ts +8 -6
  9. package/dist/declarations/src/schema/date.d.ts +8 -6
  10. package/dist/declarations/src/schema/datetime.d.ts +8 -6
  11. package/dist/declarations/src/schema/discriminatedUnion.d.ts +8 -5
  12. package/dist/declarations/src/schema/enum.d.ts +8 -6
  13. package/dist/declarations/src/schema/file.d.ts +8 -6
  14. package/dist/declarations/src/schema/image.d.ts +8 -6
  15. package/dist/declarations/src/schema/index.d.ts +29 -1
  16. package/dist/declarations/src/schema/keyOf.d.ts +8 -6
  17. package/dist/declarations/src/schema/literal.d.ts +8 -6
  18. package/dist/declarations/src/schema/locale.d.ts +1 -2
  19. package/dist/declarations/src/schema/number.d.ts +8 -6
  20. package/dist/declarations/src/schema/object.d.ts +8 -5
  21. package/dist/declarations/src/schema/record.d.ts +8 -5
  22. package/dist/declarations/src/schema/richtext.d.ts +8 -6
  23. package/dist/declarations/src/schema/route.d.ts +8 -6
  24. package/dist/declarations/src/schema/settings.d.ts +1 -1
  25. package/dist/declarations/src/schema/string.d.ts +8 -11
  26. package/dist/declarations/src/source/settings.d.ts +27 -0
  27. package/dist/{index-21fd16b1.cjs.dev.js → index-27d57011.cjs.dev.js} +589 -349
  28. package/dist/{index-06bb2832.esm.js → index-71a59a88.esm.js} +589 -349
  29. package/dist/{index-f5842720.cjs.prod.js → index-9e372bd7.cjs.prod.js} +589 -349
  30. package/dist/valbuild-core.cjs.dev.js +1 -1
  31. package/dist/valbuild-core.cjs.prod.js +1 -1
  32. package/dist/valbuild-core.esm.js +1 -1
  33. package/package.json +1 -1
  34. package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
  35. package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
  36. package/patch/dist/valbuild-core-patch.esm.js +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # @valbuild/core
2
2
 
3
+ ## 0.130.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#663](https://github.com/valbuild/val/pull/663) [`8425378`](https://github.com/valbuild/val/commit/8425378c315ea46b5d822f1130b633e0449ff1b0) Thanks [@freekh](https://github.com/freekh)! - A value that has not been written yet offers to create it, instead of looking broken
8
+
9
+ Opening an array item or a record entry whose value is `null` rendered the item
10
+ schema's fields over nothing, so every one of them reported **Not Found** — a
11
+ column of broken fields where the truth is one fact about the value: nobody has
12
+ written it. It now shows one **Create** button.
13
+
14
+ A field inside an object always had this — the checkbox beside its label — but
15
+ a value opened on its own has no such wrapper, and that is exactly what
16
+ navigating to an entry does. Both places now agree.
17
+
18
+ This is most visible with a record whose keys are declared by its schema
19
+ (`s.record(s.locale(), …)` or `s.record(s.enum("a", "b"), …)`), where an entry
20
+ nobody has written is `null` rather than absent. There the wording follows: an
21
+ unwritten entry reads as **Not translated** in the list, and the button says
22
+ **Write this translation** rather than Create.
23
+
24
+ Also fixed: `Internal.resolvePath` reported a record entry that exists with a
25
+ falsy value — `null`, but equally `""`, `0` and `false`, in any record — as a
26
+ key the record does not have, so nothing could resolve a path to one.
27
+
28
+ - [#679](https://github.com/valbuild/val/pull/679) [`473a185`](https://github.com/valbuild/val/commit/473a185f70351b44388f3bc1852649e2c1dbe001) Thanks [@freekh](https://github.com/freekh)! - `.preview()` now names a value wherever the Studio shows it, including a module's own root
29
+
30
+ Wherever the Studio shows a piece of content to a human it has to answer three
31
+ questions: what is this called, what is it, what does it look like. Every
32
+ surface answered them itself, out of the path, and they disagreed — the same
33
+ record entry read `blog1` in the heading, "Blog 1" in the scope trail and
34
+ `/blogs/blog1` in the sitemap. They now give one answer, and it is the one you
35
+ wrote:
36
+
37
+ ```ts
38
+ s.record(
39
+ s.object({ title: s.string(), author: s.string(), cover: s.image() }),
40
+ ).preview((blog) => ({
41
+ title: blog.title,
42
+ subtitle: blog.author,
43
+ image: blog.cover,
44
+ }));
45
+ ```
46
+
47
+ That title, subtitle and image are what the heading, list rows, search hits,
48
+ cards and chosen references show. Nothing is required: a project with no
49
+ `.preview()` reads exactly as it did — the route, the key, `[#3](https://github.com/valbuild/val/issues/3)`, or the
50
+ prettified file name — so this is somewhere to improve from rather than
51
+ something to adopt.
52
+
53
+ **`.preview()` on a module's own schema now works.** It was accepted and never
54
+ run: a preview was only ever reified by a CONTAINER for its rows, and a module
55
+ root has no container. So `c.define("/content/authors.val.ts", s.record(…)
56
+ .preview(…), …)` can name the module itself — "Forfattere" rather than
57
+ `authors.val.ts` — and the same is true of a field of an object.
58
+
59
+ **A page's URL is carried separately from its title.** A route is not a worse
60
+ name for a page, it is the page's identity: two drafts both titled "Launch" are
61
+ told apart by `/blog/launch-2026` and by nothing else. Title a page with
62
+ `.preview()` and its route moves to the line under the heading rather than
63
+ disappearing.
64
+
65
+ Two things a preview is deliberately NOT used for, because a preview is a
66
+ closure over source and so changes as an editor types:
67
+
68
+ - The breadcrumb, the Explorer and the Pages tree stay path segments. A trail
69
+ of titles names three things and locates none of them. The one exception is a
70
+ page, whose trail is its ROUTE instead of the file it is stored in — nobody
71
+ reaches a page through the file.
72
+ - Help text. `.describe()` is input help and is shown where a field or a key is
73
+ being ENTERED; a record key's description no longer appears in the heading,
74
+ where the key cannot be edited, and appears in every form that asks for one.
75
+
76
+ Also fixed: a just-uploaded image stayed blank until save in list rows, headings
77
+ and reference dropdowns, which built the URL of the published file rather than
78
+ the pending patch's.
79
+
80
+ - [#677](https://github.com/valbuild/val/pull/677) [`64f0de3`](https://github.com/valbuild/val/commit/64f0de339b8621cb5a6c422dfe55cae5b2bbe2a0) Thanks [@freekh](https://github.com/freekh)! - A guided tour of the Studio, and a `studio.tour` setting to turn it off
81
+
82
+ Editors kept asking what Pages, Media and Data are for. The three words are
83
+ precise inside Val and vague everywhere else, and the Studio said each of them
84
+ in three places — the rail tooltip, the panel header, the empty state — without
85
+ ever defining any.
86
+
87
+ **A guided tour**, offered by a glowing "Take a tour" button on the empty editor
88
+ at `/val/~` and kept permanently in **Quick actions**: welcome, then Pages, Media
89
+ and Data where the project has them, then the assistant where there is one, then
90
+ Review, Preview and Publish. It never opens itself, and the glow stops for good
91
+ once somebody has been through it on that browser.
92
+
93
+ Turn it off for the whole project under **Settings → Studio** — a new
94
+ `studio.tour` field on `s.settings()`, unset meaning the tour is offered. A team
95
+ that finds it noisy switches it off once, for everyone, instead of each person
96
+ dismissing it on each machine; the tour stays in Quick actions for anyone who
97
+ wants it.
98
+
99
+ Also, for the same first-run problem:
100
+
101
+ - **Rail tooltips carry a definition** under the label: "The pages of your site,
102
+ by URL", "Shared images and files, uploaded once", "Content that is not tied
103
+ to one page".
104
+ - **The empty editor is a short glossary** of the destinations this project
105
+ actually has, rather than "No item selected".
106
+ - **Empty states explain instead of reporting.** "No pages yet" now says who
107
+ creates the routes pages go under; "No galleries yet" says what a gallery is
108
+ for.
109
+ - **A project with a single media gallery opens it**, so Media shows media
110
+ instead of one collapsed row named after a module file.
111
+ - **A site map of twenty pages or fewer arrives open.** With a home page at `/`
112
+ the whole site nests under one root row, so Pages used to show a single row
113
+ called Home. Larger sites keep the old behaviour.
114
+ - **Settings points at Account** for the per-person settings — the theme, and
115
+ how the Studio behaves on this machine.
116
+ - Page rows with children now carry `aria-expanded`, as the media panel's rows
117
+ always did.
118
+
3
119
  ## 0.129.0
4
120
 
5
121
  ### Minor Changes
@@ -54,7 +54,7 @@ export { type SerializedImageSchema, ImageSchema } from "./schema/image.js";
54
54
  export { type SerializedFileSchema, FileSchema } from "./schema/file.js";
55
55
  export { type SerializedDateSchema, DateSchema } from "./schema/date.js";
56
56
  export { type SerializedSettingsSchema, SettingsSchema, } from "./schema/settings.js";
57
- export { type SettingsSource, type AssistantSettingsSource, type LocalesSettingsSource, type AssistantAvailability, type ThemeSettingsSource, type ThemeRadius, ASSISTANT_SETTINGS_MAX_LENGTH, THEME_LOGO_DIRECTORY, THEME_RADIUS_STEPS, THEME_RADIUS_LENGTHS, assistantAvailability, } from "./source/settings.js";
57
+ export { type SettingsSource, type AssistantSettingsSource, type LocalesSettingsSource, type AssistantAvailability, type ThemeSettingsSource, type StudioSettingsSource, type ThemeRadius, ASSISTANT_SETTINGS_MAX_LENGTH, THEME_LOGO_DIRECTORY, THEME_RADIUS_STEPS, THEME_RADIUS_LENGTHS, assistantAvailability, } from "./source/settings.js";
58
58
  export { type ResolvedSettingsModule, SETTINGS_MODULE_CONVENTION, isRootModuleFilePath, resolveSettingsModule, } from "./settingsModule.js";
59
59
  export { type Locale, localeTagError, localeOfValue } from "./locale.js";
60
60
  export { type SerializedLocaleSchema, LocaleSchema } from "./schema/locale.js";
@@ -2,6 +2,32 @@ import { Schema } from "./schema/index.js";
2
2
  import { SelectorSource } from "./selector/index.js";
3
3
  import { ImageSource } from "./source/media.js";
4
4
  import { ModuleFilePath, SourcePath } from "./val/index.js";
5
+ /**
6
+ * THE RULE, and the only sentence that needs to be remembered:
7
+ *
8
+ * **`.describe()` is INPUT HELP and is shown wherever that field — or a
9
+ * record's key — is being ENTERED; `.preview()` is a NAME and is shown
10
+ * wherever the value is REFERRED TO rather than edited; `.render()` is
11
+ * LAYOUT and applies only while the field is open in front of you.**
12
+ *
13
+ * The test that settles every case: **can the reader change something here?**
14
+ * If yes it is a place for a description — the input beside a label, the key
15
+ * box in "New entry", "Rename key", "Duplicate", "New page", the key half of a
16
+ * reference dropdown. If no, it is a place for a preview — a list row, a
17
+ * reference once chosen, a search hit, a sitemap row, the heading of what you
18
+ * navigated to.
19
+ *
20
+ * That is why a description is plain data on the serialized schema and a
21
+ * preview is a closure: a description is true before any value exists and says
22
+ * the same thing to everyone filling the field in, and a preview cannot exist
23
+ * without the one value it names. So a description must never be used as a
24
+ * subtitle — it would repeat one sentence under every row of a list — and a
25
+ * preview must never be used as help text, because there is nothing to preview
26
+ * until after the value has been entered.
27
+ *
28
+ * None of the three substitutes for another: a field with a perfect
29
+ * description still previews as `#3` until someone writes the preview.
30
+ */
5
31
  /**
6
32
  * A PREVIEW is how a VALUE is shown wherever a preview of it is needed — a row
7
33
  * in a sortable list, a key in a reference dropdown, a search hit, a
@@ -76,14 +102,12 @@ export type ArrayPreview = {
76
102
  items: [index: number, value: PreviewItem][];
77
103
  };
78
104
  /**
79
- * Main preview type.
80
- *
81
- * Deliberately not exported: `parent` is the discriminant, and every consumer
82
- * narrows on it rather than on the union as a whole. There is no `layout` here
83
- * on purpose - how a preview is laid out is the editor's business, not the
84
- * schema's.
105
+ * How a CONTAINER's children preview. `parent` is the discriminant, and every
106
+ * consumer narrows on it rather than on the union as a whole. There is no
107
+ * `layout` here on purpose - how a preview is laid out is the editor's
108
+ * business, not the schema's.
85
109
  */
86
- type PreviewTypes = RecordPreview | ArrayPreview;
110
+ export type PreviewRows = RecordPreview | ArrayPreview;
87
111
  type WithStatus<T> = {
88
112
  status: "error";
89
113
  message: string;
@@ -94,7 +118,40 @@ type WithStatus<T> = {
94
118
  status: "success";
95
119
  data: T;
96
120
  };
97
- export type ReifiedPreview = Record<SourcePath | ModuleFilePath, WithStatus<PreviewTypes>>;
121
+ /**
122
+ * Everything known about ONE path, which is two independent things.
123
+ *
124
+ * They are separate fields rather than a union because a path can have both,
125
+ * and that is not a corner case: `s.array(section).preview(...)` where
126
+ * `section` also previews is a section list that shows its rows AND names
127
+ * itself when it is nested in something. One of them would have had to win.
128
+ *
129
+ * - {@link self} - what THIS value is called, from its own schema's
130
+ * `.preview(...)`. This is the only answer for a value with no container to
131
+ * ask: a module root (whose `.preview` was never run before this existed) and
132
+ * any field of an object.
133
+ * - {@link rows} - what its CHILDREN are called, from the ITEM schema's
134
+ * `.preview(...)`, reified by the container. Kept alongside `self` rather
135
+ * than derived from the children's `self` entries because a list needs every
136
+ * row in one answer, and because an EMPTY list still has to preview as an
137
+ * empty list - which is a fact about the schema, not about any value.
138
+ */
139
+ export type PreviewNode = {
140
+ self?: PreviewItem;
141
+ rows?: PreviewRows;
142
+ };
143
+ export type ReifiedPreview = Record<SourcePath | ModuleFilePath, WithStatus<PreviewNode>>;
144
+ /**
145
+ * Fold `incoming` into `target`, path by path. MUTATES `target`.
146
+ *
147
+ * Every `executePreview` used to copy child results across with a plain
148
+ * assignment, which was fine while one path held one thing. It no longer does:
149
+ * a container writes `rows` at its own path and its `self` arrives from the
150
+ * same walk, so an overwrite silently drops whichever landed first. Merging is
151
+ * per FIELD, and an error at a path beats any data there - a preview that threw
152
+ * must not be reported as a preview that is merely absent.
153
+ */
154
+ export declare function mergePreviewInto(target: ReifiedPreview, incoming: ReifiedPreview): void;
98
155
  /**
99
156
  * Which paths a preview is being computed FOR.
100
157
  *
@@ -1,4 +1,30 @@
1
1
  import type { SerializedSchema } from "./schema/index.js";
2
+ /**
3
+ * THE RULE, and the only sentence that needs to be remembered:
4
+ *
5
+ * **`.describe()` is INPUT HELP and is shown wherever that field — or a
6
+ * record's key — is being ENTERED; `.preview()` is a NAME and is shown
7
+ * wherever the value is REFERRED TO rather than edited; `.render()` is
8
+ * LAYOUT and applies only while the field is open in front of you.**
9
+ *
10
+ * The test that settles every case: **can the reader change something here?**
11
+ * If yes it is a place for a description — the input beside a label, the key
12
+ * box in "New entry", "Rename key", "Duplicate", "New page", the key half of a
13
+ * reference dropdown. If no, it is a place for a preview — a list row, a
14
+ * reference once chosen, a search hit, a sitemap row, the heading of what you
15
+ * navigated to.
16
+ *
17
+ * That is why a description is plain data on the serialized schema and a
18
+ * preview is a closure: a description is true before any value exists and says
19
+ * the same thing to everyone filling the field in, and a preview cannot exist
20
+ * without the one value it names. So a description must never be used as a
21
+ * subtitle — it would repeat one sentence under every row of a list — and a
22
+ * preview must never be used as help text, because there is nothing to preview
23
+ * until after the value has been entered.
24
+ *
25
+ * None of the three substitutes for another: a field with a perfect
26
+ * description still previews as `#3` until someone writes the preview.
27
+ */
2
28
  /**
3
29
  * A RENDER is how the FIELD ITSELF is laid out in the editor, and it applies
4
30
  * only when you are looking at the field: `.render({ as: "inline" })` on an
@@ -41,10 +41,13 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
41
41
  /**
42
42
  * Describe this field.
43
43
  *
44
- * The description is shown next to the field's label in the Val editor, so
45
- * it is where you say what an editor needs to know but the field name cannot
46
- * carry. It also travels in the serialized schema, which is what the AI
47
- * assistant and the MCP tools read.
44
+ * The description is INPUT HELP: it is shown where this field's value is
45
+ * entered — beside its input in the Val editor, and for a record's key
46
+ * schema in every form that asks for a key — so it is where you say what an
47
+ * editor needs to know to fill it in RIGHT, which the field name cannot
48
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
49
+ * read somewhere else. The description also travels in the serialized
50
+ * schema, which is what the AI assistant and the MCP tools read.
48
51
  *
49
52
  * Pass `null` to clear a description set earlier.
50
53
  *
@@ -91,7 +94,7 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
91
94
  }[];
92
95
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
93
96
  protected executeSerialize(): SerializedArraySchema;
94
- protected executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope): ReifiedPreview;
97
+ protected executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope, selfIsReifiedByParent?: boolean): ReifiedPreview;
95
98
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
96
99
  protected declaresItemPreview(): boolean;
97
100
  /**
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
@@ -27,10 +27,13 @@ export declare class BooleanSchema<Src extends boolean | null> extends Schema<Sr
27
27
  /**
28
28
  * Describe this field.
29
29
  *
30
- * The description is shown next to the field's label in the Val editor, so
31
- * it is where you say what an editor needs to know but the field name cannot
32
- * carry. It also travels in the serialized schema, which is what the AI
33
- * assistant and the MCP tools read.
30
+ * The description is INPUT HELP: it is shown where this field's value is
31
+ * entered — beside its input in the Val editor, and for a record's key
32
+ * schema in every form that asks for a key — so it is where you say what an
33
+ * editor needs to know to fill it in RIGHT, which the field name cannot
34
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
35
+ * read somewhere else. The description also travels in the serialized
36
+ * schema, which is what the AI assistant and the MCP tools read.
34
37
  *
35
38
  * Pass `null` to clear a description set earlier.
36
39
  *
@@ -93,6 +96,5 @@ export declare class BooleanSchema<Src extends boolean | null> extends Schema<Sr
93
96
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
94
97
  protected declaresItemPreview(): boolean;
95
98
  protected executeSerialize(): SerializedSchema;
96
- protected executePreview(): ReifiedPreview;
97
99
  }
98
100
  export declare const boolean: () => BooleanSchema<boolean>;
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { RawString } from "./string.js";
@@ -58,10 +58,13 @@ export declare class CodeSchema<Src extends string | null> extends Schema<Src> {
58
58
  /**
59
59
  * Describe this field.
60
60
  *
61
- * The description is shown next to the field's label in the Val editor, so
62
- * it is where you say what an editor needs to know but the field name cannot
63
- * carry. It also travels in the serialized schema, which is what the AI
64
- * assistant and the MCP tools read.
61
+ * The description is INPUT HELP: it is shown where this field's value is
62
+ * entered — beside its input in the Val editor, and for a record's key
63
+ * schema in every form that asks for a key — so it is where you say what an
64
+ * editor needs to know to fill it in RIGHT, which the field name cannot
65
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
66
+ * read somewhere else. The description also travels in the serialized
67
+ * schema, which is what the AI assistant and the MCP tools read.
65
68
  *
66
69
  * Pass `null` to clear a description set earlier.
67
70
  *
@@ -143,6 +146,5 @@ export declare class CodeSchema<Src extends string | null> extends Schema<Src> {
143
146
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
144
147
  protected declaresItemPreview(): boolean;
145
148
  protected executeSerialize(): SerializedSchema;
146
- protected executePreview(): ReifiedPreview;
147
149
  }
148
150
  export declare const code: (options?: CodeOptions) => CodeSchema<RawString>;
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { ColorFormat } from "./colorFormat.js";
@@ -53,10 +53,13 @@ export declare class ColorSchema<Src extends string | null> extends Schema<Src>
53
53
  /**
54
54
  * Describe this field.
55
55
  *
56
- * The description is shown next to the field's label in the Val editor, so
57
- * it is where you say what an editor needs to know but the field name cannot
58
- * carry. It also travels in the serialized schema, which is what the AI
59
- * assistant and the MCP tools read.
56
+ * The description is INPUT HELP: it is shown where this field's value is
57
+ * entered — beside its input in the Val editor, and for a record's key
58
+ * schema in every form that asks for a key — so it is where you say what an
59
+ * editor needs to know to fill it in RIGHT, which the field name cannot
60
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
61
+ * read somewhere else. The description also travels in the serialized
62
+ * schema, which is what the AI assistant and the MCP tools read.
60
63
  *
61
64
  * Pass `null` to clear a description set earlier.
62
65
  *
@@ -129,6 +132,5 @@ export declare class ColorSchema<Src extends string | null> extends Schema<Src>
129
132
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
130
133
  protected declaresItemPreview(): boolean;
131
134
  protected executeSerialize(): SerializedSchema;
132
- protected executePreview(): ReifiedPreview;
133
135
  }
134
136
  export declare const color: (options?: ColorOptions) => ColorSchema<RawString>;
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { RawString } from "./string.js";
@@ -46,10 +46,13 @@ export declare class DateSchema<Src extends string | null> extends Schema<Src> {
46
46
  /**
47
47
  * Describe this field.
48
48
  *
49
- * The description is shown next to the field's label in the Val editor, so
50
- * it is where you say what an editor needs to know but the field name cannot
51
- * carry. It also travels in the serialized schema, which is what the AI
52
- * assistant and the MCP tools read.
49
+ * The description is INPUT HELP: it is shown where this field's value is
50
+ * entered — beside its input in the Val editor, and for a record's key
51
+ * schema in every form that asks for a key — so it is where you say what an
52
+ * editor needs to know to fill it in RIGHT, which the field name cannot
53
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
54
+ * read somewhere else. The description also travels in the serialized
55
+ * schema, which is what the AI assistant and the MCP tools read.
53
56
  *
54
57
  * Pass `null` to clear a description set earlier.
55
58
  *
@@ -132,7 +135,6 @@ export declare class DateSchema<Src extends string | null> extends Schema<Src> {
132
135
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
133
136
  protected declaresItemPreview(): boolean;
134
137
  protected executeSerialize(): SerializedSchema;
135
- protected executePreview(): ReifiedPreview;
136
138
  }
137
139
  export declare const date: (options?: Record<string, never>) => DateSchema<RawString>;
138
140
  export {};
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { RawString } from "./string.js";
@@ -50,10 +50,13 @@ export declare class DateTimeSchema<Src extends string | null> extends Schema<Sr
50
50
  /**
51
51
  * Describe this field.
52
52
  *
53
- * The description is shown next to the field's label in the Val editor, so
54
- * it is where you say what an editor needs to know but the field name cannot
55
- * carry. It also travels in the serialized schema, which is what the AI
56
- * assistant and the MCP tools read.
53
+ * The description is INPUT HELP: it is shown where this field's value is
54
+ * entered — beside its input in the Val editor, and for a record's key
55
+ * schema in every form that asks for a key — so it is where you say what an
56
+ * editor needs to know to fill it in RIGHT, which the field name cannot
57
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
58
+ * read somewhere else. The description also travels in the serialized
59
+ * schema, which is what the AI assistant and the MCP tools read.
57
60
  *
58
61
  * Pass `null` to clear a description set earlier.
59
62
  *
@@ -159,7 +162,6 @@ export declare class DateTimeSchema<Src extends string | null> extends Schema<Sr
159
162
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
160
163
  protected declaresItemPreview(): boolean;
161
164
  protected executeSerialize(): SerializedSchema;
162
- protected executePreview(): ReifiedPreview;
163
165
  }
164
166
  export declare const datetime: (options?: Record<string, never>) => DateTimeSchema<RawString>;
165
167
  export {};
@@ -62,10 +62,13 @@ export declare class DiscriminatedUnionSchema<Key extends string, T extends Disc
62
62
  /**
63
63
  * Describe this field.
64
64
  *
65
- * The description is shown next to the field's label in the Val editor, so it is
66
- * where you say what an editor needs to know but the field name cannot carry. It
67
- * also travels in the serialized schema, which is what the AI assistant and the
68
- * MCP tools read.
65
+ * The description is INPUT HELP: it is shown where this field's value is
66
+ * entered — beside its input in the Val editor, and for a record's key
67
+ * schema in every form that asks for a key — so it is where you say what an
68
+ * editor needs to know to fill it in RIGHT, which the field name cannot
69
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
70
+ * read somewhere else. The description also travels in the serialized
71
+ * schema, which is what the AI assistant and the MCP tools read.
69
72
  *
70
73
  * Pass `null` to clear a description set earlier.
71
74
  *
@@ -165,6 +168,6 @@ export declare class DiscriminatedUnionSchema<Key extends string, T extends Disc
165
168
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
166
169
  protected declaresItemPreview(): boolean;
167
170
  protected executeSerialize(): SerializedDiscriminatedUnionSchema;
168
- protected executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope): ReifiedPreview;
171
+ protected executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope, selfIsReifiedByParent?: boolean): ReifiedPreview;
169
172
  }
170
173
  export declare const discriminatedUnion: <Key extends string, T extends DiscriminatedUnionItems<Key>>(key: Key, ...objects: T) => DiscriminatedUnionSchema<Key, T, DiscriminatedUnionSourceOf<Key, T>>;
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult } from "./index.js";
2
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
2
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
3
3
  import { FieldRender } from "../render.js";
4
4
  import { SourcePath } from "../val/index.js";
5
5
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
@@ -51,10 +51,13 @@ export declare class EnumSchema<Src extends string | null> extends Schema<Src> {
51
51
  /**
52
52
  * Describe this field.
53
53
  *
54
- * The description is shown next to the field's label in the Val editor, so it is
55
- * where you say what an editor needs to know but the field name cannot carry. It
56
- * also travels in the serialized schema, which is what the AI assistant and the
57
- * MCP tools read.
54
+ * The description is INPUT HELP: it is shown where this field's value is
55
+ * entered — beside its input in the Val editor, and for a record's key
56
+ * schema in every form that asks for a key — so it is where you say what an
57
+ * editor needs to know to fill it in RIGHT, which the field name cannot
58
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
59
+ * read somewhere else. The description also travels in the serialized
60
+ * schema, which is what the AI assistant and the MCP tools read.
58
61
  *
59
62
  * Pass `null` to clear a description set earlier.
60
63
  *
@@ -120,7 +123,6 @@ export declare class EnumSchema<Src extends string | null> extends Schema<Src> {
120
123
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
121
124
  protected declaresItemPreview(): boolean;
122
125
  protected executeSerialize(): SerializedEnumSchema;
123
- protected executePreview(): ReifiedPreview;
124
126
  }
125
127
  /**
126
128
  * Define a string that must be one of a fixed set of values.
@@ -4,7 +4,7 @@ import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema }
4
4
  import { ModulePath, SourcePath } from "../val/index.js";
5
5
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
6
6
  import { ValModule } from "../index.js";
7
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
7
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
8
8
  import { FieldRender } from "../render.js";
9
9
  import { FilesetEntryMetadata } from "./fileset.js";
10
10
  export type FileOptions = {
@@ -40,10 +40,13 @@ export declare class FileSchema<Src extends FileSource | null> extends Schema<Sr
40
40
  /**
41
41
  * Describe this field.
42
42
  *
43
- * The description is shown next to the field's label in the Val editor, so
44
- * it is where you say what an editor needs to know but the field name cannot
45
- * carry. It also travels in the serialized schema, which is what the AI
46
- * assistant and the MCP tools read.
43
+ * The description is INPUT HELP: it is shown where this field's value is
44
+ * entered — beside its input in the Val editor, and for a record's key
45
+ * schema in every form that asks for a key — so it is where you say what an
46
+ * editor needs to know to fill it in RIGHT, which the field name cannot
47
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
48
+ * read somewhere else. The description also travels in the serialized
49
+ * schema, which is what the AI assistant and the MCP tools read.
47
50
  *
48
51
  * Pass `null` to clear a description set earlier.
49
52
  *
@@ -145,7 +148,6 @@ export declare class FileSchema<Src extends FileSource | null> extends Schema<Sr
145
148
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
146
149
  protected declaresItemPreview(): boolean;
147
150
  protected executeSerialize(): SerializedSchema;
148
- protected executePreview(): ReifiedPreview;
149
151
  }
150
152
  /**
151
153
  * A file picked from a gallery. Its mime type lives in the gallery, so the
@@ -3,7 +3,7 @@ import { GalleryImageSource, ImageSource } from "../source/media.js";
3
3
  import { ModulePath, SourcePath } from "../val/index.js";
4
4
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
5
5
  import { ValModule } from "../index.js";
6
- import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
6
+ import { ItemPreviewInput, PreviewItem } from "../preview.js";
7
7
  import { FieldRender } from "../render.js";
8
8
  import { AltSource, ImagesetEntryMetadata } from "./imageset.js";
9
9
  /**
@@ -85,10 +85,13 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
85
85
  /**
86
86
  * Describe this field.
87
87
  *
88
- * The description is shown next to the field's label in the Val editor, so
89
- * it is where you say what an editor needs to know but the field name cannot
90
- * carry. It also travels in the serialized schema, which is what the AI
91
- * assistant and the MCP tools read.
88
+ * The description is INPUT HELP: it is shown where this field's value is
89
+ * entered — beside its input in the Val editor, and for a record's key
90
+ * schema in every form that asks for a key — so it is where you say what an
91
+ * editor needs to know to fill it in RIGHT, which the field name cannot
92
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
93
+ * read somewhere else. The description also travels in the serialized
94
+ * schema, which is what the AI assistant and the MCP tools read.
92
95
  *
93
96
  * Pass `null` to clear a description set earlier.
94
97
  *
@@ -219,7 +222,6 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
219
222
  protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
220
223
  protected declaresItemPreview(): boolean;
221
224
  protected executeSerialize(): SerializedSchema;
222
- protected executePreview(): ReifiedPreview;
223
225
  }
224
226
  /**
225
227
  * An image picked from a gallery. Its dimensions and mime type live in the
@@ -270,13 +270,41 @@ export declare abstract class Schema<Src extends SelectorSource> {
270
270
  abstract hidden(isHidden?: boolean): Schema<Src>;
271
271
  protected abstract executeSerialize(): SerializedSchema;
272
272
  /**
273
+ * This value's preview, and every preview below it.
274
+ *
275
+ * The default is the whole of it for a LEAF: a leaf has nothing below it, so
276
+ * all it can contribute is {@link PreviewNode.self}. Containers override this
277
+ * and recurse; object and discriminated union add nothing of their own beyond
278
+ * the self they inherit here, and array and record add `rows`.
279
+ *
273
280
  * @param scope Which paths the caller needs a preview for. Absent means the
274
281
  * whole module, which is what every caller passed before scoping existed.
275
282
  * See {@link PreviewScope}: a container prunes recursion where nothing is
276
283
  * wanted, and previews a WINDOW when its own path is not wanted but some of
277
284
  * its items are — which is the single-visible-row case.
285
+ * @param selfIsReifiedByParent Set by an array or record on its DIRECT items,
286
+ * and by nothing else. Such an item's preview is the same closure the
287
+ * container is already running into {@link PreviewNode.rows}, so emitting a
288
+ * `self` for it as well would call the user's closure twice per row — which
289
+ * it did, and which the scoped-preview tests caught by counting. The flag
290
+ * does not travel further down: a grandchild is nobody's row.
291
+ */
292
+ protected executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope, selfIsReifiedByParent?: boolean): ReifiedPreview;
293
+ /**
294
+ * THIS value as a preview, at its own path — `{ self }` and nothing else.
295
+ *
296
+ * Every schema can produce one, which is the point: before this existed a
297
+ * value's preview was reified only by its CONTAINER, so a value with no
298
+ * container had none. A module root is exactly that, and `.preview(...)` on
299
+ * a module's own schema was therefore dead code — the studio showed the file
300
+ * name and nothing a developer wrote could change it. So is any field of an
301
+ * object, which reifies no rows.
302
+ *
303
+ * Gated on {@link PreviewScope.wants} rather than `wantsUnder`: a caller
304
+ * asking about a CONTAINER wants its rows, and computing a self for every
305
+ * descendant on the way past would run each item's closure twice.
278
306
  */
279
- protected abstract executePreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope): ReifiedPreview;
307
+ protected executeSelfPreview(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: PreviewScope): ReifiedPreview;
280
308
  /**
281
309
  * This value AS A PREVIEW — what a container's row, a reference dropdown or
282
310
  * a search hit shows for it. Runs the schema's own `preview` closure;