@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,13 +1,24 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { createUsePuck, registerOverlayPortal, useGetPuck } from '@puckeditor/core';
4
- import { useEffect, useId, useState } from 'react';
5
- import { pickerCopy } from '../copy.js';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Button } from '../../ui/Button.js';
6
+ import { StatusChip } from '../../ui/StatusChip.js';
7
+ import { Banner } from '../../ui/Banner.js';
8
+ import { Tabs } from '../../ui/Tabs.js';
9
+ import { accentVariables, deriveAccent, validateAccent } from '../../ui/accent.js';
10
+ import { createExperimentSlotField } from '../experimentSlotField.js';
11
+ import { resolvePuckRoutes } from '../routes.js';
12
+ import { PuckHostProvider, usePuckRoutes } from '../hostContext.js';
13
+ import { pickerCopy, slotLabels } from '../copy.js';
6
14
  import { experimentSlotDefaultProps, experimentSlotFields, experimentSlotLabel } from '../slotConfig.js';
7
15
  import { bannerFor, tabsFor } from './editor.model.js';
8
16
  import { cloneSlot } from './cloneSlot.js';
9
17
  import { CreateExperimentDrawer } from './CreateExperimentDrawer.js';
10
18
  import { editorCopy } from './copy.js';
19
+ import { acquireArms, loadArms, setArms, useArms } from './armsCache.js';
20
+ import { AddVariant, RemoveVersion, VersionActions, VersionWeights, useArmWrites } from './VersionBuilder.js';
21
+ import { ResultsReadout } from './ResultsReadout.js';
11
22
  import './editor.css';
12
23
  const usePuckSelector = createUsePuck();
13
24
  // Public Puck registration enables native controls inside the editor canvas.
@@ -25,53 +36,141 @@ function isExperiment(value) {
25
36
  }
26
37
  function EditorRender(props) {
27
38
  const getPuck = useGetPuck();
28
- const tabId = useId();
39
+ const { admin, api } = usePuckRoutes();
40
+ const root = useRef(null);
41
+ const [mode, setMode] = useState('light');
42
+ useEffect(()=>{
43
+ const node = root.current;
44
+ if (!node || !props.accent) return;
45
+ const update = ()=>setMode(node.closest('[data-theme="dark"], .dark') ? 'dark' : 'light');
46
+ update();
47
+ const observer = new MutationObserver(update);
48
+ for(let ancestor = node.parentElement; ancestor; ancestor = ancestor.parentElement){
49
+ observer.observe(ancestor, {
50
+ attributes: true,
51
+ attributeFilter: [
52
+ 'class',
53
+ 'data-theme'
54
+ ]
55
+ });
56
+ }
57
+ return ()=>observer.disconnect();
58
+ }, [
59
+ props.accent
60
+ ]);
29
61
  const experimentId = props.experiment?.id;
62
+ const bindingKey = JSON.stringify([
63
+ api,
64
+ props.id,
65
+ experimentId
66
+ ]);
67
+ const binding = useRef({
68
+ key: bindingKey,
69
+ live: true
70
+ });
71
+ binding.current.key = bindingKey;
72
+ useEffect(()=>{
73
+ binding.current.live = true;
74
+ return ()=>{
75
+ binding.current.live = false;
76
+ };
77
+ }, []);
78
+ const isCurrentBinding = ()=>binding.current.live && binding.current.key === bindingKey;
79
+ const knownArms = useArms(experimentId);
30
80
  const [loaded, setLoaded] = useState();
31
81
  const [active, setActive] = useState();
82
+ const [openMenu, setOpenMenu] = useState(null);
83
+ const [clearError, setClearError] = useState(null);
32
84
  const [creating, setCreating] = useState(false);
33
85
  // A non-empty variant is only overwritten after an explicit second confirmation.
34
86
  const [confirming, setConfirming] = useState(null);
87
+ // Keep a failed removal open even if the recovery read no longer contains that version.
88
+ const [removing, setRemoving] = useState(null);
35
89
  const pageTitle = usePuckSelector((state)=>typeof state.appState.data.root.props?.title === 'string' ? state.appState.data.root.props.title : '');
36
90
  const selected = usePuckSelector((state)=>state.selectedItem?.props.id === props.id);
37
91
  const originalEmpty = usePuckSelector((state)=>{
38
92
  const content = state.getItemById(props.id)?.props.control;
39
93
  return !Array.isArray(content) || content.length === 0;
40
94
  });
95
+ const orphanSlots = usePuckSelector((state)=>{
96
+ const item = state.getItemById(props.id);
97
+ return [
98
+ 'variantA',
99
+ 'variantB',
100
+ 'variantC'
101
+ ].filter((slot)=>{
102
+ const content = item?.props[slot];
103
+ return knownArms && !knownArms.arms.includes(slot) && Array.isArray(content) && content.length > 0;
104
+ }).join(',');
105
+ }).split(',').filter(Boolean);
41
106
  useEffect(()=>{
42
107
  if (experimentId === undefined) return;
43
108
  const controller = new AbortController();
44
109
  let current = true;
110
+ let request = 0;
111
+ const releaseArms = acquireArms(experimentId);
45
112
  async function load() {
113
+ const revision = ++request;
46
114
  try {
47
- const response = await fetch(`/api/xp-experiments/${encodeURIComponent(experimentId)}?depth=0`, {
48
- credentials: 'include',
49
- signal: controller.signal
50
- });
51
- if (!response.ok) throw new Error('load');
52
- const doc = await response.json();
53
- if (!isExperiment(doc)) throw new Error('document');
54
- if (current) setLoaded({
55
- id: experimentId,
56
- doc
57
- });
115
+ const doc = await loadArms(experimentId, async (signal)=>{
116
+ const response = await fetch(`${api}/xp-experiments/${encodeURIComponent(experimentId)}?depth=0`, {
117
+ credentials: 'include',
118
+ signal
119
+ });
120
+ if (!response.ok) throw new Error('load');
121
+ const doc = await response.json();
122
+ if (!isExperiment(doc)) throw new Error('document');
123
+ return doc;
124
+ }, controller.signal);
125
+ if (current && revision === request) {
126
+ setLoaded({
127
+ id: experimentId,
128
+ doc
129
+ });
130
+ }
58
131
  } catch {
59
- if (current) setLoaded({
60
- id: experimentId,
61
- error: true
62
- });
132
+ if (current && revision === request) {
133
+ setLoaded({
134
+ id: experimentId,
135
+ error: true
136
+ });
137
+ }
63
138
  }
64
139
  }
65
140
  void load();
141
+ // The linked test opens in another tab; refresh this same document source on return.
142
+ const onVisible = ()=>{
143
+ if (document.visibilityState === 'visible') void load();
144
+ };
145
+ document.addEventListener('visibilitychange', onVisible);
66
146
  return ()=>{
67
147
  current = false;
68
148
  controller.abort();
149
+ releaseArms();
150
+ document.removeEventListener('visibilitychange', onVisible);
69
151
  };
70
152
  }, [
71
- experimentId
153
+ experimentId,
154
+ api
72
155
  ]);
73
156
  const result = experimentId === undefined || loaded?.id !== experimentId ? undefined : loaded;
74
157
  const experiment = result?.doc;
158
+ const editable = experiment?.status === 'draft' || experiment?.status === 'stopped';
159
+ const [removed, setRemoved] = useState(null);
160
+ useEffect(()=>{
161
+ // Dialog cleanup must close the native modal before an outside tab can receive focus.
162
+ if (removed?.key === bindingKey) root.current?.querySelector('[role="tab"]')?.focus();
163
+ }, [
164
+ removed,
165
+ bindingKey
166
+ ]);
167
+ const writes = useArmWrites(`${api}/xp-experiments/${encodeURIComponent(String(experimentId))}`, (doc)=>{
168
+ setArms(experimentId, doc.arms.map((arm)=>arm.key));
169
+ if (isCurrentBinding()) setLoaded({
170
+ id: experimentId,
171
+ doc
172
+ });
173
+ }, isCurrentBinding, ()=>setClearError(null));
75
174
  const tabs = tabsFor(experiment);
76
175
  const activeSlot = active && active.id === experimentId ? active.slot : undefined;
77
176
  const slot = tabs.find((tab)=>tab.slot === activeSlot)?.slot ?? tabs[0]?.slot ?? 'control';
@@ -107,6 +206,39 @@ function EditorRender(props) {
107
206
  }
108
207
  });
109
208
  }
209
+ function removeContent(slot) {
210
+ if (!isCurrentBinding()) return;
211
+ const { getItemById, dispatch, getSelectorForId } = getPuck();
212
+ const item = getItemById(props.id), selector = getSelectorForId(props.id);
213
+ if (!item || !selector) {
214
+ setClearError({
215
+ key: bindingKey,
216
+ message: editorCopy.removeUncleared
217
+ });
218
+ return;
219
+ }
220
+ setClearError(null);
221
+ dispatch({
222
+ type: 'replace',
223
+ destinationIndex: selector.index,
224
+ destinationZone: selector.zone,
225
+ data: {
226
+ ...item,
227
+ props: {
228
+ ...item.props,
229
+ [slot]: []
230
+ }
231
+ }
232
+ });
233
+ setRemoved((current)=>({
234
+ key: bindingKey,
235
+ revision: (current?.revision ?? 0) + 1
236
+ }));
237
+ setActive({
238
+ id: experimentId,
239
+ slot: 'control'
240
+ });
241
+ }
110
242
  function bind(experiment) {
111
243
  const { getItemById, dispatch, getSelectorForId } = getPuck();
112
244
  const item = getItemById(props.id), selector = getSelectorForId(props.id);
@@ -125,7 +257,9 @@ function EditorRender(props) {
125
257
  });
126
258
  }
127
259
  return /*#__PURE__*/ _jsxs("section", {
260
+ ref: root,
128
261
  className: "xp-slot-root xp-slot-editor",
262
+ style: props.accent ? accentVariables(props.accent[mode]) : undefined,
129
263
  children: [
130
264
  /*#__PURE__*/ _jsxs("div", {
131
265
  className: "xp-slot-chrome",
@@ -136,57 +270,84 @@ function EditorRender(props) {
136
270
  children: [
137
271
  /*#__PURE__*/ _jsx("h3", {
138
272
  className: "xp-slot-title",
139
- children: experiment?.name ?? pickerCopy.label
273
+ children: experiment?.name ?? experimentSlotLabel
140
274
  }),
141
- experiment ? /*#__PURE__*/ _jsx("span", {
142
- className: "xp-slot-status",
143
- "data-status": experiment.status,
144
- children: pickerCopy.statuses[experiment.status]
145
- }) : null,
146
- /*#__PURE__*/ _jsxs("div", {
147
- className: "xp-slot-actions",
148
- children: [
149
- experimentId === undefined ? /*#__PURE__*/ _jsx("button", {
150
- className: "xp-slot-button xp-slot-button-primary",
151
- type: "button",
152
- disabled: !selected,
153
- onClick: ()=>setCreating(true),
154
- children: editorCopy.createLabel
155
- }) : null,
156
- experiment ? /*#__PURE__*/ _jsx("a", {
157
- className: "xp-slot-button xp-slot-button-primary",
158
- href: `/admin/experiments/${encodeURIComponent(String(experimentId))}${experiment.status === 'draft' ? '' : '/results'}`,
159
- target: "_blank",
160
- rel: "noopener noreferrer",
161
- children: experiment.status === 'draft' ? editorCopy.setup : editorCopy.viewResults
162
- }) : null
163
- ]
164
- })
275
+ experiment ? /*#__PURE__*/ _jsx(StatusChip, {
276
+ status: experiment.status
277
+ }) : experimentId === undefined ? /*#__PURE__*/ _jsx("span", {
278
+ className: "xp-ui-status",
279
+ children: editorCopy.notSetUp
280
+ }) : null
165
281
  ]
166
282
  }),
167
- /*#__PURE__*/ _jsxs("p", {
168
- className: experiment?.status === 'running' ? 'xp-slot-notice xp-slot-warning' : 'xp-slot-notice',
169
- role: "status",
283
+ /*#__PURE__*/ _jsx(Banner, {
284
+ tone: result?.error ? 'error' : experiment?.status === 'running' ? 'warning' : 'info',
285
+ children: experimentId === undefined ? bannerFor() : result?.error ? editorCopy.loadError : experiment ? bannerFor(experiment.status, winner) : editorCopy.loading
286
+ }),
287
+ orphanSlots.map((slot)=>/*#__PURE__*/ _jsx(Banner, {
288
+ tone: "warning",
289
+ children: editorCopy.orphan(slotLabels[slot])
290
+ }, slot)),
291
+ /*#__PURE__*/ _jsxs("div", {
292
+ className: "xp-ui-button-group",
170
293
  children: [
171
- experiment?.status === 'running' ? /*#__PURE__*/ _jsxs("svg", {
172
- className: "xp-slot-warning-icon",
173
- "aria-hidden": "true",
174
- viewBox: "0 0 24 24",
175
- fill: "none",
176
- stroke: "currentColor",
177
- strokeWidth: "2",
178
- children: [
179
- /*#__PURE__*/ _jsx("path", {
180
- d: "M12 3 2 21h20L12 3Z"
181
- }),
182
- /*#__PURE__*/ _jsx("path", {
183
- d: "M12 9v5m0 3v1"
184
- })
185
- ]
294
+ experimentId === undefined ? /*#__PURE__*/ _jsx(Button, {
295
+ variant: "primary",
296
+ disabled: !selected,
297
+ onClick: ()=>setCreating(true),
298
+ children: editorCopy.createLabel
299
+ }) : null,
300
+ experiment ? /*#__PURE__*/ _jsx(Button, {
301
+ href: `${admin}/experiments/${encodeURIComponent(String(experimentId))}${experiment.status === 'draft' ? '' : '/results'}`,
302
+ target: "_blank",
303
+ rel: "noopener noreferrer",
304
+ children: experiment.status === 'draft' ? editorCopy.setup : editorCopy.viewResults
186
305
  }) : null,
187
- experimentId === undefined ? bannerFor() : result?.error ? editorCopy.loadError : experiment ? bannerFor(experiment.status, winner) : editorCopy.loading
306
+ experiment && editable ? /*#__PURE__*/ _jsx(AddVariant, {
307
+ experiment: experiment,
308
+ writes: writes,
309
+ onSelect: (slot)=>{
310
+ if (isCurrentBinding()) setActive({
311
+ id: experimentId,
312
+ slot
313
+ });
314
+ }
315
+ }) : null
188
316
  ]
189
317
  }),
318
+ experiment && !editable ? /*#__PURE__*/ _jsx("p", {
319
+ className: "xp-ui-help",
320
+ children: editorCopy.locked[experiment.status]
321
+ }) : null,
322
+ experiment && editable ? /*#__PURE__*/ _jsx(VersionWeights, {
323
+ experiment: experiment,
324
+ writes: writes
325
+ }, `weights-${experimentId}`) : null,
326
+ writes.error ? /*#__PURE__*/ _jsx("p", {
327
+ role: "alert",
328
+ className: "xp-ui-error",
329
+ children: writes.error
330
+ }) : null,
331
+ clearError?.key === bindingKey ? /*#__PURE__*/ _jsx("p", {
332
+ role: "alert",
333
+ className: "xp-ui-error",
334
+ children: clearError.message
335
+ }) : null,
336
+ removed?.key === bindingKey ? /*#__PURE__*/ _jsx("p", {
337
+ className: "xp-ui-help",
338
+ children: editorCopy.removed
339
+ }) : null,
340
+ experiment && experiment.status !== 'draft' ? /*#__PURE__*/ _jsx(ResultsReadout, {
341
+ id: experimentId,
342
+ api: api,
343
+ experiment: experiment
344
+ }, `results-${experimentId}`) : null,
345
+ removing?.key === bindingKey ? /*#__PURE__*/ _jsx(RemoveVersion, {
346
+ arm: removing.arm,
347
+ writes: writes,
348
+ onRemove: removeContent,
349
+ onClose: ()=>setRemoving(null)
350
+ }) : null,
190
351
  creating ? /*#__PURE__*/ _jsx(CreateExperimentDrawer, {
191
352
  initialName: pageTitle,
192
353
  onCreate: bind,
@@ -197,96 +358,90 @@ function EditorRender(props) {
197
358
  role: "alert",
198
359
  children: [
199
360
  /*#__PURE__*/ _jsx("p", {
200
- className: "xp-slot-notice",
201
361
  children: editorCopy.replaceConfirm(tabs.find((tab)=>tab.slot === confirming)?.label ?? confirming)
202
362
  }),
203
363
  /*#__PURE__*/ _jsxs("div", {
204
- className: "xp-slot-actions",
364
+ className: "xp-ui-button-group",
205
365
  children: [
206
- /*#__PURE__*/ _jsx("button", {
207
- className: "xp-slot-button xp-slot-button-primary",
208
- type: "button",
366
+ /*#__PURE__*/ _jsx(Button, {
367
+ variant: "primary",
209
368
  onClick: ()=>copyTo(confirming),
210
369
  children: editorCopy.replace
211
370
  }),
212
- /*#__PURE__*/ _jsx("button", {
213
- className: "xp-slot-button",
214
- type: "button",
371
+ /*#__PURE__*/ _jsx(Button, {
215
372
  onClick: ()=>setConfirming(null),
216
373
  children: editorCopy.cancel
217
374
  })
218
375
  ]
219
376
  })
220
377
  ]
221
- }) : null,
222
- /*#__PURE__*/ _jsxs("div", {
223
- className: "xp-slot-toolbar",
224
- children: [
225
- /*#__PURE__*/ _jsx("div", {
226
- className: "xp-slot-tabs",
227
- role: "tablist",
228
- "aria-label": editorCopy.tabs,
229
- children: tabs.map((tab, index)=>/*#__PURE__*/ _jsx("button", {
230
- className: "xp-slot-tab",
231
- type: "button",
232
- role: "tab",
233
- id: `${tabId}-${tab.slot}`,
234
- "aria-selected": tab.slot === slot,
235
- tabIndex: tab.slot === slot ? 0 : -1,
236
- "aria-controls": `${tabId}-panel`,
237
- onClick: ()=>setActive({
238
- id: experimentId,
239
- slot: tab.slot
240
- }),
241
- onKeyDown: (event)=>{
242
- const next = event.key === 'ArrowRight' ? (index + 1) % tabs.length : event.key === 'ArrowLeft' ? (index + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : undefined;
243
- if (next === undefined) return;
244
- event.preventDefault();
245
- setActive({
246
- id: experimentId,
247
- slot: tabs[next].slot
248
- });
249
- const buttons = event.currentTarget.parentElement?.querySelectorAll('[role="tab"]');
250
- buttons?.[next]?.focus();
251
- },
252
- children: tab.label
253
- }, tab.slot))
254
- }),
255
- slot !== 'control' ? /*#__PURE__*/ _jsx("div", {
256
- className: "xp-slot-actions",
257
- children: /*#__PURE__*/ _jsx("button", {
258
- className: "xp-slot-button",
259
- type: "button",
260
- disabled: !selected || originalEmpty,
261
- onClick: ()=>requestCopy(slot),
262
- children: editorCopy.copy
263
- })
264
- }) : null
265
- ]
266
- })
267
- ]
268
- }),
269
- /*#__PURE__*/ _jsxs("div", {
270
- className: slotEmpty ? 'xp-slot-panel xp-slot-panel-empty' : 'xp-slot-panel',
271
- role: "tabpanel",
272
- id: `${tabId}-panel`,
273
- "aria-labelledby": `${tabId}-${slot}`,
274
- children: [
275
- /*#__PURE__*/ _jsx(Slot, {
276
- className: "xp-slot-drop-target"
277
- }),
278
- slotEmpty ? /*#__PURE__*/ _jsx("span", {
279
- className: "xp-slot-drop-label",
280
- children: editorCopy.dropZone(slot)
281
378
  }) : null
282
379
  ]
380
+ }),
381
+ /*#__PURE__*/ _jsx(Tabs, {
382
+ label: editorCopy.tabs,
383
+ value: slot,
384
+ tabListRef: registerControls,
385
+ onChange: (value)=>{
386
+ setOpenMenu(null);
387
+ setActive({
388
+ id: experimentId,
389
+ slot: value
390
+ });
391
+ },
392
+ items: tabs.map((tab)=>({
393
+ id: tab.slot,
394
+ label: tab.label,
395
+ actions: experiment ? /*#__PURE__*/ _jsx(VersionActions, {
396
+ arm: experiment.arms.find((arm)=>arm.key === tab.slot),
397
+ writes: writes,
398
+ editable: editable,
399
+ open: openMenu === tab.slot,
400
+ onOpenChange: (open)=>setOpenMenu(open ? tab.slot : null),
401
+ canCopy: selected && !originalEmpty,
402
+ onCopy: requestCopy,
403
+ onRequestRemove: ()=>setRemoving({
404
+ key: bindingKey,
405
+ arm: experiment.arms.find((arm)=>arm.key === tab.slot)
406
+ })
407
+ }, `${experimentId}-${tab.slot}`) : undefined,
408
+ content: tab.slot === slot ? /*#__PURE__*/ _jsxs("div", {
409
+ className: slotEmpty ? 'xp-slot-panel xp-slot-panel-empty' : 'xp-slot-panel',
410
+ children: [
411
+ /*#__PURE__*/ _jsx(Slot, {
412
+ className: "xp-slot-drop-target"
413
+ }),
414
+ slotEmpty ? /*#__PURE__*/ _jsx("span", {
415
+ className: "xp-slot-drop-label",
416
+ children: editorCopy.dropZone(slot)
417
+ }) : null
418
+ ]
419
+ }) : null
420
+ }))
283
421
  })
284
422
  ]
285
423
  });
286
424
  }
287
- export const ExperimentSlotEditor = {
288
- label: experimentSlotLabel,
289
- fields: experimentSlotFields,
290
- defaultProps: experimentSlotDefaultProps,
291
- render: EditorRender
292
- };
425
+ export function createExperimentSlotEditor({ routes: inputRoutes, accent } = {}) {
426
+ validateAccent(accent);
427
+ const sets = accent ? deriveAccent(accent) : undefined;
428
+ const routes = resolvePuckRoutes(inputRoutes);
429
+ return {
430
+ label: experimentSlotLabel,
431
+ fields: {
432
+ ...experimentSlotFields,
433
+ experiment: createExperimentSlotField({
434
+ routes
435
+ })
436
+ },
437
+ defaultProps: experimentSlotDefaultProps,
438
+ render: (props)=>/*#__PURE__*/ _jsx(PuckHostProvider, {
439
+ routes: routes,
440
+ children: /*#__PURE__*/ _jsx(EditorRender, {
441
+ ...props,
442
+ accent: sets
443
+ })
444
+ })
445
+ };
446
+ }
447
+ export const ExperimentSlotEditor = createExperimentSlotEditor();
@@ -0,0 +1,6 @@
1
+ import type { EditorExperiment } from './editor.model.js';
2
+ export declare function ResultsReadout({ id, api, experiment }: {
3
+ id: string | number;
4
+ api: string;
5
+ experiment: EditorExperiment;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,99 @@
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 { Button } from '../../ui/Button.js';
5
+ import { editorCopy as C } from './copy.js';
6
+ // Matches admin/results/format.ts formatCount; importing it would pull admin copy into this bundle.
7
+ const formatCount = (value)=>typeof value === 'number' && Number.isFinite(value) ? new Intl.NumberFormat('en-US', {
8
+ maximumFractionDigits: 0
9
+ }).format(value) : '—';
10
+ export function ResultsReadout({ id, api, experiment }) {
11
+ const [data, setData] = useState(null);
12
+ const [refresh, setRefresh] = useState(0);
13
+ const pending = useRef(false);
14
+ const [busy, setBusy] = useState(false);
15
+ useEffect(()=>{
16
+ const controller = new AbortController();
17
+ pending.current = true;
18
+ setBusy(true);
19
+ setData(null);
20
+ async function load() {
21
+ try {
22
+ const [summary, results] = await Promise.all([
23
+ fetch(`${api}/xp/experiments/summary?ids=${encodeURIComponent(id)}`, {
24
+ credentials: 'include',
25
+ cache: 'no-store',
26
+ signal: controller.signal
27
+ }),
28
+ fetch(`${api}/xp/experiments/${encodeURIComponent(id)}/results`, {
29
+ credentials: 'include',
30
+ cache: 'no-store',
31
+ signal: controller.signal
32
+ })
33
+ ]);
34
+ if (!summary.ok || !results.ok) throw new Error('unavailable');
35
+ const [summaries, result] = await Promise.all([
36
+ summary.json(),
37
+ results.json()
38
+ ]);
39
+ const entry = summaries[id];
40
+ if (!entry || !Object.hasOwn(entry, 'verdict') || !Object.hasOwn(result, 'snapshot')) throw new Error('unavailable');
41
+ if (!controller.signal.aborted) setData({
42
+ headline: entry.verdict?.headline ?? C.collecting,
43
+ people: result.snapshot?.exposures.perArm ?? null
44
+ });
45
+ } catch {
46
+ if (!controller.signal.aborted) setData('unavailable');
47
+ } finally{
48
+ if (!controller.signal.aborted) {
49
+ pending.current = false;
50
+ setBusy(false);
51
+ }
52
+ }
53
+ }
54
+ void load();
55
+ return ()=>controller.abort();
56
+ }, [
57
+ id,
58
+ api,
59
+ refresh
60
+ ]);
61
+ return /*#__PURE__*/ _jsxs("div", {
62
+ className: "xp-slot-readout",
63
+ children: [
64
+ data === 'unavailable' ? /*#__PURE__*/ _jsx("p", {
65
+ className: "xp-ui-help",
66
+ children: C.resultsUnavailable
67
+ }) : data ? /*#__PURE__*/ _jsxs(_Fragment, {
68
+ children: [
69
+ /*#__PURE__*/ _jsx("strong", {
70
+ children: data.headline
71
+ }),
72
+ data.people ? /*#__PURE__*/ _jsx("div", {
73
+ className: "xp-slot-people",
74
+ children: [
75
+ ...experiment.arms.filter((arm)=>arm.kind !== 'holdout'),
76
+ ...experiment.arms.filter((arm)=>arm.kind === 'holdout')
77
+ ].map((arm)=>/*#__PURE__*/ _jsx("span", {
78
+ children: (arm.kind === 'holdout' ? C.holdoutPeople : C.people)(arm.label, formatCount(data.people[arm.key] ?? 0))
79
+ }, arm.key))
80
+ }) : null
81
+ ]
82
+ }) : /*#__PURE__*/ _jsx("p", {
83
+ className: "xp-ui-help",
84
+ children: C.loading
85
+ }),
86
+ /*#__PURE__*/ _jsx(Button, {
87
+ variant: "quiet",
88
+ size: "sm",
89
+ disabled: busy,
90
+ onClick: ()=>{
91
+ if (pending.current) return;
92
+ pending.current = true;
93
+ setRefresh((value)=>value + 1);
94
+ },
95
+ children: C.refresh
96
+ })
97
+ ]
98
+ });
99
+ }