@funnelsgrove/runtime 0.1.0 → 0.1.1

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 (62) hide show
  1. package/README.md +20 -1
  2. package/dist/components/FunnelContext.d.ts +5 -2
  3. package/dist/components/FunnelContext.js +3 -0
  4. package/dist/components/FunnelEditorPanel.d.ts +3 -5
  5. package/dist/components/FunnelEditorPanel.js +3 -3
  6. package/dist/components/ManageSubscriptionScreen.d.ts +51 -0
  7. package/dist/components/ManageSubscriptionScreen.js +349 -0
  8. package/dist/components/RuntimeDevInfoBox.d.ts +23 -0
  9. package/dist/components/RuntimeDevInfoBox.js +363 -0
  10. package/dist/components/SubscriptionHandoffScreen.d.ts +31 -0
  11. package/dist/components/SubscriptionHandoffScreen.js +338 -0
  12. package/dist/config/builder-preview.protocol.d.ts +73 -0
  13. package/dist/config/builder-preview.protocol.js +3 -0
  14. package/dist/config/env.config.d.ts +44 -0
  15. package/dist/config/env.config.js +161 -0
  16. package/dist/config/font-config.d.ts +14 -0
  17. package/dist/config/font-config.js +101 -0
  18. package/dist/config/funnel-theme.d.ts +61 -10
  19. package/dist/config/funnel-theme.js +355 -35
  20. package/dist/config/funnel.manifest.types.d.ts +13 -7
  21. package/dist/content/step-content.d.ts +130 -0
  22. package/dist/content/step-content.js +381 -0
  23. package/dist/index.d.ts +33 -21
  24. package/dist/index.js +33 -21
  25. package/dist/runtime/browser-helpers.d.ts +1 -0
  26. package/dist/runtime/browser-helpers.js +14 -0
  27. package/dist/runtime/experiment-assignment.d.ts +13 -4
  28. package/dist/runtime/experiment-assignment.js +9 -27
  29. package/dist/runtime/funnel-attribution.d.ts +18 -0
  30. package/dist/runtime/funnel-attribution.js +226 -0
  31. package/dist/runtime/funnel-flow.d.ts +9 -10
  32. package/dist/runtime/funnel-flow.js +4 -18
  33. package/dist/runtime/funnel-manifest.validation.d.ts +1 -1
  34. package/dist/runtime/funnel-manifest.validation.js +2 -6
  35. package/dist/runtime/funnel-runtime.d.ts +2 -3
  36. package/dist/runtime/funnel-runtime.js +6 -13
  37. package/dist/runtime/posthog-flags.d.ts +30 -0
  38. package/dist/runtime/posthog-flags.js +71 -0
  39. package/dist/runtime/preview-bridge.d.ts +13 -3
  40. package/dist/runtime/preview-bridge.js +96 -4
  41. package/dist/runtime/preview-definition-overrides.d.ts +20 -0
  42. package/dist/runtime/preview-definition-overrides.js +148 -0
  43. package/dist/runtime/route-resolver.d.ts +2 -3
  44. package/dist/runtime/route-resolver.js +15 -26
  45. package/dist/runtime/subscription-handoff.d.ts +32 -0
  46. package/dist/runtime/subscription-handoff.js +113 -0
  47. package/dist/runtime/use-funnel-flow-controller.d.ts +19 -10
  48. package/dist/runtime/use-funnel-flow-controller.js +190 -159
  49. package/dist/sdk/userAnswers.d.ts +2 -2
  50. package/dist/services/api.service.d.ts +21 -4
  51. package/dist/services/api.service.js +165 -35
  52. package/dist/services/funnel-state.service.d.ts +8 -0
  53. package/dist/services/funnel-state.service.js +44 -0
  54. package/dist/services/preview-frame.service.d.ts +2 -2
  55. package/dist/services/preview-frame.service.js +2 -2
  56. package/dist/services/public-env.d.ts +69 -0
  57. package/dist/services/public-env.js +105 -0
  58. package/dist/services/runtime-api.config.d.ts +5 -0
  59. package/dist/services/runtime-api.config.js +12 -7
  60. package/dist/services/runtime-mode.service.d.ts +3 -0
  61. package/dist/services/runtime-mode.service.js +142 -4
  62. package/package.json +8 -2
@@ -0,0 +1,101 @@
1
+ const FONT_REGISTRY = [
2
+ {
3
+ name: 'SF Pro Text',
4
+ category: 'sans-serif',
5
+ fallbacks: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
6
+ weights: [400, 500, 600, 700, 800],
7
+ hasItalic: true,
8
+ source: 'system',
9
+ },
10
+ {
11
+ name: 'Inter',
12
+ category: 'sans-serif',
13
+ fallbacks: '"Segoe UI", sans-serif',
14
+ weights: [400, 500, 600, 700, 800],
15
+ hasItalic: true,
16
+ source: 'google',
17
+ googleFontFamily: 'Inter',
18
+ },
19
+ {
20
+ name: 'Roboto',
21
+ category: 'sans-serif',
22
+ fallbacks: '"Helvetica Neue", Arial, sans-serif',
23
+ weights: [400, 500, 700],
24
+ hasItalic: true,
25
+ source: 'google',
26
+ googleFontFamily: 'Roboto',
27
+ },
28
+ {
29
+ name: 'Open Sans',
30
+ category: 'sans-serif',
31
+ fallbacks: '"Helvetica Neue", Arial, sans-serif',
32
+ weights: [400, 500, 600, 700, 800],
33
+ hasItalic: true,
34
+ source: 'google',
35
+ googleFontFamily: 'Open+Sans',
36
+ },
37
+ {
38
+ name: 'Lato',
39
+ category: 'sans-serif',
40
+ fallbacks: '"Helvetica Neue", Arial, sans-serif',
41
+ weights: [400, 700],
42
+ hasItalic: true,
43
+ source: 'google',
44
+ googleFontFamily: 'Lato',
45
+ },
46
+ {
47
+ name: 'Poppins',
48
+ category: 'sans-serif',
49
+ fallbacks: '"Segoe UI", sans-serif',
50
+ weights: [400, 500, 600, 700],
51
+ hasItalic: true,
52
+ source: 'google',
53
+ googleFontFamily: 'Poppins',
54
+ },
55
+ {
56
+ name: 'Montserrat',
57
+ category: 'sans-serif',
58
+ fallbacks: '"Helvetica Neue", sans-serif',
59
+ weights: [400, 500, 600, 700, 800],
60
+ hasItalic: true,
61
+ source: 'google',
62
+ googleFontFamily: 'Montserrat',
63
+ },
64
+ {
65
+ name: 'Playfair Display',
66
+ category: 'serif',
67
+ fallbacks: 'Georgia, "Times New Roman", serif',
68
+ weights: [400, 500, 600, 700],
69
+ hasItalic: true,
70
+ source: 'google',
71
+ googleFontFamily: 'Playfair+Display',
72
+ },
73
+ {
74
+ name: 'Philosopher',
75
+ category: 'serif',
76
+ fallbacks: 'Georgia, serif',
77
+ weights: [400, 700],
78
+ hasItalic: true,
79
+ source: 'google',
80
+ googleFontFamily: 'Philosopher',
81
+ },
82
+ ];
83
+ export const getFontRegistry = () => {
84
+ return FONT_REGISTRY;
85
+ };
86
+ export const getFontByName = (name) => {
87
+ return FONT_REGISTRY.find((f) => f.name === name);
88
+ };
89
+ export const buildFontFamilyString = (font) => {
90
+ var _a;
91
+ const def = getFontByName(font.family);
92
+ const fallbacks = (_a = def === null || def === void 0 ? void 0 : def.fallbacks) !== null && _a !== void 0 ? _a : font.fallbacks;
93
+ return `"${font.family}", ${fallbacks}`;
94
+ };
95
+ export const buildGoogleFontsUrl = (font) => {
96
+ const def = getFontByName(font.family);
97
+ if (!def || def.source !== 'google' || !def.googleFontFamily)
98
+ return null;
99
+ const weights = def.weights.join(';');
100
+ return `https://fonts.googleapis.com/css2?family=${def.googleFontFamily}:wght@${weights}&display=swap`;
101
+ };
@@ -1,14 +1,60 @@
1
- import type { FunnelManifest } from './funnel.manifest.types';
2
- export type FunnelThemeColors = {
1
+ import type { FunnelManifest } from './funnel.manifest.types.js';
2
+ export type FunnelThemeInterfaceColors = {
3
3
  background: string;
4
+ primary: string;
5
+ primaryText: string;
6
+ secondary: string;
7
+ secondaryText: string;
4
8
  text: string;
5
- accent: string;
6
- button: string;
7
- surface: string;
8
- border: string;
9
+ };
10
+ export type FunnelThemeInterfaceOpacity = Record<keyof FunnelThemeInterfaceColors, number>;
11
+ export type FunnelThemeColors = {
12
+ interface: FunnelThemeInterfaceColors;
13
+ interfaceOpacity: FunnelThemeInterfaceOpacity;
14
+ semantic: {
15
+ accent: string;
16
+ button: string;
17
+ buttonText: string;
18
+ surface: string;
19
+ border: string;
20
+ warning: string;
21
+ danger: string;
22
+ };
23
+ derived: {
24
+ bgMuted: string;
25
+ bgSuccess: string;
26
+ bgWarning: string;
27
+ bgDanger: string;
28
+ bgNeutral: string;
29
+ textMuted: string;
30
+ accentSoft: string;
31
+ accentMuted: string;
32
+ };
33
+ };
34
+ export type FunnelThemeDarkOverrides = {
35
+ interface?: Partial<FunnelThemeInterfaceColors>;
36
+ interfaceOpacity?: Partial<FunnelThemeColors['interfaceOpacity']>;
37
+ semantic?: Partial<FunnelThemeColors['semantic']>;
38
+ derived?: Partial<FunnelThemeColors['derived']>;
39
+ };
40
+ export type FunnelThemeDevicePreset = {
41
+ name: string;
42
+ shellWidth: number;
43
+ maxWidth: number;
44
+ };
45
+ export type FunnelThemeDeviceSize = {
46
+ activePreset: string;
47
+ presets: FunnelThemeDevicePreset[];
48
+ padding: number;
49
+ cornerRadius: number;
50
+ };
51
+ export type FunnelThemeFont = {
52
+ family: string;
53
+ style: 'Normal' | 'Italic';
54
+ fallbacks: string;
9
55
  };
10
56
  export type FunnelThemeTypography = {
11
- fontFamily: string;
57
+ font: FunnelThemeFont;
12
58
  fontSizes: {
13
59
  xs: string;
14
60
  sm: string;
@@ -20,7 +66,9 @@ export type FunnelThemeTypography = {
20
66
  fontWeights: {
21
67
  regular: number;
22
68
  medium: number;
69
+ semibold: number;
23
70
  bold: number;
71
+ heavy: number;
24
72
  };
25
73
  lineHeights: {
26
74
  tight: number;
@@ -29,8 +77,6 @@ export type FunnelThemeTypography = {
29
77
  };
30
78
  };
31
79
  export type FunnelThemeLayout = {
32
- shellWidth: number;
33
- maxWidth: number;
34
80
  safeAreaOffsets: {
35
81
  top: number;
36
82
  right: number;
@@ -39,8 +85,11 @@ export type FunnelThemeLayout = {
39
85
  };
40
86
  };
41
87
  export type FunnelTheme = {
88
+ interfaceMode: 'light' | 'dark';
42
89
  colors: FunnelThemeColors;
90
+ darkOverrides: FunnelThemeDarkOverrides;
43
91
  typography: FunnelThemeTypography;
92
+ deviceSize: FunnelThemeDeviceSize;
44
93
  layout: FunnelThemeLayout;
45
94
  };
46
95
  export type FunnelRuntimeDefaults = {
@@ -56,4 +105,6 @@ export type PublishedFunnelSnapshot = {
56
105
  };
57
106
  export declare const defaultFunnelTheme: FunnelTheme;
58
107
  export declare const defaultFunnelRuntimeDefaults: FunnelRuntimeDefaults;
59
- export declare const createThemeCssVariables: (theme: FunnelTheme) => Record<string, string>;
108
+ export declare const normalizeFunnelTheme: (theme: unknown) => FunnelTheme;
109
+ export declare const resolveColors: (theme: FunnelTheme, mode: "light" | "dark") => FunnelThemeColors;
110
+ export declare const createThemeCssVariables: (theme: FunnelTheme, mode?: "light" | "dark") => Record<string, string>;
@@ -1,14 +1,50 @@
1
+ // --- Default theme ---
1
2
  export const defaultFunnelTheme = {
3
+ interfaceMode: 'light',
2
4
  colors: {
3
- background: '#ffffff',
4
- text: '#262729',
5
- accent: '#3f51b5',
6
- button: '#4db53f',
7
- surface: '#ffffff',
8
- border: '#dce2ff',
5
+ interface: {
6
+ background: '#f4effd',
7
+ primary: '#ff9332',
8
+ primaryText: '#ffffff',
9
+ secondary: '#3f51b5',
10
+ secondaryText: '#ffffff',
11
+ text: '#404040',
12
+ },
13
+ interfaceOpacity: {
14
+ background: 100,
15
+ primary: 100,
16
+ primaryText: 100,
17
+ secondary: 100,
18
+ secondaryText: 100,
19
+ text: 100,
20
+ },
21
+ semantic: {
22
+ accent: '#3f51b5',
23
+ button: '#4db53f',
24
+ buttonText: '#ffffff',
25
+ surface: '#ffffff',
26
+ border: '#dce2ff',
27
+ warning: '#f28100',
28
+ danger: '#dd3333',
29
+ },
30
+ derived: {
31
+ bgMuted: '#f5f7fb',
32
+ bgSuccess: '#f0f7f1',
33
+ bgWarning: '#fffdf9',
34
+ bgDanger: '#fff5f2',
35
+ bgNeutral: '#f4f1ef',
36
+ textMuted: '#4d5470',
37
+ accentSoft: 'rgb(63 81 181 / 15%)',
38
+ accentMuted: '#a1a9d6',
39
+ },
9
40
  },
41
+ darkOverrides: {},
10
42
  typography: {
11
- fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
43
+ font: {
44
+ family: 'SF Pro Text',
45
+ style: 'Normal',
46
+ fallbacks: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
47
+ },
12
48
  fontSizes: {
13
49
  xs: '12px',
14
50
  sm: '14px',
@@ -20,7 +56,9 @@ export const defaultFunnelTheme = {
20
56
  fontWeights: {
21
57
  regular: 400,
22
58
  medium: 500,
59
+ semibold: 600,
23
60
  bold: 700,
61
+ heavy: 800,
24
62
  },
25
63
  lineHeights: {
26
64
  tight: 1.2,
@@ -28,15 +66,14 @@ export const defaultFunnelTheme = {
28
66
  relaxed: 1.65,
29
67
  },
30
68
  },
69
+ deviceSize: {
70
+ activePreset: 'Universal',
71
+ presets: [{ name: 'Universal', shellWidth: 430, maxWidth: 430 }],
72
+ padding: 16,
73
+ cornerRadius: 16,
74
+ },
31
75
  layout: {
32
- shellWidth: 430,
33
- maxWidth: 430,
34
- safeAreaOffsets: {
35
- top: 0,
36
- right: 0,
37
- bottom: 0,
38
- left: 0,
39
- },
76
+ safeAreaOffsets: { top: 0, right: 0, bottom: 0, left: 0 },
40
77
  },
41
78
  };
42
79
  export const defaultFunnelRuntimeDefaults = {
@@ -44,26 +81,309 @@ export const defaultFunnelRuntimeDefaults = {
44
81
  funnelId: null,
45
82
  sdkPublishableKey: null,
46
83
  };
47
- export const createThemeCssVariables = (theme) => {
84
+ const asRecord = (value) => {
85
+ return typeof value === 'object' && value !== null ? value : {};
86
+ };
87
+ const str = (value, fallback) => {
88
+ return typeof value === 'string' ? value : fallback;
89
+ };
90
+ const num = (value, fallback) => {
91
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
92
+ };
93
+ const parseLegacyFontFamily = (value, fallback) => {
94
+ if (typeof value !== 'string')
95
+ return fallback;
96
+ const parts = value
97
+ .split(',')
98
+ .map((part) => part.trim())
99
+ .filter(Boolean);
100
+ if (parts.length === 0)
101
+ return fallback;
102
+ const family = parts[0].replace(/^['"]|['"]$/g, '');
103
+ const fallbacks = parts.slice(1).join(', ');
104
+ return {
105
+ family: family || fallback.family,
106
+ style: fallback.style,
107
+ fallbacks: fallbacks || fallback.fallbacks,
108
+ };
109
+ };
110
+ export const normalizeFunnelTheme = (theme) => {
111
+ var _a, _b, _c, _d;
112
+ const raw = asRecord(theme);
113
+ const defaults = defaultFunnelTheme;
114
+ const rawColors = asRecord(raw.colors);
115
+ const rawInterface = asRecord(rawColors.interface);
116
+ const rawOpacity = asRecord(rawColors.interfaceOpacity);
117
+ const rawSemantic = asRecord(rawColors.semantic);
118
+ const rawDerived = asRecord(rawColors.derived);
119
+ const rawLegacyColors = rawInterface.background || rawSemantic.accent ? {} : rawColors;
120
+ const rawDarkOverrides = asRecord(raw.darkOverrides);
121
+ const rawDarkInterface = asRecord(rawDarkOverrides.interface);
122
+ const rawDarkSemantic = asRecord(rawDarkOverrides.semantic);
123
+ const rawDarkDerived = asRecord(rawDarkOverrides.derived);
124
+ const rawDarkOpacity = asRecord(rawDarkOverrides.interfaceOpacity);
125
+ const rawTypo = asRecord(raw.typography);
126
+ const rawFont = asRecord(rawTypo.font);
127
+ const rawLegacyFont = parseLegacyFontFamily(rawTypo.fontFamily, defaults.typography.font);
128
+ const rawSizes = asRecord(rawTypo.fontSizes);
129
+ const rawWeights = asRecord(rawTypo.fontWeights);
130
+ const rawLineHeights = asRecord(rawTypo.lineHeights);
131
+ const rawDevice = asRecord(raw.deviceSize);
132
+ const rawPresets = Array.isArray(rawDevice.presets) ? rawDevice.presets : [];
133
+ const rawLayout = asRecord(raw.layout);
134
+ const rawSafe = asRecord(rawLayout.safeAreaOffsets);
135
+ const hasLegacyDevicePreset = typeof rawLayout.shellWidth === 'number' || typeof rawLayout.maxWidth === 'number';
136
+ const darkOverrides = {};
137
+ if (Object.keys(rawDarkInterface).length > 0) {
138
+ darkOverrides.interface = {};
139
+ if (typeof rawDarkInterface.background === 'string') {
140
+ darkOverrides.interface.background = rawDarkInterface.background;
141
+ }
142
+ if (typeof rawDarkInterface.primary === 'string') {
143
+ darkOverrides.interface.primary = rawDarkInterface.primary;
144
+ }
145
+ if (typeof rawDarkInterface.primaryText === 'string') {
146
+ darkOverrides.interface.primaryText = rawDarkInterface.primaryText;
147
+ }
148
+ if (typeof rawDarkInterface.secondary === 'string') {
149
+ darkOverrides.interface.secondary = rawDarkInterface.secondary;
150
+ }
151
+ if (typeof rawDarkInterface.secondaryText === 'string') {
152
+ darkOverrides.interface.secondaryText = rawDarkInterface.secondaryText;
153
+ }
154
+ if (typeof rawDarkInterface.text === 'string') {
155
+ darkOverrides.interface.text = rawDarkInterface.text;
156
+ }
157
+ }
158
+ if (Object.keys(rawDarkSemantic).length > 0) {
159
+ darkOverrides.semantic = {};
160
+ for (const key of Object.keys(rawDarkSemantic)) {
161
+ if (typeof rawDarkSemantic[key] === 'string') {
162
+ darkOverrides.semantic[key] = rawDarkSemantic[key];
163
+ }
164
+ }
165
+ }
166
+ if (Object.keys(rawDarkDerived).length > 0) {
167
+ darkOverrides.derived = {};
168
+ for (const key of Object.keys(rawDarkDerived)) {
169
+ if (typeof rawDarkDerived[key] === 'string') {
170
+ darkOverrides.derived[key] = rawDarkDerived[key];
171
+ }
172
+ }
173
+ }
174
+ if (Object.keys(rawDarkOpacity).length > 0) {
175
+ darkOverrides.interfaceOpacity = {};
176
+ if (typeof rawDarkOpacity.background === 'number') {
177
+ darkOverrides.interfaceOpacity.background = rawDarkOpacity.background;
178
+ }
179
+ if (typeof rawDarkOpacity.primary === 'number') {
180
+ darkOverrides.interfaceOpacity.primary = rawDarkOpacity.primary;
181
+ }
182
+ if (typeof rawDarkOpacity.primaryText === 'number') {
183
+ darkOverrides.interfaceOpacity.primaryText = rawDarkOpacity.primaryText;
184
+ }
185
+ if (typeof rawDarkOpacity.secondary === 'number') {
186
+ darkOverrides.interfaceOpacity.secondary = rawDarkOpacity.secondary;
187
+ }
188
+ if (typeof rawDarkOpacity.secondaryText === 'number') {
189
+ darkOverrides.interfaceOpacity.secondaryText = rawDarkOpacity.secondaryText;
190
+ }
191
+ if (typeof rawDarkOpacity.text === 'number') {
192
+ darkOverrides.interfaceOpacity.text = rawDarkOpacity.text;
193
+ }
194
+ }
195
+ return {
196
+ interfaceMode: str(raw.interfaceMode, defaults.interfaceMode),
197
+ colors: {
198
+ interface: {
199
+ background: str(rawInterface.background, str(rawLegacyColors.background, defaults.colors.interface.background)),
200
+ primary: str(rawInterface.primary, str(rawLegacyColors.primary, str(rawLegacyColors.accent, defaults.colors.interface.primary))),
201
+ primaryText: str(rawInterface.primaryText, str(rawLegacyColors.primaryText, str(rawLegacyColors.buttonText, defaults.colors.interface.primaryText))),
202
+ secondary: str(rawInterface.secondary, str(rawLegacyColors.secondary, str(rawLegacyColors.accent, defaults.colors.interface.secondary))),
203
+ secondaryText: str(rawInterface.secondaryText, str(rawLegacyColors.secondaryText, str(rawLegacyColors.text, defaults.colors.interface.secondaryText))),
204
+ text: str(rawInterface.text, str(rawLegacyColors.text, defaults.colors.interface.text)),
205
+ },
206
+ interfaceOpacity: {
207
+ background: num(rawOpacity.background, defaults.colors.interfaceOpacity.background),
208
+ primary: num(rawOpacity.primary, defaults.colors.interfaceOpacity.primary),
209
+ primaryText: num(rawOpacity.primaryText, defaults.colors.interfaceOpacity.primaryText),
210
+ secondary: num(rawOpacity.secondary, defaults.colors.interfaceOpacity.secondary),
211
+ secondaryText: num(rawOpacity.secondaryText, defaults.colors.interfaceOpacity.secondaryText),
212
+ text: num(rawOpacity.text, defaults.colors.interfaceOpacity.text),
213
+ },
214
+ semantic: {
215
+ accent: str(rawSemantic.accent, str(rawLegacyColors.accent, str(rawLegacyColors.primary, defaults.colors.semantic.accent))),
216
+ button: str(rawSemantic.button, str(rawLegacyColors.button, defaults.colors.semantic.button)),
217
+ buttonText: str(rawSemantic.buttonText, str(rawLegacyColors.buttonText, defaults.colors.semantic.buttonText)),
218
+ surface: str(rawSemantic.surface, str(rawLegacyColors.surface, defaults.colors.semantic.surface)),
219
+ border: str(rawSemantic.border, str(rawLegacyColors.border, defaults.colors.semantic.border)),
220
+ warning: str(rawSemantic.warning, str(rawLegacyColors.warning, defaults.colors.semantic.warning)),
221
+ danger: str(rawSemantic.danger, str(rawLegacyColors.danger, defaults.colors.semantic.danger)),
222
+ },
223
+ derived: {
224
+ bgMuted: str(rawDerived.bgMuted, defaults.colors.derived.bgMuted),
225
+ bgSuccess: str(rawDerived.bgSuccess, defaults.colors.derived.bgSuccess),
226
+ bgWarning: str(rawDerived.bgWarning, defaults.colors.derived.bgWarning),
227
+ bgDanger: str(rawDerived.bgDanger, defaults.colors.derived.bgDanger),
228
+ bgNeutral: str(rawDerived.bgNeutral, defaults.colors.derived.bgNeutral),
229
+ textMuted: str(rawDerived.textMuted, defaults.colors.derived.textMuted),
230
+ accentSoft: str(rawDerived.accentSoft, defaults.colors.derived.accentSoft),
231
+ accentMuted: str(rawDerived.accentMuted, defaults.colors.derived.accentMuted),
232
+ },
233
+ },
234
+ darkOverrides,
235
+ typography: {
236
+ font: {
237
+ family: str(rawFont.family, rawLegacyFont.family),
238
+ style: str(rawFont.style, rawLegacyFont.style),
239
+ fallbacks: str(rawFont.fallbacks, rawLegacyFont.fallbacks),
240
+ },
241
+ fontSizes: {
242
+ xs: str(rawSizes.xs, defaults.typography.fontSizes.xs),
243
+ sm: str(rawSizes.sm, defaults.typography.fontSizes.sm),
244
+ md: str(rawSizes.md, defaults.typography.fontSizes.md),
245
+ lg: str(rawSizes.lg, defaults.typography.fontSizes.lg),
246
+ xl: str(rawSizes.xl, defaults.typography.fontSizes.xl),
247
+ xxl: str(rawSizes.xxl, defaults.typography.fontSizes.xxl),
248
+ },
249
+ fontWeights: {
250
+ regular: num(rawWeights.regular, defaults.typography.fontWeights.regular),
251
+ medium: num(rawWeights.medium, defaults.typography.fontWeights.medium),
252
+ semibold: num(rawWeights.semibold, defaults.typography.fontWeights.semibold),
253
+ bold: num(rawWeights.bold, defaults.typography.fontWeights.bold),
254
+ heavy: num(rawWeights.heavy, defaults.typography.fontWeights.heavy),
255
+ },
256
+ lineHeights: {
257
+ tight: num(rawLineHeights.tight, defaults.typography.lineHeights.tight),
258
+ normal: num(rawLineHeights.normal, defaults.typography.lineHeights.normal),
259
+ relaxed: num(rawLineHeights.relaxed, defaults.typography.lineHeights.relaxed),
260
+ },
261
+ },
262
+ deviceSize: {
263
+ activePreset: str(rawDevice.activePreset, defaults.deviceSize.activePreset),
264
+ presets: rawPresets.length > 0
265
+ ? rawPresets.map((preset) => {
266
+ const record = asRecord(preset);
267
+ return {
268
+ name: str(record.name, 'Universal'),
269
+ shellWidth: num(record.shellWidth, 430),
270
+ maxWidth: num(record.maxWidth, 430),
271
+ };
272
+ })
273
+ : hasLegacyDevicePreset
274
+ ? [
275
+ {
276
+ name: defaults.deviceSize.activePreset,
277
+ shellWidth: num(rawLayout.shellWidth, (_b = (_a = defaults.deviceSize.presets[0]) === null || _a === void 0 ? void 0 : _a.shellWidth) !== null && _b !== void 0 ? _b : 430),
278
+ maxWidth: num(rawLayout.maxWidth, (_d = (_c = defaults.deviceSize.presets[0]) === null || _c === void 0 ? void 0 : _c.maxWidth) !== null && _d !== void 0 ? _d : 430),
279
+ },
280
+ ]
281
+ : defaults.deviceSize.presets,
282
+ padding: num(rawDevice.padding, defaults.deviceSize.padding),
283
+ cornerRadius: num(rawDevice.cornerRadius, defaults.deviceSize.cornerRadius),
284
+ },
285
+ layout: {
286
+ safeAreaOffsets: {
287
+ top: num(rawSafe.top, defaults.layout.safeAreaOffsets.top),
288
+ right: num(rawSafe.right, defaults.layout.safeAreaOffsets.right),
289
+ bottom: num(rawSafe.bottom, defaults.layout.safeAreaOffsets.bottom),
290
+ left: num(rawSafe.left, defaults.layout.safeAreaOffsets.left),
291
+ },
292
+ },
293
+ };
294
+ };
295
+ // --- Color resolution for light/dark ---
296
+ export const resolveColors = (theme, mode) => {
297
+ const normalizedTheme = normalizeFunnelTheme(theme);
298
+ if (mode === 'light')
299
+ return normalizedTheme.colors;
300
+ return {
301
+ interface: Object.assign(Object.assign({}, normalizedTheme.colors.interface), normalizedTheme.darkOverrides.interface),
302
+ interfaceOpacity: Object.assign(Object.assign({}, normalizedTheme.colors.interfaceOpacity), normalizedTheme.darkOverrides.interfaceOpacity),
303
+ semantic: Object.assign(Object.assign({}, normalizedTheme.colors.semantic), normalizedTheme.darkOverrides.semantic),
304
+ derived: Object.assign(Object.assign({}, normalizedTheme.colors.derived), normalizedTheme.darkOverrides.derived),
305
+ };
306
+ };
307
+ // --- Opacity helpers ---
308
+ const hexToRgb = (hex) => {
309
+ const cleaned = hex.replace('#', '');
310
+ if (cleaned.length !== 6)
311
+ return null;
312
+ const value = parseInt(cleaned, 16);
313
+ if (Number.isNaN(value))
314
+ return null;
315
+ return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
316
+ };
317
+ const applyOpacity = (color, opacity) => {
318
+ if (opacity >= 100)
319
+ return color;
320
+ const rgb = hexToRgb(color);
321
+ if (!rgb)
322
+ return color;
323
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${(opacity / 100).toFixed(2)})`;
324
+ };
325
+ // --- Active preset resolution ---
326
+ const resolveActivePreset = (deviceSize) => {
327
+ var _a, _b;
328
+ return ((_b = (_a = deviceSize.presets.find((p) => p.name === deviceSize.activePreset)) !== null && _a !== void 0 ? _a : deviceSize.presets[0]) !== null && _b !== void 0 ? _b : { name: 'Universal', shellWidth: 430, maxWidth: 430 });
329
+ };
330
+ // --- CSS variable generation ---
331
+ export const createThemeCssVariables = (theme, mode) => {
332
+ const normalizedTheme = normalizeFunnelTheme(theme);
333
+ const effectiveMode = mode !== null && mode !== void 0 ? mode : normalizedTheme.interfaceMode;
334
+ const colors = resolveColors(normalizedTheme, effectiveMode);
335
+ const preset = resolveActivePreset(normalizedTheme.deviceSize);
336
+ const fontFamily = `"${normalizedTheme.typography.font.family}", ${normalizedTheme.typography.font.fallbacks}`;
48
337
  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`,
338
+ '--color-bg': applyOpacity(colors.interface.background, colors.interfaceOpacity.background),
339
+ '--color-bg-raw': colors.interface.background,
340
+ '--color-bg-opacity': String(colors.interfaceOpacity.background),
341
+ '--color-primary': applyOpacity(colors.interface.primary, colors.interfaceOpacity.primary),
342
+ '--color-primary-raw': colors.interface.primary,
343
+ '--color-primary-opacity': String(colors.interfaceOpacity.primary),
344
+ '--color-primary-text': applyOpacity(colors.interface.primaryText, colors.interfaceOpacity.primaryText),
345
+ '--color-primary-text-raw': colors.interface.primaryText,
346
+ '--color-primary-text-opacity': String(colors.interfaceOpacity.primaryText),
347
+ '--color-secondary': applyOpacity(colors.interface.secondary, colors.interfaceOpacity.secondary),
348
+ '--color-secondary-raw': colors.interface.secondary,
349
+ '--color-secondary-opacity': String(colors.interfaceOpacity.secondary),
350
+ '--color-secondary-text': applyOpacity(colors.interface.secondaryText, colors.interfaceOpacity.secondaryText),
351
+ '--color-secondary-text-raw': colors.interface.secondaryText,
352
+ '--color-secondary-text-opacity': String(colors.interfaceOpacity.secondaryText),
353
+ '--color-text': applyOpacity(colors.interface.text, colors.interfaceOpacity.text),
354
+ '--color-text-raw': colors.interface.text,
355
+ '--color-text-opacity': String(colors.interfaceOpacity.text),
356
+ '--color-accent': colors.semantic.accent,
357
+ '--color-button': colors.semantic.button,
358
+ '--color-button-text': colors.semantic.buttonText,
359
+ '--color-surface': colors.semantic.surface,
360
+ '--color-border': colors.semantic.border,
361
+ '--color-warning': colors.semantic.warning,
362
+ '--color-danger': colors.semantic.danger,
363
+ '--color-bg-muted': colors.derived.bgMuted,
364
+ '--color-bg-success': colors.derived.bgSuccess,
365
+ '--color-bg-warning': colors.derived.bgWarning,
366
+ '--color-bg-danger': colors.derived.bgDanger,
367
+ '--color-bg-neutral': colors.derived.bgNeutral,
368
+ '--color-text-muted': colors.derived.textMuted,
369
+ '--color-accent-soft': colors.derived.accentSoft,
370
+ '--color-accent-muted': colors.derived.accentMuted,
371
+ '--font-family-base': fontFamily,
372
+ '--font-family-display': fontFamily,
373
+ '--font-weight-regular': String(normalizedTheme.typography.fontWeights.regular),
374
+ '--font-weight-medium': String(normalizedTheme.typography.fontWeights.medium),
375
+ '--font-weight-semibold': String(normalizedTheme.typography.fontWeights.semibold),
376
+ '--font-weight-bold': String(normalizedTheme.typography.fontWeights.bold),
377
+ '--font-weight-heavy': String(normalizedTheme.typography.fontWeights.heavy),
378
+ '--line-height-body': String(normalizedTheme.typography.lineHeights.normal),
379
+ '--line-height-heading': String(normalizedTheme.typography.lineHeights.tight),
380
+ '--shell-width': `${preset.shellWidth}px`,
381
+ '--max-width': `${preset.maxWidth}px`,
382
+ '--padding': `${normalizedTheme.deviceSize.padding}px`,
383
+ '--corner-radius': `${normalizedTheme.deviceSize.cornerRadius}px`,
384
+ '--safe-area-offset-top': `${normalizedTheme.layout.safeAreaOffsets.top}px`,
385
+ '--safe-area-offset-right': `${normalizedTheme.layout.safeAreaOffsets.right}px`,
386
+ '--safe-area-offset-bottom': `${normalizedTheme.layout.safeAreaOffsets.bottom}px`,
387
+ '--safe-area-offset-left': `${normalizedTheme.layout.safeAreaOffsets.left}px`,
68
388
  };
69
389
  };
@@ -1,4 +1,4 @@
1
- import type { FunnelStepKind, FunnelStepType } from '../steps/types';
1
+ import type { FunnelStepKind, FunnelStepType } from '../steps/types.js';
2
2
  export type TemplateArchitectureVersion = number;
3
3
  export type FunnelManifestStep = {
4
4
  id: string;
@@ -9,7 +9,16 @@ export type FunnelManifestStep = {
9
9
  kind?: FunnelStepKind;
10
10
  name?: string;
11
11
  title: string;
12
+ assetIds?: readonly string[];
12
13
  };
14
+ export type FunnelManifestImageAsset = {
15
+ type: 'image';
16
+ src: string;
17
+ width?: number;
18
+ height?: number;
19
+ preload?: 'idle' | false;
20
+ };
21
+ export type FunnelManifestAsset = string | FunnelManifestImageAsset;
13
22
  export type FunnelManifestEntryPoint = {
14
23
  id: string;
15
24
  stepId: string;
@@ -20,14 +29,11 @@ export type FunnelManifestEdge = {
20
29
  conditionId?: string;
21
30
  };
22
31
  export type FunnelManifestExperimentVariant = {
23
- id: string;
24
- label: string;
25
- trafficPercent: number;
32
+ variantKey: string;
26
33
  routeToStepId: string;
27
34
  };
28
35
  export type FunnelManifestExperiment = {
29
36
  experimentId: string;
30
- status: 'draft' | 'active';
31
37
  stepId: string;
32
38
  variants: readonly FunnelManifestExperimentVariant[];
33
39
  };
@@ -41,9 +47,9 @@ export type FunnelManifest = {
41
47
  width: number;
42
48
  height: number;
43
49
  };
44
- assets: Record<string, string>;
50
+ assets: Record<string, FunnelManifestAsset>;
45
51
  steps: readonly FunnelManifestStep[];
46
- entryPoints: readonly FunnelManifestEntryPoint[];
52
+ entryPoints?: readonly FunnelManifestEntryPoint[];
47
53
  edgesByStepId: Partial<Record<string, readonly FunnelManifestEdge[]>>;
48
54
  experiments: readonly FunnelManifestExperiment[];
49
55
  };