@funnelsgrove/runtime 0.1.0
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.
- package/README.md +42 -0
- package/dist/components/FunnelContext.d.ts +43 -0
- package/dist/components/FunnelContext.js +14 -0
- package/dist/components/FunnelEditorPanel.d.ts +22 -0
- package/dist/components/FunnelEditorPanel.js +116 -0
- package/dist/components/shared/PrimaryButton.d.ts +8 -0
- package/dist/components/shared/PrimaryButton.js +4 -0
- package/dist/config/builder-preview.protocol.d.ts +4 -0
- package/dist/config/builder-preview.protocol.js +4 -0
- package/dist/config/funnel-theme.d.ts +59 -0
- package/dist/config/funnel-theme.js +69 -0
- package/dist/config/funnel.manifest.types.d.ts +50 -0
- package/dist/config/funnel.manifest.types.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/runtime/browser-helpers.d.ts +9 -0
- package/dist/runtime/browser-helpers.js +57 -0
- package/dist/runtime/experiment-assignment.d.ts +4 -0
- package/dist/runtime/experiment-assignment.js +32 -0
- package/dist/runtime/funnel-flow.d.ts +23 -0
- package/dist/runtime/funnel-flow.js +66 -0
- package/dist/runtime/funnel-manifest.validation.d.ts +2 -0
- package/dist/runtime/funnel-manifest.validation.js +63 -0
- package/dist/runtime/funnel-runtime.d.ts +34 -0
- package/dist/runtime/funnel-runtime.js +75 -0
- package/dist/runtime/preview-bridge.d.ts +51 -0
- package/dist/runtime/preview-bridge.js +230 -0
- package/dist/runtime/route-resolver.d.ts +18 -0
- package/dist/runtime/route-resolver.js +91 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +58 -0
- package/dist/runtime/use-funnel-flow-controller.js +523 -0
- package/dist/sdk/userAnswers.d.ts +45 -0
- package/dist/sdk/userAnswers.js +10 -0
- package/dist/services/api.service.d.ts +81 -0
- package/dist/services/api.service.js +346 -0
- package/dist/services/logger.d.ts +8 -0
- package/dist/services/logger.js +16 -0
- package/dist/services/preview-frame.service.d.ts +4 -0
- package/dist/services/preview-frame.service.js +31 -0
- package/dist/services/runtime-api.config.d.ts +7 -0
- package/dist/services/runtime-api.config.js +40 -0
- package/dist/services/runtime-mode.service.d.ts +5 -0
- package/dist/services/runtime-mode.service.js +113 -0
- package/dist/steps/types.d.ts +22 -0
- package/dist/steps/types.js +17 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @funnelsgrove/runtime
|
|
2
|
+
|
|
3
|
+
Shared funnel runtime contracts and helpers.
|
|
4
|
+
|
|
5
|
+
## Build And Publish
|
|
6
|
+
|
|
7
|
+
- Build distributable output with `npm run build --workspace @funnelsgrove/runtime`.
|
|
8
|
+
- Publish from the repo root with `npm publish --workspace @funnelsgrove/runtime --access public`.
|
|
9
|
+
- Local repo installs still resolve this package through npm workspaces when another workspace depends on version `0.1.0`.
|
|
10
|
+
|
|
11
|
+
## Use This Package For
|
|
12
|
+
|
|
13
|
+
- manifest types and validation
|
|
14
|
+
- route resolution and entry-point handling
|
|
15
|
+
- experiment assignment helpers
|
|
16
|
+
- preview-bridge parsing and preview/runtime detection
|
|
17
|
+
- published theme contract and CSS variable helpers
|
|
18
|
+
- funnel context, base controls, and reusable runtime UI primitives
|
|
19
|
+
- browser-safe API client helpers used by funnels
|
|
20
|
+
|
|
21
|
+
## Responsibilities
|
|
22
|
+
|
|
23
|
+
- keep funnel mechanics consistent across every funnel
|
|
24
|
+
- expose stable contracts for routing, previews, and step identity
|
|
25
|
+
- avoid funnel-specific copy, visual design, or billing plan catalogs
|
|
26
|
+
|
|
27
|
+
## What Belongs Here
|
|
28
|
+
|
|
29
|
+
- reusable runtime helpers that any funnel can use
|
|
30
|
+
- shared browser/event/storage helpers tied to funnel behavior
|
|
31
|
+
- hosted-path navigation helpers so static previews keep their `/published/f/...` or `/catalog/...` prefix
|
|
32
|
+
- generic context and base component primitives
|
|
33
|
+
- safe local-only fallbacks when no real SDK publishable key is configured
|
|
34
|
+
- default runtime config values for API/bootstrap wiring
|
|
35
|
+
|
|
36
|
+
## What Does Not Belong Here
|
|
37
|
+
|
|
38
|
+
- funnel-specific step JSX
|
|
39
|
+
- brand/theme assets
|
|
40
|
+
- billing catalog data
|
|
41
|
+
- analytics SDK transport
|
|
42
|
+
- Stripe checkout UI
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FunnelStepId } from '../runtime/funnel-runtime';
|
|
2
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers';
|
|
3
|
+
/** A single record of a completed step, stored in the user's history. */
|
|
4
|
+
export type StepCompletionRecord = {
|
|
5
|
+
stepId: string;
|
|
6
|
+
stepName: string;
|
|
7
|
+
completedAt: string;
|
|
8
|
+
choices: FunnelUserAnswers;
|
|
9
|
+
};
|
|
10
|
+
export type FunnelUser = {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
attributes: FunnelUserAnswers;
|
|
15
|
+
/** Ordered history of every step the user has completed. */
|
|
16
|
+
completedSteps?: StepCompletionRecord[];
|
|
17
|
+
};
|
|
18
|
+
export type FunnelContextValue = {
|
|
19
|
+
activeStepId: FunnelStepId;
|
|
20
|
+
goToStep: (stepId: string) => void;
|
|
21
|
+
goNext: () => void;
|
|
22
|
+
goChoice: (choice: 'yes' | 'no', stepId?: string) => void;
|
|
23
|
+
getChoiceTargets: (stepId?: string) => {
|
|
24
|
+
yes?: FunnelStepId;
|
|
25
|
+
no?: FunnelStepId;
|
|
26
|
+
} | null;
|
|
27
|
+
setAnswer: <K extends keyof FunnelUserAnswers>(key: K, value: FunnelUserAnswers[K]) => void;
|
|
28
|
+
getAnswer: <K extends keyof FunnelUserAnswers>(key: K) => FunnelUserAnswers[K] | undefined;
|
|
29
|
+
answers: FunnelUserAnswers;
|
|
30
|
+
setAttribute: (key: string, value: unknown) => void;
|
|
31
|
+
attributes: FunnelUserAnswers;
|
|
32
|
+
user: FunnelUser;
|
|
33
|
+
setUser: (user: FunnelUser) => void;
|
|
34
|
+
/** Manually record a step as completed (used by custom step components). */
|
|
35
|
+
completeStep: (stepId: string, choices?: Record<string, unknown>) => void;
|
|
36
|
+
};
|
|
37
|
+
type FunnelProviderProps = {
|
|
38
|
+
value: FunnelContextValue;
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
};
|
|
41
|
+
export declare function FunnelProvider({ value, children }: FunnelProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export declare function useFunnel(): FunnelContextValue;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
const FunnelContext = createContext(null);
|
|
5
|
+
export function FunnelProvider({ value, children }) {
|
|
6
|
+
return _jsx(FunnelContext.Provider, { value: value, children: children });
|
|
7
|
+
}
|
|
8
|
+
export function useFunnel() {
|
|
9
|
+
const context = useContext(FunnelContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useFunnel must be used inside FunnelProvider');
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RuntimeMode } from '../services/runtime-mode.service';
|
|
2
|
+
export type FunnelEditorPanelExperiment = {
|
|
3
|
+
experimentId: string;
|
|
4
|
+
status: 'draft' | 'active';
|
|
5
|
+
variants: readonly {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
type FunnelEditorPanelProps<Experiment extends FunnelEditorPanelExperiment> = {
|
|
11
|
+
editorModeEnabled: boolean;
|
|
12
|
+
editorPanelExpanded: boolean;
|
|
13
|
+
experimentAssignmentForEditor: (experiment: Experiment) => string;
|
|
14
|
+
funnelExperiments: readonly Experiment[];
|
|
15
|
+
onRuntimeModeChange: (mode: RuntimeMode) => void;
|
|
16
|
+
onSelectVariant: (experiment: Experiment, variantId: string) => void;
|
|
17
|
+
onToggleExpanded: () => void;
|
|
18
|
+
runtimeMode: RuntimeMode;
|
|
19
|
+
};
|
|
20
|
+
export declare function FunnelEditorPanel<Experiment extends FunnelEditorPanelExperiment>({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onRuntimeModeChange, onSelectVariant, onToggleExpanded, runtimeMode, }: FunnelEditorPanelProps<Experiment>): import("react/jsx-runtime").JSX.Element | null;
|
|
21
|
+
export declare const funnelEditorPanelStyles = "\n.funnel-editor-panel {\n position: fixed;\n top: 12px;\n right: 12px;\n z-index: 1200;\n border-radius: 12px;\n border: 1px solid rgba(33, 41, 82, 0.25);\n background: rgba(255, 255, 255, 0.98);\n color: #202538;\n box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);\n}\n\n.funnel-editor-panel.is-collapsed {\n width: auto;\n}\n\n.funnel-editor-panel.is-expanded {\n width: 184px;\n}\n\n.funnel-editor-panel-toggle {\n width: 100%;\n border: 0;\n background: transparent;\n color: inherit;\n font-size: 13px;\n font-weight: 700;\n line-height: 1;\n padding: 10px 12px;\n cursor: pointer;\n text-align: left;\n}\n\n.funnel-editor-panel-content {\n border-top: 1px solid rgba(33, 41, 82, 0.15);\n padding: 10px 12px 12px;\n}\n\n.funnel-editor-panel-label {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: #53608a;\n}\n\n.funnel-editor-panel-options {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n}\n\n.funnel-editor-panel-options button {\n border: 1px solid rgba(63, 81, 181, 0.3);\n border-radius: 8px;\n background: #eef1ff;\n color: #3f51b5;\n font-size: 12px;\n font-weight: 700;\n line-height: 1;\n padding: 10px 0;\n cursor: pointer;\n}\n\n.funnel-editor-panel-options button.is-active {\n border-color: #3f51b5;\n background: #3f51b5;\n color: #ffffff;\n}\n\n.funnel-editor-panel-tests {\n margin-top: 10px;\n}\n\n.funnel-editor-panel-helper {\n margin: 0;\n font-size: 11px;\n color: #5a6387;\n}\n\n.funnel-editor-panel-test-list {\n display: grid;\n gap: 8px;\n}\n\n.funnel-editor-panel-test-item {\n display: grid;\n gap: 6px;\n font-size: 11px;\n color: #445073;\n}\n\n.funnel-editor-panel-test-item select {\n height: 28px;\n border: 1px solid rgba(63, 81, 181, 0.25);\n border-radius: 8px;\n background: #f5f7ff;\n color: #26315a;\n font-size: 12px;\n padding: 0 8px;\n}\n";
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function FunnelEditorPanel({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onRuntimeModeChange, onSelectVariant, onToggleExpanded, runtimeMode, }) {
|
|
4
|
+
if (!editorModeEnabled) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return (_jsxs("aside", { className: `funnel-editor-panel ${editorPanelExpanded ? 'is-expanded' : 'is-collapsed'}`, children: [_jsx("button", { type: 'button', className: 'funnel-editor-panel-toggle', onClick: onToggleExpanded, children: editorPanelExpanded ? 'Hide editor' : 'Editor' }), editorPanelExpanded ? (_jsxs("div", { className: 'funnel-editor-panel-content', children: [_jsx("p", { className: 'funnel-editor-panel-label', children: "MODE" }), _jsxs("div", { className: 'funnel-editor-panel-options', children: [_jsx("button", { type: 'button', className: runtimeMode === 'test' ? 'is-active' : '', onClick: () => onRuntimeModeChange('test'), children: "Test" }), _jsx("button", { type: 'button', className: runtimeMode === 'live' ? 'is-active' : '', onClick: () => onRuntimeModeChange('live'), children: "Live" })] }), _jsxs("div", { className: 'funnel-editor-panel-tests', children: [_jsx("p", { className: 'funnel-editor-panel-label', children: "A/B TESTS" }), funnelExperiments.length === 0 ? (_jsx("p", { className: 'funnel-editor-panel-helper', children: "No configured tests." })) : (_jsx("div", { className: 'funnel-editor-panel-test-list', children: funnelExperiments.map((experiment) => {
|
|
8
|
+
const assignedVariantId = experimentAssignmentForEditor(experiment);
|
|
9
|
+
return (_jsxs("label", { className: 'funnel-editor-panel-test-item', children: [_jsxs("span", { children: [experiment.experimentId, experiment.status === 'active' ? ' (running)' : ' (stopped)'] }), _jsx("select", { value: assignedVariantId, onChange: (event) => {
|
|
10
|
+
onSelectVariant(experiment, event.target.value);
|
|
11
|
+
}, children: experiment.variants.map((variant) => (_jsx("option", { value: variant.id, children: variant.label }, variant.id))) })] }, experiment.experimentId));
|
|
12
|
+
}) }))] })] })) : null] }));
|
|
13
|
+
}
|
|
14
|
+
export const funnelEditorPanelStyles = `
|
|
15
|
+
.funnel-editor-panel {
|
|
16
|
+
position: fixed;
|
|
17
|
+
top: 12px;
|
|
18
|
+
right: 12px;
|
|
19
|
+
z-index: 1200;
|
|
20
|
+
border-radius: 12px;
|
|
21
|
+
border: 1px solid rgba(33, 41, 82, 0.25);
|
|
22
|
+
background: rgba(255, 255, 255, 0.98);
|
|
23
|
+
color: #202538;
|
|
24
|
+
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.funnel-editor-panel.is-collapsed {
|
|
28
|
+
width: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.funnel-editor-panel.is-expanded {
|
|
32
|
+
width: 184px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.funnel-editor-panel-toggle {
|
|
36
|
+
width: 100%;
|
|
37
|
+
border: 0;
|
|
38
|
+
background: transparent;
|
|
39
|
+
color: inherit;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
line-height: 1;
|
|
43
|
+
padding: 10px 12px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
text-align: left;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.funnel-editor-panel-content {
|
|
49
|
+
border-top: 1px solid rgba(33, 41, 82, 0.15);
|
|
50
|
+
padding: 10px 12px 12px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.funnel-editor-panel-label {
|
|
54
|
+
margin: 0 0 8px;
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
font-weight: 700;
|
|
57
|
+
letter-spacing: 0.08em;
|
|
58
|
+
color: #53608a;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.funnel-editor-panel-options {
|
|
62
|
+
display: grid;
|
|
63
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
64
|
+
gap: 8px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.funnel-editor-panel-options button {
|
|
68
|
+
border: 1px solid rgba(63, 81, 181, 0.3);
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
background: #eef1ff;
|
|
71
|
+
color: #3f51b5;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
line-height: 1;
|
|
75
|
+
padding: 10px 0;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.funnel-editor-panel-options button.is-active {
|
|
80
|
+
border-color: #3f51b5;
|
|
81
|
+
background: #3f51b5;
|
|
82
|
+
color: #ffffff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.funnel-editor-panel-tests {
|
|
86
|
+
margin-top: 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.funnel-editor-panel-helper {
|
|
90
|
+
margin: 0;
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
color: #5a6387;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.funnel-editor-panel-test-list {
|
|
96
|
+
display: grid;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.funnel-editor-panel-test-item {
|
|
101
|
+
display: grid;
|
|
102
|
+
gap: 6px;
|
|
103
|
+
font-size: 11px;
|
|
104
|
+
color: #445073;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.funnel-editor-panel-test-item select {
|
|
108
|
+
height: 28px;
|
|
109
|
+
border: 1px solid rgba(63, 81, 181, 0.25);
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
background: #f5f7ff;
|
|
112
|
+
color: #26315a;
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
padding: 0 8px;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type PrimaryButtonProps = {
|
|
2
|
+
onClick: () => void;
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function PrimaryButton({ onClick, children, className, disabled, }: PrimaryButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function PrimaryButton({ onClick, children, className, disabled = false, }) {
|
|
3
|
+
return (_jsx("button", { className: className ? `primary-button ${className}` : 'primary-button', type: "button", onClick: onClick, disabled: disabled, children: children }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const BUILDER_PREVIEW_READY = "builder.preview.ready";
|
|
2
|
+
export declare const BUILDER_PREVIEW_ACTIVE_STEP_CHANGED = "builder.preview.activeStepChanged";
|
|
3
|
+
export declare const BUILDER_PREVIEW_GO_TO_STEP = "builder.preview.goToStep";
|
|
4
|
+
export declare const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = "builder.preview.runtimeModeChanged";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const BUILDER_PREVIEW_READY = 'builder.preview.ready';
|
|
2
|
+
export const BUILDER_PREVIEW_ACTIVE_STEP_CHANGED = 'builder.preview.activeStepChanged';
|
|
3
|
+
export const BUILDER_PREVIEW_GO_TO_STEP = 'builder.preview.goToStep';
|
|
4
|
+
export const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = 'builder.preview.runtimeModeChanged';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { FunnelManifest } from './funnel.manifest.types';
|
|
2
|
+
export type FunnelThemeColors = {
|
|
3
|
+
background: string;
|
|
4
|
+
text: string;
|
|
5
|
+
accent: string;
|
|
6
|
+
button: string;
|
|
7
|
+
surface: string;
|
|
8
|
+
border: string;
|
|
9
|
+
};
|
|
10
|
+
export type FunnelThemeTypography = {
|
|
11
|
+
fontFamily: string;
|
|
12
|
+
fontSizes: {
|
|
13
|
+
xs: string;
|
|
14
|
+
sm: string;
|
|
15
|
+
md: string;
|
|
16
|
+
lg: string;
|
|
17
|
+
xl: string;
|
|
18
|
+
xxl: string;
|
|
19
|
+
};
|
|
20
|
+
fontWeights: {
|
|
21
|
+
regular: number;
|
|
22
|
+
medium: number;
|
|
23
|
+
bold: number;
|
|
24
|
+
};
|
|
25
|
+
lineHeights: {
|
|
26
|
+
tight: number;
|
|
27
|
+
normal: number;
|
|
28
|
+
relaxed: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export type FunnelThemeLayout = {
|
|
32
|
+
shellWidth: number;
|
|
33
|
+
maxWidth: number;
|
|
34
|
+
safeAreaOffsets: {
|
|
35
|
+
top: number;
|
|
36
|
+
right: number;
|
|
37
|
+
bottom: number;
|
|
38
|
+
left: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export type FunnelTheme = {
|
|
42
|
+
colors: FunnelThemeColors;
|
|
43
|
+
typography: FunnelThemeTypography;
|
|
44
|
+
layout: FunnelThemeLayout;
|
|
45
|
+
};
|
|
46
|
+
export type FunnelRuntimeDefaults = {
|
|
47
|
+
apiUrl: string;
|
|
48
|
+
funnelId: string | null;
|
|
49
|
+
sdkPublishableKey: string | null;
|
|
50
|
+
};
|
|
51
|
+
export type PublishedFunnelSnapshot = {
|
|
52
|
+
templateArchitectureVersion: number;
|
|
53
|
+
runtime: FunnelRuntimeDefaults;
|
|
54
|
+
manifest: FunnelManifest;
|
|
55
|
+
theme: FunnelTheme;
|
|
56
|
+
};
|
|
57
|
+
export declare const defaultFunnelTheme: FunnelTheme;
|
|
58
|
+
export declare const defaultFunnelRuntimeDefaults: FunnelRuntimeDefaults;
|
|
59
|
+
export declare const createThemeCssVariables: (theme: FunnelTheme) => Record<string, string>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const defaultFunnelTheme = {
|
|
2
|
+
colors: {
|
|
3
|
+
background: '#ffffff',
|
|
4
|
+
text: '#262729',
|
|
5
|
+
accent: '#3f51b5',
|
|
6
|
+
button: '#4db53f',
|
|
7
|
+
surface: '#ffffff',
|
|
8
|
+
border: '#dce2ff',
|
|
9
|
+
},
|
|
10
|
+
typography: {
|
|
11
|
+
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
12
|
+
fontSizes: {
|
|
13
|
+
xs: '12px',
|
|
14
|
+
sm: '14px',
|
|
15
|
+
md: '16px',
|
|
16
|
+
lg: '20px',
|
|
17
|
+
xl: '28px',
|
|
18
|
+
xxl: '36px',
|
|
19
|
+
},
|
|
20
|
+
fontWeights: {
|
|
21
|
+
regular: 400,
|
|
22
|
+
medium: 500,
|
|
23
|
+
bold: 700,
|
|
24
|
+
},
|
|
25
|
+
lineHeights: {
|
|
26
|
+
tight: 1.2,
|
|
27
|
+
normal: 1.45,
|
|
28
|
+
relaxed: 1.65,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
layout: {
|
|
32
|
+
shellWidth: 430,
|
|
33
|
+
maxWidth: 430,
|
|
34
|
+
safeAreaOffsets: {
|
|
35
|
+
top: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
bottom: 0,
|
|
38
|
+
left: 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export const defaultFunnelRuntimeDefaults = {
|
|
43
|
+
apiUrl: 'https://sdk-api.funnelsgrove.com',
|
|
44
|
+
funnelId: null,
|
|
45
|
+
sdkPublishableKey: null,
|
|
46
|
+
};
|
|
47
|
+
export const createThemeCssVariables = (theme) => {
|
|
48
|
+
return {
|
|
49
|
+
'--color-bg': theme.colors.background,
|
|
50
|
+
'--color-text': theme.colors.text,
|
|
51
|
+
'--color-accent': theme.colors.accent,
|
|
52
|
+
'--color-button': theme.colors.button,
|
|
53
|
+
'--color-surface': theme.colors.surface,
|
|
54
|
+
'--color-border': theme.colors.border,
|
|
55
|
+
'--font-family-base': theme.typography.fontFamily,
|
|
56
|
+
'--font-family-display': theme.typography.fontFamily,
|
|
57
|
+
'--font-weight-regular': String(theme.typography.fontWeights.regular),
|
|
58
|
+
'--font-weight-medium': String(theme.typography.fontWeights.medium),
|
|
59
|
+
'--font-weight-bold': String(theme.typography.fontWeights.bold),
|
|
60
|
+
'--line-height-body': String(theme.typography.lineHeights.normal),
|
|
61
|
+
'--line-height-heading': String(theme.typography.lineHeights.tight),
|
|
62
|
+
'--shell-width': `${theme.layout.shellWidth}px`,
|
|
63
|
+
'--max-width': `${theme.layout.maxWidth}px`,
|
|
64
|
+
'--safe-area-offset-top': `${theme.layout.safeAreaOffsets.top}px`,
|
|
65
|
+
'--safe-area-offset-right': `${theme.layout.safeAreaOffsets.right}px`,
|
|
66
|
+
'--safe-area-offset-bottom': `${theme.layout.safeAreaOffsets.bottom}px`,
|
|
67
|
+
'--safe-area-offset-left': `${theme.layout.safeAreaOffsets.left}px`,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FunnelStepKind, FunnelStepType } from '../steps/types';
|
|
2
|
+
export type TemplateArchitectureVersion = number;
|
|
3
|
+
export type FunnelManifestStep = {
|
|
4
|
+
id: string;
|
|
5
|
+
path: string;
|
|
6
|
+
filePath: `src/steps/${string}.tsx`;
|
|
7
|
+
componentKey: string;
|
|
8
|
+
type: FunnelStepType;
|
|
9
|
+
kind?: FunnelStepKind;
|
|
10
|
+
name?: string;
|
|
11
|
+
title: string;
|
|
12
|
+
};
|
|
13
|
+
export type FunnelManifestEntryPoint = {
|
|
14
|
+
id: string;
|
|
15
|
+
stepId: string;
|
|
16
|
+
isDefault?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type FunnelManifestEdge = {
|
|
19
|
+
toStepId: string;
|
|
20
|
+
conditionId?: string;
|
|
21
|
+
};
|
|
22
|
+
export type FunnelManifestExperimentVariant = {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
trafficPercent: number;
|
|
26
|
+
routeToStepId: string;
|
|
27
|
+
};
|
|
28
|
+
export type FunnelManifestExperiment = {
|
|
29
|
+
experimentId: string;
|
|
30
|
+
status: 'draft' | 'active';
|
|
31
|
+
stepId: string;
|
|
32
|
+
variants: readonly FunnelManifestExperimentVariant[];
|
|
33
|
+
};
|
|
34
|
+
export type FunnelManifest = {
|
|
35
|
+
templateArchitectureVersion: TemplateArchitectureVersion;
|
|
36
|
+
meta: {
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
viewport: {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
};
|
|
44
|
+
assets: Record<string, string>;
|
|
45
|
+
steps: readonly FunnelManifestStep[];
|
|
46
|
+
entryPoints: readonly FunnelManifestEntryPoint[];
|
|
47
|
+
edgesByStepId: Partial<Record<string, readonly FunnelManifestEdge[]>>;
|
|
48
|
+
experiments: readonly FunnelManifestExperiment[];
|
|
49
|
+
};
|
|
50
|
+
export type FunnelStepComponentKey = string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './config/builder-preview.protocol';
|
|
2
|
+
export * from './config/funnel.manifest.types';
|
|
3
|
+
export * from './config/funnel-theme';
|
|
4
|
+
export * from './runtime/experiment-assignment';
|
|
5
|
+
export * from './runtime/browser-helpers';
|
|
6
|
+
export * from './runtime/funnel-flow';
|
|
7
|
+
export * from './runtime/funnel-manifest.validation';
|
|
8
|
+
export * from './runtime/funnel-runtime';
|
|
9
|
+
export * from './runtime/use-funnel-flow-controller';
|
|
10
|
+
export * from './runtime/preview-bridge';
|
|
11
|
+
export * from './runtime/route-resolver';
|
|
12
|
+
export * from './services/api.service';
|
|
13
|
+
export * from './services/logger';
|
|
14
|
+
export * from './services/preview-frame.service';
|
|
15
|
+
export * from './services/runtime-api.config';
|
|
16
|
+
export * from './services/runtime-mode.service';
|
|
17
|
+
export * from './sdk/userAnswers';
|
|
18
|
+
export * from './components/FunnelContext';
|
|
19
|
+
export * from './components/FunnelEditorPanel';
|
|
20
|
+
export * from './components/shared/PrimaryButton';
|
|
21
|
+
export * from './steps/types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './config/builder-preview.protocol';
|
|
2
|
+
export * from './config/funnel.manifest.types';
|
|
3
|
+
export * from './config/funnel-theme';
|
|
4
|
+
export * from './runtime/experiment-assignment';
|
|
5
|
+
export * from './runtime/browser-helpers';
|
|
6
|
+
export * from './runtime/funnel-flow';
|
|
7
|
+
export * from './runtime/funnel-manifest.validation';
|
|
8
|
+
export * from './runtime/funnel-runtime';
|
|
9
|
+
export * from './runtime/use-funnel-flow-controller';
|
|
10
|
+
export * from './runtime/preview-bridge';
|
|
11
|
+
export * from './runtime/route-resolver';
|
|
12
|
+
export * from './services/api.service';
|
|
13
|
+
export * from './services/logger';
|
|
14
|
+
export * from './services/preview-frame.service';
|
|
15
|
+
export * from './services/runtime-api.config';
|
|
16
|
+
export * from './services/runtime-mode.service';
|
|
17
|
+
export * from './sdk/userAnswers';
|
|
18
|
+
export * from './components/FunnelContext';
|
|
19
|
+
export * from './components/FunnelEditorPanel';
|
|
20
|
+
export * from './components/shared/PrimaryButton';
|
|
21
|
+
export * from './steps/types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const canUseDom: () => boolean;
|
|
2
|
+
export declare const readWindowStorageValue: (storageKey: string) => string | null;
|
|
3
|
+
export declare const writeWindowStorageValue: (storageKey: string, value: string) => void;
|
|
4
|
+
export declare const dispatchWindowCustomEvent: <Detail>(eventType: string, detail: Detail) => void;
|
|
5
|
+
export declare const buildHostedStepLocation: (input: {
|
|
6
|
+
currentHref: string;
|
|
7
|
+
stepPath: string;
|
|
8
|
+
stepId: string;
|
|
9
|
+
}) => string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const canUseDom = () => {
|
|
2
|
+
return typeof window !== 'undefined';
|
|
3
|
+
};
|
|
4
|
+
export const readWindowStorageValue = (storageKey) => {
|
|
5
|
+
if (!canUseDom()) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return window.localStorage.getItem(storageKey);
|
|
10
|
+
}
|
|
11
|
+
catch (_a) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const writeWindowStorageValue = (storageKey, value) => {
|
|
16
|
+
if (!canUseDom()) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(storageKey, value);
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
// ignore storage access failures
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const dispatchWindowCustomEvent = (eventType, detail) => {
|
|
27
|
+
if (!canUseDom()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
window.dispatchEvent(new CustomEvent(eventType, {
|
|
31
|
+
detail,
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
const getHostedFunnelBaseSegments = (pathname) => {
|
|
35
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
|
36
|
+
if (pathSegments[0] === 'published' && pathSegments[1] === 'f' && pathSegments[2]) {
|
|
37
|
+
return pathSegments.slice(0, 3);
|
|
38
|
+
}
|
|
39
|
+
if (pathSegments[0] === 'preview' && pathSegments[1] === 's' && pathSegments[2]) {
|
|
40
|
+
return pathSegments.slice(0, 3);
|
|
41
|
+
}
|
|
42
|
+
if (pathSegments[0] === 'catalog' && pathSegments[1]) {
|
|
43
|
+
return pathSegments.slice(0, 2);
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
};
|
|
47
|
+
export const buildHostedStepLocation = (input) => {
|
|
48
|
+
const nextUrl = new URL(input.currentHref);
|
|
49
|
+
const normalizedStepPath = input.stepPath.startsWith('/') ? input.stepPath : `/${input.stepPath}`;
|
|
50
|
+
const hostedBaseSegments = getHostedFunnelBaseSegments(nextUrl.pathname);
|
|
51
|
+
nextUrl.pathname =
|
|
52
|
+
hostedBaseSegments.length > 0
|
|
53
|
+
? `/${hostedBaseSegments.join('/')}${normalizedStepPath}`
|
|
54
|
+
: normalizedStepPath;
|
|
55
|
+
nextUrl.searchParams.set('step', input.stepId);
|
|
56
|
+
return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
|
|
57
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FunnelManifestExperiment } from '../config/funnel.manifest.types';
|
|
2
|
+
export declare const getExperimentAssignmentStorageKey: (funnelId: string, userId: string, experimentId: string) => string;
|
|
3
|
+
export declare const getExperimentAssignmentAttributeKey: (experimentId: string) => string;
|
|
4
|
+
export declare const chooseAssignedVariantId: (experiment: FunnelManifestExperiment, userId: string, existingVariantId?: string | null) => string | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const hashToBucket = (input) => {
|
|
2
|
+
let hash = 0;
|
|
3
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
4
|
+
hash = (hash * 31 + input.charCodeAt(index)) % 104729;
|
|
5
|
+
}
|
|
6
|
+
return Math.abs(hash % 100);
|
|
7
|
+
};
|
|
8
|
+
export const getExperimentAssignmentStorageKey = (funnelId, userId, experimentId) => {
|
|
9
|
+
return `funnel:experiment:${funnelId}:${userId}:${experimentId}`;
|
|
10
|
+
};
|
|
11
|
+
export const getExperimentAssignmentAttributeKey = (experimentId) => {
|
|
12
|
+
return `experiment.${experimentId}.variant`;
|
|
13
|
+
};
|
|
14
|
+
export const chooseAssignedVariantId = (experiment, userId, existingVariantId) => {
|
|
15
|
+
var _a;
|
|
16
|
+
if (!userId.trim()) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const variantsById = new Map(experiment.variants.map((variant) => [variant.id, variant]));
|
|
20
|
+
if (existingVariantId && variantsById.has(existingVariantId)) {
|
|
21
|
+
return existingVariantId;
|
|
22
|
+
}
|
|
23
|
+
const bucket = hashToBucket(`${experiment.experimentId}:${userId}`);
|
|
24
|
+
let cumulative = 0;
|
|
25
|
+
for (const variant of experiment.variants) {
|
|
26
|
+
cumulative += Number.isFinite(variant.trafficPercent) ? variant.trafficPercent : 0;
|
|
27
|
+
if (bucket < cumulative) {
|
|
28
|
+
return variant.id;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return ((_a = experiment.variants[experiment.variants.length - 1]) === null || _a === void 0 ? void 0 : _a.id) || null;
|
|
32
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FunnelManifestExperiment } from '../config/funnel.manifest.types';
|
|
2
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers';
|
|
3
|
+
export declare const isPreviewStepLockRequested: () => boolean;
|
|
4
|
+
export declare const getRequestedEntryPointId: () => string | null;
|
|
5
|
+
export declare const getRuntimeFunnelIdentity: () => string;
|
|
6
|
+
export declare const getAssignedVariantId: (experiment: FunnelManifestExperiment, userId: string, storage: {
|
|
7
|
+
read: (storageKey: string) => string | null;
|
|
8
|
+
write: (storageKey: string, variantId: string) => void;
|
|
9
|
+
}) => string | null;
|
|
10
|
+
export declare const resolveNextStepFromContext: <StepId extends string>(input: {
|
|
11
|
+
stepId: StepId;
|
|
12
|
+
attributes: FunnelUserAnswers;
|
|
13
|
+
userId: string;
|
|
14
|
+
safeInitialStepId: StepId;
|
|
15
|
+
resolveRenderableStepId: (stepId: string | null | undefined) => StepId | null;
|
|
16
|
+
getConfiguredNextStepId: (stepId: StepId, context: {
|
|
17
|
+
attributes: FunnelUserAnswers;
|
|
18
|
+
userId: string;
|
|
19
|
+
getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
|
|
20
|
+
}) => StepId | null;
|
|
21
|
+
getSequentialNextStepId: (stepId: StepId) => StepId | null;
|
|
22
|
+
getAssignedVariantId: (experiment: FunnelManifestExperiment, userId: string) => string | null;
|
|
23
|
+
}) => StepId;
|