@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 +85 -0
- package/index.js +4 -0
- package/package.json +30 -0
- package/schemas/campaignItem.js +56 -0
- package/schemas/fileManager.js +69 -0
- package/schemas/options.js +218 -0
- package/schemas/theme.js +32 -0
- package/types/index.d.ts +4 -0
- package/types/schemas/campaignItem.d.ts +12325 -0
- package/types/schemas/fileManager.d.ts +92 -0
- package/types/schemas/options.d.ts +16654 -0
- package/types/schemas/theme.d.ts +72 -0
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
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));
|
package/schemas/theme.js
ADDED
|
@@ -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
|
+
});
|
package/types/index.d.ts
ADDED