@nuitee/booking-widget 1.0.2 → 1.0.3

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.
@@ -1,4 +1,4 @@
1
- (function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_live_51T0SdzEj1UJpIPAMQQUH55w3Dj1E07CihP3iTOVO1IM4mMVAJvE86BiUmzGQKuNPPl05btyY39lRca8PSzRzZ9K700CncmOGQ0';})();
1
+ (function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';window.__BOOKING_WIDGET_DEFAULT_COLORS__={"background":"#022c32","text":"#ffffff","primary":"#f59e0b","primaryText":"#ffffff","card":"#395b60"};})();
2
2
  // ===== Icons (Lucide/shadcn) =====
3
3
  const icons = {
4
4
  calendar: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`,
@@ -51,6 +51,54 @@ function escapeHTML(value) {
51
51
  .replace(/'/g, '&#39;');
52
52
  }
53
53
 
54
+ function deriveWidgetStyles(c) {
55
+ if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
56
+ var expandHex = function (hex) {
57
+ if (!hex || typeof hex !== 'string') return null;
58
+ var h = hex.replace(/^#/, '').trim();
59
+ if (h.length === 3) return '#' + h.split('').map(function (x) { return x + x; }).join('');
60
+ return h.length === 6 ? '#' + h : null;
61
+ };
62
+ var hexToRgb = function (hex) {
63
+ var x = expandHex(hex);
64
+ if (!x) return null;
65
+ return [parseInt(x.slice(1, 3), 16), parseInt(x.slice(3, 5), 16), parseInt(x.slice(5, 7), 16)];
66
+ };
67
+ var hexToHsl = function (hex) {
68
+ var x = expandHex(hex);
69
+ if (!x) return null;
70
+ var r = parseInt(x.slice(1, 3), 16) / 255, g = parseInt(x.slice(3, 5), 16) / 255, b = parseInt(x.slice(5, 7), 16) / 255;
71
+ var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
72
+ if (max === min) h = s = 0;
73
+ else {
74
+ var d = max - min;
75
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
76
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
77
+ else if (max === g) h = ((b - r) / d + 2) / 6;
78
+ else h = ((r - g) / d + 4) / 6;
79
+ }
80
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
81
+ };
82
+ var bg = c.background || '#1a1a1a', fg = c.text || '#e0e0e0', primary = c.primary || '#3b82f6', primaryFg = c.primaryText || '#ffffff';
83
+ var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
84
+ var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
85
+ if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
86
+ styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
87
+ if (bgHsl) {
88
+ styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
89
+ styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
90
+ styles['--input-bg'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 6) + '%)';
91
+ }
92
+ if (fgHsl) {
93
+ styles['--secondary-fg'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(20, fgHsl[2] - 10) + '%)';
94
+ styles['--muted'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(30, fgHsl[2] - 25) + '%)';
95
+ }
96
+ styles['--font-serif'] = "'Playfair Display', Georgia, serif";
97
+ styles['--font-sans'] = "'Inter', system-ui, sans-serif";
98
+ styles['--radius'] = '0.75rem';
99
+ return styles;
100
+ }
101
+
54
102
  function getDefaultRooms() {
55
103
  return [];
56
104
  }
@@ -59,6 +107,9 @@ function getDefaultRates() {
59
107
  return [];
60
108
  }
61
109
 
110
+ // In-memory cache shared across all widget instances (keyed by propertyKey).
111
+ var __bwConfigCache = {};
112
+
62
113
  // ===== Core Widget Class =====
63
114
  class BookingWidget {
64
115
  constructor(options = {}) {
@@ -108,14 +159,20 @@ class BookingWidget {
108
159
  s3BaseUrl: options.s3BaseUrl || null,
109
160
  /** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
110
161
  confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
111
- // Color customization options
112
- colors: {
113
- background: options.colors?.background || null,
114
- text: options.colors?.text || null,
115
- primary: options.colors?.primary || null,
116
- primaryText: options.colors?.primaryText || null,
117
- ...options.colors
118
- },
162
+ // Color customization: CONFIG defaults from load-config, installer colors override
163
+ colors: (function () {
164
+ const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
165
+ const inst = options.colors && typeof options.colors === 'object' ? options.colors : {};
166
+ return {
167
+ background: inst.background ?? dc.background ?? null,
168
+ text: inst.text ?? dc.text ?? null,
169
+ primary: inst.primary ?? dc.primary ?? null,
170
+ primaryText: inst.primaryText ?? dc.primaryText ?? null,
171
+ // When card omitted, use installer's background for consistency
172
+ card: inst.card ?? inst.background ?? dc.card ?? dc.background ?? null,
173
+ ...inst
174
+ };
175
+ })(),
119
176
  ...options
120
177
  };
121
178
 
@@ -166,6 +223,16 @@ class BookingWidget {
166
223
  this.stripeInstance = null;
167
224
  this.elementsInstance = null;
168
225
 
226
+ // Store raw installer colors so they can be re-merged with API-fetched colors.
227
+ this._rawInstallerColors = options.colors && typeof options.colors === 'object'
228
+ ? Object.assign({}, options.colors)
229
+ : {};
230
+
231
+ // Config fetch state: 'idle' | 'loading' | 'loaded' | 'error'
232
+ this._configState = 'idle';
233
+ this._configError = null;
234
+ this._configPromise = null;
235
+
169
236
  this.calendarMonth = null;
170
237
  this.calendarYear = null;
171
238
  this.pickState = 0;
@@ -229,8 +296,13 @@ class BookingWidget {
229
296
  // ===== Widget Open/Close =====
230
297
  open() {
231
298
  if (!this.container) this.init();
299
+ if (!this.overlay || !this.widget) return; // container element not found
232
300
  this.overlay.classList.add('active');
233
301
  this.widget.classList.add('active');
302
+ // Only fetch config when propertyKey is present; missing-key is rendered inside the modal.
303
+ if (this.options.propertyKey && String(this.options.propertyKey).trim() && this._configState === 'idle') {
304
+ this._fetchRuntimeConfig();
305
+ }
234
306
  this.render();
235
307
  if (this.options.onOpen) this.options.onOpen();
236
308
  }
@@ -266,6 +338,15 @@ class BookingWidget {
266
338
  }
267
339
 
268
340
  goToStep(step) {
341
+ // Block room/rate navigation until runtime config (colors + styles) has loaded.
342
+ if ((step === 'rooms' || step === 'rates') && this._configState !== 'loaded') {
343
+ if (this._configState === 'loading' && this._configPromise) {
344
+ this._configPromise.then(() => {
345
+ if (this._configState === 'loaded') this.goToStep(step);
346
+ });
347
+ }
348
+ return;
349
+ }
269
350
  const doRender = () => {
270
351
  if (step !== 'summary' && step !== 'payment') {
271
352
  this.checkoutShowPaymentForm = false;
@@ -326,20 +407,19 @@ class BookingWidget {
326
407
  }
327
408
 
328
409
  this.container = container;
329
-
330
- // Create overlay
410
+
411
+ // Always create the overlay and modal so the error is shown inside the dialog,
412
+ // consistent with the React and Vue behaviour.
331
413
  this.overlay = document.createElement('div');
332
414
  this.overlay.className = 'booking-widget-overlay';
333
415
  this.overlay.addEventListener('click', () => this.close());
334
416
  container.appendChild(this.overlay);
335
417
 
336
- // Create widget modal
337
418
  this.widget = document.createElement('div');
338
419
  this.widget.className = 'booking-widget-modal';
339
420
  this.widget.innerHTML = `
340
421
  <button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
341
422
  <div class="booking-widget-step-indicator"></div>
342
- <div class="booking-widget-property-key-message" role="alert" style="display:none;margin:0 1.5em 1em;padding:0.75em 1em;background:var(--destructive,#ef4444);color:#fff;border-radius:8px;font-size:0.9em;"></div>
343
423
  <div class="booking-widget-step-content"></div>
344
424
  `;
345
425
  this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
@@ -349,35 +429,82 @@ class BookingWidget {
349
429
  });
350
430
  container.appendChild(this.widget);
351
431
 
352
- // Apply custom colors
353
432
  this.applyColors();
354
-
355
- // Inject CSS if not already present
356
433
  this.injectCSS();
357
434
  }
358
435
 
359
436
  applyColors() {
360
437
  if (!this.widget) return;
361
-
362
438
  const colors = this.options.colors;
363
439
  if (!colors) return;
364
-
365
- const style = this.widget.style;
366
-
367
- if (colors.background) {
368
- style.setProperty('--bg', colors.background);
369
- style.setProperty('--card', colors.background);
370
- }
371
- if (colors.text) {
372
- style.setProperty('--fg', colors.text);
373
- style.setProperty('--card-fg', colors.text);
374
- }
375
- if (colors.primary) {
376
- style.setProperty('--primary', colors.primary);
377
- }
378
- if (colors.primaryText) {
379
- style.setProperty('--primary-fg', colors.primaryText);
440
+ const styles = deriveWidgetStyles(colors);
441
+ const el = this.widget.style;
442
+ for (var k in styles) if (styles[k] != null && styles[k] !== '') el.setProperty(k, styles[k]);
443
+ }
444
+
445
+ /**
446
+ * Merge API-fetched colors with installer-provided raw colors and update
447
+ * this.options.colors so applyColors() uses the runtime values.
448
+ */
449
+ _applyApiColors(apiColors) {
450
+ const inst = this._rawInstallerColors || {};
451
+ this.options.colors = {
452
+ background: inst.background != null ? inst.background : (apiColors.background || null),
453
+ text: inst.text != null ? inst.text : (apiColors.text || null),
454
+ primary: inst.primary != null ? inst.primary : (apiColors.primary || null),
455
+ primaryText: inst.primaryText != null ? inst.primaryText : (apiColors.primaryText || null),
456
+ card: inst.card != null ? inst.card : (inst.background != null ? inst.background : (apiColors.card || null)),
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Fetch runtime styling config from /load-config.
462
+ * Caches result in __bwConfigCache (module-level, shared across instances).
463
+ * On success: merges colors, applies them, and re-renders.
464
+ * On error: sets _configState to 'error' and re-renders to show the error banner.
465
+ */
466
+ _fetchRuntimeConfig() {
467
+ const self = this;
468
+ const propertyKey = this.options.propertyKey;
469
+ const key = String(propertyKey).trim();
470
+
471
+ if (__bwConfigCache[key]) {
472
+ this._applyApiColors(__bwConfigCache[key]);
473
+ this._configState = 'loaded';
474
+ this.applyColors();
475
+ return Promise.resolve(__bwConfigCache[key]);
380
476
  }
477
+
478
+ this._configState = 'loading';
479
+ this.render();
480
+
481
+ const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + '&mode=sandbox';
482
+ this._configPromise = fetch(url)
483
+ .then(function (res) {
484
+ if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
485
+ return res.json();
486
+ })
487
+ .then(function (data) {
488
+ const apiColors = {};
489
+ if (data.widgetBackground) apiColors.background = data.widgetBackground;
490
+ if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
491
+ if (data.primaryColor) apiColors.primary = data.primaryColor;
492
+ if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
493
+ if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
494
+ __bwConfigCache[key] = apiColors;
495
+ self._applyApiColors(apiColors);
496
+ self._configState = 'loaded';
497
+ self.applyColors();
498
+ self.render();
499
+ return apiColors;
500
+ })
501
+ .catch(function (err) {
502
+ self._configError = err.message || 'Failed to load widget configuration.';
503
+ self._configState = 'error';
504
+ self.render();
505
+ });
506
+
507
+ return this._configPromise;
381
508
  }
382
509
 
383
510
  injectCSS() {
@@ -411,7 +538,7 @@ class BookingWidget {
411
538
  if (!el) return;
412
539
  const key = this.options.propertyKey;
413
540
  const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
414
- if (!hasPropertyKey || this.state.step === 'confirmation') {
541
+ if (!hasPropertyKey || this._configState !== 'loaded' || this.state.step === 'confirmation') {
415
542
  el.innerHTML = '';
416
543
  return;
417
544
  }
@@ -434,23 +561,73 @@ class BookingWidget {
434
561
  renderPropertyKeyMessage() {
435
562
  const el = this.widget.querySelector('.booking-widget-property-key-message');
436
563
  if (!el) return;
437
- const key = this.options.propertyKey;
438
- const missing = !key || (typeof key === 'string' && !key.trim());
439
- if (missing) {
440
- el.style.display = 'block';
441
- el.innerHTML = 'The propertyKey is missing. Please provide it as a prop to load rooms from the API.';
442
- } else {
443
- el.style.display = 'none';
444
- }
564
+ // Config errors are now rendered inside renderStepContent via the full error card.
565
+ el.style.display = 'none';
566
+ }
567
+
568
+ _retryConfigFetch() {
569
+ this._configState = 'idle';
570
+ this._configError = null;
571
+ this._configPromise = null;
572
+ this._fetchRuntimeConfig();
573
+ }
574
+
575
+ _configErrorHTML(type, title, body, hint) {
576
+ const lockIcon = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`;
577
+ const warnIcon = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`;
578
+ const retryIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;
579
+ const icon = type === 'missing' ? lockIcon : warnIcon;
580
+ const badgeLabel = type === 'missing'
581
+ ? `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> Missing Configuration`
582
+ : 'Configuration Error';
583
+ const retryBtn = type === 'fetch'
584
+ ? `<button class="booking-widget-config-error__retry" onclick="window.bookingWidgetInstance && window.bookingWidgetInstance._retryConfigFetch()">${retryIcon} Try Again</button>`
585
+ : '';
586
+ return `<div class="booking-widget-config-error" role="alert">
587
+ <div class="booking-widget-config-error__icon-wrap">${icon}</div>
588
+ <span class="booking-widget-config-error__badge">${badgeLabel}</span>
589
+ <h3 class="booking-widget-config-error__title">${escapeHTML(title)}</h3>
590
+ <p class="booking-widget-config-error__desc">${body}</p>
591
+ <div class="booking-widget-config-error__divider"></div>
592
+ <p class="booking-widget-config-error__hint">${escapeHTML(hint)}</p>
593
+ ${retryBtn}
594
+ </div>`;
445
595
  }
446
596
 
447
597
  // ===== Step Renderers =====
448
598
  renderStepContent() {
449
599
  const el = this.widget.querySelector('.booking-widget-step-content');
450
600
  if (!el) return;
601
+
602
+ // Missing propertyKey — show error card inside the modal, same as React/Vue.
451
603
  const key = this.options.propertyKey;
452
- const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
453
- if (!hasPropertyKey) {
604
+ if (!key || !String(key).trim()) {
605
+ el.innerHTML = this._configErrorHTML(
606
+ 'missing',
607
+ 'Widget Not Configured',
608
+ 'A <code>propertyKey</code> option is required to initialize this booking widget. Please provide it to load rooms and availability.',
609
+ 'Contact the site administrator to configure this widget.'
610
+ );
611
+ return;
612
+ }
613
+
614
+ if (this._configState === 'loading') {
615
+ el.innerHTML = `<div class="booking-widget-config-loading">
616
+ <div class="booking-widget-config-loading__spinner"></div>
617
+ <span class="booking-widget-config-loading__text">Loading configuration\u2026</span>
618
+ </div>`;
619
+ return;
620
+ }
621
+ if (this._configState === 'error') {
622
+ el.innerHTML = this._configErrorHTML(
623
+ 'fetch',
624
+ 'Could Not Load Config',
625
+ escapeHTML(this._configError || 'An unexpected error occurred.'),
626
+ 'Please try again or contact support.'
627
+ );
628
+ return;
629
+ }
630
+ if (this._configState !== 'loaded') {
454
631
  el.innerHTML = '';
455
632
  return;
456
633
  }
@@ -618,6 +618,16 @@ function buildCheckoutPayload(state, options = {}) {
618
618
  * @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
619
619
  * @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
620
620
  */
621
+ function generateUUID() {
622
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
623
+ return crypto.randomUUID();
624
+ }
625
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
626
+ const r = Math.random() * 16 | 0;
627
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
628
+ });
629
+ }
630
+
621
631
  function buildPaymentIntentPayload(state, options = {}) {
622
632
  const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
623
633
  const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
@@ -657,6 +667,7 @@ function buildPaymentIntentPayload(state, options = {}) {
657
667
  email: g.email ?? '',
658
668
  occupancies: occupanciesForPayload,
659
669
  source_transaction: 'booking engine',
670
+ booking_code: generateUUID(),
660
671
  },
661
672
  };
662
673
  if (sandbox) out.sandbox = true;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Color utilities for widget theming. Installers pass 4 base colors (background, text, primary, primaryText);
3
+ * we derive --card, --secondary, --muted, --border, --input-bg for a coherent theme.
4
+ */
5
+
6
+ function expandHex(hex) {
7
+ if (!hex || typeof hex !== 'string') return null;
8
+ const h = hex.replace(/^#/, '').trim();
9
+ if (h.length === 3) return '#' + h.split('').map((c) => c + c).join('');
10
+ return h.length === 6 ? '#' + h : null;
11
+ }
12
+
13
+ function hexToRgb(hex) {
14
+ const expanded = expandHex(hex);
15
+ if (!expanded) return null;
16
+ return [
17
+ parseInt(expanded.slice(1, 3), 16),
18
+ parseInt(expanded.slice(3, 5), 16),
19
+ parseInt(expanded.slice(5, 7), 16),
20
+ ];
21
+ }
22
+
23
+ function hexToHsl(hex) {
24
+ const expanded = expandHex(hex);
25
+ if (!expanded) return null;
26
+ let r = parseInt(expanded.slice(1, 3), 16) / 255;
27
+ let g = parseInt(expanded.slice(3, 5), 16) / 255;
28
+ let b = parseInt(expanded.slice(5, 7), 16) / 255;
29
+ const max = Math.max(r, g, b);
30
+ const min = Math.min(r, g, b);
31
+ let h, s;
32
+ const l = (max + min) / 2;
33
+ if (max === min) {
34
+ h = s = 0;
35
+ } else {
36
+ const d = max - min;
37
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
38
+ switch (max) {
39
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
40
+ case g: h = ((b - r) / d + 2) / 6; break;
41
+ case b: h = ((r - g) / d + 4) / 6; break;
42
+ default: h = 0;
43
+ }
44
+ }
45
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
46
+ }
47
+
48
+ /**
49
+ * Derive full widget CSS variables from 4 base colors.
50
+ * @param {Object} c - { background, text, primary, primaryText, card? }
51
+ * @returns {Object} CSS variable names to values, e.g. { '--bg': '#1a1a1a', ... }
52
+ */
53
+ export function deriveWidgetStyles(c) {
54
+ if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
55
+ const bg = c.background || '#1a1a1a';
56
+ const fg = c.text || '#e0e0e0';
57
+ const primary = c.primary || '#3b82f6';
58
+ const primaryFg = c.primaryText || '#ffffff';
59
+ const cardExplicit = c.card;
60
+
61
+ const primaryRgb = hexToRgb(primary);
62
+ const bgHsl = hexToHsl(bg);
63
+ const fgHsl = hexToHsl(fg);
64
+
65
+ const styles = {
66
+ '--primary': primary,
67
+ '--primary-fg': primaryFg,
68
+ '--bg': bg,
69
+ '--fg': fg,
70
+ '--card-fg': fg,
71
+ };
72
+
73
+ if (primaryRgb) styles['--primary-rgb'] = `${primaryRgb[0]}, ${primaryRgb[1]}, ${primaryRgb[2]}`;
74
+
75
+ if (cardExplicit) {
76
+ styles['--card'] = cardExplicit + "40";
77
+ styles['--card-solid'] = cardExplicit;
78
+ } else if (bgHsl) {
79
+ const cardVal = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 2)}%)`;
80
+ styles['--card'] = cardVal;
81
+ styles['--card-solid'] = cardVal;
82
+ } else {
83
+ styles['--card'] = bg;
84
+ styles['--card-solid'] = bg;
85
+ }
86
+
87
+ if (bgHsl) {
88
+ styles['--secondary'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 10)}%)`;
89
+ styles['--border'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 14)}%)`;
90
+ styles['--input-bg'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 6)}%)`;
91
+ }
92
+
93
+ if (fgHsl) {
94
+ styles['--secondary-fg'] = `hsl(${fgHsl[0]}, ${fgHsl[1]}%, ${Math.max(20, fgHsl[2] - 10)}%)`;
95
+ styles['--muted'] = `hsl(${fgHsl[0]}, ${fgHsl[1]}%, ${Math.max(30, fgHsl[2] - 25)}%)`;
96
+ }
97
+
98
+ styles['--font-serif'] = "'Playfair Display', Georgia, serif";
99
+ styles['--font-sans'] = "'Inter', system-ui, sans-serif";
100
+ styles['--radius'] = '0.75rem';
101
+
102
+ return styles;
103
+ }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Config from CONFIG_URL (default https://ai.thehotelplanet.com/load-config). No .env used.
3
3
  * Property key is NOT from load-config; installers must set it (VITE_PROPERTY_KEY in app .env or propertyKey option).
4
+ * DEFAULT_COLORS from load-config CONFIG; installers can override via colors prop.
4
5
  */
5
- export const STRIPE_PUBLISHABLE_KEY = 'pk_live_51T0SdzEj1UJpIPAMQQUH55w3Dj1E07CihP3iTOVO1IM4mMVAJvE86BiUmzGQKuNPPl05btyY39lRca8PSzRzZ9K700CncmOGQ0';
6
+ export const STRIPE_PUBLISHABLE_KEY = 'pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';
6
7
  export const API_BASE_URL = 'https://ai.thehotelplanet.com';
7
8
  export const VITE_CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/api';
8
9
  export const VITE_AWS_S3_PATH = 'https://nuitee-s3-temp.s3.us-west-1.amazonaws.com';
10
+ export const DEFAULT_COLORS = {"background":"#022c32","text":"#ffffff","primary":"#f59e0b","primaryText":"#ffffff","card":"#395b60"};
@@ -106,6 +106,145 @@
106
106
  font-weight: 500;
107
107
  }
108
108
 
109
+ /* ===== Config Error State ===== */
110
+ .booking-widget-config-error {
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: center;
114
+ justify-content: center;
115
+ gap: 1.1em;
116
+ padding: 4em 2em;
117
+ text-align: center;
118
+ min-height: 300px;
119
+ }
120
+
121
+ .booking-widget-config-error__icon-wrap {
122
+ width: 5em;
123
+ height: 5em;
124
+ border-radius: 50%;
125
+ background: radial-gradient(circle, rgba(239,68,68,0.12) 0%, rgba(239,68,68,0.04) 100%);
126
+ border: 1.5px solid rgba(239, 68, 68, 0.22);
127
+ box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.06);
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ color: #f87171;
132
+ flex-shrink: 0;
133
+ margin-bottom: 0.25em;
134
+ }
135
+
136
+ .booking-widget-config-error__badge {
137
+ display: inline-flex;
138
+ align-items: center;
139
+ gap: 0.4em;
140
+ font-size: 0.7em;
141
+ font-weight: 600;
142
+ letter-spacing: 0.12em;
143
+ text-transform: uppercase;
144
+ color: #f87171;
145
+ background: rgba(239, 68, 68, 0.1);
146
+ border: 1px solid rgba(239, 68, 68, 0.18);
147
+ border-radius: 99em;
148
+ padding: 0.3em 0.85em;
149
+ }
150
+
151
+ .booking-widget-config-error__title {
152
+ font-family: var(--font-serif, 'Playfair Display', Georgia, serif);
153
+ font-size: 1.35em;
154
+ font-weight: 600;
155
+ color: var(--fg, #e8e0d5);
156
+ margin: 0;
157
+ letter-spacing: -0.01em;
158
+ }
159
+
160
+ .booking-widget-config-error__desc {
161
+ font-size: 0.875em;
162
+ color: var(--secondary-fg, #a09080);
163
+ max-width: 25em;
164
+ line-height: 1.7;
165
+ margin: 0;
166
+ }
167
+
168
+ .booking-widget-config-error__desc code {
169
+ font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace;
170
+ font-size: 0.88em;
171
+ background: rgba(255, 255, 255, 0.06);
172
+ color: var(--primary, #f59e0b);
173
+ padding: 0.12em 0.45em;
174
+ border-radius: 0.3em;
175
+ border: 1px solid rgba(255, 255, 255, 0.09);
176
+ }
177
+
178
+ .booking-widget-config-error__divider {
179
+ width: 2.5em;
180
+ height: 1.5px;
181
+ background: var(--border, rgba(255,255,255,0.1));
182
+ border-radius: 1px;
183
+ }
184
+
185
+ .booking-widget-config-error__hint {
186
+ font-size: 0.78em;
187
+ color: var(--muted, #6b5f50);
188
+ max-width: 21em;
189
+ line-height: 1.6;
190
+ margin: 0;
191
+ }
192
+
193
+ .booking-widget-config-error__retry {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 0.45em;
197
+ padding: 0.55em 1.4em;
198
+ background: transparent;
199
+ color: var(--secondary-fg, #a09080);
200
+ border: 1.5px solid var(--border, rgba(255,255,255,0.13));
201
+ border-radius: 99em;
202
+ font-size: 0.8em;
203
+ font-family: var(--font-sans, system-ui, sans-serif);
204
+ font-weight: 500;
205
+ cursor: pointer;
206
+ letter-spacing: 0.02em;
207
+ transition: border-color 0.2s, color 0.2s, background 0.2s;
208
+ margin-top: 0.25em;
209
+ }
210
+
211
+ .booking-widget-config-error__retry:hover {
212
+ border-color: var(--primary, #f59e0b);
213
+ color: var(--primary, #f59e0b);
214
+ background: rgba(245, 158, 11, 0.06);
215
+ }
216
+
217
+ /* ===== Config Loading State ===== */
218
+ .booking-widget-config-loading {
219
+ display: flex;
220
+ flex-direction: column;
221
+ align-items: center;
222
+ justify-content: center;
223
+ gap: 1.25em;
224
+ padding: 4em 2em;
225
+ text-align: center;
226
+ min-height: 300px;
227
+ }
228
+
229
+ .booking-widget-config-loading__spinner {
230
+ width: 2.75em;
231
+ height: 2.75em;
232
+ border: 2px solid var(--border, rgba(255,255,255,0.1));
233
+ border-top-color: var(--primary, hsl(38,60%,55%));
234
+ border-radius: 50%;
235
+ animation: bw-spin 0.75s linear infinite;
236
+ }
237
+
238
+ @keyframes bw-spin {
239
+ to { transform: rotate(360deg); }
240
+ }
241
+
242
+ .booking-widget-config-loading__text {
243
+ font-size: 0.875em;
244
+ color: var(--muted, #888);
245
+ letter-spacing: 0.01em;
246
+ }
247
+
109
248
  /* ===== Step Indicator ===== */
110
249
  .booking-widget-step-indicator {
111
250
  display: flex;
@@ -448,7 +587,7 @@
448
587
  }
449
588
 
450
589
  .booking-widget-modal .date-trigger:hover {
451
- border-color: hsl(38, 60%, 55%, 0.5);
590
+ border-color: var(--primary);
452
591
  }
453
592
 
454
593
  .booking-widget-modal .date-trigger .placeholder {
@@ -472,7 +611,7 @@
472
611
  max-width: calc(100vw - 2em);
473
612
  box-sizing: border-box;
474
613
  z-index: 10;
475
- background: var(--card);
614
+ background: var(--card-solid, var(--card));
476
615
  border: 1px solid var(--border);
477
616
  border-radius: var(--radius);
478
617
  padding: 1em;