@nuitee/booking-widget 1.0.1 → 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.
@@ -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;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Runtime configuration service for the booking widget.
3
+ *
4
+ * Fetches styling config from /load-config keyed by propertyKey.
5
+ * Results are cached in memory so subsequent calls for the same key are instant.
6
+ * Throws (or rejects) when propertyKey is missing — callers must handle the
7
+ * "locked" state and display an error UI rather than attempting to fall back.
8
+ */
9
+
10
+ import { DEFAULT_COLORS } from '../core/stripe-config.js';
11
+ import { deriveWidgetStyles } from '../core/color-utils.js';
12
+
13
+ const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
14
+
15
+ /** In-memory cache: propertyKey → raw API color object */
16
+ const _configCache = new Map();
17
+
18
+ /**
19
+ * Map the /load-config response field names to the internal color keys used
20
+ * throughout the widget.
21
+ *
22
+ * API field → internal key
23
+ * widgetBackground → background
24
+ * widgetTextColor → text
25
+ * primaryColor → primary
26
+ * buttonTextColor → primaryText
27
+ * widgetCardColor → card
28
+ */
29
+ function mapApiColors(data) {
30
+ const mapped = {};
31
+ if (data.widgetBackground) mapped.background = data.widgetBackground;
32
+ if (data.widgetTextColor) mapped.text = data.widgetTextColor;
33
+ if (data.primaryColor) mapped.primary = data.primaryColor;
34
+ if (data.buttonTextColor) mapped.primaryText = data.buttonTextColor;
35
+ if (data.widgetCardColor) mapped.card = data.widgetCardColor;
36
+ return mapped;
37
+ }
38
+
39
+ /**
40
+ * Merge colors following priority (highest → lowest):
41
+ * 1. installerColors (explicit values in the `colors` prop win over everything)
42
+ * 2. mappedApiColors (values fetched from /load-config override package defaults)
43
+ * 3. DEFAULT_COLORS (built-in fallback)
44
+ *
45
+ * @param {object} mappedApiColors - Colors mapped from the API response.
46
+ * @param {object|null} installerColors - Colors passed via the `colors` prop.
47
+ * @returns {object} Resolved color set with all five standard keys.
48
+ */
49
+ export function mergeColors(mappedApiColors, installerColors) {
50
+ const base = { ...DEFAULT_COLORS, ...(mappedApiColors || {}) };
51
+ if (!installerColors || typeof installerColors !== 'object') return base;
52
+ return {
53
+ background: installerColors.background ?? base.background,
54
+ text: installerColors.text ?? base.text,
55
+ primary: installerColors.primary ?? base.primary,
56
+ primaryText: installerColors.primaryText ?? base.primaryText,
57
+ card: installerColors.card ?? installerColors.background ?? base.card,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Fetch runtime styling configuration from the /load-config endpoint.
63
+ *
64
+ * - Throws synchronously (or rejects) when propertyKey is null, undefined, or
65
+ * an empty string. Callers must catch this and render a "Missing Configuration"
66
+ * error state rather than attempting to render the normal widget UI.
67
+ * - Caches the raw API color payload by propertyKey. Subsequent calls for the
68
+ * same key re-use the cache and only re-merge installer overrides.
69
+ *
70
+ * @param {string} propertyKey - The hotel property key / API key.
71
+ * @param {object|null} installerColors - Optional installer color overrides.
72
+ * @returns {Promise<{ apiColors: object, colors: object, widgetStyles: object }>}
73
+ */
74
+ export async function fetchRuntimeConfig(propertyKey, installerColors = null) {
75
+ if (!propertyKey || !String(propertyKey).trim()) {
76
+ throw new Error('propertyKey is required to initialize the booking widget.');
77
+ }
78
+
79
+ const key = String(propertyKey).trim();
80
+
81
+ if (_configCache.has(key)) {
82
+ const apiColors = _configCache.get(key);
83
+ const colors = mergeColors(apiColors, installerColors);
84
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
85
+ }
86
+
87
+ const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}&mode=sandbox`;
88
+ const res = await fetch(url);
89
+ if (!res.ok) {
90
+ throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
91
+ }
92
+
93
+ const data = await res.json();
94
+ const apiColors = mapApiColors(data);
95
+ _configCache.set(key, apiColors);
96
+
97
+ const colors = mergeColors(apiColors, installerColors);
98
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
99
+ }
@@ -10,7 +10,7 @@
10
10
  <button class="booking-widget-close" @click="requestClose">
11
11
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
12
12
  </button>
13
- <div v-if="state.step !== 'confirmation'" class="booking-widget-step-indicator">
13
+ <div v-if="hasPropertyKey && configLoaded && state.step !== 'confirmation'" class="booking-widget-step-indicator">
14
14
  <template v-for="(step, i) in STEPS" :key="step.key">
15
15
  <div class="step-item">
16
16
  <span
@@ -34,7 +34,46 @@
34
34
  </span>
35
35
  </template>
36
36
  </div>
37
- <div class="booking-widget-step-content">
37
+ <!-- Missing propertyKey error -->
38
+ <div v-if="!propertyKey || !String(propertyKey).trim()" class="booking-widget-config-error" role="alert">
39
+ <div class="booking-widget-config-error__icon-wrap">
40
+ <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>
41
+ </div>
42
+ <span class="booking-widget-config-error__badge">
43
+ <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>
44
+ Missing Configuration
45
+ </span>
46
+ <h3 class="booking-widget-config-error__title">Widget Not Configured</h3>
47
+ <p class="booking-widget-config-error__desc">
48
+ A <code>propertyKey</code> prop is required to initialize this booking widget. Please provide it to load rooms and availability.
49
+ </p>
50
+ <div class="booking-widget-config-error__divider"></div>
51
+ <p class="booking-widget-config-error__hint">Contact the site administrator to configure this widget.</p>
52
+ </div>
53
+
54
+ <!-- Config fetch error -->
55
+ <div v-else-if="configError" class="booking-widget-config-error" role="alert">
56
+ <div class="booking-widget-config-error__icon-wrap">
57
+ <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>
58
+ </div>
59
+ <span class="booking-widget-config-error__badge">Configuration Error</span>
60
+ <h3 class="booking-widget-config-error__title">Could Not Load Config</h3>
61
+ <p class="booking-widget-config-error__desc">{{ configError }}</p>
62
+ <div class="booking-widget-config-error__divider"></div>
63
+ <p class="booking-widget-config-error__hint">Please try again or contact support.</p>
64
+ <button class="booking-widget-config-error__retry" @click="_initRuntimeConfig()">
65
+ <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>
66
+ Try Again
67
+ </button>
68
+ </div>
69
+
70
+ <!-- Config loading -->
71
+ <div v-else-if="configLoading" class="booking-widget-config-loading">
72
+ <div class="booking-widget-config-loading__spinner"></div>
73
+ <span class="booking-widget-config-loading__text">Loading configuration…</span>
74
+ </div>
75
+
76
+ <div v-else class="booking-widget-step-content">
38
77
  <!-- Dates Step -->
39
78
  <div v-if="state.step === 'dates'">
40
79
  <h2 class="step-title">Plan Your Stay</h2>
@@ -442,6 +481,8 @@ import { loadStripe } from '@stripe/stripe-js';
442
481
  import '../core/styles.css';
443
482
  import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
444
483
  import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
484
+ import { fetchRuntimeConfig } from '../utils/config-service.js';
485
+
445
486
 
446
487
  const BASE_STEPS = [
447
488
  { key: 'dates', label: 'Dates & Guests', num: '01' },
@@ -467,6 +508,8 @@ export default {
467
508
  propertyId: { type: [String, Number], default: '' },
468
509
  /** Property key/hash for decrypt/pref (pass from your app instead of env). */
469
510
  propertyKey: { type: String, default: '' },
511
+ /** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
512
+ mode: { type: String, default: '' },
470
513
  availabilityBaseUrl: { type: String, default: '' },
471
514
  propertyBaseUrl: { type: String, default: '' },
472
515
  s3BaseUrl: { type: String, default: '' },
@@ -492,6 +535,10 @@ export default {
492
535
  loadingRates: false,
493
536
  apiError: null,
494
537
  confirmationCode: null,
538
+ configLoading: false,
539
+ configLoaded: false,
540
+ configError: null,
541
+ runtimeWidgetStyles: {},
495
542
  calendarMonth: new Date().getMonth(),
496
543
  calendarYear: new Date().getFullYear(),
497
544
  pickState: 0,
@@ -556,9 +603,10 @@ export default {
556
603
  const url = this.effectivePaymentIntentUrl;
557
604
  if (!url) return null;
558
605
  return async (payload) => {
606
+ const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
559
607
  const headers = { 'Content-Type': 'application/json' };
560
- if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
561
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
608
+ if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
609
+ const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
562
610
  if (!res.ok) throw new Error(await res.text());
563
611
  const data = await res.json();
564
612
  const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
@@ -566,6 +614,12 @@ export default {
566
614
  return { clientSecret, confirmationToken };
567
615
  };
568
616
  },
617
+ isSandbox() {
618
+ return this.mode === 'sandbox';
619
+ },
620
+ hasPropertyKey() {
621
+ return !!(this.propertyKey != null && this.propertyKey !== '' && String(this.propertyKey).trim() !== '');
622
+ },
569
623
  bookingApiRef() {
570
624
  if (this.bookingApi && typeof this.bookingApi.fetchRooms === 'function') return this.bookingApi;
571
625
  if ((this.effectiveApiBaseUrl || this.propertyKey) && typeof createBookingApi === 'function') {
@@ -576,29 +630,14 @@ export default {
576
630
  s3BaseUrl: this.effectiveS3BaseUrl || undefined,
577
631
  propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
578
632
  propertyKey: this.propertyKey || undefined,
633
+ mode: this.mode === 'sandbox' ? 'sandbox' : undefined,
579
634
  headers: this.apiSecret ? { 'X-API-Key': this.apiSecret } : undefined,
580
635
  });
581
636
  }
582
637
  return null;
583
638
  },
584
639
  widgetStyles() {
585
- if (!this.colors) return {};
586
- const styles = {};
587
- if (this.colors.background) {
588
- styles['--bg'] = this.colors.background;
589
- styles['--card'] = this.colors.background;
590
- }
591
- if (this.colors.text) {
592
- styles['--fg'] = this.colors.text;
593
- styles['--card-fg'] = this.colors.text;
594
- }
595
- if (this.colors.primary) {
596
- styles['--primary'] = this.colors.primary;
597
- }
598
- if (this.colors.primaryText) {
599
- styles['--primary-fg'] = this.colors.primaryText;
600
- }
601
- return styles;
640
+ return this.runtimeWidgetStyles;
602
641
  },
603
642
  nights() {
604
643
  if (!this.state.checkIn || !this.state.checkOut) return 0;
@@ -684,7 +723,17 @@ export default {
684
723
  return this.isOpen && !this.isClosing && this.isReadyForOpen;
685
724
  },
686
725
  },
726
+ created() {
727
+ this._initRuntimeConfig();
728
+ },
687
729
  watch: {
730
+ propertyKey() {
731
+ this._initRuntimeConfig();
732
+ },
733
+ colors: {
734
+ deep: true,
735
+ handler() { this._initRuntimeConfig(); },
736
+ },
688
737
  isOpen: {
689
738
  handler(open) {
690
739
  if (open && this.onOpen) this.onOpen();
@@ -718,10 +767,31 @@ export default {
718
767
  },
719
768
  },
720
769
  methods: {
770
+ async _initRuntimeConfig() {
771
+ if (!this.propertyKey || !String(this.propertyKey).trim()) {
772
+ this.configError = 'propertyKey is required to initialize the booking widget.';
773
+ this.configLoading = false;
774
+ this.configLoaded = false;
775
+ this.runtimeWidgetStyles = {};
776
+ return;
777
+ }
778
+ this.configLoading = true;
779
+ this.configError = null;
780
+ this.configLoaded = false;
781
+ try {
782
+ const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors);
783
+ this.runtimeWidgetStyles = widgetStyles;
784
+ this.configLoaded = true;
785
+ } catch (err) {
786
+ this.configError = err?.message || 'Failed to load widget configuration.';
787
+ } finally {
788
+ this.configLoading = false;
789
+ }
790
+ },
721
791
  async fetchConfirmationDetails(token) {
722
792
  const t = String(token || '').trim();
723
793
  if (!t) throw new Error('Missing confirmation token');
724
- const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}`;
794
+ const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
725
795
  const res = await fetch(url, { method: 'POST' });
726
796
  if (!res.ok) throw new Error(await res.text());
727
797
  return await res.json();
@@ -750,7 +820,7 @@ export default {
750
820
  },
751
821
  async loadStripePaymentElement() {
752
822
  if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.stripePublishableKey || typeof this.effectiveCreatePaymentIntent !== 'function') return;
753
- const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined });
823
+ const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
754
824
  this.paymentElementReady = false;
755
825
  try {
756
826
  await this.$nextTick();
@@ -789,6 +859,8 @@ export default {
789
859
  return i === ci ? 'active' : i < ci ? 'past' : 'future';
790
860
  },
791
861
  goToStep(step) {
862
+ // Block navigation to room/rate steps until runtime config is loaded.
863
+ if ((step === 'rooms' || step === 'rates') && !this.configLoaded) return;
792
864
  if (step !== 'summary' && step !== 'payment') this.checkoutShowPaymentForm = false;
793
865
  if (step === 'payment') this.checkoutShowPaymentForm = true;
794
866
  this.state.step = step;
@@ -859,7 +931,7 @@ export default {
859
931
  },
860
932
  confirmReservation() {
861
933
  if (!this.canSubmit) return;
862
- const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined });
934
+ const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
863
935
  // Summary step with Stripe: go to payment step (form loads there)
864
936
  if (this.state.step === 'summary' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
865
937
  this.apiError = null;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuitee/booking-widget",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A beautiful, customizable booking widget modal that can be embedded in any website. Supports vanilla JavaScript, React, and Vue.js.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -34,7 +34,7 @@
34
34
  "build": "node scripts/generate-stripe-config.js && npm run build:css && npm run build:js && npm run build:react && npm run build:vue && npm run build:types",
35
35
  "build:stripe-config": "node scripts/generate-stripe-config.js",
36
36
  "build:css": "mkdir -p dist dist/core && cp src/core/styles.css dist/booking-widget.css && cp src/core/styles.css dist/core/styles.css",
37
- "build:js": "mkdir -p dist dist/core && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
37
+ "build:js": "mkdir -p dist dist/core dist/utils && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && cp src/core/color-utils.js dist/core/color-utils.js && cp src/utils/config-service.js dist/utils/config-service.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
38
38
  "build:react": "mkdir -p dist/react && cp src/react/BookingWidget.jsx dist/react/BookingWidget.jsx && cp src/core/styles.css dist/react/styles.css",
39
39
  "build:vue": "mkdir -p dist/vue && cp src/vue/BookingWidget.vue dist/vue/BookingWidget.vue && cp src/core/styles.css dist/vue/styles.css",
40
40
  "build:types": "npm run build:types:main && npm run build:types:react && npm run build:types:vue",