@delmaredigital/payload-experiments 0.1.0-rc.7 → 0.2.0-rc.2

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 (207) hide show
  1. package/README.md +28 -0
  2. package/dist/admin/LayerUsage.d.ts +1 -1
  3. package/dist/admin/LayerUsage.js +23 -11
  4. package/dist/admin/MetricSourceSelect.js +2 -1
  5. package/dist/admin/TestsNavLink.d.ts +1 -0
  6. package/dist/admin/TestsNavLink.js +30 -0
  7. package/dist/admin/allocation.model.js +1 -1
  8. package/dist/admin/copy/actions.d.ts +0 -2
  9. package/dist/admin/copy/actions.js +0 -2
  10. package/dist/admin/copy/advanced.d.ts +55 -0
  11. package/dist/admin/copy/advanced.js +55 -0
  12. package/dist/admin/copy/armSelect.js +1 -1
  13. package/dist/admin/copy/arms.d.ts +0 -13
  14. package/dist/admin/copy/arms.js +0 -13
  15. package/dist/admin/copy/{who.d.ts → audience.d.ts} +1 -9
  16. package/dist/admin/copy/{who.js → audience.js} +1 -9
  17. package/dist/admin/copy/history.d.ts +0 -4
  18. package/dist/admin/copy/history.js +3 -7
  19. package/dist/admin/copy/metrics.d.ts +2 -2
  20. package/dist/admin/copy/metrics.js +2 -2
  21. package/dist/admin/copy/newTest.d.ts +32 -0
  22. package/dist/admin/copy/newTest.js +32 -0
  23. package/dist/admin/copy/readiness.d.ts +45 -0
  24. package/dist/admin/copy/readiness.js +50 -0
  25. package/dist/admin/copy/recommendation.d.ts +26 -0
  26. package/dist/admin/copy/recommendation.js +25 -0
  27. package/dist/admin/copy/results.d.ts +0 -1
  28. package/dist/admin/copy/results.js +0 -1
  29. package/dist/admin/copy/review.d.ts +0 -17
  30. package/dist/admin/copy/review.js +1 -112
  31. package/dist/admin/copy/setting.d.ts +10 -0
  32. package/dist/admin/copy/setting.js +10 -0
  33. package/dist/admin/copy/surface.d.ts +36 -0
  34. package/dist/admin/copy/surface.js +47 -0
  35. package/dist/admin/copy/testPage.d.ts +88 -0
  36. package/dist/admin/copy/testPage.js +88 -0
  37. package/dist/admin/paths.d.ts +5 -13
  38. package/dist/admin/paths.js +5 -13
  39. package/dist/admin/review/review.model.d.ts +3 -26
  40. package/dist/admin/review/review.model.js +3 -30
  41. package/dist/admin/surface/ResultsRoute.d.ts +7 -0
  42. package/dist/admin/surface/ResultsRoute.js +58 -0
  43. package/dist/admin/surface/SurfaceShell.d.ts +6 -0
  44. package/dist/admin/surface/SurfaceShell.js +35 -0
  45. package/dist/admin/surface/TestsHome.d.ts +4 -0
  46. package/dist/admin/surface/TestsHome.js +212 -0
  47. package/dist/admin/surface/TestsSurface.d.ts +2 -0
  48. package/dist/admin/surface/TestsSurface.js +36 -0
  49. package/dist/admin/surface/contracts.d.ts +48 -0
  50. package/dist/admin/surface/contracts.js +8 -0
  51. package/dist/admin/surface/home.model.d.ts +41 -0
  52. package/dist/admin/surface/home.model.js +59 -0
  53. package/dist/admin/surface/newTest/NewTest.d.ts +7 -0
  54. package/dist/admin/surface/newTest/NewTest.js +355 -0
  55. package/dist/admin/surface/newTest/newTest.css +9 -0
  56. package/dist/admin/surface/pageBlock/pageBlock.model.d.ts +6 -0
  57. package/dist/admin/surface/pageBlock/pageBlock.model.js +8 -0
  58. package/dist/admin/surface/pageBlock/parts.d.ts +2 -0
  59. package/dist/admin/surface/pageBlock/parts.js +64 -0
  60. package/dist/admin/surface/readiness/ActionDialogs.d.ts +15 -0
  61. package/dist/admin/surface/readiness/ActionDialogs.js +274 -0
  62. package/dist/admin/surface/readiness/Banner.d.ts +2 -0
  63. package/dist/admin/surface/readiness/Banner.js +138 -0
  64. package/dist/admin/surface/readiness/SaveStatus.d.ts +4 -0
  65. package/dist/admin/surface/readiness/SaveStatus.js +50 -0
  66. package/dist/admin/surface/readiness/parts.d.ts +2 -0
  67. package/dist/admin/surface/readiness/parts.js +9 -0
  68. package/dist/admin/surface/readiness/readiness.model.d.ts +22 -0
  69. package/dist/admin/surface/readiness/readiness.model.js +107 -0
  70. package/dist/admin/surface/readiness/useTransition.d.ts +10 -0
  71. package/dist/admin/surface/readiness/useTransition.js +34 -0
  72. package/dist/admin/surface/results/ResultsBody.d.ts +7 -0
  73. package/dist/admin/surface/results/ResultsBody.js +262 -0
  74. package/dist/admin/surface/results/recommendation.model.d.ts +12 -0
  75. package/dist/admin/surface/results/recommendation.model.js +59 -0
  76. package/dist/admin/surface/results/useResults.d.ts +56 -0
  77. package/dist/admin/surface/results/useResults.js +183 -0
  78. package/dist/admin/surface/setting/parts.d.ts +3 -0
  79. package/dist/admin/surface/setting/parts.js +263 -0
  80. package/dist/admin/surface/setting/setting.css +29 -0
  81. package/dist/admin/surface/setting/setting.model.d.ts +8 -0
  82. package/dist/admin/surface/setting/setting.model.js +25 -0
  83. package/dist/admin/surface/surface.css +50 -0
  84. package/dist/admin/surface/test/AdvancedSection.d.ts +2 -0
  85. package/dist/admin/surface/test/AdvancedSection.js +153 -0
  86. package/dist/admin/surface/test/GoalSection.d.ts +2 -0
  87. package/dist/admin/surface/test/GoalSection.js +198 -0
  88. package/dist/admin/surface/test/LengthSection.d.ts +2 -0
  89. package/dist/admin/surface/test/LengthSection.js +87 -0
  90. package/dist/admin/surface/test/Section.d.ts +7 -0
  91. package/dist/admin/surface/test/Section.js +27 -0
  92. package/dist/admin/surface/test/TargetingSection.d.ts +2 -0
  93. package/dist/admin/surface/test/TargetingSection.js +164 -0
  94. package/dist/admin/surface/test/TestHeader.d.ts +4 -0
  95. package/dist/admin/surface/test/TestHeader.js +69 -0
  96. package/dist/admin/surface/test/TestPage.d.ts +7 -0
  97. package/dist/admin/surface/test/TestPage.js +84 -0
  98. package/dist/admin/surface/test/VersionsSection.d.ts +4 -0
  99. package/dist/admin/surface/test/VersionsSection.js +208 -0
  100. package/dist/admin/surface/test/fields.d.ts +55 -0
  101. package/dist/admin/surface/test/fields.js +185 -0
  102. package/dist/admin/surface/test/pageParts.d.ts +2 -0
  103. package/dist/admin/surface/test/pageParts.js +39 -0
  104. package/dist/admin/surface/test/testPage.css +23 -0
  105. package/dist/admin/surface/test/testPage.model.d.ts +17 -0
  106. package/dist/admin/surface/test/testPage.model.js +53 -0
  107. package/dist/admin/surface/testDocument.model.d.ts +39 -0
  108. package/dist/admin/surface/testDocument.model.js +61 -0
  109. package/dist/admin/surface/useTestDocument.d.ts +22 -0
  110. package/dist/admin/surface/useTestDocument.js +345 -0
  111. package/dist/admin/targeting/targeting.model.js +2 -2
  112. package/dist/admin/useLayerUsage.d.ts +0 -5
  113. package/dist/admin/useLayerUsage.js +1 -39
  114. package/dist/admin/views/CollectionEditRedirect.d.ts +4 -0
  115. package/dist/admin/views/CollectionEditRedirect.js +14 -0
  116. package/dist/admin/views/CollectionListRedirect.d.ts +2 -0
  117. package/dist/admin/views/CollectionListRedirect.js +8 -0
  118. package/dist/admin/views/ExperimentsRootView.d.ts +2 -0
  119. package/dist/admin/views/ExperimentsRootView.js +175 -0
  120. package/dist/admin/views/access.model.d.ts +7 -0
  121. package/dist/admin/views/access.model.js +5 -0
  122. package/dist/collections/experiments/config.js +18 -125
  123. package/dist/collections/experiments/createDefaults.js +6 -0
  124. package/dist/collections/layers.js +43 -12
  125. package/dist/collections/metrics.js +70 -14
  126. package/dist/collections/serialize.js +4 -1
  127. package/dist/db/snapshots.d.ts +2 -1
  128. package/dist/db/snapshots.js +2 -2
  129. package/dist/endpoints/results.js +7 -0
  130. package/dist/endpoints/summary.d.ts +1 -1
  131. package/dist/endpoints/summary.js +37 -1
  132. package/dist/exports/adminClient.d.ts +2 -13
  133. package/dist/exports/adminClient.js +2 -13
  134. package/dist/exports/adminRsc.d.ts +3 -0
  135. package/dist/exports/adminRsc.js +3 -0
  136. package/dist/exports/index.d.ts +1 -1
  137. package/dist/options.js +29 -0
  138. package/dist/plugin.js +25 -0
  139. package/dist/puck/editor/CreateExperimentDrawer.d.ts +2 -1
  140. package/dist/puck/editor/CreateExperimentDrawer.js +17 -57
  141. package/dist/puck/editor/ExperimentSlotEditor.js +6 -0
  142. package/dist/puck/editor/copy.d.ts +10 -9
  143. package/dist/puck/editor/copy.js +11 -10
  144. package/dist/puck/editor/createTest.d.ts +7 -0
  145. package/dist/puck/editor/createTest.js +50 -0
  146. package/dist/puck/editor/editor.model.d.ts +28 -1
  147. package/dist/puck/editor/editor.model.js +61 -0
  148. package/dist/results/analyse.js +4 -1
  149. package/dist/results/verdict.d.ts +1 -0
  150. package/dist/results/verdict.js +7 -4
  151. package/dist/transitions/checkStart.d.ts +1 -0
  152. package/dist/transitions/placements.d.ts +5 -1
  153. package/dist/transitions/placements.js +14 -7
  154. package/dist/transitions/preflightData.d.ts +7 -1
  155. package/dist/transitions/preflightData.js +199 -148
  156. package/dist/transitions/start.js +2 -2
  157. package/dist/types/admin.d.ts +1 -0
  158. package/dist/types/contracts.d.ts +17 -0
  159. package/dist/types/endpoints.d.ts +11 -1
  160. package/dist/types/snapshot.d.ts +1 -0
  161. package/dist/types/surface.d.ts +29 -0
  162. package/dist/types/surface.js +1 -0
  163. package/package.json +7 -1
  164. package/dist/access.d.ts +0 -1
  165. package/dist/access.js +0 -2
  166. package/dist/admin/AllocationField.d.ts +0 -2
  167. package/dist/admin/AllocationField.js +0 -80
  168. package/dist/admin/FlagPointSelect.d.ts +0 -2
  169. package/dist/admin/FlagPointSelect.js +0 -54
  170. package/dist/admin/LockedFieldNotice.d.ts +0 -2
  171. package/dist/admin/LockedFieldNotice.js +0 -26
  172. package/dist/admin/PhaseHistory.d.ts +0 -2
  173. package/dist/admin/PhaseHistory.js +0 -76
  174. package/dist/admin/StatusCell.d.ts +0 -3
  175. package/dist/admin/StatusCell.js +0 -19
  176. package/dist/admin/SummaryCell.d.ts +0 -2
  177. package/dist/admin/SummaryCell.js +0 -32
  178. package/dist/admin/actions/AddMetricDialog.d.ts +0 -6
  179. package/dist/admin/actions/AddMetricDialog.js +0 -138
  180. package/dist/admin/actions/AllocateDialog.d.ts +0 -6
  181. package/dist/admin/actions/AllocateDialog.js +0 -81
  182. package/dist/admin/actions/ExperimentActions.d.ts +0 -3
  183. package/dist/admin/actions/ExperimentActions.js +0 -110
  184. package/dist/admin/arms/ArmsField.d.ts +0 -2
  185. package/dist/admin/arms/ArmsField.js +0 -310
  186. package/dist/admin/arms/ParamsEditor.d.ts +0 -16
  187. package/dist/admin/arms/ParamsEditor.js +0 -79
  188. package/dist/admin/copy/tabs.d.ts +0 -7
  189. package/dist/admin/copy/tabs.js +0 -7
  190. package/dist/admin/permissions.model.d.ts +0 -10
  191. package/dist/admin/permissions.model.js +0 -4
  192. package/dist/admin/phaseHistory.model.d.ts +0 -19
  193. package/dist/admin/phaseHistory.model.js +0 -19
  194. package/dist/admin/results/ResultsView.d.ts +0 -3
  195. package/dist/admin/results/ResultsView.js +0 -312
  196. package/dist/admin/review/ReviewPanel.d.ts +0 -2
  197. package/dist/admin/review/ReviewPanel.js +0 -268
  198. package/dist/admin/summaryBatcher.d.ts +0 -8
  199. package/dist/admin/summaryBatcher.js +0 -33
  200. package/dist/admin/targeting/TargetingAttributeField.d.ts +0 -2
  201. package/dist/admin/targeting/TargetingAttributeField.js +0 -31
  202. package/dist/admin/targeting/TargetingOpField.d.ts +0 -2
  203. package/dist/admin/targeting/TargetingOpField.js +0 -45
  204. package/dist/admin/targeting/TargetingValueField.d.ts +0 -2
  205. package/dist/admin/targeting/TargetingValueField.js +0 -142
  206. package/dist/index.d.ts +0 -1
  207. package/dist/index.js +0 -4
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { TEST_PAGE as C } from '../../copy/testPage.js';
5
+ import { ARMS_COPY, GROUP_LABELS, issueMessage } from '../../copy/arms.js';
6
+ import { addHoldoutArm, addVariant, issuesByRow, removeArm, splitEvenly, totalWeight, withSlotsFor } from '../../arms/arms.model.js';
7
+ import { allocationSentence } from '../../allocation.model.js';
8
+ import { balanceWeight, orderedVersions, trafficShares } from './testPage.model.js';
9
+ import { FieldErrors, ReadOnly, reasonFor, save, TextEdit } from './fields.js';
10
+ import { Section } from './Section.js';
11
+ const COLOURS = [
12
+ 'var(--theme-success-500)',
13
+ 'var(--theme-elevation-500)',
14
+ 'var(--theme-warning-500)',
15
+ 'var(--theme-error-500)',
16
+ 'var(--theme-elevation-300)'
17
+ ];
18
+ export function VersionsSection({ test, surface, parts }) {
19
+ const doc = test.doc;
20
+ const [weightEdit, setWeightEdit] = useState(null);
21
+ const releasedAllocation = useRef(null);
22
+ const [allocation, setAllocation] = useState(doc.allocation);
23
+ useEffect(()=>setWeightEdit(null), [
24
+ doc.arms
25
+ ]);
26
+ useEffect(()=>setAllocation(doc.allocation), [
27
+ doc.allocation
28
+ ]);
29
+ const reason = reasonFor(test, 'arms');
30
+ const live = weightEdit ? balanceWeight(doc.arms, weightEdit.key, weightEdit.weight) : doc.arms;
31
+ const ordered = orderedVersions(live);
32
+ const shares = trafficShares(ordered.map((row)=>row.arm));
33
+ const issues = issuesByRow(live, doc.kind);
34
+ const update = (transform)=>save(test, 'arms', ()=>({
35
+ arms: withSlotsFor(transform(test.doc.arms), test.doc.kind)
36
+ }));
37
+ const commitAllocation = ()=>{
38
+ if (allocation === test.doc.allocation || releasedAllocation.current === allocation) return;
39
+ releasedAllocation.current = allocation;
40
+ save(test, 'allocation', ()=>({
41
+ allocation
42
+ }));
43
+ };
44
+ return /*#__PURE__*/ _jsxs(Section, {
45
+ test: test,
46
+ sectionId: "versions",
47
+ children: [
48
+ /*#__PURE__*/ _jsx("div", {
49
+ className: "xp-version-rows",
50
+ children: ordered.map(({ arm, index }, position)=>/*#__PURE__*/ _jsxs("article", {
51
+ className: "xp-version-row",
52
+ children: [
53
+ /*#__PURE__*/ _jsxs("h3", {
54
+ children: [
55
+ /*#__PURE__*/ _jsx("span", {
56
+ style: {
57
+ background: COLOURS[position]
58
+ },
59
+ className: "xp-version-colour"
60
+ }),
61
+ GROUP_LABELS[arm.key]
62
+ ]
63
+ }),
64
+ /*#__PURE__*/ _jsx(TextEdit, {
65
+ label: `${GROUP_LABELS[arm.key]}: ${C.label}`,
66
+ value: arm.label,
67
+ reason: reason,
68
+ onCommit: (label)=>update((arms)=>arms.map((current)=>current.key === arm.key ? {
69
+ ...current,
70
+ label
71
+ } : current))
72
+ }),
73
+ /*#__PURE__*/ _jsx(TextEdit, {
74
+ label: `${GROUP_LABELS[arm.key]}: ${C.weight}`,
75
+ type: "number",
76
+ min: 0,
77
+ max: 100,
78
+ value: doc.arms[index].weight,
79
+ reason: reason,
80
+ validate: (raw)=>raw.trim() !== '' && Number.isInteger(Number(raw)) && Number(raw) >= 0 && Number(raw) <= 100,
81
+ invalidMessage: C.invalidShare,
82
+ onDraft: (value)=>setWeightEdit({
83
+ key: arm.key,
84
+ weight: Number(value)
85
+ }),
86
+ onCommit: (value)=>update((arms)=>balanceWeight(arms, arm.key, Number(value)))
87
+ }),
88
+ /*#__PURE__*/ _jsx(parts.VersionContent, {
89
+ test: test,
90
+ surface: surface,
91
+ armIndex: index
92
+ }),
93
+ issues.rows[index]?.map((issue, i)=>/*#__PURE__*/ _jsx("p", {
94
+ role: "alert",
95
+ children: issueMessage(issue, totalWeight(live))
96
+ }, i)),
97
+ /*#__PURE__*/ _jsx(FieldErrors, {
98
+ test: test,
99
+ path: `arms.${index}`
100
+ }),
101
+ !reason && arm.kind !== 'control' ? /*#__PURE__*/ _jsx("button", {
102
+ type: "button",
103
+ onClick: ()=>update((arms)=>splitEvenly(removeArm(arms, arm.key))),
104
+ children: C.remove
105
+ }) : null
106
+ ]
107
+ }, arm.key))
108
+ }),
109
+ /*#__PURE__*/ _jsx("div", {
110
+ className: "xp-traffic-bar",
111
+ role: "img",
112
+ "aria-label": `${C.traffic}: ${ordered.map(({ arm })=>`${arm.label} ${arm.weight}%`).join(', ')}`,
113
+ children: ordered.map(({ arm }, i)=>/*#__PURE__*/ _jsx("span", {
114
+ style: {
115
+ width: `${shares[i]}%`,
116
+ background: COLOURS[i]
117
+ }
118
+ }, arm.key))
119
+ }),
120
+ issues.form.map((issue, i)=>/*#__PURE__*/ _jsx("p", {
121
+ role: "alert",
122
+ children: issueMessage(issue, totalWeight(live))
123
+ }, i)),
124
+ test.errors.arms ? /*#__PURE__*/ _jsx("p", {
125
+ role: "alert",
126
+ children: test.errors.arms
127
+ }) : null,
128
+ !reason ? /*#__PURE__*/ _jsxs(_Fragment, {
129
+ children: [
130
+ /*#__PURE__*/ _jsxs("div", {
131
+ className: "xp-section-actions",
132
+ children: [
133
+ /*#__PURE__*/ _jsx("button", {
134
+ type: "button",
135
+ disabled: doc.arms.filter((arm)=>arm.kind === 'variant').length >= 3,
136
+ onClick: ()=>update((arms)=>addVariant(arms, doc.kind)),
137
+ children: C.addVersion
138
+ }),
139
+ /*#__PURE__*/ _jsx("button", {
140
+ type: "button",
141
+ disabled: doc.arms.some((arm)=>arm.kind === 'holdout'),
142
+ onClick: ()=>update(addHoldoutArm),
143
+ children: C.addHoldout
144
+ }),
145
+ /*#__PURE__*/ _jsx("button", {
146
+ type: "button",
147
+ onClick: ()=>update(splitEvenly),
148
+ children: C.split
149
+ })
150
+ ]
151
+ }),
152
+ /*#__PURE__*/ _jsx("p", {
153
+ children: ARMS_COPY.holdout
154
+ }),
155
+ /*#__PURE__*/ _jsx("p", {
156
+ children: C.balanceHelp
157
+ })
158
+ ]
159
+ }) : null,
160
+ reasonFor(test, 'allocation') ? /*#__PURE__*/ _jsx(ReadOnly, {
161
+ label: C.allocation,
162
+ value: `${doc.allocation}%`,
163
+ reason: reasonFor(test, 'allocation')
164
+ }) : /*#__PURE__*/ _jsxs("label", {
165
+ className: "xp-field",
166
+ children: [
167
+ C.allocation,
168
+ /*#__PURE__*/ _jsx("input", {
169
+ type: "range",
170
+ min: 1,
171
+ max: 100,
172
+ value: allocation,
173
+ onChange: (e)=>{
174
+ releasedAllocation.current = null;
175
+ setAllocation(Number(e.target.value));
176
+ },
177
+ onPointerUp: commitAllocation,
178
+ onKeyUp: (e)=>{
179
+ if ([
180
+ 'ArrowLeft',
181
+ 'ArrowRight',
182
+ 'ArrowUp',
183
+ 'ArrowDown',
184
+ 'Home',
185
+ 'End',
186
+ 'PageUp',
187
+ 'PageDown'
188
+ ].includes(e.key)) commitAllocation();
189
+ },
190
+ onBlur: commitAllocation
191
+ })
192
+ ]
193
+ }),
194
+ /*#__PURE__*/ _jsx("p", {
195
+ children: allocationSentence({
196
+ allocation: reasonFor(test, 'allocation') ? doc.allocation : allocation,
197
+ unitType: doc.unitType,
198
+ arms: live,
199
+ status: doc.status
200
+ })
201
+ }),
202
+ /*#__PURE__*/ _jsx(FieldErrors, {
203
+ test: test,
204
+ path: "allocation"
205
+ })
206
+ ]
207
+ });
208
+ }
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { SectionProps, TestDocApi } from '../contracts.js';
3
+ import type { EditableField } from '../../../types/admin.js';
4
+ import type { TestDocumentPatch } from '../testDocument.model.js';
5
+ export declare function reasonFor(test: TestDocApi, field: string): string | null;
6
+ export declare function save(test: TestDocApi, field: EditableField, patch: () => TestDocumentPatch): void;
7
+ export declare function FieldErrors({ test, path }: {
8
+ test: TestDocApi;
9
+ path: string;
10
+ }): import("react").JSX.Element;
11
+ export declare function ReadOnly({ label, value, reason }: {
12
+ label: string;
13
+ value: ReactNode;
14
+ reason: string | null;
15
+ }): import("react").JSX.Element;
16
+ /** Local typing is intentionally separate from staging: build the current group on commit. */
17
+ export declare function TextEdit({ label, value, onCommit, reason, multiline, placeholder, type, min, max, required, onDraft, validate, invalidMessage }: {
18
+ label: string;
19
+ value: string | number;
20
+ onCommit: (value: string) => void;
21
+ reason?: string | null;
22
+ onDraft?: (value: string) => void;
23
+ multiline?: boolean;
24
+ placeholder?: string;
25
+ type?: 'text' | 'number';
26
+ min?: number;
27
+ max?: number;
28
+ required?: boolean;
29
+ validate?: (value: string) => boolean;
30
+ invalidMessage?: string;
31
+ }): import("react").JSX.Element;
32
+ export declare const positiveInteger: (raw: string) => boolean;
33
+ export declare function SelectEdit({ label, value, options, reason, onCommit, readOnlyValue }: {
34
+ label: string;
35
+ value: string;
36
+ options: readonly {
37
+ label: string;
38
+ value: string;
39
+ }[];
40
+ reason?: string | null;
41
+ onCommit: (value: string) => void;
42
+ readOnlyValue?: string;
43
+ }): import("react").JSX.Element;
44
+ export declare function CheckboxList({ label, value, options, reason, onToggle, readOnlyValue }: {
45
+ label: string;
46
+ value: string[];
47
+ options: readonly {
48
+ label: string;
49
+ value: string;
50
+ }[];
51
+ reason?: string | null;
52
+ onToggle: (value: string, checked: boolean) => void;
53
+ readOnlyValue?: string;
54
+ }): import("react").JSX.Element;
55
+ export declare function Hypothesis({ test }: SectionProps): import("react").JSX.Element;
@@ -0,0 +1,185 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useState } from 'react';
4
+ import { TEST_PAGE as C } from '../../copy/testPage.js';
5
+ import { EDITABLE_BY_STATUS } from '../../../collections/experiments/editability.js';
6
+ import { lockedReason } from '../../lockedNotice.model.js';
7
+ export function reasonFor(test, field) {
8
+ return test.doc ? lockedReason(test.doc.status, field, EDITABLE_BY_STATUS) : null;
9
+ }
10
+ export function save(test, field, patch) {
11
+ if (!test.doc || reasonFor(test, field)) return;
12
+ test.stage(patch());
13
+ void test.commit();
14
+ }
15
+ export function FieldErrors({ test, path }) {
16
+ return /*#__PURE__*/ _jsx(_Fragment, {
17
+ children: Object.entries(test.errors).filter(([key])=>key === path || key.startsWith(`${path}.`)).map(([key, message])=>/*#__PURE__*/ _jsx("p", {
18
+ className: "xp-field-error",
19
+ role: "alert",
20
+ children: message
21
+ }, key))
22
+ });
23
+ }
24
+ export function ReadOnly({ label, value, reason }) {
25
+ return /*#__PURE__*/ _jsxs("div", {
26
+ className: "xp-read-only",
27
+ title: reason ?? undefined,
28
+ children: [
29
+ /*#__PURE__*/ _jsx("strong", {
30
+ children: label
31
+ }),
32
+ /*#__PURE__*/ _jsx("p", {
33
+ children: value
34
+ }),
35
+ reason ? /*#__PURE__*/ _jsx("small", {
36
+ children: reason
37
+ }) : null
38
+ ]
39
+ });
40
+ }
41
+ /** Local typing is intentionally separate from staging: build the current group on commit. */ export function TextEdit({ label, value, onCommit, reason, multiline, placeholder, type = 'text', min, max, required, onDraft, validate, invalidMessage }) {
42
+ const id = useId();
43
+ const [draft, setDraft] = useState(String(value));
44
+ const [error, setError] = useState(null);
45
+ useEffect(()=>setDraft(String(value)), [
46
+ value
47
+ ]);
48
+ const commit = ()=>{
49
+ if (draft === String(value)) return;
50
+ if (validate && !validate(draft)) {
51
+ setError(draft.trim() ? invalidMessage ?? null : null);
52
+ setDraft(String(value));
53
+ onDraft?.(String(value));
54
+ return;
55
+ }
56
+ onCommit(draft);
57
+ };
58
+ if (reason) return /*#__PURE__*/ _jsx(ReadOnly, {
59
+ label: label,
60
+ value: value === '' && placeholder ? placeholder : value,
61
+ reason: reason
62
+ });
63
+ return /*#__PURE__*/ _jsxs(_Fragment, {
64
+ children: [
65
+ /*#__PURE__*/ _jsxs("label", {
66
+ className: "xp-field",
67
+ htmlFor: id,
68
+ children: [
69
+ /*#__PURE__*/ _jsx("span", {
70
+ children: label
71
+ }),
72
+ multiline ? /*#__PURE__*/ _jsx("textarea", {
73
+ id: id,
74
+ value: draft,
75
+ placeholder: placeholder,
76
+ onChange: (e)=>{
77
+ setDraft(e.target.value);
78
+ onDraft?.(e.target.value);
79
+ },
80
+ onBlur: commit
81
+ }) : /*#__PURE__*/ _jsx("input", {
82
+ id: id,
83
+ type: type,
84
+ value: draft,
85
+ min: min,
86
+ max: max,
87
+ required: required,
88
+ placeholder: placeholder,
89
+ "aria-invalid": error ? true : undefined,
90
+ "aria-describedby": error ? `${id}-error` : undefined,
91
+ onChange: (e)=>{
92
+ setError(null);
93
+ setDraft(e.target.value);
94
+ onDraft?.(e.target.value);
95
+ },
96
+ onBlur: commit,
97
+ onKeyDown: (e)=>{
98
+ if (e.key === 'Enter') e.currentTarget.blur();
99
+ }
100
+ })
101
+ ]
102
+ }),
103
+ error ? /*#__PURE__*/ _jsx("p", {
104
+ className: "xp-field-error",
105
+ id: `${id}-error`,
106
+ role: "alert",
107
+ children: error
108
+ }) : null
109
+ ]
110
+ });
111
+ }
112
+ export const positiveInteger = (raw)=>raw.trim() !== '' && Number.isInteger(Number(raw)) && Number(raw) > 0;
113
+ export function SelectEdit({ label, value, options, reason, onCommit, readOnlyValue }) {
114
+ const id = useId();
115
+ if (reason) {
116
+ return /*#__PURE__*/ _jsx(ReadOnly, {
117
+ label: label,
118
+ reason: reason,
119
+ value: readOnlyValue ?? options.find((o)=>o.value === value)?.label ?? value
120
+ });
121
+ }
122
+ return /*#__PURE__*/ _jsxs("label", {
123
+ className: "xp-field",
124
+ htmlFor: id,
125
+ children: [
126
+ /*#__PURE__*/ _jsx("span", {
127
+ children: label
128
+ }),
129
+ /*#__PURE__*/ _jsx("select", {
130
+ id: id,
131
+ value: value,
132
+ onChange: (e)=>onCommit(e.target.value),
133
+ children: options.map((option)=>/*#__PURE__*/ _jsx("option", {
134
+ value: option.value,
135
+ children: option.label
136
+ }, option.value))
137
+ })
138
+ ]
139
+ });
140
+ }
141
+ export function CheckboxList({ label, value, options, reason, onToggle, readOnlyValue }) {
142
+ if (reason) return /*#__PURE__*/ _jsx(ReadOnly, {
143
+ label: label,
144
+ reason: reason,
145
+ value: readOnlyValue ?? value.map((v)=>options.find((o)=>o.value === v)?.label ?? v).join(', ')
146
+ });
147
+ return /*#__PURE__*/ _jsxs("fieldset", {
148
+ className: "xp-field",
149
+ children: [
150
+ /*#__PURE__*/ _jsx("legend", {
151
+ children: label
152
+ }),
153
+ options.map((option)=>/*#__PURE__*/ _jsxs("label", {
154
+ children: [
155
+ /*#__PURE__*/ _jsx("input", {
156
+ type: "checkbox",
157
+ checked: value.includes(option.value),
158
+ onChange: (e)=>onToggle(option.value, e.target.checked)
159
+ }),
160
+ option.label
161
+ ]
162
+ }, option.value))
163
+ ]
164
+ });
165
+ }
166
+ export function Hypothesis({ test }) {
167
+ return /*#__PURE__*/ _jsxs(_Fragment, {
168
+ children: [
169
+ /*#__PURE__*/ _jsx(TextEdit, {
170
+ label: C.hypothesis,
171
+ value: test.doc?.hypothesis ?? '',
172
+ multiline: true,
173
+ placeholder: C.hypothesisPlaceholder,
174
+ reason: reasonFor(test, 'hypothesis'),
175
+ onCommit: (value)=>save(test, 'hypothesis', ()=>({
176
+ hypothesis: value
177
+ }))
178
+ }),
179
+ /*#__PURE__*/ _jsx(FieldErrors, {
180
+ test: test,
181
+ path: "hypothesis"
182
+ })
183
+ ]
184
+ });
185
+ }
@@ -0,0 +1,2 @@
1
+ import type { KindParts } from '../contracts.js';
2
+ export declare const pageParts: KindParts;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { TEST_PAGE as C } from '../../copy/testPage.js';
4
+ import { pageTargetIssue, setPageTarget, showsPageTarget } from '../../arms/pageTarget.model.js';
5
+ import { FieldErrors, reasonFor, save, TextEdit } from './fields.js';
6
+ function VersionContent({ test, armIndex }) {
7
+ const arm = test.doc?.arms[armIndex];
8
+ if (!arm || !test.doc) return null;
9
+ if (!showsPageTarget(test.doc.kind, arm.kind)) return /*#__PURE__*/ _jsx("p", {
10
+ children: C.holdoutContent
11
+ });
12
+ const issue = pageTargetIssue(arm.pageTarget, arm.kind);
13
+ return /*#__PURE__*/ _jsxs(_Fragment, {
14
+ children: [
15
+ /*#__PURE__*/ _jsx(TextEdit, {
16
+ label: `${arm.label}: ${C.pageAddress}`,
17
+ value: arm.pageTarget ?? '',
18
+ reason: reasonFor(test, 'arms'),
19
+ onCommit: (value)=>save(test, 'arms', ()=>({
20
+ arms: test.doc.arms.map((current)=>current.key === arm.key ? setPageTarget(current, value) : current)
21
+ }))
22
+ }),
23
+ issue ? /*#__PURE__*/ _jsx("p", {
24
+ role: "alert",
25
+ children: issue
26
+ }) : null,
27
+ /*#__PURE__*/ _jsx(FieldErrors, {
28
+ test: test,
29
+ path: `arms.${armIndex}.pageTarget`
30
+ })
31
+ ]
32
+ });
33
+ }
34
+ export const pageParts = {
35
+ Binding: ()=>/*#__PURE__*/ _jsx("p", {
36
+ children: C.pageBinding
37
+ }),
38
+ VersionContent
39
+ };
@@ -0,0 +1,23 @@
1
+ .xp-test-page { color: var(--theme-text); background: var(--theme-bg); max-width: 90rem; margin: 0 auto; padding: 1.5rem; }
2
+ .xp-test-header { position: sticky; top: 0; z-index: 10; background: var(--theme-bg); padding: 1rem 0; border-bottom: 1px solid var(--theme-elevation-150); }
3
+ .xp-header-meta, .xp-section-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-block: 1rem; }
4
+ .xp-kind-chip, .xp-status-pill { border: 1px solid var(--theme-elevation-250); border-radius: 1rem; padding: .25rem .75rem; }
5
+ .xp-status-pill--success, .xp-content-ready, .xp-section-check--done { color: var(--theme-success-600); }
6
+ .xp-content-empty, .xp-status-pill--warning { color: var(--theme-warning-600); }
7
+ .xp-test-section { padding-block: 1.5rem; border-bottom: 1px solid var(--theme-elevation-150); scroll-margin-top: 15rem; }
8
+ .xp-version-rows { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }
9
+ .xp-version-row { border: 1px solid var(--theme-elevation-150); border-radius: var(--style-radius-s); padding: 1rem; }
10
+ .xp-version-colour { display: inline-block; width: .75rem; height: .75rem; margin-right: .5rem; border-radius: 50%; }
11
+ .xp-traffic-bar { display: flex; height: 1.25rem; background: var(--theme-elevation-100); margin-block: 1.5rem; overflow: hidden; border-radius: var(--style-radius-s); }
12
+ .xp-traffic-bar > span { display: block; }
13
+ .xp-field { display: flex; flex-direction: column; gap: .5rem; margin-block: 1rem; }
14
+ .xp-field input, .xp-field textarea, .xp-field select { font: inherit; color: var(--theme-text); background: var(--theme-input-bg); border: 1px solid var(--theme-elevation-250); border-radius: var(--style-radius-s); padding: .65rem; }
15
+ .xp-field textarea { min-height: 6rem; resize: vertical; }
16
+ .xp-field input[type=range] { padding: 0; }
17
+ .xp-read-only { padding-block: .5rem; }
18
+ .xp-read-only p { margin-block: .5rem; }
19
+ .xp-field-error { color: var(--theme-error-600); }
20
+ .xp-test-page button { cursor: pointer; color: var(--theme-text); background: var(--theme-elevation-100); border: 1px solid var(--theme-elevation-250); padding: .65rem 1rem; border-radius: var(--style-radius-s); font: inherit; }
21
+ .xp-test-page button:disabled { opacity: .5; cursor: default; }
22
+ .xp-test-page summary { cursor: pointer; padding-block: .75rem; }
23
+ .xp-targeting-row { border-bottom: 1px solid var(--theme-elevation-150); padding-bottom: 1rem; }
@@ -0,0 +1,17 @@
1
+ import type { Arm, ArmKey, ExperimentKind, ExposurePolicy } from '../../../types/contracts.js';
2
+ import type { PreflightResult } from '../../../types/endpoints.js';
3
+ export declare function orderedVersions(arms: readonly Arm[]): Array<{
4
+ arm: Arm;
5
+ index: number;
6
+ }>;
7
+ /** Normalised visual shares only; invalid saved weights still surface as validation issues. */
8
+ export declare function trafficShares(arms: readonly Arm[]): number[];
9
+ export declare function durationSuggestion(preflight: PreflightResult | null, savedDays?: number | null): {
10
+ weeks: number;
11
+ plannedDurationDays: number;
12
+ sequentialTuningParameter: number;
13
+ feasible: boolean;
14
+ } | null;
15
+ export declare function exposureOptions(kind: ExperimentKind): readonly ExposurePolicy[];
16
+ /** Keep the edited share exact and distribute the remaining whole percentages proportionally. */
17
+ export declare function balanceWeight(arms: Arm[], key: ArmKey, weight: number): Arm[];
@@ -0,0 +1,53 @@
1
+ import { setWeight } from '../../arms/arms.model.js';
2
+ const ORDER = [
3
+ 'control',
4
+ 'variantA',
5
+ 'variantB',
6
+ 'variantC',
7
+ 'holdout'
8
+ ];
9
+ export function orderedVersions(arms) {
10
+ return arms.map((arm, index)=>({
11
+ arm,
12
+ index
13
+ })).sort((a, b)=>ORDER.indexOf(a.arm.key) - ORDER.indexOf(b.arm.key));
14
+ }
15
+ /** Normalised visual shares only; invalid saved weights still surface as validation issues. */ export function trafficShares(arms) {
16
+ const weights = arms.map((arm)=>Number.isFinite(arm.weight) ? Math.max(0, arm.weight) : 0);
17
+ const sum = weights.reduce((a, b)=>a + b, 0);
18
+ return weights.map((weight)=>sum ? weight / sum * 100 : 0);
19
+ }
20
+ export function durationSuggestion(preflight, savedDays) {
21
+ const estimate = preflight?.estimate;
22
+ return estimate && estimate.plannedDurationDays !== savedDays ? estimate : null;
23
+ }
24
+ export function exposureOptions(kind) {
25
+ return kind === 'block' ? [
26
+ 'render',
27
+ 'viewport'
28
+ ] : kind === 'page' ? [
29
+ 'pageview'
30
+ ] : [
31
+ 'manual'
32
+ ];
33
+ }
34
+ /** Keep the edited share exact and distribute the remaining whole percentages proportionally. */ export function balanceWeight(arms, key, weight) {
35
+ if (!arms.some((arm)=>arm.key === key) || !Number.isInteger(weight) || weight < 0 || weight > 100) return arms;
36
+ const others = arms.filter((arm)=>arm.key !== key);
37
+ if (!others.length) return arms;
38
+ const total = others.reduce((sum, arm)=>sum + Math.max(0, Number.isFinite(arm.weight) ? arm.weight : 0), 0);
39
+ const denominator = total || others.length;
40
+ const raw = others.map((arm)=>(total ? Math.max(0, Number.isFinite(arm.weight) ? arm.weight : 0) : 1) * (100 - weight) / denominator);
41
+ const whole = raw.map(Math.floor);
42
+ const order = raw.map((_, i)=>i).sort((a, b)=>raw[b] - whole[b] - (raw[a] - whole[a]) || a - b);
43
+ const remainder = 100 - weight - whole.reduce((sum, value)=>sum + value, 0);
44
+ for(let i = 0; i < remainder; i++)whole[order[i]]++;
45
+ const shares = new Map(others.map((arm, i)=>[
46
+ arm.key,
47
+ whole[i]
48
+ ]));
49
+ return setWeight(arms, key, weight).map((arm)=>shares.has(arm.key) ? {
50
+ ...arm,
51
+ weight: shares.get(arm.key)
52
+ } : arm);
53
+ }
@@ -0,0 +1,39 @@
1
+ import type { EditableField } from '../../types/admin.js';
2
+ import type { Arm, DocId, ExperimentKind, ExposurePolicy, Status, TargetingRule, UnitType } from '../../types/contracts.js';
3
+ /** The depth-zero collection document, including fields owned by the server. */
4
+ export interface TestDocument {
5
+ id: DocId;
6
+ name: string;
7
+ slug: string;
8
+ status: Status;
9
+ kind: ExperimentKind;
10
+ unitType: UnitType;
11
+ hypothesis?: string | null;
12
+ arms: Arm[];
13
+ targeting?: TargetingRule[] | null;
14
+ allocation: number;
15
+ flagPoint?: string | null;
16
+ exposurePolicy: ExposurePolicy;
17
+ metrics?: {
18
+ primary?: DocId | null;
19
+ secondary?: DocId[] | null;
20
+ guardrails?: DocId[] | null;
21
+ } | null;
22
+ analysis?: {
23
+ method?: 'sequential' | 'bayesian' | null;
24
+ cuped?: 'auto' | 'off' | null;
25
+ minExposuresPerArm?: number | null;
26
+ plannedDurationDays?: number | null;
27
+ attributionWindowHours?: number | null;
28
+ sequentialTuningParameter?: number | null;
29
+ reachPerDay?: number | null;
30
+ } | null;
31
+ [key: string]: unknown;
32
+ }
33
+ export type TestDocumentPatch = Partial<Pick<TestDocument, EditableField>>;
34
+ /** Drop unknown and locked keys even when the caller is untyped. Groups replace whole groups. */
35
+ export declare function editablePatch(status: Status, partial: TestDocumentPatch): TestDocumentPatch;
36
+ /** Only changed top-level keys go on the wire; arrays and groups remain intact. */
37
+ export declare function changedPatch(saved: TestDocument, staged: TestDocumentPatch): TestDocumentPatch;
38
+ /** Payload validation details may be nested under errors[0].data or top-level data. */
39
+ export declare function payloadFieldErrors(envelope: unknown, section?: string, fallback?: string): Record<string, string>;