@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 +2 -2
- package/dist/{catalog-y3xQ2jXW.js → catalog-ClcCUjf7.js} +13 -13
- package/dist/catalog.js +1 -1
- package/dist/{icons-DIqOmKoe.js → icons-BK5Lk0cP.js} +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +175 -74
- package/dist/{panel-BN_DMA4R.js → panel-CfS3SuRX.js} +292 -143
- package/instructions/CHANGELOG.md +1 -0
- package/instructions/MIGRATION.md +1 -0
- package/instructions/versions/1.2.0/CHANGELOG.md +45 -0
- package/instructions/versions/1.2.0/MIGRATION.md +15 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as SESSION_TTL_S, r as MAX_MESSAGE_CHARS } from "./protocol-BuNqZAMt.js";
|
|
2
|
-
import { a as House, c as TopicIcon, d as saveConversation, i as Cross, l as clearConversation, n as Check, o as Restart, p as sendMessage, r as Chevron, t as Arrow, u as loadConversation } from "./icons-
|
|
3
|
-
import { r as chatCatalog, u as statePath } from "./catalog-
|
|
2
|
+
import { a as House, c as TopicIcon, d as saveConversation, i as Cross, l as clearConversation, n as Check, o as Restart, p as sendMessage, r as Chevron, t as Arrow, u as loadConversation } from "./icons-BK5Lk0cP.js";
|
|
3
|
+
import { r as chatCatalog, u as statePath } from "./catalog-ClcCUjf7.js";
|
|
4
4
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { JSONUIProvider, Renderer, defineRegistry, useStateValue } from "@json-render/react";
|
|
@@ -130,26 +130,39 @@ const useStay = (stayId, rentalId) => {
|
|
|
130
130
|
};
|
|
131
131
|
const useRental = (rentalId) => useStateValue(statePath("rentals", rentalId)) ?? null;
|
|
132
132
|
const day = (value) => /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
133
|
+
/** The locale dates are formatted in: British English ("13 May"), as the replies write dates and as
|
|
134
|
+
* European visitors read them, never the US order "5/13" that a bare "en" gives. */
|
|
135
|
+
const dateLocale = (locale) => locale === "en" ? "en-GB" : "de-DE";
|
|
133
136
|
/** "12.–19. Juli 2027" from two `YYYY-MM-DD` days. */
|
|
134
|
-
const period = (stay, locale) => new Intl.DateTimeFormat(locale, {
|
|
137
|
+
const period = (stay, locale) => new Intl.DateTimeFormat(dateLocale(locale), {
|
|
135
138
|
day: "numeric",
|
|
136
139
|
month: "short",
|
|
137
140
|
year: "numeric",
|
|
138
141
|
timeZone: "UTC"
|
|
139
142
|
}).formatRange(day(stay.start), day(stay.end));
|
|
140
|
-
/** "14.–21. Juli": the short form
|
|
141
|
-
const shortPeriod = (stay, locale) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
/** "14.–21. Juli", "26. Dez.–2. Jan.": the short form, without years even over New Year. */
|
|
144
|
+
const shortPeriod = (stay, locale) => {
|
|
145
|
+
const short = new Intl.DateTimeFormat(dateLocale(locale), {
|
|
146
|
+
day: "numeric",
|
|
147
|
+
month: "short",
|
|
148
|
+
timeZone: "UTC"
|
|
149
|
+
});
|
|
150
|
+
return stay.start.slice(0, 4) === stay.end.slice(0, 4) ? short.formatRange(day(stay.start), day(stay.end)) : `${short.format(day(stay.start))}–${short.format(day(stay.end))}`;
|
|
151
|
+
};
|
|
152
|
+
/** "1 Kind", "2 Kinder": a count in its plural form, its number and noun joined by a no-break space,
|
|
153
|
+
* so a narrow card never ends a line on the "2". */
|
|
154
|
+
const counted = (forms, n, locale) => (forms[new Intl.PluralRules(locale).select(n)] ?? forms.other).replace("{n} ", `${n}\u00a0`).replace("{n}", String(n));
|
|
155
|
+
/** "2 Schlafzimmer" from the PMS, its number and noun joined by a no-break space as in `counted`. */
|
|
156
|
+
const keepCount = (fact) => fact.replace(/^(\d+) /, "$1\xA0");
|
|
148
157
|
/** A review count as the PMS formats it ("36", or "1 Bewertungen" on v10), as the number it states. */
|
|
149
158
|
const reviewCount = (count) => Number(count.replace(/\D/g, ""));
|
|
150
159
|
const partyText = (stay, copy, locale) => {
|
|
151
160
|
const children = stay.party.children + stay.party.babies;
|
|
152
|
-
return [
|
|
161
|
+
return [
|
|
162
|
+
counted(copy.adults, stay.party.adults, locale),
|
|
163
|
+
...children ? [counted(copy.children, children, locale)] : [],
|
|
164
|
+
...stay.party.pets ? [counted(copy.pets, stay.party.pets, locale)] : []
|
|
165
|
+
].join(", ");
|
|
153
166
|
};
|
|
154
167
|
/** A bookable stay goes to the rental's booking page with everything chosen, where the site has one;
|
|
155
168
|
* otherwise to the rental page (or the search) with the stay filled in. */
|
|
@@ -174,12 +187,25 @@ const ReplyLink = ({ href, className, children }) => {
|
|
|
174
187
|
children
|
|
175
188
|
});
|
|
176
189
|
};
|
|
190
|
+
/** What a booking link is for, for a screen reader: in a links list, eight "Buchen" say nothing. */
|
|
191
|
+
const LinkTarget = ({ rentalId, stay }) => {
|
|
192
|
+
const { locale } = useChat();
|
|
193
|
+
const rental = useRental(rentalId);
|
|
194
|
+
if (!rental) return null;
|
|
195
|
+
return /* @__PURE__ */ jsx("span", {
|
|
196
|
+
className: "sr-only",
|
|
197
|
+
children: `, ${rental.name}${stay?.bookable ? `, ${shortPeriod(stay, locale)}` : ""}`
|
|
198
|
+
});
|
|
199
|
+
};
|
|
177
200
|
const BookLink = ({ rentalId, stay }) => {
|
|
178
201
|
const { copy } = useChat();
|
|
179
|
-
return /* @__PURE__ */
|
|
202
|
+
return /* @__PURE__ */ jsxs(ReplyLink, {
|
|
180
203
|
className: "button",
|
|
181
204
|
href: useBookHref(rentalId, stay),
|
|
182
|
-
children: stay?.bookable ? copy.bookStay : copy.viewRental
|
|
205
|
+
children: [stay?.bookable ? copy.bookStay : copy.viewRental, /* @__PURE__ */ jsx(LinkTarget, {
|
|
206
|
+
rentalId,
|
|
207
|
+
stay
|
|
208
|
+
})]
|
|
183
209
|
});
|
|
184
210
|
};
|
|
185
211
|
/** A row that snaps item by item, with arrow buttons and "n / total"; swipe, scroll or keys move it. */
|
|
@@ -220,7 +246,7 @@ const Snapper = ({ count, className, label, children }) => {
|
|
|
220
246
|
"aria-label": copy.previous,
|
|
221
247
|
disabled: index === 0,
|
|
222
248
|
onClick: () => go(-1),
|
|
223
|
-
children: "
|
|
249
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "prev" })
|
|
224
250
|
}),
|
|
225
251
|
/* @__PURE__ */ jsxs("span", {
|
|
226
252
|
className: "muted",
|
|
@@ -235,7 +261,7 @@ const Snapper = ({ count, className, label, children }) => {
|
|
|
235
261
|
"aria-label": copy.next,
|
|
236
262
|
disabled: index >= count - 1,
|
|
237
263
|
onClick: () => go(1),
|
|
238
|
-
children: "
|
|
264
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "next" })
|
|
239
265
|
})
|
|
240
266
|
]
|
|
241
267
|
})]
|
|
@@ -301,7 +327,6 @@ const Gallery = ({ images, name }) => {
|
|
|
301
327
|
}),
|
|
302
328
|
/* @__PURE__ */ jsx("span", {
|
|
303
329
|
className: "sr-only",
|
|
304
|
-
"aria-live": "polite",
|
|
305
330
|
children: `${copy.photo} ${Math.min(index + 1, images.length)} / ${images.length}`
|
|
306
331
|
})
|
|
307
332
|
] })]
|
|
@@ -331,17 +356,21 @@ const Wishes = ({ wishes }) => {
|
|
|
331
356
|
children: wishes.map((wish) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(Check, {}), wish] }, wish))
|
|
332
357
|
});
|
|
333
358
|
};
|
|
334
|
-
/** "7 Nächte ·
|
|
359
|
+
/** "7 Nächte · 6.–13. Mai": a stay's length and dates, short. */
|
|
335
360
|
const stayMeta = (stay, copy, locale) => {
|
|
336
361
|
const nights = Math.round((day(stay.end).getTime() - day(stay.start).getTime()) / 864e5);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
362
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
363
|
+
counted(copy.nights, nights, locale),
|
|
364
|
+
" ·",
|
|
365
|
+
" ",
|
|
366
|
+
/* @__PURE__ */ jsx("span", {
|
|
367
|
+
className: "period",
|
|
368
|
+
children: shortPeriod(stay, locale)
|
|
369
|
+
})
|
|
370
|
+
] });
|
|
343
371
|
};
|
|
344
|
-
/** A card's foot: the stay's total
|
|
372
|
+
/** A card's foot: the stay's total, its length and dates and the party it is for, and the way to
|
|
373
|
+
* book or to the rental. */
|
|
345
374
|
const CardFoot = ({ rentalId, stay }) => {
|
|
346
375
|
const { copy, locale } = useChat();
|
|
347
376
|
const href = useBookHref(rentalId, stay);
|
|
@@ -350,19 +379,29 @@ const CardFoot = ({ rentalId, stay }) => {
|
|
|
350
379
|
children: [stay && /* @__PURE__ */ jsx("div", {
|
|
351
380
|
className: "foot-price",
|
|
352
381
|
children: !stay.bookable ? /* @__PURE__ */ jsx("span", {
|
|
353
|
-
className: "muted",
|
|
354
|
-
children: copy.notBookable
|
|
355
|
-
}) : /* @__PURE__ */ jsxs(Fragment, { children: [stay.total && /* @__PURE__ */ jsx("span", {
|
|
356
|
-
className: "price",
|
|
357
|
-
children: stay.total
|
|
358
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
359
382
|
className: "foot-meta",
|
|
360
|
-
children:
|
|
361
|
-
})
|
|
362
|
-
|
|
383
|
+
children: copy.notBookable
|
|
384
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
385
|
+
stay.total && /* @__PURE__ */ jsx("span", {
|
|
386
|
+
className: "price",
|
|
387
|
+
children: stay.total
|
|
388
|
+
}),
|
|
389
|
+
/* @__PURE__ */ jsx("span", {
|
|
390
|
+
className: "foot-meta",
|
|
391
|
+
children: stayMeta(stay, copy, locale)
|
|
392
|
+
}),
|
|
393
|
+
/* @__PURE__ */ jsx("span", {
|
|
394
|
+
className: "foot-meta",
|
|
395
|
+
children: partyText(stay, copy, locale)
|
|
396
|
+
})
|
|
397
|
+
] })
|
|
398
|
+
}), /* @__PURE__ */ jsxs(ReplyLink, {
|
|
363
399
|
className: "button compact",
|
|
364
400
|
href,
|
|
365
|
-
children: stay?.bookable ? copy.book : copy.view
|
|
401
|
+
children: [stay?.bookable ? copy.book : copy.view, /* @__PURE__ */ jsx(LinkTarget, {
|
|
402
|
+
rentalId,
|
|
403
|
+
stay
|
|
404
|
+
})]
|
|
366
405
|
})]
|
|
367
406
|
});
|
|
368
407
|
};
|
|
@@ -398,7 +437,7 @@ const Card = ({ rentalId, stayId }) => {
|
|
|
398
437
|
}),
|
|
399
438
|
rental.highlights.length > 0 && /* @__PURE__ */ jsx("p", {
|
|
400
439
|
className: "specs",
|
|
401
|
-
children: rental.highlights.slice(0, 3).join(" · ")
|
|
440
|
+
children: rental.highlights.slice(0, 3).map(keepCount).join(" · ")
|
|
402
441
|
}),
|
|
403
442
|
/* @__PURE__ */ jsx(Wishes, { wishes })
|
|
404
443
|
]
|
|
@@ -436,17 +475,17 @@ const Calendar = ({ rentalId, stayId }) => {
|
|
|
436
475
|
const [shown, setShown] = useState(() => Math.max(0, stay ? months.indexOf(stay.start.slice(0, 7)) : 0));
|
|
437
476
|
if (!calendar || months.length === 0) return null;
|
|
438
477
|
const month = months[Math.min(shown, months.length - 1)];
|
|
439
|
-
const weekdays = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(locale, {
|
|
478
|
+
const weekdays = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(dateLocale(locale), {
|
|
440
479
|
weekday: "short",
|
|
441
480
|
timeZone: "UTC"
|
|
442
481
|
}).format(day(`2024-01-0${i + 1}`)));
|
|
443
|
-
const title = new Intl.DateTimeFormat(locale, {
|
|
482
|
+
const title = new Intl.DateTimeFormat(dateLocale(locale), {
|
|
444
483
|
month: "long",
|
|
445
484
|
year: "numeric",
|
|
446
485
|
timeZone: "UTC"
|
|
447
486
|
}).format(day(`${month}-01`));
|
|
448
487
|
const inStay = (date) => Boolean(stay && date >= stay.start && date <= stay.end);
|
|
449
|
-
const fullDate = new Intl.DateTimeFormat(locale, {
|
|
488
|
+
const fullDate = new Intl.DateTimeFormat(dateLocale(locale), {
|
|
450
489
|
weekday: "long",
|
|
451
490
|
day: "numeric",
|
|
452
491
|
month: "long",
|
|
@@ -468,7 +507,7 @@ const Calendar = ({ rentalId, stayId }) => {
|
|
|
468
507
|
"aria-label": copy.previous,
|
|
469
508
|
disabled: shown === 0,
|
|
470
509
|
onClick: () => setShown(shown - 1),
|
|
471
|
-
children: "
|
|
510
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "prev" })
|
|
472
511
|
}),
|
|
473
512
|
/* @__PURE__ */ jsxs("span", { children: [title, rental ? ` · ${rental.name}` : ""] }),
|
|
474
513
|
/* @__PURE__ */ jsx("button", {
|
|
@@ -476,7 +515,7 @@ const Calendar = ({ rentalId, stayId }) => {
|
|
|
476
515
|
"aria-label": copy.next,
|
|
477
516
|
disabled: shown >= months.length - 1,
|
|
478
517
|
onClick: () => setShown(shown + 1),
|
|
479
|
-
children: "
|
|
518
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "next" })
|
|
480
519
|
})
|
|
481
520
|
]
|
|
482
521
|
}),
|
|
@@ -523,6 +562,8 @@ const CompareColumn = ({ rentalId, stayId }) => {
|
|
|
523
562
|
const stay = useStay(stayId, rentalId);
|
|
524
563
|
const href = useBookHref(rentalId, stay);
|
|
525
564
|
if (!rental) return null;
|
|
565
|
+
const rooms = rental.rooms.slice(0, 3);
|
|
566
|
+
const highlights = rental.highlights.filter((highlight) => !rooms.includes(highlight));
|
|
526
567
|
return /* @__PURE__ */ jsxs("div", {
|
|
527
568
|
className: "compare-col",
|
|
528
569
|
children: [
|
|
@@ -531,12 +572,17 @@ const CompareColumn = ({ rentalId, stayId }) => {
|
|
|
531
572
|
/* @__PURE__ */ jsx("dt", { children: copy.compareRating }),
|
|
532
573
|
/* @__PURE__ */ jsx("dd", { children: rental.rating ? `★ ${rental.rating.value} (${counted(copy.reviews, reviewCount(rental.rating.count), locale)})` : "–" }),
|
|
533
574
|
/* @__PURE__ */ jsx("dt", { children: copy.compareRooms }),
|
|
534
|
-
/* @__PURE__ */ jsx("dd", { children:
|
|
575
|
+
/* @__PURE__ */ jsx("dd", { children: rooms.map(keepCount).join(", ") || "–" }),
|
|
535
576
|
/* @__PURE__ */ jsx("dt", { children: copy.compareHighlights }),
|
|
536
|
-
/* @__PURE__ */ jsx("dd", { children:
|
|
577
|
+
/* @__PURE__ */ jsx("dd", { children: highlights.slice(0, 4).map(keepCount).join(", ") || "–" }),
|
|
537
578
|
stay && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: copy.compareStay }), /* @__PURE__ */ jsxs("dd", { children: [
|
|
538
579
|
shortPeriod(stay, locale),
|
|
539
580
|
/* @__PURE__ */ jsx("br", {}),
|
|
581
|
+
/* @__PURE__ */ jsx("span", {
|
|
582
|
+
className: "muted",
|
|
583
|
+
children: partyText(stay, copy, locale)
|
|
584
|
+
}),
|
|
585
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
540
586
|
!stay.bookable ? /* @__PURE__ */ jsx("span", {
|
|
541
587
|
className: "muted",
|
|
542
588
|
children: copy.notBookable
|
|
@@ -546,23 +592,24 @@ const CompareColumn = ({ rentalId, stayId }) => {
|
|
|
546
592
|
})
|
|
547
593
|
] })] })
|
|
548
594
|
] }),
|
|
549
|
-
/* @__PURE__ */
|
|
595
|
+
/* @__PURE__ */ jsxs(ReplyLink, {
|
|
550
596
|
className: "button",
|
|
551
597
|
href,
|
|
552
|
-
children: stay?.bookable ? copy.
|
|
598
|
+
children: [stay?.bookable ? copy.book : copy.view, /* @__PURE__ */ jsx(LinkTarget, {
|
|
599
|
+
rentalId,
|
|
600
|
+
stay
|
|
601
|
+
})]
|
|
553
602
|
})
|
|
554
603
|
]
|
|
555
604
|
});
|
|
556
605
|
};
|
|
557
|
-
const AlternativeChip = ({
|
|
606
|
+
const AlternativeChip = ({ stay }) => {
|
|
558
607
|
const { locale } = useChat();
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
if (!stay?.bookable) return null;
|
|
562
|
-
return /* @__PURE__ */ jsxs(ReplyLink, {
|
|
608
|
+
const href = useBookHref(stay.rentalId, stay);
|
|
609
|
+
return /* @__PURE__ */ jsx(ReplyLink, {
|
|
563
610
|
className: "chip-link",
|
|
564
611
|
href,
|
|
565
|
-
children: [/* @__PURE__ */ jsx("strong", { children: shortPeriod(stay, locale) }), stay.total ? ` · ${stay.total}` : ""]
|
|
612
|
+
children: /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("strong", { children: shortPeriod(stay, locale) }), stay.total ? ` · ${stay.total}` : ""] })
|
|
566
613
|
});
|
|
567
614
|
};
|
|
568
615
|
/** One arrival of a price calendar: its bar, the PMS's total, the day; tapping asks for that stay. */
|
|
@@ -571,10 +618,8 @@ const PriceColumn = ({ rentalId, option, asked, cheapest }) => {
|
|
|
571
618
|
const rental = useRental(rentalId);
|
|
572
619
|
const stay = useStay(option.stayId, rentalId);
|
|
573
620
|
const free = Boolean(stay?.bookable && stay.total);
|
|
574
|
-
const arrival = new Intl.DateTimeFormat(locale, {
|
|
575
|
-
|
|
576
|
-
day: "numeric",
|
|
577
|
-
month: "numeric",
|
|
621
|
+
const arrival = (options) => new Intl.DateTimeFormat(dateLocale(locale), {
|
|
622
|
+
...options,
|
|
578
623
|
timeZone: "UTC"
|
|
579
624
|
}).format(day(option.start));
|
|
580
625
|
const dates = period(option, locale);
|
|
@@ -606,18 +651,45 @@ const PriceColumn = ({ rentalId, option, asked, cheapest }) => {
|
|
|
606
651
|
"aria-hidden": "true",
|
|
607
652
|
children: free ? stay.total : copy.closed
|
|
608
653
|
}),
|
|
609
|
-
/* @__PURE__ */
|
|
654
|
+
/* @__PURE__ */ jsxs("span", {
|
|
610
655
|
className: "muted",
|
|
611
656
|
"aria-hidden": "true",
|
|
612
|
-
children:
|
|
657
|
+
children: [
|
|
658
|
+
arrival({ weekday: "short" }),
|
|
659
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
660
|
+
arrival({
|
|
661
|
+
day: "numeric",
|
|
662
|
+
month: "short"
|
|
663
|
+
})
|
|
664
|
+
]
|
|
613
665
|
})
|
|
614
666
|
]
|
|
615
667
|
}) });
|
|
616
668
|
};
|
|
669
|
+
/** Which ends of a scrolled row have more beyond them, for the fade there. */
|
|
670
|
+
const hiddenEdges = (row) => {
|
|
671
|
+
const start = row.scrollLeft > 1;
|
|
672
|
+
const end = row.scrollLeft + row.clientWidth < row.scrollWidth - 1;
|
|
673
|
+
return start && end ? "both" : start ? "start" : end ? "end" : void 0;
|
|
674
|
+
};
|
|
617
675
|
const PriceCalendarView = ({ rentalId }) => {
|
|
618
676
|
const { copy, locale } = useChat();
|
|
619
677
|
const rental = useRental(rentalId);
|
|
620
678
|
const calendar = useStateValue(statePath("priceCalendars", rentalId)) ?? null;
|
|
679
|
+
const row = useRef(null);
|
|
680
|
+
const [fade, setFade] = useState();
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
const element = row.current;
|
|
683
|
+
if (!element || !calendar) return;
|
|
684
|
+
const cheapest = calendar.options.findIndex((option) => option.stayId !== null && option.stayId === calendar.cheapest);
|
|
685
|
+
const asked = calendar.options.findIndex((option) => option.start === calendar.asked);
|
|
686
|
+
const column = element.children[cheapest >= 0 ? cheapest : asked];
|
|
687
|
+
if (column) {
|
|
688
|
+
const end = column.offsetLeft + column.offsetWidth;
|
|
689
|
+
if (end > element.clientWidth) element.scrollLeft = end - element.clientWidth;
|
|
690
|
+
}
|
|
691
|
+
setFade(hiddenEdges(element));
|
|
692
|
+
}, [calendar]);
|
|
621
693
|
if (!calendar || calendar.options.length === 0) return null;
|
|
622
694
|
return /* @__PURE__ */ jsx("section", {
|
|
623
695
|
className: "card price-calendar",
|
|
@@ -634,7 +706,10 @@ const PriceCalendarView = ({ rentalId }) => {
|
|
|
634
706
|
children: [counted(copy.priceCalendarNights, calendar.nights, locale), rental ? ` · ${rental.name}` : ""]
|
|
635
707
|
}),
|
|
636
708
|
/* @__PURE__ */ jsx("ul", {
|
|
709
|
+
ref: row,
|
|
637
710
|
className: "price-columns",
|
|
711
|
+
"data-fade": fade,
|
|
712
|
+
onScroll: (event) => setFade(hiddenEdges(event.currentTarget)),
|
|
638
713
|
children: calendar.options.map((option) => /* @__PURE__ */ jsx(PriceColumn, {
|
|
639
714
|
rentalId,
|
|
640
715
|
option,
|
|
@@ -755,7 +830,7 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
755
830
|
return /* @__PURE__ */ jsx(Snapper, {
|
|
756
831
|
count: props.items.length,
|
|
757
832
|
className: "carousel",
|
|
758
|
-
label: copy.
|
|
833
|
+
label: copy.rentalsFound,
|
|
759
834
|
children: props.items.map((item) => /* @__PURE__ */ jsx(Card, {
|
|
760
835
|
rentalId: item.rentalId,
|
|
761
836
|
stayId: item.stayId
|
|
@@ -763,21 +838,17 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
763
838
|
});
|
|
764
839
|
},
|
|
765
840
|
StayQuote: ({ props }) => {
|
|
766
|
-
const { copy, locale } = useChat();
|
|
767
841
|
const stay = useStay(props.stayId);
|
|
768
842
|
const rental = useRental(stay?.rentalId ?? "");
|
|
769
843
|
if (!stay) return null;
|
|
770
844
|
return /* @__PURE__ */ jsxs("article", {
|
|
771
845
|
className: "card stay-card",
|
|
772
|
-
children: [/* @__PURE__ */
|
|
846
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
773
847
|
className: "card-body",
|
|
774
|
-
children:
|
|
848
|
+
children: rental && /* @__PURE__ */ jsx("h3", {
|
|
775
849
|
className: "card-title",
|
|
776
850
|
children: rental.name
|
|
777
|
-
})
|
|
778
|
-
className: "specs",
|
|
779
|
-
children: partyText(stay, copy, locale)
|
|
780
|
-
})]
|
|
851
|
+
})
|
|
781
852
|
}), /* @__PURE__ */ jsx(CardFoot, {
|
|
782
853
|
rentalId: stay.rentalId,
|
|
783
854
|
stay
|
|
@@ -785,18 +856,28 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
785
856
|
});
|
|
786
857
|
},
|
|
787
858
|
PriceBreakdown: ({ props }) => {
|
|
788
|
-
const { copy } = useChat();
|
|
859
|
+
const { copy, locale } = useChat();
|
|
789
860
|
const stay = useStay(props.stayId);
|
|
861
|
+
const rental = useRental(stay?.rentalId ?? "");
|
|
790
862
|
if (!stay?.bookable || !stay.total) return null;
|
|
863
|
+
const label = rental ? `${copy.priceBreakdown} · ${rental.name}` : copy.priceBreakdown;
|
|
791
864
|
return /* @__PURE__ */ jsx("section", {
|
|
792
865
|
className: "card breakdown",
|
|
793
|
-
"aria-label":
|
|
866
|
+
"aria-label": label,
|
|
794
867
|
children: /* @__PURE__ */ jsxs("div", {
|
|
795
868
|
className: "card-body",
|
|
796
869
|
children: [
|
|
797
870
|
/* @__PURE__ */ jsx("span", {
|
|
798
871
|
className: "muted",
|
|
799
|
-
children:
|
|
872
|
+
children: label
|
|
873
|
+
}),
|
|
874
|
+
/* @__PURE__ */ jsxs("span", {
|
|
875
|
+
className: "foot-meta",
|
|
876
|
+
children: [
|
|
877
|
+
stayMeta(stay, copy, locale),
|
|
878
|
+
" · ",
|
|
879
|
+
partyText(stay, copy, locale)
|
|
880
|
+
]
|
|
800
881
|
}),
|
|
801
882
|
stay.lines.map((line) => /* @__PURE__ */ jsxs("div", {
|
|
802
883
|
className: "line",
|
|
@@ -812,14 +893,26 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
812
893
|
},
|
|
813
894
|
AlternativeStays: ({ props }) => {
|
|
814
895
|
const { copy } = useChat();
|
|
815
|
-
|
|
896
|
+
const rental = useRental(props.rentalId);
|
|
897
|
+
const stays = useStateValue("/stays") ?? {};
|
|
898
|
+
const bookable = props.stayIds.flatMap((stayId) => {
|
|
899
|
+
const stay = stays[stayId];
|
|
900
|
+
return stay?.rentalId === props.rentalId && stay.bookable ? [stay] : [];
|
|
901
|
+
});
|
|
902
|
+
if (bookable.length === 0) return null;
|
|
903
|
+
const label = rental ? `${copy.otherDates} · ${rental.name}` : copy.otherDates;
|
|
904
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
816
905
|
className: "alternatives",
|
|
817
906
|
role: "group",
|
|
818
|
-
"aria-label":
|
|
819
|
-
children:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
907
|
+
"aria-label": label,
|
|
908
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
909
|
+
className: "alt-caption",
|
|
910
|
+
"aria-hidden": "true",
|
|
911
|
+
children: label
|
|
912
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
913
|
+
className: "alt-chips",
|
|
914
|
+
children: bookable.map((stay) => /* @__PURE__ */ jsx(AlternativeChip, { stay }, stay.id))
|
|
915
|
+
})]
|
|
823
916
|
});
|
|
824
917
|
},
|
|
825
918
|
AvailabilityCalendar: ({ props }) => /* @__PURE__ */ jsx(Calendar, {
|
|
@@ -860,12 +953,12 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
860
953
|
if (!send) return null;
|
|
861
954
|
return /* @__PURE__ */ jsx("div", {
|
|
862
955
|
className: "quick-replies",
|
|
863
|
-
children: props.options.map((option) => /* @__PURE__ */
|
|
956
|
+
children: props.options.map((option) => /* @__PURE__ */ jsx("button", {
|
|
864
957
|
type: "button",
|
|
865
958
|
className: "chip",
|
|
866
959
|
disabled: busy,
|
|
867
960
|
onClick: () => send(option),
|
|
868
|
-
children:
|
|
961
|
+
children: option
|
|
869
962
|
}, option))
|
|
870
963
|
});
|
|
871
964
|
}
|
|
@@ -951,19 +1044,45 @@ const Consent = ({ copy, pending, onAccept, buttonRef }) => /* @__PURE__ */ jsxs
|
|
|
951
1044
|
})
|
|
952
1045
|
]
|
|
953
1046
|
});
|
|
1047
|
+
/** Shown until the reply's text: a search takes seconds, and three dots alone read as nothing.
|
|
1048
|
+
* Hidden from a screen reader: inside the busy log it may go unannounced, so the panel's own status,
|
|
1049
|
+
* outside the log, says it. */
|
|
954
1050
|
const Thinking = ({ label }) => /* @__PURE__ */ jsxs("div", {
|
|
955
1051
|
className: "thinking",
|
|
956
|
-
|
|
1052
|
+
"aria-hidden": "true",
|
|
957
1053
|
children: [
|
|
958
1054
|
/* @__PURE__ */ jsx("i", {}),
|
|
959
1055
|
/* @__PURE__ */ jsx("i", {}),
|
|
960
1056
|
/* @__PURE__ */ jsx("i", {}),
|
|
961
1057
|
/* @__PURE__ */ jsx("span", {
|
|
962
|
-
className: "
|
|
1058
|
+
className: "thinking-label",
|
|
963
1059
|
children: label
|
|
964
1060
|
})
|
|
965
1061
|
]
|
|
966
1062
|
});
|
|
1063
|
+
/** Why the chat cannot answer, as its reply to the visitor's message, and where to go instead. */
|
|
1064
|
+
const Fallback = ({ reason, copy, searchPath, contactPath }) => /* @__PURE__ */ jsxs("div", {
|
|
1065
|
+
className: "fallback",
|
|
1066
|
+
role: "status",
|
|
1067
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1068
|
+
className: "message",
|
|
1069
|
+
"data-role": "assistant",
|
|
1070
|
+
children: reason === "busy" ? copy.busy : copy.unavailable
|
|
1071
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1072
|
+
className: "fallback-links",
|
|
1073
|
+
children: [/* @__PURE__ */ jsx("a", {
|
|
1074
|
+
className: "button",
|
|
1075
|
+
href: searchPath,
|
|
1076
|
+
children: copy.searchLink
|
|
1077
|
+
}), /* @__PURE__ */ jsx("a", {
|
|
1078
|
+
className: "link",
|
|
1079
|
+
href: contactPath,
|
|
1080
|
+
children: copy.contactLink
|
|
1081
|
+
})]
|
|
1082
|
+
})]
|
|
1083
|
+
});
|
|
1084
|
+
/** A refusal that a second try can get past: the message goes back into the field to send again. */
|
|
1085
|
+
const RETRYABLE = /* @__PURE__ */ new Set(["busy", "error"]);
|
|
967
1086
|
function Panel(props) {
|
|
968
1087
|
const { copy, endpoint, open, pending, onSent, onStarted, onClose } = props;
|
|
969
1088
|
const stored = useMemo(() => loadConversation(endpoint), [endpoint]);
|
|
@@ -974,19 +1093,39 @@ function Panel(props) {
|
|
|
974
1093
|
const [unavailable, setUnavailable] = useState(null);
|
|
975
1094
|
const session = useRef(stored?.sessionId ?? null);
|
|
976
1095
|
const abort = useRef(null);
|
|
977
|
-
const
|
|
1096
|
+
const list = useRef(null);
|
|
1097
|
+
const following = useRef(true);
|
|
1098
|
+
const lastRole = useRef(stored?.messages.at(-1)?.role);
|
|
978
1099
|
const start = useRef(null);
|
|
979
1100
|
const composer = useRef(null);
|
|
980
1101
|
const panel = useRef(null);
|
|
981
1102
|
const lastActivity = useRef(stored?.savedAt ?? Date.now());
|
|
982
1103
|
const hasMessages = messages.length > 0;
|
|
1104
|
+
const last = messages.at(-1);
|
|
1105
|
+
const waiting = streaming && !unavailable && (last?.role === "visitor" || !last?.parts.some((part) => part.kind === "text"));
|
|
983
1106
|
const expired = () => Date.now() - lastActivity.current >= SESSION_TTL_S * 1e3;
|
|
984
1107
|
useEffect(() => () => abort.current?.abort(), []);
|
|
985
1108
|
useEffect(() => {
|
|
986
|
-
|
|
987
|
-
|
|
1109
|
+
const role = messages.at(-1)?.role;
|
|
1110
|
+
const replyStarts = role === "assistant" && lastRole.current === "visitor";
|
|
1111
|
+
lastRole.current = role;
|
|
1112
|
+
const element = list.current;
|
|
1113
|
+
if (!open || !element) return;
|
|
1114
|
+
const turnTop = ([...element.querySelectorAll(":scope > .turn")].at(-1)?.offsetTop ?? 0) - 12;
|
|
1115
|
+
if (replyStarts) element.scrollTop = turnTop;
|
|
1116
|
+
else if (role === "assistant" && following.current) element.scrollTop = Math.max(element.scrollTop, Math.min(element.scrollHeight - element.clientHeight, turnTop));
|
|
1117
|
+
else if (role === "visitor" || following.current) element.scrollTop = element.scrollHeight;
|
|
1118
|
+
}, [
|
|
1119
|
+
messages,
|
|
1120
|
+
open,
|
|
1121
|
+
waiting,
|
|
1122
|
+
unavailable
|
|
1123
|
+
]);
|
|
988
1124
|
useEffect(() => {
|
|
989
|
-
if (open)
|
|
1125
|
+
if (!open) return;
|
|
1126
|
+
const touch = window.matchMedia?.("(pointer: coarse)").matches;
|
|
1127
|
+
if (accepted && hasMessages && touch) panel.current?.focus();
|
|
1128
|
+
else (accepted ? composer : start).current?.focus();
|
|
990
1129
|
}, [accepted, open]);
|
|
991
1130
|
useEffect(() => {
|
|
992
1131
|
onStarted(hasMessages);
|
|
@@ -1012,16 +1151,21 @@ function Panel(props) {
|
|
|
1012
1151
|
clearConversation(endpoint);
|
|
1013
1152
|
}
|
|
1014
1153
|
setDraft("");
|
|
1154
|
+
const restoreDraft = () => setDraft((current) => current || message);
|
|
1015
1155
|
setUnavailable(null);
|
|
1016
1156
|
setStreaming(true);
|
|
1017
|
-
setMessages((current) =>
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1157
|
+
setMessages((current) => {
|
|
1158
|
+
const before = fresh ? [] : current;
|
|
1159
|
+
const last = before.at(-1);
|
|
1160
|
+
return [...last?.role === "visitor" && last.unconfirmed && last.parts[0]?.kind === "text" && last.parts[0].text === message ? before.slice(0, -1) : before, {
|
|
1161
|
+
role: "visitor",
|
|
1162
|
+
parts: [{
|
|
1163
|
+
kind: "text",
|
|
1164
|
+
text: message
|
|
1165
|
+
}],
|
|
1166
|
+
unconfirmed: true
|
|
1167
|
+
}];
|
|
1168
|
+
});
|
|
1025
1169
|
const sentAt = Date.now();
|
|
1026
1170
|
const controller = new AbortController();
|
|
1027
1171
|
abort.current = controller;
|
|
@@ -1042,14 +1186,19 @@ function Panel(props) {
|
|
|
1042
1186
|
kind: "ui",
|
|
1043
1187
|
spec: chatEvent.spec
|
|
1044
1188
|
}));
|
|
1045
|
-
else if (chatEvent.type === "unavailable")
|
|
1046
|
-
|
|
1189
|
+
else if (chatEvent.type === "unavailable") {
|
|
1190
|
+
setUnavailable(chatEvent.reason);
|
|
1191
|
+
if (RETRYABLE.has(chatEvent.reason)) restoreDraft();
|
|
1192
|
+
} else if (chatEvent.type === "done") {
|
|
1047
1193
|
lastActivity.current = sentAt;
|
|
1048
1194
|
setMessages(confirmed);
|
|
1049
1195
|
}
|
|
1050
1196
|
}, controller.signal);
|
|
1051
1197
|
} catch {
|
|
1052
|
-
if (!controller.signal.aborted)
|
|
1198
|
+
if (!controller.signal.aborted) {
|
|
1199
|
+
setUnavailable("error");
|
|
1200
|
+
restoreDraft();
|
|
1201
|
+
}
|
|
1053
1202
|
} finally {
|
|
1054
1203
|
if (abort.current === controller) setStreaming(false);
|
|
1055
1204
|
}
|
|
@@ -1094,7 +1243,8 @@ function Panel(props) {
|
|
|
1094
1243
|
const focusable = [...element.querySelectorAll("a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled])")];
|
|
1095
1244
|
const active = element.getRootNode().activeElement;
|
|
1096
1245
|
const edge = event.shiftKey ? focusable[0] : focusable.at(-1);
|
|
1097
|
-
|
|
1246
|
+
const outside = !element.contains(active) || event.shiftKey && active === element;
|
|
1247
|
+
if (focusable.length === 0 || active === edge || outside) {
|
|
1098
1248
|
event.preventDefault();
|
|
1099
1249
|
(event.shiftKey ? focusable.at(-1) : focusable[0])?.focus();
|
|
1100
1250
|
}
|
|
@@ -1103,7 +1253,6 @@ function Panel(props) {
|
|
|
1103
1253
|
event.preventDefault();
|
|
1104
1254
|
submit(draft);
|
|
1105
1255
|
};
|
|
1106
|
-
const waiting = streaming && messages.at(-1)?.role === "visitor";
|
|
1107
1256
|
return /* @__PURE__ */ jsxs(ChatContext.Provider, {
|
|
1108
1257
|
value: {
|
|
1109
1258
|
...props,
|
|
@@ -1121,6 +1270,7 @@ function Panel(props) {
|
|
|
1121
1270
|
"aria-modal": "true",
|
|
1122
1271
|
"aria-label": copy.title,
|
|
1123
1272
|
ref: panel,
|
|
1273
|
+
tabIndex: -1,
|
|
1124
1274
|
hidden: !open,
|
|
1125
1275
|
inert: !open,
|
|
1126
1276
|
onKeyDown: (event) => {
|
|
@@ -1156,50 +1306,50 @@ function Panel(props) {
|
|
|
1156
1306
|
className: "notice",
|
|
1157
1307
|
children: copy.notice
|
|
1158
1308
|
}),
|
|
1159
|
-
accepted ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1309
|
+
accepted ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1310
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1311
|
+
className: "messages",
|
|
1312
|
+
role: "log",
|
|
1313
|
+
"aria-busy": streaming,
|
|
1314
|
+
ref: list,
|
|
1315
|
+
onScroll: (event) => {
|
|
1316
|
+
const element = event.currentTarget;
|
|
1317
|
+
following.current = element.scrollHeight - element.scrollTop - element.clientHeight < 80;
|
|
1318
|
+
},
|
|
1319
|
+
children: [
|
|
1320
|
+
/* @__PURE__ */ jsx("div", {
|
|
1171
1321
|
className: "message",
|
|
1172
|
-
"data-role":
|
|
1173
|
-
children:
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
!hasMessages && copy.suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1322
|
+
"data-role": "assistant",
|
|
1323
|
+
children: copy.greeting
|
|
1324
|
+
}),
|
|
1325
|
+
messages.map((message, index) => /* @__PURE__ */ jsxs("div", {
|
|
1326
|
+
className: message.role === "assistant" ? "ui" : "turn",
|
|
1327
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1328
|
+
className: "sr-only",
|
|
1329
|
+
children: [message.role === "assistant" ? copy.title : copy.yourMessage, ":"]
|
|
1330
|
+
}), message.parts.map((part, partIndex) => part.kind === "text" ? /* @__PURE__ */ jsx("div", {
|
|
1331
|
+
className: "message",
|
|
1332
|
+
"data-role": message.role,
|
|
1333
|
+
children: message.role === "assistant" ? /* @__PURE__ */ jsx(RichText, { text: part.text }) : part.text
|
|
1334
|
+
}, partIndex) : /* @__PURE__ */ jsx(SpecView, { spec: part.spec }, partIndex))]
|
|
1335
|
+
}, index)),
|
|
1336
|
+
unavailable && /* @__PURE__ */ jsx(Fallback, {
|
|
1337
|
+
reason: unavailable,
|
|
1338
|
+
copy,
|
|
1339
|
+
searchPath: props.searchPath,
|
|
1340
|
+
contactPath: props.contactPath
|
|
1341
|
+
}),
|
|
1342
|
+
waiting && /* @__PURE__ */ jsx(Thinking, { label: copy.thinking })
|
|
1343
|
+
]
|
|
1344
|
+
}),
|
|
1345
|
+
/* @__PURE__ */ jsx("p", {
|
|
1346
|
+
className: "sr-only",
|
|
1347
|
+
role: "status",
|
|
1348
|
+
children: waiting ? copy.thinking : ""
|
|
1349
|
+
}),
|
|
1350
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1351
|
+
className: "dock",
|
|
1352
|
+
children: [!hasMessages && copy.suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1203
1353
|
className: "suggestions",
|
|
1204
1354
|
children: copy.suggestions.map((suggestion) => /* @__PURE__ */ jsxs("button", {
|
|
1205
1355
|
type: "button",
|
|
@@ -1207,8 +1357,7 @@ function Panel(props) {
|
|
|
1207
1357
|
onClick: () => void submit(suggestion),
|
|
1208
1358
|
children: [/* @__PURE__ */ jsx(TopicIcon, { text: suggestion }), suggestion]
|
|
1209
1359
|
}, suggestion))
|
|
1210
|
-
}),
|
|
1211
|
-
/* @__PURE__ */ jsxs("form", {
|
|
1360
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
1212
1361
|
className: "composer",
|
|
1213
1362
|
onSubmit: send,
|
|
1214
1363
|
children: [/* @__PURE__ */ jsx("textarea", {
|
|
@@ -1231,9 +1380,9 @@ function Panel(props) {
|
|
|
1231
1380
|
disabled: streaming || !draft.trim(),
|
|
1232
1381
|
children: /* @__PURE__ */ jsx(Arrow, {})
|
|
1233
1382
|
})]
|
|
1234
|
-
})
|
|
1235
|
-
|
|
1236
|
-
|
|
1383
|
+
})]
|
|
1384
|
+
})
|
|
1385
|
+
] }) : /* @__PURE__ */ jsx(Consent, {
|
|
1237
1386
|
copy,
|
|
1238
1387
|
pending,
|
|
1239
1388
|
onAccept: () => setAccepted(true),
|