@delmaredigital/payload-experiments 0.2.0-rc.3 → 0.2.0-rc.5

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 (85) hide show
  1. package/README.md +22 -0
  2. package/dist/admin/PreviewLinks.js +8 -4
  3. package/dist/admin/TestsDashboardCards.d.ts +3 -0
  4. package/dist/admin/TestsDashboardCards.js +52 -0
  5. package/dist/admin/TestsNavLink.d.ts +3 -1
  6. package/dist/admin/TestsNavLink.js +62 -18
  7. package/dist/admin/actions/ConfirmDialog.d.ts +1 -0
  8. package/dist/admin/actions/ConfirmDialog.js +7 -5
  9. package/dist/admin/components/ChipsInput.d.ts +12 -0
  10. package/dist/admin/components/ChipsInput.js +96 -0
  11. package/dist/admin/components/RootViewMessage.d.ts +5 -0
  12. package/dist/admin/components/RootViewMessage.js +15 -0
  13. package/dist/admin/components/chipsInput.css +11 -0
  14. package/dist/admin/copy/audience.d.ts +4 -0
  15. package/dist/admin/copy/audience.js +4 -0
  16. package/dist/admin/copy/goals.d.ts +44 -0
  17. package/dist/admin/copy/goals.js +46 -0
  18. package/dist/admin/copy/review.d.ts +1 -0
  19. package/dist/admin/copy/review.js +1 -0
  20. package/dist/admin/copy/surface.d.ts +4 -0
  21. package/dist/admin/copy/surface.js +4 -0
  22. package/dist/admin/dashboard.css +35 -0
  23. package/dist/admin/paths.d.ts +1 -0
  24. package/dist/admin/paths.js +1 -0
  25. package/dist/admin/previewLinks.model.d.ts +9 -0
  26. package/dist/admin/previewLinks.model.js +34 -0
  27. package/dist/admin/results/ArmTable.js +2 -1
  28. package/dist/admin/results/CappedCell.js +2 -1
  29. package/dist/admin/results/CiChart.js +8 -7
  30. package/dist/admin/results/DataQualityPanel.js +3 -2
  31. package/dist/admin/results/VerdictCard.js +3 -2
  32. package/dist/admin/styles.css +8 -8
  33. package/dist/admin/styles.d.ts +1 -0
  34. package/dist/admin/styles.js +1 -0
  35. package/dist/admin/surface/SurfaceShell.d.ts +1 -0
  36. package/dist/admin/surface/SurfaceShell.js +1 -0
  37. package/dist/admin/surface/TestsHome.js +1 -1
  38. package/dist/admin/surface/TestsSurface.js +10 -1
  39. package/dist/admin/surface/goals/GoalForm.d.ts +7 -0
  40. package/dist/admin/surface/goals/GoalForm.js +470 -0
  41. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +5 -0
  42. package/dist/admin/surface/goals/GoalsBreadcrumb.js +47 -0
  43. package/dist/admin/surface/goals/GoalsList.d.ts +6 -0
  44. package/dist/admin/surface/goals/GoalsList.js +135 -0
  45. package/dist/admin/surface/goals/data.d.ts +3 -0
  46. package/dist/admin/surface/goals/data.js +24 -0
  47. package/dist/admin/surface/goals/goals.css +15 -0
  48. package/dist/admin/surface/goals/goals.model.d.ts +30 -0
  49. package/dist/admin/surface/goals/goals.model.js +26 -0
  50. package/dist/admin/surface/newTest/newTest.css +6 -6
  51. package/dist/admin/surface/readiness/Banner.js +5 -4
  52. package/dist/admin/surface/results/ResultsBody.js +3 -2
  53. package/dist/admin/surface/setting/setting.css +11 -11
  54. package/dist/admin/surface/surface.css +29 -29
  55. package/dist/admin/surface/test/GoalSection.js +32 -1
  56. package/dist/admin/surface/test/TargetingSection.js +27 -10
  57. package/dist/admin/surface/test/VersionsSection.js +6 -5
  58. package/dist/admin/surface/test/testPage.css +12 -12
  59. package/dist/admin/targeting/targeting.model.d.ts +6 -1
  60. package/dist/admin/targeting/targeting.model.js +18 -2
  61. package/dist/admin/theme.css +44 -0
  62. package/dist/admin/theme.d.ts +50 -0
  63. package/dist/admin/theme.js +49 -0
  64. package/dist/admin/views/ExperimentsRootView.js +18 -18
  65. package/dist/collections/events.js +1 -0
  66. package/dist/collections/identities.js +1 -0
  67. package/dist/collections/layers.js +1 -0
  68. package/dist/collections/metricValidation.d.ts +3 -0
  69. package/dist/collections/metricValidation.js +9 -0
  70. package/dist/collections/metrics.js +4 -5
  71. package/dist/collections/phases.js +1 -0
  72. package/dist/collections/snapshots.js +1 -0
  73. package/dist/exports/adminClient.d.ts +1 -0
  74. package/dist/exports/adminClient.js +1 -0
  75. package/dist/globals/state.js +1 -0
  76. package/dist/plugin.js +8 -2
  77. package/dist/puck/editor/CreateExperimentDrawer.d.ts +1 -0
  78. package/dist/puck/editor/CreateExperimentDrawer.js +34 -25
  79. package/dist/puck/editor/ExperimentSlotEditor.d.ts +1 -0
  80. package/dist/puck/editor/ExperimentSlotEditor.js +137 -78
  81. package/dist/puck/editor/copy.d.ts +3 -1
  82. package/dist/puck/editor/copy.js +3 -1
  83. package/dist/puck/editor/editor.css +302 -0
  84. package/dist/types/surface.d.ts +2 -1
  85. package/package.json +2 -1
package/README.md CHANGED
@@ -466,6 +466,13 @@ for these patterns.
466
466
 
467
467
  ## Admin and client
468
468
 
469
+ The sidebar includes a **Tests** group with **All tests** and **Goals**. The
470
+ dashboard also includes **Tests** and **Goals** cards above its widgets. Goals
471
+ appears only for users who can manage tests. Both additions follow the host's
472
+ admin route and keep host-provided navigation and dashboard entries first.
473
+ After upgrading, run `payload generate:importmap` to register the dashboard card
474
+ component.
475
+
469
476
  ### Client assignments and exposure
470
477
 
471
478
  Read assignments on the server and pass them to `ExperimentsProvider` from
@@ -530,6 +537,21 @@ components: { ExperimentSlot }
530
537
  components: { ExperimentSlot: ExperimentSlotEditor }
531
538
  ```
532
539
 
540
+ Load the editor stylesheet inside the Puck iframe. With Tailwind v4, add the
541
+ package import **after** Tailwind in the CSS that builds your editor iframe
542
+ stylesheet (the stylesheet listed in payload-puck's `editorStylesheets`):
543
+
544
+ ```css
545
+ @import "tailwindcss";
546
+ @import "@delmaredigital/payload-experiments/puck/editor.css";
547
+ ```
548
+
549
+ Alternatively, serve the exported `puck/editor.css` file from a public URL and
550
+ append that URL to payload-puck's `editorStylesheets`, after the host stylesheet.
551
+ The editor CSS includes its own light and dark palette, selected by
552
+ `data-theme="dark"` or the `dark` class on the iframe's `<html>`. The client component's CSS import alone
553
+ does not reach an iframe configured with `syncHostStyles: false`.
554
+
533
555
  Resolve the Puck data on the server with `resolveExperimentData` before
534
556
  rendering it. The render component shows only the selected group's slot. The
535
557
  editor lets you select a block experiment, edit each group's content, copy the
@@ -6,6 +6,7 @@ import { armLabel } from './labels.js';
6
6
  import { useApi } from './useApi.js';
7
7
  import { REVIEW_COPY as C } from './copy/review.js';
8
8
  import { API_MESSAGES } from './copy/common.js';
9
+ import { previewPages } from './previewLinks.model.js';
9
10
  export function PreviewLinks({ experimentId, placements, arms }) {
10
11
  const api = useApi();
11
12
  const inFlight = useRef(false);
@@ -39,10 +40,13 @@ export function PreviewLinks({ experimentId, placements, arms }) {
39
40
  return /*#__PURE__*/ _jsxs("div", {
40
41
  className: "xp-preview-links",
41
42
  children: [
42
- placements.map((placement)=>/*#__PURE__*/ _jsxs("div", {
43
+ previewPages(placements).map((placement)=>/*#__PURE__*/ _jsxs("div", {
43
44
  children: [
44
- /*#__PURE__*/ _jsx("p", {
45
- children: placement.title
45
+ /*#__PURE__*/ _jsxs("p", {
46
+ children: [
47
+ placement.title,
48
+ placement.slotCount > 1 ? ` ${C.placedTimes(placement.slotCount)}` : null
49
+ ]
46
50
  }),
47
51
  placement.url ? arms.filter((arm)=>arm.kind !== 'holdout' && arm.key !== 'holdout').map((arm)=>/*#__PURE__*/ _jsxs(Button, {
48
52
  disabled: busy,
@@ -59,7 +63,7 @@ export function PreviewLinks({ experimentId, placements, arms }) {
59
63
  children: C.noPreview
60
64
  })
61
65
  ]
62
- }, `${placement.collection}/${placement.id}`)),
66
+ }, placement.key)),
63
67
  error ? /*#__PURE__*/ _jsx("p", {
64
68
  role: "alert",
65
69
  children: error
@@ -0,0 +1,3 @@
1
+ import './theme.css';
2
+ import './dashboard.css';
3
+ export declare function TestsDashboardCards(): import("react").JSX.Element | null;
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import './theme.css';
4
+ import './dashboard.css';
5
+ import { Card, useAuth, useConfig } from '@payloadcms/ui';
6
+ import { formatAdminURL } from 'payload/shared';
7
+ import { SURFACE_COPY } from './copy/surface.js';
8
+ export function TestsDashboardCards() {
9
+ const { config } = useConfig();
10
+ const { permissions } = useAuth();
11
+ const testPermissions = permissions?.collections?.['xp-experiments'];
12
+ const canManage = testPermissions?.update === true;
13
+ if (!canManage) return null;
14
+ const href = (path)=>formatAdminURL({
15
+ adminRoute: config.routes.admin,
16
+ path
17
+ });
18
+ return /*#__PURE__*/ _jsxs("section", {
19
+ className: "xp-dash-group",
20
+ "aria-labelledby": "xp-dash-title",
21
+ children: [
22
+ /*#__PURE__*/ _jsx("h2", {
23
+ className: "xp-dash-label",
24
+ id: "xp-dash-title",
25
+ children: SURFACE_COPY.title
26
+ }),
27
+ /*#__PURE__*/ _jsxs("ul", {
28
+ className: "xp-dash-card-list",
29
+ children: [
30
+ /*#__PURE__*/ _jsx("li", {
31
+ children: /*#__PURE__*/ _jsx(Card, {
32
+ title: SURFACE_COPY.navAllTests,
33
+ titleAs: "h3",
34
+ buttonAriaLabel: SURFACE_COPY.dashShowTests,
35
+ href: href('/experiments'),
36
+ id: "card-xp-tests"
37
+ })
38
+ }),
39
+ /*#__PURE__*/ _jsx("li", {
40
+ children: /*#__PURE__*/ _jsx(Card, {
41
+ title: SURFACE_COPY.navGoals,
42
+ titleAs: "h3",
43
+ buttonAriaLabel: SURFACE_COPY.dashShowGoals,
44
+ href: href('/experiments/goals'),
45
+ id: "card-xp-goals"
46
+ })
47
+ })
48
+ ]
49
+ })
50
+ ]
51
+ });
52
+ }
@@ -1 +1,3 @@
1
- export declare function TestsNavLink(): import("react").JSX.Element;
1
+ import './theme.css';
2
+ declare function TestsNav(): import("react").JSX.Element | null;
3
+ export { TestsNav as TestsNavLink };
@@ -1,30 +1,74 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Link, useConfig } from '@payloadcms/ui';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import './theme.css';
4
+ import { Link, NavGroup, useAuth, useConfig } from '@payloadcms/ui';
4
5
  import { formatAdminURL } from 'payload/shared';
5
6
  import { usePathname } from 'next/navigation.js';
6
7
  import { SURFACE_COPY } from './copy/surface.js';
7
- export function TestsNavLink() {
8
+ function TestsNav() {
8
9
  const { config } = useConfig();
9
- const path = usePathname();
10
+ const { permissions } = useAuth();
11
+ const pathname = usePathname();
10
12
  const href = formatAdminURL({
11
13
  adminRoute: config.routes.admin,
12
14
  path: '/experiments'
13
15
  });
16
+ const goalsHref = formatAdminURL({
17
+ adminRoute: config.routes.admin,
18
+ path: '/experiments/goals'
19
+ });
14
20
  const base = href.replace(/\/$/, '');
15
- const active = path === base || path?.startsWith(`${base}/`);
16
- return /*#__PURE__*/ _jsxs(Link, {
17
- href: href,
18
- className: "nav__link",
19
- "aria-current": active ? 'page' : undefined,
20
- children: [
21
- active ? /*#__PURE__*/ _jsx("div", {
22
- className: "nav__link-indicator"
23
- }) : null,
24
- /*#__PURE__*/ _jsx("span", {
25
- className: "nav__link-label",
26
- children: SURFACE_COPY.title
27
- })
28
- ]
21
+ const goalsBase = goalsHref.replace(/\/$/, '');
22
+ const goalsActive = pathname === goalsBase || Boolean(pathname?.startsWith(`${goalsBase}/`));
23
+ const testsActive = !goalsActive && (pathname === base || Boolean(pathname?.startsWith(`${base}/`)));
24
+ const testPermissions = permissions?.collections?.['xp-experiments'];
25
+ const canManage = testPermissions?.update === true;
26
+ if (!canManage) return null;
27
+ const entries = [
28
+ {
29
+ id: 'nav-xp-tests',
30
+ href,
31
+ label: SURFACE_COPY.navAllTests,
32
+ active: testsActive
33
+ },
34
+ {
35
+ id: 'nav-xp-goals',
36
+ href: goalsHref,
37
+ label: SURFACE_COPY.navGoals,
38
+ active: goalsActive
39
+ }
40
+ ];
41
+ // getPreference is async and can fetch; let NavGroup default to open.
42
+ return /*#__PURE__*/ _jsx(NavGroup, {
43
+ label: SURFACE_COPY.title,
44
+ children: entries.map((entry)=>{
45
+ const props = {
46
+ className: 'nav__link',
47
+ id: entry.id,
48
+ 'aria-current': entry.active ? 'page' : undefined
49
+ };
50
+ const content = /*#__PURE__*/ _jsxs(_Fragment, {
51
+ children: [
52
+ entry.active ? /*#__PURE__*/ _jsx("div", {
53
+ className: "nav__link-indicator"
54
+ }) : null,
55
+ /*#__PURE__*/ _jsx("span", {
56
+ className: "nav__link-label",
57
+ children: entry.label
58
+ })
59
+ ]
60
+ });
61
+ return pathname === entry.href ? /*#__PURE__*/ _jsx("div", {
62
+ ...props,
63
+ children: content
64
+ }, entry.id) : /*#__PURE__*/ _jsx(Link, {
65
+ ...props,
66
+ href: entry.href,
67
+ prefetch: false,
68
+ children: content
69
+ }, entry.id);
70
+ })
29
71
  });
30
72
  }
73
+ // Preserve the existing import-map key.
74
+ export { TestsNav as TestsNavLink };
@@ -1,3 +1,4 @@
1
+ import '../theme.css';
1
2
  import type { ReactNode } from 'react';
2
3
  import type { DocId } from '../../types/contracts.js';
3
4
  import type { ExperimentAction } from './actions.model.js';
@@ -1,5 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import '../theme.css';
4
+ import { theme } from '../theme.js';
3
5
  import { useEffect, useId, useRef, useState } from 'react';
4
6
  import { createPortal } from 'react-dom';
5
7
  import { Button, TextInput } from '@payloadcms/ui';
@@ -24,7 +26,7 @@ export function DialogFrame({ title, onClose, children }) {
24
26
  position: 'fixed',
25
27
  inset: 0,
26
28
  zIndex: 1000,
27
- background: 'var(--theme-overlay)',
29
+ background: theme.color.overlay,
28
30
  display: 'grid',
29
31
  placeItems: 'center'
30
32
  },
@@ -35,14 +37,14 @@ export function DialogFrame({ title, onClose, children }) {
35
37
  "aria-labelledby": heading,
36
38
  tabIndex: -1,
37
39
  style: {
38
- background: 'var(--theme-bg)',
39
- color: 'var(--theme-text)',
40
- border: '1px solid var(--theme-elevation-200)',
40
+ background: theme.color.bg,
41
+ color: theme.color.text,
42
+ border: `1px solid ${theme.color.borderStrong}`,
41
43
  padding: '2rem',
42
44
  width: 'min(36rem, 90vw)',
43
45
  maxHeight: '90vh',
44
46
  overflow: 'auto',
45
- borderRadius: 'var(--style-radius-s)'
47
+ borderRadius: theme.radius.small
46
48
  },
47
49
  onKeyDown: (event)=>{
48
50
  if (event.defaultPrevented) return;
@@ -0,0 +1,12 @@
1
+ import '../theme.css';
2
+ import './chipsInput.css';
3
+ export interface ChipsInputProps {
4
+ value: string[];
5
+ onChange: (value: string[]) => void;
6
+ label: string;
7
+ description?: string;
8
+ placeholder?: string;
9
+ disabled?: boolean;
10
+ id: string;
11
+ }
12
+ export declare function ChipsInput({ value, onChange, label, description, placeholder, disabled, id }: ChipsInputProps): import("react").JSX.Element;
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef, useState } from 'react';
4
+ import { AUDIENCE_COPY } from '../copy/audience.js';
5
+ import '../theme.css';
6
+ import './chipsInput.css';
7
+ export function ChipsInput({ value, onChange, label, description, placeholder, disabled = false, id }) {
8
+ const [draft, setDraft] = useState('');
9
+ const input = useRef(null);
10
+ const commit = (text)=>{
11
+ if (disabled) return;
12
+ const next = [
13
+ ...new Set([
14
+ ...value,
15
+ ...text.split(',')
16
+ ].map((item)=>item.trim()).filter(Boolean))
17
+ ];
18
+ if (next.length !== value.length || next.some((item, index)=>item !== value[index])) onChange(next);
19
+ setDraft('');
20
+ };
21
+ const remove = (index)=>{
22
+ if (disabled) return;
23
+ onChange(value.filter((_, i)=>i !== index));
24
+ input.current?.focus();
25
+ };
26
+ return /*#__PURE__*/ _jsxs("div", {
27
+ className: "xp-chips",
28
+ role: "group",
29
+ "aria-labelledby": `${id}-label`,
30
+ "aria-disabled": disabled,
31
+ children: [
32
+ /*#__PURE__*/ _jsx("label", {
33
+ id: `${id}-label`,
34
+ htmlFor: id,
35
+ children: label
36
+ }),
37
+ /*#__PURE__*/ _jsxs("div", {
38
+ className: "xp-chips-control",
39
+ children: [
40
+ value.map((item, index)=>/*#__PURE__*/ _jsxs("span", {
41
+ className: "xp-chips-item",
42
+ children: [
43
+ /*#__PURE__*/ _jsx("span", {
44
+ children: item
45
+ }),
46
+ /*#__PURE__*/ _jsx("button", {
47
+ type: "button",
48
+ disabled: disabled,
49
+ "aria-label": AUDIENCE_COPY.removeValue(item),
50
+ onClick: ()=>remove(index),
51
+ children: AUDIENCE_COPY.removeSymbol
52
+ })
53
+ ]
54
+ }, item)),
55
+ /*#__PURE__*/ _jsx("input", {
56
+ ref: input,
57
+ id: id,
58
+ type: "text",
59
+ value: draft,
60
+ disabled: disabled,
61
+ placeholder: placeholder,
62
+ "aria-describedby": description ? `${id}-description` : undefined,
63
+ onChange: (event)=>setDraft(event.target.value),
64
+ onBlur: ()=>{
65
+ if (draft.trim()) commit(draft);
66
+ },
67
+ onKeyDown: (event)=>{
68
+ if (disabled || event.nativeEvent.isComposing) return;
69
+ if (event.key === 'Enter' || event.key === ',') {
70
+ event.preventDefault();
71
+ commit(draft);
72
+ } else if (event.key === 'Tab' && draft.trim()) commit(draft);
73
+ else if (event.key === 'Backspace' && !draft && value.length) {
74
+ event.preventDefault();
75
+ remove(value.length - 1);
76
+ }
77
+ },
78
+ onPaste: (event)=>{
79
+ if (disabled) return;
80
+ const pasted = event.clipboardData.getData('text');
81
+ if (!pasted.includes(',')) return;
82
+ event.preventDefault();
83
+ const start = event.currentTarget.selectionStart ?? draft.length;
84
+ const end = event.currentTarget.selectionEnd ?? start;
85
+ commit(draft.slice(0, start) + pasted + draft.slice(end));
86
+ }
87
+ })
88
+ ]
89
+ }),
90
+ description ? /*#__PURE__*/ _jsx("p", {
91
+ id: `${id}-description`,
92
+ children: description
93
+ }) : null
94
+ ]
95
+ });
96
+ }
@@ -0,0 +1,5 @@
1
+ import '../theme.css';
2
+ export declare function RootViewMessage({ children, alert }: {
3
+ children: string;
4
+ alert?: boolean;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import '../theme.css';
4
+ import { theme } from '../theme.js';
5
+ export function RootViewMessage({ children, alert = false }) {
6
+ return /*#__PURE__*/ _jsx("div", {
7
+ role: alert ? 'alert' : undefined,
8
+ style: {
9
+ padding: theme.space[4],
10
+ color: theme.color.text,
11
+ background: theme.color.bg
12
+ },
13
+ children: children
14
+ });
15
+ }
@@ -0,0 +1,11 @@
1
+ .xp-chips { display: grid; gap: var(--xp-space-2); margin-block: var(--xp-space-4); color: var(--xp-text); font-family: var(--xp-font-body); }
2
+ .xp-chips-control { display: flex; align-items: center; flex-wrap: wrap; gap: var(--xp-space-1); padding: var(--xp-space-2); border: 1px solid var(--xp-border-control); border-radius: var(--xp-radius); background: var(--xp-input-bg); }
3
+ .xp-chips-control:focus-within { outline: 2px solid var(--xp-text); outline-offset: 2px; }
4
+ .xp-chips-item { display: inline-flex; align-items: center; gap: var(--xp-space-1); padding-inline-start: var(--xp-space-2); border-radius: var(--xp-radius); background: var(--xp-surface-raised); overflow-wrap: anywhere; max-width: 100%; }
5
+ .xp-chips .xp-chips-item button { padding: var(--xp-space-1) var(--xp-space-2); border: 0; background: transparent; color: var(--xp-text); font: inherit; cursor: pointer; }
6
+ .xp-chips .xp-chips-control input { flex: 1; min-width: 8rem; width: 0; border: 0; padding: var(--xp-space-1); background: transparent; color: var(--xp-text); font: inherit; }
7
+ .xp-chips input:focus { outline: none; }
8
+ .xp-chips button:focus-visible { outline: 2px solid var(--xp-text); outline-offset: 2px; }
9
+ .xp-chips[aria-disabled='true'] { opacity: .5; }
10
+ .xp-chips[aria-disabled='true'] button { cursor: default; }
11
+ .xp-chips p { margin: 0; color: var(--xp-text-muted); font-size: var(--xp-font-size-small); }
@@ -1,4 +1,8 @@
1
1
  export declare const AUDIENCE_COPY: {
2
+ readonly listPlaceholder: "Type a value and press Enter";
3
+ readonly listDescription: "Add one value per chip, for example each page path.";
4
+ readonly removeValue: (value: string) => string;
5
+ readonly removeSymbol: "×";
2
6
  readonly emptyListNote: "Add at least one value — an empty list can't be saved.";
3
7
  };
4
8
  export declare const STRING_OP_LABELS: {
@@ -1,4 +1,8 @@
1
1
  export const AUDIENCE_COPY = {
2
+ listPlaceholder: 'Type a value and press Enter',
3
+ listDescription: 'Add one value per chip, for example each page path.',
4
+ removeValue: (value)=>`Remove ${value}`,
5
+ removeSymbol: '×',
2
6
  emptyListNote: "Add at least one value — an empty list can't be saved."
3
7
  };
4
8
  export const STRING_OP_LABELS = {
@@ -0,0 +1,44 @@
1
+ export declare const GOALS_COPY: {
2
+ readonly title: "Goals";
3
+ readonly newGoal: "New goal";
4
+ readonly name: "Name";
5
+ readonly source: "Measured by";
6
+ readonly directionColumn: "Better when";
7
+ readonly direction: "Better when it goes";
8
+ readonly increase: "Goes up";
9
+ readonly decrease: "Goes down";
10
+ readonly usedBy: "Used by";
11
+ readonly status: "Status";
12
+ readonly inUse: "In use";
13
+ readonly unused: "Not used";
14
+ readonly empty: "A goal is an outcome you want a test to improve, such as a registration or a gift.";
15
+ readonly loading: "Loading goals…";
16
+ readonly loadError: "Couldn't load goals. Try again.";
17
+ readonly retry: "Try again";
18
+ readonly slug: "Short name";
19
+ readonly slugHelp: "A unique short name for this goal.";
20
+ readonly slugLocked: "The short name stays the same once a test uses this goal.";
21
+ readonly chooseSource: "Choose an outcome";
22
+ readonly window: "Count results for (hours)";
23
+ readonly minimum: "Minimum completions before deciding";
24
+ readonly advanced: "Advanced";
25
+ readonly capping: "Limit very large values";
26
+ readonly capType: "Limit by";
27
+ readonly capValue: "Limit value";
28
+ readonly noCap: "No limit";
29
+ readonly percentile: "Based on past amounts";
30
+ readonly absolute: "Fixed amount";
31
+ readonly capHelp: "For past amounts, enter a number such as 0.99 to limit only the largest 1%; or enter a fixed amount.";
32
+ readonly threshold: "Warn when it gets worse by (%)";
33
+ readonly save: "Save";
34
+ readonly saving: "Saving…";
35
+ readonly cancel: "Cancel";
36
+ readonly remove: "Delete";
37
+ readonly thresholdError: "Enter a percentage of zero or more, or leave blank for 5%.";
38
+ readonly deleting: "Deleting…";
39
+ readonly saveError: "Couldn't save the goal. Try again.";
40
+ readonly deleteError: "Couldn't delete the goal. Try again.";
41
+ readonly breadcrumb: "Breadcrumb";
42
+ readonly missing: "This goal could not be found.";
43
+ };
44
+ export declare function goalUsedBy(count: number): string;
@@ -0,0 +1,46 @@
1
+ export const GOALS_COPY = {
2
+ title: 'Goals',
3
+ newGoal: 'New goal',
4
+ name: 'Name',
5
+ source: 'Measured by',
6
+ directionColumn: 'Better when',
7
+ direction: 'Better when it goes',
8
+ increase: 'Goes up',
9
+ decrease: 'Goes down',
10
+ usedBy: 'Used by',
11
+ status: 'Status',
12
+ inUse: 'In use',
13
+ unused: 'Not used',
14
+ empty: 'A goal is an outcome you want a test to improve, such as a registration or a gift.',
15
+ loading: 'Loading goals…',
16
+ loadError: "Couldn't load goals. Try again.",
17
+ retry: 'Try again',
18
+ slug: 'Short name',
19
+ slugHelp: 'A unique short name for this goal.',
20
+ slugLocked: 'The short name stays the same once a test uses this goal.',
21
+ chooseSource: 'Choose an outcome',
22
+ window: 'Count results for (hours)',
23
+ minimum: 'Minimum completions before deciding',
24
+ advanced: 'Advanced',
25
+ capping: 'Limit very large values',
26
+ capType: 'Limit by',
27
+ capValue: 'Limit value',
28
+ noCap: 'No limit',
29
+ percentile: 'Based on past amounts',
30
+ absolute: 'Fixed amount',
31
+ capHelp: 'For past amounts, enter a number such as 0.99 to limit only the largest 1%; or enter a fixed amount.',
32
+ threshold: 'Warn when it gets worse by (%)',
33
+ save: 'Save',
34
+ saving: 'Saving…',
35
+ cancel: 'Cancel',
36
+ remove: 'Delete',
37
+ thresholdError: 'Enter a percentage of zero or more, or leave blank for 5%.',
38
+ deleting: 'Deleting…',
39
+ saveError: "Couldn't save the goal. Try again.",
40
+ deleteError: "Couldn't delete the goal. Try again.",
41
+ breadcrumb: 'Breadcrumb',
42
+ missing: 'This goal could not be found.'
43
+ };
44
+ export function goalUsedBy(count) {
45
+ return `Used by ${count} ${count === 1 ? 'test' : 'tests'}`;
46
+ }
@@ -1,6 +1,7 @@
1
1
  export declare const REVIEW_COPY: {
2
2
  readonly noBaseline: "No data yet for this metric.";
3
3
  readonly preview: "Preview";
4
+ readonly placedTimes: (n: number) => string;
4
5
  readonly noPreview: "Preview isn't set up for this page type";
5
6
  readonly raise: "Raise allocation";
6
7
  readonly addMetric: "Add a metric";
@@ -1,6 +1,7 @@
1
1
  export const REVIEW_COPY = {
2
2
  noBaseline: 'No data yet for this metric.',
3
3
  preview: 'Preview',
4
+ placedTimes: (n)=>`(placed ${n} times on this page)`,
4
5
  noPreview: "Preview isn't set up for this page type",
5
6
  raise: 'Raise allocation',
6
7
  addMetric: 'Add a metric',
@@ -3,6 +3,10 @@ export declare const SURFACE_COPY: {
3
3
  readonly newTest: "New test";
4
4
  readonly goals: "Goals";
5
5
  readonly search: "Search tests";
6
+ readonly navAllTests: "All tests";
7
+ readonly navGoals: "Goals";
8
+ readonly dashShowTests: "Show all tests";
9
+ readonly dashShowGoals: "Show all goals";
6
10
  readonly backToAdmin: "Back to admin";
7
11
  readonly limitedList: "Showing the 200 most recent tests.";
8
12
  readonly notInstalled: "The Tests screen isn't installed yet. Run `payload generate:importmap` and reload.";
@@ -3,6 +3,10 @@ export const SURFACE_COPY = {
3
3
  newTest: 'New test',
4
4
  goals: 'Goals',
5
5
  search: 'Search tests',
6
+ navAllTests: 'All tests',
7
+ navGoals: 'Goals',
8
+ dashShowTests: 'Show all tests',
9
+ dashShowGoals: 'Show all goals',
6
10
  backToAdmin: 'Back to admin',
7
11
  limitedList: 'Showing the 200 most recent tests.',
8
12
  notInstalled: "The Tests screen isn't installed yet. Run `payload generate:importmap` and reload.",
@@ -0,0 +1,35 @@
1
+ .xp-dash-group {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--xp-space-4);
5
+ margin-bottom: var(--xp-space-4);
6
+ }
7
+
8
+ .xp-dash-label { margin: 0; }
9
+
10
+ .xp-dash-card-list {
11
+ padding: 0;
12
+ margin: 0;
13
+ list-style: none;
14
+ display: grid;
15
+ grid-template-columns: repeat(5, 1fr);
16
+ gap: var(--xp-dash-card-gap);
17
+ }
18
+
19
+ .xp-dash-card-list .card { height: 100%; }
20
+
21
+ @media (max-width: 1440px) {
22
+ .xp-dash-card-list { grid-template-columns: repeat(4, 1fr); }
23
+ }
24
+
25
+ @media (max-width: 1024px) {
26
+ .xp-dash-card-list { grid-template-columns: repeat(2, 1fr); }
27
+ }
28
+
29
+ @media (max-width: 768px) {
30
+ .xp-dash-card-list { gap: var(--xp-dash-card-gap-small); }
31
+ }
32
+
33
+ @media (max-width: 400px) {
34
+ .xp-dash-card-list { grid-template-columns: repeat(1, 1fr); }
35
+ }
@@ -6,3 +6,4 @@ export declare const XP_TESTS_LIST_REDIRECT = "@delmaredigital/payload-experimen
6
6
  export declare const XP_TEST_EDIT_REDIRECT = "@delmaredigital/payload-experiments/admin/rsc#CollectionEditRedirect";
7
7
  export declare const XP_TESTS_NAV_LINK = "@delmaredigital/payload-experiments/admin/client#TestsNavLink";
8
8
  export declare const XP_TESTS_SURFACE = "@delmaredigital/payload-experiments/admin/client#TestsSurface";
9
+ export declare const XP_TESTS_DASHBOARD_CARDS = "@delmaredigital/payload-experiments/admin/client#TestsDashboardCards";
@@ -6,3 +6,4 @@ export const XP_TESTS_LIST_REDIRECT = '@delmaredigital/payload-experiments/admin
6
6
  export const XP_TEST_EDIT_REDIRECT = '@delmaredigital/payload-experiments/admin/rsc#CollectionEditRedirect';
7
7
  export const XP_TESTS_NAV_LINK = '@delmaredigital/payload-experiments/admin/client#TestsNavLink';
8
8
  export const XP_TESTS_SURFACE = '@delmaredigital/payload-experiments/admin/client#TestsSurface';
9
+ export const XP_TESTS_DASHBOARD_CARDS = '@delmaredigital/payload-experiments/admin/client#TestsDashboardCards';
@@ -0,0 +1,9 @@
1
+ import type { PreflightResult } from '../types/endpoints.js';
2
+ type Placement = PreflightResult['placements'][number];
3
+ type PreviewPage = Pick<Placement, 'collection' | 'id' | 'title' | 'url'> & {
4
+ key: string;
5
+ slotCount: number;
6
+ };
7
+ /** Preview URLs identify pages; counts use the most placements in any one version. */
8
+ export declare function previewPages(placements: readonly Placement[]): PreviewPage[];
9
+ export {};