@nuitee/booking-widget 1.0.2 → 1.0.4

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/USAGE.md CHANGED
@@ -254,7 +254,7 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
254
254
  | `onComplete` | function | Called when the booking is completed, with booking data. |
255
255
  | `onOpen` | function | Optional. Called when the widget opens. |
256
256
  | `propertyKey` | string | Property key for real rooms/rates/booking; omit for demo data. |
257
- | `colors` | object | Optional. `{ background, text, primary, primaryText }`. |
257
+ | `colors` | object | Optional. Pass 4 colors: `{ background, text, primary, primaryText }`. Card, muted, border, etc. are derived. Add `card` to override. |
258
258
 
259
259
  ### Vanilla constructor options
260
260
 
@@ -266,7 +266,7 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
266
266
  | `onClose` | function | Called when the user closes the widget. |
267
267
  | `onComplete` | function | Called when the booking is completed. |
268
268
  | `onOpen` | function | Optional. Called when the widget opens. |
269
- | `colors` | object | Optional. `{ background, text, primary, primaryText }`. |
269
+ | `colors` | object | Optional. Pass 4 colors: `{ background, text, primary, primaryText }`. Card, muted, border, etc. are derived. Add `card` to override. |
270
270
 
271
271
  ### Vanilla methods
272
272
 
@@ -286,4 +286,4 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
286
286
  | Use in vanilla (no build) | Load the standalone script and CSS, `new BookingWidget({ containerId, propertyKey, ... })`, then `init()` and `open()`. |
287
287
  | Use in vanilla (with build) | Import `@nuitee/booking-widget` and `@nuitee/booking-widget/css`, pass `propertyKey` and callbacks, then `open()`. |
288
288
  | Real rooms/rates/booking | Pass `propertyKey`. The widget uses its own runtime config for API and payments. |
289
- | Custom colors | Pass `colors: { background, text, primary, primaryText }`. |
289
+ | Custom colors | Pass `colors: { background, text, primary, primaryText }`. Card, muted, border, input-bg derive from these. |
@@ -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
  /**
3
3
  * Shared Booking API layer for the @nuitee/booking-widget.
4
4
  * Used by Vanilla JS, Vue, and React variants so integration lives in one place.
@@ -619,6 +619,16 @@ function buildCheckoutPayload(state, options = {}) {
619
619
  * @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
620
620
  * @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
621
621
  */
622
+ function generateUUID() {
623
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
624
+ return crypto.randomUUID();
625
+ }
626
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
627
+ const r = Math.random() * 16 | 0;
628
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
629
+ });
630
+ }
631
+
622
632
  function buildPaymentIntentPayload(state, options = {}) {
623
633
  const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
624
634
  const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
@@ -658,6 +668,7 @@ function buildPaymentIntentPayload(state, options = {}) {
658
668
  email: g.email ?? '',
659
669
  occupancies: occupanciesForPayload,
660
670
  source_transaction: 'booking engine',
671
+ booking_code: generateUUID(),
661
672
  },
662
673
  };
663
674
  if (sandbox) out.sandbox = true;
@@ -828,10 +839,64 @@ if (typeof window !== 'undefined') {
828
839
  return `<span class="icon" style="display:inline-flex;align-items:center;justify-content:center;width:${size};height:${size};min-width:${size};min-height:${size};vertical-align:middle;flex-shrink:0;color:inherit;">${sizedIcon}</span>`;
829
840
  }
830
841
 
831
- // Copy the BookingWidget class from core/widget.js
832
- // This is a simplified version that will be bundled
842
+ function deriveWidgetStyles(c) {
843
+ if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
844
+ function expandHex(hex) {
845
+ if (!hex || typeof hex !== 'string') return null;
846
+ var h = hex.replace(/^#/, '').trim();
847
+ if (h.length === 3) return '#' + h.split('').map(function (x) { return x + x; }).join('');
848
+ return h.length === 6 ? '#' + h : null;
849
+ }
850
+ function hexToRgb(hex) {
851
+ var x = expandHex(hex);
852
+ if (!x) return null;
853
+ return [parseInt(x.slice(1, 3), 16), parseInt(x.slice(3, 5), 16), parseInt(x.slice(5, 7), 16)];
854
+ }
855
+ function hexToHsl(hex) {
856
+ var x = expandHex(hex);
857
+ if (!x) return null;
858
+ 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;
859
+ var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
860
+ if (max === min) h = s = 0;
861
+ else {
862
+ var d = max - min;
863
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
864
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
865
+ else if (max === g) h = ((b - r) / d + 2) / 6;
866
+ else h = ((r - g) / d + 4) / 6;
867
+ }
868
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
869
+ }
870
+ var bg = c.background || '#1a1a1a', fg = c.text || '#e0e0e0', primary = c.primary || '#3b82f6', primaryFg = c.primaryText || '#ffffff';
871
+ var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
872
+ var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
873
+ if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
874
+ styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
875
+ if (bgHsl) {
876
+ styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
877
+ styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
878
+ styles['--input-bg'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 6) + '%)';
879
+ }
880
+ if (fgHsl) {
881
+ styles['--secondary-fg'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(20, fgHsl[2] - 10) + '%)';
882
+ styles['--muted'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(30, fgHsl[2] - 25) + '%)';
883
+ }
884
+ styles['--font-serif'] = "'Playfair Display', Georgia, serif";
885
+ styles['--font-sans'] = "'Inter', system-ui, sans-serif";
886
+ styles['--radius'] = '0.75rem';
887
+ return styles;
888
+ }
889
+
833
890
  const builtInStripeKey = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_STRIPE_KEY__) ? window.__BOOKING_WIDGET_STRIPE_KEY__ : '';
834
891
 
892
+ function escapeHTML(value) {
893
+ const s = String(value ?? '');
894
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
895
+ }
896
+
897
+ // In-memory config cache shared across all instances, keyed by propertyKey.
898
+ const __bwConfigCache = {};
899
+
835
900
  class BookingWidget {
836
901
  constructor(options = {}) {
837
902
  const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
@@ -869,14 +934,20 @@ if (typeof window !== 'undefined') {
869
934
  mode: options.mode || null,
870
935
  bookingApi: options.bookingApi || null,
871
936
  cssUrl: options.cssUrl || null,
872
- // Color customization options
873
- colors: {
874
- background: options.colors?.background || null,
875
- text: options.colors?.text || null,
876
- primary: options.colors?.primary || null,
877
- primaryText: options.colors?.primaryText || null,
878
- ...options.colors
879
- },
937
+ // Color customization: CONFIG defaults from load-config, installer colors override
938
+ colors: (function () {
939
+ const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
940
+ const inst = options.colors && typeof options.colors === 'object' ? options.colors : {};
941
+ return {
942
+ background: inst.background ?? dc.background ?? null,
943
+ text: inst.text ?? dc.text ?? null,
944
+ primary: inst.primary ?? dc.primary ?? null,
945
+ primaryText: inst.primaryText ?? dc.primaryText ?? null,
946
+ // When card omitted, use installer's background for consistency
947
+ card: inst.card ?? inst.background ?? dc.card ?? dc.background ?? null,
948
+ ...inst
949
+ };
950
+ })(),
880
951
  ...options
881
952
  };
882
953
 
@@ -930,6 +1001,15 @@ if (typeof window !== 'undefined') {
930
1001
  this.container = null;
931
1002
  this.overlay = null;
932
1003
  this.widget = null;
1004
+
1005
+ // Store raw installer colors for re-merging after API config fetch.
1006
+ this._rawInstallerColors = options.colors && typeof options.colors === 'object'
1007
+ ? Object.assign({}, options.colors)
1008
+ : {};
1009
+ // Config fetch state: 'idle' | 'loading' | 'loaded' | 'error'
1010
+ this._configState = 'idle';
1011
+ this._configError = null;
1012
+ this._configPromise = null;
933
1013
  }
934
1014
 
935
1015
  getNights() {
@@ -966,8 +1046,13 @@ if (typeof window !== 'undefined') {
966
1046
 
967
1047
  open() {
968
1048
  if (!this.container) this.init();
1049
+ if (!this.overlay || !this.widget) return; // container element not found
969
1050
  this.overlay.classList.add('active');
970
1051
  this.widget.classList.add('active');
1052
+ // Only fetch config when propertyKey is present; missing-key is rendered inside the modal.
1053
+ if (this.options.propertyKey && String(this.options.propertyKey).trim() && this._configState === 'idle') {
1054
+ this._fetchRuntimeConfig();
1055
+ }
971
1056
  this.render();
972
1057
  if (this.options.onOpen) this.options.onOpen();
973
1058
  }
@@ -994,6 +1079,12 @@ if (typeof window !== 'undefined') {
994
1079
  }
995
1080
 
996
1081
  goToStep(step) {
1082
+ if ((step === 'rooms' || step === 'rates') && this._configState !== 'loaded') {
1083
+ if (this._configState === 'loading' && this._configPromise) {
1084
+ this._configPromise.then(() => { if (this._configState === 'loaded') this.goToStep(step); });
1085
+ }
1086
+ return;
1087
+ }
997
1088
  if (step !== 'summary' && step !== 'payment') {
998
1089
  this.checkoutShowPaymentForm = false;
999
1090
  this.paymentElementReady = false;
@@ -1045,7 +1136,9 @@ if (typeof window !== 'undefined') {
1045
1136
  }
1046
1137
 
1047
1138
  this.container = container;
1048
-
1139
+
1140
+ // Always create the overlay and modal so the error is shown inside the dialog,
1141
+ // consistent with the React and Vue behaviour.
1049
1142
  this.overlay = document.createElement('div');
1050
1143
  this.overlay.className = 'booking-widget-overlay';
1051
1144
  this.overlay.addEventListener('click', () => this.close());
@@ -1056,44 +1149,116 @@ if (typeof window !== 'undefined') {
1056
1149
  this.widget.innerHTML = `
1057
1150
  <button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
1058
1151
  <div class="booking-widget-step-indicator"></div>
1059
- <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>
1060
1152
  <div class="booking-widget-step-content"></div>
1061
1153
  `;
1062
1154
  this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
1155
+ this.widget.querySelector('.booking-widget-step-indicator').addEventListener('click', (e) => {
1156
+ const stepEl = e.target.closest('[data-step]');
1157
+ if (stepEl && stepEl.dataset.step) this.goToStep(stepEl.dataset.step);
1158
+ });
1063
1159
  container.appendChild(this.widget);
1064
1160
 
1065
1161
  if (typeof window !== 'undefined') {
1066
1162
  window.bookingWidgetInstance = this;
1067
1163
  }
1068
1164
 
1069
- // Apply custom colors
1070
1165
  this.applyColors();
1071
-
1072
1166
  this.injectCSS();
1073
1167
  }
1074
1168
 
1075
1169
  applyColors() {
1076
1170
  if (!this.widget) return;
1077
-
1078
1171
  const colors = this.options.colors;
1079
1172
  if (!colors) return;
1080
-
1081
- const style = this.widget.style;
1082
-
1083
- if (colors.background) {
1084
- style.setProperty('--bg', colors.background);
1085
- style.setProperty('--card', colors.background);
1086
- }
1087
- if (colors.text) {
1088
- style.setProperty('--fg', colors.text);
1089
- style.setProperty('--card-fg', colors.text);
1090
- }
1091
- if (colors.primary) {
1092
- style.setProperty('--primary', colors.primary);
1093
- }
1094
- if (colors.primaryText) {
1095
- style.setProperty('--primary-fg', colors.primaryText);
1173
+ const styles = deriveWidgetStyles(colors);
1174
+ const el = this.widget.style;
1175
+ for (var k in styles) if (styles[k] != null && styles[k] !== '') el.setProperty(k, styles[k]);
1176
+ }
1177
+
1178
+ _applyApiColors(apiColors) {
1179
+ const inst = this._rawInstallerColors || {};
1180
+ this.options.colors = {
1181
+ background: inst.background != null ? inst.background : (apiColors.background || null),
1182
+ text: inst.text != null ? inst.text : (apiColors.text || null),
1183
+ primary: inst.primary != null ? inst.primary : (apiColors.primary || null),
1184
+ primaryText: inst.primaryText != null ? inst.primaryText : (apiColors.primaryText || null),
1185
+ card: inst.card != null ? inst.card : (inst.background != null ? inst.background : (apiColors.card || null)),
1186
+ };
1187
+ }
1188
+
1189
+ _fetchRuntimeConfig() {
1190
+ const self = this;
1191
+ const key = String(this.options.propertyKey).trim();
1192
+ const isSandbox = this.options.mode === 'sandbox';
1193
+ // Cache key is mode-aware so sandbox and live configs are stored separately.
1194
+ const cacheKey = isSandbox ? key + ':sandbox' : key;
1195
+
1196
+ if (__bwConfigCache[cacheKey]) {
1197
+ this._applyApiColors(__bwConfigCache[cacheKey]);
1198
+ this._configState = 'loaded';
1199
+ this.applyColors();
1200
+ return Promise.resolve(__bwConfigCache[cacheKey]);
1096
1201
  }
1202
+
1203
+ this._configState = 'loading';
1204
+ this.render();
1205
+
1206
+ const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
1207
+ this._configPromise = fetch(url)
1208
+ .then(function (res) {
1209
+ if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
1210
+ return res.json();
1211
+ })
1212
+ .then(function (data) {
1213
+ const apiColors = {};
1214
+ if (data.widgetBackground) apiColors.background = data.widgetBackground;
1215
+ if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
1216
+ if (data.primaryColor) apiColors.primary = data.primaryColor;
1217
+ if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1218
+ if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
1219
+ __bwConfigCache[cacheKey] = apiColors;
1220
+ self._applyApiColors(apiColors);
1221
+ self._configState = 'loaded';
1222
+ self.applyColors();
1223
+ self.render();
1224
+ return apiColors;
1225
+ })
1226
+ .catch(function (err) {
1227
+ self._configError = err.message || 'Failed to load widget configuration.';
1228
+ self._configState = 'error';
1229
+ self.render();
1230
+ });
1231
+
1232
+ return this._configPromise;
1233
+ }
1234
+
1235
+ _retryConfigFetch() {
1236
+ this._configState = 'idle';
1237
+ this._configError = null;
1238
+ this._configPromise = null;
1239
+ this._fetchRuntimeConfig();
1240
+ }
1241
+
1242
+ _configErrorHTML(type, title, body, hint) {
1243
+ 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>`;
1244
+ 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>`;
1245
+ 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>`;
1246
+ const icon = type === 'missing' ? lockIcon : warnIcon;
1247
+ const badgeLabel = type === 'missing'
1248
+ ? `<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`
1249
+ : 'Configuration Error';
1250
+ const retryBtn = type === 'fetch'
1251
+ ? `<button class="booking-widget-config-error__retry" onclick="window.bookingWidgetInstance && window.bookingWidgetInstance._retryConfigFetch()">${retryIcon} Try Again</button>`
1252
+ : '';
1253
+ return `<div class="booking-widget-config-error" role="alert">
1254
+ <div class="booking-widget-config-error__icon-wrap">${icon}</div>
1255
+ <span class="booking-widget-config-error__badge">${badgeLabel}</span>
1256
+ <h3 class="booking-widget-config-error__title">${escapeHTML(title)}</h3>
1257
+ <p class="booking-widget-config-error__desc">${body}</p>
1258
+ <div class="booking-widget-config-error__divider"></div>
1259
+ <p class="booking-widget-config-error__hint">${escapeHTML(hint)}</p>
1260
+ ${retryBtn}
1261
+ </div>`;
1097
1262
  }
1098
1263
 
1099
1264
  injectCSS() {
@@ -1118,16 +1283,7 @@ if (typeof window !== 'undefined') {
1118
1283
  }
1119
1284
 
1120
1285
  renderPropertyKeyMessage() {
1121
- const el = this.widget.querySelector('.booking-widget-property-key-message');
1122
- if (!el) return;
1123
- const key = this.options.propertyKey;
1124
- const missing = !key || (typeof key === 'string' && !key.trim());
1125
- if (missing) {
1126
- el.style.display = 'block';
1127
- el.innerHTML = 'The propertyKey is missing. Please provide it as a prop to load rooms from the API.';
1128
- } else {
1129
- el.style.display = 'none';
1130
- }
1286
+ // Config errors are rendered directly inside renderStepContent as full cards.
1131
1287
  }
1132
1288
 
1133
1289
  renderStepIndicator() {
@@ -1135,7 +1291,7 @@ if (typeof window !== 'undefined') {
1135
1291
  if (!el) return;
1136
1292
  const key = this.options.propertyKey;
1137
1293
  const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
1138
- if (!hasPropertyKey || this.state.step === 'confirmation') {
1294
+ if (!hasPropertyKey || this._configState !== 'loaded' || this.state.step === 'confirmation') {
1139
1295
  el.innerHTML = '';
1140
1296
  return;
1141
1297
  }
@@ -1158,9 +1314,36 @@ if (typeof window !== 'undefined') {
1158
1314
  renderStepContent() {
1159
1315
  const el = this.widget.querySelector('.booking-widget-step-content');
1160
1316
  if (!el) return;
1317
+
1318
+ // Missing propertyKey — show error card inside the modal, same as React/Vue.
1161
1319
  const key = this.options.propertyKey;
1162
- const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
1163
- if (!hasPropertyKey) {
1320
+ if (!key || !String(key).trim()) {
1321
+ el.innerHTML = this._configErrorHTML(
1322
+ 'missing',
1323
+ 'Widget Not Configured',
1324
+ 'A <code>propertyKey</code> option is required to initialize this booking widget. Please provide it to load rooms and availability.',
1325
+ 'Contact the site administrator to configure this widget.'
1326
+ );
1327
+ return;
1328
+ }
1329
+
1330
+ if (this._configState === 'loading') {
1331
+ el.innerHTML = `<div class="booking-widget-config-loading">
1332
+ <div class="booking-widget-config-loading__spinner"></div>
1333
+ <span class="booking-widget-config-loading__text">Loading configuration\u2026</span>
1334
+ </div>`;
1335
+ return;
1336
+ }
1337
+ if (this._configState === 'error') {
1338
+ el.innerHTML = this._configErrorHTML(
1339
+ 'fetch',
1340
+ 'Could Not Load Config',
1341
+ escapeHTML(this._configError || 'An unexpected error occurred.'),
1342
+ 'Please try again or contact support.'
1343
+ );
1344
+ return;
1345
+ }
1346
+ if (this._configState !== 'loaded') {
1164
1347
  el.innerHTML = '';
1165
1348
  return;
1166
1349
  }
@@ -81,6 +81,7 @@
81
81
  --bg: hsl(30, 10%, 8%);
82
82
  --fg: hsl(40, 20%, 90%);
83
83
  --card: hsl(30, 8%, 12%);
84
+ --card-solid: hsl(30, 8%, 12%);
84
85
  --card-fg: hsl(40, 20%, 90%);
85
86
  --primary: hsl(38, 60%, 55%);
86
87
  --primary-fg: hsl(30, 10%, 8%);
@@ -106,6 +107,145 @@
106
107
  font-weight: 500;
107
108
  }
108
109
 
110
+ /* ===== Config Error State ===== */
111
+ .booking-widget-config-error {
112
+ display: flex;
113
+ flex-direction: column;
114
+ align-items: center;
115
+ justify-content: center;
116
+ gap: 1.1em;
117
+ padding: 4em 2em;
118
+ text-align: center;
119
+ min-height: 300px;
120
+ }
121
+
122
+ .booking-widget-config-error__icon-wrap {
123
+ width: 5em;
124
+ height: 5em;
125
+ border-radius: 50%;
126
+ background: radial-gradient(circle, rgba(239,68,68,0.12) 0%, rgba(239,68,68,0.04) 100%);
127
+ border: 1.5px solid rgba(239, 68, 68, 0.22);
128
+ box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.06);
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ color: #f87171;
133
+ flex-shrink: 0;
134
+ margin-bottom: 0.25em;
135
+ }
136
+
137
+ .booking-widget-config-error__badge {
138
+ display: inline-flex;
139
+ align-items: center;
140
+ gap: 0.4em;
141
+ font-size: 0.7em;
142
+ font-weight: 600;
143
+ letter-spacing: 0.12em;
144
+ text-transform: uppercase;
145
+ color: #f87171;
146
+ background: rgba(239, 68, 68, 0.1);
147
+ border: 1px solid rgba(239, 68, 68, 0.18);
148
+ border-radius: 99em;
149
+ padding: 0.3em 0.85em;
150
+ }
151
+
152
+ .booking-widget-config-error__title {
153
+ font-family: var(--font-serif, 'Playfair Display', Georgia, serif);
154
+ font-size: 1.35em;
155
+ font-weight: 600;
156
+ color: var(--fg, #e8e0d5);
157
+ margin: 0;
158
+ letter-spacing: -0.01em;
159
+ }
160
+
161
+ .booking-widget-config-error__desc {
162
+ font-size: 0.875em;
163
+ color: var(--secondary-fg, #a09080);
164
+ max-width: 25em;
165
+ line-height: 1.7;
166
+ margin: 0;
167
+ }
168
+
169
+ .booking-widget-config-error__desc code {
170
+ font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace;
171
+ font-size: 0.88em;
172
+ background: rgba(255, 255, 255, 0.06);
173
+ color: var(--primary, #f59e0b);
174
+ padding: 0.12em 0.45em;
175
+ border-radius: 0.3em;
176
+ border: 1px solid rgba(255, 255, 255, 0.09);
177
+ }
178
+
179
+ .booking-widget-config-error__divider {
180
+ width: 2.5em;
181
+ height: 1.5px;
182
+ background: var(--border, rgba(255,255,255,0.1));
183
+ border-radius: 1px;
184
+ }
185
+
186
+ .booking-widget-config-error__hint {
187
+ font-size: 0.78em;
188
+ color: var(--muted, #6b5f50);
189
+ max-width: 21em;
190
+ line-height: 1.6;
191
+ margin: 0;
192
+ }
193
+
194
+ .booking-widget-config-error__retry {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 0.45em;
198
+ padding: 0.55em 1.4em;
199
+ background: transparent;
200
+ color: var(--secondary-fg, #a09080);
201
+ border: 1.5px solid var(--border, rgba(255,255,255,0.13));
202
+ border-radius: 99em;
203
+ font-size: 0.8em;
204
+ font-family: var(--font-sans, system-ui, sans-serif);
205
+ font-weight: 500;
206
+ cursor: pointer;
207
+ letter-spacing: 0.02em;
208
+ transition: border-color 0.2s, color 0.2s, background 0.2s;
209
+ margin-top: 0.25em;
210
+ }
211
+
212
+ .booking-widget-config-error__retry:hover {
213
+ border-color: var(--primary, #f59e0b);
214
+ color: var(--primary, #f59e0b);
215
+ background: rgba(245, 158, 11, 0.06);
216
+ }
217
+
218
+ /* ===== Config Loading State ===== */
219
+ .booking-widget-config-loading {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+ justify-content: center;
224
+ gap: 1.25em;
225
+ padding: 4em 2em;
226
+ text-align: center;
227
+ min-height: 300px;
228
+ }
229
+
230
+ .booking-widget-config-loading__spinner {
231
+ width: 2.75em;
232
+ height: 2.75em;
233
+ border: 2px solid var(--border, rgba(255,255,255,0.1));
234
+ border-top-color: var(--primary, hsl(38,60%,55%));
235
+ border-radius: 50%;
236
+ animation: bw-spin 0.75s linear infinite;
237
+ }
238
+
239
+ @keyframes bw-spin {
240
+ to { transform: rotate(360deg); }
241
+ }
242
+
243
+ .booking-widget-config-loading__text {
244
+ font-size: 0.875em;
245
+ color: var(--muted, #888);
246
+ letter-spacing: 0.01em;
247
+ }
248
+
109
249
  /* ===== Step Indicator ===== */
110
250
  .booking-widget-step-indicator {
111
251
  display: flex;
@@ -448,7 +588,7 @@
448
588
  }
449
589
 
450
590
  .booking-widget-modal .date-trigger:hover {
451
- border-color: hsl(38, 60%, 55%, 0.5);
591
+ border-color: var(--primary);
452
592
  }
453
593
 
454
594
  .booking-widget-modal .date-trigger .placeholder {
@@ -472,7 +612,7 @@
472
612
  max-width: calc(100vw - 2em);
473
613
  box-sizing: border-box;
474
614
  z-index: 10;
475
- background: var(--card);
615
+ background: var(--card-solid);
476
616
  border: 1px solid var(--border);
477
617
  border-radius: var(--radius);
478
618
  padding: 1em;