@xyd-js/host 0.1.0-build.158

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.
@@ -0,0 +1,279 @@
1
+ // TODO: ts + use bunled
2
+ // TODO: custom user logic about context
3
+ (function () {
4
+ const abTestingSettings = window.__xydAbTestingSettings;
5
+ const abTestingProviders = abTestingSettings?.providers || {}
6
+
7
+ // Context storage configuration
8
+ const contextMaxAge = abTestingSettings?.contextMaxAge || 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
9
+ const contextStorageKey = abTestingSettings?.contextStorageKey || "__xydABContextKey";
10
+
11
+ // Utility functions for base64 encoding/decoding
12
+ function isBase64(str) {
13
+ try {
14
+ return btoa(atob(str)) === str;
15
+ } catch (err) {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function encodeContext(context) {
21
+ return btoa(JSON.stringify(context));
22
+ }
23
+
24
+ function decodeContext(encodedContext) {
25
+ try {
26
+ if (!isBase64(encodedContext)) {
27
+ return null;
28
+ }
29
+ return JSON.parse(atob(encodedContext));
30
+ } catch (err) {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // Context storage functions
36
+ function saveContext(context) {
37
+ try {
38
+ const encodedContext = encodeContext(context);
39
+ localStorage.setItem(contextStorageKey, encodedContext);
40
+ } catch (err) {
41
+ console.debug('Error saving context to localStorage:', err);
42
+ }
43
+ }
44
+
45
+ function loadContext() {
46
+ try {
47
+ const encodedContext = localStorage.getItem(contextStorageKey);
48
+ if (!encodedContext) {
49
+ return null;
50
+ }
51
+
52
+ const context = decodeContext(encodedContext);
53
+ if (!context) {
54
+ // Invalid base64, remove the corrupted data
55
+ localStorage.removeItem(contextStorageKey);
56
+ return null;
57
+ }
58
+
59
+ // Check if context is expired
60
+ if (context.timestamp && Date.now() - context.timestamp > contextMaxAge) {
61
+ localStorage.removeItem(contextStorageKey);
62
+ return null;
63
+ }
64
+
65
+ return context;
66
+ } catch (err) {
67
+ console.debug('Error loading context from localStorage:', err);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ const xhr = new XMLHttpRequest();
73
+ let featuresUrl = ""
74
+
75
+ let ctx;
76
+
77
+ function randomId() {
78
+ return crypto.randomUUID();
79
+ }
80
+
81
+ // Try to load existing context, create new one if not found or expired
82
+ let existingContext = loadContext();
83
+ let userId = existingContext?.userId || randomId();
84
+ if (!existingContext?.userId) {
85
+ // Save context with timestamp
86
+ const contextToSave = {
87
+ userId: userId,
88
+ timestamp: Date.now(),
89
+ };
90
+ saveContext(contextToSave);
91
+ }
92
+
93
+ if (abTestingProviders?.growthbook) {
94
+ if (!abTestingProviders.growthbook.clientKey) {
95
+ console.error('GrowthBook client key is not set in settings.');
96
+ delete window.__xydAbTestingSettings;
97
+ return;
98
+ }
99
+
100
+ featuresUrl = `${'https://cdn.growthbook.io'}/api/features/${abTestingProviders?.growthbook.clientKey}`;
101
+ }
102
+
103
+ if (abTestingProviders?.launchdarkly) {
104
+ if (!abTestingProviders.launchdarkly.env) {
105
+ console.error('LaunchDarkly env is not set in settings.');
106
+ delete window.__xydAbTestingSettings;
107
+ return;
108
+ }
109
+
110
+ ctx = {
111
+ kind: "user",
112
+ key: userId
113
+ }
114
+
115
+ const ctxBase64 = btoa(JSON.stringify(ctx));
116
+
117
+ // TODO: use sdk in the future
118
+ featuresUrl = `https://app.launchdarkly.com/sdk/evalx/${abTestingProviders.launchdarkly.env}/contexts/${ctxBase64}`
119
+ }
120
+
121
+ xhr.open('GET', featuresUrl, false);
122
+ xhr.send(null);
123
+ if (xhr.status != 200) {
124
+ console.error(`Error fetching feature flags: ${xhr.status} ${xhr.statusText}`);
125
+ delete window.__xydAbTestingSettings;
126
+ return;
127
+ }
128
+
129
+ try {
130
+ const resp = JSON.parse(xhr.responseText);
131
+ let features = {};
132
+ let client;
133
+
134
+ if (abTestingProviders?.growthbook) {
135
+ // Normalize GrowthBook features: extract defaultValue from each feature
136
+ const rawFeatures = resp.features || {};
137
+ Object.entries(rawFeatures).forEach(([flagKey, flagData]) => {
138
+ if (flagData && typeof flagData === 'object' && 'defaultValue' in flagData) {
139
+ features[flagKey] = flagData.defaultValue;
140
+ }
141
+ });
142
+
143
+ // TODO: finish
144
+ const gbContext = {
145
+ apiHost: abTestingProviders.growthbook.apiHost || 'https://cdn.growthbook.io',
146
+ clientKey: abTestingProviders.growthbook.clientKey,
147
+ attributes: {
148
+ id: userId,
149
+ },
150
+ };
151
+ /*
152
+ * optional init options
153
+ * @see https://docs.growthbook.io/lib/js#switching-to-init
154
+ */
155
+ const initOptions = {
156
+ timeout: 2000,
157
+ payload: resp,
158
+ // streaming: false,
159
+ };
160
+
161
+ // TODO: Add proper type declarations for OpenFeature and GrowthbookProvider
162
+ const OpenFeature = window.OpenFeature;
163
+ const GrowthbookProvider = window.GrowthbookProvider;
164
+
165
+ const ghOpenFeature = new GrowthbookProvider.GrowthbookClientProvider(gbContext, initOptions)
166
+ if (OpenFeature && GrowthbookProvider) {
167
+ OpenFeature.OpenFeature.setProvider(ghOpenFeature);
168
+ client = OpenFeature.OpenFeature.getClient();
169
+ }
170
+ }
171
+
172
+ if (abTestingProviders?.launchdarkly) {
173
+ // Normalize LaunchDarkly features: extract value from each flag
174
+ Object.entries(resp).forEach(([flagKey, flagData]) => {
175
+ if (flagData && typeof flagData === 'object' && 'value' in flagData) {
176
+ features[flagKey] = flagData.value;
177
+ }
178
+ });
179
+
180
+ // Reconstruct bootstrap state from API response
181
+ const bootstrapFlags = {};
182
+ const flagsState = {};
183
+
184
+ // TODO: !!! USE LAUNCHDARKLY SDK !!! BUT WE NEED TO DO THIS SYNC
185
+ Object.entries(resp).forEach(([flagKey, flagData]) => {
186
+ if (flagData && typeof flagData === 'object' && 'value' in flagData) {
187
+ const flag = flagData;
188
+ // Set the flag value in bootstrap
189
+ bootstrapFlags[flagKey] = flag.value;
190
+
191
+ // Set the full flag state
192
+ flagsState[flagKey] = flag
193
+ }
194
+ });
195
+
196
+ // TODO: use OpenFeature.OpenFeature.setProvider
197
+ const OpenFeature = window.OpenFeature;
198
+ const LaunchDarklyProvider = window.LaunchDarklyProvider;
199
+
200
+ if (OpenFeature && LaunchDarklyProvider) {
201
+ OpenFeature.OpenFeature.setProvider(
202
+ new LaunchDarklyProvider.LaunchDarklyClientProvider(
203
+ abTestingProviders.launchdarkly.env,
204
+ {
205
+ bootstrap: {
206
+ ...bootstrapFlags,
207
+ $flagsState: flagsState
208
+ }
209
+ }
210
+ ),
211
+ ctx
212
+ );
213
+ client = OpenFeature.OpenFeature.getClient();
214
+ }
215
+ }
216
+
217
+ if (client) {
218
+ window.openfeature = client
219
+ }
220
+ console.log
221
+ const events = client.emitterAccessor()
222
+ events.emit("PROVIDER_READY")
223
+ events.emit("PROVIDER_CONFIGURATION_CHANGED")
224
+
225
+ // Create dynamic CSS for feature flags
226
+ let cssRules = [];
227
+
228
+ // Default rule: hide all feature elements
229
+ cssRules.push('[data-feature-flag] { display: none !important; }');
230
+
231
+ // Generate rules for each feature flag
232
+ // TODO: !!! FOR SOME REASON WE NEED THIS HACK EVEN IF WE EMIT THE EVENTS (maybe it's wrong used?) - BUT CHANGE THAT IN THE FUTURE !!!
233
+ setTimeout(() => {
234
+ Object.entries(features || {}).forEach(([featureKey, flagValue]) => {
235
+ // Determine the type of feature flag and get its current value
236
+ let currentValue;
237
+ // Now features is normalized: {[flag]: value}
238
+
239
+ // Check if it's a boolean feature
240
+ if (typeof flagValue === 'boolean') {
241
+ // TODO: impl default values from markdown + navigation
242
+ currentValue = client.getBooleanValue(featureKey, false);
243
+ }
244
+ // Check if it's a string feature
245
+ else if (typeof flagValue === 'string') {
246
+ currentValue = client.getStringValue(featureKey, "");
247
+ }
248
+ // Check if it's a number feature
249
+ else if (typeof flagValue === 'number') {
250
+ currentValue = client.getNumberValue(featureKey, 0);
251
+ }
252
+ // For object features, use getObjectValue
253
+ else if (typeof flagValue === 'object' && flagValue !== null) {
254
+ currentValue = client.getObjectValue(featureKey);
255
+ }
256
+ // Fallback to string evaluation for unknown types
257
+ else {
258
+ currentValue = client.getStringValue(featureKey, "");
259
+ }
260
+
261
+ if (currentValue !== undefined) {
262
+ // Create rule to show elements when feature flag matches the expected value
263
+ cssRules.push(`[data-feature-flag="${featureKey}"][data-feature-match="${currentValue}"] { display: block !important; }`);
264
+ }
265
+ });
266
+
267
+ // Apply the CSS rules
268
+ if (cssRules.length > 0) {
269
+ const style = document.createElement('style');
270
+ style.textContent = cssRules.join('\n');
271
+ document.head.appendChild(style);
272
+ }
273
+ })
274
+ } catch (e) {
275
+ console.error('Error processing feature flags:', e);
276
+ } finally {
277
+ delete window.__xydAbTestingSettings;
278
+ }
279
+ })();
@@ -0,0 +1,14 @@
1
+ (function () {
2
+ (window.requestAnimationFrame ?? window.setTimeout)(() => {
3
+ if (typeof window === "undefined") {
4
+ return
5
+ }
6
+
7
+ const bannerHeight = document.querySelector("xyd-banner")?.clientHeight ?? 0;
8
+ if (!bannerHeight) {
9
+ return
10
+ }
11
+
12
+ document.documentElement.style.setProperty("--xyd-banner-height-dynamic", `${String(bannerHeight)}px`)
13
+ })
14
+ })()
@@ -0,0 +1,21 @@
1
+ (function () {
2
+ try {
3
+ var theme = localStorage.getItem('xyd-color-scheme') || 'auto';
4
+ var isDark = false;
5
+
6
+ if (theme === 'dark') {
7
+ isDark = true;
8
+ } else if (theme === 'auto') {
9
+ isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
10
+ }
11
+
12
+ if (isDark) {
13
+ document.documentElement.setAttribute('data-color-scheme', 'dark');
14
+ }
15
+ } catch (e) {
16
+ // Fallback to system preference if localStorage fails
17
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
18
+ document.documentElement.setAttribute('data-color-scheme', 'dark');
19
+ }
20
+ }
21
+ })();