@supashiphq/javascript-sdk 0.7.7

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/dist/index.js ADDED
@@ -0,0 +1,1072 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SupaClient: () => SupaClient,
24
+ ToolbarPlugin: () => SupaToolbarPlugin
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/utils.ts
29
+ function sleep(ms) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+ async function retry(fn, maxAttempts = 3, backoff = 1e3, onRetry) {
33
+ let lastError;
34
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
35
+ try {
36
+ return await fn();
37
+ } catch (error) {
38
+ lastError = error;
39
+ const willRetry = attempt < maxAttempts;
40
+ if (onRetry) {
41
+ onRetry(attempt, lastError, willRetry);
42
+ }
43
+ if (!willRetry) break;
44
+ await sleep(backoff * Math.pow(2, attempt - 1));
45
+ }
46
+ }
47
+ throw lastError;
48
+ }
49
+
50
+ // src/plugins/toolbar-plugin.ts
51
+ var DEFAULT_STORAGE_KEY = "supaship-feature-overrides";
52
+ var NO_FEATURES_MESSAGE = `No feature flags configured in the client.`;
53
+ var SupaToolbarPlugin = class {
54
+ constructor(config = {}) {
55
+ this.name = "toolbar-plugin";
56
+ this.storageKey = DEFAULT_STORAGE_KEY;
57
+ this.config = {
58
+ enabled: config.enabled ?? "auto",
59
+ position: {
60
+ placement: config.position?.placement ?? "bottom-right",
61
+ offset: config.position?.offset ?? { x: "1rem", y: "1rem" }
62
+ },
63
+ onOverrideChange: config.onOverrideChange
64
+ };
65
+ this.state = {
66
+ overrides: {},
67
+ features: /* @__PURE__ */ new Set(),
68
+ featureValues: {},
69
+ searchQuery: "",
70
+ useLocalOverrides: true
71
+ };
72
+ }
73
+ cleanup() {
74
+ this.removeToolbar();
75
+ }
76
+ shouldShowToolbar() {
77
+ if (this.config.enabled === true) return true;
78
+ if (this.config.enabled === false) return false;
79
+ if (typeof window !== "undefined") {
80
+ return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "" || window.location.hostname.endsWith(".local") || window.location.hostname.endsWith(".localhost");
81
+ }
82
+ return false;
83
+ }
84
+ onInit(params) {
85
+ const { availableFeatures, context, clientId } = params;
86
+ this.clientId = clientId;
87
+ this.storageKey = DEFAULT_STORAGE_KEY;
88
+ this.state.overrides = this.loadOverrides();
89
+ this.state.features = new Set(Object.keys(availableFeatures));
90
+ this.state.featureValues = { ...availableFeatures };
91
+ this.state.context = context;
92
+ if (this.shouldShowToolbar()) {
93
+ this.injectToolbar();
94
+ }
95
+ this.updateToolbarUI();
96
+ }
97
+ async beforeGetFeatures(_featureNames, context) {
98
+ this.state.context = context;
99
+ this.state.overrides = this.loadOverrides();
100
+ this.updateToolbarUI();
101
+ }
102
+ async afterGetFeatures(results, context) {
103
+ Object.keys(results).forEach((name) => {
104
+ this.state.featureValues[name] = results[name];
105
+ });
106
+ if (this.state.useLocalOverrides) {
107
+ Object.keys(this.state.overrides).forEach((featureName) => {
108
+ if (featureName in results) {
109
+ results[featureName] = this.state.overrides[featureName];
110
+ }
111
+ });
112
+ }
113
+ Object.keys(results).forEach((name) => this.state.features.add(name));
114
+ this.state.context = context;
115
+ this.updateToolbarUI();
116
+ }
117
+ loadOverrides() {
118
+ if (typeof window === "undefined" || !window.localStorage) {
119
+ return {};
120
+ }
121
+ try {
122
+ const stored = window.localStorage.getItem(this.storageKey);
123
+ return stored ? JSON.parse(stored) : {};
124
+ } catch {
125
+ return {};
126
+ }
127
+ }
128
+ saveOverrides(feature, value, allOverrides) {
129
+ if (typeof window === "undefined" || !window.localStorage) {
130
+ return;
131
+ }
132
+ try {
133
+ window.localStorage.setItem(this.storageKey, JSON.stringify(allOverrides));
134
+ this.config.onOverrideChange?.(
135
+ { feature: feature ?? "", value: value ?? null },
136
+ allOverrides ?? {}
137
+ );
138
+ } catch (error) {
139
+ console.error("Supaship: Failed to save feature overrides:", error);
140
+ }
141
+ }
142
+ setOverride(featureName, value) {
143
+ this.state.overrides[featureName] = value;
144
+ this.saveOverrides(featureName, value, this.state.overrides);
145
+ this.updateToolbarUI();
146
+ }
147
+ removeOverride(featureName) {
148
+ delete this.state.overrides[featureName];
149
+ this.saveOverrides(featureName, null, this.state.overrides);
150
+ this.updateToolbarUI();
151
+ }
152
+ clearAllOverrides() {
153
+ this.state.overrides = {};
154
+ this.saveOverrides("", null, this.state.overrides);
155
+ this.updateToolbarUI();
156
+ }
157
+ getOverrides() {
158
+ return { ...this.state.overrides };
159
+ }
160
+ injectToolbar() {
161
+ if (typeof window === "undefined" || typeof document === "undefined") {
162
+ return;
163
+ }
164
+ const toolbarId = `supaship-toolbar-${this.clientId}`;
165
+ if (document.getElementById(toolbarId)) {
166
+ return;
167
+ }
168
+ const toolbar = document.createElement("div");
169
+ toolbar.id = toolbarId;
170
+ toolbar.setAttribute("data-supaship-client", this.clientId || "");
171
+ toolbar.innerHTML = this.getToolbarHTML();
172
+ this.injectStyles();
173
+ document.body.appendChild(toolbar);
174
+ this.attachEventListeners();
175
+ }
176
+ removeToolbar() {
177
+ if (typeof document === "undefined") {
178
+ return;
179
+ }
180
+ const toolbar = document.getElementById(`supaship-toolbar-${this.clientId}`);
181
+ if (toolbar) {
182
+ toolbar.remove();
183
+ }
184
+ const styles = document.getElementById("supaship-toolbar-styles");
185
+ if (styles) {
186
+ styles.remove();
187
+ }
188
+ }
189
+ getToolbarHTML() {
190
+ const { placement, offset } = this.config.position;
191
+ const positionClass = `supaship-toolbar-${placement}`;
192
+ const offsetX = offset?.x ?? "1rem";
193
+ const offsetY = offset?.y ?? "1rem";
194
+ const toggleId = `supaship-toolbar-toggle-${this.clientId}`;
195
+ const panelId = `supaship-toolbar-panel-${this.clientId}`;
196
+ const searchId = `supaship-search-input-${this.clientId}`;
197
+ const clearId = `supaship-clear-all-${this.clientId}`;
198
+ const contentId = `supaship-toolbar-content-${this.clientId}`;
199
+ return `
200
+ <div class="supaship-toolbar-container ${positionClass}" style="--offset-x: ${offsetX}; --offset-y: ${offsetY};">
201
+ <button class="supaship-toolbar-toggle" id="${toggleId}" aria-label="Toggle feature flags">
202
+ <svg
203
+ xmlns="http://www.w3.org/2000/svg"
204
+ viewBox="0 0 256 256"
205
+ width="24"
206
+ style="vertical-align: middle;">
207
+ <rect width="256" height="256" rx="16" fill="none"></rect>
208
+ <line
209
+ x1="40"
210
+ y1="128"
211
+ x2="128"
212
+ y2="40"
213
+ fill="none"
214
+ stroke="currentColor"
215
+ stroke-linecap="round"
216
+ stroke-linejoin="round"
217
+ stroke-width="16"></line>
218
+ <line
219
+ x1="216"
220
+ y1="40"
221
+ x2="40"
222
+ y2="216"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ stroke-linecap="round"
226
+ stroke-linejoin="round"
227
+ stroke-width="16"></line>
228
+ <line
229
+ x1="216"
230
+ y1="128"
231
+ x2="128"
232
+ y2="216"
233
+ fill="none"
234
+ stroke="currentColor"
235
+ stroke-linecap="round"
236
+ stroke-linejoin="round"
237
+ stroke-width="16"></line>
238
+ </svg>
239
+ </button>
240
+ <div class="supaship-toolbar-panel" id="${panelId}">
241
+ <div class="supaship-toolbar-header">
242
+ <input
243
+ type="text"
244
+ class="supaship-search-input"
245
+ id="${searchId}"
246
+ placeholder="Search features"
247
+ />
248
+ <button
249
+ class="supaship-header-btn"
250
+ id="${clearId}"
251
+ aria-label="Reset all overrides"
252
+ title="Reset all overrides to default"
253
+ >
254
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="18" height="18">
255
+ <path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z" fill="currentColor"/>
256
+ </svg>
257
+ </button>
258
+ </div>
259
+ <div class="supaship-toolbar-content" id="${contentId}">
260
+ <div class="supaship-toolbar-empty">${NO_FEATURES_MESSAGE}</div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ `;
265
+ }
266
+ injectStyles() {
267
+ if (typeof document === "undefined") {
268
+ return;
269
+ }
270
+ if (document.getElementById("supaship-toolbar-styles")) {
271
+ return;
272
+ }
273
+ const styles = document.createElement("style");
274
+ styles.id = "supaship-toolbar-styles";
275
+ styles.textContent = `
276
+ .supaship-toolbar-container {
277
+ position: fixed;
278
+ z-index: 999999;
279
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
280
+ font-size: 14px;
281
+ }
282
+
283
+ .supaship-toolbar-bottom-right {
284
+ bottom: var(--offset-y);
285
+ right: var(--offset-x);
286
+ }
287
+
288
+ .supaship-toolbar-bottom-left {
289
+ bottom: var(--offset-y);
290
+ left: var(--offset-x);
291
+ }
292
+
293
+ .supaship-toolbar-top-right {
294
+ top: var(--offset-y);
295
+ right: var(--offset-x);
296
+ }
297
+
298
+ .supaship-toolbar-top-left {
299
+ top: var(--offset-y);
300
+ left: var(--offset-x);
301
+ }
302
+
303
+ .supaship-toolbar-toggle {
304
+ position: relative;
305
+ width: 36px;
306
+ height: 36px;
307
+ border-radius: 100%;
308
+ background: #000;
309
+ border: none;
310
+ color: white;
311
+ cursor: pointer;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
316
+ transition: transform 0.2s, box-shadow 0.2s;
317
+ }
318
+
319
+ .supaship-toolbar-toggle:hover {
320
+ transform: scale(1.05);
321
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
322
+ }
323
+
324
+ .supaship-toolbar-panel {
325
+ position: absolute;
326
+ bottom: 48px;
327
+ right: 0;
328
+ width: 300px;
329
+ max-height: 600px;
330
+ background: #1a1a1a;
331
+ border-radius: 12px;
332
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
333
+ display: none;
334
+ flex-direction: column;
335
+ overflow: hidden;
336
+ border: 1px solid #333;
337
+ }
338
+
339
+ .supaship-toolbar-bottom-left .supaship-toolbar-panel,
340
+ .supaship-toolbar-top-left .supaship-toolbar-panel {
341
+ right: auto;
342
+ left: 0;
343
+ }
344
+
345
+ .supaship-toolbar-top-right .supaship-toolbar-panel,
346
+ .supaship-toolbar-top-left .supaship-toolbar-panel {
347
+ bottom: auto;
348
+ top: 60px;
349
+ }
350
+
351
+ .supaship-toolbar-panel.open {
352
+ display: flex;
353
+ }
354
+
355
+ .supaship-toolbar-header {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 12px;
359
+ padding: 12px;
360
+ border-bottom: 1px solid #333;
361
+ background: #0f0f0f;
362
+ }
363
+
364
+ .supaship-search-input {
365
+ flex: 1;
366
+ background: transparent;
367
+ border: none;
368
+ color: #e5e5e5;
369
+ padding: 0;
370
+ font-size: 13px;
371
+ outline: none;
372
+ }
373
+
374
+ .supaship-search-input::placeholder {
375
+ color: #888;
376
+ }
377
+
378
+ .supaship-header-btn {
379
+ background: transparent;
380
+ border: none;
381
+ color: #e5e5e5;
382
+ width: 24px;
383
+ height: 20px;
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ cursor: pointer;
388
+ transition: all 0.2s;
389
+ padding: 0;
390
+ }
391
+
392
+ .supaship-header-btn:hover {
393
+ color: #ef4444;
394
+ }
395
+
396
+ .supaship-toolbar-content {
397
+ flex: 1;
398
+ overflow-y: auto;
399
+ padding: 8px;
400
+ min-height: 200px;
401
+ }
402
+
403
+ .supaship-toolbar-empty {
404
+ padding: 32px 16px;
405
+ text-align: center;
406
+ color: #888;
407
+ }
408
+
409
+ .supaship-feature-item {
410
+ padding: 0 6px;
411
+ }
412
+
413
+ .supaship-feature-item.disabled {
414
+ opacity: 0.5;
415
+ pointer-events: none;
416
+ }
417
+
418
+ .supaship-feature-row {
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: space-between;
422
+ gap: 12px;
423
+ min-height: 32px;
424
+ }
425
+
426
+ .supaship-feature-name {
427
+ font-weight: 500;
428
+ color: #e5e5e5;
429
+ font-size: 13px;
430
+ flex: 1;
431
+ min-width: 0;
432
+ }
433
+
434
+ .supaship-feature-actions {
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 8px;
438
+ flex-shrink: 0;
439
+ min-height: 20px;
440
+ }
441
+
442
+ .supaship-feature-content {
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 8px;
446
+ }
447
+
448
+ .supaship-feature-input {
449
+ flex: 1;
450
+ padding: 6px 8px;
451
+ background: #1a1a1a;
452
+ border: 1px solid #555;
453
+ color: #e5e5e5;
454
+ border-radius: 4px;
455
+ font-size: 13px;
456
+ font-family: 'Monaco', 'Courier New', monospace;
457
+ outline: none;
458
+ resize: vertical;
459
+ min-height: 60px;
460
+ margin-bottom: 8px;
461
+ }
462
+
463
+ .supaship-feature-input:focus {
464
+ border-color: #667eea;
465
+ }
466
+
467
+ .supaship-btn {
468
+ padding: 4px 12px;
469
+ border: none;
470
+ border-radius: 4px;
471
+ font-size: 12px;
472
+ font-weight: 500;
473
+ cursor: pointer;
474
+ transition: background 0.2s;
475
+ }
476
+
477
+ .supaship-btn-primary {
478
+ background: #444;
479
+ color: white;
480
+ }
481
+
482
+ .supaship-btn-primary:hover {
483
+ background: #555;
484
+ }
485
+
486
+ .supaship-btn-secondary {
487
+ background: #444;
488
+ color: #e5e5e5;
489
+ }
490
+
491
+ .supaship-btn-secondary:hover {
492
+ background: #555;
493
+ }
494
+
495
+ .supaship-btn:disabled {
496
+ opacity: 0.5;
497
+ cursor: not-allowed;
498
+ }
499
+
500
+ .supaship-btn:disabled:hover {
501
+ background: #444;
502
+ }
503
+
504
+ .supaship-header-btn:disabled {
505
+ opacity: 0.3;
506
+ cursor: not-allowed;
507
+ }
508
+
509
+ .supaship-header-btn:disabled:hover {
510
+ color: #e5e5e5;
511
+ }
512
+
513
+ .supaship-toggle {
514
+ position: relative;
515
+ display: inline-block;
516
+ width: 32px;
517
+ height: 18px;
518
+ flex-shrink: 0;
519
+ }
520
+
521
+ .supaship-toggle input {
522
+ opacity: 0;
523
+ width: 0;
524
+ height: 0;
525
+ }
526
+
527
+ .supaship-toggle-slider {
528
+ position: absolute;
529
+ cursor: pointer;
530
+ top: 0;
531
+ left: 0;
532
+ right: 0;
533
+ bottom: 0;
534
+ background-color: #333;
535
+ border: 1px solid #555;
536
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
537
+ border-radius: 20px;
538
+ }
539
+
540
+ .supaship-toggle-slider:before {
541
+ position: absolute;
542
+ content: "";
543
+ height: 14px;
544
+ width: 14px;
545
+ left: 2px;
546
+ bottom: 1px;
547
+ background-color: #666;
548
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
549
+ border-radius: 50%;
550
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
551
+ }
552
+
553
+ .supaship-toggle input:checked + .supaship-toggle-slider {
554
+ background-color: #fff;
555
+ border-color: #fff;
556
+ }
557
+
558
+ .supaship-toggle input:checked + .supaship-toggle-slider:before {
559
+ transform: translateX(13px);
560
+ background-color: #000;
561
+ }
562
+
563
+ .supaship-toggle input:disabled + .supaship-toggle-slider {
564
+ opacity: 0.5;
565
+ cursor: not-allowed;
566
+ }
567
+
568
+ .supaship-toggle:hover .supaship-toggle-slider:before {
569
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
570
+ }
571
+
572
+ .supaship-btn-icon {
573
+ background: transparent;
574
+ border: none;
575
+ color: #e5e5e5;
576
+ width: 20px;
577
+ height: 20px;
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ border-radius: 4px;
582
+ cursor: pointer;
583
+ padding: 0;
584
+ transition: all 0.2s;
585
+ flex-shrink: 0;
586
+ }
587
+
588
+ .supaship-btn-icon:hover {
589
+ background: #444;
590
+ color: #ef4444;
591
+ }
592
+ `;
593
+ document.head.appendChild(styles);
594
+ }
595
+ attachEventListeners() {
596
+ if (typeof document === "undefined") {
597
+ return;
598
+ }
599
+ const toggleId = `supaship-toolbar-toggle-${this.clientId}`;
600
+ const panelId = `supaship-toolbar-panel-${this.clientId}`;
601
+ const clearId = `supaship-clear-all-${this.clientId}`;
602
+ const searchId = `supaship-search-input-${this.clientId}`;
603
+ const contentId = `supaship-toolbar-content-${this.clientId}`;
604
+ const toggle = document.getElementById(toggleId);
605
+ const panel = document.getElementById(panelId);
606
+ const clearAll = document.getElementById(clearId);
607
+ const searchInput = document.getElementById(searchId);
608
+ const content = document.getElementById(contentId);
609
+ toggle?.addEventListener("click", () => {
610
+ panel?.classList.toggle("open");
611
+ });
612
+ clearAll?.addEventListener("click", () => {
613
+ this.clearAllOverrides();
614
+ });
615
+ searchInput?.addEventListener("input", (e) => {
616
+ this.state.searchQuery = e.target.value.toLowerCase();
617
+ this.updateToolbarUI();
618
+ });
619
+ if (content) {
620
+ content.addEventListener("click", (e) => {
621
+ const target = e.target;
622
+ const buttonElement = target.closest("button[data-action]");
623
+ if (!buttonElement) return;
624
+ e.preventDefault();
625
+ e.stopPropagation();
626
+ const featureName = buttonElement.dataset.feature;
627
+ const action = buttonElement.dataset.action;
628
+ if (action === "remove") {
629
+ this.removeOverride(featureName);
630
+ } else if (action === "set") {
631
+ const textarea = content.querySelector(
632
+ `textarea[data-feature="${featureName}"]`
633
+ );
634
+ if (textarea && textarea.value.trim()) {
635
+ try {
636
+ const value = JSON.parse(textarea.value);
637
+ this.setOverride(featureName, value);
638
+ } catch {
639
+ this.setOverride(featureName, { value: textarea.value });
640
+ }
641
+ }
642
+ }
643
+ });
644
+ content.addEventListener("change", (e) => {
645
+ const target = e.target;
646
+ if (target.type === "checkbox" && target.dataset.type === "boolean") {
647
+ const featureName = target.dataset.feature;
648
+ const newValue = target.checked;
649
+ this.setOverride(featureName, newValue);
650
+ }
651
+ });
652
+ content.addEventListener("input", (e) => {
653
+ const target = e.target;
654
+ if (target.tagName === "TEXTAREA" && target.dataset.feature) {
655
+ const featureName = target.dataset.feature;
656
+ const originalValue = target.dataset.original || "";
657
+ const overrideBtn = content.querySelector(
658
+ `button[data-action="set"][data-feature="${featureName}"]`
659
+ );
660
+ if (overrideBtn) {
661
+ const hasChanged = target.value !== originalValue;
662
+ const hasContent = target.value.trim().length > 0;
663
+ overrideBtn.disabled = !hasChanged || !hasContent;
664
+ }
665
+ }
666
+ });
667
+ content.addEventListener("paste", (e) => {
668
+ const target = e.target;
669
+ if (target.tagName === "TEXTAREA" && target.dataset.feature) {
670
+ setTimeout(() => {
671
+ const featureName = target.dataset.feature;
672
+ const originalValue = target.dataset.original || "";
673
+ const overrideBtn = content.querySelector(
674
+ `button[data-action="set"][data-feature="${featureName}"]`
675
+ );
676
+ if (overrideBtn) {
677
+ const hasChanged = target.value !== originalValue;
678
+ const hasContent = target.value.trim().length > 0;
679
+ overrideBtn.disabled = !hasChanged || !hasContent;
680
+ }
681
+ }, 0);
682
+ }
683
+ });
684
+ content.addEventListener("keydown", (e) => {
685
+ const target = e.target;
686
+ if (target.tagName === "TEXTAREA" && target.dataset.feature && (e.ctrlKey || e.metaKey) && e.key === "Enter") {
687
+ e.preventDefault();
688
+ const featureName = target.dataset.feature;
689
+ const overrideBtn = content.querySelector(
690
+ `button[data-action="set"][data-feature="${featureName}"]`
691
+ );
692
+ if (overrideBtn && !overrideBtn.disabled) {
693
+ overrideBtn.click();
694
+ }
695
+ }
696
+ });
697
+ }
698
+ }
699
+ updateToolbarUI() {
700
+ if (typeof document === "undefined") {
701
+ return;
702
+ }
703
+ const contentId = `supaship-toolbar-content-${this.clientId}`;
704
+ const clearId = `supaship-clear-all-${this.clientId}`;
705
+ const content = document.getElementById(contentId);
706
+ const clearAllBtn = document.getElementById(clearId);
707
+ if (!content) {
708
+ console.warn("[Toolbar] Content element not found:", contentId);
709
+ return;
710
+ }
711
+ const hasOverrides = Object.keys(this.state.overrides).length > 0;
712
+ if (clearAllBtn) {
713
+ clearAllBtn.disabled = !hasOverrides;
714
+ }
715
+ const features = Array.from(this.state.features).sort();
716
+ const filteredFeatures = features.filter(
717
+ (name) => name.toLowerCase().includes(this.state.searchQuery)
718
+ );
719
+ if (filteredFeatures.length === 0) {
720
+ content.innerHTML = this.state.searchQuery ? '<div class="supaship-toolbar-empty">No matching features found</div>' : `<div class="supaship-toolbar-empty">${NO_FEATURES_MESSAGE}</div>`;
721
+ return;
722
+ }
723
+ const htmlContent = filteredFeatures.map((featureName) => {
724
+ const hasOverride = featureName in this.state.overrides;
725
+ const currentValue = this.state.featureValues[featureName];
726
+ const overrideValue = hasOverride ? this.state.overrides[featureName] : currentValue;
727
+ const isDisabled = !this.state.useLocalOverrides;
728
+ const itemClass = `supaship-feature-item ${isDisabled ? "disabled" : ""}`;
729
+ const isBoolean = typeof currentValue === "boolean" || hasOverride && typeof overrideValue === "boolean";
730
+ if (isBoolean) {
731
+ const isChecked = hasOverride ? overrideValue === true : currentValue === true;
732
+ return `
733
+ <div class="${itemClass}">
734
+ <div class="supaship-feature-row">
735
+ <span class="supaship-feature-name">${this.escapeHtml(featureName)}</span>
736
+ <div class="supaship-feature-actions">
737
+ ${hasOverride ? `
738
+ <button
739
+ class="supaship-btn-icon"
740
+ data-feature="${this.escapeHtml(featureName)}"
741
+ data-action="remove"
742
+ title="Reset to default"
743
+ >
744
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="14" height="14">
745
+ <path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z" fill="currentColor"/>
746
+ </svg>
747
+ </button>
748
+ ` : ""}
749
+ <label class="supaship-toggle">
750
+ <input
751
+ type="checkbox"
752
+ ${isChecked ? "checked" : ""}
753
+ data-feature="${this.escapeHtml(featureName)}"
754
+ data-type="boolean"
755
+ />
756
+ <span class="supaship-toggle-slider"></span>
757
+ </label>
758
+ </div>
759
+ </div>
760
+ </div>
761
+ `;
762
+ } else {
763
+ const currentDisplayValue = hasOverride ? JSON.stringify(overrideValue) : currentValue !== void 0 ? JSON.stringify(currentValue) : "";
764
+ const escapedFeatureName = this.escapeHtml(featureName);
765
+ const escapedCurrentDisplayValue = this.escapeHtml(currentDisplayValue);
766
+ const escapedTextareaContent = hasOverride ? this.escapeHtml(JSON.stringify(overrideValue)) : escapedCurrentDisplayValue;
767
+ return `
768
+ <div class="${itemClass}">
769
+ <div class="supaship-feature-row">
770
+ <span class="supaship-feature-name">${escapedFeatureName}</span>
771
+ <div class="supaship-feature-actions">
772
+ ${hasOverride ? `
773
+ <button
774
+ class="supaship-btn-icon"
775
+ data-feature="${escapedFeatureName}"
776
+ data-action="remove"
777
+ title="Reset to default"
778
+ >
779
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="14" height="14">
780
+ <path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z" fill="currentColor"/>
781
+ </svg>
782
+ </button>
783
+ ` : ""}
784
+ <button
785
+ class="supaship-btn supaship-btn-primary"
786
+ data-feature="${escapedFeatureName}"
787
+ data-action="set"
788
+ disabled>
789
+ Override
790
+ </button>
791
+ </div>
792
+ </div>
793
+ <div class="supaship-feature-content">
794
+ <textarea
795
+ class="supaship-feature-input"
796
+ placeholder="Override JSON value"
797
+ data-feature="${escapedFeatureName}"
798
+ data-original="${escapedCurrentDisplayValue}"
799
+ >${escapedTextareaContent}</textarea>
800
+ </div>
801
+ </div>
802
+ `;
803
+ }
804
+ }).join("");
805
+ requestAnimationFrame(() => {
806
+ content.innerHTML = htmlContent;
807
+ content.querySelectorAll("textarea[data-feature]").forEach((textarea) => {
808
+ const textareaElement = textarea;
809
+ const featureName = textareaElement.dataset.feature;
810
+ const originalValue = textareaElement.dataset.original || "";
811
+ const overrideBtn = content.querySelector(
812
+ `button[data-action="set"][data-feature="${featureName}"]`
813
+ );
814
+ if (overrideBtn) {
815
+ const hasChanged = textareaElement.value !== originalValue;
816
+ const hasContent = textareaElement.value.trim().length > 0;
817
+ overrideBtn.disabled = !hasChanged || !hasContent;
818
+ }
819
+ });
820
+ });
821
+ }
822
+ escapeHtml(text) {
823
+ const div = typeof document !== "undefined" ? document.createElement("div") : null;
824
+ if (div) {
825
+ div.textContent = text;
826
+ return div.innerHTML;
827
+ }
828
+ return text.replace(/[&<>"']/g, (char) => {
829
+ const escapeMap = {
830
+ "&": "&amp;",
831
+ "<": "&lt;",
832
+ ">": "&gt;",
833
+ '"': "&quot;",
834
+ "'": "&#39;"
835
+ };
836
+ return escapeMap[char];
837
+ });
838
+ }
839
+ };
840
+
841
+ // src/constants.ts
842
+ var DEFAULT_FEATURES_URL = "https://edge.supaship.com/v1/features";
843
+ var DEFAULT_EVENTS_URL = "https://edge.supaship.com/v1/events";
844
+
845
+ // src/client.ts
846
+ var SupaClient = class {
847
+ constructor(config) {
848
+ this.apiKey = config.apiKey;
849
+ this.environment = config.environment;
850
+ this.defaultContext = config.context;
851
+ this.featureDefinitions = config.features;
852
+ this.clientId = this.generateClientId();
853
+ this.networkConfig = {
854
+ featuresAPIUrl: config.networkConfig?.featuresAPIUrl || DEFAULT_FEATURES_URL,
855
+ eventsAPIUrl: config.networkConfig?.eventsAPIUrl || DEFAULT_EVENTS_URL,
856
+ retry: {
857
+ enabled: config.networkConfig?.retry?.enabled ?? true,
858
+ maxAttempts: config.networkConfig?.retry?.maxAttempts ?? 3,
859
+ backoff: config.networkConfig?.retry?.backoff ?? 1e3
860
+ },
861
+ requestTimeoutMs: config.networkConfig?.requestTimeoutMs ?? 1e4
862
+ };
863
+ const globalFetch = typeof globalThis !== "undefined" ? globalThis.fetch : void 0;
864
+ if (config.networkConfig?.fetchFn) {
865
+ this.fetchImpl = config.networkConfig.fetchFn;
866
+ } else if (typeof globalFetch === "function") {
867
+ this.fetchImpl = globalFetch.bind(globalThis);
868
+ } else {
869
+ throw new Error(
870
+ "No fetch implementation available. Provide fetchFn in config or use a runtime with global fetch (e.g., Node 18+, browsers)."
871
+ );
872
+ }
873
+ this.plugins = this.initializePlugins(config);
874
+ Promise.all(
875
+ this.plugins.map(
876
+ (plugin) => plugin.onInit?.({
877
+ clientId: this.clientId,
878
+ availableFeatures: this.featureDefinitions,
879
+ context: this.defaultContext
880
+ })
881
+ )
882
+ ).catch(console.error);
883
+ }
884
+ /**
885
+ * Generate a unique client ID
886
+ */
887
+ generateClientId() {
888
+ return `supaship-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
889
+ }
890
+ /**
891
+ * Initialize plugins with automatic toolbar plugin in browser environments
892
+ */
893
+ initializePlugins(config) {
894
+ const plugins = config.plugins || [];
895
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
896
+ if (config.toolbar === false) {
897
+ return plugins;
898
+ }
899
+ if (isBrowser) {
900
+ const hasToolbarPlugin = plugins.some((p) => p.name === "toolbar-plugin");
901
+ if (!hasToolbarPlugin) {
902
+ const toolbarConfig = config.toolbar || { enabled: "auto" };
903
+ const toolbarPlugin = new SupaToolbarPlugin(toolbarConfig);
904
+ return [toolbarPlugin, ...plugins];
905
+ }
906
+ }
907
+ return plugins;
908
+ }
909
+ /**
910
+ * Updates the default context for the client
911
+ * @param context - New context to merge with or replace the existing context
912
+ * @param mergeWithExisting - Whether to merge with existing context (default: true)
913
+ */
914
+ updateContext(context, mergeWithExisting = true) {
915
+ const oldContext = this.defaultContext;
916
+ if (mergeWithExisting && this.defaultContext) {
917
+ this.defaultContext = { ...this.defaultContext, ...context };
918
+ } else {
919
+ this.defaultContext = context;
920
+ }
921
+ Promise.all(
922
+ this.plugins.map(
923
+ (plugin) => plugin.onContextUpdate?.(oldContext, this.defaultContext, "updateContext")
924
+ )
925
+ ).catch(console.error);
926
+ }
927
+ /**
928
+ * Gets the current default context
929
+ */
930
+ getContext() {
931
+ return this.defaultContext;
932
+ }
933
+ /**
934
+ * Gets the fallback value for a feature from its definition
935
+ */
936
+ getFeatureFallback(featureName) {
937
+ return this.featureDefinitions[featureName];
938
+ }
939
+ getVariationValue(variation, fallback) {
940
+ if (variation !== void 0 && variation !== null) {
941
+ return variation;
942
+ }
943
+ return fallback ?? null;
944
+ }
945
+ async getFeature(featureName, options) {
946
+ const { context } = options ?? {};
947
+ const mergedContext = typeof context === "object" && context !== null ? { ...this.defaultContext ?? {}, ...context } : this.defaultContext;
948
+ try {
949
+ const response = await this.getFeatures([featureName], {
950
+ context: mergedContext
951
+ });
952
+ const value = response[featureName];
953
+ return value;
954
+ } catch (error) {
955
+ await Promise.all(this.plugins.map((plugin) => plugin.onError?.(error, mergedContext)));
956
+ const fallbackValue = this.featureDefinitions[featureName];
957
+ await Promise.all(
958
+ this.plugins.map(
959
+ (plugin) => plugin.onFallbackUsed?.(
960
+ featureName,
961
+ fallbackValue,
962
+ error
963
+ )
964
+ )
965
+ );
966
+ return fallbackValue;
967
+ }
968
+ }
969
+ async getFeatures(featureNames, options) {
970
+ const { context: contextOverride } = options ?? {};
971
+ const mergedContext = typeof contextOverride === "object" && contextOverride !== null ? { ...this.defaultContext ?? {}, ...contextOverride } : this.defaultContext;
972
+ if (contextOverride) {
973
+ await Promise.all(
974
+ this.plugins.map(
975
+ (plugin) => plugin.onContextUpdate?.(this.defaultContext, mergedContext, "request")
976
+ )
977
+ );
978
+ }
979
+ const featureNamesArray = featureNames.map((name) => name);
980
+ try {
981
+ await Promise.all(
982
+ this.plugins.map((plugin) => plugin.beforeGetFeatures?.(featureNamesArray, mergedContext))
983
+ );
984
+ const fetchFeatures = async () => {
985
+ const url = this.networkConfig.featuresAPIUrl;
986
+ const headers = {
987
+ "Content-Type": "application/json",
988
+ Authorization: `Bearer ${this.apiKey}`
989
+ };
990
+ const body = JSON.stringify({
991
+ apiKey: this.apiKey,
992
+ environment: this.environment,
993
+ features: featureNamesArray,
994
+ context: mergedContext
995
+ });
996
+ await Promise.all(this.plugins.map((plugin) => plugin.beforeRequest?.(url, body, headers)));
997
+ const startTime = Date.now();
998
+ const AbortCtrl = typeof globalThis !== "undefined" ? globalThis.AbortController : void 0;
999
+ let controller;
1000
+ let timeoutId;
1001
+ if (this.networkConfig.requestTimeoutMs && typeof AbortCtrl === "function") {
1002
+ controller = new AbortCtrl();
1003
+ timeoutId = setTimeout(() => controller?.abort(), this.networkConfig.requestTimeoutMs);
1004
+ }
1005
+ let response;
1006
+ try {
1007
+ response = await this.fetchImpl(url, {
1008
+ method: "POST",
1009
+ headers,
1010
+ body,
1011
+ signal: controller?.signal
1012
+ });
1013
+ } finally {
1014
+ if (timeoutId) clearTimeout(timeoutId);
1015
+ }
1016
+ const duration = Date.now() - startTime;
1017
+ await Promise.all(
1018
+ this.plugins.map((plugin) => plugin.afterResponse?.(response, { duration }))
1019
+ );
1020
+ if (!response.ok) {
1021
+ throw new Error(`Failed to fetch features: ${response.statusText}`);
1022
+ }
1023
+ const data = await response.json();
1024
+ const result2 = {};
1025
+ featureNamesArray.forEach((name) => {
1026
+ const variation = data.features[name]?.variation;
1027
+ result2[name] = this.getVariationValue(
1028
+ variation,
1029
+ this.featureDefinitions[name]
1030
+ );
1031
+ });
1032
+ return result2;
1033
+ };
1034
+ const result = this.networkConfig.retry.enabled ? await retry(
1035
+ fetchFeatures,
1036
+ this.networkConfig.retry.maxAttempts,
1037
+ this.networkConfig.retry.backoff,
1038
+ (attempt, error, willRetry) => {
1039
+ Promise.all(
1040
+ this.plugins.map((plugin) => plugin.onRetryAttempt?.(attempt, error, willRetry))
1041
+ ).catch(console.error);
1042
+ }
1043
+ ) : await fetchFeatures();
1044
+ await Promise.all(
1045
+ this.plugins.map((plugin) => plugin.afterGetFeatures?.(result, mergedContext))
1046
+ );
1047
+ return result;
1048
+ } catch (error) {
1049
+ await Promise.all(this.plugins.map((plugin) => plugin.onError?.(error, mergedContext)));
1050
+ const fallbackResult = {};
1051
+ featureNamesArray.forEach((featureName) => {
1052
+ fallbackResult[featureName] = this.featureDefinitions[featureName];
1053
+ Promise.all(
1054
+ this.plugins.map(
1055
+ (plugin) => plugin.onFallbackUsed?.(
1056
+ featureName,
1057
+ this.featureDefinitions[featureName],
1058
+ error
1059
+ )
1060
+ )
1061
+ ).catch(console.error);
1062
+ });
1063
+ return fallbackResult;
1064
+ }
1065
+ }
1066
+ };
1067
+ // Annotate the CommonJS export names for ESM import in node:
1068
+ 0 && (module.exports = {
1069
+ SupaClient,
1070
+ ToolbarPlugin
1071
+ });
1072
+ //# sourceMappingURL=index.js.map