@v-office/website-chat 0.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,577 @@
1
+ import { r as MAX_MESSAGE_CHARS, t as CHAT_LOCALES } from "./protocol-BuNqZAMt.js";
2
+ import { c as chatAvailable, i as Spark, o as loadConversation, t as Arrow } from "./icons-B5PAY3aU.js";
3
+ import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/copy.ts
7
+ const DEFAULT_COPY = {
8
+ de: {
9
+ open: "Beschreiben Sie Ihre Reise",
10
+ resume: "Gespräch fortsetzen",
11
+ close: "Chat schließen",
12
+ title: "Reiseassistent",
13
+ newConversation: "Neues Gespräch",
14
+ notice: "Sie chatten mit einer KI. Preise und Verfügbarkeit in den Karten kommen direkt aus unserem Buchungssystem.",
15
+ greeting: "Hallo! Wann möchten Sie reisen, und mit wie vielen Personen?",
16
+ suggestions: [
17
+ "Ein Ferienhaus für 4 Personen in den Sommerferien",
18
+ "Eine Woche mit Hund, gern mit eingezäuntem Garten",
19
+ "Ein Wochenende zu zweit mit Sauna",
20
+ "Was ist in den Herbstferien noch frei?"
21
+ ],
22
+ placeholder: "Wann, mit wem, was ist Ihnen wichtig?",
23
+ send: "Senden",
24
+ thinking: "Die Antwort wird geschrieben",
25
+ yourMessage: "Ihre Nachricht",
26
+ consent: "Der Chat wird von einer KI beantwortet. Ihre Nachrichten werden dazu an unseren KI-Dienstleister übermittelt.",
27
+ consentAccept: "Chat starten",
28
+ unavailable: "Der Chat ist gerade nicht verfügbar.",
29
+ busy: "Einen Moment, die Antwort läuft noch.",
30
+ searchLink: "Unterkünfte suchen",
31
+ contactLink: "Kontakt aufnehmen",
32
+ bookStay: "Zur Buchung",
33
+ viewRental: "Zur Unterkunft",
34
+ total: "Gesamtpreis",
35
+ notBookable: "Für diesen Zeitraum nicht buchbar",
36
+ photo: "Foto",
37
+ previous: "Zurück",
38
+ next: "Weiter",
39
+ reviews: {
40
+ one: "{n} Bewertung",
41
+ other: "{n} Bewertungen"
42
+ },
43
+ priceBreakdown: "So setzt sich der Preis zusammen",
44
+ otherDates: "Andere freie Zeiträume",
45
+ free: "frei",
46
+ booked: "belegt",
47
+ closed: "nicht buchbar",
48
+ yourStay: "Ihr Zeitraum",
49
+ compareRooms: "Räume",
50
+ compareHighlights: "Highlights",
51
+ compareRating: "Bewertung",
52
+ compareStay: "Aufenthalt",
53
+ priceCalendar: "Preise rund um Ihren Termin",
54
+ priceCalendarNights: {
55
+ one: "Gesamtpreis für {n} Nacht",
56
+ other: "Gesamtpreis für {n} Nächte"
57
+ },
58
+ cheapest: "Am günstigsten",
59
+ reviewsTitle: "Was Gäste sagen",
60
+ matchesWishes: "Passt zu Ihren Wünschen",
61
+ mapLoad: "Karte laden",
62
+ mapNotice: "Beim Laden der Karte wird die IP-Adresse an OpenFreeMap übertragen.",
63
+ mapLabel: "Lage von {name}",
64
+ adults: {
65
+ one: "{n} Erwachsener",
66
+ other: "{n} Erwachsene"
67
+ },
68
+ children: {
69
+ one: "{n} Kind",
70
+ other: "{n} Kinder"
71
+ }
72
+ },
73
+ en: {
74
+ open: "Describe your trip",
75
+ resume: "Continue the conversation",
76
+ close: "Close chat",
77
+ title: "Travel assistant",
78
+ newConversation: "New conversation",
79
+ notice: "You are chatting with an AI. Prices and availability in the cards come straight from our booking system.",
80
+ greeting: "Hello! When would you like to travel, and how many of you?",
81
+ suggestions: [
82
+ "A holiday home for 4 in the summer holidays",
83
+ "A week with our dog, ideally with a fenced garden",
84
+ "A weekend for two with a sauna",
85
+ "What is still free in the autumn holidays?"
86
+ ],
87
+ placeholder: "When, who with, what matters to you?",
88
+ send: "Send",
89
+ thinking: "Writing the answer",
90
+ yourMessage: "Your message",
91
+ consent: "The chat is answered by an AI. Your messages are sent to our AI provider for that.",
92
+ consentAccept: "Start chat",
93
+ unavailable: "The chat is not available right now.",
94
+ busy: "One moment, the reply is still running.",
95
+ searchLink: "Search stays",
96
+ contactLink: "Contact us",
97
+ bookStay: "Book this stay",
98
+ viewRental: "View the rental",
99
+ total: "Total",
100
+ notBookable: "Not bookable for these dates",
101
+ photo: "Photo",
102
+ previous: "Previous",
103
+ next: "Next",
104
+ reviews: {
105
+ one: "{n} review",
106
+ other: "{n} reviews"
107
+ },
108
+ priceBreakdown: "How the price adds up",
109
+ otherDates: "Other free periods",
110
+ free: "free",
111
+ booked: "taken",
112
+ closed: "not bookable",
113
+ yourStay: "Your stay",
114
+ compareRooms: "Rooms",
115
+ compareHighlights: "Highlights",
116
+ compareRating: "Rating",
117
+ compareStay: "Stay",
118
+ priceCalendar: "Prices around your dates",
119
+ priceCalendarNights: {
120
+ one: "Total for {n} night",
121
+ other: "Total for {n} nights"
122
+ },
123
+ cheapest: "Cheapest",
124
+ reviewsTitle: "What guests say",
125
+ matchesWishes: "Matches your wishes",
126
+ mapLoad: "Load map",
127
+ mapNotice: "Loading the map sends the IP address to OpenFreeMap.",
128
+ mapLabel: "Location of {name}",
129
+ adults: {
130
+ one: "{n} adult",
131
+ other: "{n} adults"
132
+ },
133
+ children: {
134
+ one: "{n} child",
135
+ other: "{n} children"
136
+ }
137
+ }
138
+ };
139
+ //#endregion
140
+ //#region src/theme.ts
141
+ /** The curated looks over one layout: `classic` (soft shadows, medium corners), `soft` (round corners,
142
+ * lighter shadows), `minimal` (borders instead of shadows, small corners). */
143
+ const CHAT_THEMES = [
144
+ "classic",
145
+ "soft",
146
+ "minimal"
147
+ ];
148
+ /** See {@link ColorVariables}. */
149
+ const colorVariables = (colors) => ({
150
+ ...colors?.primary && { "--chat-primary": colors.primary },
151
+ ...colors?.secondary && { "--chat-secondary": colors.secondary }
152
+ });
153
+ /** The widget's stylesheet, inserted into its shadow root. */
154
+ const CHAT_CSS = `
155
+ :host {
156
+ /* Important: a page's own rules on the host element (\`div\`, \`*\`) beat a shadow root's normal
157
+ \`:host\` rules, and only important ones reverse that. */
158
+ all: initial !important;
159
+ font-family: inherit !important;
160
+ --chat-primary: #1f4f8b;
161
+ --chat-on-primary: #fff;
162
+ --chat-secondary: #eaf0f7;
163
+ --chat-surface: #fff;
164
+ --chat-surface-soft: #f5f7fa;
165
+ --chat-text: #1b1f24;
166
+ --chat-muted: #5b6470;
167
+ --chat-border: #d9dee5;
168
+ --chat-radius: 16px;
169
+ --chat-shadow: 0 18px 48px rgb(15 23 42 / 0.18);
170
+ --chat-card-shadow: 0 1px 3px rgb(15 23 42 / 0.08), 0 8px 24px rgb(15 23 42 / 0.06);
171
+ /* Not set here, so a site's value reaches the widget: \`--chat-top\` keeps a sticky header uncovered,
172
+ \`--chat-width\` widens the panel on wide screens. */
173
+ }
174
+ * { box-sizing: border-box; }
175
+ .chat { color: var(--chat-text); font-size: 15px; line-height: 1.45; }
176
+
177
+ /* The entry pill: frosted glass over any page, the chat's mark, a ring running round it until the
178
+ visitor has asked something. Phones tap it; wide screens type into it. */
179
+ .entry { position: fixed; right: 20px; bottom: 20px; z-index: 2147483000; }
180
+ [data-position="left"] .entry { right: auto; left: 20px; }
181
+ .pill { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 12px;
182
+ padding: 8px 20px 8px 8px; border-radius: 999px; border: 1px solid rgb(255 255 255 / 0.7);
183
+ background: rgb(255 255 255 / 0.86); backdrop-filter: blur(16px) saturate(1.4);
184
+ -webkit-backdrop-filter: blur(16px) saturate(1.4); box-shadow: var(--chat-shadow);
185
+ color: var(--chat-text); font: inherit; text-align: left; cursor: pointer; }
186
+ .spark { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 999px;
187
+ background: var(--chat-primary); color: var(--chat-on-primary); }
188
+ .pill-text { display: flex; flex-direction: column; min-width: 0; flex: 1; line-height: 1.25; }
189
+ .pill-title { font-size: 12px; font-weight: 600; color: var(--chat-primary); }
190
+ .pill-prompt { font-size: 14px; font-weight: 600; }
191
+ .launcher-field { display: none; }
192
+ .launcher-field input { border: 0; outline: none; background: transparent; padding: 0; margin: 0;
193
+ font: inherit; font-size: 14px; font-weight: 600; color: var(--chat-text); width: 100%; }
194
+ .launcher-field input::placeholder { color: var(--chat-text); opacity: 1; }
195
+ .launcher-field:focus-within input::placeholder { color: var(--chat-muted); }
196
+ /* The field has no outline of its own: the pill shows keyboard focus, typed text or not. */
197
+ .launcher-field:focus-within { outline: 2px solid var(--chat-primary); outline-offset: 2px; }
198
+ .glow { position: absolute; inset: -3px; border-radius: 999px; overflow: hidden; padding: 3px;
199
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
200
+ -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) content-box exclude,
201
+ linear-gradient(#000 0 0); }
202
+ .glow::before { content: ""; position: absolute; inset: -150%;
203
+ background: conic-gradient(from 0deg, transparent 0 62%, var(--chat-primary) 82%, transparent 100%); }
204
+ @media (min-width: 1024px) {
205
+ .launcher[data-narrow] { display: none; }
206
+ .launcher-field { display: flex; width: 26rem; padding: 8px; cursor: text; }
207
+ }
208
+
209
+ /* The panel: over the dimmed page, a card at the screen's edge; on a phone a sheet from the bottom. */
210
+ .scrim { position: fixed; left: 0; right: 0; bottom: 0; top: var(--chat-top, 0px); z-index: 2147482999;
211
+ background: rgb(15 23 42 / 0.45); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
212
+ .panel { position: fixed; z-index: 2147483000; left: 0; right: 0; bottom: 0;
213
+ top: calc(var(--chat-top, 0px) + 12px); display: flex; flex-direction: column;
214
+ background: var(--chat-surface); border-radius: var(--chat-radius) var(--chat-radius) 0 0;
215
+ box-shadow: var(--chat-shadow); overflow: hidden; }
216
+ .scrim[hidden], .panel[hidden] { display: none; }
217
+ @media (min-width: 1024px) {
218
+ .panel { left: auto; right: 20px; bottom: 20px; top: calc(var(--chat-top, 0px) + 20px);
219
+ width: min(var(--chat-width, 600px), calc(100vw - 40px)); border-radius: var(--chat-radius); }
220
+ [data-position="left"] .panel { right: auto; left: 20px; }
221
+ }
222
+ .header { display: flex; align-items: center; gap: 8px; padding: 16px 20px 6px; }
223
+ .title { margin: 0 auto 0 0; font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
224
+ text-transform: uppercase; color: var(--chat-primary); }
225
+ .icon-button { flex: none; display: grid; place-items: center; width: 36px; height: 36px; padding: 0;
226
+ border: 1px solid var(--chat-border); border-radius: 999px; background: var(--chat-surface);
227
+ color: var(--chat-text); cursor: pointer; }
228
+ .icon-button:hover { border-color: var(--chat-primary); }
229
+ .notice { margin: 0; padding: 0 20px 12px; font-size: 12px; color: var(--chat-muted);
230
+ border-bottom: 1px solid var(--chat-border); }
231
+ .messages { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 20px 20px 36px;
232
+ display: flex; flex-direction: column; gap: 16px; background: var(--chat-surface-soft);
233
+ container-type: inline-size; }
234
+ .turn { display: flex; flex-direction: column; }
235
+ .message { max-width: 88%; white-space: pre-wrap; overflow-wrap: anywhere; }
236
+ .message[data-role="visitor"] { align-self: flex-end; margin-left: 48px; padding: 10px 14px;
237
+ border-radius: var(--chat-radius); background: var(--chat-secondary); }
238
+ .message[data-role="assistant"] { align-self: flex-start; max-width: 100%; }
239
+ .ui { align-self: stretch; display: flex; flex-direction: column; gap: 12px; }
240
+ .thinking { display: flex; gap: 5px; padding: 4px 2px; }
241
+ .thinking i { width: 7px; height: 7px; border-radius: 999px; background: var(--chat-primary); opacity: 0.5; }
242
+ .dock { position: relative; padding: 10px 20px 16px; background: var(--chat-surface); }
243
+ .dock::before { content: ""; position: absolute; left: 0; right: 0; top: -28px; height: 28px;
244
+ pointer-events: none; background: linear-gradient(to top, var(--chat-surface-soft), transparent); }
245
+ .suggestions { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; margin: 0 -20px 10px;
246
+ padding: 0 20px; }
247
+ .suggestions::-webkit-scrollbar { display: none; }
248
+ .suggestion { flex: none; max-width: 100%; border: 1px solid transparent; border-radius: 20px;
249
+ background: var(--chat-secondary); color: var(--chat-text); padding: 8px 14px; font: inherit;
250
+ font-size: 13px; font-weight: 500; line-height: 1.35; text-align: left; white-space: nowrap;
251
+ cursor: pointer; }
252
+ .suggestion:hover { border-color: var(--chat-primary); background: var(--chat-surface); }
253
+ @media (min-width: 1024px) {
254
+ .suggestions { flex-wrap: wrap; overflow: visible; margin: 0 0 10px; padding: 0; }
255
+ .suggestion { white-space: normal; }
256
+ }
257
+ .composer { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 18px;
258
+ border: 1px solid var(--chat-border); border-radius: 24px; background: var(--chat-surface);
259
+ box-shadow: 0 4px 16px rgb(15 23 42 / 0.08); }
260
+ .composer:focus-within { border-color: var(--chat-text); }
261
+ /* 16px keeps iOS from zooming into the field. */
262
+ .composer textarea { flex: 1; min-width: 0; min-height: 36px; max-height: 128px; resize: none; border: 0;
263
+ outline: none; background: transparent; padding: 6px 0; font: inherit; font-size: 16px;
264
+ line-height: 1.5; color: inherit; field-sizing: content; }
265
+ .send { flex: none; display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0;
266
+ border-radius: 999px; background: var(--chat-primary); color: var(--chat-on-primary); cursor: pointer; }
267
+ .send:disabled { opacity: 0.3; cursor: not-allowed; }
268
+ .consent { flex: 1; overflow-y: auto; padding: 24px 20px; display: flex; flex-direction: column; gap: 14px;
269
+ background: var(--chat-surface-soft); }
270
+ .consent p { margin: 0; }
271
+ .pending { margin: 0; align-self: flex-end; max-width: 88%; padding: 10px 14px;
272
+ border-radius: var(--chat-radius); background: var(--chat-secondary); white-space: pre-wrap; }
273
+ .pending figcaption { margin-bottom: 2px; }
274
+ .fallback { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 10px; }
275
+ .fallback p { margin: 0; }
276
+
277
+ /* What the replies show. */
278
+ .card { border-radius: var(--chat-radius); overflow: hidden; background: var(--chat-surface);
279
+ box-shadow: var(--chat-card-shadow); }
280
+ .gallery-wrap, .carousel-wrap { position: relative; }
281
+ .gallery, .carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
282
+ .gallery::-webkit-scrollbar, .carousel::-webkit-scrollbar { display: none; }
283
+ .gallery img { flex: 0 0 100%; scroll-snap-align: start; display: block; width: 100%; aspect-ratio: 5 / 3;
284
+ height: auto; object-fit: cover; }
285
+ .gallery-wrap .snap-nav { position: absolute; left: 8px; right: 8px; bottom: 8px; }
286
+ .gallery-wrap .snap-nav .muted { background: rgb(0 0 0 / 0.5); color: #fff; padding: 1px 7px;
287
+ border-radius: 999px; font-size: 11px; }
288
+ .carousel { gap: 12px; padding: 2px 2px 6px; }
289
+ .carousel > .card { flex: 0 0 86%; scroll-snap-align: start; }
290
+ /* Room for two: results side by side, all of them, instead of a row to swipe. */
291
+ @container (min-width: 520px) {
292
+ .carousel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); overflow: visible; }
293
+ .carousel-wrap > .snap-nav { display: none; }
294
+ }
295
+ .snap-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
296
+ .snap-nav button, .calendar-head button { border: 1px solid var(--chat-border); background: var(--chat-surface);
297
+ color: var(--chat-text); border-radius: 999px; width: 28px; height: 28px; cursor: pointer; font: inherit;
298
+ line-height: 1; }
299
+ .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.35; cursor: default; }
300
+ .rating { color: #b7791f; font-weight: 700; }
301
+ .breakdown .line { display: flex; justify-content: space-between; gap: 12px; }
302
+ .breakdown .total { border-top: 1px solid var(--chat-border); padding-top: 6px; margin-top: 2px;
303
+ font-weight: 700; }
304
+ .alternatives, .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
305
+ .chip-link { border: 1px solid var(--chat-primary); color: var(--chat-primary); border-radius: 999px;
306
+ padding: 5px 10px; text-decoration: none; font-size: 13px; }
307
+ .chip { border: 1px solid transparent; background: var(--chat-secondary); color: var(--chat-text);
308
+ border-radius: 999px; padding: 7px 13px; font: inherit; font-size: 13px; cursor: pointer; }
309
+ .chip:hover { border-color: var(--chat-primary); background: var(--chat-surface); }
310
+ .calendar { border: 1px solid var(--chat-border); border-radius: var(--chat-radius); padding: 12px;
311
+ background: var(--chat-surface); }
312
+ .calendar-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
313
+ font-weight: 700; margin-bottom: 8px; text-align: center; }
314
+ .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; text-align: center;
315
+ font-size: 12px; }
316
+ .weekday { color: var(--chat-muted); font-weight: 600; }
317
+ .day { padding: 4px 0; border-radius: 6px; }
318
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
319
+ white-space: nowrap; }
320
+ .day[data-status="free"], .legend i[data-status="free"] { background: #e3f1e7; color: #1f5133; }
321
+ .day[data-status="booked"], .legend i[data-status="booked"] { background: #f1f2f4; color: #9aa1a9;
322
+ text-decoration: line-through; }
323
+ .day[data-status="closed"] { color: #b8bec6; }
324
+ .day[data-stay], .legend i[data-stay] { background: var(--chat-primary); color: var(--chat-on-primary);
325
+ text-decoration: none; }
326
+ .legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; }
327
+ .legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 4px;
328
+ vertical-align: -1px; }
329
+ .compare { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
330
+ .compare-col { border-radius: var(--chat-radius); padding: 10px; background: var(--chat-surface);
331
+ box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 13px;
332
+ min-width: 0; }
333
+ /* Three columns leave a button about 90px: "Zur Buchung" fits on one line only without side padding. */
334
+ .compare-col .button { padding: 8px 2px; font-size: 13px; }
335
+ .compare-col dl { margin: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
336
+ .compare-col dt { color: var(--chat-muted); font-size: 11px; margin-top: 4px; }
337
+ .compare-col dd { margin: 0; overflow-wrap: anywhere; }
338
+ .card-body { padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 4px; }
339
+ .card-title { margin: 0; font-size: 15px; font-weight: 700; }
340
+ .muted { color: var(--chat-muted); font-size: 13px; }
341
+ .price { font-weight: 700; }
342
+ .button { display: inline-block; margin-top: 6px; text-align: center; text-decoration: none;
343
+ padding: 10px 16px; border-radius: 999px; background: var(--chat-primary); color: var(--chat-on-primary);
344
+ border: 0; cursor: pointer; font: inherit; font-weight: 600; }
345
+ .link { color: var(--chat-primary); }
346
+ /* A link out of the chat while a reply still streams (ReplyLink). */
347
+ [aria-disabled="true"] { opacity: 0.5; cursor: progress; }
348
+
349
+ /* Wishes a found stay matches: a badge on the photo, the wishes ticked under the name. */
350
+ .card-media { position: relative; }
351
+ .badge { position: absolute; left: 10px; top: 10px; z-index: 1; padding: 4px 10px; border-radius: 999px;
352
+ background: var(--chat-surface); color: var(--chat-primary); font-size: 12px; font-weight: 600;
353
+ box-shadow: var(--chat-card-shadow); }
354
+ .wishes { list-style: none; margin: 2px 0; padding: 0; display: flex; flex-wrap: wrap; gap: 2px 10px;
355
+ font-size: 13px; }
356
+ .wishes li::before { content: "✓ "; color: var(--chat-primary); font-weight: 700; }
357
+
358
+ /* The price calendar: a column per arrival, the bar's height its total. */
359
+ .price-columns { list-style: none; margin: 10px 0 0; padding: 0; display: grid; grid-auto-flow: column;
360
+ grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto; scrollbar-width: none; }
361
+ .price-column { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 4px;
362
+ padding: 6px 2px; border: 1px solid transparent; border-radius: 10px; background: transparent;
363
+ color: inherit; font: inherit; cursor: pointer; }
364
+ .price-column:disabled { cursor: default; }
365
+ .price-column[data-free]:not(:disabled):hover { border-color: var(--chat-primary); }
366
+ .price-column[data-asked] { border-color: var(--chat-border); background: var(--chat-surface-soft); }
367
+ .bar-track { display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 72px; }
368
+ .bar { width: 60%; border-radius: 6px 6px 2px 2px; background: var(--chat-secondary); }
369
+ .price-column[data-cheapest] .bar, .legend i[data-cheapest] { background: var(--chat-primary); }
370
+ .legend i[data-asked] { background: var(--chat-surface-soft); border: 1px solid var(--chat-border); }
371
+ .price-text { font-size: 12px; font-weight: 700; white-space: nowrap; }
372
+ .price-column:not([data-free]) .price-text { color: var(--chat-muted); font-weight: 500; }
373
+
374
+ /* Reviews: the PMS's score and grade, its categories, a few texts. */
375
+ .score { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
376
+ .score-value { display: grid; place-items: center; min-width: 48px; height: 48px; padding: 0 8px;
377
+ border-radius: 12px; background: var(--chat-primary); color: var(--chat-on-primary); font-size: 18px;
378
+ font-weight: 700; }
379
+ .review-categories { margin: 8px 0 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
380
+ gap: 4px 16px; font-size: 13px; }
381
+ .review-categories div { display: flex; justify-content: space-between; gap: 8px; }
382
+ .review-categories dt { color: var(--chat-muted); }
383
+ .review-categories dd { margin: 0; font-weight: 600; }
384
+ .excerpt { margin: 8px 0 0; padding: 6px 12px; border-left: 3px solid var(--chat-secondary); }
385
+ .excerpt p { margin: 0; }
386
+
387
+ /* A rental's facts, in the groups the PMS lists them in. */
388
+ .fact-group h4 { margin: 8px 0 4px; font-size: 13px; color: var(--chat-muted); }
389
+ .fact-group ul { margin: 0; padding-left: 18px; columns: 2; column-gap: 20px; font-size: 14px; }
390
+ @container (max-width: 380px) { .fact-group ul { columns: 1; } }
391
+
392
+ /* The map, loaded on a tap. MapLibre's own stylesheet cannot reach the shadow root: these are the
393
+ layout rules of it that a map with one marker and no controls needs. */
394
+ .map { position: relative; height: 220px; }
395
+ .map-gate { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
396
+ min-height: 150px; padding: 16px; text-align: center; background: var(--chat-surface-soft); }
397
+ .map-gate p { margin: 0; }
398
+ .map-attribution { margin: 0; padding: 2px 8px; font-size: 10px; color: var(--chat-muted); text-align: right; }
399
+ .map-attribution a { color: inherit; }
400
+ .map-pin { display: block; width: 18px; height: 18px; border-radius: 999px; background: var(--chat-primary);
401
+ border: 3px solid #fff; box-shadow: 0 2px 6px rgb(0 0 0 / 0.35); }
402
+ .maplibregl-map { position: relative; overflow: hidden; }
403
+ .maplibregl-canvas-container, .maplibregl-canvas { position: absolute; left: 0; top: 0; }
404
+ /* The canvas takes keyboard focus: a ring for the keyboard, none for a click or a tap. */
405
+ .maplibregl-canvas:focus:not(:focus-visible) { outline: none; }
406
+ .maplibregl-canvas:focus-visible { outline: 2px solid var(--chat-primary); outline-offset: -2px; }
407
+ .maplibregl-marker { position: absolute; top: 0; left: 0; will-change: transform; }
408
+
409
+ [data-theme="soft"] { --chat-radius: 24px; --chat-shadow: 0 12px 32px rgb(15 23 42 / 0.12);
410
+ --chat-card-shadow: 0 4px 18px rgb(15 23 42 / 0.07); }
411
+ [data-theme="minimal"] { --chat-radius: 6px; --chat-shadow: none; --chat-card-shadow: none; }
412
+ [data-theme="minimal"] .panel, [data-theme="minimal"] .pill, [data-theme="minimal"] .card,
413
+ [data-theme="minimal"] .compare-col { border: 1px solid var(--chat-border); }
414
+ [data-theme="minimal"] .pill, [data-theme="minimal"] .composer, [data-theme="minimal"] .suggestion,
415
+ [data-theme="minimal"] .chip { border-radius: var(--chat-radius); }
416
+
417
+ @keyframes chat-spin { to { transform: rotate(1turn); } }
418
+ @keyframes chat-in { from { opacity: 0; transform: translateY(16px); } }
419
+ @keyframes chat-fade { from { opacity: 0; } }
420
+ @keyframes chat-pulse { 50% { opacity: 1; transform: translateY(-2px); } }
421
+ @media (prefers-reduced-motion: no-preference) {
422
+ .glow::before { animation: chat-spin 3.2s linear infinite; }
423
+ .entry { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
424
+ .pill { transition: transform 200ms ease-out; }
425
+ .pill:hover { transform: translateY(-2px); }
426
+ .panel { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
427
+ .scrim { animation: chat-fade 300ms ease-out; }
428
+ .thinking i { animation: chat-pulse 1.1s ease-in-out infinite; }
429
+ .thinking i:nth-child(2) { animation-delay: 160ms; }
430
+ .thinking i:nth-child(3) { animation-delay: 320ms; }
431
+ }
432
+ /* Without motion the ring stands still, in the primary colour all round. */
433
+ @media (prefers-reduced-motion: reduce) {
434
+ .glow::before { background: var(--chat-primary); opacity: 0.5; }
435
+ }
436
+ `;
437
+ //#endregion
438
+ //#region src/WebsiteChat.tsx
439
+ const Panel = lazy(() => import("./panel-C0Bb7ln-.js"));
440
+ /** See {@link WebsiteChatComponent}. */
441
+ const WebsiteChat = function WebsiteChat(props) {
442
+ const { endpoint, locale, theme = "classic", position = "right" } = props;
443
+ const host = useRef(null);
444
+ const launcher = useRef(null);
445
+ const field = useRef(null);
446
+ const wasOpen = useRef(false);
447
+ const [root, setRoot] = useState(null);
448
+ const [available, setAvailable] = useState(false);
449
+ const [mounted, setMounted] = useState(false);
450
+ const [open, setOpen] = useState(false);
451
+ const [started, setStarted] = useState(false);
452
+ const [pending, setPending] = useState(null);
453
+ const [entry, setEntry] = useState("");
454
+ const copy = useMemo(() => ({
455
+ ...DEFAULT_COPY[locale],
456
+ ...props.copy
457
+ }), [locale, props.copy]);
458
+ const close = useCallback(() => setOpen(false), []);
459
+ const sent = useCallback(() => setPending(null), []);
460
+ const show = () => {
461
+ setMounted(true);
462
+ setOpen(true);
463
+ };
464
+ useEffect(() => {
465
+ const controller = new AbortController();
466
+ setAvailable(false);
467
+ setOpen(false);
468
+ setMounted(false);
469
+ setPending(null);
470
+ setEntry("");
471
+ setStarted(loadConversation(endpoint) !== null);
472
+ chatAvailable(endpoint, controller.signal).then(setAvailable);
473
+ return () => controller.abort();
474
+ }, [endpoint]);
475
+ useEffect(() => {
476
+ if (wasOpen.current && !open) {
477
+ const button = launcher.current;
478
+ if (button?.checkVisibility?.() === false) field.current?.focus();
479
+ else button?.focus();
480
+ }
481
+ wasOpen.current = open;
482
+ }, [open]);
483
+ useEffect(() => {
484
+ const element = host.current;
485
+ if (!available || !element) return;
486
+ setRoot(element.shadowRoot ?? element.attachShadow({ mode: "open" }));
487
+ }, [available]);
488
+ const submitEntry = (event) => {
489
+ event.preventDefault();
490
+ const message = entry.trim();
491
+ if (message) setPending(message.slice(0, MAX_MESSAGE_CHARS));
492
+ setEntry("");
493
+ show();
494
+ };
495
+ return /* @__PURE__ */ jsx("div", {
496
+ ref: host,
497
+ style: colorVariables(props.colors),
498
+ children: root && available && createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: CHAT_CSS }), /* @__PURE__ */ jsxs("div", {
499
+ className: "chat",
500
+ "data-theme": theme,
501
+ "data-position": position,
502
+ children: [mounted && /* @__PURE__ */ jsx(Suspense, {
503
+ fallback: null,
504
+ children: /* @__PURE__ */ jsx(Panel, {
505
+ endpoint,
506
+ clientVersion: "1.0.1",
507
+ locale,
508
+ copy,
509
+ searchPath: props.searchPath,
510
+ contactPath: props.contactPath,
511
+ open,
512
+ pending,
513
+ onSent: sent,
514
+ onStarted: setStarted,
515
+ onClose: close
516
+ }, endpoint)
517
+ }), !open && /* @__PURE__ */ jsxs("div", {
518
+ className: "entry",
519
+ "data-glow": started ? void 0 : "",
520
+ children: [
521
+ !started && /* @__PURE__ */ jsx("span", {
522
+ className: "glow",
523
+ "aria-hidden": "true"
524
+ }),
525
+ /* @__PURE__ */ jsxs("button", {
526
+ ref: launcher,
527
+ type: "button",
528
+ className: "launcher pill",
529
+ "data-narrow": started ? void 0 : "",
530
+ onClick: show,
531
+ children: [/* @__PURE__ */ jsx(Spark, {}), started ? /* @__PURE__ */ jsx("span", {
532
+ className: "pill-prompt",
533
+ children: copy.resume
534
+ }) : /* @__PURE__ */ jsxs("span", {
535
+ className: "pill-text",
536
+ children: [/* @__PURE__ */ jsx("span", {
537
+ className: "pill-title",
538
+ children: copy.title
539
+ }), /* @__PURE__ */ jsx("span", {
540
+ className: "pill-prompt",
541
+ children: copy.open
542
+ })]
543
+ })]
544
+ }),
545
+ !started && /* @__PURE__ */ jsxs("form", {
546
+ className: "launcher-field pill",
547
+ onSubmit: submitEntry,
548
+ children: [
549
+ /* @__PURE__ */ jsx(Spark, {}),
550
+ /* @__PURE__ */ jsxs("label", {
551
+ className: "pill-text",
552
+ children: [/* @__PURE__ */ jsx("span", {
553
+ className: "pill-title",
554
+ children: copy.title
555
+ }), /* @__PURE__ */ jsx("input", {
556
+ ref: field,
557
+ value: entry,
558
+ maxLength: 2e3,
559
+ placeholder: copy.open,
560
+ onChange: (event) => setEntry(event.target.value)
561
+ })]
562
+ }),
563
+ /* @__PURE__ */ jsx("button", {
564
+ type: "submit",
565
+ className: "send",
566
+ "aria-label": copy.send,
567
+ children: /* @__PURE__ */ jsx(Arrow, {})
568
+ })
569
+ ]
570
+ })
571
+ ]
572
+ })]
573
+ })] }), root)
574
+ });
575
+ };
576
+ //#endregion
577
+ export { CHAT_LOCALES, CHAT_THEMES, DEFAULT_COPY, WebsiteChat };