@sevenfold/setto-client 0.20.1 → 0.22.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/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export type { SettoImageProps } from './SettoImage';
15
15
  export { SettoRepeater } from './SettoRepeater';
16
16
  export type { SettoRepeaterProps } from './SettoRepeater';
17
17
  export { SettoForm, FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS, fieldTypeHasOptions, fieldTypeHasPlaceholder, formFieldsPrefix, isFormFieldType, readLocaleFields, settoSubmissionUrl, } from './modules/forms';
18
+ export { SettoChat } from './modules/chat';
18
19
  export type { SettoFormProps, SettoFormClassNames, FormFieldProp, FormSettings, FormSubmissionRow, FormValue, FormValues, ResolvedFormField, SubmissionEnvelope, } from './modules/forms';
19
20
  export { SETTO_MODULES } from './modules';
20
21
  export type { SettoModule } from './modules';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A public visitor chat mounted once by SettoProvider. The server decides
3
+ * whether it exists for a site; a disabled or unreachable chat draws nothing.
4
+ */
5
+ export declare function SettoChat(): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,3 @@
1
+ import type { SettoModule } from '../module';
2
+ export declare const chatModule: SettoModule;
3
+ export { SettoChat } from './SettoChat';
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
- import { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1, useId, useInsertionEffect } from "react";
3
+ import { useState, useRef, useEffect, useMemo, useId, useInsertionEffect, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1 } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import * as ReactDOM from "react-dom";
6
6
  import { createPortal } from "react-dom";
@@ -118,8 +118,8 @@ function redirectForVisitor(pathname, languages, browserLanguages, stored, prima
118
118
  const base = wanted.toLowerCase().split("-")[0];
119
119
  if (!base) continue;
120
120
  if (base === primary.toLowerCase().split("-")[0]) return null;
121
- const norwegian = ["no", "nb", "nn"];
122
- if (norwegian.includes(base) && norwegian.includes(primary.toLowerCase())) return null;
121
+ const norwegian2 = ["no", "nb", "nn"];
122
+ if (norwegian2.includes(base) && norwegian2.includes(primary.toLowerCase())) return null;
123
123
  const hit = languages.find((tag) => tag.toLowerCase().split("-")[0] === base);
124
124
  if (hit && hit !== primary) return pathForLanguage(pathname, hit, languages, primary);
125
125
  }
@@ -22032,6 +22032,334 @@ function EditModeShell({ children, themeStore, active }) {
22032
22032
  active && sectionToolbarRoot && themeStore && SectionTools ? createPortal(/* @__PURE__ */ jsx(SectionTools, {}), sectionToolbarRoot) : null
22033
22033
  ] });
22034
22034
  }
22035
+ const VISITOR_KEY = "setto-chat-visitor";
22036
+ function visitorId() {
22037
+ try {
22038
+ const stored = window.localStorage.getItem(VISITOR_KEY);
22039
+ if (stored && /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(stored)) return stored;
22040
+ const id = crypto.randomUUID();
22041
+ window.localStorage.setItem(VISITOR_KEY, id);
22042
+ return id;
22043
+ } catch {
22044
+ return crypto.randomUUID();
22045
+ }
22046
+ }
22047
+ function norwegian(locale) {
22048
+ return locale.toLowerCase().startsWith("no");
22049
+ }
22050
+ function intro(locale, name) {
22051
+ return norwegian(locale) ? `Hei! Jeg er Barista, ${name ? `${name}s` : "nettstedets"} AI-assistent. Jeg svarer ut fra det teamet har delt her, og hjelper deg videre til dem når det trengs.` : `Hello! I’m Barista, the AI assistant for ${name ?? "this website"}. I answer from what the team has shared here and help you reach them when needed.`;
22052
+ }
22053
+ function contactTeam() {
22054
+ const link = document.querySelector(
22055
+ 'a[href*="kontakt" i], a[href*="contact" i], a[href^="mailto:"]'
22056
+ );
22057
+ if (link) link.click();
22058
+ else window.location.hash = "kontakt";
22059
+ }
22060
+ function SettoChat() {
22061
+ const { config } = useSetto();
22062
+ const { i18n } = useTranslation();
22063
+ const [settings, setSettings] = useState(null);
22064
+ const [open, setOpen] = useState(false);
22065
+ const [messages, setMessages] = useState([]);
22066
+ const [draft, setDraft] = useState("");
22067
+ const [sending, setSending] = useState(false);
22068
+ const [leadOpen, setLeadOpen] = useState(false);
22069
+ const [leadStatus, setLeadStatus] = useState("idle");
22070
+ const [lead, setLead] = useState({ name: "", email: "", phone: "", interest: "", consent: false });
22071
+ const conversationId = useRef(null);
22072
+ const visitor = useRef(null);
22073
+ const logRef = useRef(null);
22074
+ const formId = useId();
22075
+ useChatStyles();
22076
+ useEffect(() => {
22077
+ if (window.location.pathname.startsWith("/setto")) return;
22078
+ let cancelled = false;
22079
+ fetch(`${config.apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/settings/public`, {
22080
+ headers: { Accept: "application/json" }
22081
+ }).then(async (response) => response.ok ? await response.json() : null).then((next) => {
22082
+ if (!cancelled && next?.enabled) setSettings(next);
22083
+ }).catch(() => {
22084
+ });
22085
+ return () => {
22086
+ cancelled = true;
22087
+ };
22088
+ }, [config.apiUrl, config.siteId]);
22089
+ useEffect(() => {
22090
+ if (!open || messages.length > 0 || !settings) return;
22091
+ setMessages([{ role: "assistant", content: intro(i18n.language, settings.displayName) }]);
22092
+ }, [open, messages.length, settings, i18n.language]);
22093
+ useEffect(() => {
22094
+ if (open) logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
22095
+ }, [messages, open]);
22096
+ if (!settings || window.location.pathname.startsWith("/setto")) return null;
22097
+ const locale = i18n.language || "no";
22098
+ const labels = norwegian(locale) ? {
22099
+ open: `Snakk med ${settings.displayName}`,
22100
+ close: "Lukk chatten",
22101
+ title: "Spør Barista",
22102
+ disclosure: "AI-assistent · svarer fra nettstedets godkjente informasjon",
22103
+ placeholder: "Skriv spørsmålet ditt …",
22104
+ send: "Send",
22105
+ sending: "Svarer …",
22106
+ contact: "Kontakt teamet",
22107
+ followUp: "Be om oppfølging",
22108
+ followUpTitle: "La teamet følge opp",
22109
+ followUpDetail: "Legg igjen kontaktinformasjon hvis du vil at teamet skal følge opp denne samtalen.",
22110
+ name: "Navn",
22111
+ email: "E-post",
22112
+ phone: "Telefon",
22113
+ interest: "Hva vil du ha hjelp med?",
22114
+ consent: "Ja, teamet kan bruke opplysningene mine til å følge opp denne henvendelsen.",
22115
+ leadSend: "Send forespørsel",
22116
+ leadSent: "Takk — forespørselen er sendt til teamet.",
22117
+ leadFailed: "Kunne ikke lagre forespørselen. Prøv igjen eller kontakt teamet direkte.",
22118
+ leadConversation: "Still et spørsmål først, så kan vi knytte oppfølgingen til samtalen.",
22119
+ failed: "Jeg får ikke svart akkurat nå. Kontakt gjerne teamet direkte."
22120
+ } : {
22121
+ open: `Chat with ${settings.displayName}`,
22122
+ close: "Close chat",
22123
+ title: "Ask Barista",
22124
+ disclosure: "AI assistant · answers from this website’s approved information",
22125
+ placeholder: "Write your question …",
22126
+ send: "Send",
22127
+ sending: "Replying …",
22128
+ contact: "Contact the team",
22129
+ followUp: "Ask for follow-up",
22130
+ followUpTitle: "Let the team follow up",
22131
+ followUpDetail: "Leave contact details if you want the team to follow up on this conversation.",
22132
+ name: "Name",
22133
+ email: "Email",
22134
+ phone: "Phone",
22135
+ interest: "What would you like help with?",
22136
+ consent: "Yes, the team may use my details to follow up on this enquiry.",
22137
+ leadSend: "Send request",
22138
+ leadSent: "Thanks — your request has been sent to the team.",
22139
+ leadFailed: "We could not save your request. Please try again or contact the team directly.",
22140
+ leadConversation: "Ask a question first so we can link the follow-up to this conversation.",
22141
+ failed: "I cannot reply right now. Please contact the team directly."
22142
+ };
22143
+ const submit = async (event) => {
22144
+ event.preventDefault();
22145
+ const message = draft.trim();
22146
+ if (!message || sending) return;
22147
+ setDraft("");
22148
+ setSending(true);
22149
+ setMessages((previous) => [...previous, { role: "visitor", content: message }, { role: "assistant", content: "" }]);
22150
+ visitor.current ??= visitorId();
22151
+ try {
22152
+ const response = await fetch(
22153
+ `${config.apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/messages`,
22154
+ {
22155
+ method: "POST",
22156
+ headers: { "Content-Type": "application/json", Accept: "text/plain, application/json" },
22157
+ body: JSON.stringify({
22158
+ conversationId: conversationId.current ?? void 0,
22159
+ visitorId: visitor.current,
22160
+ locale,
22161
+ page: window.location.href,
22162
+ message
22163
+ })
22164
+ }
22165
+ );
22166
+ const headerConversation = response.headers.get("X-Setto-Conversation");
22167
+ if (headerConversation) conversationId.current = headerConversation;
22168
+ const contentType = response.headers.get("content-type") ?? "";
22169
+ if (!response.ok) throw new Error(`chat_failed_${response.status}`);
22170
+ if (contentType.includes("application/json")) {
22171
+ const body = await response.json();
22172
+ if (body.conversationId) conversationId.current = body.conversationId;
22173
+ setMessages((previous) => replaceLastAssistant(previous, body.answer || labels.failed));
22174
+ return;
22175
+ }
22176
+ const reader = response.body?.getReader();
22177
+ if (!reader) throw new Error("chat_missing_stream");
22178
+ const decoder = new TextDecoder();
22179
+ let answer = "";
22180
+ while (true) {
22181
+ const { done, value } = await reader.read();
22182
+ if (done) break;
22183
+ answer += decoder.decode(value, { stream: true });
22184
+ setMessages((previous) => replaceLastAssistant(previous, answer));
22185
+ }
22186
+ answer += decoder.decode();
22187
+ if (!answer.trim()) setMessages((previous) => replaceLastAssistant(previous, labels.failed));
22188
+ } catch {
22189
+ setMessages((previous) => replaceLastAssistant(previous, labels.failed));
22190
+ } finally {
22191
+ setSending(false);
22192
+ }
22193
+ };
22194
+ const submitLead = async (event) => {
22195
+ event.preventDefault();
22196
+ if (leadStatus === "sending") return;
22197
+ if (!conversationId.current || !visitor.current) {
22198
+ setLeadStatus("failed");
22199
+ return;
22200
+ }
22201
+ setLeadStatus("sending");
22202
+ try {
22203
+ const response = await fetch(`${config.apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/leads`, {
22204
+ method: "POST",
22205
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
22206
+ body: JSON.stringify({
22207
+ conversationId: conversationId.current,
22208
+ visitorId: visitor.current,
22209
+ consent: lead.consent,
22210
+ name: lead.name.trim() || void 0,
22211
+ email: lead.email.trim() || void 0,
22212
+ phone: lead.phone.trim() || void 0,
22213
+ interest: lead.interest.trim() || void 0
22214
+ })
22215
+ });
22216
+ if (!response.ok) throw new Error(`lead_failed_${response.status}`);
22217
+ setLeadStatus("sent");
22218
+ } catch {
22219
+ setLeadStatus("failed");
22220
+ }
22221
+ };
22222
+ return /* @__PURE__ */ jsxs("aside", { className: "setto-chat", "aria-label": labels.title, "data-setto-ui": true, children: [
22223
+ open ? /* @__PURE__ */ jsxs("section", { className: "setto-chat__panel", role: "dialog", "aria-modal": "false", "aria-labelledby": `${formId}-title`, children: [
22224
+ /* @__PURE__ */ jsxs("header", { className: "setto-chat__header", children: [
22225
+ /* @__PURE__ */ jsxs("div", { children: [
22226
+ /* @__PURE__ */ jsx("p", { className: "setto-chat__eyebrow", children: "BARISTA" }),
22227
+ /* @__PURE__ */ jsx("h2", { id: `${formId}-title`, children: labels.title }),
22228
+ /* @__PURE__ */ jsx("p", { children: labels.disclosure })
22229
+ ] }),
22230
+ /* @__PURE__ */ jsx("button", { className: "setto-chat__close", type: "button", onClick: () => setOpen(false), "aria-label": labels.close, children: "×" })
22231
+ ] }),
22232
+ /* @__PURE__ */ jsx("div", { className: "setto-chat__log", ref: logRef, "aria-live": "polite", children: messages.map((item, index) => /* @__PURE__ */ jsx("div", { className: `setto-chat__message setto-chat__message--${item.role}`, children: item.content || /* @__PURE__ */ jsx("span", { className: "setto-chat__typing", children: "···" }) }, `${item.role}-${index}`)) }),
22233
+ /* @__PURE__ */ jsxs("div", { className: "setto-chat__contact", children: [
22234
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: contactTeam, children: [
22235
+ labels.contact,
22236
+ " ",
22237
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "↗" })
22238
+ ] }),
22239
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => {
22240
+ setLeadOpen((value) => !value);
22241
+ setLeadStatus("idle");
22242
+ }, children: labels.followUp })
22243
+ ] }),
22244
+ leadOpen ? /* @__PURE__ */ jsxs("form", { className: "setto-chat__lead", onSubmit: submitLead, children: [
22245
+ /* @__PURE__ */ jsx("strong", { children: labels.followUpTitle }),
22246
+ /* @__PURE__ */ jsx("p", { children: labels.followUpDetail }),
22247
+ /* @__PURE__ */ jsxs("div", { className: "setto-chat__lead-grid", children: [
22248
+ /* @__PURE__ */ jsxs("label", { children: [
22249
+ labels.name,
22250
+ /* @__PURE__ */ jsx("input", { value: lead.name, maxLength: 160, onChange: (event) => setLead((current) => ({ ...current, name: event.target.value })) })
22251
+ ] }),
22252
+ /* @__PURE__ */ jsxs("label", { children: [
22253
+ labels.email,
22254
+ /* @__PURE__ */ jsx("input", { type: "email", value: lead.email, maxLength: 320, onChange: (event) => setLead((current) => ({ ...current, email: event.target.value })) })
22255
+ ] }),
22256
+ /* @__PURE__ */ jsxs("label", { children: [
22257
+ labels.phone,
22258
+ /* @__PURE__ */ jsx("input", { type: "tel", value: lead.phone, maxLength: 64, onChange: (event) => setLead((current) => ({ ...current, phone: event.target.value })) })
22259
+ ] }),
22260
+ /* @__PURE__ */ jsxs("label", { children: [
22261
+ labels.interest,
22262
+ /* @__PURE__ */ jsx("input", { value: lead.interest, maxLength: 500, onChange: (event) => setLead((current) => ({ ...current, interest: event.target.value })) })
22263
+ ] })
22264
+ ] }),
22265
+ /* @__PURE__ */ jsxs("label", { className: "setto-chat__consent", children: [
22266
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: lead.consent, onChange: (event) => setLead((current) => ({ ...current, consent: event.target.checked })) }),
22267
+ labels.consent
22268
+ ] }),
22269
+ leadStatus === "sent" ? /* @__PURE__ */ jsx("p", { className: "setto-chat__lead-success", children: labels.leadSent }) : null,
22270
+ leadStatus === "failed" ? /* @__PURE__ */ jsx("p", { className: "setto-chat__lead-error", children: conversationId.current ? labels.leadFailed : labels.leadConversation }) : null,
22271
+ leadStatus !== "sent" ? /* @__PURE__ */ jsx("button", { type: "submit", disabled: leadStatus === "sending" || !lead.consent || !lead.email.trim() && !lead.phone.trim(), children: leadStatus === "sending" ? labels.sending : labels.leadSend }) : null
22272
+ ] }) : null,
22273
+ /* @__PURE__ */ jsxs("form", { className: "setto-chat__composer", onSubmit: submit, children: [
22274
+ /* @__PURE__ */ jsx("label", { className: "setto-chat__sr", htmlFor: `${formId}-input`, children: labels.placeholder }),
22275
+ /* @__PURE__ */ jsx(
22276
+ "textarea",
22277
+ {
22278
+ id: `${formId}-input`,
22279
+ value: draft,
22280
+ rows: 1,
22281
+ maxLength: 1500,
22282
+ onChange: (event) => setDraft(event.target.value),
22283
+ placeholder: labels.placeholder,
22284
+ disabled: sending,
22285
+ onKeyDown: (event) => {
22286
+ if (event.key === "Enter" && !event.shiftKey) {
22287
+ event.preventDefault();
22288
+ event.currentTarget.form?.requestSubmit();
22289
+ }
22290
+ }
22291
+ }
22292
+ ),
22293
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: !draft.trim() || sending, children: sending ? labels.sending : labels.send })
22294
+ ] })
22295
+ ] }) : null,
22296
+ /* @__PURE__ */ jsxs("button", { className: "setto-chat__launcher", type: "button", onClick: () => setOpen(true), "aria-expanded": open, children: [
22297
+ /* @__PURE__ */ jsx("span", { className: "setto-chat__launcher-mark", "aria-hidden": "true", children: "✦" }),
22298
+ /* @__PURE__ */ jsx("span", { children: labels.open })
22299
+ ] })
22300
+ ] });
22301
+ }
22302
+ function replaceLastAssistant(messages, content) {
22303
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
22304
+ if (messages[index]?.role === "assistant") {
22305
+ const next = [...messages];
22306
+ next[index] = { role: "assistant", content };
22307
+ return next;
22308
+ }
22309
+ }
22310
+ return [...messages, { role: "assistant", content }];
22311
+ }
22312
+ function useChatStyles() {
22313
+ useInsertionEffect(() => {
22314
+ const style = document.createElement("style");
22315
+ style.dataset.settoChat = "";
22316
+ style.textContent = `
22317
+ .setto-chat { position: fixed; right: max(16px, env(safe-area-inset-right)); bottom: max(16px, env(safe-area-inset-bottom)); z-index: 2147483000; color: #231d17; font-family: Archivo, ui-sans-serif, sans-serif; }
22318
+ .setto-chat *, .setto-chat *::before, .setto-chat *::after { box-sizing: border-box; }
22319
+ .setto-chat__launcher { border: 0; min-height: 54px; max-width: min(290px, calc(100vw - 32px)); border-radius: 999px; padding: 8px 18px 8px 9px; display: flex; align-items: center; gap: 10px; background: #251d16; color: #fff9ef; box-shadow: 0 14px 40px rgba(35,29,23,.28); font: 600 14px/1 Archivo, ui-sans-serif, sans-serif; cursor: pointer; transition: transform .18s ease, box-shadow .18s ease; }
22320
+ .setto-chat__launcher:hover { transform: translateY(-2px); box-shadow: 0 18px 46px rgba(35,29,23,.36); }
22321
+ .setto-chat__launcher-mark { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; background: #df6540; color: #fff8eb; font-size: 18px; }
22322
+ .setto-chat__panel { width: min(390px, calc(100vw - 32px)); height: min(590px, calc(100dvh - 104px)); margin-bottom: 12px; overflow: hidden; display: grid; grid-template-rows: auto 1fr auto auto auto; border: 1px solid rgba(37,29,22,.15); border-radius: 20px; background: #fffaf1; box-shadow: 0 22px 70px rgba(35,29,23,.3); }
22323
+ .setto-chat__header { padding: 20px 20px 16px; display: flex; justify-content: space-between; gap: 14px; border-bottom: 1px solid #eadfce; background: radial-gradient(circle at 0 0, #f4d4a9, transparent 52%), #fffaf1; }
22324
+ .setto-chat__eyebrow { margin: 0 0 4px; color: #b54628; font-size: 10px; font-weight: 700; letter-spacing: .14em; }
22325
+ .setto-chat__header h2 { margin: 0; color: #211a14; font-family: "Instrument Serif", Georgia, serif; font-size: 29px; font-weight: 400; line-height: 1; }
22326
+ .setto-chat__header p:not(.setto-chat__eyebrow) { margin: 7px 0 0; max-width: 280px; color: #70665a; font-size: 12px; line-height: 1.35; }
22327
+ .setto-chat__close { width: 32px; height: 32px; border: 0; border-radius: 50%; background: rgba(64,47,31,.07); color: #4f453a; font: 300 28px/27px Georgia, serif; cursor: pointer; }
22328
+ .setto-chat__log { min-height: 0; overflow: auto; padding: 18px; display: flex; flex-direction: column; gap: 10px; scroll-behavior: smooth; }
22329
+ .setto-chat__message { max-width: 88%; padding: 11px 13px; border-radius: 14px; font-size: 14px; line-height: 1.48; white-space: pre-wrap; overflow-wrap: anywhere; }
22330
+ .setto-chat__message--assistant { align-self: flex-start; background: #efe7da; color: #302820; border-bottom-left-radius: 3px; }
22331
+ .setto-chat__message--visitor { align-self: flex-end; background: #2b221b; color: #fffaf1; border-bottom-right-radius: 3px; }
22332
+ .setto-chat__typing { display: inline-block; min-width: 24px; animation: setto-chat-pulse 1.1s ease-in-out infinite; letter-spacing: 2px; }
22333
+ @keyframes setto-chat-pulse { 50% { opacity: .3; } }
22334
+ .setto-chat__contact { padding: 0 18px 12px; display: flex; gap: 14px; flex-wrap: wrap; }
22335
+ .setto-chat__contact button { border: 0; padding: 0; background: transparent; color: #b54628; font: 600 12px/1.4 Archivo, ui-sans-serif, sans-serif; cursor: pointer; }
22336
+ .setto-chat__lead { display: grid; gap: 8px; max-height: 255px; overflow: auto; padding: 12px 18px; border-top: 1px solid #eadfce; background: #fff7ed; font-size: 12px; line-height: 1.4; }
22337
+ .setto-chat__lead p { margin: 0; color: #70665a; }
22338
+ .setto-chat__lead-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
22339
+ .setto-chat__lead label { display: grid; gap: 3px; color: #5f5347; font-weight: 600; }
22340
+ .setto-chat__lead input:not([type="checkbox"]) { width: 100%; border: 1px solid #d8ccba; border-radius: 7px; background: #fffdf8; color: #231d17; padding: 7px; font: 13px Archivo, ui-sans-serif, sans-serif; }
22341
+ .setto-chat__lead .setto-chat__consent { grid-template-columns: auto 1fr; align-items: start; font-weight: 400; }
22342
+ .setto-chat__lead button { justify-self: start; min-height: 34px; border: 0; border-radius: 8px; padding: 0 12px; background: #b54628; color: #fffaf1; font: 600 12px Archivo, ui-sans-serif, sans-serif; cursor: pointer; }
22343
+ .setto-chat__lead button:disabled { opacity: .5; cursor: default; }
22344
+ .setto-chat__lead-success { color: #30683d !important; font-weight: 600; }
22345
+ .setto-chat__lead-error { color: #9b2f1e !important; font-weight: 600; }
22346
+ .setto-chat__composer { padding: 12px; display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 8px; border-top: 1px solid #eadfce; background: #fffdf8; }
22347
+ .setto-chat__composer textarea { width: 100%; max-height: 100px; resize: none; border: 1px solid #d8ccba; border-radius: 10px; background: #fffaf1; color: #231d17; padding: 10px; font: 14px/1.35 Archivo, ui-sans-serif, sans-serif; outline: none; }
22348
+ .setto-chat__composer textarea:focus { border-color: #b54628; box-shadow: 0 0 0 3px rgba(181,70,40,.12); }
22349
+ .setto-chat__composer button { min-height: 40px; border: 0; border-radius: 9px; padding: 0 13px; background: #b54628; color: #fffaf1; font: 600 13px Archivo, ui-sans-serif, sans-serif; cursor: pointer; }
22350
+ .setto-chat__composer button:disabled { opacity: .5; cursor: default; }
22351
+ .setto-chat__sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
22352
+ @media (max-width: 480px) { .setto-chat { right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); } .setto-chat__panel { width: calc(100vw - 24px); height: min(610px, calc(100dvh - 88px)); } .setto-chat__launcher { min-height: 50px; } .setto-chat__lead-grid { grid-template-columns: 1fr; } }
22353
+ @media (prefers-reduced-motion: reduce) { .setto-chat *, .setto-chat *::before, .setto-chat *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
22354
+ `;
22355
+ document.head.appendChild(style);
22356
+ return () => style.remove();
22357
+ }, []);
22358
+ }
22359
+ const chatModule = {
22360
+ id: "chat",
22361
+ label: "Chat"
22362
+ };
22035
22363
  const SettoContext = createContext(null);
22036
22364
  function SettoProvider({ config, children }) {
22037
22365
  const supabase = useMemo(() => getSupabase(config.supabase), [config.supabase]);
@@ -22141,6 +22469,7 @@ function SettoProvider({ config, children }) {
22141
22469
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
22142
22470
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
22143
22471
  /* @__PURE__ */ jsx(SettoChrome, {}),
22472
+ /* @__PURE__ */ jsx(SettoChat, {}),
22144
22473
  /* @__PURE__ */ jsx(AutoLanguageSwitcher, { languages })
22145
22474
  ] }) });
22146
22475
  }
@@ -23909,7 +24238,7 @@ const domainModule = {
23909
24238
  id: "domain",
23910
24239
  label: "Domene"
23911
24240
  };
23912
- const SETTO_MODULES = [formsModule, domainModule];
24241
+ const SETTO_MODULES = [formsModule, chatModule, domainModule];
23913
24242
  function SettoVideo({ srcKey, posterKey, title, className }) {
23914
24243
  const { t, i18n } = useTranslation();
23915
24244
  const { store } = useSetto();
@@ -24949,6 +25278,7 @@ export {
24949
25278
  SettoAnimation,
24950
25279
  SettoBlock,
24951
25280
  SettoBlockTree,
25281
+ SettoChat,
24952
25282
  SettoChrome,
24953
25283
  SettoContext,
24954
25284
  SettoForm,