@sevenfold/setto-client 0.26.0 → 0.28.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.
@@ -5899,21 +5899,21 @@ var PostgresTypes;
5899
5899
  PostgresTypes2["tsrange"] = "tsrange";
5900
5900
  PostgresTypes2["tstzrange"] = "tstzrange";
5901
5901
  })(PostgresTypes || (PostgresTypes = {}));
5902
- const convertChangeData = (columns, record, options = {}) => {
5902
+ const convertChangeData = (columns, record2, options = {}) => {
5903
5903
  var _a;
5904
5904
  const skipTypes = (_a = options.skipTypes) !== null && _a !== void 0 ? _a : [];
5905
- if (!record) {
5905
+ if (!record2) {
5906
5906
  return {};
5907
5907
  }
5908
- return Object.keys(record).reduce((acc, rec_key) => {
5909
- acc[rec_key] = convertColumn(rec_key, columns, record, skipTypes);
5908
+ return Object.keys(record2).reduce((acc, rec_key) => {
5909
+ acc[rec_key] = convertColumn(rec_key, columns, record2, skipTypes);
5910
5910
  return acc;
5911
5911
  }, {});
5912
5912
  };
5913
- const convertColumn = (columnName, columns, record, skipTypes) => {
5913
+ const convertColumn = (columnName, columns, record2, skipTypes) => {
5914
5914
  const column = columns.find((x) => x.name === columnName);
5915
5915
  const colType = column === null || column === void 0 ? void 0 : column.type;
5916
- const value = record[columnName];
5916
+ const value = record2[columnName];
5917
5917
  if (colType && !skipTypes.includes(colType)) {
5918
5918
  return convertCell(colType, value);
5919
5919
  }
@@ -34654,6 +34654,139 @@ function remarkGfm(options) {
34654
34654
  fromMarkdownExtensions.push(gfmFromMarkdown());
34655
34655
  toMarkdownExtensions.push(gfmToMarkdown(settings));
34656
34656
  }
34657
+ const POLL_FALLBACK_MS = 5e3;
34658
+ const POLL_SUBSCRIBED_MS = 3e4;
34659
+ const CHANGED_DEBOUNCE_MS = 250;
34660
+ const TYPING_THROTTLE_MS = 2500;
34661
+ const TYPING_EXPIRY_MS = 6e3;
34662
+ function pollIntervalMs(subscribed) {
34663
+ return subscribed ? POLL_SUBSCRIBED_MS : POLL_FALLBACK_MS;
34664
+ }
34665
+ function conversationTopic(value) {
34666
+ return typeof value === "string" && value.length <= 200 && /^chat-conversation:\S+$/.test(value) ? value : null;
34667
+ }
34668
+ function isSide(value) {
34669
+ return value === "team" || value === "visitor";
34670
+ }
34671
+ function record(payload) {
34672
+ return payload && typeof payload === "object" ? payload : null;
34673
+ }
34674
+ function parseTypingSignal(payload) {
34675
+ const value = record(payload);
34676
+ if (!value || !isSide(value.side) || typeof value.typing !== "boolean") return null;
34677
+ return { side: value.side, typing: value.typing };
34678
+ }
34679
+ function parseReadSignal(payload) {
34680
+ const value = record(payload);
34681
+ if (!value || !isSide(value.side) || typeof value.at !== "string") return null;
34682
+ return timestampValue(value.at) === null ? null : { side: value.side, at: value.at };
34683
+ }
34684
+ const TIMESTAMP = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(:\d{2})?(?:\.(\d+))?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
34685
+ function timestampValue(value) {
34686
+ if (!value) return null;
34687
+ const match = TIMESTAMP.exec(value.trim());
34688
+ if (!match) {
34689
+ const parsed = Date.parse(value);
34690
+ return Number.isFinite(parsed) ? parsed : null;
34691
+ }
34692
+ const [, date, minutes, seconds = ":00", fraction, zone] = match;
34693
+ let offset = "Z";
34694
+ if (zone && zone.toUpperCase() !== "Z") {
34695
+ const digits = zone.slice(1).replace(":", "");
34696
+ offset = `${zone[0]}${digits.slice(0, 2)}:${digits.slice(2, 4) || "00"}`;
34697
+ }
34698
+ const whole = Date.parse(`${date}T${minutes}${seconds}${offset}`);
34699
+ if (!Number.isFinite(whole)) return null;
34700
+ return fraction ? whole + Number(`0.${fraction}`) * 1e3 : whole;
34701
+ }
34702
+ function laterTimestamp(current, next) {
34703
+ const nextValue = timestampValue(next);
34704
+ if (nextValue === null) return current;
34705
+ const currentValue = timestampValue(current);
34706
+ return currentValue === null || nextValue > currentValue ? next : current;
34707
+ }
34708
+ function createTypingSender(send, now = Date.now) {
34709
+ let typing = false;
34710
+ let lastSentAt = Number.NEGATIVE_INFINITY;
34711
+ function reset() {
34712
+ typing = false;
34713
+ lastSentAt = Number.NEGATIVE_INFINITY;
34714
+ }
34715
+ function stop() {
34716
+ if (!typing) return;
34717
+ reset();
34718
+ send(false);
34719
+ }
34720
+ function update(draft) {
34721
+ if (!draft.trim()) {
34722
+ stop();
34723
+ return;
34724
+ }
34725
+ const at = now();
34726
+ if (typing && at - lastSentAt < TYPING_THROTTLE_MS) return;
34727
+ if (send(true)) {
34728
+ typing = true;
34729
+ lastSentAt = at;
34730
+ }
34731
+ }
34732
+ return { update, stop, reset };
34733
+ }
34734
+ function nextTeamTypingDeadline(current, signal, now) {
34735
+ if (signal.side !== "team") return current;
34736
+ return signal.typing ? now + TYPING_EXPIRY_MS : null;
34737
+ }
34738
+ function latestHumanMessageKey(messages) {
34739
+ for (let index2 = messages.length - 1; index2 >= 0; index2 -= 1) {
34740
+ const message = messages[index2];
34741
+ if (message?.role === "human") return message.id ?? message.created_at ?? null;
34742
+ }
34743
+ return null;
34744
+ }
34745
+ function teamTypingIdentity(messages) {
34746
+ for (let index2 = messages.length - 1; index2 >= 0; index2 -= 1) {
34747
+ const message = messages[index2];
34748
+ if (message && (message.role === "human" || message.kind === "agent_joined")) {
34749
+ return { name: message.authorName?.trim() || null, avatarUrl: message.avatarUrl || null };
34750
+ }
34751
+ }
34752
+ return { name: null, avatarUrl: null };
34753
+ }
34754
+ function teamTypingLabel(norwegian2, name2) {
34755
+ if (norwegian2) return name2 ? `${name2} skriver …` : "Teamet skriver …";
34756
+ return name2 ? `${name2} is typing …` : "The team is typing …";
34757
+ }
34758
+ function seenVisitorMessageIndex(messages, humanJoined, teamReadAt) {
34759
+ const readValue = humanJoined ? timestampValue(teamReadAt) : null;
34760
+ if (readValue === null) return -1;
34761
+ for (let index2 = messages.length - 1; index2 >= 0; index2 -= 1) {
34762
+ if (messages[index2]?.role !== "visitor") continue;
34763
+ const created = timestampValue(messages[index2]?.created_at);
34764
+ return created !== null && created <= readValue ? index2 : -1;
34765
+ }
34766
+ return -1;
34767
+ }
34768
+ function readMarkerThrough(messages, context) {
34769
+ if (!context.humanJoined || !context.chatOpen || !context.pageVisible) return null;
34770
+ let newest = null;
34771
+ let newestValue = Number.NEGATIVE_INFINITY;
34772
+ for (const message of messages) {
34773
+ if (message.role === "visitor") continue;
34774
+ const value = timestampValue(message.created_at);
34775
+ if (value !== null && value > newestValue) {
34776
+ newest = message.created_at;
34777
+ newestValue = value;
34778
+ }
34779
+ }
34780
+ if (newest === null) return null;
34781
+ const sentValue = timestampValue(context.lastSent);
34782
+ return sentValue !== null && newestValue <= sentValue ? null : newest;
34783
+ }
34784
+ function refreshOutcome(started, current) {
34785
+ if (started.conversationId !== current.conversationId) return "drop";
34786
+ if (started.request !== current.request) return "drop";
34787
+ if (current.busy || started.turn !== current.turn) return "defer";
34788
+ return "apply";
34789
+ }
34657
34790
  const VISITOR_KEY = "setto-chat-visitor";
34658
34791
  const ACTIVE_CONVERSATION_KEY = "setto-chat-active";
34659
34792
  const NAVIGATION_KEY = "setto-chat-navigation";
@@ -34843,6 +34976,25 @@ function AgentJoinedEvent({ message }) {
34843
34976
  /* @__PURE__ */ jsx("div", { className: "setto-chat__message-stack", children: /* @__PURE__ */ jsx("div", { className: "setto-chat__message-bubble", children: /* @__PURE__ */ jsx("span", { className: "setto-chat__message-copy", children: message.content }) }) })
34844
34977
  ] });
34845
34978
  }
34979
+ function TeamTypingRow({ label, avatarUrl }) {
34980
+ return /* @__PURE__ */ jsxs(
34981
+ "div",
34982
+ {
34983
+ className: "setto-chat__message setto-chat__message--human setto-chat__message--typing",
34984
+ "aria-live": "off",
34985
+ children: [
34986
+ /* @__PURE__ */ jsx("span", { className: "setto-chat__avatar setto-chat__avatar--human", "aria-hidden": "true", children: avatarUrl ? /* @__PURE__ */ jsx("img", { src: avatarUrl, alt: "" }) : /* @__PURE__ */ jsx(PersonIcon, {}) }),
34987
+ /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-stack", children: [
34988
+ /* @__PURE__ */ jsx("span", { className: "setto-chat__speaker", children: label }),
34989
+ /* @__PURE__ */ jsx("div", { className: "setto-chat__message-bubble", children: /* @__PURE__ */ jsx("div", { className: "setto-chat__message-copy", children: /* @__PURE__ */ jsx("span", { className: "setto-chat__typing", "aria-hidden": "true", children: "···" }) }) })
34990
+ ] })
34991
+ ]
34992
+ }
34993
+ );
34994
+ }
34995
+ function SeenMarker({ label }) {
34996
+ return /* @__PURE__ */ jsx("span", { className: "setto-chat__receipt", "aria-live": "off", children: label });
34997
+ }
34846
34998
  function StaggeredText({ children }) {
34847
34999
  return Children.map(children, (child) => {
34848
35000
  if (typeof child !== "string") return child;
@@ -34884,6 +35036,7 @@ function SiteChatComposer({
34884
35036
  value,
34885
35037
  onChange,
34886
35038
  onSubmit,
35039
+ onBlur,
34887
35040
  busy,
34888
35041
  queued,
34889
35042
  placeholder,
@@ -34964,7 +35117,10 @@ function SiteChatComposer({
34964
35117
  onChange: (event) => onChange(event.target.value),
34965
35118
  onKeyDown: handleKeyDown,
34966
35119
  onFocus: () => setFocused(true),
34967
- onBlur: () => setFocused(false),
35120
+ onBlur: () => {
35121
+ setFocused(false);
35122
+ onBlur?.();
35123
+ },
34968
35124
  placeholder,
34969
35125
  rows: 1,
34970
35126
  maxLength: 1500
@@ -35235,8 +35391,70 @@ function useChatAutoScroll(active, logRef, pinToBottomRef, programmaticScrollRef
35235
35391
  };
35236
35392
  }, [active, logRef, pinToBottomRef, programmaticScrollRef]);
35237
35393
  }
35394
+ function usePageVisible() {
35395
+ const [pageVisible, setPageVisible] = useState(
35396
+ () => typeof document === "undefined" || document.visibilityState !== "hidden"
35397
+ );
35398
+ useEffect(() => {
35399
+ const update = () => setPageVisible(document.visibilityState !== "hidden");
35400
+ update();
35401
+ document.addEventListener("visibilitychange", update);
35402
+ return () => document.removeEventListener("visibilitychange", update);
35403
+ }, []);
35404
+ return pageVisible;
35405
+ }
35406
+ const channelRemovals = /* @__PURE__ */ new Map();
35407
+ function useConversationChannel(supabase, topic, handlers2) {
35408
+ const handlersRef = useRef(handlers2);
35409
+ const channelRef = useRef(null);
35410
+ useEffect(() => {
35411
+ handlersRef.current = handlers2;
35412
+ });
35413
+ useEffect(() => {
35414
+ if (!topic) return;
35415
+ let active = true;
35416
+ let channel = null;
35417
+ let changedTimer = 0;
35418
+ const join2 = () => {
35419
+ if (!active) return;
35420
+ channel = supabase.channel(topic).on("broadcast", { event: "changed" }, () => {
35421
+ window.clearTimeout(changedTimer);
35422
+ changedTimer = window.setTimeout(
35423
+ () => handlersRef.current.onChanged(),
35424
+ CHANGED_DEBOUNCE_MS
35425
+ );
35426
+ }).on("broadcast", { event: "read" }, (message) => {
35427
+ const signal = parseReadSignal(message.payload);
35428
+ if (signal) handlersRef.current.onRead(signal);
35429
+ }).on("broadcast", { event: "typing" }, (message) => {
35430
+ const signal = parseTypingSignal(message.payload);
35431
+ if (signal) handlersRef.current.onTyping(signal);
35432
+ });
35433
+ channelRef.current = channel;
35434
+ channel.subscribe((status) => {
35435
+ if (active) handlersRef.current.onStatus(status === "SUBSCRIBED");
35436
+ });
35437
+ };
35438
+ const previousRemoval = channelRemovals.get(topic);
35439
+ if (previousRemoval) void previousRemoval.then(join2);
35440
+ else join2();
35441
+ return () => {
35442
+ active = false;
35443
+ window.clearTimeout(changedTimer);
35444
+ if (!channel) return;
35445
+ handlersRef.current.onLeave();
35446
+ channelRef.current = null;
35447
+ handlersRef.current.onStatus(false);
35448
+ const removal = supabase.removeChannel(channel).catch(() => void 0).finally(() => {
35449
+ if (channelRemovals.get(topic) === removal) channelRemovals.delete(topic);
35450
+ });
35451
+ channelRemovals.set(topic, removal);
35452
+ };
35453
+ }, [supabase, topic]);
35454
+ return channelRef;
35455
+ }
35238
35456
  function SettoChat() {
35239
- const { config } = useSetto();
35457
+ const { config, supabase } = useSetto();
35240
35458
  const { i18n } = useTranslation();
35241
35459
  const apiUrl = config.apiUrl.replace(/\/+$/, "");
35242
35460
  const [settings, setSettings] = useState(null);
@@ -35262,9 +35480,53 @@ function SettoChat() {
35262
35480
  const pinToBottomRef = useRef(true);
35263
35481
  const programmaticScrollRef = useRef(false);
35264
35482
  const filesRef = useRef([]);
35483
+ const [realtime, setRealtime] = useState(null);
35484
+ const [subscribed, setSubscribed] = useState(false);
35485
+ const [teamTypingUntil, setTeamTypingUntil] = useState(null);
35486
+ const subscribedRef = useRef(false);
35487
+ const turnRef = useRef({ busy: false, generation: 0 });
35488
+ const refreshRequestRef = useRef(0);
35489
+ const pendingRefreshRef = useRef(false);
35490
+ const loadedConversationRef = useRef(null);
35491
+ const latestHumanRef = useRef(null);
35492
+ const readMarkersRef = useRef(/* @__PURE__ */ new Map());
35493
+ const refreshRef = useRef(async () => void 0);
35494
+ const typingSenderRef = useRef(null);
35265
35495
  const formId = useId();
35266
35496
  const palette = useMemo(() => chatPalette(config.brandColors), [config.brandColors]);
35267
35497
  const desktopPopup = useDesktopPopup();
35498
+ const pageVisible = usePageVisible();
35499
+ const channelRef = useConversationChannel(supabase, mounted ? realtime?.topic ?? null : null, {
35500
+ onStatus(next) {
35501
+ subscribedRef.current = next;
35502
+ setSubscribed(next);
35503
+ if (next) void refreshRef.current();
35504
+ else setTeamTypingUntil(null);
35505
+ },
35506
+ onChanged() {
35507
+ void refreshRef.current();
35508
+ },
35509
+ onRead(signal) {
35510
+ if (signal.side !== "team") return;
35511
+ setRealtime(
35512
+ (current) => current ? { ...current, teamReadAt: laterTimestamp(current.teamReadAt, signal.at) } : current
35513
+ );
35514
+ },
35515
+ onTyping(signal) {
35516
+ setTeamTypingUntil((current) => nextTeamTypingDeadline(current, signal, Date.now()));
35517
+ },
35518
+ onLeave() {
35519
+ typingSender().stop();
35520
+ }
35521
+ });
35522
+ function typingSender() {
35523
+ return typingSenderRef.current ??= createTypingSender((typing) => {
35524
+ const channel = channelRef.current;
35525
+ if (!channel || !subscribedRef.current || channel.state !== "joined") return false;
35526
+ void channel.send({ type: "broadcast", event: "typing", payload: { side: "visitor", typing } }).catch(() => void 0);
35527
+ return true;
35528
+ });
35529
+ }
35268
35530
  useChatStyles();
35269
35531
  useChatViewport(mounted && !desktopPopup, logRef, pinToBottomRef, programmaticScrollRef);
35270
35532
  useChatPinTracking(mounted, logRef, pinToBottomRef, programmaticScrollRef);
@@ -35288,6 +35550,7 @@ function SettoChat() {
35288
35550
  failed: "Jeg får ikke svart akkurat nå. Kontakt gjerne teamet direkte.",
35289
35551
  image: "Bilde",
35290
35552
  team: "Teamet",
35553
+ seen: "Sett",
35291
35554
  emptyChats: "Nye samtaler du starter her vises i denne listen."
35292
35555
  } : {
35293
35556
  open: siteName ? `Open chat with ${siteName}` : "Open chat",
@@ -35304,6 +35567,7 @@ function SettoChat() {
35304
35567
  failed: "I cannot reply right now. Please contact the team directly.",
35305
35568
  image: "Image",
35306
35569
  team: "The team",
35570
+ seen: "Seen",
35307
35571
  emptyChats: "New conversations you start here will appear in this list."
35308
35572
  };
35309
35573
  function ensureVisitor() {
@@ -35318,9 +35582,24 @@ function SettoChat() {
35318
35582
  }
35319
35583
  }
35320
35584
  function applyConversation(next) {
35585
+ const sameConversation = loadedConversationRef.current === next.conversationId;
35321
35586
  rememberConversation(next.conversationId);
35587
+ loadedConversationRef.current = next.conversationId;
35322
35588
  setHumanJoined(next.humanJoined);
35323
35589
  setMessages(next.messages);
35590
+ const supported = "realtimeTopic" in next || "teamReadAt" in next || "visitorReadAt" in next;
35591
+ const serverTeamReadAt = typeof next.teamReadAt === "string" ? next.teamReadAt : null;
35592
+ setRealtime((current) => ({
35593
+ conversationId: next.conversationId,
35594
+ supported,
35595
+ topic: conversationTopic(next.realtimeTopic),
35596
+ teamReadAt: current?.conversationId === next.conversationId ? laterTimestamp(current.teamReadAt, serverTeamReadAt) : serverTeamReadAt
35597
+ }));
35598
+ const latestHuman = latestHumanMessageKey(next.messages);
35599
+ if (!sameConversation || latestHuman && latestHuman !== latestHumanRef.current) {
35600
+ setTeamTypingUntil(null);
35601
+ }
35602
+ latestHumanRef.current = latestHuman;
35324
35603
  const navigation = [...next.messages].reverse().find((message) => message.kind === "navigation" && message.action?.type === "navigate");
35325
35604
  if (!navigation?.id || !navigation.action?.url) return;
35326
35605
  try {
@@ -35348,9 +35627,51 @@ function SettoChat() {
35348
35627
  } catch {
35349
35628
  }
35350
35629
  }
35630
+ async function refreshConversation() {
35631
+ const id = conversationId.current;
35632
+ const visitorIdValue = visitor.current;
35633
+ if (!id || !visitorIdValue) return;
35634
+ if (turnRef.current.busy) {
35635
+ pendingRefreshRef.current = true;
35636
+ return;
35637
+ }
35638
+ pendingRefreshRef.current = false;
35639
+ refreshRequestRef.current += 1;
35640
+ const ticket = {
35641
+ conversationId: id,
35642
+ turn: turnRef.current.generation,
35643
+ request: refreshRequestRef.current
35644
+ };
35645
+ try {
35646
+ const query = new URLSearchParams({ visitorId: visitorIdValue });
35647
+ const response = await fetch(
35648
+ `${apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/conversations/${encodeURIComponent(id)}/public?${query}`,
35649
+ { headers: { Accept: "application/json" } }
35650
+ );
35651
+ if (!response.ok) return;
35652
+ const next = await response.json();
35653
+ const outcome = refreshOutcome(ticket, {
35654
+ conversationId: conversationId.current,
35655
+ turn: turnRef.current.generation,
35656
+ request: refreshRequestRef.current,
35657
+ busy: turnRef.current.busy
35658
+ });
35659
+ if (outcome === "apply") {
35660
+ applyConversation(next);
35661
+ } else if (outcome === "defer") {
35662
+ pendingRefreshRef.current = true;
35663
+ if (!turnRef.current.busy) void refreshConversation();
35664
+ }
35665
+ } catch {
35666
+ }
35667
+ }
35668
+ useEffect(() => {
35669
+ refreshRef.current = refreshConversation;
35670
+ });
35351
35671
  async function loadConversation(id) {
35352
35672
  const visitorIdValue = ensureVisitor();
35353
35673
  setLoadingConversation(true);
35674
+ refreshRequestRef.current += 1;
35354
35675
  try {
35355
35676
  const query = new URLSearchParams({ visitorId: visitorIdValue });
35356
35677
  const response = await fetch(
@@ -35413,34 +35734,71 @@ function SettoChat() {
35413
35734
  },
35414
35735
  []
35415
35736
  );
35737
+ const polledConversation = realtime?.conversationId ?? null;
35416
35738
  useEffect(() => {
35417
35739
  if (!mounted || !conversationId.current || !visitor.current || sending) return;
35418
- let cancelled = false;
35419
- const refresh = async () => {
35420
- try {
35421
- const query = new URLSearchParams({ visitorId: visitor.current });
35422
- const response = await fetch(
35423
- `${apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/conversations/${encodeURIComponent(conversationId.current)}/public?${query}`,
35424
- { headers: { Accept: "application/json" } }
35425
- );
35426
- if (!response.ok || cancelled) return;
35427
- const next = await response.json();
35428
- if (!cancelled) applyConversation(next);
35429
- } catch {
35740
+ const timer = window.setInterval(() => {
35741
+ if (document.visibilityState === "hidden") return;
35742
+ void refreshRef.current();
35743
+ }, pollIntervalMs(subscribed));
35744
+ return () => window.clearInterval(timer);
35745
+ }, [mounted, sending, subscribed, polledConversation]);
35746
+ const pageWasVisibleRef = useRef(pageVisible);
35747
+ useEffect(() => {
35748
+ const returned = pageVisible && !pageWasVisibleRef.current;
35749
+ pageWasVisibleRef.current = pageVisible;
35750
+ if (returned && mounted) void refreshRef.current();
35751
+ }, [pageVisible, mounted]);
35752
+ useEffect(() => {
35753
+ if (teamTypingUntil === null) return;
35754
+ const timer = window.setTimeout(
35755
+ () => setTeamTypingUntil(null),
35756
+ Math.max(0, teamTypingUntil - Date.now())
35757
+ );
35758
+ return () => window.clearTimeout(timer);
35759
+ }, [teamTypingUntil]);
35760
+ useEffect(() => {
35761
+ const id = conversationId.current;
35762
+ const visitorIdValue = visitor.current;
35763
+ if (!id || !visitorIdValue || realtime?.conversationId !== id || !realtime.supported) return;
35764
+ let marker = readMarkersRef.current.get(id);
35765
+ if (!marker) {
35766
+ marker = { sent: null, pending: null, unsupported: false };
35767
+ readMarkersRef.current.set(id, marker);
35768
+ }
35769
+ if (marker.unsupported) return;
35770
+ const through = readMarkerThrough(messages, {
35771
+ humanJoined,
35772
+ chatOpen: mounted && visible,
35773
+ pageVisible,
35774
+ lastSent: laterTimestamp(marker.sent, marker.pending)
35775
+ });
35776
+ if (!through) return;
35777
+ const state = marker;
35778
+ state.pending = through;
35779
+ fetch(
35780
+ `${apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/conversations/${encodeURIComponent(id)}/public/read`,
35781
+ {
35782
+ method: "POST",
35783
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
35784
+ body: JSON.stringify({ visitorId: visitorIdValue, through })
35430
35785
  }
35431
- };
35432
- const timer = window.setInterval(() => void refresh(), 5e3);
35433
- return () => {
35434
- cancelled = true;
35435
- window.clearInterval(timer);
35436
- };
35437
- }, [apiUrl, config.siteId, mounted, sending]);
35786
+ ).then((response) => {
35787
+ if (response.ok) state.sent = laterTimestamp(state.sent, through);
35788
+ else if (response.status === 404) state.unsupported = true;
35789
+ }).catch(() => void 0).finally(() => {
35790
+ if (state.pending === through) state.pending = null;
35791
+ });
35792
+ }, [apiUrl, config.siteId, messages, humanJoined, mounted, visible, pageVisible, realtime]);
35438
35793
  useEffect(() => {
35439
35794
  if (!sending && queued) {
35440
35795
  setQueued(false);
35441
35796
  void sendTurn();
35442
35797
  }
35443
35798
  }, [sending, queued]);
35799
+ useEffect(() => {
35800
+ if (!sending && pendingRefreshRef.current) void refreshRef.current();
35801
+ }, [sending]);
35444
35802
  useEffect(() => {
35445
35803
  if (!mounted) return;
35446
35804
  const closeOnEscape = (event) => {
@@ -35473,7 +35831,12 @@ function SettoChat() {
35473
35831
  });
35474
35832
  }
35475
35833
  function startNew() {
35834
+ typingSender().stop();
35476
35835
  rememberConversation(null);
35836
+ loadedConversationRef.current = null;
35837
+ latestHumanRef.current = null;
35838
+ setRealtime(null);
35839
+ setTeamTypingUntil(null);
35477
35840
  setHumanJoined(false);
35478
35841
  setMessages([{ role: "assistant", content: intro(locale, settings?.displayName ?? void 0) }]);
35479
35842
  setDraft("");
@@ -35497,6 +35860,7 @@ function SettoChat() {
35497
35860
  }
35498
35861
  function queueOrSend() {
35499
35862
  if (!draft.trim() && files.length === 0) return;
35863
+ typingSender().stop();
35500
35864
  if (sending) {
35501
35865
  setQueued(true);
35502
35866
  return;
@@ -35507,6 +35871,9 @@ function SettoChat() {
35507
35871
  const content2 = draft.trim();
35508
35872
  const turnFiles = files;
35509
35873
  if (!content2 && turnFiles.length === 0) return;
35874
+ turnRef.current.busy = true;
35875
+ turnRef.current.generation += 1;
35876
+ typingSender().stop();
35510
35877
  pinToBottomRef.current = true;
35511
35878
  setDraft("");
35512
35879
  setFiles([]);
@@ -35591,9 +35958,15 @@ function SettoChat() {
35591
35958
  setMessages((previous2) => replaceLastAssistant(previous2, labels.failed));
35592
35959
  } finally {
35593
35960
  for (const image2 of turnFiles) URL.revokeObjectURL(image2.preview);
35961
+ turnRef.current.busy = false;
35962
+ if (conversationId.current && loadedConversationRef.current !== conversationId.current) {
35963
+ pendingRefreshRef.current = true;
35964
+ }
35594
35965
  setSending(false);
35595
35966
  }
35596
35967
  }
35968
+ const seenIndex = seenVisitorMessageIndex(messages, humanJoined, realtime?.teamReadAt ?? null);
35969
+ const typingIdentity = teamTypingUntil !== null ? teamTypingIdentity(messages) : null;
35597
35970
  return /* @__PURE__ */ jsxs(
35598
35971
  "aside",
35599
35972
  {
@@ -35690,42 +36063,52 @@ function SettoChat() {
35690
36063
  ref: logRef,
35691
36064
  "aria-live": "polite",
35692
36065
  "aria-busy": loadingConversation,
35693
- children: /* @__PURE__ */ jsx("div", { className: "setto-chat__message-list", children: messages.map(
35694
- (item, index2) => item.role === "system" && item.kind === "agent_joined" ? /* @__PURE__ */ jsx(AgentJoinedEvent, { message: item }, item.id ?? `agent-joined-${index2}`) : /* @__PURE__ */ jsxs(
35695
- "div",
35696
- {
35697
- className: `setto-chat__message setto-chat__message--${item.role}`,
35698
- children: [
35699
- item.role === "assistant" || item.role === "human" ? /* @__PURE__ */ jsx(
35700
- "span",
35701
- {
35702
- className: `setto-chat__avatar setto-chat__avatar--${item.role}`,
35703
- "aria-hidden": "true",
35704
- children: item.role === "human" && item.avatarUrl ? /* @__PURE__ */ jsx("img", { src: item.avatarUrl, alt: "" }) : item.role === "human" ? /* @__PURE__ */ jsx(PersonIcon, {}) : /* @__PURE__ */ jsx(BaristaIcon, {})
35705
- }
35706
- ) : null,
35707
- /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-stack", children: [
35708
- item.role === "assistant" || item.role === "human" ? /* @__PURE__ */ jsx("span", { className: "setto-chat__speaker", children: item.role === "human" ? item.authorName || labels.team : "Barista" }) : null,
35709
- /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-bubble", children: [
35710
- /* @__PURE__ */ jsx("div", { className: "setto-chat__message-copy", children: item.content ? item.role === "assistant" ? /* @__PURE__ */ jsx(SiteChatMarkdown, { content: item.content, reveal: item.reveal }) : item.content : /* @__PURE__ */ jsx("span", { className: "setto-chat__typing", children: "···" }) }),
35711
- item.attachments?.map((attachment) => /* @__PURE__ */ jsx(
35712
- "a",
35713
- {
35714
- className: "setto-chat__image",
35715
- href: attachment.url,
35716
- target: "_blank",
35717
- rel: "noreferrer",
35718
- children: /* @__PURE__ */ jsx("img", { src: attachment.url, alt: labels.image })
35719
- },
35720
- attachment.id
35721
- ))
36066
+ children: /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-list", children: [
36067
+ messages.map(
36068
+ (item, index2) => item.role === "system" && item.kind === "agent_joined" ? /* @__PURE__ */ jsx(AgentJoinedEvent, { message: item }, item.id ?? `agent-joined-${index2}`) : /* @__PURE__ */ jsxs(
36069
+ "div",
36070
+ {
36071
+ className: `setto-chat__message setto-chat__message--${item.role}`,
36072
+ children: [
36073
+ item.role === "assistant" || item.role === "human" ? /* @__PURE__ */ jsx(
36074
+ "span",
36075
+ {
36076
+ className: `setto-chat__avatar setto-chat__avatar--${item.role}`,
36077
+ "aria-hidden": "true",
36078
+ children: item.role === "human" && item.avatarUrl ? /* @__PURE__ */ jsx("img", { src: item.avatarUrl, alt: "" }) : item.role === "human" ? /* @__PURE__ */ jsx(PersonIcon, {}) : /* @__PURE__ */ jsx(BaristaIcon, {})
36079
+ }
36080
+ ) : null,
36081
+ /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-stack", children: [
36082
+ item.role === "assistant" || item.role === "human" ? /* @__PURE__ */ jsx("span", { className: "setto-chat__speaker", children: item.role === "human" ? item.authorName || labels.team : "Barista" }) : null,
36083
+ /* @__PURE__ */ jsxs("div", { className: "setto-chat__message-bubble", children: [
36084
+ /* @__PURE__ */ jsx("div", { className: "setto-chat__message-copy", children: item.content ? item.role === "assistant" ? /* @__PURE__ */ jsx(SiteChatMarkdown, { content: item.content, reveal: item.reveal }) : item.content : /* @__PURE__ */ jsx("span", { className: "setto-chat__typing", children: "···" }) }),
36085
+ item.attachments?.map((attachment) => /* @__PURE__ */ jsx(
36086
+ "a",
36087
+ {
36088
+ className: "setto-chat__image",
36089
+ href: attachment.url,
36090
+ target: "_blank",
36091
+ rel: "noreferrer",
36092
+ children: /* @__PURE__ */ jsx("img", { src: attachment.url, alt: labels.image })
36093
+ },
36094
+ attachment.id
36095
+ ))
36096
+ ] }),
36097
+ index2 === seenIndex ? /* @__PURE__ */ jsx(SeenMarker, { label: labels.seen }) : null
35722
36098
  ] })
35723
- ] })
35724
- ]
35725
- },
35726
- item.id ?? `${item.role}-${index2}`
35727
- )
35728
- ) })
36099
+ ]
36100
+ },
36101
+ item.id ?? `${item.role}-${index2}`
36102
+ )
36103
+ ),
36104
+ typingIdentity ? /* @__PURE__ */ jsx(
36105
+ TeamTypingRow,
36106
+ {
36107
+ label: teamTypingLabel(isNo, typingIdentity.name),
36108
+ avatarUrl: typingIdentity.avatarUrl
36109
+ }
36110
+ ) : null
36111
+ ] })
35729
36112
  }
35730
36113
  ),
35731
36114
  /* @__PURE__ */ jsxs("div", { className: "setto-chat__composer-shell", children: [
@@ -35733,8 +36116,12 @@ function SettoChat() {
35733
36116
  SiteChatComposer,
35734
36117
  {
35735
36118
  value: draft,
35736
- onChange: setDraft,
36119
+ onChange: (value) => {
36120
+ setDraft(value);
36121
+ typingSender().update(value);
36122
+ },
35737
36123
  onSubmit: queueOrSend,
36124
+ onBlur: () => typingSender().stop(),
35738
36125
  busy: sending,
35739
36126
  queued,
35740
36127
  placeholder: humanJoined ? labels.humanPlaceholder : labels.placeholder,
@@ -35801,6 +36188,7 @@ function useChatStyles() {
35801
36188
  .setto-chat__main{position:relative;z-index:1;display:flex;flex:1;min-width:0;max-width:820px;height:100%;margin:0 auto;flex-direction:column;background:var(--setto-chat-canvas)}.setto-chat__header{position:absolute;inset:0 0 auto;z-index:30;min-height:58px;padding:.35rem .75rem;display:flex;align-items:center;gap:.5rem;background:linear-gradient(to bottom,color-mix(in srgb,var(--setto-chat-crema) 88%,transparent),color-mix(in srgb,var(--setto-chat-crema) 72%,transparent));border-bottom:1px solid color-mix(in srgb,var(--setto-chat-ink) 12%,transparent);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);isolation:isolate}.setto-chat__round-button{flex:none;width:46px;height:46px;padding:0;border:1px solid color-mix(in srgb,var(--setto-chat-ink) 12%,transparent);border-radius:50%;background:var(--setto-chat-raised);color:var(--setto-chat-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background 160ms ease,border-color 160ms ease,color 160ms ease}.setto-chat__round-button:hover{background:var(--setto-chat-crema);border-color:var(--setto-chat-accent);color:var(--setto-chat-accent)}.setto-chat__round-button:active{transform:scale(.96)}.setto-chat__brand{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;line-height:1.15;text-align:center}.setto-chat__brand h2{margin:0;color:var(--setto-chat-ink);font-size:1.02rem;font-weight:600;line-height:1.15}.setto-chat__brand p{max-width:100%;margin:2px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:color-mix(in srgb,var(--setto-chat-ink) 56%,transparent);font-size:.78rem;line-height:1.15}
35802
36189
  .setto-chat__body{position:relative;display:flex;flex:1 1 auto;min-height:0;height:100%;flex-direction:column}.setto-chat__log{display:flex;flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;flex-direction:column;padding:calc(58px + 1.3rem) .75rem 8rem;scroll-behavior:auto;-webkit-overflow-scrolling:touch}.setto-chat__message-list{display:flex;flex-direction:column}.setto-chat__message{--setto-chat-bubble:color-mix(in srgb,var(--setto-chat-ink) 7%,var(--setto-chat-canvas));display:flex;align-items:flex-end;gap:.55rem;width:100%;margin-bottom:1rem;color:color-mix(in srgb,var(--setto-chat-ink) 92%,transparent);font-size:16px;line-height:1.5;overflow-wrap:anywhere}.setto-chat__message-stack{display:flex;min-width:0;max-width:min(78%,36rem);flex-direction:column;align-items:flex-start}.setto-chat__message-bubble{position:relative;min-width:2.5rem;padding:.58rem .82rem;border-radius:19px 19px 19px 5px;background:var(--setto-chat-bubble);white-space:pre-wrap}.setto-chat__message--assistant .setto-chat__message-bubble::before,.setto-chat__message--human .setto-chat__message-bubble::before{content:"";position:absolute;z-index:0;left:-7px;bottom:0;width:16px;height:17px;border-bottom-right-radius:14px;background:var(--setto-chat-bubble)}.setto-chat__message--assistant .setto-chat__message-bubble::after,.setto-chat__message--human .setto-chat__message-bubble::after{content:"";position:absolute;z-index:1;left:-10px;bottom:0;width:10px;height:18px;border-bottom-right-radius:10px;background:var(--setto-chat-canvas)}.setto-chat__message-copy,.setto-chat__image{position:relative;z-index:2}.setto-chat__message--visitor{--setto-chat-bubble:#0a84ff;justify-content:flex-end;color:#fff}.setto-chat__message--visitor .setto-chat__message-stack{align-items:flex-end}.setto-chat__message--visitor .setto-chat__message-bubble{border-radius:19px 19px 5px 19px}.setto-chat__message--visitor .setto-chat__message-bubble::before{content:"";position:absolute;z-index:0;right:-7px;bottom:0;width:16px;height:17px;border-bottom-left-radius:14px;background:var(--setto-chat-bubble)}.setto-chat__message--visitor .setto-chat__message-bubble::after{content:"";position:absolute;z-index:1;right:-10px;bottom:0;width:10px;height:18px;border-bottom-left-radius:10px;background:var(--setto-chat-canvas)}.setto-chat__message--system{justify-content:center;color:color-mix(in srgb,var(--setto-chat-ink) 55%,transparent);font-size:.78rem;line-height:1.45;text-align:center}.setto-chat__message--system .setto-chat__message-stack{max-width:85%;align-items:center}.setto-chat__message--system .setto-chat__message-bubble{padding:.25rem .6rem;background:transparent}.setto-chat__avatar{position:relative;z-index:3;display:grid;place-items:center;flex:0 0 34px;width:34px;height:34px;margin-bottom:1px;overflow:hidden;border:2px solid var(--setto-chat-canvas);border-radius:50%;background:var(--setto-chat-crema);color:var(--setto-chat-ink);box-shadow:0 2px 8px color-mix(in srgb,var(--setto-chat-ink) 14%,transparent)}.setto-chat__avatar--assistant{background:var(--setto-chat-accent);color:var(--setto-chat-raised)}.setto-chat__avatar img{display:block;width:100%;height:100%;object-fit:cover}.setto-chat__speaker{display:block;margin:0 0 .2rem .4rem;color:color-mix(in srgb,var(--setto-chat-ink) 52%,transparent);font-size:.69rem;font-weight:500;line-height:1.2}.setto-chat__typing{display:inline-block;animation:setto-chat-pulse 1s ease-in-out infinite}.setto-chat__image{display:block;width:max-content;max-width:100%;margin-top:.65rem;overflow:hidden;border-radius:13px}.setto-chat__image img{display:block;width:min(280px,100%);max-height:260px;object-fit:cover}
35803
36190
  .setto-chat__message--agent-joined{justify-content:flex-start;align-items:center;gap:.55rem;padding:.1rem .4rem;color:color-mix(in srgb,var(--setto-chat-ink) 62%,transparent);font-size:.8rem;line-height:1.4;text-align:left}.setto-chat__message--agent-joined .setto-chat__message-stack{max-width:calc(100% - 38px);align-items:flex-start}.setto-chat__message--agent-joined .setto-chat__message-bubble{min-width:0;padding:.2rem 0;background:transparent;white-space:normal}.setto-chat__avatar--joined{flex-basis:30px;width:30px;height:30px;margin:0;border-width:1px;box-shadow:0 2px 7px color-mix(in srgb,var(--setto-chat-ink) 12%,transparent)}
36191
+ .setto-chat__receipt{display:block;margin:.26rem .45rem 0 0;color:color-mix(in srgb,var(--setto-chat-ink) 52%,transparent);font-size:.69rem;font-weight:500;line-height:1.2}.setto-chat__message--typing{animation:setto-chat-typing-in 180ms cubic-bezier(.2,.8,.2,1) both}.setto-chat__message--typing .setto-chat__message-bubble{min-width:3.25rem;text-align:center}.setto-chat__message--typing .setto-chat__typing{font-weight:700;letter-spacing:.06em}@keyframes setto-chat-typing-in{from{opacity:0;transform:translateY(4px)}}@media(prefers-reduced-motion:reduce){.setto-chat__message--typing{animation:none}}
35804
36192
  .setto-chat__composer-shell{position:absolute;left:0;right:0;bottom:0;z-index:25;display:grid;grid-template-columns:minmax(0,1fr);padding:1.5rem .75rem 0;background:transparent;isolation:isolate}.setto-chat__composer-shell::before{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(to bottom,transparent 0%,color-mix(in srgb,var(--setto-chat-canvas) 82%,transparent) 46%,var(--setto-chat-canvas) 66%);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);-webkit-mask-image:linear-gradient(to bottom,rgba(0,0,0,0) 0%,rgba(0,0,0,.4) 34%,rgba(0,0,0,1) 66%);mask-image:linear-gradient(to bottom,rgba(0,0,0,0) 0%,rgba(0,0,0,.4) 34%,rgba(0,0,0,1) 66%)}.setto-chat__composer{position:relative;z-index:1;display:flex;flex-direction:column;gap:.15rem;width:100%;min-width:0;margin:0;padding:.35rem .4rem;border:2px solid color-mix(in srgb,var(--setto-chat-ink) 10%,transparent);border-radius:24px;background:var(--setto-chat-canvas);box-shadow:0 8px 24px -16px color-mix(in srgb,var(--setto-chat-ink) 28%,transparent);transition:padding 220ms cubic-bezier(.16,1,.3,1),border-color 200ms ease,box-shadow 360ms cubic-bezier(.16,1,.3,1)}.setto-chat__composer.is-open{padding:.55rem .5rem}.setto-chat__composer:focus-within{border-color:var(--setto-chat-accent)}.setto-chat__composer textarea,.setto-chat__composer textarea:hover,.setto-chat__composer textarea:focus,.setto-chat__composer textarea:focus-visible{width:100%;min-height:36px;max-height:200px;padding:.45rem .25rem;resize:none;-webkit-appearance:none!important;appearance:none!important;border:0!important;border-radius:0!important;outline:0!important;box-shadow:none!important;background:transparent!important;color:var(--setto-chat-ink);font-family:"Archivo Variable",Archivo,system-ui,sans-serif;font-size:16px;line-height:1.35;transition:min-height 220ms cubic-bezier(.16,1,.3,1)}.setto-chat__composer.is-open textarea{min-height:40px}.setto-chat__composer textarea::placeholder{color:color-mix(in srgb,var(--setto-chat-ink) 52%,transparent)}.setto-chat__composer-tools{display:flex;align-items:center;gap:.4rem;width:100%;min-width:0}.setto-chat__attach{flex:none;width:34px;height:34px;padding:0;border:0;border-radius:50%;background:transparent;color:color-mix(in srgb,var(--setto-chat-ink) 58%,transparent);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background 160ms ease,color 160ms ease}.setto-chat__attach:hover:not(:disabled){background:color-mix(in srgb,var(--setto-chat-ink) 6%,transparent);color:var(--setto-chat-ink)}.setto-chat__attach:disabled{opacity:.4;cursor:default}.setto-chat__send{flex:none;width:36px;height:36px;margin-left:auto;padding:0;border:0;border-radius:50%;background:var(--setto-chat-accent);color:var(--setto-chat-raised);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 360ms cubic-bezier(.16,1,.3,1),box-shadow 360ms cubic-bezier(.16,1,.3,1),background 200ms ease,opacity 200ms ease}.setto-chat__send:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 12px 24px -12px color-mix(in srgb,var(--setto-chat-ink) 45%,transparent)}.setto-chat__send:active:not(:disabled){transform:translateY(0);box-shadow:0 4px 10px -8px color-mix(in srgb,var(--setto-chat-ink) 40%,transparent);transition-duration:90ms}.setto-chat__send:disabled{opacity:.35;cursor:default}.setto-chat__send.is-queued{animation:setto-chat-queued 1.1s ease-in-out infinite}.setto-chat__attachments{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;margin:0;padding:.15rem .15rem 0}.setto-chat__attachments li{position:relative;width:56px;height:56px;border-radius:10px;overflow:hidden;border:1px solid color-mix(in srgb,var(--setto-chat-ink) 12%,transparent);background:var(--setto-chat-crema)}.setto-chat__attachments img{display:block;width:100%;height:100%;object-fit:cover}.setto-chat__attachments button{position:absolute;top:2px;right:2px;width:18px;height:18px;padding:0;border:0;border-radius:50%;background:color-mix(in srgb,var(--setto-chat-ink) 72%,transparent);color:#fff;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}.setto-chat__composer-floor{position:relative;z-index:1;width:calc(100% + 1.5rem);height:max(.35rem,env(safe-area-inset-bottom));margin-left:-.75rem;background:var(--setto-chat-canvas)}
35805
36193
  .setto-chat__launcher{width:58px;height:58px;padding:3px;border:1px solid rgba(255,255,255,.54);border-radius:50%;background:color-mix(in srgb,var(--setto-chat-raised) 35%,transparent);box-shadow:0 10px 30px color-mix(in srgb,var(--setto-chat-ink) 24%,transparent),inset 0 0 0 1px color-mix(in srgb,var(--setto-chat-ink) 9%,transparent);backdrop-filter:blur(14px) saturate(115%);-webkit-backdrop-filter:blur(14px) saturate(115%);color:var(--setto-chat-ink);display:grid;place-items:center;cursor:pointer;transition:transform 180ms ease,box-shadow 180ms ease}.setto-chat__launcher:hover{transform:translateY(-2px);box-shadow:0 15px 35px color-mix(in srgb,var(--setto-chat-ink) 28%,transparent),inset 0 0 0 1px color-mix(in srgb,var(--setto-chat-ink) 9%,transparent)}.setto-chat__launcher span{width:46px;height:46px;border-radius:50%;display:grid;place-items:center;background:var(--setto-chat-accent);color:var(--setto-chat-raised)}.setto-chat__launcher img{display:block;width:50px;height:50px;border-radius:50%;object-fit:cover}.setto-chat__launcher:focus-visible,.setto-chat button:focus-visible,.setto-chat input:focus-visible{outline:2px solid var(--setto-chat-accent);outline-offset:2px}@keyframes setto-chat-pulse{50%{opacity:.35}}@keyframes setto-chat-queued{50%{opacity:.55}}
35806
36194
  @media(max-width:700px){.setto-chat__chats{position:absolute;inset:0 auto 0 0;transform:translateX(-100%);transition:transform 260ms cubic-bezier(.16,1,.3,1);box-shadow:14px 0 35px color-mix(in srgb,var(--setto-chat-ink) 16%,transparent)}.setto-chat__chats--open{transform:none}.setto-chat__main{width:100%;max-width:none;margin:0}.setto-chat__log{padding-left:.75rem;padding-right:.75rem}}@media(min-width:701px){.setto-chat--mounted{top:auto;right:max(18px,env(safe-area-inset-right));bottom:max(18px,env(safe-area-inset-bottom));left:auto;width:min(440px,calc(100vw - 36px));height:min(720px,calc(100dvh - 36px));overflow:visible;background:transparent}.setto-chat__shell{position:relative;top:auto;left:auto;width:100%;height:100%;border:1px solid color-mix(in srgb,var(--setto-chat-ink) 14%,transparent);border-radius:26px;background:var(--setto-chat-canvas);box-shadow:0 28px 80px -24px color-mix(in srgb,var(--setto-chat-ink) 42%,transparent),0 8px 24px -12px color-mix(in srgb,var(--setto-chat-ink) 24%,transparent);transform:translateY(16px) scale(.975);transform-origin:right bottom}.setto-chat--visible .setto-chat__shell{transform:none}.setto-chat__chats{position:absolute;inset:0 auto 0 0;width:min(82%,280px);height:100%;transform:translateX(-100%);transition:transform 260ms cubic-bezier(.16,1,.3,1);box-shadow:14px 0 35px color-mix(in srgb,var(--setto-chat-ink) 16%,transparent)}.setto-chat__chats--open{transform:none}.setto-chat__main{width:100%;max-width:none;margin:0}.setto-chat__header,.setto-chat__log{padding-left:.9rem;padding-right:.9rem}.setto-chat__composer-shell{padding-left:.9rem;padding-right:.9rem}.setto-chat__composer-floor{width:calc(100% + 1.8rem);height:.45rem;margin-left:-.9rem}.setto-chat__message{font-size:15.5px}.setto-chat__message-stack{max-width:82%}}@media(prefers-reduced-motion:reduce){.setto-chat,.setto-chat__shell,.setto-chat__chats,.setto-chat__launcher,.setto-chat__composer,.setto-chat__composer textarea,.setto-chat__send{transition:none!important}.setto-chat__typing,.setto-chat__send.is-queued{animation:none}}
@@ -36954,19 +37342,19 @@ function readLocaleFields(i18n, lng, formId, ns = "translation") {
36954
37342
  for (const itemKey of collectKeysUnder(bundle, prefix)) {
36955
37343
  const item = list2[itemKey];
36956
37344
  if (!item || typeof item !== "object") continue;
36957
- const record = item;
36958
- const name2 = readString(record, "name").trim();
37345
+ const record2 = item;
37346
+ const name2 = readString(record2, "name").trim();
36959
37347
  if (!name2) continue;
36960
- const type = isFormFieldType(record.type) ? record.type : "text";
37348
+ const type = isFormFieldType(record2.type) ? record2.type : "text";
36961
37349
  fields.push({
36962
37350
  itemKey,
36963
37351
  name: name2,
36964
37352
  type,
36965
37353
  labelKey: `${prefix}.${itemKey}.label`,
36966
- label: readString(record, "label"),
36967
- placeholder: readString(record, "placeholder"),
36968
- required: readString(record, "required") === "true",
36969
- options: parseOptions(readString(record, "options"))
37354
+ label: readString(record2, "label"),
37355
+ placeholder: readString(record2, "placeholder"),
37356
+ required: readString(record2, "required") === "true",
37357
+ options: parseOptions(readString(record2, "options"))
36970
37358
  });
36971
37359
  }
36972
37360
  return fields;
@@ -37755,6 +38143,142 @@ function toEmbedUrl(url) {
37755
38143
  return null;
37756
38144
  }
37757
38145
  }
38146
+ const MAP_MIN_ZOOM = 1;
38147
+ const MAP_MAX_ZOOM = 19;
38148
+ const MAP_DEFAULT_ZOOM = 15;
38149
+ const MAP_DEFAULT_TITLE = "Kart";
38150
+ const MERCATOR_MAX_LAT = 85.0511287798066;
38151
+ const MAP_ASSUMED_WIDTH = 320;
38152
+ const MAP_ASSUMED_HEIGHT = 240;
38153
+ const TILE_SIZE = 256;
38154
+ function parseCoordinate(value) {
38155
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
38156
+ if (typeof value !== "string") return null;
38157
+ const text2 = value.trim().replace(",", ".");
38158
+ if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(text2)) return null;
38159
+ const n = Number(text2);
38160
+ return Number.isFinite(n) ? n : null;
38161
+ }
38162
+ function parseZoom(value) {
38163
+ const n = parseCoordinate(value);
38164
+ if (n === null) return MAP_DEFAULT_ZOOM;
38165
+ return Math.min(MAP_MAX_ZOOM, Math.max(MAP_MIN_ZOOM, Math.round(n)));
38166
+ }
38167
+ function parseMapLocation(raw) {
38168
+ if (!raw) return null;
38169
+ const lat = parseCoordinate(raw.lat);
38170
+ const lng = parseCoordinate(raw.lng);
38171
+ if (lat === null || lng === null) return null;
38172
+ if (Math.abs(lat) > MERCATOR_MAX_LAT || Math.abs(lng) > 180) return null;
38173
+ return { lat, lng, zoom: parseZoom(raw.zoom) };
38174
+ }
38175
+ function formatCoordinate(n) {
38176
+ const fixed = n.toFixed(6);
38177
+ const trimmed = fixed.includes(".") ? fixed.replace(/0+$/, "").replace(/\.$/, "") : fixed;
38178
+ return trimmed === "-0" ? "0" : trimmed;
38179
+ }
38180
+ function serialiseMapLocation(location2) {
38181
+ return {
38182
+ lat: formatCoordinate(location2.lat),
38183
+ lng: formatCoordinate(location2.lng),
38184
+ zoom: String(parseZoom(location2.zoom))
38185
+ };
38186
+ }
38187
+ function worldSize(zoom) {
38188
+ return TILE_SIZE * 2 ** zoom;
38189
+ }
38190
+ function latToWorldY(lat, zoom) {
38191
+ const clamped = Math.max(-MERCATOR_MAX_LAT, Math.min(MERCATOR_MAX_LAT, lat));
38192
+ const sin = Math.sin(clamped * Math.PI / 180);
38193
+ return (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * worldSize(zoom);
38194
+ }
38195
+ function worldYToLat(y, zoom) {
38196
+ const n = Math.PI - 2 * Math.PI * y / worldSize(zoom);
38197
+ return 180 / Math.PI * Math.atan(Math.sinh(n));
38198
+ }
38199
+ function mapBbox(location2, width = MAP_ASSUMED_WIDTH, height = MAP_ASSUMED_HEIGHT) {
38200
+ const degreesPerPixel = 360 / worldSize(location2.zoom);
38201
+ const halfLng = width / 2 * degreesPerPixel;
38202
+ const y = latToWorldY(location2.lat, location2.zoom);
38203
+ const max = worldSize(location2.zoom);
38204
+ return {
38205
+ minLng: Math.max(-180, location2.lng - halfLng),
38206
+ maxLng: Math.min(180, location2.lng + halfLng),
38207
+ minLat: worldYToLat(Math.min(max, y + height / 2), location2.zoom),
38208
+ maxLat: worldYToLat(Math.max(0, y - height / 2), location2.zoom)
38209
+ };
38210
+ }
38211
+ function mapEmbedUrl(location2) {
38212
+ const box = mapBbox(location2);
38213
+ const bbox = [box.minLng, box.minLat, box.maxLng, box.maxLat].map(formatCoordinate).join(",");
38214
+ const marker = `${formatCoordinate(location2.lat)},${formatCoordinate(location2.lng)}`;
38215
+ return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${marker}`;
38216
+ }
38217
+ function writeMapLocation(store, languages, key, location2) {
38218
+ const values = serialiseMapLocation(location2);
38219
+ store.batch(() => {
38220
+ for (const lng of languages) {
38221
+ store.set(`${key}.lat`, lng, values.lat);
38222
+ store.set(`${key}.lng`, lng, values.lng);
38223
+ store.set(`${key}.zoom`, lng, values.zoom);
38224
+ }
38225
+ });
38226
+ }
38227
+ function SettoMap({ k, className, style, loading = "lazy" }) {
38228
+ const { t, i18n } = useTranslation();
38229
+ const { editMode, store } = useSetto();
38230
+ const guestEdit = useGuestEdit();
38231
+ const editable = editMode || guestEdit.active;
38232
+ const MapTools = useSettoChromeSlot("map-tools");
38233
+ const [, force] = useState(0);
38234
+ useEffect(() => {
38235
+ if (!store) return;
38236
+ return store.subscribe(() => force((x) => x + 1));
38237
+ }, [store]);
38238
+ const lng = i18n.language;
38239
+ const read = (field) => {
38240
+ const key = `${k}.${field}`;
38241
+ return store?.get(key, lng) || t(key, { defaultValue: "" });
38242
+ };
38243
+ const location2 = parseMapLocation({ lat: read("lat"), lng: read("lng"), zoom: read("zoom") });
38244
+ const title = read("title") || MAP_DEFAULT_TITLE;
38245
+ const tools = editable && store && MapTools ? /* @__PURE__ */ jsx(
38246
+ MapTools,
38247
+ {
38248
+ location: location2,
38249
+ onChange: (next) => writeMapLocation(store, store.languages(lng), k, next)
38250
+ }
38251
+ ) : null;
38252
+ if (!location2) {
38253
+ return /* @__PURE__ */ jsx("div", { className, style, "data-setto-map": k, "data-setto-map-empty": "1", children: tools ? /* @__PURE__ */ jsx("div", { style: fillStyle, children: tools }) : null });
38254
+ }
38255
+ return /* @__PURE__ */ jsx("div", { className, style, "data-setto-map": k, children: /* @__PURE__ */ jsxs("div", { style: fillStyle, children: [
38256
+ /* @__PURE__ */ jsx(
38257
+ "iframe",
38258
+ {
38259
+ src: mapEmbedUrl(location2),
38260
+ title,
38261
+ loading,
38262
+ referrerPolicy: "strict-origin-when-cross-origin",
38263
+ style: iframeStyle
38264
+ }
38265
+ ),
38266
+ tools
38267
+ ] }) });
38268
+ }
38269
+ const fillStyle = {
38270
+ position: "relative",
38271
+ width: "100%",
38272
+ height: "100%",
38273
+ borderRadius: "inherit",
38274
+ overflow: "hidden"
38275
+ };
38276
+ const iframeStyle = {
38277
+ display: "block",
38278
+ width: "100%",
38279
+ height: "100%",
38280
+ border: 0
38281
+ };
37758
38282
  const DEFAULT_DURATION = {
37759
38283
  fadeIn: 600,
37760
38284
  slideUp: 700,
@@ -38739,6 +39263,7 @@ export {
38739
39263
  SettoIcon,
38740
39264
  SettoImage,
38741
39265
  SettoLanguages,
39266
+ SettoMap,
38742
39267
  SettoMark,
38743
39268
  SettoProvider,
38744
39269
  SettoRepeater,