@v-office/website-chat 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,8 +41,8 @@ It takes its colours and corners from the site's shadcn tokens (`--primary`,
41
41
  are square (`--radius: 0`) gets square buttons too.
42
42
 
43
43
  A site with a sticky header sets `--chat-top` (its height) on any element around the widget, such as
44
- `:root`: the dimmed page and the panel start below it. `--chat-width` widens the panel on wide screens
45
- (default `700px`).
44
+ `:root`: from 1024 px wide, the dimmed page and the panel start below it; narrower, the chat opens as a
45
+ full-height sheet over the header. `--chat-width` widens the panel on wide screens (default `700px`).
46
46
 
47
47
  The service places these components in replies: `RentalCard` (photo gallery, rating, the wishes the
48
48
  search that found it applied), `RentalCarousel`, `Compare`, `StayQuote`, `PriceBreakdown`,
@@ -28,67 +28,67 @@ const chatCatalog = defineCatalog(schema, {
28
28
  },
29
29
  RentalCarousel: {
30
30
  props: z.object({ items: z.array(offer).min(2).max(8) }),
31
- description: "Several rentals side by side, swipeable, each as a RentalCard. Use it for search results instead of several RentalCards."
31
+ description: "Several rentals side by side, swipeable, each as a RentalCard: search results, instead of several RentalCards. After a flexible search, each rental with its first period as its stayId."
32
32
  },
33
33
  StayQuote: {
34
34
  props: z.object({ stayId: id }),
35
- description: "One priced stay: the rental, the dates, the party, the total and a booking button."
35
+ description: "One priced stay: the rental, the dates, the party, the total and a booking button. Show it whenever the visitor asks what one stay costs, with the stayId from quote_stay."
36
36
  },
37
37
  PriceBreakdown: {
38
38
  props: z.object({ stayId: id }),
39
- description: "What a quoted stay's total is made of (nights, cleaning, pets, …), line by line. Only for a stay from quote_stay."
39
+ description: "What a stay's total is made of (nights, cleaning, pets, …), line by line, when the visitor asks. Only for a stay from quote_stay or configure_stay."
40
40
  },
41
41
  AlternativeStays: {
42
42
  props: z.object({
43
43
  rentalId: id,
44
44
  stayIds: z.array(id).min(1).max(6)
45
45
  }),
46
- description: "Free periods of one rental as tappable choices with their prices: other dates when the asked ones are taken, or the periods a flexible search found."
46
+ description: "Other free periods of one rental as tappable choices with their prices, right after that rental's card: the rental the reply is about, the one taken on the asked dates, or a rental's further periods after a flexible search; never the stay its card shows. For a rental the visitor named, its RentalCard comes first (stayId null when taken). For several taken rentals use a RentalCarousel with each rental's first free period as its stayId, and say the dates differ. At most one AlternativeStays per reply."
47
47
  },
48
48
  AvailabilityCalendar: {
49
49
  props: z.object({
50
50
  rentalId: id,
51
51
  stayId: id.nullable()
52
52
  }),
53
- description: "A rental's month calendar of free and taken days, with a stay's dates marked when there is one. Only after get_availability."
53
+ description: "A rental's month calendar of free and taken days, with a stay's dates marked when there is one. Show it whenever the visitor asks when a rental is free, after get_availability. Read-only: the visitor cannot pick days in it."
54
54
  },
55
55
  PriceCalendar: {
56
56
  props: z.object({ rentalId: id }),
57
- description: "One rental's total for the same length of stay at the arrivals around the asked one, cheapest marked; tapping one asks for it. Only after price_calendar."
57
+ description: "One rental's total for the same length of stay at the arrivals around the asked one; tapping one asks for it. For other or cheaper arrival dates, only after price_calendar. Not what one stay costs: that is a StayQuote."
58
58
  },
59
59
  ReviewSummary: {
60
60
  props: z.object({ rentalId: id }),
61
- description: "A rental's guest rating as the booking system sums it up, its category ratings and a few review texts. Only after get_reviews."
61
+ description: "A rental's guest rating as the booking system sums it up, its category ratings and a few review texts, when the visitor asks how guests rate it. Only after get_reviews; sum up in a sentence, in your own words, what guests value."
62
62
  },
63
63
  RentalFacts: {
64
64
  props: z.object({
65
65
  rentalId: id,
66
66
  groups: z.array(z.string().trim().min(1).max(80)).min(1).max(4)
67
67
  }),
68
- description: "The facts of one rental in the groups get_rental listed (by their label, such as Haustiere or Lage), for a question about pets, the kitchen or the surroundings."
68
+ description: "The facts of one rental in the groups get_rental listed, by their label. Show it whenever the visitor asks what one rental has or allows (pets, kitchen, parking, surroundings), after get_rental; answer the question in your text as well."
69
69
  },
70
70
  RentalMap: {
71
71
  props: z.object({ rentalId: id }),
72
- description: "Where one rental is: a map the visitor loads with a tap, and a link to its page."
72
+ description: "Where one rental is, when the visitor asks: a map the visitor loads with a tap, and a link to its page."
73
73
  },
74
74
  Compare: {
75
75
  props: z.object({ items: z.array(offer).min(2).max(3) }),
76
- description: "Two or three rentals side by side: rating, rooms, highlights, the stay and its price."
76
+ description: "Two or three rentals side by side: rating, rooms, highlights, the stay and its price. Show it whenever the visitor weighs or compares two or three rentals, with stays of the same dates; a rental taken then gets stayId null."
77
77
  },
78
78
  StayLink: {
79
79
  props: z.object({
80
80
  rentalId: id,
81
81
  stayId: id.nullable()
82
82
  }),
83
- description: "A button to the rental's page on this website, with the stay filled in, to book there."
83
+ description: "A button to the rental's page on this website, with the stay filled in, to book there. Only for a rental or stay shown without one: cards, carousels, comparisons and quotes carry their own booking buttons."
84
84
  },
85
85
  ContactLink: {
86
86
  props: z.object({}),
87
- description: "A link to this website's contact page."
87
+ description: "A link to this website's contact page. Show it whenever you send the visitor to the agency; never only offer it."
88
88
  },
89
89
  QuickReplies: {
90
90
  props: z.object({ options: z.array(z.string().trim().min(1).max(40)).min(1).max(4) }),
91
- description: "Up to four short answers the visitor can tap to send as their next message (\"Nur mit Sauna\", \"Andere Termine\"). Never a fact or a price."
91
+ description: "Up to four short answers the visitor can tap, in the language of your reply (a date, a party size, a wish): with every question you ask. Never a fact or a price."
92
92
  }
93
93
  },
94
94
  actions: {}
package/dist/catalog.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { i as SESSION_TTL_S, n as CHAT_PATH, r as MAX_MESSAGE_CHARS, t as CHAT_LOCALES } from "./protocol-BuNqZAMt.js";
2
- import { a as componentsFor, c as rendersBold, i as chatSpec, l as stateKey, n as PLACEABLE_COMPONENTS, o as handsOverStays, r as chatCatalog, s as parseItem, t as COMPONENT_SINCE, u as statePath } from "./catalog-y3xQ2jXW.js";
2
+ import { a as componentsFor, c as rendersBold, i as chatSpec, l as stateKey, n as PLACEABLE_COMPONENTS, o as handsOverStays, r as chatCatalog, s as parseItem, t as COMPONENT_SINCE, u as statePath } from "./catalog-ClcCUjf7.js";
3
3
  export { CHAT_LOCALES, CHAT_PATH, COMPONENT_SINCE, MAX_MESSAGE_CHARS, PLACEABLE_COMPONENTS, SESSION_TTL_S, chatCatalog, chatSpec, componentsFor, handsOverStays, parseItem, rendersBold, stateKey, statePath };
@@ -171,7 +171,7 @@ const TOPICS = [
171
171
  },
172
172
  {
173
173
  topic: "people",
174
- words: /famili|kinder|zu zweit|personen|erwachsene|gruppe|family|kids|people|adults|couple/
174
+ words: /famili|kinder|zu zweit|personen|erwachsene|gruppe|family|kids|people|adults|couple|for two(?! weeks| nights)/
175
175
  },
176
176
  {
177
177
  topic: "date",
package/dist/index.d.ts CHANGED
@@ -45,6 +45,8 @@ interface ChatCopy {
45
45
  readonly photo: string;
46
46
  readonly previous: string;
47
47
  readonly next: string;
48
+ /** A carousel of found rentals, for a screen reader. */
49
+ readonly rentalsFound: string;
48
50
  /** `{n}` reviews, per plural form. */
49
51
  readonly reviews: Readonly<Partial<Record<Intl.LDMLPluralRule, string>>> & {
50
52
  readonly other: string;
@@ -82,6 +84,10 @@ interface ChatCopy {
82
84
  readonly children: Readonly<Partial<Record<Intl.LDMLPluralRule, string>>> & {
83
85
  readonly other: string;
84
86
  };
87
+ /** The pets in a stay's party, after the people. */
88
+ readonly pets: Readonly<Partial<Record<Intl.LDMLPluralRule, string>>> & {
89
+ readonly other: string;
90
+ };
85
91
  }
86
92
  export declare const DEFAULT_COPY: Readonly<Record<ChatLocale, ChatCopy>>;
87
93
  //#endregion
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { r as MAX_MESSAGE_CHARS, t as CHAT_LOCALES } from "./protocol-BuNqZAMt.js";
2
- import { f as chatAvailable, s as Spark, t as Arrow, u as loadConversation } from "./icons-DIqOmKoe.js";
2
+ import { f as chatAvailable, s as Spark, t as Arrow, u as loadConversation } from "./icons-BK5Lk0cP.js";
3
3
  import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -14,10 +14,10 @@ const DEFAULT_COPY = {
14
14
  notice: "Sie chatten mit einer KI. Preise und Verfügbarkeit in den Karten kommen direkt aus unserem Buchungssystem.",
15
15
  greeting: "Hallo! Erzählen Sie mir einfach, wie Ihr Urlaub aussehen soll: wann, mit wem und was Ihnen wichtig ist. Ich suche passende Unterkünfte, prüfe freie Termine und Preise und zeige Ihnen, was passt.",
16
16
  suggestions: [
17
- "Ein Ferienhaus für 4 Personen in den Sommerferien",
17
+ "Sommerwoche für 2 Erwachsene und 2 Kinder",
18
18
  "Eine Woche mit Hund, gern mit eingezäuntem Garten",
19
19
  "Ein Wochenende zu zweit mit Sauna",
20
- "Was ist in den Herbstferien noch frei?"
20
+ "Was ist am nächsten Wochenende noch frei?"
21
21
  ],
22
22
  placeholder: "Wann, mit wem, was ist Ihnen wichtig?",
23
23
  send: "Senden",
@@ -43,6 +43,7 @@ const DEFAULT_COPY = {
43
43
  photo: "Foto",
44
44
  previous: "Zurück",
45
45
  next: "Weiter",
46
+ rentalsFound: "Gefundene Unterkünfte",
46
47
  reviews: {
47
48
  one: "{n} Bewertung",
48
49
  other: "{n} Bewertungen"
@@ -75,6 +76,10 @@ const DEFAULT_COPY = {
75
76
  children: {
76
77
  one: "{n} Kind",
77
78
  other: "{n} Kinder"
79
+ },
80
+ pets: {
81
+ one: "{n} Haustier",
82
+ other: "{n} Haustiere"
78
83
  }
79
84
  },
80
85
  en: {
@@ -86,10 +91,10 @@ const DEFAULT_COPY = {
86
91
  notice: "You are chatting with an AI. Prices and availability in the cards come straight from our booking system.",
87
92
  greeting: "Hello! Just tell me what your holiday should look like: when, who with and what matters to you. I find rentals that fit, check free dates and prices and show you what suits.",
88
93
  suggestions: [
89
- "A holiday home for 4 in the summer holidays",
94
+ "A summer week for 2 adults and 2 kids",
90
95
  "A week with our dog, ideally with a fenced garden",
91
96
  "A weekend for two with a sauna",
92
- "What is still free in the autumn holidays?"
97
+ "Anything free next weekend?"
93
98
  ],
94
99
  placeholder: "When, who with, what matters to you?",
95
100
  send: "Send",
@@ -115,6 +120,7 @@ const DEFAULT_COPY = {
115
120
  photo: "Photo",
116
121
  previous: "Previous",
117
122
  next: "Next",
123
+ rentalsFound: "Rentals found",
118
124
  reviews: {
119
125
  one: "{n} review",
120
126
  other: "{n} reviews"
@@ -147,6 +153,10 @@ const DEFAULT_COPY = {
147
153
  children: {
148
154
  one: "{n} child",
149
155
  other: "{n} children"
156
+ },
157
+ pets: {
158
+ one: "{n} pet",
159
+ other: "{n} pets"
150
160
  }
151
161
  }
152
162
  };
@@ -246,6 +256,8 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
246
256
  ${SITE_RADIUS.chat}: var(${SITE_RADIUS.site}, ${SITE_RADIUS.own});
247
257
  --chat-secondary: color-mix(in srgb, var(--chat-primary) 12%, var(--chat-surface));
248
258
  --chat-on-secondary: var(--chat-text);
259
+ /* The tinted bubbles' edge: on a dark surface the tint alone barely stands off the message area. */
260
+ --chat-tint-edge: color-mix(in srgb, var(--chat-primary) 30%, transparent);
249
261
  /* The small marks (the chat's name and sparkle, a card's place, icons, the ring): the primary,
250
262
  unless a site sets an accent of its own (\`colors.accent\`). */
251
263
  --chat-accent: var(--chat-primary);
@@ -255,15 +267,21 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
255
267
  --chat-round: calc(var(--chat-base-radius) * 100);
256
268
  --chat-shadow: 0 18px 48px rgb(15 23 42 / 0.18);
257
269
  --chat-card-shadow: 0 1px 3px rgb(15 23 42 / 0.08), 0 8px 24px rgb(15 23 42 / 0.06);
258
- /* Not set here, so a site's value reaches the widget: \`--chat-top\` keeps a sticky header uncovered,
259
- \`--chat-width\` widens the panel on wide screens. */
270
+ /* Not set here, so a site's value reaches the widget, on wide screens (from 1024px): \`--chat-top\`
271
+ keeps a sticky header uncovered, \`--chat-width\` widens the panel. A phone's sheet covers the
272
+ header: the panel is modal, so the header could not be tapped and would only cost height. */
260
273
  }
261
274
  * { box-sizing: border-box; }
262
275
  .chat { color: var(--chat-text); font-size: 15px; line-height: 1.45; }
276
+ /* One focus ring for every control, in the site's primary; a click or a tap shows none. Without
277
+ specificity (:where), so a control that needs its ring elsewhere sets it with a plain rule. */
278
+ :where(a, button, [tabindex]:not([tabindex="-1"])):focus-visible { outline: 2px solid var(--chat-primary);
279
+ outline-offset: 2px; }
263
280
 
264
281
  /* The entry pill: frosted glass over any page, the chat's mark, a ring running round it until the
265
282
  visitor has asked something. Phones tap it; wide screens type into it. */
266
- .entry { position: fixed; right: 20px; bottom: 20px; z-index: 2147483000; }
283
+ .entry { position: fixed; right: 20px; bottom: calc(20px + env(safe-area-inset-bottom, 0px));
284
+ z-index: 2147483000; }
267
285
  [data-position="left"] .entry { right: auto; left: 20px; }
268
286
  .pill { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 12px;
269
287
  padding: 8px 20px 8px 8px; border-radius: var(--chat-round);
@@ -297,69 +315,75 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
297
315
  }
298
316
 
299
317
  /* The panel: over the dimmed page, a card at the screen's edge; on a phone a sheet from the bottom. */
300
- .scrim { position: fixed; left: 0; right: 0; bottom: 0; top: var(--chat-top, 0px); z-index: 2147482999;
318
+ .scrim { position: fixed; inset: 0; z-index: 2147482999;
301
319
  background: rgb(15 23 42 / 0.45); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
320
+ /* Clipped, not hidden, where supported: a clipping box is no scroll container, so no script, focus or
321
+ find-in-page can scroll the panel and shift its header out of view. */
302
322
  .panel { position: fixed; z-index: 2147483000; left: 0; right: 0; bottom: 0;
303
- top: calc(var(--chat-top, 0px) + 12px); display: flex; flex-direction: column;
323
+ top: 12px; display: flex; flex-direction: column;
304
324
  background: var(--chat-surface); border-radius: var(--chat-radius) var(--chat-radius) 0 0;
305
- box-shadow: var(--chat-shadow); overflow: hidden; }
325
+ box-shadow: var(--chat-shadow); overflow: hidden; overflow: clip; }
306
326
  .scrim[hidden], .panel[hidden] { display: none; }
327
+ /* Focused by script only (a phone reopening a conversation), never a stop for Tab. */
328
+ .panel:focus { outline: none; }
307
329
  @media (min-width: 1024px) {
330
+ .scrim { top: var(--chat-top, 0px); }
308
331
  .panel { left: auto; right: 20px; bottom: 20px; top: calc(var(--chat-top, 0px) + 20px);
309
332
  width: min(var(--chat-width, 700px), calc(100vw - 40px)); border-radius: var(--chat-radius); }
310
333
  [data-position="left"] .panel { right: auto; left: 20px; }
311
334
  }
312
- .header { display: flex; align-items: center; gap: 8px; padding: 20px 24px 4px; }
335
+ .header { display: flex; align-items: center; gap: 8px; padding: 12px 24px 4px; }
336
+ @media (min-width: 1024px) { .header { padding-top: 20px; } }
313
337
  .title { margin: 0 auto 0 0; font-size: 13px; font-weight: 600; letter-spacing: 0.16em;
314
338
  text-transform: uppercase; color: var(--chat-accent); }
315
339
  .icon-button { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0;
316
340
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
317
341
  color: var(--chat-text); cursor: pointer; }
318
- .icon-button:hover { border-color: var(--chat-primary); }
319
342
  /* Closing is the panel's one strong control, filled like the site's buttons. */
320
343
  .close { background: var(--chat-primary); border-color: var(--chat-primary); color: var(--chat-on-primary); }
321
- .close:hover { opacity: 0.9; }
322
- .notice { margin: 0; padding: 0 24px 14px; font-size: 12px; color: var(--chat-muted); }
323
- .messages { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 24px 24px 40px;
344
+ .notice { margin: 0; padding: 0 24px 14px; font-size: 13px; color: var(--chat-muted); }
345
+ /* Positioned: the absolute speaker labels (.sr-only) scroll with the list instead of giving the panel a
346
+ scroll range, and a turn's offsetTop is measured from the list. */
347
+ .messages { position: relative; flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 24px 24px 40px;
324
348
  display: flex; flex-direction: column; gap: 16px; background: var(--chat-surface-soft);
325
349
  container-type: inline-size; }
326
350
  .turn { display: flex; flex-direction: column; }
327
351
  .message { max-width: 88%; white-space: pre-wrap; overflow-wrap: anywhere; }
328
352
  .message[data-role="visitor"] { align-self: flex-end; margin-left: 48px; padding: 12px 16px;
329
- border-radius: calc(var(--chat-radius) * 0.75); background: var(--chat-secondary);
353
+ border: 1px solid var(--chat-tint-edge); border-radius: calc(var(--chat-radius) * 0.75);
354
+ background: var(--chat-secondary);
330
355
  color: var(--chat-on-secondary); }
331
356
  .message[data-role="assistant"] { align-self: flex-start; max-width: 100%; font-size: 16px; line-height: 1.6; }
332
357
  .message strong { font-weight: 600; }
333
358
  .ui { align-self: stretch; display: flex; flex-direction: column; gap: 12px; }
334
- .thinking { display: flex; gap: 5px; padding: 4px 2px; }
359
+ .thinking { display: flex; align-items: center; gap: 5px; padding: 4px 2px; }
360
+ .thinking-label { margin-left: 6px; color: var(--chat-muted); font-size: 14px; }
335
361
  .thinking i { width: 7px; height: 7px; border-radius: 999px; background: var(--chat-accent); opacity: 0.5; }
336
- .dock { position: relative; padding: 10px 20px 16px; background: var(--chat-surface-soft); }
362
+ .dock { position: relative; padding: 10px 20px calc(16px + env(safe-area-inset-bottom, 0px));
363
+ background: var(--chat-surface-soft); }
337
364
  .dock::before { content: ""; position: absolute; left: 0; right: 0; top: -28px; height: 28px;
338
365
  pointer-events: none; background: linear-gradient(to top, var(--chat-surface-soft), transparent); }
339
- .suggestions { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; margin: 0 -20px 10px;
340
- padding: 0 20px; }
341
- .suggestions::-webkit-scrollbar { display: none; }
342
- /* Suggestions and quick replies: filled pills, an icon for the topic in the accent colour. */
366
+ /* Suggestions wrap, on a phone too: in a row that scrolls, one cut chip hides the others. */
367
+ .suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
368
+ /* Suggestions and quick replies: filled pills; a suggestion has an icon for its topic in the accent
369
+ colour (a quick reply is the model's words, an icon guessed from them can be wrong). */
343
370
  .suggestion, .chip { flex: none; max-width: 100%; display: inline-flex; align-items: center; gap: 8px;
344
- border: 1px solid transparent; border-radius: var(--chat-round); background: var(--chat-secondary);
371
+ border: 1px solid var(--chat-tint-edge); border-radius: var(--chat-round); background: var(--chat-secondary);
345
372
  color: var(--chat-on-secondary); padding: 8px 16px 8px 12px; font: inherit; font-size: 14px;
346
- font-weight: 600; line-height: 1.35; text-align: left; white-space: nowrap; cursor: pointer; }
347
- .suggestion svg, .chip svg { flex: none; color: var(--chat-accent); }
348
- /* Suggestions scroll in one row; a quick reply wraps inside the message's width. */
349
- .chip { white-space: normal; }
350
- .suggestion:hover { border-color: var(--chat-primary); background: var(--chat-surface); color: var(--chat-text); }
351
- @media (min-width: 1024px) {
352
- .suggestions { flex-wrap: wrap; overflow: visible; margin: 0 0 10px; padding: 0; }
353
- .suggestion { white-space: normal; }
354
- }
373
+ font-weight: 600; line-height: 1.35; text-align: left; cursor: pointer; }
374
+ .chip { padding-left: 16px; }
375
+ .suggestion svg { flex: none; color: var(--chat-accent); }
355
376
  .composer { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 18px;
356
377
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
357
378
  box-shadow: 0 4px 16px rgb(15 23 42 / 0.08); }
358
- .composer:focus-within { border-color: var(--chat-text); }
379
+ /* The field's ring, not the send button's too (that one has its own). */
380
+ .composer:has(textarea:focus-visible) { outline: 2px solid var(--chat-primary); outline-offset: 2px; }
359
381
  /* 16px keeps iOS from zooming into the field. */
360
382
  .composer textarea { flex: 1; min-width: 0; min-height: 36px; max-height: 128px; resize: none; border: 0;
361
383
  outline: none; background: transparent; padding: 6px 0; font: inherit; font-size: 16px;
362
384
  line-height: 1.5; color: inherit; field-sizing: content; }
385
+ /* The field's only visible label: the muted text colour, not the browser's grey (below AA in dark). */
386
+ .composer textarea::placeholder { color: var(--chat-muted); opacity: 1; }
363
387
  .send { flex: none; display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0;
364
388
  border-radius: var(--chat-round); background: var(--chat-primary); color: var(--chat-on-primary); cursor: pointer; }
365
389
  .send:disabled { opacity: 0.3; cursor: not-allowed; }
@@ -374,8 +398,11 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
374
398
  white-space: pre-wrap; }
375
399
  /* The bubble's own text colour, softened: the muted grey suits the surface, not a set secondary. */
376
400
  .pending figcaption { margin-bottom: 2px; color: inherit; opacity: 0.75; }
377
- .fallback { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 10px; }
401
+ /* The chat cannot answer: its reply to the visitor's message, the ways on as buttons. */
402
+ .fallback { display: flex; flex-direction: column; gap: 12px; }
378
403
  .fallback p { margin: 0; }
404
+ .fallback-links { display: flex; flex-wrap: wrap; gap: 8px; }
405
+ .fallback-links > .button { margin: 0; }
379
406
 
380
407
  /* What the replies show. */
381
408
  .card { border-radius: var(--chat-radius); overflow: hidden; background: var(--chat-surface);
@@ -397,7 +424,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
397
424
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.18); cursor: pointer; opacity: 0; }
398
425
  .gallery-arrow[data-side="prev"] { left: 10px; }
399
426
  .gallery-arrow[data-side="next"] { right: 10px; }
400
- .gallery-wrap:hover .gallery-arrow:not(:disabled), .gallery-arrow:focus-visible { opacity: 1; }
427
+ .gallery-arrow:focus-visible { opacity: 1; }
401
428
  @media (hover: none) { .gallery-arrow:not(:focus-visible) { pointer-events: none; } }
402
429
  /* The row scrolls, so it clips: room below for the cards' shadow, taken back from the gap under it. */
403
430
  .carousel { gap: 12px; padding: 2px 2px 16px; margin-bottom: -10px; }
@@ -408,74 +435,112 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
408
435
  .carousel > .card { flex-basis: calc((100% - 24px) / 2.15); }
409
436
  .carousel:has(> .card:nth-child(2):last-child) > .card { flex-basis: calc((100% - 12px) / 2); }
410
437
  .carousel-wrap:has(> .carousel > .card:nth-child(2):last-child) > .snap-nav { display: none; }
411
- /* "n / total" counts one card per step, which a row of two no longer takes: the arrows stay. */
412
- .carousel-wrap > .snap-nav > span { visibility: hidden; }
438
+ /* The count follows the first card in view, and two show at once: it would skip numbers. */
439
+ .carousel-wrap > .snap-nav > span { display: none; }
413
440
  }
414
- .snap-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
415
- .snap-nav button, .calendar-head button { border: 1px solid var(--chat-border); background: var(--chat-surface);
416
- color: var(--chat-text); border-radius: var(--chat-round); width: 28px; height: 28px; cursor: pointer; font: inherit;
417
- line-height: 1; }
418
- .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.35; cursor: default; }
441
+ /* The row's controls, grouped at its end: "‹ 2 / 8 ›". */
442
+ .snap-nav { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 6px; }
443
+ .snap-nav button, .calendar-head button { flex: none; display: grid; place-items: center; width: 36px;
444
+ height: 36px; padding: 0; border: 1px solid var(--chat-border); background: var(--chat-surface);
445
+ color: var(--chat-text); border-radius: var(--chat-round); cursor: pointer; }
446
+ .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.5; cursor: default; }
419
447
  .rating { color: #b7791f; font-weight: 700; }
420
448
  .breakdown .line { display: flex; justify-content: space-between; gap: 12px; }
421
449
  .breakdown .total { border-top: 1px solid var(--chat-border); padding-top: 6px; margin-top: 2px;
422
450
  font-weight: 700; }
423
- .alternatives, .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
424
- .chip-link { border: 1px solid var(--chat-primary); color: var(--chat-primary); border-radius: var(--chat-round);
425
- padding: 5px 10px; text-decoration: none; font-size: 13px; }
426
- .chip:hover { border-color: var(--chat-primary); background: var(--chat-surface); color: var(--chat-text); }
451
+ .alt-chips, .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
452
+ .alt-caption { margin: 0 0 4px; font-size: 13px; color: var(--chat-muted); }
453
+ .chip-link { display: inline-flex; align-items: center; border: 1px solid var(--chat-primary);
454
+ color: var(--chat-primary); border-radius: var(--chat-round); padding: 5px 10px; text-decoration: none;
455
+ font-size: 14px; }
427
456
  .calendar { border: 1px solid var(--chat-border); border-radius: var(--chat-radius); padding: 12px;
428
457
  background: var(--chat-surface); }
429
458
  .calendar-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
430
459
  font-weight: 700; margin-bottom: 8px; text-align: center; }
431
460
  .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; text-align: center;
432
- font-size: 12px; }
461
+ font-size: 13px; }
433
462
  .weekday { color: var(--chat-muted); font-weight: 600; }
434
463
  .day { padding: 4px 0; border-radius: min(6px, var(--chat-radius)); }
435
464
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
436
465
  white-space: nowrap; }
466
+ /* Free, booked and the stay must tell apart in dark too: a stronger tint for free, an edge for booked
467
+ (inset, so a booked day is no taller than the others). */
437
468
  .day[data-status="free"], .legend i[data-status="free"] {
438
- background: color-mix(in srgb, var(--chat-primary) 14%, var(--chat-surface)); color: var(--chat-text); }
469
+ background: color-mix(in srgb, var(--chat-primary) 26%, var(--chat-surface)); color: var(--chat-text); }
439
470
  .day[data-status="booked"], .legend i[data-status="booked"] { background: var(--chat-surface-soft);
440
- color: var(--chat-muted);
471
+ box-shadow: inset 0 0 0 1px var(--chat-border); color: var(--chat-muted);
441
472
  text-decoration: line-through; }
442
473
  .day[data-status="closed"] { color: var(--chat-muted); opacity: 0.6; }
443
474
  .day[data-stay], .legend i[data-stay] { background: var(--chat-primary); color: var(--chat-on-primary);
444
475
  text-decoration: none; }
445
- .legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; }
446
- .legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 4px;
447
- vertical-align: -1px; }
476
+ .legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 13px; }
477
+ .legend i { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 4px;
478
+ vertical-align: -1px; border: 1px solid var(--chat-border); }
448
479
  .compare { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
449
480
  .compare-col { border-radius: var(--chat-radius); padding: 10px; background: var(--chat-surface);
450
- box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 13px;
451
- min-width: 0; }
452
- /* Three columns leave a button about 90px: "Zur Buchung" fits on one line only without side padding. */
453
- .compare-col .button { padding: 8px 2px; font-size: 13px; }
481
+ box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 14px;
482
+ min-width: 0; hyphens: auto; }
454
483
  .compare-col dl { margin: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
455
- .compare-col dt { color: var(--chat-muted); font-size: 11px; margin-top: 4px; }
456
- .compare-col dd { margin: 0; overflow-wrap: anywhere; }
484
+ .compare-col dt { color: var(--chat-muted); font-size: 12px; margin-top: 4px; }
485
+ .compare-col dd { margin: 0; overflow-wrap: break-word; }
486
+ .compare-col .price { font-size: 16px; }
487
+ /* A phone has room for two columns, not three of 100px with a word on each line: the third peeks in
488
+ and the row scrolls, with room below for the columns' shadow, as the carousel. */
489
+ @container (max-width: 519px) {
490
+ /* A phone shows one card at a time: each as tall as its own content, so the first card's price and
491
+ button are not pushed down to the tallest card's foot. */
492
+ .carousel { align-items: flex-start; }
493
+ .compare { grid-auto-columns: minmax(150px, 1fr); overflow-x: auto; scroll-snap-type: x mandatory;
494
+ scrollbar-width: none; padding: 2px 2px 16px; margin-bottom: -10px; }
495
+ .compare::-webkit-scrollbar { display: none; }
496
+ .compare-col { scroll-snap-align: start; }
497
+ }
498
+ /* Side by side, the rows line up across the columns (a long name pushes every row of its column down
499
+ otherwise): the name, the list's eight cells and the button share the comparison's ten rows. */
500
+ @supports (grid-template-rows: subgrid) {
501
+ @container (min-width: 520px) {
502
+ .compare { grid-template-rows: repeat(10, auto); row-gap: 0; }
503
+ .compare-col { display: grid; grid-row: span 10; grid-template-rows: subgrid; }
504
+ .compare-col dl { display: grid; grid-row: span 8; grid-template-rows: subgrid; }
505
+ .compare-col .button { grid-row: -2; }
506
+ }
507
+ }
457
508
  .card-body { padding: 16px 18px 12px; display: flex; flex-direction: column; gap: 6px; }
458
509
  .card-title { margin: 0; font-size: 18px; font-weight: 500; line-height: 1.3; }
510
+ /* One long name (v9 names often run to a dozen words) would make every card of a row taller: in a
511
+ carousel the name stops at three lines. The single card, the quote and Compare show it whole. */
512
+ .carousel .card-title { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
513
+ overflow: hidden; }
459
514
  /* A rental card: place and score, name, facts and wishes; the stay's price and button below a line. */
460
515
  .rental-card, .stay-card { display: flex; flex-direction: column; }
461
516
  .rental-card .card-body { flex: 1; }
462
517
  .card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; }
463
518
  .place { color: var(--chat-accent); font-size: 14px; }
464
519
  .score-chip { margin-left: auto; padding: 2px 8px; border-radius: calc(var(--chat-radius) * 0.4);
465
- background: var(--chat-secondary); color: var(--chat-on-secondary); font-size: 12px; font-weight: 700; }
520
+ background: var(--chat-secondary); color: var(--chat-on-secondary); font-size: 13px; font-weight: 700; }
466
521
  .specs { margin: 0; color: var(--chat-muted); font-size: 14px; }
467
522
  .card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
468
523
  margin: 0 18px; padding: 12px 0 16px; border-top: 1px solid var(--chat-border); }
469
524
  .foot-price { display: flex; flex-direction: column; min-width: 0; }
470
525
  .foot-price .price { font-size: 18px; line-height: 1.3; }
471
- .foot-meta { color: var(--chat-muted); font-size: 12px; }
526
+ .foot-meta { color: var(--chat-muted); font-size: 14px; }
527
+ .foot-meta .period { white-space: nowrap; }
472
528
  .button.compact { margin: 0 0 0 auto; padding: 9px 20px; font-size: 14px; white-space: nowrap; }
473
529
  .muted { color: var(--chat-muted); font-size: 13px; }
474
530
  .price { font-weight: 700; }
475
- .button { display: inline-block; margin-top: 6px; text-align: center; text-decoration: none;
476
- padding: 10px 16px; border-radius: var(--chat-round); background: var(--chat-primary); color: var(--chat-on-primary);
477
- border: 0; cursor: pointer; font: inherit; font-weight: 600; }
531
+ /* Relative: the link's screen-reader name (an absolute .sr-only) stays inside a scrolled row, which
532
+ clips it, instead of sticking out of the row and widening the panel. */
533
+ .button { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-top: 6px;
534
+ text-align: center; text-decoration: none; padding: 10px 16px; border-radius: var(--chat-round);
535
+ background: var(--chat-primary); color: var(--chat-on-primary); border: 0; cursor: pointer; font: inherit;
536
+ font-weight: 600; }
478
537
  .link { color: var(--chat-primary); }
538
+ /* A button on its own in a reply: as wide as its label needs, not the panel's 650px. */
539
+ .ui > .button, .ui > .link { align-self: flex-start; min-width: 14rem; }
540
+ /* The way to the contact page in a reply (ContactLink) and in the fallback: a button, outlined. */
541
+ .ui > .link, .fallback-links > .link { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px;
542
+ border: 1px solid var(--chat-primary); border-radius: var(--chat-round); text-decoration: none;
543
+ font-weight: 600; }
479
544
  /* A link out of the chat while a reply still streams (ReplyLink). */
480
545
  [aria-disabled="true"] { opacity: 0.5; cursor: progress; }
481
546
 
@@ -484,6 +549,12 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
484
549
  .no-photo { display: none; aspect-ratio: 5 / 3; place-items: center; background: var(--chat-surface-soft);
485
550
  color: var(--chat-muted); }
486
551
  .carousel .no-photo { display: grid; }
552
+ /* A card on its own in a wide panel: a flatter photo, so the card's name and price fit the list with it. */
553
+ @container (min-width: 520px) {
554
+ .ui > .rental-card .gallery img { aspect-ratio: 2 / 1; max-height: 280px; }
555
+ /* In a carousel too: the cards' prices and the reply under them come up above the dock sooner. */
556
+ .carousel .gallery img, .carousel .no-photo { aspect-ratio: 2 / 1; }
557
+ }
487
558
  /* Wishes a found stay matches, ticked under the name. */
488
559
  .wishes { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
489
560
  .wishes li { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px 4px 10px;
@@ -491,21 +562,31 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
491
562
  .wishes svg { color: var(--chat-accent); }
492
563
 
493
564
  /* The price calendar: a column per arrival, the bar's height its total. */
494
- .price-columns { list-style: none; margin: 10px 0 0; padding: 0; display: grid; grid-auto-flow: column;
495
- grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto; scrollbar-width: none; }
496
- .price-column { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 4px;
565
+ .price-columns { position: relative; list-style: none; margin: 10px 0 0; padding: 0; display: grid;
566
+ grid-auto-flow: column; grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto;
567
+ scrollbar-width: none; }
568
+ .price-columns::-webkit-scrollbar { display: none; }
569
+ /* More columns beyond an edge fade out there. */
570
+ .price-columns[data-fade="end"] { mask-image: linear-gradient(to left, transparent, #000 32px); }
571
+ .price-columns[data-fade="start"] { mask-image: linear-gradient(to right, transparent, #000 32px); }
572
+ .price-columns[data-fade="both"] { mask-image: linear-gradient(to right, transparent, #000 32px,
573
+ #000 calc(100% - 32px), transparent); }
574
+ /* The arrival's day at the column's foot, level across the row when "nicht buchbar" takes two lines. */
575
+ .price-column { height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 4px;
497
576
  padding: 6px 2px; border: 1px solid transparent; border-radius: 10px; background: transparent;
498
577
  color: inherit; font: inherit; cursor: pointer; }
499
578
  .price-column:disabled { cursor: default; }
500
- .price-column[data-free]:not(:disabled):hover { border-color: var(--chat-primary); }
579
+ /* Inside the row that scrolls, so it clips: the ring sits inside the column. */
580
+ .price-column:focus-visible { outline-offset: -2px; }
501
581
  .price-column[data-asked] { border-color: var(--chat-border); background: var(--chat-surface-soft); }
502
582
  .bar-track { display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 72px; }
503
583
  .bar { width: 60%; border-radius: min(6px, var(--chat-radius)) min(6px, var(--chat-radius)) 0 0;
504
584
  background: color-mix(in srgb, var(--chat-primary) 30%, var(--chat-surface)); }
505
585
  .price-column[data-cheapest] .bar, .legend i[data-cheapest] { background: var(--chat-primary); }
506
586
  .legend i[data-asked] { background: var(--chat-surface-soft); border: 1px solid var(--chat-border); }
507
- .price-text { font-size: 12px; font-weight: 700; white-space: nowrap; }
508
- .price-column:not([data-free]) .price-text { color: var(--chat-muted); font-weight: 500; }
587
+ .price-column > .muted { margin-top: auto; }
588
+ .price-text { font-size: 14px; font-weight: 700; white-space: nowrap; }
589
+ .price-column:not([data-free]) .price-text { color: var(--chat-muted); font-weight: 500; white-space: normal; }
509
590
 
510
591
  /* Reviews: the PMS's score and grade, its categories, a few texts. */
511
592
  .score { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
@@ -531,7 +612,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
531
612
  .map-gate { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
532
613
  min-height: 150px; padding: 16px; text-align: center; background: var(--chat-surface-soft); }
533
614
  .map-gate p { margin: 0; }
534
- .map-attribution { margin: 0; padding: 2px 8px; font-size: 10px; color: var(--chat-muted); text-align: right; }
615
+ .map-attribution { margin: 0; padding: 2px 8px; font-size: 12px; color: var(--chat-muted); text-align: right; }
535
616
  .map-attribution a { color: inherit; }
536
617
  .map-pin { display: block; width: 18px; height: 18px; border-radius: 999px; background: var(--chat-primary);
537
618
  border: 3px solid var(--chat-surface); box-shadow: 0 2px 6px rgb(0 0 0 / 0.35); }
@@ -542,6 +623,24 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
542
623
  .maplibregl-canvas:focus-visible { outline: 2px solid var(--chat-primary); outline-offset: -2px; }
543
624
  .maplibregl-marker { position: absolute; top: 0; left: 0; will-change: transform; }
544
625
 
626
+ /* Hover only where there is a pointer: on a touch screen a tapped chip would keep its hover look. */
627
+ @media (hover: hover) {
628
+ .icon-button:hover { border-color: var(--chat-primary); }
629
+ .close:hover { opacity: 0.9; }
630
+ .suggestion:hover, .chip:not(:disabled):hover { border-color: var(--chat-primary);
631
+ background: var(--chat-surface); color: var(--chat-text); }
632
+ .gallery-wrap:hover .gallery-arrow:not(:disabled) { opacity: 1; }
633
+ .price-column[data-free]:not(:disabled):hover { border-color: var(--chat-primary); }
634
+ }
635
+
636
+ /* Fingers: every control at least 44px each way. */
637
+ @media (pointer: coarse) {
638
+ .icon-button, .send, .snap-nav button, .calendar-head button, .gallery-arrow { width: 44px; height: 44px; }
639
+ .composer { padding: 4px 4px 4px 18px; }
640
+ .composer textarea { min-height: 44px; padding-block: 10px; }
641
+ .chip, .suggestion, .button, .chip-link, .ui > .link, .fallback-links > .link { min-height: 44px; }
642
+ }
643
+
545
644
  [data-theme="soft"] { --chat-radius: calc(var(--chat-base-radius) * 2.4); --chat-shadow: 0 12px 32px rgb(15 23 42 / 0.12);
546
645
  --chat-card-shadow: 0 4px 18px rgb(15 23 42 / 0.07); }
547
646
  [data-theme="minimal"] { --chat-radius: calc(var(--chat-base-radius) * 0.6); --chat-shadow: none; --chat-card-shadow: none; }
@@ -554,11 +653,13 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
554
653
  @keyframes chat-in { from { opacity: 0; transform: translateY(16px); } }
555
654
  @keyframes chat-fade { from { opacity: 0; } }
556
655
  @keyframes chat-pulse { 50% { opacity: 1; transform: translateY(-2px); } }
656
+ @media (prefers-reduced-motion: no-preference) and (hover: hover) {
657
+ .pill:hover { transform: translateY(-2px); }
658
+ }
557
659
  @media (prefers-reduced-motion: no-preference) {
558
660
  .glow::before { animation: chat-spin 3.2s linear infinite; }
559
661
  .entry { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
560
662
  .pill { transition: transform 200ms ease-out; }
561
- .pill:hover { transform: translateY(-2px); }
562
663
  .panel { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
563
664
  .scrim { animation: chat-fade 300ms ease-out; }
564
665
  .thinking i { animation: chat-pulse 1.1s ease-in-out infinite; }
@@ -572,7 +673,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
572
673
  `;
573
674
  //#endregion
574
675
  //#region src/WebsiteChat.tsx
575
- const Panel = lazy(() => import("./panel-BN_DMA4R.js"));
676
+ const Panel = lazy(() => import("./panel-CfS3SuRX.js"));
576
677
  /** See {@link WebsiteChatComponent}. */
577
678
  const WebsiteChat = function WebsiteChat(props) {
578
679
  const { endpoint, locale, theme = "classic", position = "right" } = props;
@@ -644,7 +745,7 @@ const WebsiteChat = function WebsiteChat(props) {
644
745
  fallback: null,
645
746
  children: /* @__PURE__ */ jsx(Panel, {
646
747
  endpoint,
647
- clientVersion: "1.1.3",
748
+ clientVersion: "1.2.0",
648
749
  locale,
649
750
  copy,
650
751
  searchPath: props.searchPath,