@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
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useState } from 'react';
4
+ import { TextInput, NumberInput, Textarea, Select, SearchInput, CheckboxList as UICheckboxList } from '../../../ui/Inputs.js';
4
5
  import { TEST_PAGE as C } from '../../copy/testPage.js';
5
6
  import { EDITABLE_BY_STATUS } from '../../../collections/experiments/editability.js';
6
7
  import { lockedReason } from '../../lockedNotice.model.js';
@@ -21,13 +22,19 @@ export function FieldErrors({ test, path }) {
21
22
  }, key))
22
23
  });
23
24
  }
24
- export function ReadOnly({ label, value, reason }) {
25
+ export function ReadOnly({ label, value, reason, labelAction }) {
25
26
  return /*#__PURE__*/ _jsxs("div", {
26
27
  className: "xp-read-only",
27
28
  title: reason ?? undefined,
28
29
  children: [
29
- /*#__PURE__*/ _jsx("strong", {
30
- children: label
30
+ /*#__PURE__*/ _jsxs("div", {
31
+ className: "xp-ui-field-label",
32
+ children: [
33
+ /*#__PURE__*/ _jsx("strong", {
34
+ children: label
35
+ }),
36
+ labelAction
37
+ ]
31
38
  }),
32
39
  /*#__PURE__*/ _jsx("p", {
33
40
  children: value
@@ -38,7 +45,7 @@ export function ReadOnly({ label, value, reason }) {
38
45
  ]
39
46
  });
40
47
  }
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 }) {
48
+ /** 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, hideLabel = false, disabled = false, help }) {
42
49
  const id = useId();
43
50
  const [draft, setDraft] = useState(String(value));
44
51
  const [error, setError] = useState(null);
@@ -55,111 +62,103 @@ export function ReadOnly({ label, value, reason }) {
55
62
  }
56
63
  onCommit(draft);
57
64
  };
58
- if (reason) return /*#__PURE__*/ _jsx(ReadOnly, {
65
+ if (reason && !disabled) return /*#__PURE__*/ _jsx(ReadOnly, {
59
66
  label: label,
60
67
  value: value === '' && placeholder ? placeholder : value,
61
68
  reason: reason
62
69
  });
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
- ]
70
+ const Input = multiline ? Textarea : type === 'number' ? NumberInput : TextInput;
71
+ return /*#__PURE__*/ _jsx("div", {
72
+ className: hideLabel ? 'xp-grid-field' : undefined,
73
+ title: reason ?? undefined,
74
+ children: /*#__PURE__*/ _jsx(Input, {
75
+ id: id,
76
+ label: label,
77
+ "aria-label": label,
78
+ value: draft,
79
+ disabled: disabled,
80
+ placeholder: placeholder,
81
+ min: min,
82
+ max: max,
83
+ required: required,
84
+ help: help,
85
+ error: error ?? undefined,
86
+ onChange: (e)=>{
87
+ setError(null);
88
+ setDraft(e.target.value);
89
+ onDraft?.(e.target.value);
90
+ },
91
+ onBlur: commit,
92
+ onKeyDown: (e)=>{
93
+ if (!multiline && e.key === 'Enter') e.currentTarget.blur();
94
+ }
95
+ })
110
96
  });
111
97
  }
112
98
  export const positiveInteger = (raw)=>raw.trim() !== '' && Number.isInteger(Number(raw)) && Number(raw) > 0;
113
- export function SelectEdit({ label, value, options, reason, onCommit, readOnlyValue }) {
99
+ export function SelectEdit({ label, value, options, reason, onCommit, readOnlyValue, labelAction }) {
114
100
  const id = useId();
115
101
  if (reason) {
116
102
  return /*#__PURE__*/ _jsx(ReadOnly, {
117
103
  label: label,
118
104
  reason: reason,
105
+ labelAction: labelAction,
119
106
  value: readOnlyValue ?? options.find((o)=>o.value === value)?.label ?? value
120
107
  });
121
108
  }
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
- ]
109
+ return /*#__PURE__*/ _jsx(Select, {
110
+ id: id,
111
+ label: label,
112
+ labelAction: labelAction,
113
+ value: value,
114
+ onChange: (e)=>onCommit(e.target.value),
115
+ children: options.map((option)=>/*#__PURE__*/ _jsx("option", {
116
+ value: option.value,
117
+ children: option.label
118
+ }, option.value))
139
119
  });
140
120
  }
141
- export function CheckboxList({ label, value, options, reason, onToggle, readOnlyValue }) {
121
+ export function CheckboxList({ label, value, options, reason, onToggle, readOnlyValue, searchable = false }) {
122
+ const id = useId();
123
+ const [query, setQuery] = useState('');
124
+ const visible = searchable ? options.filter((option)=>value.includes(option.value) || option.label.toLowerCase().includes(query.toLowerCase())) : options;
142
125
  if (reason) return /*#__PURE__*/ _jsx(ReadOnly, {
143
126
  label: label,
144
127
  reason: reason,
145
128
  value: readOnlyValue ?? value.map((v)=>options.find((o)=>o.value === v)?.label ?? v).join(', ')
146
129
  });
147
- return /*#__PURE__*/ _jsxs("fieldset", {
148
- className: "xp-field",
130
+ return /*#__PURE__*/ _jsx(UICheckboxList, {
131
+ id: id,
132
+ label: label,
133
+ value: value,
134
+ options: visible,
135
+ onChange: (next)=>{
136
+ const changed = options.find((option)=>next.includes(option.value) !== value.includes(option.value));
137
+ if (changed) onToggle(changed.value, next.includes(changed.value));
138
+ },
139
+ children: searchable ? /*#__PURE__*/ _jsx(SearchInput, {
140
+ id: `${id}-search`,
141
+ label: C.searchGoals,
142
+ value: query,
143
+ onChange: (event)=>setQuery(event.target.value)
144
+ }) : null
145
+ });
146
+ }
147
+ export function TestName({ test }) {
148
+ return /*#__PURE__*/ _jsxs(_Fragment, {
149
149
  children: [
150
- /*#__PURE__*/ _jsx("legend", {
151
- children: label
150
+ /*#__PURE__*/ _jsx(TextEdit, {
151
+ label: C.name,
152
+ value: test.doc?.name ?? '',
153
+ reason: reasonFor(test, 'name'),
154
+ onCommit: (value)=>save(test, 'name', ()=>({
155
+ name: value
156
+ }))
152
157
  }),
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))
158
+ /*#__PURE__*/ _jsx(FieldErrors, {
159
+ test: test,
160
+ path: "name"
161
+ })
163
162
  ]
164
163
  });
165
164
  }
@@ -0,0 +1,3 @@
1
+ import type { SectionId } from '../contracts.js';
2
+ /** Open disclosures before scrolling, then give keyboard users a meaningful landing. */
3
+ export declare function jumpToSection(section: SectionId): void;
@@ -0,0 +1,10 @@
1
+ /** Open disclosures before scrolling, then give keyboard users a meaningful landing. */ export function jumpToSection(section) {
2
+ const target = document.getElementById(`section-${section}`);
3
+ if (target instanceof HTMLDetailsElement) target.open = true;
4
+ target?.scrollIntoView?.({
5
+ block: 'start'
6
+ });
7
+ document.getElementById(`title-${section}`)?.focus({
8
+ preventScroll: true
9
+ });
10
+ }
@@ -1,17 +1,23 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { TEST_PAGE as C } from '../../copy/testPage.js';
4
4
  import { pageTargetIssue, setPageTarget, showsPageTarget } from '../../arms/pageTarget.model.js';
5
5
  import { FieldErrors, reasonFor, save, TextEdit } from './fields.js';
6
6
  function VersionContent({ test, armIndex }) {
7
7
  const arm = test.doc?.arms[armIndex];
8
8
  if (!arm || !test.doc) return null;
9
- if (!showsPageTarget(test.doc.kind, arm.kind)) return /*#__PURE__*/ _jsx("p", {
9
+ if (!showsPageTarget(test.doc.kind, arm.kind)) return /*#__PURE__*/ _jsx("span", {
10
+ className: "xp-content-chip",
10
11
  children: C.holdoutContent
11
12
  });
12
13
  const issue = pageTargetIssue(arm.pageTarget, arm.kind);
13
- return /*#__PURE__*/ _jsxs(_Fragment, {
14
+ return /*#__PURE__*/ _jsxs("div", {
15
+ className: "xp-version-content",
14
16
  children: [
17
+ /*#__PURE__*/ _jsx("span", {
18
+ className: `xp-content-chip ${issue ? 'xp-content-empty' : 'xp-content-ready'}`,
19
+ children: issue ? C.addPageAddress : C.pageReady
20
+ }),
15
21
  /*#__PURE__*/ _jsx(TextEdit, {
16
22
  label: `${arm.label}: ${C.pageAddress}`,
17
23
  value: arm.pageTarget ?? '',
@@ -1,23 +1,90 @@
1
1
  .xp-test-page { color: var(--xp-text); background: var(--xp-bg); max-width: 90rem; margin: 0 auto; padding: 1.5rem; }
2
+ .xp-test-page--setup { min-width: 0; padding: 0 0 160px; }
2
3
  .xp-test-header { position: sticky; top: 0; z-index: 10; background: var(--xp-bg); padding: 1rem 0; border-bottom: 1px solid var(--xp-border); }
3
4
  .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(--xp-border-control); border-radius: 1rem; padding: .25rem .75rem; }
5
+ .xp-kind-chip, .xp-status-pill { border: 1px solid var(--xp-border-strong); border-radius: 1rem; padding: .25rem .75rem; }
5
6
  .xp-status-pill--success, .xp-content-ready, .xp-section-check--done { color: var(--xp-success-text); }
6
7
  .xp-content-empty, .xp-status-pill--warning { color: var(--xp-warning-text); }
7
- .xp-test-section { padding-block: 1.5rem; border-bottom: 1px solid var(--xp-border); 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(--xp-border); border-radius: var(--xp-radius); 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(--xp-surface-raised); margin-block: 1.5rem; overflow: hidden; border-radius: var(--xp-radius); }
8
+ .xp-test-section, .xp-test-advanced { scroll-margin-top: var(--xp-space-6); }
9
+ .xp-version-rows { display: grid; grid-template-columns: minmax(0, 1.3fr) var(--xp-version-share-width) minmax(var(--xp-version-state-width), 1fr) var(--xp-version-remove-width); gap: var(--xp-space-3); align-items: center; }
10
+ .xp-version-row, .xp-version-grid-head { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; gap: var(--xp-space-3); align-items: center; }
11
+ .xp-version-grid-head { margin-bottom: var(--xp-space-2); font: var(--xp-text-small); color: var(--xp-text-2); }
12
+ .xp-version-row > .xp-field-error, .xp-version-row > [role=alert] { grid-column: 1 / -1; }
13
+ .xp-version-remove { grid-column: 4; }
14
+ .xp-grid-field .xp-ui-field { margin: 0; }
15
+ .xp-grid-field .xp-ui-field > label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
16
+ .xp-version-content { display: flex; flex-wrap: nowrap; gap: var(--xp-space-2); align-items: center; min-width: 0; }
17
+ .xp-version-content .xp-ui-button { white-space: nowrap; }
18
+ .xp-content-chip { flex-shrink: 0; display: inline-flex; width: fit-content; border-radius: var(--xp-radius-chip); padding: var(--xp-space-half) var(--xp-space-2); font: var(--xp-text-small); background: var(--xp-surface-2); color: var(--xp-text-2); }
19
+ .xp-content-chip.xp-content-ready { background: var(--xp-success-bg); color: var(--xp-success); }
20
+ .xp-content-chip.xp-content-empty { background: var(--xp-warning-bg); color: var(--xp-warning); }
21
+ .xp-traffic-bar { display: flex; height: var(--xp-space-2); gap: var(--xp-space-half); margin-block: var(--xp-space-5) var(--xp-space-3); overflow: hidden; border-radius: var(--xp-space-1); }
12
22
  .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(--xp-text); background: var(--xp-input-bg); border: 1px solid var(--xp-border-control); border-radius: var(--xp-radius); padding: .65rem; }
15
- .xp-field textarea { min-height: 6rem; resize: vertical; }
16
- .xp-field input[type=range] { padding: 0; }
23
+ .xp-split-legend { display: flex; flex-wrap: wrap; gap: var(--xp-space-4); font: var(--xp-text-small); color: var(--xp-text-2); }
24
+ .xp-split-legend i { display: inline-block; width: var(--xp-legend-key-size); height: var(--xp-legend-key-size); border-radius: var(--xp-legend-key-radius); margin-right: var(--xp-space-one-half); }
25
+ .xp-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--xp-space-4); }
26
+ .xp-test-advanced .xp-field-grid { padding-top: var(--xp-space-4); }
27
+ .xp-field-grid > div { min-width: 0; }
28
+ .xp-field-grid p, .xp-page-binding p { color: var(--xp-text-2); font: var(--xp-text-small); }
29
+ .xp-read-only code { font-family: var(--xp-mono); overflow-wrap: anywhere; }
30
+ .xp-ui-checkbox-list { display: flex; flex-wrap: wrap; gap: var(--xp-space-2) var(--xp-space-4); }
31
+ .xp-duration-suggestion { align-items: center; margin-top: var(--xp-space-4); }
17
32
  .xp-read-only { padding-block: .5rem; }
18
33
  .xp-read-only p { margin-block: .5rem; }
19
34
  .xp-field-error { color: var(--xp-error-text); }
20
- .xp-test-page button { cursor: pointer; color: var(--xp-text); background: var(--xp-surface-raised); border: 1px solid var(--xp-border-control); padding: .65rem 1rem; border-radius: var(--xp-radius); 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(--xp-border); padding-bottom: 1rem; }
35
+
36
+ .xp-targeting-row { display: grid; grid-template-columns: var(--xp-targeting-attribute-width) var(--xp-targeting-comparison-width) minmax(0, 1fr); align-items: start; gap: var(--xp-space-3); border: 1px solid var(--xp-border); border-radius: var(--xp-radius-control); padding: var(--xp-space-4); margin-bottom: var(--xp-space-4); }
37
+ .xp-targeting-row > .xp-ui-button-group, .xp-targeting-row > p { grid-column: 1 / -1; }
38
+ @media (max-width: 720px) {
39
+ .xp-version-rows { grid-template-columns: minmax(0, 1fr) var(--xp-version-share-width-mobile); }
40
+ .xp-version-content { flex-wrap: wrap; }
41
+ .xp-version-remove:empty { display: none; }
42
+ .xp-version-content, .xp-setting-values, .xp-version-remove { grid-column: 1 / -1; }
43
+ .xp-version-grid-head > :nth-child(n+3) { display: none; }
44
+ .xp-targeting-row, .xp-field-grid { grid-template-columns: 1fr; }
45
+ }
46
+
47
+ .xp-test-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); align-items: start; gap: var(--xp-space-8); }
48
+ .xp-test-content { display: grid; gap: var(--xp-space-8); min-width: 0; max-width: 760px; }
49
+ .xp-test-rail { position: sticky; top: var(--xp-space-6); min-width: 0; }
50
+ .xp-test-rail-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--xp-space-1); }
51
+ .xp-test-rail-item { display: flex; align-items: center; gap: var(--xp-space-2); padding: var(--xp-space-2); border-radius: var(--xp-radius-control); color: var(--xp-text-2); text-decoration: none; font: var(--xp-text-small); }
52
+ .xp-test-rail-item:hover, .xp-test-rail-item.is-active { background: var(--xp-surface-2); color: var(--xp-text); }
53
+ .xp-test-rail-item.is-active { font-weight: 600; }
54
+ .xp-test-rail-item--advanced { margin-top: var(--xp-space-3); color: var(--xp-text-3); }
55
+ .xp-test-rail-mark { flex: none; color: var(--xp-text-3); }
56
+ .xp-test-rail-mark--done { color: var(--xp-success); }
57
+ .xp-test-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
58
+ .xp-test-rail-item:focus-visible, .xp-test-advanced > summary:focus-visible, .xp-test-content h2:focus { outline: 2px solid var(--xp-focus); outline-offset: 2px; }
59
+ .xp-test-advanced { border: 1px solid var(--xp-border); border-radius: var(--xp-radius-card); background: var(--xp-surface); }
60
+ .xp-test-advanced > summary { cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: var(--xp-space-3); padding: var(--xp-space-4) var(--xp-space-5); color: var(--xp-text-2); }
61
+ .xp-test-advanced > summary h2 { margin: 0; font: var(--xp-text-body); }
62
+ .xp-test-advanced[open] > summary { border-bottom: 1px solid var(--xp-border); }
63
+ .xp-test-readiness-bar { position: fixed; bottom: 0; inset-inline: 0; z-index: 20; background: var(--xp-surface); border-top: 1px solid var(--xp-border); }
64
+ .xp-readiness { color: var(--xp-text); background: var(--xp-surface); padding: var(--xp-space-4); border: 1px solid var(--xp-border-strong); border-radius: var(--xp-radius-control); }
65
+ .xp-readiness--ready:not(.xp-readiness--bar) { background: var(--xp-success-bg); }
66
+ .xp-readiness--bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--xp-space-2) var(--xp-space-4); padding-block: var(--xp-space-3); min-height: 64px; border: 0; border-radius: 0; }
67
+ .xp-readiness__state { display: flex; align-items: center; flex-wrap: wrap; gap: var(--xp-space-2); min-width: 0; }
68
+ .xp-readiness__state p, .xp-readiness--bar > p { margin: 0; }
69
+ .xp-readiness__state > p { font-weight: 500; }
70
+ .xp-readiness--bar .xp-save-status, .xp-readiness__separator { color: var(--xp-text-3); font: var(--xp-text-small); }
71
+ .xp-readiness__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--xp-space-2); }
72
+ .xp-readiness--bar > .xp-readiness__actions { grid-column: 2; grid-row: 1 / span 3; justify-content: flex-end; }
73
+ @media (max-width: 960px) {
74
+ .xp-test-layout { grid-template-columns: minmax(0, 1fr); gap: var(--xp-space-5); }
75
+ .xp-test-rail { top: 0; z-index: 2; background: var(--xp-bg); padding-block: var(--xp-space-2); border-bottom: 1px solid var(--xp-border); }
76
+ .xp-test-rail-list { flex-direction: row; overflow-x: auto; }
77
+ .xp-test-rail-item { white-space: nowrap; }
78
+ .xp-test-rail-item--advanced { margin-top: 0; }
79
+ .xp-test-content { max-width: none; }
80
+ .xp-test-section, .xp-test-advanced { scroll-margin-top: 64px; }
81
+ }
82
+ @media (max-width: 720px) {
83
+ .xp-readiness--bar { grid-template-columns: minmax(0, 1fr); }
84
+ /* The sentence wraps under the save state here, so a trailing separator would dangle. */
85
+ .xp-readiness--bar .xp-readiness__separator { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
86
+ .xp-readiness--bar > .xp-readiness__actions { grid-column: 1; grid-row: auto; justify-content: flex-start; }
87
+ }
88
+
89
+ .xp-ui-checkbox-list > .xp-ui-field:has(input[type="search"]) { flex-basis: 100%; }
90
+ .xp-targeting-row .xp-ui-chips { margin-block: 0; }
@@ -1,4 +1,4 @@
1
- import type { Arm, ArmKey, ExperimentKind, ExposurePolicy } from '../../../types/contracts.js';
1
+ import type { Arm, ExperimentKind, ExposurePolicy } from '../../../types/contracts.js';
2
2
  import type { PreflightResult } from '../../../types/endpoints.js';
3
3
  export declare function orderedVersions(arms: readonly Arm[]): Array<{
4
4
  arm: Arm;
@@ -13,5 +13,4 @@ export declare function durationSuggestion(preflight: PreflightResult | null, sa
13
13
  feasible: boolean;
14
14
  } | null;
15
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[];
16
+ export { balanceWeight } from '../../../arms/model.js';
@@ -1,4 +1,3 @@
1
- import { setWeight } from '../../arms/arms.model.js';
2
1
  const ORDER = [
3
2
  'control',
4
3
  'variantA',
@@ -31,23 +30,4 @@ export function exposureOptions(kind) {
31
30
  'manual'
32
31
  ];
33
32
  }
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
- }
33
+ export { balanceWeight } from '../../../arms/model.js';
@@ -155,11 +155,13 @@ function stage(session, partial) {
155
155
  }
156
156
  session.publish();
157
157
  }
158
- async function readDocument() {
158
+ async function readDocument(forSave = false) {
159
159
  const response = await fetch(url, {
160
160
  method: 'GET',
161
161
  credentials: 'include',
162
- signal: controller.signal
162
+ ...forSave ? {} : {
163
+ signal: controller.signal
164
+ }
163
165
  });
164
166
  if (!response.ok) throw new Error(API_MESSAGES.unreadable);
165
167
  const doc = await response.json();
@@ -181,6 +183,10 @@ function stage(session, partial) {
181
183
  let failed = false;
182
184
  try {
183
185
  if (!session.saved) return;
186
+ if (typeof session.saved.updatedAt !== 'string' || !session.saved.updatedAt) {
187
+ session.saved = await readDocument(true);
188
+ if (typeof session.saved.updatedAt !== 'string' || !session.saved.updatedAt) throw new Error(API_MESSAGES.unreadable);
189
+ }
184
190
  const patch = changedPatch(session.saved, queued);
185
191
  changed = Object.keys(patch).length > 0;
186
192
  if (changed) {
@@ -209,8 +215,9 @@ function stage(session, partial) {
209
215
  ...edit
210
216
  }), {});
211
217
  session.queue = [];
218
+ // Server text is never shown; the stale-edit sentence is the admin's own.
212
219
  session.view.errors = {
213
- _section: staleError.message || TEST_PAGE.staleEdit
220
+ _section: TEST_PAGE.staleEdit
214
221
  };
215
222
  } else if (!response.ok) session.view.errors = {
216
223
  ...session.view.errors,
@@ -221,8 +228,8 @@ function stage(session, partial) {
221
228
  const doc = body?.doc;
222
229
  const readable = doc && doc.id != null && doc.status;
223
230
  // A 2xx has applied even if its response body was lost. Preserve it while re-reading.
224
- // The old timestamp is no longer a valid baseline. Omit the header until a
225
- // readable PATCH or GET restores it, including when this read is aborted.
231
+ // The old timestamp is no longer a valid baseline. The next queued save
232
+ // must recover it with a GET before sending another PATCH.
226
233
  session.saved = readable ? doc : {
227
234
  ...session.saved,
228
235
  ...patch,
@@ -1,6 +1,7 @@
1
1
  import type { TargetingRule } from '../../types/contracts.js';
2
2
  import type { SerializedAttributeKeys } from '../../types/admin.js';
3
3
  import type { TargetingOp } from '../../targeting/ops.js';
4
+ export { readListValue } from '../../targeting/values.js';
4
5
  type Attribute = SerializedAttributeKeys[string];
5
6
  export type AttributeType = Attribute['type'];
6
7
  export declare function opsFor(type?: AttributeType): readonly TargetingOp[];
@@ -13,11 +14,8 @@ export declare function valueInput(attribute?: Attribute, op?: TargetingOp): {
13
14
  }[];
14
15
  };
15
16
  export declare function coerceValue(type: AttributeType, op: TargetingOp, raw: unknown): string | number | boolean | string[] | null;
16
- /** Only the read boundary accepts legacy comma-separated document values. */
17
- export declare function readListValue(raw: unknown): string[];
18
17
  /** Normalize the read boundary before any whole-group edit. */
19
18
  export declare function readTargetingRules(rules: TargetingRule[]): TargetingRule[];
20
19
  export declare function rowPath(path: string): string;
21
20
  export declare function emptyListHint(op: TargetingOp, value: unknown): string | undefined;
22
21
  export declare function humaniseAttribute(key: string): string;
23
- export {};
@@ -1,5 +1,7 @@
1
1
  import { TARGETING_OPS, isOpValid } from '../../targeting/ops.js';
2
+ import { coerceListValue, readListValue } from '../../targeting/values.js';
2
3
  import { NUMBER_OP_LABELS, STRING_OP_LABELS, AUDIENCE_COPY } from '../copy/audience.js';
4
+ export { readListValue } from '../../targeting/values.js';
3
5
  export function opsFor(type) {
4
6
  return type ? TARGETING_OPS[type] : [];
5
7
  }
@@ -27,11 +29,7 @@ export function valueInput(attribute, op = 'eq') {
27
29
  export function coerceValue(type, op, raw) {
28
30
  if (!isOpValid(type, op)) return null;
29
31
  if (type === 'string') {
30
- if (op === 'in' || op === 'nin') return [
31
- ...new Set((Array.isArray(raw) ? raw.map(String) : [
32
- String(raw ?? '')
33
- ]).map((value)=>value.trim()).filter(Boolean))
34
- ];
32
+ if (op === 'in' || op === 'nin') return coerceListValue(raw);
35
33
  return raw == null ? '' : String(raw);
36
34
  }
37
35
  if (type === 'number') {
@@ -41,9 +39,6 @@ export function coerceValue(type, op, raw) {
41
39
  }
42
40
  return raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
43
41
  }
44
- /** Only the read boundary accepts legacy comma-separated document values. */ export function readListValue(raw) {
45
- return coerceValue('string', 'in', typeof raw === 'string' ? raw.split(',') : raw);
46
- }
47
42
  /** Normalize the read boundary before any whole-group edit. */ export function readTargetingRules(rules) {
48
43
  return rules.map((rule)=>rule.op === 'in' || rule.op === 'nin' ? {
49
44
  ...rule,