@sevenfold/setto-client 0.16.0 → 0.17.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/LanguageSwitcher.d.ts +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/lib/languages.d.ts +43 -0
- package/dist/setto-client.js +251 -2
- package/dist/setto-client.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control a visitor uses to read the site in their own language.
|
|
3
|
+
*
|
|
4
|
+
* Mounted by `SettoProvider` rather than placed by the agent, and only when
|
|
5
|
+
* the site actually speaks more than one language — so it costs a Norwegian
|
|
6
|
+
* plumber's site nothing, and a site that gains a second language grows the
|
|
7
|
+
* control without anybody editing a header.
|
|
8
|
+
*
|
|
9
|
+
* That decision is worth stating because it is a trade. Auto-mounting means
|
|
10
|
+
* the switcher cannot sit inside a design it knows nothing about, so it sits
|
|
11
|
+
* in a corner instead: quiet, small, and out of the way of whatever the agent
|
|
12
|
+
* built. A site that wants it in its own header can render `<SettoLanguages>`
|
|
13
|
+
* anywhere and pass `inline`, and the floating one stands down.
|
|
14
|
+
*
|
|
15
|
+
* Styling is inline for the same reason the editing chrome's is: this renders
|
|
16
|
+
* inside a page whose stylesheet Setto did not write, and most of these sites
|
|
17
|
+
* ship Tailwind's Preflight, which would otherwise strip a bare `<button>`
|
|
18
|
+
* back to nothing.
|
|
19
|
+
*/
|
|
20
|
+
export interface SettoLanguagesProps {
|
|
21
|
+
/** Every language the site speaks, primary first. */
|
|
22
|
+
languages: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* Rendered in the flow of the page rather than fixed in a corner.
|
|
25
|
+
*
|
|
26
|
+
* Also tells the auto-mounted copy to stand down, so a site that places its
|
|
27
|
+
* own switcher does not end up with two.
|
|
28
|
+
*/
|
|
29
|
+
inline?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function SettoLanguages({ languages, inline }: SettoLanguagesProps): import("react/jsx-runtime").JSX.Element | null;
|
|
32
|
+
/**
|
|
33
|
+
* The floating copy, mounted by the provider.
|
|
34
|
+
*
|
|
35
|
+
* Renders nothing when the site places its own, and nothing when there is only
|
|
36
|
+
* one language to be in.
|
|
37
|
+
*/
|
|
38
|
+
export declare function AutoLanguageSwitcher({ languages }: {
|
|
39
|
+
languages: readonly string[];
|
|
40
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { SettoLanguages } from './LanguageSwitcher';
|
|
2
|
+
export { DEFAULT_LANGUAGE, languageName, pickInitialLanguage, siteLanguages, } from './lib/languages';
|
|
1
3
|
export { SettoProvider, useSetto } from './provider';
|
|
2
4
|
export { GuestEditProvider, useGuestEdit } from './guest-edit';
|
|
3
5
|
export { SettoChrome, useSettoChromeSlot } from './chrome';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which languages a site speaks, and which one a visitor is reading.
|
|
3
|
+
*
|
|
4
|
+
* Every Setto site has been Norwegian-only, not because the machinery could
|
|
5
|
+
* not hold more — `I18nStore` has taken a language argument since the
|
|
6
|
+
* beginning and the publish path already serialises one bundle per language —
|
|
7
|
+
* but because nothing ever told a site it had a second one, and nothing gave a
|
|
8
|
+
* visitor a way to ask for it.
|
|
9
|
+
*
|
|
10
|
+
* The choice is deliberately per browser and not per URL. A path prefix
|
|
11
|
+
* (`/en/…`) is the better answer for search engines and the wrong answer for
|
|
12
|
+
* this pipeline today: every site is a single-page app whose routes the agent
|
|
13
|
+
* writes, and quietly doubling every route is not a thing to do to thirteen
|
|
14
|
+
* live sites in one release. Recorded here rather than hidden: when these
|
|
15
|
+
* sites want to be found in two languages, this is the decision to revisit.
|
|
16
|
+
*/
|
|
17
|
+
/** Norwegian is the floor: every site has it, and it is what the agent writes. */
|
|
18
|
+
export declare const DEFAULT_LANGUAGE = "no";
|
|
19
|
+
/** «Norsk», or the tag itself when we have no name for it. */
|
|
20
|
+
export declare function languageName(tag: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The languages this site actually speaks, primary first, no duplicates.
|
|
23
|
+
*
|
|
24
|
+
* A configured list that forgets to include the primary language is a site
|
|
25
|
+
* whose own copy is unreachable, so the primary is inserted rather than
|
|
26
|
+
* trusted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function siteLanguages(configured: readonly string[] | undefined, primary?: string): string[];
|
|
29
|
+
/** What this browser last chose here, if anything. */
|
|
30
|
+
export declare function readStoredLanguage(): string | null;
|
|
31
|
+
export declare function storeLanguage(tag: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* The language to open in.
|
|
34
|
+
*
|
|
35
|
+
* Their own previous choice first — it is the only signal that is definitely
|
|
36
|
+
* about this site. Then what the browser asks for, matched on the base tag so
|
|
37
|
+
* `en-GB` finds `en`. Then the site's primary, which is always present.
|
|
38
|
+
*
|
|
39
|
+
* Note what is *not* here: no geolocation, and no guessing from a domain. A
|
|
40
|
+
* Norwegian abroad is still Norwegian, and a site that decides it knows better
|
|
41
|
+
* than `Accept-Language` is a site people fight with.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pickInitialLanguage(available: readonly string[], browserLanguages?: readonly string[], stored?: string | null): string;
|
package/dist/setto-client.js
CHANGED
|
@@ -1,9 +1,242 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1, useId, useInsertionEffect } from "react";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
import * as ReactDOM from "react-dom";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
|
+
const STORAGE_KEY$1 = "setto.lang";
|
|
8
|
+
const DEFAULT_LANGUAGE = "no";
|
|
9
|
+
const ENDONYMS = {
|
|
10
|
+
no: "Norsk",
|
|
11
|
+
nb: "Norsk",
|
|
12
|
+
nn: "Nynorsk",
|
|
13
|
+
en: "English",
|
|
14
|
+
sv: "Svenska",
|
|
15
|
+
da: "Dansk",
|
|
16
|
+
de: "Deutsch",
|
|
17
|
+
fr: "Français",
|
|
18
|
+
es: "Español",
|
|
19
|
+
it: "Italiano",
|
|
20
|
+
nl: "Nederlands",
|
|
21
|
+
pl: "Polski",
|
|
22
|
+
pt: "Português",
|
|
23
|
+
fi: "Suomi",
|
|
24
|
+
is: "Íslenska",
|
|
25
|
+
lt: "Lietuvių",
|
|
26
|
+
uk: "Українська",
|
|
27
|
+
ru: "Русский",
|
|
28
|
+
ar: "العربية",
|
|
29
|
+
tr: "Türkçe",
|
|
30
|
+
so: "Soomaali",
|
|
31
|
+
ti: "ትግርኛ",
|
|
32
|
+
th: "ไทย",
|
|
33
|
+
vi: "Tiếng Việt",
|
|
34
|
+
zh: "中文",
|
|
35
|
+
ja: "日本語"
|
|
36
|
+
};
|
|
37
|
+
function languageName(tag) {
|
|
38
|
+
const base = tag.toLowerCase().split("-")[0] ?? tag;
|
|
39
|
+
return ENDONYMS[base] ?? tag.toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
function siteLanguages(configured, primary = DEFAULT_LANGUAGE) {
|
|
42
|
+
const out = [primary];
|
|
43
|
+
for (const tag of configured ?? []) {
|
|
44
|
+
if (tag && !out.includes(tag)) out.push(tag);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function readStoredLanguage() {
|
|
49
|
+
try {
|
|
50
|
+
return window.localStorage.getItem(STORAGE_KEY$1);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function storeLanguage(tag) {
|
|
56
|
+
try {
|
|
57
|
+
window.localStorage.setItem(STORAGE_KEY$1, tag);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function pickInitialLanguage(available, browserLanguages = typeof navigator === "undefined" ? [] : navigator.languages ?? [navigator.language], stored = null) {
|
|
62
|
+
const fallback = available[0] ?? DEFAULT_LANGUAGE;
|
|
63
|
+
if (stored && available.includes(stored)) return stored;
|
|
64
|
+
for (const wanted of browserLanguages) {
|
|
65
|
+
const base = wanted.toLowerCase().split("-")[0];
|
|
66
|
+
if (!base) continue;
|
|
67
|
+
const hit = available.find((tag) => tag.toLowerCase().split("-")[0] === base);
|
|
68
|
+
if (hit) return hit;
|
|
69
|
+
if ((base === "nb" || base === "nn" || base === "no") && available.includes("no")) {
|
|
70
|
+
return "no";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
const INLINE_MARKER = "data-setto-languages-inline";
|
|
76
|
+
function SettoLanguages({ languages, inline = false }) {
|
|
77
|
+
const { i18n } = useTranslation();
|
|
78
|
+
const [open, setOpen] = useState(false);
|
|
79
|
+
const ref = useRef(null);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!inline) return;
|
|
82
|
+
const root = document.documentElement;
|
|
83
|
+
const count = Number(root.getAttribute(INLINE_MARKER) ?? "0") + 1;
|
|
84
|
+
root.setAttribute(INLINE_MARKER, String(count));
|
|
85
|
+
return () => {
|
|
86
|
+
const left = Number(root.getAttribute(INLINE_MARKER) ?? "1") - 1;
|
|
87
|
+
if (left > 0) root.setAttribute(INLINE_MARKER, String(left));
|
|
88
|
+
else root.removeAttribute(INLINE_MARKER);
|
|
89
|
+
};
|
|
90
|
+
}, [inline]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open) return;
|
|
93
|
+
const onDown = (e) => {
|
|
94
|
+
if (!ref.current?.contains(e.target)) setOpen(false);
|
|
95
|
+
};
|
|
96
|
+
const onKey = (e) => {
|
|
97
|
+
if (e.key === "Escape") setOpen(false);
|
|
98
|
+
};
|
|
99
|
+
document.addEventListener("mousedown", onDown);
|
|
100
|
+
document.addEventListener("keydown", onKey);
|
|
101
|
+
return () => {
|
|
102
|
+
document.removeEventListener("mousedown", onDown);
|
|
103
|
+
document.removeEventListener("keydown", onKey);
|
|
104
|
+
};
|
|
105
|
+
}, [open]);
|
|
106
|
+
if (languages.length < 2) return null;
|
|
107
|
+
const current = i18n.language;
|
|
108
|
+
const choose = (tag) => {
|
|
109
|
+
setOpen(false);
|
|
110
|
+
if (tag === current) return;
|
|
111
|
+
storeLanguage(tag);
|
|
112
|
+
void i18n.changeLanguage(tag);
|
|
113
|
+
document.documentElement.lang = tag;
|
|
114
|
+
};
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
|
|
116
|
+
/* @__PURE__ */ jsxs(
|
|
117
|
+
"button",
|
|
118
|
+
{
|
|
119
|
+
type: "button",
|
|
120
|
+
onClick: () => setOpen((o) => !o),
|
|
121
|
+
"aria-expanded": open,
|
|
122
|
+
"aria-haspopup": "listbox",
|
|
123
|
+
"aria-label": `Språk: ${languageName(current)}`,
|
|
124
|
+
style: triggerStyle,
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsx(GlobeIcon, {}),
|
|
127
|
+
languageName(current)
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
open ? /* @__PURE__ */ jsx("ul", { role: "listbox", style: inline ? inlineListStyle : floatingListStyle, children: languages.map((tag) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
role: "option",
|
|
136
|
+
"aria-selected": tag === current,
|
|
137
|
+
onClick: () => choose(tag),
|
|
138
|
+
style: {
|
|
139
|
+
...optionStyle,
|
|
140
|
+
fontWeight: tag === current ? 600 : 400
|
|
141
|
+
},
|
|
142
|
+
children: languageName(tag)
|
|
143
|
+
}
|
|
144
|
+
) }, tag)) }) : null
|
|
145
|
+
] });
|
|
146
|
+
}
|
|
147
|
+
function AutoLanguageSwitcher({ languages }) {
|
|
148
|
+
const [inlineElsewhere, setInlineElsewhere] = useState(false);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const check = () => setInlineElsewhere(document.documentElement.hasAttribute(INLINE_MARKER));
|
|
151
|
+
check();
|
|
152
|
+
const observer = new MutationObserver(check);
|
|
153
|
+
observer.observe(document.documentElement, {
|
|
154
|
+
attributes: true,
|
|
155
|
+
attributeFilter: [INLINE_MARKER]
|
|
156
|
+
});
|
|
157
|
+
return () => observer.disconnect();
|
|
158
|
+
}, []);
|
|
159
|
+
if (inlineElsewhere || languages.length < 2) return null;
|
|
160
|
+
return /* @__PURE__ */ jsx(SettoLanguages, { languages });
|
|
161
|
+
}
|
|
162
|
+
function GlobeIcon() {
|
|
163
|
+
return /* @__PURE__ */ jsxs(
|
|
164
|
+
"svg",
|
|
165
|
+
{
|
|
166
|
+
width: "15",
|
|
167
|
+
height: "15",
|
|
168
|
+
viewBox: "0 0 24 24",
|
|
169
|
+
fill: "none",
|
|
170
|
+
stroke: "currentColor",
|
|
171
|
+
strokeWidth: "1.8",
|
|
172
|
+
strokeLinecap: "round",
|
|
173
|
+
strokeLinejoin: "round",
|
|
174
|
+
"aria-hidden": "true",
|
|
175
|
+
children: [
|
|
176
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
|
|
177
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.4 2.6 3.6 5.6 3.6 9s-1.2 6.4-3.6 9c-2.4-2.6-3.6-5.6-3.6-9S9.6 5.6 12 3z" })
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const floatingWrapStyle = {
|
|
183
|
+
position: "fixed",
|
|
184
|
+
left: "max(16px, env(safe-area-inset-left))",
|
|
185
|
+
bottom: "max(16px, env(safe-area-inset-bottom))",
|
|
186
|
+
zIndex: 2147482e3,
|
|
187
|
+
fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
|
|
188
|
+
};
|
|
189
|
+
const inlineWrapStyle = {
|
|
190
|
+
position: "relative",
|
|
191
|
+
display: "inline-block",
|
|
192
|
+
fontFamily: "inherit"
|
|
193
|
+
};
|
|
194
|
+
const triggerStyle = {
|
|
195
|
+
display: "inline-flex",
|
|
196
|
+
alignItems: "center",
|
|
197
|
+
gap: 7,
|
|
198
|
+
minHeight: 40,
|
|
199
|
+
padding: "8px 14px",
|
|
200
|
+
borderRadius: 999,
|
|
201
|
+
border: "1px solid rgba(127, 127, 127, 0.32)",
|
|
202
|
+
// Translucent over whatever the site put behind it, and legible either way.
|
|
203
|
+
background: "rgba(255, 255, 255, 0.82)",
|
|
204
|
+
backdropFilter: "blur(8px)",
|
|
205
|
+
WebkitBackdropFilter: "blur(8px)",
|
|
206
|
+
color: "#1a1a1a",
|
|
207
|
+
fontSize: 13.5,
|
|
208
|
+
fontWeight: 500,
|
|
209
|
+
lineHeight: 1,
|
|
210
|
+
cursor: "pointer",
|
|
211
|
+
boxShadow: "0 4px 14px rgba(0, 0, 0, 0.12)"
|
|
212
|
+
};
|
|
213
|
+
const listBase = {
|
|
214
|
+
position: "absolute",
|
|
215
|
+
margin: 0,
|
|
216
|
+
padding: 4,
|
|
217
|
+
listStyle: "none",
|
|
218
|
+
minWidth: 150,
|
|
219
|
+
borderRadius: 12,
|
|
220
|
+
border: "1px solid rgba(127, 127, 127, 0.24)",
|
|
221
|
+
background: "#ffffff",
|
|
222
|
+
boxShadow: "0 12px 32px rgba(0, 0, 0, 0.18)",
|
|
223
|
+
zIndex: 1
|
|
224
|
+
};
|
|
225
|
+
const floatingListStyle = { ...listBase, left: 0, bottom: "calc(100% + 8px)" };
|
|
226
|
+
const inlineListStyle = { ...listBase, left: 0, top: "calc(100% + 8px)" };
|
|
227
|
+
const optionStyle = {
|
|
228
|
+
display: "block",
|
|
229
|
+
width: "100%",
|
|
230
|
+
minHeight: 40,
|
|
231
|
+
padding: "9px 12px",
|
|
232
|
+
border: 0,
|
|
233
|
+
borderRadius: 8,
|
|
234
|
+
background: "transparent",
|
|
235
|
+
color: "#1a1a1a",
|
|
236
|
+
fontSize: 13.5,
|
|
237
|
+
textAlign: "left",
|
|
238
|
+
cursor: "pointer"
|
|
239
|
+
};
|
|
7
240
|
function __rest(s, e) {
|
|
8
241
|
var t = {};
|
|
9
242
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
@@ -21767,6 +22000,16 @@ function SettoProvider({ config, children }) {
|
|
|
21767
22000
|
}, []);
|
|
21768
22001
|
const access = useSiteAccess(supabase, session, config.siteId);
|
|
21769
22002
|
const { i18n } = useTranslation();
|
|
22003
|
+
const languages = useMemo(
|
|
22004
|
+
() => siteLanguages(config.languages, config.languages?.[0]),
|
|
22005
|
+
[config.languages]
|
|
22006
|
+
);
|
|
22007
|
+
useEffect(() => {
|
|
22008
|
+
if (!i18n || languages.length < 2) return;
|
|
22009
|
+
const wanted = pickInitialLanguage(languages, void 0, readStoredLanguage());
|
|
22010
|
+
if (wanted && wanted !== i18n.language) void i18n.changeLanguage(wanted);
|
|
22011
|
+
document.documentElement.lang = wanted;
|
|
22012
|
+
}, [i18n, languages]);
|
|
21770
22013
|
const storeRef = useRef(null);
|
|
21771
22014
|
const themeStoreRef = useRef(null);
|
|
21772
22015
|
const assetStoreRef = useRef(null);
|
|
@@ -21809,7 +22052,8 @@ function SettoProvider({ config, children }) {
|
|
|
21809
22052
|
const editShellActive = editMode && !!value.store;
|
|
21810
22053
|
return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
21811
22054
|
/* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
|
|
21812
|
-
/* @__PURE__ */ jsx(SettoChrome, {})
|
|
22055
|
+
/* @__PURE__ */ jsx(SettoChrome, {}),
|
|
22056
|
+
/* @__PURE__ */ jsx(AutoLanguageSwitcher, { languages })
|
|
21813
22057
|
] }) });
|
|
21814
22058
|
}
|
|
21815
22059
|
function useSetto() {
|
|
@@ -24603,6 +24847,7 @@ export {
|
|
|
24603
24847
|
BLOCK_REGISTRY,
|
|
24604
24848
|
BLOCK_TYPES,
|
|
24605
24849
|
CHECKOUT_INTENT,
|
|
24850
|
+
DEFAULT_LANGUAGE,
|
|
24606
24851
|
EMPTY_I18N_STORE_SNAPSHOT,
|
|
24607
24852
|
FAB_INSET,
|
|
24608
24853
|
FAB_SIZE,
|
|
@@ -24621,6 +24866,7 @@ export {
|
|
|
24621
24866
|
SettoForm,
|
|
24622
24867
|
SettoIcon,
|
|
24623
24868
|
SettoImage,
|
|
24869
|
+
SettoLanguages,
|
|
24624
24870
|
SettoMark,
|
|
24625
24871
|
SettoProvider,
|
|
24626
24872
|
SettoRepeater,
|
|
@@ -24640,11 +24886,14 @@ export {
|
|
|
24640
24886
|
isAdminRoute,
|
|
24641
24887
|
isFormFieldType,
|
|
24642
24888
|
isLinkContext,
|
|
24889
|
+
languageName,
|
|
24643
24890
|
nodeChildren,
|
|
24644
24891
|
parseBlockTree,
|
|
24892
|
+
pickInitialLanguage,
|
|
24645
24893
|
readLocaleFields,
|
|
24646
24894
|
settoSubmissionUrl,
|
|
24647
24895
|
siteHomeUrl,
|
|
24896
|
+
siteLanguages,
|
|
24648
24897
|
useCoarsePointer,
|
|
24649
24898
|
useGuestEdit,
|
|
24650
24899
|
useSectionTheme,
|