@u-krupaveho-kraba/form-schemas 0.2.0 → 0.4.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 +26 -9
- package/index.js +9 -6
- package/package.json +1 -1
- package/schema/{campaign.schema.json → campaignItem.schema.json} +3850 -1513
- package/schemas/campaign.js +87 -39
- package/schemas/element.js +16 -2
- package/schemas/elementProperties.js +130 -9
- package/schemas/form.js +31 -6
- package/schemas/formDisplay.js +1 -0
- package/schemas/step.js +4 -0
- package/types/index.d.ts +7 -4
- package/types/schemas/campaign.d.ts +28001 -13479
- package/types/schemas/element.d.ts +969 -415
- package/types/schemas/elementProperties.d.ts +196 -41
- package/types/schemas/form.d.ts +3720 -1597
- package/types/schemas/formDisplay.d.ts +62 -56
- package/types/schemas/step.d.ts +409 -145
package/schemas/campaign.js
CHANGED
|
@@ -3,7 +3,10 @@ import { FormSchema } from "./form.js";
|
|
|
3
3
|
export const FormReferenceSchema = z.object({
|
|
4
4
|
ref: z.string(),
|
|
5
5
|
});
|
|
6
|
-
export const CampaignFormDefinitionSchema = z.union([
|
|
6
|
+
export const CampaignFormDefinitionSchema = z.union([
|
|
7
|
+
FormSchema,
|
|
8
|
+
FormReferenceSchema,
|
|
9
|
+
]);
|
|
7
10
|
export const VariantSchema = z.object({
|
|
8
11
|
id: z.string(),
|
|
9
12
|
name: z.string(),
|
|
@@ -19,44 +22,85 @@ export const CampaignScheduleSchema = z.object({
|
|
|
19
22
|
startsAt: z.optional(z.string()),
|
|
20
23
|
endsAt: z.optional(z.string()),
|
|
21
24
|
});
|
|
22
|
-
// Used
|
|
23
|
-
//
|
|
24
|
-
// ticket 117) because DisplayRuleState's display-count tracking was
|
|
25
|
-
// per-campaign only. Every CampaignItem now owns its own display-budget
|
|
26
|
-
// key (form-shared's displayKey), so that restriction no longer protects
|
|
27
|
-
// anything -- one shape serves both levels.
|
|
25
|
+
// Used on CampaignItem (the only level left post-ADR-0025's Campaign
|
|
26
|
+
// removal).
|
|
28
27
|
export const CampaignTargetingSchema = z.object({
|
|
29
28
|
urlPatterns: z.optional(z.array(z.string())),
|
|
30
29
|
schedule: z.optional(CampaignScheduleSchema),
|
|
31
30
|
frequency: z.optional(FrequencyTargetingSchema),
|
|
32
31
|
});
|
|
33
|
-
export const ImmediateTriggerSchema = z.object({
|
|
34
|
-
|
|
32
|
+
export const ImmediateTriggerSchema = z.object({
|
|
33
|
+
type: z.literal("immediate"),
|
|
34
|
+
});
|
|
35
|
+
export const DelayTriggerSchema = z.object({
|
|
36
|
+
type: z.literal("delay"),
|
|
37
|
+
seconds: z.number(),
|
|
38
|
+
});
|
|
35
39
|
export const ScrollDepthTriggerSchema = z.object({
|
|
36
40
|
type: z.literal("scroll_depth"),
|
|
37
41
|
percent: z.number(),
|
|
38
42
|
});
|
|
39
|
-
export const ExitIntentTriggerSchema = z.object({
|
|
43
|
+
export const ExitIntentTriggerSchema = z.object({
|
|
44
|
+
type: z.literal("exit_intent"),
|
|
45
|
+
});
|
|
40
46
|
export const TriggerConditionSchema = z.discriminatedUnion("type", [
|
|
41
47
|
ImmediateTriggerSchema,
|
|
42
48
|
DelayTriggerSchema,
|
|
43
49
|
ScrollDepthTriggerSchema,
|
|
44
50
|
ExitIntentTriggerSchema,
|
|
45
51
|
]);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
52
|
+
// The unit of independent client-side evaluation -- since ADR 0025 (Campaign
|
|
53
|
+
// wrapper deleted), the only level left in the schema. Default: concurrent
|
|
54
|
+
// with every other item published for the same Website. `exclusiveWith`,
|
|
49
55
|
// when shared by two or more items, makes them compete instead (first
|
|
50
|
-
// eligible one, by array order, wins) -- see
|
|
51
|
-
//
|
|
52
|
-
|
|
56
|
+
// eligible one, by array order, wins) -- see CampaignItemsFileSchema's
|
|
57
|
+
// cross-item checks below for the invariants this and `id` need enforced.
|
|
58
|
+
//
|
|
59
|
+
// `trigger` is optional (ADR 0028, reversing this schema's own original
|
|
60
|
+
// choice) -- a CampaignItem with no rule configured at all is now an
|
|
61
|
+
// ordinary item, not a distinct mode; the widget defaults a missing
|
|
62
|
+
// trigger to `{type: "immediate"}` at evaluation time (see
|
|
63
|
+
// apps/form-widget/src/lib/runWidget.ts) rather than this schema forcing
|
|
64
|
+
// every producer to write that default explicitly.
|
|
65
|
+
//
|
|
66
|
+
// The single source of truth for CampaignItem's shared envelope fields --
|
|
67
|
+
// `packages/form-shared`'s `CampaignItemBase` and `apps/form-editor`'s own
|
|
68
|
+
// copy both derive from this schema (`z.infer`/`Pick`) rather than
|
|
69
|
+
// hand-declaring the same field set a third and fourth time (architecture
|
|
70
|
+
// review, 2026-09-10).
|
|
71
|
+
export const CampaignItemBaseSchema = z.object({
|
|
72
|
+
// Stable, author-assigned -- also the display-budget key (see
|
|
73
|
+
// types/displayRuleState.ts), the DOM mount attribute, and the Submit
|
|
74
|
+
// Payload/Funnel Event `itemId` (ADR 0025). Never an array index:
|
|
75
|
+
// reordering the item list must not change what an item's stored display
|
|
76
|
+
// history refers to. Required unique across every item published for one
|
|
77
|
+
// Website -- there is no outer wrapper left to namespace two items
|
|
78
|
+
// sharing an id.
|
|
53
79
|
id: z.string(),
|
|
80
|
+
// Human label for the authoring UI ("Footer bar", "Black Friday banner")
|
|
81
|
+
// -- never shown to an End Customer.
|
|
54
82
|
name: z.optional(z.string()),
|
|
55
83
|
targeting: z.optional(CampaignTargetingSchema),
|
|
56
|
-
trigger: TriggerConditionSchema,
|
|
84
|
+
trigger: z.optional(TriggerConditionSchema),
|
|
57
85
|
exclusiveWith: z.optional(z.string()),
|
|
58
|
-
|
|
86
|
+
// Freeform, integrator-set reporting/attribution tag (ADR 0025) -- lets
|
|
87
|
+
// several independent items share one identity in the integrator's own
|
|
88
|
+
// reporting (e.g. Ecomail populates it from its own form_campaigns.id).
|
|
89
|
+
// Inert at evaluation time: no role in uniqueness, exclusiveWith
|
|
90
|
+
// clustering, or eligibility. This is the direct replacement for what the
|
|
91
|
+
// deleted Campaign wrapper's `id`/`name` used to provide structurally.
|
|
92
|
+
campaignId: z.optional(z.string()),
|
|
59
93
|
});
|
|
94
|
+
// ADR 0028: a CampaignItem carries its own content directly -- either a
|
|
95
|
+
// single `definition` (the common, non-AB case) or `variants` (opt-in AB
|
|
96
|
+
// test), never both, never neither. Kept as a plain `z.union` rather than a
|
|
97
|
+
// `z.discriminatedUnion` -- there is no literal tag field to discriminate
|
|
98
|
+
// on, same precedent as CampaignFormDefinitionSchema one level down (a
|
|
99
|
+
// Form-shaped object vs. a `{ref}`-shaped one, disambiguated structurally).
|
|
100
|
+
export const CampaignItemSchema = z.union([
|
|
101
|
+
z.object({ ...CampaignItemBaseSchema.shape, variants: z.array(VariantSchema) }),
|
|
102
|
+
z.object({ ...CampaignItemBaseSchema.shape, definition: CampaignFormDefinitionSchema }),
|
|
103
|
+
]);
|
|
60
104
|
// A lone item carrying an exclusiveWith value no sibling shares behaves
|
|
61
105
|
// identically to one with no value at all (a cluster of one just always
|
|
62
106
|
// wins), so it's never a runtime bug -- but it's always either dead
|
|
@@ -64,9 +108,11 @@ export const CampaignItemSchema = z.object({
|
|
|
64
108
|
// typo in one of the two values. Reject it at publish time rather than
|
|
65
109
|
// let it ship silently as a no-op field. `.check()` runs after the object
|
|
66
110
|
// itself validates, so `items` here is already `CampaignItem[]`-shaped.
|
|
67
|
-
|
|
111
|
+
// Post-ADR-0025: clusters across the *entire* flat item list for a Website
|
|
112
|
+
// in one pass, not scoped to any wrapper.
|
|
113
|
+
function eachExclusiveWithValueIsShared(file) {
|
|
68
114
|
const counts = new Map();
|
|
69
|
-
for (const item of
|
|
115
|
+
for (const item of file.items) {
|
|
70
116
|
if (item.exclusiveWith === undefined)
|
|
71
117
|
continue;
|
|
72
118
|
counts.set(item.exclusiveWith, (counts.get(item.exclusiveWith) ?? 0) + 1);
|
|
@@ -74,31 +120,33 @@ function eachExclusiveWithValueIsShared(campaign) {
|
|
|
74
120
|
return [...counts.values()].every((count) => count >= 2);
|
|
75
121
|
}
|
|
76
122
|
const EXCLUSIVE_WITH_SHARED_MESSAGE = "exclusiveWith value is used by only one item -- either remove it or add a second item that shares it";
|
|
77
|
-
|
|
123
|
+
// ADR 0025: CampaignItem.id now does every job the deleted Campaign.id used
|
|
124
|
+
// to do (DOM mount attribute, displayKey namespacing, analytics identity),
|
|
125
|
+
// so it must be unique across the whole flat item list for a Website --
|
|
126
|
+
// there is no outer wrapper left to namespace two items sharing an id.
|
|
127
|
+
function everyItemIdIsUnique(file) {
|
|
128
|
+
const ids = file.items.map((item) => item.id);
|
|
129
|
+
return new Set(ids).size === ids.length;
|
|
130
|
+
}
|
|
131
|
+
const ITEM_ID_UNIQUE_MESSAGE = "item id is used by more than one item -- CampaignItem.id must be unique within a Website's item list";
|
|
132
|
+
export const CampaignItemsFileSchema = z
|
|
78
133
|
.object({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
targeting: z.optional(CampaignTargetingSchema),
|
|
134
|
+
formatVersion: z.literal(1),
|
|
135
|
+
account: z.string(),
|
|
136
|
+
website: z.string(),
|
|
83
137
|
items: z.array(CampaignItemSchema),
|
|
84
138
|
})
|
|
85
|
-
.check(z.refine(eachExclusiveWithValueIsShared, EXCLUSIVE_WITH_SHARED_MESSAGE));
|
|
139
|
+
.check(z.refine(eachExclusiveWithValueIsShared, EXCLUSIVE_WITH_SHARED_MESSAGE), z.refine(everyItemIdIsUnique, ITEM_ID_UNIQUE_MESSAGE));
|
|
86
140
|
// Ticket 113: field names match the real, already-deployed Ecomail publish
|
|
87
141
|
// job's actual index-entry output, not FormReferenceSchema's {ref} shape.
|
|
88
|
-
|
|
142
|
+
// Renamed from CampaignEntry(Ref)Schema by ADR 0025 -- one entry now
|
|
143
|
+
// references/inlines a single CampaignItem, not a whole (deleted) Campaign.
|
|
144
|
+
export const CampaignItemEntryRefSchema = z.object({
|
|
89
145
|
id: z.string(),
|
|
90
146
|
url: z.string(),
|
|
91
147
|
rules: z.optional(z.unknown()),
|
|
92
148
|
});
|
|
93
|
-
export const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// CampaignSlot shape never shipped to a real customer (127/128/129 landed
|
|
98
|
-
// 2026-08-12, zero adoption yet), so this revision replaces it in place.
|
|
99
|
-
export const CampaignsFileSchema = z.object({
|
|
100
|
-
formatVersion: z.literal(1),
|
|
101
|
-
account: z.string(),
|
|
102
|
-
website: z.string(),
|
|
103
|
-
campaigns: z.array(CampaignSchema),
|
|
104
|
-
});
|
|
149
|
+
export const CampaignItemEntrySchema = z.union([
|
|
150
|
+
CampaignItemEntryRefSchema,
|
|
151
|
+
CampaignItemSchema,
|
|
152
|
+
]);
|
package/schemas/element.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import * as z from "zod/mini";
|
|
2
|
-
import { BaseElementPropertiesSchema, ButtonElementPropertiesSchema, CheckboxElementPropertiesSchema, HtmlElementPropertiesSchema, ImageElementPropertiesSchema, RadioElementPropertiesSchema, SelectElementPropertiesSchema, SpacerElementPropertiesSchema, TextElementPropertiesSchema, TextInputElementPropertiesSchema, } from "./elementProperties.js";
|
|
2
|
+
import { BaseElementPropertiesSchema, ButtonElementPropertiesSchema, ButtonWidthSchema, CheckboxElementPropertiesSchema, CountdownElementPropertiesSchema, FloatingImageElementPropertiesSchema, GifElementPropertiesSchema, HtmlElementPropertiesSchema, ImageElementPropertiesSchema, RadioElementPropertiesSchema, SelectElementPropertiesSchema, SpacerElementPropertiesSchema, TextElementPropertiesSchema, TextInputElementPropertiesSchema, } from "./elementProperties.js";
|
|
3
3
|
export const ElementTypeSchema = z.enum([
|
|
4
4
|
"text_input",
|
|
5
5
|
"text",
|
|
6
6
|
"block",
|
|
7
7
|
"button",
|
|
8
8
|
"image",
|
|
9
|
+
"floating_image",
|
|
10
|
+
"gif",
|
|
9
11
|
"column",
|
|
10
12
|
"checkbox",
|
|
11
13
|
"radio",
|
|
12
14
|
"select",
|
|
13
15
|
"html",
|
|
14
16
|
"spacer",
|
|
17
|
+
"countdown",
|
|
15
18
|
]);
|
|
16
19
|
// mirrors the Element<T, P> generic helper in form-shared/types/element.ts.
|
|
17
20
|
// Takes a raw shape (not a ZodMiniObject) — matching zod's own `extend()`
|
|
@@ -32,11 +35,14 @@ export const ButtonElementSchema = elementSchema("button", ButtonElementProperti
|
|
|
32
35
|
export const TextElementSchema = elementSchema("text", TextElementPropertiesSchema.shape);
|
|
33
36
|
export const TextInputElementSchema = elementSchema("text_input", TextInputElementPropertiesSchema.shape);
|
|
34
37
|
export const ImageElementSchema = elementSchema("image", ImageElementPropertiesSchema.shape);
|
|
38
|
+
export const FloatingImageElementSchema = elementSchema("floating_image", FloatingImageElementPropertiesSchema.shape);
|
|
39
|
+
export const GifElementSchema = elementSchema("gif", GifElementPropertiesSchema.shape);
|
|
35
40
|
export const CheckboxElementSchema = elementSchema("checkbox", CheckboxElementPropertiesSchema.shape);
|
|
36
41
|
export const RadioElementSchema = elementSchema("radio", RadioElementPropertiesSchema.shape);
|
|
37
42
|
export const SelectElementSchema = elementSchema("select", SelectElementPropertiesSchema.shape);
|
|
38
43
|
export const HtmlElementSchema = elementSchema("html", HtmlElementPropertiesSchema.shape);
|
|
39
44
|
export const SpacerElementSchema = elementSchema("spacer", SpacerElementPropertiesSchema.shape);
|
|
45
|
+
export const CountdownElementSchema = elementSchema("countdown", CountdownElementPropertiesSchema.shape);
|
|
40
46
|
export const BlockColumnElementSchema = z.object({
|
|
41
47
|
id: z.string(),
|
|
42
48
|
type: z.literal("column"),
|
|
@@ -50,6 +56,7 @@ export const BlockColumnElementSchema = z.object({
|
|
|
50
56
|
});
|
|
51
57
|
const blockLayoutBase = {
|
|
52
58
|
...BaseElementPropertiesSchema.shape,
|
|
59
|
+
width: z.optional(ButtonWidthSchema),
|
|
53
60
|
};
|
|
54
61
|
export const BlockElementPropertiesSchema = z.discriminatedUnion("block_type", [
|
|
55
62
|
z.object({
|
|
@@ -65,7 +72,11 @@ export const BlockElementPropertiesSchema = z.discriminatedUnion("block_type", [
|
|
|
65
72
|
z.object({
|
|
66
73
|
...blockLayoutBase,
|
|
67
74
|
block_type: z.literal("triple_column"),
|
|
68
|
-
columns: z.tuple([
|
|
75
|
+
columns: z.tuple([
|
|
76
|
+
BlockColumnElementSchema,
|
|
77
|
+
BlockColumnElementSchema,
|
|
78
|
+
BlockColumnElementSchema,
|
|
79
|
+
]),
|
|
69
80
|
}),
|
|
70
81
|
]);
|
|
71
82
|
export const BlockElementSchema = z.object({
|
|
@@ -78,6 +89,8 @@ export const FormElementSchema = z.discriminatedUnion("type", [
|
|
|
78
89
|
TextElementSchema,
|
|
79
90
|
TextInputElementSchema,
|
|
80
91
|
ImageElementSchema,
|
|
92
|
+
FloatingImageElementSchema,
|
|
93
|
+
GifElementSchema,
|
|
81
94
|
CheckboxElementSchema,
|
|
82
95
|
RadioElementSchema,
|
|
83
96
|
SelectElementSchema,
|
|
@@ -85,4 +98,5 @@ export const FormElementSchema = z.discriminatedUnion("type", [
|
|
|
85
98
|
BlockColumnElementSchema,
|
|
86
99
|
HtmlElementSchema,
|
|
87
100
|
SpacerElementSchema,
|
|
101
|
+
CountdownElementSchema,
|
|
88
102
|
]);
|
|
@@ -16,9 +16,19 @@ export const LinkTargetSchema = z.object({
|
|
|
16
16
|
url: z.string(),
|
|
17
17
|
openInNewTab: z.optional(z.boolean()),
|
|
18
18
|
});
|
|
19
|
+
// ADR 0026: where a successful Submit lands. Omitted/undefined means "use
|
|
20
|
+
// the effective default" -- the Thank You Page when the Form has one,
|
|
21
|
+
// otherwise no navigation (today's behavior).
|
|
22
|
+
export const PostSubmitDestinationSchema = z.enum([
|
|
23
|
+
"next_step",
|
|
24
|
+
"thank_you_page",
|
|
25
|
+
]);
|
|
19
26
|
export const ButtonActionSchema = z.discriminatedUnion("type", [
|
|
20
27
|
z.object({ type: z.literal("next_step") }),
|
|
21
|
-
z.object({
|
|
28
|
+
z.object({
|
|
29
|
+
type: z.literal("submit"),
|
|
30
|
+
onSuccess: z.optional(PostSubmitDestinationSchema),
|
|
31
|
+
}),
|
|
22
32
|
z.object({ type: z.literal("close") }),
|
|
23
33
|
z.object({ type: z.literal("jump_to"), targetStepId: z.string() }),
|
|
24
34
|
z.object({ type: z.literal("redirect"), ...LinkTargetSchema.shape }),
|
|
@@ -38,6 +48,13 @@ export const ButtonElementPropertiesSchema = z.object({
|
|
|
38
48
|
fontSize: z.optional(z.number()),
|
|
39
49
|
fontWeight: z.optional(z.enum(["normal", "bold"])),
|
|
40
50
|
fontStyle: z.optional(z.enum(["normal", "italic"])),
|
|
51
|
+
// Alignment of the text inside the button's own box -- distinct from
|
|
52
|
+
// `align` below, which positions the button itself within its wrapper.
|
|
53
|
+
textAlign: z.optional(ButtonAlignSchema),
|
|
54
|
+
// A CSS gradient function string (e.g. "linear-gradient(90deg, ...)"),
|
|
55
|
+
// layered on top of the base `backgroundColor` (BaseContainerProperties)
|
|
56
|
+
// rather than replacing it -- same background-image/-color split LPE uses.
|
|
57
|
+
backgroundImage: z.optional(z.string()),
|
|
41
58
|
hover: z.optional(z.object({
|
|
42
59
|
backgroundColor: z.optional(z.string()),
|
|
43
60
|
fontColor: z.optional(z.string()),
|
|
@@ -47,7 +64,14 @@ export const ButtonElementPropertiesSchema = z.object({
|
|
|
47
64
|
height: z.optional(z.number()),
|
|
48
65
|
align: z.optional(ButtonAlignSchema),
|
|
49
66
|
});
|
|
50
|
-
export const InputTypeSchema = z.enum([
|
|
67
|
+
export const InputTypeSchema = z.enum([
|
|
68
|
+
"text",
|
|
69
|
+
"email",
|
|
70
|
+
"telephone",
|
|
71
|
+
"number",
|
|
72
|
+
"date",
|
|
73
|
+
"url",
|
|
74
|
+
]);
|
|
51
75
|
export const InputFieldSchema = z.object({
|
|
52
76
|
type: InputTypeSchema,
|
|
53
77
|
name: z.string(),
|
|
@@ -62,7 +86,12 @@ export const PreferenceOptionSchema = z.object({
|
|
|
62
86
|
value: z.string(),
|
|
63
87
|
label: z.string(),
|
|
64
88
|
});
|
|
65
|
-
export const PreferenceTypeSchema = z.enum([
|
|
89
|
+
export const PreferenceTypeSchema = z.enum([
|
|
90
|
+
"checkbox",
|
|
91
|
+
"hidden",
|
|
92
|
+
"radio",
|
|
93
|
+
"select",
|
|
94
|
+
]);
|
|
66
95
|
export const PreferenceSchema = z.object({
|
|
67
96
|
id: z.union([z.string(), z.number()]),
|
|
68
97
|
name: z.string(),
|
|
@@ -80,7 +109,9 @@ export const PreferenceSchema = z.object({
|
|
|
80
109
|
// -- which already wants a "true = valid" predicate -- can use it directly;
|
|
81
110
|
// the one caller that wants the opposite (editorStore.ts's save-gate)
|
|
82
111
|
// negates it at that single call site instead.
|
|
83
|
-
export const satisfiesCustomKeyRequirement = (value) => !(value.preference === null &&
|
|
112
|
+
export const satisfiesCustomKeyRequirement = (value) => !(value.preference === null &&
|
|
113
|
+
value.custom_options !== undefined &&
|
|
114
|
+
!value.customKey);
|
|
84
115
|
const CUSTOM_KEY_REQUIRED_MESSAGE = "customKey is required when custom_options is used without a linked preference";
|
|
85
116
|
export const PreferenceElementPropertiesSchema = z
|
|
86
117
|
.object({
|
|
@@ -91,14 +122,44 @@ export const PreferenceElementPropertiesSchema = z
|
|
|
91
122
|
required: z.boolean(),
|
|
92
123
|
})
|
|
93
124
|
.check(z.refine(satisfiesCustomKeyRequirement, CUSTOM_KEY_REQUIRED_MESSAGE));
|
|
125
|
+
// Shared corner-radius preset for Checkbox's `shape` and Select's
|
|
126
|
+
// `controlRadius` -- same value set and same icon-button-group UI, applied
|
|
127
|
+
// to two different controls (ADR 0023: select's control styling stays
|
|
128
|
+
// independent, but the *shape vocabulary* is deliberately shared).
|
|
129
|
+
export const BoxShapeSchema = z.enum(["square", "rounded", "pill"]);
|
|
130
|
+
// Stacked (default, today's only behavior) vs. inline option layout --
|
|
131
|
+
// shared by Checkbox and Radio, meaningless for Select (a single dropdown).
|
|
132
|
+
export const PreferenceFieldLayoutSchema = z.enum(["vertical", "horizontal"]);
|
|
94
133
|
export const SelectElementPropertiesSchema = z
|
|
95
134
|
.object({
|
|
96
135
|
...PreferenceElementPropertiesSchema.shape,
|
|
97
136
|
placeholder: z.string(),
|
|
137
|
+
labelColor: z.optional(z.string()),
|
|
138
|
+
controlTextColor: z.optional(z.string()),
|
|
139
|
+
controlBackgroundColor: z.optional(z.string()),
|
|
140
|
+
controlBorderColor: z.optional(z.string()),
|
|
141
|
+
controlRadius: z.optional(BoxShapeSchema),
|
|
142
|
+
})
|
|
143
|
+
.check(z.refine(satisfiesCustomKeyRequirement, CUSTOM_KEY_REQUIRED_MESSAGE));
|
|
144
|
+
export const CheckboxElementPropertiesSchema = z
|
|
145
|
+
.object({
|
|
146
|
+
...PreferenceElementPropertiesSchema.shape,
|
|
147
|
+
accentColor: z.optional(z.string()),
|
|
148
|
+
labelColor: z.optional(z.string()),
|
|
149
|
+
shape: z.optional(BoxShapeSchema),
|
|
150
|
+
layout: z.optional(PreferenceFieldLayoutSchema),
|
|
151
|
+
})
|
|
152
|
+
.check(z.refine(satisfiesCustomKeyRequirement, CUSTOM_KEY_REQUIRED_MESSAGE));
|
|
153
|
+
export const RadioStyleSchema = z.enum(["outline", "filled"]);
|
|
154
|
+
export const RadioElementPropertiesSchema = z
|
|
155
|
+
.object({
|
|
156
|
+
...PreferenceElementPropertiesSchema.shape,
|
|
157
|
+
accentColor: z.optional(z.string()),
|
|
158
|
+
labelColor: z.optional(z.string()),
|
|
159
|
+
style: z.optional(RadioStyleSchema),
|
|
160
|
+
layout: z.optional(PreferenceFieldLayoutSchema),
|
|
98
161
|
})
|
|
99
162
|
.check(z.refine(satisfiesCustomKeyRequirement, CUSTOM_KEY_REQUIRED_MESSAGE));
|
|
100
|
-
export const CheckboxElementPropertiesSchema = PreferenceElementPropertiesSchema;
|
|
101
|
-
export const RadioElementPropertiesSchema = PreferenceElementPropertiesSchema;
|
|
102
163
|
export const TextElementPropertiesSchema = z.object({
|
|
103
164
|
text: z.string(),
|
|
104
165
|
color: z.string(),
|
|
@@ -114,22 +175,82 @@ export const TextElementPropertiesSchema = z.object({
|
|
|
114
175
|
export const HtmlElementPropertiesSchema = z.object({
|
|
115
176
|
html: z.string(),
|
|
116
177
|
});
|
|
178
|
+
// Mirrors ButtonWidthSchema's full/custom split, minus "auto" -- see
|
|
179
|
+
// ImageWidth's doc comment in form-shared/types/elementProperties.ts.
|
|
180
|
+
export const ImageWidthSchema = z.union([
|
|
181
|
+
z.literal("full"),
|
|
182
|
+
z.object({ unit: z.enum(["px", "%"]), value: z.number() }),
|
|
183
|
+
]);
|
|
117
184
|
export const ImageElementPropertiesSchema = z.object({
|
|
118
185
|
src: z.string(),
|
|
186
|
+
alt: z.optional(z.string()),
|
|
119
187
|
link: z.optional(LinkTargetSchema),
|
|
120
|
-
width: z.optional(
|
|
188
|
+
width: z.optional(ImageWidthSchema),
|
|
121
189
|
align: z.optional(z.enum(["left", "center", "right"])),
|
|
122
190
|
objectFit: z.optional(z.enum(["cover", "contain", "fill"])),
|
|
123
191
|
objectPosition: z.optional(z.string()),
|
|
124
192
|
});
|
|
125
|
-
//
|
|
126
|
-
//
|
|
193
|
+
// Mirrors FloatingImageElementProperties in
|
|
194
|
+
// form-shared/types/elementProperties.ts -- spelled out explicitly (not
|
|
195
|
+
// derived from ImageElementPropertiesSchema) for the same reason the
|
|
196
|
+
// TypeScript type is: `width`/`align` don't carry over unchanged (`width`
|
|
197
|
+
// becomes a required number set via on-canvas resize, not the full/auto/
|
|
198
|
+
// custom ImageWidthSchema; `align` is dropped, meaningless once the element
|
|
199
|
+
// is taken out of flow), and `position` is a free {x,y} pixel offset set by
|
|
200
|
+
// dragging, not the popup Form's anchor+distance shape.
|
|
127
201
|
export const FloatingImageElementPropertiesSchema = z.object({
|
|
128
202
|
src: z.string(),
|
|
203
|
+
alt: z.optional(z.string()),
|
|
204
|
+
link: z.optional(LinkTargetSchema),
|
|
205
|
+
objectFit: z.optional(z.enum(["cover", "contain", "fill"])),
|
|
206
|
+
objectPosition: z.optional(z.string()),
|
|
207
|
+
position: z.object({
|
|
208
|
+
x: z.number(),
|
|
209
|
+
y: z.number(),
|
|
210
|
+
}),
|
|
211
|
+
width: z.number(),
|
|
212
|
+
height: z.number(),
|
|
129
213
|
});
|
|
214
|
+
// Same shape as ImageElementPropertiesSchema -- mirrors
|
|
215
|
+
// GifElementProperties in form-shared/types/elementProperties.ts.
|
|
216
|
+
export const GifElementPropertiesSchema = ImageElementPropertiesSchema;
|
|
130
217
|
export const SpacerElementPropertiesSchema = z.object({
|
|
131
218
|
height: z.number(),
|
|
132
219
|
showDivider: z.optional(z.boolean()),
|
|
133
220
|
dividerColor: z.optional(z.string()),
|
|
134
221
|
dividerThickness: z.optional(z.number()),
|
|
135
222
|
});
|
|
223
|
+
// "fixed_time" is an evergreen duration -- it restarts fresh from `duration`
|
|
224
|
+
// every time a visitor loads the page (per-visitor, e.g. a "hurry, offer
|
|
225
|
+
// ends in 10 minutes" nudge). "fixed_date" is one real calendar deadline,
|
|
226
|
+
// the same for every visitor. Mirrors OptiMonk's countdown mode split.
|
|
227
|
+
export const CountdownModeSchema = z.enum(["fixed_date", "fixed_time"]);
|
|
228
|
+
export const CountdownDurationSchema = z.object({
|
|
229
|
+
days: z.number(),
|
|
230
|
+
hours: z.number(),
|
|
231
|
+
minutes: z.number(),
|
|
232
|
+
seconds: z.number(),
|
|
233
|
+
});
|
|
234
|
+
export const CountdownLabelsSchema = z.object({
|
|
235
|
+
days: z.string(),
|
|
236
|
+
hours: z.string(),
|
|
237
|
+
minutes: z.string(),
|
|
238
|
+
seconds: z.string(),
|
|
239
|
+
});
|
|
240
|
+
export const CountdownElementPropertiesSchema = z.object({
|
|
241
|
+
mode: CountdownModeSchema,
|
|
242
|
+
// Local wall-clock datetime string ("YYYY-MM-DDTHH:mm"), used when
|
|
243
|
+
// mode === "fixed_date" -- matches a native <input type="datetime-local">
|
|
244
|
+
// value directly, and JS's Date constructor already parses a timezone-less
|
|
245
|
+
// date-time string as local time, so no timezone field is needed.
|
|
246
|
+
targetDate: z.string(),
|
|
247
|
+
// Used when mode === "fixed_time"; restarted from this on every mount.
|
|
248
|
+
duration: CountdownDurationSchema,
|
|
249
|
+
showLabels: z.boolean(),
|
|
250
|
+
labels: CountdownLabelsSchema,
|
|
251
|
+
fontFamily: z.optional(z.string()),
|
|
252
|
+
fontSize: z.optional(z.number()),
|
|
253
|
+
color: z.optional(z.string()),
|
|
254
|
+
labelColor: z.optional(z.string()),
|
|
255
|
+
boxBackgroundColor: z.optional(z.string()),
|
|
256
|
+
});
|
package/schemas/form.js
CHANGED
|
@@ -2,7 +2,12 @@ import * as z from "zod/mini";
|
|
|
2
2
|
import { HexSchema } from "./helpers.js";
|
|
3
3
|
import { StepSchema } from "./step.js";
|
|
4
4
|
import { BasicFormDisplaySchema, MobilePopupFormDisplaySchema, PopupFormDisplaySchema, SliderFormDisplaySchema, } from "./formDisplay.js";
|
|
5
|
-
export const FormTypeSchema = z.enum([
|
|
5
|
+
export const FormTypeSchema = z.enum([
|
|
6
|
+
"popup",
|
|
7
|
+
"basic",
|
|
8
|
+
"slider",
|
|
9
|
+
"mobile_popup",
|
|
10
|
+
]);
|
|
6
11
|
export const FormLanguageSchema = z.enum(["cs", "en"]);
|
|
7
12
|
export const FormThemeSchema = z.object({
|
|
8
13
|
corners: z.enum(["none", "sm", "md", "lg"]),
|
|
@@ -19,15 +24,35 @@ const formBase = {
|
|
|
19
24
|
usedFonts: z.array(z.string()),
|
|
20
25
|
};
|
|
21
26
|
export const FormSchema = z.discriminatedUnion("type", [
|
|
22
|
-
z.object({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
z.object({
|
|
28
|
+
...formBase,
|
|
29
|
+
type: z.literal("basic"),
|
|
30
|
+
display: BasicFormDisplaySchema,
|
|
31
|
+
}),
|
|
32
|
+
z.object({
|
|
33
|
+
...formBase,
|
|
34
|
+
type: z.literal("popup"),
|
|
35
|
+
display: PopupFormDisplaySchema,
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
...formBase,
|
|
39
|
+
type: z.literal("slider"),
|
|
40
|
+
display: SliderFormDisplaySchema,
|
|
41
|
+
}),
|
|
42
|
+
z.object({
|
|
43
|
+
...formBase,
|
|
44
|
+
type: z.literal("mobile_popup"),
|
|
45
|
+
display: MobilePopupFormDisplaySchema,
|
|
46
|
+
}),
|
|
26
47
|
]);
|
|
27
|
-
// Renamed from FormCampaignSchema (ADR 0016) -- mirrors MountedForm in
|
|
48
|
+
// Renamed from FormCampaignSchema (ADR 0016) -- mirrors MountedForm in
|
|
49
|
+
// form-shared. `campaignId` is optional (ADR 0025): sourced 1:1 from the
|
|
50
|
+
// winning CampaignItem's own optional reporting tag, present only when an
|
|
51
|
+
// integrator set one.
|
|
28
52
|
export const MountedFormSchema = z.object({
|
|
29
53
|
id: z.string(),
|
|
30
54
|
name: z.string(),
|
|
31
55
|
definition: FormSchema,
|
|
32
56
|
variantId: z.optional(z.string()),
|
|
57
|
+
campaignId: z.optional(z.string()),
|
|
33
58
|
});
|
package/schemas/formDisplay.js
CHANGED
|
@@ -7,6 +7,7 @@ export const FormBorderSchema = z.object({
|
|
|
7
7
|
style: BorderStyleSchema,
|
|
8
8
|
sides: z.optional(z.enum(["all", "top", "right", "bottom", "left"])),
|
|
9
9
|
useTheme: z.optional(z.boolean()),
|
|
10
|
+
radius: z.optional(z.number()),
|
|
10
11
|
});
|
|
11
12
|
export const FormShadowSchema = z.object({
|
|
12
13
|
offsetX: z.number(),
|
package/schemas/step.js
CHANGED
|
@@ -6,10 +6,14 @@ export const StepDisplayPropertiesSchema = z.object({
|
|
|
6
6
|
backgroundColor: z.optional(HexSchema),
|
|
7
7
|
minHeight: z.optional(z.number()),
|
|
8
8
|
});
|
|
9
|
+
// ADR 0026: marks the Step shown once a Submit succeeds. Omitted/undefined
|
|
10
|
+
// means an ordinary content step -- there is no explicit "content" literal.
|
|
11
|
+
export const StepKindSchema = z.literal("thankYou");
|
|
9
12
|
export const StepSchema = z.object({
|
|
10
13
|
type: z.literal("step"),
|
|
11
14
|
name: z.string(),
|
|
12
15
|
elements: z.array(FormElementSchema),
|
|
13
16
|
id: z.string(),
|
|
14
17
|
displayProperties: StepDisplayPropertiesSchema,
|
|
18
|
+
kind: z.optional(StepKindSchema),
|
|
15
19
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ export * from "./schemas/campaign.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function getFormJsonSchema(): Record<string, unknown>;
|
|
13
13
|
/**
|
|
14
|
-
* Serializes the
|
|
15
|
-
* exported to ecomailapp so its PHP publish job can validate
|
|
16
|
-
* real schema instead of drifting silently
|
|
14
|
+
* Serializes the CampaignItem contract as standard JSON Schema — the
|
|
15
|
+
* artifact exported to ecomailapp so its PHP publish job can validate each
|
|
16
|
+
* item it builds against the real schema instead of drifting silently
|
|
17
|
+
* (ticket 115/118, ADR 0017). Renamed from `getCampaignJsonSchema` by ADR
|
|
18
|
+
* 0025: there is no `Campaign` wrapper left to serialize, `CampaignItem` is
|
|
19
|
+
* now the atomic authored/published unit.
|
|
17
20
|
*/
|
|
18
|
-
export declare function
|
|
21
|
+
export declare function getCampaignItemJsonSchema(): Record<string, unknown>;
|