@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4

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 (336) hide show
  1. package/README.md +202 -26
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +50 -21
  4. package/dist/admin/TestsNavLink.d.ts +3 -1
  5. package/dist/admin/TestsNavLink.js +2 -2
  6. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  7. package/dist/admin/actions/ConfirmDialog.js +37 -82
  8. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  9. package/dist/admin/actions/DecideDialog.js +30 -13
  10. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  11. package/dist/admin/actions/MakeContentResult.js +115 -16
  12. package/dist/admin/actions/actions.model.d.ts +2 -4
  13. package/dist/admin/actions/actions.model.js +2 -3
  14. package/dist/admin/allocation.model.js +3 -5
  15. package/dist/admin/api.d.ts +10 -4
  16. package/dist/admin/api.js +11 -7
  17. package/dist/admin/armSelect/ArmSelectField.js +14 -22
  18. package/dist/admin/arms/arms.model.d.ts +1 -21
  19. package/dist/admin/arms/arms.model.js +1 -153
  20. package/dist/admin/copy/actions.d.ts +39 -3
  21. package/dist/admin/copy/actions.js +40 -4
  22. package/dist/admin/copy/armSelect.js +5 -5
  23. package/dist/admin/copy/arms.d.ts +1 -6
  24. package/dist/admin/copy/arms.js +1 -16
  25. package/dist/admin/copy/audience.d.ts +2 -1
  26. package/dist/admin/copy/audience.js +3 -2
  27. package/dist/admin/copy/common.d.ts +13 -6
  28. package/dist/admin/copy/common.js +14 -8
  29. package/dist/admin/copy/goals.d.ts +21 -6
  30. package/dist/admin/copy/goals.js +27 -6
  31. package/dist/admin/copy/readiness.d.ts +22 -0
  32. package/dist/admin/copy/readiness.js +21 -0
  33. package/dist/admin/copy/recommendation.d.ts +0 -3
  34. package/dist/admin/copy/recommendation.js +0 -3
  35. package/dist/admin/copy/results.d.ts +43 -2
  36. package/dist/admin/copy/results.js +44 -3
  37. package/dist/admin/copy/review.d.ts +2 -2
  38. package/dist/admin/copy/review.js +2 -2
  39. package/dist/admin/copy/setting.d.ts +2 -0
  40. package/dist/admin/copy/setting.js +2 -0
  41. package/dist/admin/copy/surface.d.ts +7 -5
  42. package/dist/admin/copy/surface.js +9 -5
  43. package/dist/admin/copy/testPage.d.ts +10 -0
  44. package/dist/admin/copy/testPage.js +10 -0
  45. package/dist/admin/format/date.d.ts +2 -0
  46. package/dist/admin/format/date.js +10 -0
  47. package/dist/admin/paths.d.ts +2 -1
  48. package/dist/admin/paths.js +2 -1
  49. package/dist/admin/previewLinks.model.d.ts +4 -2
  50. package/dist/admin/previewLinks.model.js +8 -7
  51. package/dist/admin/results/CiChart.js +11 -21
  52. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  53. package/dist/admin/results/DataQualityPanel.js +30 -17
  54. package/dist/admin/results/DataTable.d.ts +16 -0
  55. package/dist/admin/results/DataTable.js +152 -0
  56. package/dist/admin/results/StatTile.d.ts +7 -0
  57. package/dist/admin/results/StatTile.js +22 -0
  58. package/dist/admin/results/VerdictHero.d.ts +13 -0
  59. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  60. package/dist/admin/results/legacy.d.ts +16 -0
  61. package/dist/admin/results/legacy.js +27 -0
  62. package/dist/admin/results/releasePages.model.d.ts +17 -0
  63. package/dist/admin/results/releasePages.model.js +10 -0
  64. package/dist/admin/results/results.css +64 -0
  65. package/dist/admin/results/results.model.d.ts +11 -7
  66. package/dist/admin/results/results.model.js +24 -7
  67. package/dist/admin/results/snapshot.d.ts +14 -0
  68. package/dist/admin/results/snapshot.js +3 -0
  69. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  70. package/dist/admin/surface/ResultsRoute.js +59 -34
  71. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  72. package/dist/admin/surface/SurfaceShell.js +49 -14
  73. package/dist/admin/surface/TestsHome.d.ts +6 -1
  74. package/dist/admin/surface/TestsHome.js +182 -114
  75. package/dist/admin/surface/TestsSurface.js +56 -16
  76. package/dist/admin/surface/goals/GoalForm.js +341 -308
  77. package/dist/admin/surface/goals/GoalsList.js +123 -89
  78. package/dist/admin/surface/goals/data.d.ts +8 -2
  79. package/dist/admin/surface/goals/data.js +24 -16
  80. package/dist/admin/surface/goals/goals.css +10 -13
  81. package/dist/admin/surface/home.model.d.ts +8 -8
  82. package/dist/admin/surface/home.model.js +23 -12
  83. package/dist/admin/surface/newTest/NewTest.js +9 -9
  84. package/dist/admin/surface/pageBlock/parts.js +31 -26
  85. package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
  86. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  87. package/dist/admin/surface/readiness/Banner.js +58 -33
  88. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  89. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  90. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  91. package/dist/admin/surface/readiness/readiness.model.js +25 -9
  92. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  93. package/dist/admin/surface/readiness/useTransition.js +2 -1
  94. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  95. package/dist/admin/surface/results/ReleasePages.js +116 -0
  96. package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
  97. package/dist/admin/surface/results/ResultsBody.js +277 -105
  98. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  99. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  100. package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
  101. package/dist/admin/surface/results/recommendation.model.js +12 -9
  102. package/dist/admin/surface/results/useResults.d.ts +21 -3
  103. package/dist/admin/surface/results/useResults.js +2 -1
  104. package/dist/admin/surface/setting/parts.js +75 -88
  105. package/dist/admin/surface/setting/setting.css +3 -15
  106. package/dist/admin/surface/surface.css +61 -47
  107. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  108. package/dist/admin/surface/test/GoalSection.js +46 -47
  109. package/dist/admin/surface/test/LengthSection.js +46 -34
  110. package/dist/admin/surface/test/Rail.d.ts +4 -0
  111. package/dist/admin/surface/test/Rail.js +83 -0
  112. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  113. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  114. package/dist/admin/surface/test/Section.d.ts +1 -1
  115. package/dist/admin/surface/test/Section.js +36 -16
  116. package/dist/admin/surface/test/TargetingSection.js +168 -142
  117. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  118. package/dist/admin/surface/test/TestPage.js +146 -68
  119. package/dist/admin/surface/test/VersionsSection.js +151 -120
  120. package/dist/admin/surface/test/fields.d.ts +11 -4
  121. package/dist/admin/surface/test/fields.js +84 -85
  122. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  123. package/dist/admin/surface/test/jumpToSection.js +10 -0
  124. package/dist/admin/surface/test/pageParts.js +9 -3
  125. package/dist/admin/surface/test/testPage.css +81 -14
  126. package/dist/admin/surface/test/testPage.model.d.ts +2 -3
  127. package/dist/admin/surface/test/testPage.model.js +1 -21
  128. package/dist/admin/surface/useTestDocument.js +12 -5
  129. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  130. package/dist/admin/targeting/targeting.model.js +3 -8
  131. package/dist/admin/theme.css +115 -36
  132. package/dist/admin/theme.d.ts +38 -0
  133. package/dist/admin/theme.js +38 -1
  134. package/dist/admin/useApi.d.ts +17 -2
  135. package/dist/admin/useLayerUsage.js +14 -23
  136. package/dist/admin/views/ExperimentsRootView.js +12 -2
  137. package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
  138. package/dist/admin/views/TestsNavLinkServer.js +43 -0
  139. package/dist/arms/copy.d.ts +6 -0
  140. package/dist/arms/copy.js +16 -0
  141. package/dist/arms/model.d.ts +23 -0
  142. package/dist/arms/model.js +173 -0
  143. package/dist/collections/experiments/config.js +25 -0
  144. package/dist/collections/experiments/lockHook.js +3 -1
  145. package/dist/collections/identities.js +6 -1
  146. package/dist/collections/metrics.js +7 -0
  147. package/dist/collections/metricsGuards.d.ts +9 -0
  148. package/dist/collections/metricsGuards.js +33 -0
  149. package/dist/contentDone.d.ts +4 -0
  150. package/dist/contentDone.js +5 -0
  151. package/dist/db/appends.d.ts +6 -2
  152. package/dist/db/appends.js +10 -4
  153. package/dist/db/identities.d.ts +8 -7
  154. package/dist/db/identities.js +9 -6
  155. package/dist/db/locks.d.ts +6 -0
  156. package/dist/db/locks.js +13 -1
  157. package/dist/db/metricUsage.d.ts +19 -0
  158. package/dist/db/metricUsage.js +54 -0
  159. package/dist/db/names.d.ts +5 -0
  160. package/dist/db/names.js +5 -0
  161. package/dist/db/pageOccupancy.d.ts +12 -0
  162. package/dist/db/pageOccupancy.js +45 -0
  163. package/dist/db/publishedContent.d.ts +10 -0
  164. package/dist/db/publishedContent.js +43 -0
  165. package/dist/db/results.d.ts +2 -0
  166. package/dist/db/results.js +6 -2
  167. package/dist/db/snapshots.d.ts +18 -6
  168. package/dist/db/snapshots.js +25 -8
  169. package/dist/db/sweep.js +11 -6
  170. package/dist/endpoints/index.js +19 -1
  171. package/dist/endpoints/results.js +5 -0
  172. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  173. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  174. package/dist/endpoints/summary.js +20 -2
  175. package/dist/endpoints/transitions.d.ts +15 -0
  176. package/dist/endpoints/transitions.js +36 -0
  177. package/dist/errors.d.ts +8 -0
  178. package/dist/errors.js +28 -0
  179. package/dist/exports/adminRsc.d.ts +1 -0
  180. package/dist/exports/adminRsc.js +1 -0
  181. package/dist/exports/index.d.ts +2 -1
  182. package/dist/exports/index.js +1 -0
  183. package/dist/exports/puck.d.ts +2 -1
  184. package/dist/exports/puck.js +1 -1
  185. package/dist/exports/puckEditor.d.ts +4 -1
  186. package/dist/exports/puckEditor.js +3 -1
  187. package/dist/exports/rsc.d.ts +6 -1
  188. package/dist/exports/rsc.js +2 -0
  189. package/dist/hooks/pageGuard.d.ts +5 -0
  190. package/dist/hooks/pageGuard.js +139 -0
  191. package/dist/lib/fetchAllDocs.d.ts +16 -0
  192. package/dist/lib/fetchAllDocs.js +56 -0
  193. package/dist/log.js +6 -3
  194. package/dist/manifest/compile.d.ts +5 -0
  195. package/dist/manifest/compile.js +5 -1
  196. package/dist/options.js +16 -0
  197. package/dist/plugin.js +30 -2
  198. package/dist/puck/ExperimentPicker.js +47 -16
  199. package/dist/puck/ExperimentSlot.d.ts +51 -1
  200. package/dist/puck/ExperimentSlot.js +23 -12
  201. package/dist/puck/copy.d.ts +6 -5
  202. package/dist/puck/copy.js +6 -5
  203. package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
  204. package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
  205. package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
  206. package/dist/puck/editor/ResultsReadout.d.ts +6 -0
  207. package/dist/puck/editor/ResultsReadout.js +99 -0
  208. package/dist/puck/editor/VersionBuilder.d.ts +34 -0
  209. package/dist/puck/editor/VersionBuilder.js +317 -0
  210. package/dist/puck/editor/armWrites.d.ts +7 -0
  211. package/dist/puck/editor/armWrites.js +71 -0
  212. package/dist/puck/editor/armsCache.d.ts +14 -0
  213. package/dist/puck/editor/armsCache.js +78 -0
  214. package/dist/puck/editor/copy.d.ts +33 -1
  215. package/dist/puck/editor/copy.js +45 -8
  216. package/dist/puck/editor/createTest.d.ts +1 -1
  217. package/dist/puck/editor/createTest.js +25 -14
  218. package/dist/puck/editor/editor.chrome.css +121 -0
  219. package/dist/puck/editor/editor.css +288 -204
  220. package/dist/puck/editor/editor.model.d.ts +4 -1
  221. package/dist/puck/editor/editor.model.js +10 -7
  222. package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
  223. package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
  224. package/dist/puck/experimentSlotField.d.ts +2 -0
  225. package/dist/puck/experimentSlotField.js +11 -3
  226. package/dist/puck/hostContext.d.ts +10 -0
  227. package/dist/puck/hostContext.js +12 -0
  228. package/dist/puck/routes.d.ts +12 -0
  229. package/dist/puck/routes.js +11 -0
  230. package/dist/puck/slotConfig.d.ts +1 -1
  231. package/dist/puck/slotConfig.js +2 -2
  232. package/dist/results/aggregate.js +4 -3
  233. package/dist/results/analyse.d.ts +4 -1
  234. package/dist/results/analyse.js +27 -11
  235. package/dist/results/cuped.js +3 -2
  236. package/dist/results/identityCredit.d.ts +15 -7
  237. package/dist/results/identityCredit.js +32 -33
  238. package/dist/results/rolloutUnits.js +2 -1
  239. package/dist/results/snapshot.js +8 -2
  240. package/dist/results/srm.js +2 -1
  241. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  242. package/dist/sanitizeReadinessDetail.js +63 -0
  243. package/dist/server/linkIdentity.d.ts +10 -1
  244. package/dist/server/linkIdentity.js +17 -15
  245. package/dist/server/metadata.d.ts +5 -0
  246. package/dist/server/metadata.js +18 -3
  247. package/dist/server/normaliseContentRelationships.d.ts +7 -0
  248. package/dist/server/normaliseContentRelationships.js +62 -0
  249. package/dist/server/recordContentChange.d.ts +1 -1
  250. package/dist/server/recordContentChange.js +17 -8
  251. package/dist/server/runResults.d.ts +3 -2
  252. package/dist/server/runResults.js +157 -35
  253. package/dist/targeting/values.d.ts +4 -0
  254. package/dist/targeting/values.js +10 -0
  255. package/dist/transitions/addMetric.js +8 -0
  256. package/dist/transitions/archive.js +2 -3
  257. package/dist/transitions/checkStart.d.ts +10 -3
  258. package/dist/transitions/checkStart.js +25 -0
  259. package/dist/transitions/contentFields.d.ts +7 -0
  260. package/dist/transitions/contentFields.js +20 -0
  261. package/dist/transitions/decide.js +22 -2
  262. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  263. package/dist/transitions/dismissContentEntry.js +45 -0
  264. package/dist/transitions/makeContent.d.ts +5 -2
  265. package/dist/transitions/makeContent.js +217 -118
  266. package/dist/transitions/phaseRecord.d.ts +8 -2
  267. package/dist/transitions/placements.d.ts +3 -6
  268. package/dist/transitions/placements.js +12 -1
  269. package/dist/transitions/preflightData.d.ts +9 -1
  270. package/dist/transitions/preflightData.js +138 -20
  271. package/dist/transitions/releasePages.d.ts +8 -0
  272. package/dist/transitions/releasePages.js +26 -0
  273. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  274. package/dist/transitions/restoreContentEntry.js +47 -0
  275. package/dist/transitions/start.js +62 -7
  276. package/dist/types/contracts.d.ts +24 -4
  277. package/dist/types/endpoints.d.ts +34 -3
  278. package/dist/types/index.d.ts +1 -0
  279. package/dist/types/index.js +1 -0
  280. package/dist/types/reasons.d.ts +3 -0
  281. package/dist/types/reasons.js +1 -0
  282. package/dist/types/snapshot.d.ts +6 -1
  283. package/dist/ui/Banner.d.ts +2 -0
  284. package/dist/ui/Banner.js +2 -0
  285. package/dist/ui/Breadcrumb.d.ts +7 -0
  286. package/dist/ui/Breadcrumb.js +25 -0
  287. package/dist/ui/Button.d.ts +15 -0
  288. package/dist/ui/Button.js +32 -0
  289. package/dist/ui/Card.d.ts +12 -0
  290. package/dist/ui/Card.js +40 -0
  291. package/dist/ui/Checkbox.d.ts +2 -0
  292. package/dist/ui/Checkbox.js +2 -0
  293. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  294. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  295. package/dist/ui/Dialog.d.ts +13 -0
  296. package/dist/ui/Dialog.js +85 -0
  297. package/dist/ui/EmptyState.d.ts +2 -0
  298. package/dist/ui/EmptyState.js +2 -0
  299. package/dist/ui/Feedback.d.ts +24 -0
  300. package/dist/ui/Feedback.js +40 -0
  301. package/dist/ui/Inputs.d.ts +34 -0
  302. package/dist/ui/Inputs.js +225 -0
  303. package/dist/ui/NumberInput.d.ts +2 -0
  304. package/dist/ui/NumberInput.js +2 -0
  305. package/dist/ui/Range.d.ts +2 -0
  306. package/dist/ui/Range.js +2 -0
  307. package/dist/ui/SearchInput.d.ts +2 -0
  308. package/dist/ui/SearchInput.js +2 -0
  309. package/dist/ui/SectionCard.d.ts +2 -0
  310. package/dist/ui/SectionCard.js +2 -0
  311. package/dist/ui/Select.d.ts +2 -0
  312. package/dist/ui/Select.js +2 -0
  313. package/dist/ui/StatusChip.d.ts +2 -0
  314. package/dist/ui/StatusChip.js +2 -0
  315. package/dist/ui/Tabs.d.ts +16 -0
  316. package/dist/ui/Tabs.js +61 -0
  317. package/dist/ui/TextInput.d.ts +2 -0
  318. package/dist/ui/TextInput.js +2 -0
  319. package/dist/ui/Textarea.d.ts +2 -0
  320. package/dist/ui/Textarea.js +2 -0
  321. package/dist/ui/Toggle.d.ts +2 -0
  322. package/dist/ui/Toggle.js +2 -0
  323. package/dist/ui/accent.d.ts +40 -0
  324. package/dist/ui/accent.js +139 -0
  325. package/dist/ui/index.d.ts +9 -0
  326. package/dist/ui/index.js +9 -0
  327. package/dist/ui/ui.css +92 -0
  328. package/package.json +5 -2
  329. package/dist/admin/components/chipsInput.css +0 -11
  330. package/dist/admin/results/ArmTable.d.ts +0 -8
  331. package/dist/admin/results/ArmTable.js +0 -113
  332. package/dist/admin/results/VerdictCard.d.ts +0 -9
  333. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  334. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  335. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  336. package/dist/admin/surface/test/TestHeader.js +0 -70
@@ -0,0 +1,34 @@
1
+ import type { Arm } from '../../types/contracts.js';
2
+ import type { ArmSlot } from '../../types/puck.js';
3
+ import type { EditorExperiment } from './editor.model.js';
4
+ export declare function useArmWrites(url: string, publish: (doc: EditorExperiment) => void, isCurrent: () => boolean, onWrite: () => void): {
5
+ write: (transform: (arms: Arm[]) => Arm[]) => Promise<Arm[] | undefined>;
6
+ busy: boolean;
7
+ error: string | null;
8
+ };
9
+ export type ArmWrites = ReturnType<typeof useArmWrites>;
10
+ export declare function AddVariant({ experiment, writes, onSelect }: {
11
+ experiment: EditorExperiment;
12
+ writes: ArmWrites;
13
+ onSelect: (slot: ArmSlot) => void;
14
+ }): import("react").JSX.Element | null;
15
+ export declare function VersionActions({ arm, writes, editable, open, onOpenChange, canCopy, onCopy, onRequestRemove }: {
16
+ arm: EditorExperiment['arms'][number];
17
+ writes: ArmWrites;
18
+ editable: boolean;
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ canCopy: boolean;
22
+ onCopy: (slot: ArmSlot) => void;
23
+ onRequestRemove: () => void;
24
+ }): import("react").JSX.Element;
25
+ export declare function RemoveVersion({ arm, writes, onRemove, onClose }: {
26
+ arm: EditorExperiment['arms'][number];
27
+ writes: ArmWrites;
28
+ onRemove: (slot: ArmSlot) => void;
29
+ onClose: () => void;
30
+ }): import("react").JSX.Element;
31
+ export declare function VersionWeights({ experiment, writes }: {
32
+ experiment: EditorExperiment;
33
+ writes: ArmWrites;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,317 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from 'react';
4
+ import { addVariant, removeArm, balanceWeight, splitEvenly, totalWeight, withSlotsFor } from '../../arms/model.js';
5
+ import { checkArms } from '../../collections/experiments/armInvariants.js';
6
+ import { issueMessage } from '../../arms/copy.js';
7
+ import { Button } from '../../ui/Button.js';
8
+ import { Dialog } from '../../ui/Dialog.js';
9
+ import { NumberInput, TextInput } from '../../ui/Inputs.js';
10
+ import { ArmWriteError, writeArms } from './armWrites.js';
11
+ import { editorCopy as C } from './copy.js';
12
+ export function useArmWrites(url, publish, isCurrent, onWrite) {
13
+ const guard = useRef(false);
14
+ const [busy, setBusy] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ async function write(transform) {
17
+ if (guard.current) return;
18
+ guard.current = true;
19
+ onWrite();
20
+ setBusy(true);
21
+ setError(null);
22
+ try {
23
+ return await writeArms(url, transform, publish);
24
+ } catch (error) {
25
+ if (isCurrent()) setError(error instanceof ArmWriteError ? error.message : C.writeNetwork);
26
+ } finally{
27
+ guard.current = false;
28
+ setBusy(false);
29
+ }
30
+ }
31
+ return {
32
+ write,
33
+ busy,
34
+ error
35
+ };
36
+ }
37
+ export function AddVariant({ experiment, writes, onSelect }) {
38
+ if (experiment.arms.filter((arm)=>arm.kind === 'variant').length >= 3) return null;
39
+ return /*#__PURE__*/ _jsx(Button, {
40
+ disabled: writes.busy,
41
+ onClick: async ()=>{
42
+ let added;
43
+ const saved = await writes.write((arms)=>{
44
+ const next = addVariant(arms, 'block');
45
+ added = next.find((arm)=>!arms.some((existing)=>existing.key === arm.key))?.key;
46
+ return next;
47
+ });
48
+ if (saved && added) onSelect(added);
49
+ },
50
+ children: C.add
51
+ });
52
+ }
53
+ export function VersionActions({ arm, writes, editable, open, onOpenChange, canCopy, onCopy, onRequestRemove }) {
54
+ const id = useId();
55
+ const [name, setName] = useState(null);
56
+ const [invalid, setInvalid] = useState(false);
57
+ const trigger = useRef(null);
58
+ const focusTrigger = ()=>trigger.current?.querySelector('button')?.focus();
59
+ const restoreFocus = useRef(false);
60
+ useEffect(()=>{
61
+ if (restoreFocus.current && !writes.busy) {
62
+ restoreFocus.current = false;
63
+ focusTrigger();
64
+ }
65
+ }, [
66
+ writes.busy,
67
+ name
68
+ ]);
69
+ useEffect(()=>{
70
+ if (!open) return;
71
+ const node = trigger.current;
72
+ const doc = node?.ownerDocument;
73
+ const outside = (event)=>{
74
+ if (node && !event.composedPath().includes(node)) onOpenChange(false);
75
+ };
76
+ // Focus leaving the canvas iframe (Puck's own panels) or the window fires window blur, not a document pointerdown.
77
+ const leave = ()=>onOpenChange(false);
78
+ const view = doc?.defaultView;
79
+ doc?.addEventListener('pointerdown', outside);
80
+ view?.addEventListener('blur', leave);
81
+ return ()=>{
82
+ doc?.removeEventListener('pointerdown', outside);
83
+ view?.removeEventListener('blur', leave);
84
+ };
85
+ }, [
86
+ open,
87
+ onOpenChange
88
+ ]);
89
+ const closeMenu = ()=>{
90
+ onOpenChange(false);
91
+ focusTrigger();
92
+ };
93
+ const slot = arm.key;
94
+ async function rename() {
95
+ if (!name?.trim()) {
96
+ setInvalid(true);
97
+ return;
98
+ }
99
+ const label = name.trim();
100
+ const saved = await writes.write((arms)=>arms.map((current)=>current.key === arm.key ? {
101
+ ...current,
102
+ label
103
+ } : current));
104
+ if (saved) {
105
+ restoreFocus.current = true;
106
+ setName(null);
107
+ }
108
+ }
109
+ return /*#__PURE__*/ _jsxs("div", {
110
+ ref: trigger,
111
+ className: "xp-slot-version-actions",
112
+ onBlur: (event)=>{
113
+ // Safari/Firefox may blur a keyboard-focused trigger without focusing the clicked item.
114
+ // Outside pointerdown handles real outside clicks, including non-focusable targets.
115
+ if (event.relatedTarget !== null && !event.currentTarget.contains(event.relatedTarget)) onOpenChange(false);
116
+ },
117
+ onKeyDown: (event)=>{
118
+ if (event.key === 'Escape' && !writes.busy) {
119
+ event.preventDefault();
120
+ closeMenu();
121
+ setName(null);
122
+ }
123
+ },
124
+ children: [
125
+ /*#__PURE__*/ _jsxs("div", {
126
+ className: "xp-slot-version-anchor",
127
+ children: [
128
+ editable || arm.kind !== 'control' ? /*#__PURE__*/ _jsx(Button, {
129
+ size: "sm",
130
+ variant: "quiet",
131
+ "aria-label": C.menu(arm.label),
132
+ "aria-expanded": open,
133
+ "aria-controls": `${id}-menu`,
134
+ disabled: writes.busy,
135
+ onClick: ()=>onOpenChange(!open),
136
+ children: "⋯"
137
+ }) : null,
138
+ open ? /*#__PURE__*/ _jsxs("div", {
139
+ id: `${id}-menu`,
140
+ className: "xp-slot-version-menu",
141
+ role: "group",
142
+ tabIndex: -1,
143
+ "aria-label": C.menu(arm.label),
144
+ children: [
145
+ editable ? /*#__PURE__*/ _jsx(Button, {
146
+ variant: "quiet",
147
+ size: "sm",
148
+ onClick: ()=>{
149
+ setName(arm.label);
150
+ setInvalid(false);
151
+ closeMenu();
152
+ },
153
+ children: C.rename
154
+ }) : null,
155
+ arm.kind !== 'control' ? /*#__PURE__*/ _jsxs(_Fragment, {
156
+ children: [
157
+ /*#__PURE__*/ _jsx(Button, {
158
+ variant: "quiet",
159
+ size: "sm",
160
+ disabled: !canCopy,
161
+ onClick: ()=>{
162
+ closeMenu();
163
+ onCopy(slot);
164
+ },
165
+ children: C.copy
166
+ }),
167
+ editable ? /*#__PURE__*/ _jsx(Button, {
168
+ variant: "quiet",
169
+ size: "sm",
170
+ onClick: ()=>{
171
+ closeMenu();
172
+ onRequestRemove();
173
+ },
174
+ children: C.remove
175
+ }) : null
176
+ ]
177
+ }) : null
178
+ ]
179
+ }) : null
180
+ ]
181
+ }),
182
+ name !== null ? /*#__PURE__*/ _jsx("div", {
183
+ className: "xp-slot-version-rename",
184
+ children: /*#__PURE__*/ _jsx(TextInput, {
185
+ id: `${id}-name`,
186
+ label: C.renameLabel,
187
+ autoFocus: true,
188
+ value: name,
189
+ disabled: writes.busy,
190
+ error: invalid ? C.emptyLabel : undefined,
191
+ onChange: (event)=>{
192
+ setName(event.target.value);
193
+ setInvalid(false);
194
+ },
195
+ onKeyDown: (event)=>{
196
+ if (event.key === 'Enter') {
197
+ event.preventDefault();
198
+ void rename();
199
+ }
200
+ }
201
+ })
202
+ }) : null
203
+ ]
204
+ });
205
+ }
206
+ export function RemoveVersion({ arm, writes, onRemove, onClose }) {
207
+ const slot = arm.key;
208
+ const [tried, setTried] = useState(false);
209
+ return /*#__PURE__*/ _jsx(Dialog, {
210
+ open: true,
211
+ title: C.removeTitle(arm.label),
212
+ initialFocus: "cancel",
213
+ dismissible: !writes.busy,
214
+ onClose: ()=>onClose(),
215
+ children: /*#__PURE__*/ _jsxs("div", {
216
+ className: "xp-slot-drawer",
217
+ children: [
218
+ /*#__PURE__*/ _jsx("p", {
219
+ children: C.removeHelp
220
+ }),
221
+ tried && writes.error ? /*#__PURE__*/ _jsx("p", {
222
+ role: "alert",
223
+ className: "xp-ui-error",
224
+ children: writes.error
225
+ }) : null,
226
+ /*#__PURE__*/ _jsxs("div", {
227
+ className: "xp-ui-button-group",
228
+ children: [
229
+ /*#__PURE__*/ _jsx(Button, {
230
+ "data-dialog-cancel": true,
231
+ disabled: writes.busy,
232
+ onClick: ()=>onClose(),
233
+ children: C.cancel
234
+ }),
235
+ /*#__PURE__*/ _jsx(Button, {
236
+ variant: "danger",
237
+ busy: writes.busy,
238
+ onClick: async ()=>{
239
+ setTried(true);
240
+ const saved = await writes.write((arms)=>splitEvenly(removeArm(arms, arm.key)));
241
+ if (saved) {
242
+ onRemove(slot);
243
+ onClose();
244
+ }
245
+ },
246
+ children: C.remove
247
+ })
248
+ ]
249
+ })
250
+ ]
251
+ })
252
+ });
253
+ }
254
+ export function VersionWeights({ experiment, writes }) {
255
+ const id = useId();
256
+ const split = useRef(null);
257
+ const [edits, setEdits] = useState({});
258
+ const invalidKeys = new Set(Object.entries(edits).filter(([, raw])=>raw.trim() === '' || !Number.isInteger(Number(raw)) || Number(raw) < 0 || Number(raw) > 100).map(([key])=>key));
259
+ const apply = (arms)=>arms.reduce((next, arm)=>edits[arm.key] === undefined ? next : (()=>{
260
+ const value = edits[arm.key].trim() === '' ? NaN : Number(edits[arm.key]);
261
+ return invalidKeys.has(arm.key) ? next : balanceWeight(next, arm.key, value);
262
+ })(), arms);
263
+ const live = withSlotsFor(apply(experiment.arms), 'block');
264
+ const issues = checkArms(live, 'block');
265
+ async function commit() {
266
+ if (!Object.keys(edits).length || invalidKeys.size || issues.length) return;
267
+ const saved = await writes.write(apply);
268
+ if (saved) setEdits({});
269
+ }
270
+ return /*#__PURE__*/ _jsxs("div", {
271
+ className: "xp-slot-weights",
272
+ children: [
273
+ /*#__PURE__*/ _jsx("div", {
274
+ className: "xp-slot-weight-inputs",
275
+ children: experiment.arms.map((arm)=>/*#__PURE__*/ _jsx(NumberInput, {
276
+ id: `${id}-${arm.key}`,
277
+ label: C.weight(arm.label),
278
+ min: 0,
279
+ max: 100,
280
+ step: 1,
281
+ value: edits[arm.key] ?? live.find((current)=>current.key === arm.key)?.weight ?? '',
282
+ disabled: writes.busy,
283
+ error: invalidKeys.has(arm.key) ? C.invalidShare : undefined,
284
+ onChange: (event)=>setEdits({
285
+ [arm.key]: event.target.value
286
+ }),
287
+ onBlur: (event)=>{
288
+ if (!split.current?.contains(event.relatedTarget)) void commit();
289
+ },
290
+ onKeyDown: (event)=>{
291
+ if (event.key === 'Enter') {
292
+ event.preventDefault();
293
+ void commit();
294
+ }
295
+ }
296
+ }, arm.key))
297
+ }),
298
+ Object.keys(edits).length && !invalidKeys.size ? issues.map((issue, i)=>/*#__PURE__*/ _jsx("p", {
299
+ role: "alert",
300
+ className: "xp-ui-error",
301
+ children: issueMessage(issue, totalWeight(live))
302
+ }, i)) : null,
303
+ /*#__PURE__*/ _jsx("div", {
304
+ ref: split,
305
+ children: /*#__PURE__*/ _jsx(Button, {
306
+ variant: "quiet",
307
+ size: "sm",
308
+ disabled: writes.busy,
309
+ onClick: async ()=>{
310
+ if (await writes.write(splitEvenly)) setEdits({});
311
+ },
312
+ children: C.split
313
+ })
314
+ })
315
+ ]
316
+ });
317
+ }
@@ -0,0 +1,7 @@
1
+ import type { Arm } from '../../types/contracts.js';
2
+ import type { EditorExperiment } from './editor.model.js';
3
+ export declare class ArmWriteError extends Error {
4
+ }
5
+ export declare function readExperiment(url: string, signal?: AbortSignal): Promise<EditorExperiment>;
6
+ /** Each intent is applied to a fresh document, guarded by the collection's compare-and-set header. */
7
+ export declare function writeArms(url: string, transform: (arms: Arm[]) => Arm[], publish: (doc: EditorExperiment) => void): Promise<Arm[]>;
@@ -0,0 +1,71 @@
1
+ import { checkArms } from '../../collections/experiments/armInvariants.js';
2
+ import { issueMessage } from '../../arms/copy.js';
3
+ import { totalWeight, withSlotsFor } from '../../arms/model.js';
4
+ import { XP_IF_UPDATED_AT_HEADER } from '../../constants.js';
5
+ import { editorCopy, writeRefusal } from './copy.js';
6
+ export class ArmWriteError extends Error {
7
+ }
8
+ export async function readExperiment(url, signal) {
9
+ const response = await fetch(url, {
10
+ credentials: 'include',
11
+ cache: 'no-store',
12
+ ...signal ? {
13
+ signal
14
+ } : {}
15
+ });
16
+ if (!response.ok) throw new ArmWriteError(writeRefusal(undefined, response.status));
17
+ const doc = await response.json().catch(()=>undefined);
18
+ if (!doc || typeof doc !== 'object') throw new ArmWriteError(editorCopy.writeUnreadable);
19
+ const value = doc;
20
+ if (typeof value.name !== 'string' || typeof value.status !== 'string' || typeof value.updatedAt !== 'string' || !value.updatedAt || !Array.isArray(value.arms) || !value.arms.length || value.arms.some((arm)=>!arm || typeof arm !== 'object' || !Object.hasOwn(editorCopy.labels, arm.key) && arm.key !== 'holdout' || ![
21
+ 'control',
22
+ 'variant',
23
+ 'holdout'
24
+ ].includes(arm.kind) || typeof arm.label !== 'string' || typeof arm.weight !== 'number' || !Number.isFinite(arm.weight))) throw new ArmWriteError(editorCopy.writeUnreadable);
25
+ if (![
26
+ 'draft',
27
+ 'stopped',
28
+ 'running',
29
+ 'decided',
30
+ 'archived'
31
+ ].includes(value.status)) throw new ArmWriteError(editorCopy.lockedGeneric);
32
+ return value;
33
+ }
34
+ /** Each intent is applied to a fresh document, guarded by the collection's compare-and-set header. */ export async function writeArms(url, transform, publish) {
35
+ const fresh = await readExperiment(`${url}?depth=0`);
36
+ publish(fresh);
37
+ if (fresh.status !== 'draft' && fresh.status !== 'stopped') throw new ArmWriteError(editorCopy.locked[fresh.status]);
38
+ if (!fresh.updatedAt || !Array.isArray(fresh.arms)) throw new ArmWriteError(editorCopy.writeUnreadable);
39
+ const arms = withSlotsFor(transform(fresh.arms), 'block');
40
+ const issue = checkArms(arms, 'block')[0];
41
+ if (issue) throw new ArmWriteError(issueMessage(issue, totalWeight(arms)));
42
+ if (arms.some((arm)=>!arm.label.trim())) throw new ArmWriteError(editorCopy.emptyLabel);
43
+ const response = await fetch(url, {
44
+ method: 'PATCH',
45
+ credentials: 'include',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ [XP_IF_UPDATED_AT_HEADER]: fresh.updatedAt
49
+ },
50
+ body: JSON.stringify({
51
+ arms
52
+ })
53
+ });
54
+ const body = await response.json().catch(()=>undefined);
55
+ if (!response.ok) {
56
+ const codes = Array.isArray(body?.errors) ? body.errors : [];
57
+ const stale = response.status === 409 && codes.some((error)=>error?.code === 'stale-edit');
58
+ if (stale) {
59
+ try {
60
+ publish(await readExperiment(`${url}?depth=0`));
61
+ } catch {}
62
+ }
63
+ throw new ArmWriteError(stale ? editorCopy.staleEdit : writeRefusal(codes[0]?.code, response.status));
64
+ }
65
+ // A successful PATCH is applied even when its response body is unavailable.
66
+ publish(body?.doc ?? {
67
+ ...fresh,
68
+ arms
69
+ });
70
+ return arms;
71
+ }
@@ -0,0 +1,14 @@
1
+ import type { DocId } from '../../types/contracts.js';
2
+ import type { EditorExperiment } from './editor.model.js';
3
+ type Arms = {
4
+ arms: string[];
5
+ };
6
+ /** Only the first mounted block discards a document left by an earlier editor. */
7
+ export declare function acquireArms(testId: DocId): () => void;
8
+ /** Share the request, but abort it only after its last mounted consumer leaves. */
9
+ export declare function loadArms(testId: DocId, load: (signal: AbortSignal) => Promise<EditorExperiment>, signal: AbortSignal): Promise<EditorExperiment>;
10
+ export declare function subscribe(listener: () => void): () => void;
11
+ /** The editor's document fetch is the only producer; undefined means unknown. */
12
+ export declare function setArms(testId: DocId, arms: string[] | undefined): void;
13
+ export declare function useArms(testId: DocId | undefined): Arms | undefined;
14
+ export {};
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+ import { useSyncExternalStore } from 'react';
3
+ const cache = new Map();
4
+ const listeners = new Set();
5
+ const pending = new Map();
6
+ const mounted = new Map();
7
+ /** Only the first mounted block discards a document left by an earlier editor. */ export function acquireArms(testId) {
8
+ const key = String(testId);
9
+ const count = mounted.get(key) ?? 0;
10
+ mounted.set(key, count + 1);
11
+ if (count === 0 && !pending.has(key)) setArms(testId, undefined);
12
+ let released = false;
13
+ return ()=>{
14
+ if (released) return;
15
+ released = true;
16
+ const count = (mounted.get(key) ?? 1) - 1;
17
+ if (count === 0) mounted.delete(key);
18
+ else mounted.set(key, count);
19
+ };
20
+ }
21
+ /** Share the request, but abort it only after its last mounted consumer leaves. */ export function loadArms(testId, load, signal) {
22
+ const key = String(testId);
23
+ let request = pending.get(key);
24
+ if (!request) {
25
+ const controller = new AbortController();
26
+ request = {
27
+ controller,
28
+ consumers: [],
29
+ promise: undefined
30
+ };
31
+ const entry = request;
32
+ entry.promise = load(controller.signal).then((doc)=>{
33
+ if (!controller.signal.aborted) setArms(testId, doc.arms.map((arm)=>arm.key));
34
+ return doc;
35
+ }, (error)=>{
36
+ if (!controller.signal.aborted) setArms(testId, undefined);
37
+ throw error;
38
+ }).finally(()=>{
39
+ if (pending.get(key) === entry) pending.delete(key);
40
+ });
41
+ pending.set(key, entry);
42
+ }
43
+ const entry = request;
44
+ if (!entry.consumers.includes(signal)) entry.consumers.push(signal);
45
+ const release = ()=>{
46
+ entry.consumers = entry.consumers.filter((consumer)=>consumer !== signal);
47
+ if (!entry.consumers.length) {
48
+ entry.controller.abort();
49
+ if (pending.get(key) === entry) pending.delete(key);
50
+ }
51
+ };
52
+ signal.addEventListener('abort', release, {
53
+ once: true
54
+ });
55
+ return entry.promise.finally(()=>{
56
+ signal.removeEventListener('abort', release);
57
+ entry.consumers = entry.consumers.filter((consumer)=>consumer !== signal);
58
+ });
59
+ }
60
+ export function subscribe(listener) {
61
+ listeners.add(listener);
62
+ return ()=>{
63
+ listeners.delete(listener);
64
+ };
65
+ }
66
+ /** The editor's document fetch is the only producer; undefined means unknown. */ export function setArms(testId, arms) {
67
+ if (arms) cache.set(String(testId), {
68
+ arms: [
69
+ ...arms
70
+ ]
71
+ });
72
+ else cache.delete(String(testId));
73
+ for (const listener of listeners)listener();
74
+ }
75
+ const unknown = ()=>undefined;
76
+ export function useArms(testId) {
77
+ return useSyncExternalStore(subscribe, ()=>testId === undefined ? undefined : cache.get(String(testId)), unknown);
78
+ }
@@ -4,13 +4,15 @@ export declare const editorCopy: {
4
4
  readonly variantA: "Variant A";
5
5
  readonly variantB: "Variant B";
6
6
  readonly variantC: "Variant C";
7
+ readonly holdout: "Holdout";
7
8
  };
9
+ readonly notSetUp: "Not set up";
8
10
  readonly unbound: "Choose or create a test to show different versions.";
9
11
  readonly draft: "This block shows the Original to everyone until the test starts.";
10
12
  readonly running: "Changing this content mid-test mixes two versions in one result";
11
13
  readonly decided: (winner: string) => string;
12
14
  readonly archived: "This test is archived; the winning content is shown to everyone.";
13
- readonly copy: "Copy Original to this version";
15
+ readonly copy: "Copy Original here";
14
16
  readonly dropZone: (slot: "control" | "variantA" | "variantB" | "variantC") => string;
15
17
  readonly replaceConfirm: (label: string) => string;
16
18
  readonly replace: "Replace";
@@ -24,5 +26,35 @@ export declare const editorCopy: {
24
26
  readonly createError: "Couldn't create the test. Try again.";
25
27
  readonly loading: "Loading test…";
26
28
  readonly loadError: "Couldn't load this test. Try selecting it again.";
29
+ readonly orphan: (label: string) => string;
27
30
  readonly tabs: "Test versions";
31
+ readonly add: "Add variant";
32
+ readonly menu: (label: string) => string;
33
+ readonly rename: "Rename";
34
+ readonly renameLabel: "Version name";
35
+ readonly emptyLabel: "Enter a name for this version.";
36
+ readonly remove: "Remove";
37
+ readonly removeTitle: (label: string) => string;
38
+ readonly removeHelp: "This removes the version from the test and clears its content from this page.";
39
+ readonly removed: "Save the page to finish removing this version's content.";
40
+ readonly removeUncleared: "The version was removed from the test, but its content couldn't be cleared here. Delete it from the outline, then save the page.";
41
+ readonly lockedGeneric: "This test's versions can't change. View the test page for details.";
42
+ readonly holdoutPeople: (label: string, count: string) => string;
43
+ readonly weight: (label: string) => string;
44
+ readonly invalidShare: "Use a whole number from 0 to 100.";
45
+ readonly split: "Split evenly";
46
+ readonly locked: {
47
+ readonly running: "This test is running, so its versions can't change. Stop it on the test page to edit them.";
48
+ readonly decided: "This test has a decision, so its versions can't change. View the results on the test page.";
49
+ readonly archived: "This test is archived, so its versions can't change. View the results on the test page.";
50
+ };
51
+ readonly staleEdit: "Someone else changed this test. Reload to see their changes.";
52
+ readonly writeError: "Couldn't complete this change. Try again.";
53
+ readonly writeNetwork: "Couldn't reach the server. Check your connection and try again.";
54
+ readonly writeUnreadable: "Couldn't read the server's response. Reload and try again.";
55
+ readonly collecting: "Collecting";
56
+ readonly resultsUnavailable: "Results aren't available here.";
57
+ readonly refresh: "Refresh";
58
+ readonly people: (label: string, count: string) => string;
28
59
  };
60
+ export declare function writeRefusal(code: string | undefined, status: number): string;
@@ -1,16 +1,13 @@
1
+ import { ARM_LABELS } from '../../labels/arms.js';
1
2
  export const editorCopy = {
2
- labels: {
3
- control: 'Original',
4
- variantA: 'Variant A',
5
- variantB: 'Variant B',
6
- variantC: 'Variant C'
7
- },
3
+ labels: ARM_LABELS,
4
+ notSetUp: 'Not set up',
8
5
  unbound: 'Choose or create a test to show different versions.',
9
6
  draft: 'This block shows the Original to everyone until the test starts.',
10
7
  running: 'Changing this content mid-test mixes two versions in one result',
11
8
  decided: (winner)=>`Decided: ${winner}. Use 'Make it the content' on the test to finish.`,
12
9
  archived: 'This test is archived; the winning content is shown to everyone.',
13
- copy: 'Copy Original to this version',
10
+ copy: 'Copy Original here',
14
11
  dropZone: (slot)=>slot === 'control' ? 'Drop the original content here' : `Drop ${editorCopy.labels[slot]} content here`,
15
12
  replaceConfirm: (label)=>`${label} already has content. Replace it with a copy of the Original?`,
16
13
  replace: 'Replace',
@@ -24,5 +21,45 @@ export const editorCopy = {
24
21
  createError: "Couldn't create the test. Try again.",
25
22
  loading: 'Loading test…',
26
23
  loadError: "Couldn't load this test. Try selecting it again.",
27
- tabs: 'Test versions'
24
+ orphan: (label)=>`${label} has content but isn't part of this test. Move or delete it; visitors never see it.`,
25
+ tabs: 'Test versions',
26
+ add: 'Add variant',
27
+ menu: (label)=>`Options for ${label}`,
28
+ rename: 'Rename',
29
+ renameLabel: 'Version name',
30
+ emptyLabel: 'Enter a name for this version.',
31
+ remove: 'Remove',
32
+ removeTitle: (label)=>`Remove ${label}?`,
33
+ removeHelp: 'This removes the version from the test and clears its content from this page.',
34
+ removed: "Save the page to finish removing this version's content.",
35
+ removeUncleared: "The version was removed from the test, but its content couldn't be cleared here. Delete it from the outline, then save the page.",
36
+ lockedGeneric: "This test's versions can't change. View the test page for details.",
37
+ holdoutPeople: (label, count)=>`${label}: ${count} people (not compared)`,
38
+ weight: (label)=>`${label} traffic (%)`,
39
+ invalidShare: 'Use a whole number from 0 to 100.',
40
+ split: 'Split evenly',
41
+ locked: {
42
+ running: "This test is running, so its versions can't change. Stop it on the test page to edit them.",
43
+ decided: "This test has a decision, so its versions can't change. View the results on the test page.",
44
+ archived: "This test is archived, so its versions can't change. View the results on the test page."
45
+ },
46
+ staleEdit: 'Someone else changed this test. Reload to see their changes.',
47
+ writeError: "Couldn't complete this change. Try again.",
48
+ writeNetwork: "Couldn't reach the server. Check your connection and try again.",
49
+ writeUnreadable: "Couldn't read the server's response. Reload and try again.",
50
+ collecting: 'Collecting',
51
+ resultsUnavailable: "Results aren't available here.",
52
+ refresh: 'Refresh',
53
+ people: (label, count)=>`${label}: ${count} people`
28
54
  };
55
+ // Minimal editor-side equivalent of admin/copy/common.ts: the editor graph cannot import admin.
56
+ // Never accept server text as a fallback.
57
+ export function writeRefusal(code, status) {
58
+ if (code === 'stale-edit') return editorCopy.staleEdit;
59
+ if (code === 'unauthenticated' || status === 401) return 'Your session has expired. Sign in again.';
60
+ if (code === 'not-found' || status === 404) return 'This test no longer exists.';
61
+ if (code === 'commit-unverified') return 'The change may not have been saved. Reload and try again.';
62
+ if (code === 'not-implemented' || status === 501) return "This isn't available yet.";
63
+ if (code === 'forbidden' || status === 403) return "You don't have permission to do this.";
64
+ return editorCopy.writeError;
65
+ }
@@ -4,4 +4,4 @@ import { newTestBody } from './editor.model.js';
4
4
  export declare class CreateRefused extends Error {
5
5
  }
6
6
  /** Shared lookup, complete draft creation and one retry for a racing slug collision. */
7
- export declare function createTest(apiRoute: string, input: Omit<Parameters<typeof newTestBody>[0], 'slug'>): Promise<ExperimentRef>;
7
+ export declare function createTest(api: string, input: Omit<Parameters<typeof newTestBody>[0], 'slug'>): Promise<ExperimentRef>;