@u-krupaveho-kraba/form-editor-schemas 0.1.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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @topol/form-editor-schemas
2
+
3
+ Zod v4 schemas for the **Topol Form Editor's host-facing contract** — the
4
+ options a Host App passes to `TopolEditor.init()`, the `CampaignItem` shape it
5
+ loads and gets back on save, and the theme library it owns. Built on
6
+ [`zod/mini`](https://zod.dev/packages/mini) for a smaller runtime footprint.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @topol/form-editor-schemas @topol/form-schemas zod
12
+ ```
13
+
14
+ `zod` (`^4.1.11`) and `@topol/form-schemas` (`^0.4.0`) are peer dependencies,
15
+ not bundled.
16
+
17
+ ## What's in here
18
+
19
+ - **`TopolFormEditorOptionsSchema`** — the whole `init()` contract: the mount
20
+ target, locale, theme library, the `item`/`items` being edited, the file
21
+ manager config, and every Host callback (`onSave`, `onThemeSave`, …).
22
+ - **`CampaignItemSchema` / `VariantSchema`** — the *editor-facing* item shape,
23
+ carrying a resolved `form` rather than the wire schema's `definition` (the
24
+ editor only ever holds real content to render, never a lazy `{ref}`
25
+ pointer). Their envelope fields come straight from `@topol/form-schemas`'
26
+ `CampaignItemBaseSchema`.
27
+
28
+ `form` is **nullable**. Inbound, `null` means a brand-new, never-saved
29
+ variant, and opens on a blank canvas. It is reachable outbound too, but
30
+ only narrowly: `onSave` mirrors back what the editor holds for a variant it
31
+ was handed and never asked to load: once a variant has been through
32
+ `load()`, a blank canvas has been substituted and it never saves back
33
+ `null`. A *missing* `form` key is rejected either way: an item carries
34
+ `form` or `variants`, never neither.
35
+ - **`ThemeGroupSchema` / `ThemeEntrySchema` / `ThemeSavePayloadSchema`** — the
36
+ Host-owned theme library and the payload the Theme Panel sends back to it.
37
+ - **`FileManagerOptionsSchema`** — an inlined mirror of `@topol/image-picker`'s
38
+ `FileOptions`, until that package gets a publish story of its own.
39
+
40
+ ## Usage
41
+
42
+ ```ts
43
+ import {
44
+ TopolFormEditorOptionsSchema,
45
+ type TopolFormEditorOptions,
46
+ } from "@topol/form-editor-schemas";
47
+
48
+ const result = TopolFormEditorOptionsSchema.safeParse(options);
49
+ if (!result.success) {
50
+ // result.error describes exactly which field failed and why
51
+ }
52
+ ```
53
+
54
+ The editor validates both `init()`'s options and `load()`'s payload against
55
+ these schemas at runtime and **hard-stops** on failure — it refuses to mount,
56
+ or refuses to apply, and reports through the `onError` callback (plus a
57
+ `console.error`, since `onError` is optional) rather than rendering a
58
+ partially-broken editor.
59
+
60
+ ## Why this is separate from `@topol/form-schemas`
61
+
62
+ `@topol/form-schemas` describes the **published campaign JSON** — a stored
63
+ artifact consumed by the form widget on customer sites and by backend publish
64
+ jobs, which needs long-lived backward compatibility.
65
+
66
+ This package describes **how a host boots the editor UI** — a live integration
67
+ surface, not a stored artifact, with a different audience and a different
68
+ compatibility promise. One version number can't clearly signal both "is my
69
+ published campaign JSON still valid" and "did my editor integration just
70
+ break". The dependency runs one way only: this package peer-depends on
71
+ `@topol/form-schemas` for `Form`/`FormTheme`, never the reverse.
72
+
73
+ ## Versioning
74
+
75
+ This package's version tracks the editor's own contract changes and is bumped
76
+ in the same change that alters the contract. It is independent of
77
+ `@topol/form-schemas`' version line.
78
+
79
+ ## Source
80
+
81
+ Part of the [Topol](https://topol.io) editors monorepo, which is a private
82
+ repository — the `repository` field above won't resolve for external
83
+ visitors. This package is published publicly because its contents (schema
84
+ shapes, no business logic or credentials) are safe to distribute even though
85
+ the source repo isn't.
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./schemas/campaignItem.js";
2
+ export * from "./schemas/theme.js";
3
+ export * from "./schemas/fileManager.js";
4
+ export * from "./schemas/options.js";
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@u-krupaveho-kraba/form-editor-schemas",
3
+ "version": "0.1.0",
4
+ "description": "Zod schemas for the Topol Form Editor's host-facing init/save contract",
5
+ "author": "Topol.io",
6
+ "license": "ISC",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TOPOL-io/editors.git",
10
+ "directory": "packages/form-editor-schemas"
11
+ },
12
+ "type": "module",
13
+ "main": "./index.js",
14
+ "module": "./index.js",
15
+ "types": "./types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./types/index.d.ts",
19
+ "import": "./index.js",
20
+ "require": "./index.js"
21
+ }
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "peerDependencies": {
27
+ "@u-krupaveho-kraba/form-schemas": "^0.4.0",
28
+ "zod": "^4.1.11"
29
+ }
30
+ }
@@ -0,0 +1,56 @@
1
+ import * as z from "zod/mini";
2
+ import { CampaignItemBaseSchema, FormSchema } from "@u-krupaveho-kraba/form-schemas";
3
+ /**
4
+ * One AB-test alternative inside a `CampaignItem` (ADR 0028) -- its own
5
+ * `form` content lives right here, not behind a separate `load()` envelope.
6
+ *
7
+ * `form` (not the wire schema's `definition`) because the editor can only
8
+ * ever hold real content to render/edit, never a lazy `{ref}` pointer --
9
+ * see ADR 0028's "Considered and rejected" for why that union doesn't carry
10
+ * over to this layer.
11
+ *
12
+ * Nullable, not optional: a brand-new, never-saved variant (a `topol_forms`
13
+ * row with no definition yet) is sent as `form: null` and opens on a blank
14
+ * canvas (ticket 138). A *missing* `form` key is still rejected -- that's
15
+ * how the item union below enforces ADR 0028's "either `form` or
16
+ * `variants`, never neither".
17
+ *
18
+ * `null` is also reachable on the way *out*, though only narrowly: `onSave`
19
+ * mirrors back what the editor holds for a variant it was handed but never
20
+ * asked to load content for. Once a variant has been through `load()` the
21
+ * editor has substituted a blank canvas, so a loaded variant never saves
22
+ * back `null`.
23
+ */
24
+ export const VariantSchema = z.object({
25
+ id: z.string(),
26
+ name: z.string(),
27
+ form: z.nullable(FormSchema),
28
+ trafficWeight: z.optional(z.number()),
29
+ });
30
+ /**
31
+ * The unit of independent client-side evaluation the editor session reads/
32
+ * edits (ADR 0028, reopening ticket 102/110). Carries its own content
33
+ * directly -- either a single `form` (the common, non-AB case) or
34
+ * `variants` (opt-in AB test), never both, never neither -- mirroring
35
+ * `@topol/form-schemas`' wire `CampaignItem` one-for-one except for that
36
+ * `form`/`definition` naming (see `VariantSchema`'s doc comment).
37
+ *
38
+ * The envelope fields (`id`, `name`, `targeting`, `trigger`,
39
+ * `exclusiveWith`, `campaignId`) are spread from `@topol/form-schemas`'
40
+ * `CampaignItemBaseSchema` rather than restated here -- the whole point of
41
+ * peer-depending on that package (ADR 0029 decision 2's one-directional
42
+ * dependency). `targeting`/`trigger` are this item's own rule, editable via
43
+ * the editor's targeting panel; `exclusiveWith` is shown read-only and is
44
+ * never editable there.
45
+ *
46
+ * Kept as a plain `z.union` rather than a `z.discriminatedUnion`, same as
47
+ * the wire schema: there is no literal tag field to discriminate on, the
48
+ * two branches are told apart structurally.
49
+ */
50
+ export const CampaignItemSchema = z.union([
51
+ z.object({
52
+ ...CampaignItemBaseSchema.shape,
53
+ variants: z.array(VariantSchema),
54
+ }),
55
+ z.object({ ...CampaignItemBaseSchema.shape, form: z.nullable(FormSchema) }),
56
+ ]);
@@ -0,0 +1,69 @@
1
+ import * as z from "zod/mini";
2
+ /**
3
+ * Locales the file manager UI ships translations for -- mirrors
4
+ * `@topol/image-picker`'s own `ILanguage`.
5
+ */
6
+ export const FileManagerLanguageSchema = z.enum([
7
+ "en",
8
+ "fr",
9
+ "pt",
10
+ "es",
11
+ "ja",
12
+ "zh",
13
+ "ru",
14
+ "tr",
15
+ "de",
16
+ "sv",
17
+ "sk",
18
+ "nl",
19
+ "it",
20
+ "fi",
21
+ "ro",
22
+ "cs",
23
+ "pl",
24
+ "ko",
25
+ "vi",
26
+ "he",
27
+ "ar",
28
+ ]);
29
+ /**
30
+ * `TopolFormEditorOptions.fileManager` -- the image picker / file manager's
31
+ * own configuration, forwarded through the editor untouched.
32
+ *
33
+ * **Inlined deliberately** (ADR 0029's "What's deliberately not built
34
+ * here"): the real declaration is `FileOptions` in `@topol/image-picker`,
35
+ * which is `"private": true` and has no publish story of its own. Giving it
36
+ * one is ticket 166, split off so this package wouldn't stall on a second
37
+ * package's plumbing. Until then this mirrors that type by hand -- and so
38
+ * must be re-checked against it whenever it changes, exactly the drift risk
39
+ * ADR 0029 exists to remove everywhere else.
40
+ */
41
+ export const FileManagerOptionsSchema = z.object({
42
+ imageMaxSize: z.number(),
43
+ language: z.optional(FileManagerLanguageSchema),
44
+ urls: z.object({
45
+ folders: z.string(),
46
+ lambda: z.optional(z.string()),
47
+ imageUpload: z.string(),
48
+ GCS_SIGNED_URL: z.optional(z.string()),
49
+ }),
50
+ type: z.optional(z.string()),
51
+ id: z.optional(z.string()),
52
+ userId: z.string(),
53
+ apiKey: z.string(),
54
+ imageCompressionOptions: z.optional(z.object({
55
+ qualityJpeg: z.number(),
56
+ qualityPng: z.number(),
57
+ maxWidth: z.optional(z.number()),
58
+ enableAutoResize: z.optional(z.boolean()),
59
+ })),
60
+ pexelsApiKey: z.optional(z.string()),
61
+ light: z.optional(z.boolean()),
62
+ imageCreation: z.optional(z.boolean()),
63
+ disallowRemove: z.optional(z.boolean()),
64
+ preferences: z.optional(z.object({
65
+ defaultTilesView: z.optional(z.boolean()),
66
+ hidePexelsIntegration: z.optional(z.boolean()),
67
+ maxUploadingFiles: z.optional(z.number()),
68
+ })),
69
+ });
@@ -0,0 +1,218 @@
1
+ import * as z from "zod/mini";
2
+ import { FormThemeSchema, InputFieldSchema, PreferenceSchema, } from "@u-krupaveho-kraba/form-schemas";
3
+ import { CampaignItemSchema, } from "./campaignItem.js";
4
+ import { FileManagerOptionsSchema } from "./fileManager.js";
5
+ import { ThemeGroupSchema, } from "./theme.js";
6
+ /**
7
+ * A Host-supplied callback. Functions can't be described structurally by
8
+ * Zod (and never survive the postMessage hop into the child iframe anyway,
9
+ * see `ChildInitOptions` in the editor itself), so validation is "is this
10
+ * callable" and the *signature* is carried by the type parameter, which is
11
+ * what `z.infer` reports.
12
+ */
13
+ const callback = () => z.custom((value) => typeof value === "function");
14
+ /**
15
+ * The Form Editor's own authoring-UI locale -- which language the person
16
+ * *building* a form sees the editor chrome in.
17
+ *
18
+ * Deliberately its own enum rather than a reuse of `@topol/form-schemas`'
19
+ * `FormLanguageSchema`, even though both list exactly `["en", "cs"]` today.
20
+ * That one is the locale of a *form's content* (`Form.language`, what an End
21
+ * Customer reads); this one is an editor-UI concern. Aliasing them would
22
+ * mean the day either list grows -- a new widget translation, or a new
23
+ * editor translation -- it silently widens the other contract too.
24
+ */
25
+ export const EditorLanguageSchema = z.enum(["en", "cs"]);
26
+ /** Comments left-panel section config. Omit, or leave `url` unset, to disable it. */
27
+ export const CommentsConfigSchema = z.object({
28
+ /** Endpoint for the Comments feature (list/get/post/resolve), same pattern as `aiUrl`. Required for the panel to show at all. */
29
+ url: z.optional(z.string()),
30
+ /** Force-disables the panel even when `url` is set. Has no effect the other way -- omitting `url` disables it regardless of this flag. */
31
+ enabled: z.optional(z.boolean()),
32
+ });
33
+ /** Autosaves left-panel section config. Omit, or leave `url` unset, to disable it. */
34
+ export const AutosaveConfigSchema = z.object({
35
+ /** Endpoint for the Autosaves feature (list/get/save), same pattern as `aiUrl`. Required for the panel to show at all. */
36
+ url: z.optional(z.string()),
37
+ /** Force-disables the panel even when `url` is set. Has no effect the other way -- omitting `url` disables it regardless of this flag. */
38
+ enabled: z.optional(z.boolean()),
39
+ /** Autosave interval in seconds. Minimum 30s in production; defaults to 60s. */
40
+ interval: z.optional(z.number()),
41
+ });
42
+ /** Display identity for a person shown as the author on comments/autosaves. */
43
+ export const EditorUserSchema = z.object({
44
+ userId: z.union([z.string(), z.number()]),
45
+ name: z.string(),
46
+ avatarUrl: z.optional(z.string()),
47
+ });
48
+ export const EditorAuthorizeSchema = z.object({
49
+ apiKey: z.string(),
50
+ userId: z.union([z.string(), z.number()]),
51
+ });
52
+ export const EditorCallbacksSchema = z.object({
53
+ /**
54
+ * Fires on Save, carrying the complete, current `item`/`items` tree --
55
+ * whichever shape was configured at init, mirrored exactly, never
56
+ * wrapped/unwrapped the other way (ADR 0028). No dirty-only filtering: a
57
+ * host diffs against its own last-saved copy if it wants to know what
58
+ * changed.
59
+ */
60
+ onSave: z.optional(callback()),
61
+ onCreatePreference: z.optional(callback()),
62
+ /** Fired when the editor fails to initialize, or when a payload the Host sent fails validation (ADR 0029 decision 3). */
63
+ onError: z.optional(callback()),
64
+ /** Fired when the user clicks the WindowBar's close button (requires `"close"` in `windowBar`). The Host is responsible for tearing down/hiding the editor -- the child does not attempt its own teardown. */
65
+ onEditorClose: z.optional(callback()),
66
+ /**
67
+ * Fired when the user clicks the WindowBar's rename pencil, carrying the
68
+ * *current* `name` -- same delegate-to-Host shape as LPE's
69
+ * `onTemplateRename`. The child does not rename anything itself; the Host
70
+ * is expected to run its own rename UI and, if it commits a change, push
71
+ * the new value back down through a later `init()`/`name` update.
72
+ * Presence of this callback (not a separate boolean flag) is what shows
73
+ * the rename pencil.
74
+ */
75
+ onFormRename: z.optional(callback()),
76
+ /** Fired INSTEAD OF `onSave` when the user picks "Save and close" -- carries the same payload `onSave` would, plus signals the Host App to tear down/hide the editor. `onSave` does not also fire for this action. */
77
+ onSaveAndClose: z.optional(callback()),
78
+ /**
79
+ * Fired when the user saves a diverged theme from the Theme Panel (ticket
80
+ * 151, ADR 0022) -- `id` present means "update this owned theme in
81
+ * place," absent means "fork a new one" (the Host assigns the new id, the
82
+ * panel never generates one). The Host persists it and resolves with the
83
+ * complete, current `ThemeGroup[]` -- the panel replaces its local copy
84
+ * with whatever comes back rather than computing placement itself.
85
+ * Presence of this callback (not a separate boolean flag) is what shows
86
+ * the Save affordance at all.
87
+ */
88
+ onThemeSave: z.optional(callback()),
89
+ /**
90
+ * Renames an owned (unlocked) theme in place -- ticket 151, ADR 0022. A
91
+ * Locked Theme has no rename affordance, so `id` always refers to
92
+ * something the Host already owns. Resolves with the complete, current
93
+ * `ThemeGroup[]`, same replace-local-copy convention as `onThemeSave`.
94
+ */
95
+ onThemeRename: z.optional(callback()),
96
+ /**
97
+ * Deletes an owned (unlocked) theme -- ticket 151, ADR 0022. Same id/
98
+ * resolution/no-Locked-affordance rules as `onThemeRename`. The panel
99
+ * asks for confirmation itself before calling this.
100
+ */
101
+ onThemeRemove: z.optional(callback()),
102
+ });
103
+ /**
104
+ * ADR 0028: `item` and `items` are mutually exclusive -- a Host sends
105
+ * exactly one, or neither. Sending both isn't a runtime crash today (the
106
+ * editor's `options.item ?? options.items` picks `item` and silently
107
+ * discards `items`), which is precisely why it needs rejecting: quietly
108
+ * editing half the session a Host asked for is the "partially-broken"
109
+ * outcome ADR 0029 decision 3 exists to prevent.
110
+ *
111
+ * An *empty* `items` doesn't count as "sent": `items`' own doc says 0 entries
112
+ * behaves like neither was sent at all, so `{item, items: []}` has no
113
+ * ambiguity to reject -- there is nothing in `items` to silently discard.
114
+ * Rejecting it would contradict the field one line below it.
115
+ *
116
+ * Validity polarity (true = satisfies the rule) so `z.refine` reads the way
117
+ * `form-schemas`' own cross-field checks do.
118
+ */
119
+ function onlyOneOfItemAndItems(options) {
120
+ const carriesItems = Array.isArray(options.items) && options.items.length > 0;
121
+ return options.item == null || !carriesItems;
122
+ }
123
+ const ITEM_AND_ITEMS_EXCLUSIVE_MESSAGE = "item and items are mutually exclusive -- send one or the other, not both";
124
+ /**
125
+ * Everything a Host App passes to `TopolEditor.init()` -- the whole boot
126
+ * contract for embedding the Form Editor, and the reason this package
127
+ * exists (ADR 0029: it used to live only in `apps/form-editor/src/types/`,
128
+ * so every host hand-mirrored it and drifted).
129
+ *
130
+ * Validated at runtime by `init()` itself, with a hard stop on failure
131
+ * (ADR 0029 decision 3) -- a Host with a malformed payload has no safe
132
+ * partial state to mount into.
133
+ *
134
+ * Deliberately *not* here: `apps/form-editor`'s own transport internals
135
+ * (`ChildInitOptions`'s `hasOn*` flags, `CallbackName`, `EditorCallbacks`),
136
+ * which are an implementation detail of the postMessage bridge rather than
137
+ * part of the contract a Host authors against.
138
+ */
139
+ /**
140
+ * `el` is `z.nullish`, not `z.optional`: a Vue host mounting the editor
141
+ * holds its container as a template ref typed `HTMLElement | null`, so
142
+ * `el: containerRef.value` legitimately carries `null` before that ref
143
+ * resolves. `null` here means "no container given" and falls back to
144
+ * `document.body`, exactly as an absent key does.
145
+ *
146
+ * Every *other* field stays strict. Widening them all would push
147
+ * `T | null | undefined` onto every Host writing TypeScript against this
148
+ * contract -- a real ergonomic cost -- to tolerate a `null` that, outside
149
+ * `el`, has no evidenced Host pattern behind it and is more likely a
150
+ * mistake worth surfacing (ADR 0029 decision 3).
151
+ */
152
+ export const TopolFormEditorOptionsSchema = z
153
+ .object({
154
+ /** Container element (or CSS selector) to mount the editor iframe into. Defaults to document.body. */
155
+ el: z.nullish(z.union([
156
+ z.string(),
157
+ // Duck-typed on nodeType rather than `instanceof HTMLElement`: an
158
+ // element handed over from another window/iframe realm is not an
159
+ // `instanceof` of *this* realm's HTMLElement, and rejecting one here
160
+ // would refuse to mount for a Host that previously worked.
161
+ z.custom((value) => value?.nodeType === 1),
162
+ ])),
163
+ /** Editor authoring-UI locale. Defaults to "en". Read once at bootstrap -- there is no mechanism to change it after the editor mounts. */
164
+ language: z.optional(EditorLanguageSchema),
165
+ /** Per-key overrides for editor UI strings, keyed by dot-path (e.g. "controls.autosaves.title"). Applied once at bootstrap, merged into whichever locale ends up active. */
166
+ textOverride: z.optional(z.record(z.string(), z.string())),
167
+ /** Accepts the legacy flat shape for backward compatibility; a Host App is encouraged to move to `ThemeGroup[]` to supply locked/Brand Kit themes. */
168
+ themes: z.optional(z.union([z.array(FormThemeSchema), z.array(ThemeGroupSchema)])),
169
+ previewUrl: z.optional(z.string()),
170
+ /**
171
+ * A single `CampaignItem` this session reads/edits (ADR 0028) -- no array
172
+ * wrapper for something that isn't structurally a list. Mutually
173
+ * exclusive with `items` below; a host sends exactly one of the two.
174
+ */
175
+ item: z.optional(CampaignItemSchema),
176
+ /**
177
+ * Multiple `CampaignItem`s this session reads/edits (ADR 0028, reopening
178
+ * ticket 102/110) -- mutually exclusive with `item` above. 0 entries
179
+ * behaves like neither was sent at all.
180
+ */
181
+ items: z.optional(z.array(CampaignItemSchema)),
182
+ /** Which variant (inside whichever `item`/`items` entry contains it) the tab row should open on. For an entry with no `variants` (a bare `form`), matches that entry's own `id`. Defaults to the first entry of the first item. */
183
+ activeVariantId: z.optional(z.string()),
184
+ /** The account's registered website(s). 0: unchanged demo preview. 1: previewed directly, no picker. 2+: a picker in the Preview panel lets the user switch between them. */
185
+ websites: z.optional(z.array(z.string())),
186
+ inputFields: z.optional(z.union([
187
+ z.array(InputFieldSchema),
188
+ z.object({ extends: z.array(InputFieldSchema) }),
189
+ ])),
190
+ preferences: z.optional(z.array(PreferenceSchema)),
191
+ customPreferences: z.optional(z.boolean()),
192
+ /** Which buttons the WindowBar strip above TopBar shows. Omit, or leave empty, to keep the WindowBar hidden entirely (mirrors LPE's `windowBar` option). */
193
+ windowBar: z.optional(z.array(z.enum(["close", "fullscreen"]))),
194
+ /** Form/template name shown on the WindowBar's left side (mirrors LPE's `title`). Showing this alone (with no `windowBar` buttons) is enough to reveal the WindowBar strip. */
195
+ name: z.optional(z.string()),
196
+ callbacks: z.optional(EditorCallbacksSchema),
197
+ fileManager: z.optional(FileManagerOptionsSchema),
198
+ authorize: z.optional(EditorAuthorizeSchema),
199
+ /**
200
+ * Display identity for the current user -- shown as the author on
201
+ * comments/autosaves. Separate from `authorize`: `authorize.userId` is an
202
+ * account/license id sent to the real `/authorize` handshake, while
203
+ * `user.userId` identifies the specific person for comment/autosave
204
+ * attribution -- the two are independent and may differ (matching LPE's
205
+ * `authorize`/`currentUser` split).
206
+ */
207
+ user: z.optional(EditorUserSchema),
208
+ /** The account's team roster, used to resolve any OTHER author's name/avatar (not just the current user's own) on comments/autosaves. No need to exclude the current user -- self is always resolved from `user` first regardless. */
209
+ teamUsers: z.optional(z.array(EditorUserSchema)),
210
+ aiUrl: z.optional(z.string()),
211
+ widgetUrl: z.optional(z.union([
212
+ z.string(),
213
+ z.object({ overwrite: z.literal(true), script: z.string() }),
214
+ ])),
215
+ comments: z.optional(CommentsConfigSchema),
216
+ autosaves: z.optional(AutosaveConfigSchema),
217
+ })
218
+ .check(z.refine(onlyOneOfItemAndItems, ITEM_AND_ITEMS_EXCLUSIVE_MESSAGE));
@@ -0,0 +1,32 @@
1
+ import * as z from "zod/mini";
2
+ import { FormThemeSchema } from "@u-krupaveho-kraba/form-schemas";
3
+ /**
4
+ * One theme in the Host-owned theme library (ticket 151, ADR 0022). `id` is
5
+ * absent for a preset the Host never persisted; `locked` marks a Brand Kit
6
+ * theme the editor may select but not edit in place.
7
+ */
8
+ export const ThemeEntrySchema = z.object({
9
+ id: z.optional(z.string()),
10
+ name: z.string(),
11
+ locked: z.boolean(),
12
+ value: FormThemeSchema,
13
+ });
14
+ /**
15
+ * A label-only grouping of themes ("Presets", "Your saved themes") -- the
16
+ * Host owns both the labels and the placement; the Theme Panel never
17
+ * computes either (ADR 0022).
18
+ */
19
+ export const ThemeGroupSchema = z.object({
20
+ label: z.string(),
21
+ themes: z.array(ThemeEntrySchema),
22
+ });
23
+ /**
24
+ * What the panel sends to `onThemeSave` (ADR 0022). `id` present means
25
+ * "update this owned theme in place"; absent means "fork a new one" -- the
26
+ * Host assigns the new id, the panel never generates one itself.
27
+ */
28
+ export const ThemeSavePayloadSchema = z.object({
29
+ id: z.optional(z.string()),
30
+ name: z.string(),
31
+ value: FormThemeSchema,
32
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./schemas/campaignItem.js";
2
+ export * from "./schemas/theme.js";
3
+ export * from "./schemas/fileManager.js";
4
+ export * from "./schemas/options.js";