featurely-site-manager 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,848 @@
1
+ // src/index.ts
2
+ var SiteManager = class {
3
+ constructor(config) {
4
+ this.siteConfig = null;
5
+ this.pollIntervalId = null;
6
+ this.messageContainers = /* @__PURE__ */ new Map();
7
+ this.dismissedMessages = /* @__PURE__ */ new Set();
8
+ this.featureFlagBuckets = /* @__PURE__ */ new Map();
9
+ // Consistent bucketing for percentage rollouts
10
+ this.analyticsQueue = [];
11
+ this.analyticsFlushIntervalId = null;
12
+ this.sessionId = this.generateSessionId();
13
+ var _a, _b, _c;
14
+ if (!config.apiKey) {
15
+ throw new Error("Featurely Site Manager: apiKey is required");
16
+ }
17
+ if (!config.projectId) {
18
+ throw new Error("Featurely Site Manager: projectId is required");
19
+ }
20
+ this.config = {
21
+ apiKey: config.apiKey,
22
+ projectId: config.projectId,
23
+ apiUrl: config.apiUrl || "https://featurely.com",
24
+ pollInterval: (_a = config.pollInterval) != null ? _a : 6e4,
25
+ userEmail: config.userEmail,
26
+ userId: config.userId,
27
+ bypassCheck: config.bypassCheck,
28
+ onMaintenanceEnabled: config.onMaintenanceEnabled,
29
+ onMaintenanceDisabled: config.onMaintenanceDisabled,
30
+ onMessageReceived: config.onMessageReceived,
31
+ onMessageDismissed: config.onMessageDismissed,
32
+ onFeatureFlagsUpdated: config.onFeatureFlagsUpdated,
33
+ onError: config.onError,
34
+ enableAnalytics: (_b = config.enableAnalytics) != null ? _b : true,
35
+ analyticsFlushInterval: (_c = config.analyticsFlushInterval) != null ? _c : 6e4
36
+ };
37
+ this.loadDismissedMessages();
38
+ }
39
+ /**
40
+ * Initialize and start the site manager
41
+ */
42
+ async init() {
43
+ if (typeof window === "undefined" || typeof document === "undefined") {
44
+ console.warn(
45
+ "Featurely Site Manager: Can only be initialized in a browser environment"
46
+ );
47
+ return;
48
+ }
49
+ await this.fetchConfig();
50
+ this.startPolling();
51
+ if (this.config.enableAnalytics) {
52
+ this.startAnalyticsFlushing();
53
+ }
54
+ this.injectStyles();
55
+ }
56
+ /**
57
+ * Stop the site manager and clean up
58
+ */
59
+ destroy() {
60
+ this.stopPolling();
61
+ this.stopAnalyticsFlushing();
62
+ this.flushAnalytics();
63
+ this.clearMessages();
64
+ }
65
+ /**
66
+ * Update user email for whitelist checks
67
+ */
68
+ setUser(email, userId) {
69
+ var _a;
70
+ this.config.userEmail = email;
71
+ if (userId) {
72
+ this.config.userId = userId;
73
+ }
74
+ if ((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) {
75
+ this.checkMaintenanceMode();
76
+ }
77
+ }
78
+ /**
79
+ * Check if a feature flag is enabled for the current user
80
+ */
81
+ isFeatureEnabled(flagKey) {
82
+ var _a;
83
+ if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
84
+ return false;
85
+ }
86
+ const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
87
+ if (!flag || !flag.enabled) {
88
+ return false;
89
+ }
90
+ const isEnabled = this.evaluateFeatureFlag(flag);
91
+ const trackKey = `feature_flag_tracked_${flagKey}`;
92
+ if (typeof sessionStorage !== "undefined" && !sessionStorage.getItem(trackKey)) {
93
+ this.trackEvent("feature_flag_evaluated", {
94
+ flagKey,
95
+ enabled: isEnabled
96
+ });
97
+ sessionStorage.setItem(trackKey, "1");
98
+ }
99
+ return isEnabled;
100
+ }
101
+ /**
102
+ * Get the variant for a feature flag (for A/B testing)
103
+ */
104
+ getFeatureVariant(flagKey) {
105
+ var _a;
106
+ if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
107
+ return null;
108
+ }
109
+ const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
110
+ if (!flag || !flag.enabled || !flag.variants || flag.variants.length === 0) {
111
+ return null;
112
+ }
113
+ if (!this.evaluateFeatureFlag(flag)) {
114
+ return null;
115
+ }
116
+ const cacheKey = `variant_${flagKey}`;
117
+ const cached = localStorage.getItem(cacheKey);
118
+ if (cached) {
119
+ return cached;
120
+ }
121
+ const bucket = this.getUserBucket(flagKey);
122
+ let cumulative = 0;
123
+ for (const variant of flag.variants) {
124
+ cumulative += variant.weight;
125
+ if (bucket < cumulative) {
126
+ localStorage.setItem(cacheKey, variant.key);
127
+ return variant.key;
128
+ }
129
+ }
130
+ const defaultVariant = flag.defaultVariant || flag.variants[0].key;
131
+ localStorage.setItem(cacheKey, defaultVariant);
132
+ return defaultVariant;
133
+ }
134
+ /**
135
+ * Get all feature flags
136
+ */
137
+ getAllFeatureFlags() {
138
+ var _a;
139
+ return ((_a = this.siteConfig) == null ? void 0 : _a.featureFlags) || [];
140
+ }
141
+ /**
142
+ * Get all enabled feature flags for the current user
143
+ */
144
+ getEnabledFeatures() {
145
+ var _a;
146
+ if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
147
+ return [];
148
+ }
149
+ return this.siteConfig.featureFlags.filter((flag) => flag.enabled && this.evaluateFeatureFlag(flag)).map((flag) => flag.key);
150
+ }
151
+ /**
152
+ * Manually refresh configuration
153
+ */
154
+ async refresh() {
155
+ await this.fetchConfig();
156
+ }
157
+ /**
158
+ * Track a custom analytics event
159
+ * @param eventName - Name of the event (e.g., 'button_clicked', 'feature_used')
160
+ * @param properties - Optional event properties (e.g., { button: 'signup', page: '/home' })
161
+ */
162
+ trackEvent(eventName, properties) {
163
+ if (!this.config.enableAnalytics) {
164
+ return;
165
+ }
166
+ this.analyticsQueue.push({
167
+ eventName,
168
+ properties,
169
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
170
+ });
171
+ if (this.analyticsQueue.length >= 10) {
172
+ this.flushAnalytics();
173
+ }
174
+ }
175
+ // ============================================================================
176
+ // Configuration Fetching
177
+ // ============================================================================
178
+ async fetchConfig() {
179
+ var _a, _b;
180
+ try {
181
+ const response = await fetch(
182
+ `${this.config.apiUrl}/api/public/v1/site-config?projectId=${this.config.projectId}`,
183
+ {
184
+ headers: {
185
+ "X-API-Key": this.config.apiKey
186
+ }
187
+ }
188
+ );
189
+ if (!response.ok) {
190
+ throw new Error(`Failed to fetch site configuration: ${response.statusText}`);
191
+ }
192
+ const newConfig = await response.json();
193
+ const configChanged = JSON.stringify(newConfig) !== JSON.stringify(this.siteConfig);
194
+ if (configChanged) {
195
+ const wasMaintenanceEnabled = (_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled;
196
+ const oldFeatureFlags = ((_b = this.siteConfig) == null ? void 0 : _b.featureFlags) || [];
197
+ this.siteConfig = newConfig;
198
+ if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
199
+ this.enableMaintenanceMode();
200
+ } else if (!newConfig.maintenance.enabled && wasMaintenanceEnabled) {
201
+ this.disableMaintenanceMode();
202
+ } else if (newConfig.maintenance.enabled) {
203
+ this.checkMaintenanceMode();
204
+ }
205
+ if (JSON.stringify(newConfig.featureFlags) !== JSON.stringify(oldFeatureFlags)) {
206
+ if (this.config.onFeatureFlagsUpdated) {
207
+ this.config.onFeatureFlagsUpdated(newConfig.featureFlags);
208
+ }
209
+ }
210
+ this.updateMessages();
211
+ }
212
+ } catch (error) {
213
+ console.error("Featurely Site Manager: Failed to fetch configuration", error);
214
+ if (this.config.onError && error instanceof Error) {
215
+ this.config.onError(error);
216
+ }
217
+ }
218
+ }
219
+ startPolling() {
220
+ if (this.pollIntervalId) return;
221
+ this.pollIntervalId = setInterval(() => {
222
+ this.fetchConfig();
223
+ }, this.config.pollInterval);
224
+ }
225
+ stopPolling() {
226
+ if (this.pollIntervalId) {
227
+ clearInterval(this.pollIntervalId);
228
+ this.pollIntervalId = null;
229
+ }
230
+ }
231
+ // ============================================================================
232
+ // Analytics
233
+ // ============================================================================
234
+ startAnalyticsFlushing() {
235
+ if (this.analyticsFlushIntervalId) return;
236
+ this.analyticsFlushIntervalId = setInterval(() => {
237
+ this.flushAnalytics();
238
+ }, this.config.analyticsFlushInterval);
239
+ }
240
+ stopAnalyticsFlushing() {
241
+ if (this.analyticsFlushIntervalId) {
242
+ clearInterval(this.analyticsFlushIntervalId);
243
+ this.analyticsFlushIntervalId = null;
244
+ }
245
+ }
246
+ async flushAnalytics() {
247
+ if (this.analyticsQueue.length === 0) {
248
+ return;
249
+ }
250
+ const eventsToSend = [...this.analyticsQueue];
251
+ this.analyticsQueue = [];
252
+ for (const event of eventsToSend) {
253
+ try {
254
+ await fetch(
255
+ `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`,
256
+ {
257
+ method: "POST",
258
+ headers: {
259
+ "Content-Type": "application/json"
260
+ },
261
+ body: JSON.stringify({
262
+ eventName: event.eventName,
263
+ properties: event.properties,
264
+ userId: this.config.userId,
265
+ sessionId: this.sessionId,
266
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
267
+ platform: typeof navigator !== "undefined" ? navigator.platform : void 0
268
+ })
269
+ }
270
+ );
271
+ } catch (error) {
272
+ console.error("Failed to send analytics event:", error);
273
+ }
274
+ }
275
+ }
276
+ generateSessionId() {
277
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
278
+ }
279
+ // ============================================================================
280
+ // Feature Flags
281
+ // ============================================================================
282
+ /**
283
+ * Evaluate if a feature flag should be enabled for the current user
284
+ */
285
+ evaluateFeatureFlag(flag) {
286
+ if (!flag.enabled) {
287
+ return false;
288
+ }
289
+ const userEmail = this.config.userEmail;
290
+ if (flag.excludeEmails && userEmail && flag.excludeEmails.includes(userEmail)) {
291
+ return false;
292
+ }
293
+ if (flag.targetEmails && flag.targetEmails.length > 0) {
294
+ if (!userEmail || !flag.targetEmails.includes(userEmail)) {
295
+ return false;
296
+ }
297
+ return true;
298
+ }
299
+ if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
300
+ const bucket = this.getUserBucket(flag.key);
301
+ return bucket < flag.rolloutPercentage;
302
+ }
303
+ return true;
304
+ }
305
+ /**
306
+ * Get consistent bucket (0-99) for a user+flag combination
307
+ * This ensures the same user always gets the same result for percentage rollouts
308
+ */
309
+ getUserBucket(flagKey) {
310
+ if (this.featureFlagBuckets.has(flagKey)) {
311
+ return this.featureFlagBuckets.get(flagKey);
312
+ }
313
+ const identifier = this.config.userId || this.config.userEmail || this.getAnonymousId();
314
+ const hash = this.simpleHash(`${identifier}:${flagKey}`);
315
+ const bucket = hash % 100;
316
+ this.featureFlagBuckets.set(flagKey, bucket);
317
+ return bucket;
318
+ }
319
+ /**
320
+ * Simple hash function for consistent bucketing
321
+ */
322
+ simpleHash(str) {
323
+ let hash = 0;
324
+ for (let i = 0; i < str.length; i++) {
325
+ const char = str.charCodeAt(i);
326
+ hash = (hash << 5) - hash + char;
327
+ hash = hash & hash;
328
+ }
329
+ return Math.abs(hash);
330
+ }
331
+ /**
332
+ * Get or create anonymous user ID for bucketing
333
+ */
334
+ getAnonymousId() {
335
+ const storageKey = "featurely_anonymous_id";
336
+ let id = localStorage.getItem(storageKey);
337
+ if (!id) {
338
+ id = `anon_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
339
+ localStorage.setItem(storageKey, id);
340
+ }
341
+ return id;
342
+ }
343
+ // ============================================================================
344
+ // Maintenance Mode
345
+ // ============================================================================
346
+ checkMaintenanceMode() {
347
+ var _a;
348
+ if (!((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled)) return;
349
+ if (this.shouldBypassMaintenance()) {
350
+ return;
351
+ }
352
+ this.showMaintenancePage();
353
+ }
354
+ enableMaintenanceMode() {
355
+ if (!this.siteConfig) return;
356
+ if (this.shouldBypassMaintenance()) {
357
+ return;
358
+ }
359
+ this.showMaintenancePage();
360
+ this.trackEvent("maintenance_enabled", {
361
+ maintenanceType: this.siteConfig.maintenance.type
362
+ });
363
+ if (this.config.onMaintenanceEnabled) {
364
+ this.config.onMaintenanceEnabled(this.siteConfig.maintenance);
365
+ }
366
+ }
367
+ disableMaintenanceMode() {
368
+ this.hideMaintenancePage();
369
+ this.trackEvent("maintenance_disabled");
370
+ if (this.config.onMaintenanceDisabled) {
371
+ this.config.onMaintenanceDisabled();
372
+ }
373
+ }
374
+ shouldBypassMaintenance() {
375
+ if (!this.siteConfig) return false;
376
+ const whitelist = this.siteConfig.maintenance.whitelist;
377
+ if (this.config.bypassCheck && this.config.bypassCheck()) {
378
+ return true;
379
+ }
380
+ if (whitelist.localStorageKeys && whitelist.localStorageKeys.length > 0) {
381
+ const hasKey = whitelist.localStorageKeys.some((key) => {
382
+ try {
383
+ return localStorage.getItem(key) !== null;
384
+ } catch {
385
+ return false;
386
+ }
387
+ });
388
+ if (hasKey) return true;
389
+ }
390
+ if (whitelist.emails && this.config.userEmail) {
391
+ if (whitelist.emails.includes(this.config.userEmail)) {
392
+ return true;
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+ showMaintenancePage() {
398
+ if (!this.siteConfig) return;
399
+ const config = this.siteConfig.maintenance;
400
+ const overlay = document.createElement("div");
401
+ overlay.id = "featurely-maintenance-overlay";
402
+ overlay.className = "featurely-maintenance-overlay";
403
+ if (config.type === "custom" && config.customHtml) {
404
+ overlay.innerHTML = config.customHtml;
405
+ } else {
406
+ overlay.innerHTML = this.getDefaultMaintenanceHtml(config);
407
+ }
408
+ document.body.appendChild(overlay);
409
+ }
410
+ hideMaintenancePage() {
411
+ const overlay = document.getElementById("featurely-maintenance-overlay");
412
+ if (overlay) {
413
+ overlay.remove();
414
+ }
415
+ }
416
+ getDefaultMaintenanceHtml(config) {
417
+ const restoration = config.expectedRestoration ? new Date(config.expectedRestoration).toLocaleString() : "soon";
418
+ const statusLink = config.showStatusLink && config.statusPageUrl ? `<p><a href="${config.statusPageUrl}" target="_blank" rel="noopener noreferrer" class="featurely-status-link">View Status Page \u2192</a></p>` : "";
419
+ return `
420
+ <div class="featurely-maintenance-content">
421
+ <div class="featurely-maintenance-icon">\u{1F527}</div>
422
+ <h1>We'll be back soon!</h1>
423
+ <p>We're performing scheduled maintenance.</p>
424
+ <p class="featurely-maintenance-time">Expected restoration: <strong>${restoration}</strong></p>
425
+ ${statusLink}
426
+ <p class="featurely-maintenance-footer">Thank you for your patience.</p>
427
+ </div>
428
+ `;
429
+ }
430
+ // ============================================================================
431
+ // Status Messages
432
+ // ============================================================================
433
+ updateMessages() {
434
+ if (!this.siteConfig) return;
435
+ const currentTime = /* @__PURE__ */ new Date();
436
+ const currentPath = window.location.pathname;
437
+ const activeMessages = this.siteConfig.messages.filter((message) => {
438
+ if (this.dismissedMessages.has(message.id)) {
439
+ return false;
440
+ }
441
+ if (message.startsAt && new Date(message.startsAt) > currentTime) {
442
+ return false;
443
+ }
444
+ if (message.expiresAt && new Date(message.expiresAt) < currentTime) {
445
+ return false;
446
+ }
447
+ if (message.targetPages && message.targetPages.length > 0) {
448
+ const matches = message.targetPages.some((pattern) => {
449
+ const regex = new RegExp(pattern);
450
+ return regex.test(currentPath);
451
+ });
452
+ if (!matches) return false;
453
+ }
454
+ return true;
455
+ });
456
+ this.messageContainers.forEach((container, id) => {
457
+ if (!activeMessages.find((m) => m.id === id)) {
458
+ container.remove();
459
+ this.messageContainers.delete(id);
460
+ }
461
+ });
462
+ activeMessages.forEach((message) => {
463
+ if (!this.messageContainers.has(message.id)) {
464
+ this.showMessage(message);
465
+ if (this.config.onMessageReceived) {
466
+ this.config.onMessageReceived(message);
467
+ }
468
+ }
469
+ });
470
+ }
471
+ showMessage(message) {
472
+ var _a;
473
+ const messageEl = document.createElement("div");
474
+ messageEl.className = `featurely-message featurely-message-${message.type} featurely-message-${message.position} featurely-message-${message.style}`;
475
+ messageEl.dataset.messageId = message.id;
476
+ const icon = message.icon || this.getDefaultIcon(message.type);
477
+ const ctaHtml = message.cta ? `<a href="${message.cta.url}" class="featurely-message-cta">${message.cta.text}</a>` : "";
478
+ const closeButton = message.dismissible ? `<button class="featurely-message-close" aria-label="Close message">&times;</button>` : "";
479
+ messageEl.innerHTML = `
480
+ <div class="featurely-message-content">
481
+ <div class="featurely-message-icon">${icon}</div>
482
+ <div class="featurely-message-text">
483
+ <strong class="featurely-message-title">${message.title}</strong>
484
+ <span class="featurely-message-body">${message.message}</span>
485
+ </div>
486
+ ${ctaHtml}
487
+ ${closeButton}
488
+ </div>
489
+ `;
490
+ if (message.dismissible) {
491
+ const closeBtn = messageEl.querySelector(".featurely-message-close");
492
+ closeBtn == null ? void 0 : closeBtn.addEventListener("click", () => this.dismissMessage(message.id));
493
+ }
494
+ if ((_a = message.cta) == null ? void 0 : _a.action) {
495
+ const ctaEl = messageEl.querySelector(".featurely-message-cta");
496
+ ctaEl == null ? void 0 : ctaEl.addEventListener("click", (e) => {
497
+ var _a2;
498
+ e.preventDefault();
499
+ if ((_a2 = message.cta) == null ? void 0 : _a2.action) {
500
+ this.handleMessageAction(message.cta.action, message);
501
+ }
502
+ });
503
+ }
504
+ document.body.appendChild(messageEl);
505
+ setTimeout(() => messageEl.classList.add("featurely-message-show"), 10);
506
+ this.messageContainers.set(message.id, messageEl);
507
+ this.trackEvent("message_shown", {
508
+ messageId: message.id,
509
+ messageType: message.type,
510
+ messagePosition: message.position,
511
+ messageStyle: message.style
512
+ });
513
+ if (message.style === "toast" && message.dismissible) {
514
+ setTimeout(() => this.dismissMessage(message.id), 5e3);
515
+ }
516
+ }
517
+ dismissMessage(messageId) {
518
+ var _a, _b;
519
+ const messageEl = this.messageContainers.get(messageId);
520
+ if (!messageEl) return;
521
+ messageEl.classList.remove("featurely-message-show");
522
+ setTimeout(() => {
523
+ messageEl.remove();
524
+ this.messageContainers.delete(messageId);
525
+ }, 300);
526
+ this.dismissedMessages.add(messageId);
527
+ this.saveDismissedMessages();
528
+ this.trackEvent("message_dismissed", {
529
+ messageId,
530
+ messageType: ((_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.find((m) => m.id === messageId)) == null ? void 0 : _b.type) || "unknown"
531
+ });
532
+ if (this.config.onMessageDismissed) {
533
+ this.config.onMessageDismissed(messageId);
534
+ }
535
+ }
536
+ clearMessages() {
537
+ this.messageContainers.forEach((el) => el.remove());
538
+ this.messageContainers.clear();
539
+ }
540
+ handleMessageAction(action, message) {
541
+ var _a;
542
+ console.log("Message action triggered:", action, message);
543
+ if ((_a = message.cta) == null ? void 0 : _a.url) {
544
+ window.location.href = message.cta.url;
545
+ }
546
+ }
547
+ getDefaultIcon(type) {
548
+ const icons = {
549
+ info: "\u2139\uFE0F",
550
+ warning: "\u26A0\uFE0F",
551
+ error: "\u274C",
552
+ success: "\u2705"
553
+ };
554
+ return icons[type];
555
+ }
556
+ // ============================================================================
557
+ // Persistence
558
+ // ============================================================================
559
+ loadDismissedMessages() {
560
+ try {
561
+ const stored = localStorage.getItem("featurely_dismissed_messages");
562
+ if (stored) {
563
+ this.dismissedMessages = new Set(JSON.parse(stored));
564
+ }
565
+ } catch {
566
+ }
567
+ }
568
+ saveDismissedMessages() {
569
+ try {
570
+ localStorage.setItem(
571
+ "featurely_dismissed_messages",
572
+ JSON.stringify(Array.from(this.dismissedMessages))
573
+ );
574
+ } catch {
575
+ }
576
+ }
577
+ // ============================================================================
578
+ // Styles
579
+ // ============================================================================
580
+ injectStyles() {
581
+ if (document.getElementById("featurely-site-manager-styles")) {
582
+ return;
583
+ }
584
+ const style = document.createElement("style");
585
+ style.id = "featurely-site-manager-styles";
586
+ style.textContent = `
587
+ /* Maintenance Overlay */
588
+ .featurely-maintenance-overlay {
589
+ position: fixed;
590
+ top: 0;
591
+ left: 0;
592
+ width: 100%;
593
+ height: 100%;
594
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
595
+ z-index: 999999;
596
+ display: flex;
597
+ align-items: center;
598
+ justify-content: center;
599
+ color: white;
600
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
601
+ }
602
+
603
+ .featurely-maintenance-content {
604
+ text-align: center;
605
+ max-width: 600px;
606
+ padding: 40px;
607
+ background: rgba(255, 255, 255, 0.1);
608
+ border-radius: 16px;
609
+ backdrop-filter: blur(10px);
610
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
611
+ }
612
+
613
+ .featurely-maintenance-icon {
614
+ font-size: 64px;
615
+ margin-bottom: 20px;
616
+ }
617
+
618
+ .featurely-maintenance-content h1 {
619
+ font-size: 36px;
620
+ margin: 0 0 16px 0;
621
+ font-weight: 700;
622
+ }
623
+
624
+ .featurely-maintenance-content p {
625
+ font-size: 18px;
626
+ margin: 12px 0;
627
+ opacity: 0.9;
628
+ }
629
+
630
+ .featurely-maintenance-time {
631
+ margin: 24px 0;
632
+ padding: 16px;
633
+ background: rgba(255, 255, 255, 0.1);
634
+ border-radius: 8px;
635
+ }
636
+
637
+ .featurely-status-link {
638
+ display: inline-block;
639
+ margin: 20px 0;
640
+ padding: 12px 24px;
641
+ background: rgba(255, 255, 255, 0.2);
642
+ color: white;
643
+ text-decoration: none;
644
+ border-radius: 8px;
645
+ font-weight: 600;
646
+ transition: all 0.2s;
647
+ }
648
+
649
+ .featurely-status-link:hover {
650
+ background: rgba(255, 255, 255, 0.3);
651
+ transform: translateY(-2px);
652
+ }
653
+
654
+ .featurely-maintenance-footer {
655
+ margin-top: 32px;
656
+ font-size: 14px;
657
+ opacity: 0.7;
658
+ }
659
+
660
+ /* Status Messages */
661
+ .featurely-message {
662
+ position: fixed;
663
+ left: 0;
664
+ right: 0;
665
+ z-index: 999998;
666
+ padding: 16px 20px;
667
+ background: white;
668
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
669
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
670
+ opacity: 0;
671
+ transform: translateY(-20px);
672
+ transition: all 0.3s ease;
673
+ }
674
+
675
+ .featurely-message-show {
676
+ opacity: 1;
677
+ transform: translateY(0);
678
+ }
679
+
680
+ .featurely-message-top {
681
+ top: 0;
682
+ }
683
+
684
+ .featurely-message-bottom {
685
+ bottom: 0;
686
+ }
687
+
688
+ .featurely-message-toast {
689
+ left: auto;
690
+ right: 20px;
691
+ max-width: 400px;
692
+ border-radius: 8px;
693
+ }
694
+
695
+ .featurely-message-toast.featurely-message-top {
696
+ top: 20px;
697
+ }
698
+
699
+ .featurely-message-toast.featurely-message-bottom {
700
+ bottom: 20px;
701
+ }
702
+
703
+ .featurely-message-content {
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 12px;
707
+ }
708
+
709
+ .featurely-message-icon {
710
+ font-size: 24px;
711
+ flex-shrink: 0;
712
+ }
713
+
714
+ .featurely-message-text {
715
+ flex: 1;
716
+ display: flex;
717
+ flex-direction: column;
718
+ gap: 4px;
719
+ }
720
+
721
+ .featurely-message-title {
722
+ font-size: 14px;
723
+ font-weight: 600;
724
+ display: block;
725
+ }
726
+
727
+ .featurely-message-body {
728
+ font-size: 14px;
729
+ opacity: 0.9;
730
+ }
731
+
732
+ .featurely-message-cta {
733
+ padding: 8px 16px;
734
+ border-radius: 6px;
735
+ text-decoration: none;
736
+ font-weight: 600;
737
+ font-size: 14px;
738
+ white-space: nowrap;
739
+ transition: all 0.2s;
740
+ }
741
+
742
+ .featurely-message-close {
743
+ background: none;
744
+ border: none;
745
+ font-size: 24px;
746
+ line-height: 1;
747
+ cursor: pointer;
748
+ padding: 0;
749
+ width: 24px;
750
+ height: 24px;
751
+ display: flex;
752
+ align-items: center;
753
+ justify-content: center;
754
+ opacity: 0.5;
755
+ transition: opacity 0.2s;
756
+ flex-shrink: 0;
757
+ }
758
+
759
+ .featurely-message-close:hover {
760
+ opacity: 1;
761
+ }
762
+
763
+ /* Message Types */
764
+ .featurely-message-info {
765
+ background: #e3f2fd;
766
+ color: #1976d2;
767
+ }
768
+
769
+ .featurely-message-info .featurely-message-cta {
770
+ background: #1976d2;
771
+ color: white;
772
+ }
773
+
774
+ .featurely-message-info .featurely-message-cta:hover {
775
+ background: #1565c0;
776
+ }
777
+
778
+ .featurely-message-warning {
779
+ background: #fff3e0;
780
+ color: #f57c00;
781
+ }
782
+
783
+ .featurely-message-warning .featurely-message-cta {
784
+ background: #f57c00;
785
+ color: white;
786
+ }
787
+
788
+ .featurely-message-warning .featurely-message-cta:hover {
789
+ background: #ef6c00;
790
+ }
791
+
792
+ .featurely-message-error {
793
+ background: #ffebee;
794
+ color: #c62828;
795
+ }
796
+
797
+ .featurely-message-error .featurely-message-cta {
798
+ background: #c62828;
799
+ color: white;
800
+ }
801
+
802
+ .featurely-message-error .featurely-message-cta:hover {
803
+ background: #b71c1c;
804
+ }
805
+
806
+ .featurely-message-success {
807
+ background: #e8f5e9;
808
+ color: #2e7d32;
809
+ }
810
+
811
+ .featurely-message-success .featurely-message-cta {
812
+ background: #2e7d32;
813
+ color: white;
814
+ }
815
+
816
+ .featurely-message-success .featurely-message-cta:hover {
817
+ background: #1b5e20;
818
+ }
819
+
820
+ /* Mobile Responsive */
821
+ @media (max-width: 640px) {
822
+ .featurely-message-toast {
823
+ left: 10px;
824
+ right: 10px;
825
+ max-width: none;
826
+ }
827
+
828
+ .featurely-maintenance-content {
829
+ padding: 20px;
830
+ }
831
+
832
+ .featurely-maintenance-content h1 {
833
+ font-size: 28px;
834
+ }
835
+
836
+ .featurely-maintenance-content p {
837
+ font-size: 16px;
838
+ }
839
+ }
840
+ `;
841
+ document.head.appendChild(style);
842
+ }
843
+ };
844
+ var index_default = SiteManager;
845
+ export {
846
+ SiteManager,
847
+ index_default as default
848
+ };