@hadoku/task-ui-components 0.0.0-stage → 4.20.4
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/LICENSE +21 -0
- package/README.md +917 -2
- package/dist/app-header.css +288 -0
- package/dist/bento.css +143 -0
- package/dist/button.css +227 -0
- package/dist/components/AppHeader.d.ts +61 -0
- package/dist/components/AppHeader.d.ts.map +1 -0
- package/dist/components/AppHeader.js +23 -0
- package/dist/components/AppHeader.js.map +1 -0
- package/dist/components/BentoCard.d.ts +23 -0
- package/dist/components/BentoCard.d.ts.map +1 -0
- package/dist/components/BentoCard.js +49 -0
- package/dist/components/BentoCard.js.map +1 -0
- package/dist/components/BentoGrid.d.ts +24 -0
- package/dist/components/BentoGrid.d.ts.map +1 -0
- package/dist/components/BentoGrid.js +22 -0
- package/dist/components/BentoGrid.js.map +1 -0
- package/dist/components/Button.d.ts +28 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +51 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/ConnectedSettings.d.ts +53 -0
- package/dist/components/ConnectedSettings.d.ts.map +1 -0
- package/dist/components/ConnectedSettings.js +368 -0
- package/dist/components/ConnectedSettings.js.map +1 -0
- package/dist/components/ConnectedThemePicker.d.ts +52 -0
- package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
- package/dist/components/ConnectedThemePicker.js +32 -0
- package/dist/components/ConnectedThemePicker.js.map +1 -0
- package/dist/components/ContextMenu.d.ts +22 -0
- package/dist/components/ContextMenu.d.ts.map +1 -0
- package/dist/components/ContextMenu.js +23 -0
- package/dist/components/ContextMenu.js.map +1 -0
- package/dist/components/HadokuThemePicker.d.ts +27 -0
- package/dist/components/HadokuThemePicker.d.ts.map +1 -0
- package/dist/components/HadokuThemePicker.js +19 -0
- package/dist/components/HadokuThemePicker.js.map +1 -0
- package/dist/components/LoadingSkeleton.d.ts +14 -0
- package/dist/components/LoadingSkeleton.d.ts.map +1 -0
- package/dist/components/LoadingSkeleton.js +62 -0
- package/dist/components/LoadingSkeleton.js.map +1 -0
- package/dist/components/Modal.d.ts +39 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/Modal.js +106 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/NumberInput.d.ts +26 -0
- package/dist/components/NumberInput.d.ts.map +1 -0
- package/dist/components/NumberInput.js +131 -0
- package/dist/components/NumberInput.js.map +1 -0
- package/dist/components/Select.d.ts +52 -0
- package/dist/components/Select.d.ts.map +1 -0
- package/dist/components/Select.js +281 -0
- package/dist/components/Select.js.map +1 -0
- package/dist/components/SettingsModal.d.ts +69 -0
- package/dist/components/SettingsModal.d.ts.map +1 -0
- package/dist/components/SettingsModal.js +141 -0
- package/dist/components/SettingsModal.js.map +1 -0
- package/dist/components/ThemeIcons.d.ts +26 -0
- package/dist/components/ThemeIcons.d.ts.map +1 -0
- package/dist/components/ThemeIcons.js +74 -0
- package/dist/components/ThemeIcons.js.map +1 -0
- package/dist/components/ThemePicker.d.ts +28 -0
- package/dist/components/ThemePicker.d.ts.map +1 -0
- package/dist/components/ThemePicker.js +101 -0
- package/dist/components/ThemePicker.js.map +1 -0
- package/dist/components/Toast.d.ts +13 -0
- package/dist/components/Toast.d.ts.map +1 -0
- package/dist/components/Toast.js +70 -0
- package/dist/components/Toast.js.map +1 -0
- package/dist/components/Toaster.d.ts +19 -0
- package/dist/components/Toaster.d.ts.map +1 -0
- package/dist/components/Toaster.js +19 -0
- package/dist/components/Toaster.js.map +1 -0
- package/dist/components/Tooltip.d.ts +49 -0
- package/dist/components/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip.js +143 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/context-menu.css +11 -0
- package/dist/controls.css +137 -0
- package/dist/hooks/useReducedMotion.d.ts +7 -0
- package/dist/hooks/useReducedMotion.d.ts.map +1 -0
- package/dist/hooks/useReducedMotion.js +25 -0
- package/dist/hooks/useReducedMotion.js.map +1 -0
- package/dist/hooks/useToast.d.ts +18 -0
- package/dist/hooks/useToast.d.ts.map +1 -0
- package/dist/hooks/useToast.js +28 -0
- package/dist/hooks/useToast.js.map +1 -0
- package/dist/icon-btn.css +64 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/menuPlacement.d.ts +16 -0
- package/dist/lib/menuPlacement.d.ts.map +1 -0
- package/dist/lib/menuPlacement.js +19 -0
- package/dist/lib/menuPlacement.js.map +1 -0
- package/dist/lib/selectMenus.d.ts +30 -0
- package/dist/lib/selectMenus.d.ts.map +1 -0
- package/dist/lib/selectMenus.js +236 -0
- package/dist/lib/selectMenus.js.map +1 -0
- package/dist/lib/settingsClient.d.ts +144 -0
- package/dist/lib/settingsClient.d.ts.map +1 -0
- package/dist/lib/settingsClient.js +157 -0
- package/dist/lib/settingsClient.js.map +1 -0
- package/dist/lib/themeScope.d.ts +19 -0
- package/dist/lib/themeScope.d.ts.map +1 -0
- package/dist/lib/themeScope.js +10 -0
- package/dist/lib/themeScope.js.map +1 -0
- package/dist/lib/titleTooltips.d.ts +38 -0
- package/dist/lib/titleTooltips.d.ts.map +1 -0
- package/dist/lib/titleTooltips.js +166 -0
- package/dist/lib/titleTooltips.js.map +1 -0
- package/dist/lib/tooltipPlacement.d.ts +14 -0
- package/dist/lib/tooltipPlacement.d.ts.map +1 -0
- package/dist/lib/tooltipPlacement.js +34 -0
- package/dist/lib/tooltipPlacement.js.map +1 -0
- package/dist/modal.css +129 -0
- package/dist/number-input.css +131 -0
- package/dist/scrollbar.css +165 -0
- package/dist/select.css +251 -0
- package/dist/styles.css +1866 -0
- package/dist/task-ui-components.css +1866 -0
- package/dist/theme-picker.css +219 -0
- package/dist/toaster.css +159 -0
- package/dist/tooltip.css +103 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +60 -4
- package/src/app-header.css +288 -0
- package/src/bento.css +143 -0
- package/src/button.css +227 -0
- package/src/components/AppHeader.tsx +87 -0
- package/src/components/BentoCard.tsx +72 -0
- package/src/components/BentoGrid.tsx +48 -0
- package/src/components/Button.tsx +81 -0
- package/src/components/ConnectedSettings.tsx +545 -0
- package/src/components/ConnectedThemePicker.tsx +79 -0
- package/src/components/ContextMenu.tsx +48 -0
- package/src/components/HadokuThemePicker.tsx +39 -0
- package/src/components/LoadingSkeleton.tsx +84 -0
- package/src/components/Modal.tsx +171 -0
- package/src/components/NumberInput.tsx +161 -0
- package/src/components/Select.tsx +359 -0
- package/src/components/SettingsModal.tsx +235 -0
- package/src/components/ThemeIcons.tsx +127 -0
- package/src/components/ThemePicker.tsx +149 -0
- package/src/components/Toast.tsx +92 -0
- package/src/components/Toaster.tsx +43 -0
- package/src/components/Tooltip.tsx +212 -0
- package/src/context-menu.css +11 -0
- package/src/controls.css +137 -0
- package/src/hooks/useReducedMotion.ts +36 -0
- package/src/hooks/useToast.ts +57 -0
- package/src/icon-btn.css +64 -0
- package/src/index.ts +153 -0
- package/src/lib/menuPlacement.ts +31 -0
- package/src/lib/selectMenus.ts +303 -0
- package/src/lib/settingsClient.ts +425 -0
- package/src/lib/themeScope.ts +23 -0
- package/src/lib/titleTooltips.ts +236 -0
- package/src/lib/tooltipPlacement.ts +59 -0
- package/src/modal.css +129 -0
- package/src/number-input.css +131 -0
- package/src/scrollbar.css +165 -0
- package/src/select.css +251 -0
- package/src/theme-picker.css +219 -0
- package/src/toaster.css +159 -0
- package/src/tooltip.css +103 -0
- package/src/types/index.ts +43 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { themeScopeOf } from "./themeScope.js";
|
|
2
|
+
import { placeMenu } from "./menuPlacement.js";
|
|
3
|
+
const TYPEAHEAD_RESET_MS = 600;
|
|
4
|
+
const LIST_ID = "hdk-select-menu";
|
|
5
|
+
const CHECK_SVG = '<svg class="hdk-icon hdk-select__check" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>';
|
|
6
|
+
const OPEN_KEYS = /* @__PURE__ */ new Set(["ArrowDown", "ArrowUp", " ", "F4"]);
|
|
7
|
+
let uninstallCurrent = null;
|
|
8
|
+
function eligible(el) {
|
|
9
|
+
return el instanceof HTMLSelectElement && !el.multiple && el.size <= 1 && !el.disabled && !el.closest("[data-native-select]");
|
|
10
|
+
}
|
|
11
|
+
function installSelectMenus() {
|
|
12
|
+
if (uninstallCurrent) return uninstallCurrent;
|
|
13
|
+
let lastPointer = "mouse";
|
|
14
|
+
let select = null;
|
|
15
|
+
let list = null;
|
|
16
|
+
let options = [];
|
|
17
|
+
let items = [];
|
|
18
|
+
let active = -1;
|
|
19
|
+
const typeahead = { text: "", at: 0 };
|
|
20
|
+
function setActive(i) {
|
|
21
|
+
if (items[active]) items[active].classList.remove("hdk-select__option--active");
|
|
22
|
+
active = i;
|
|
23
|
+
const item = items[i];
|
|
24
|
+
if (!item || !list) return;
|
|
25
|
+
item.classList.add("hdk-select__option--active");
|
|
26
|
+
list.setAttribute("aria-activedescendant", item.id);
|
|
27
|
+
item.scrollIntoView({ block: "nearest" });
|
|
28
|
+
}
|
|
29
|
+
function step(from, dir) {
|
|
30
|
+
for (let i = 1; i <= options.length; i++) {
|
|
31
|
+
const next = (from + dir * i + options.length) % options.length;
|
|
32
|
+
if (!isOff(next)) return next;
|
|
33
|
+
}
|
|
34
|
+
return from;
|
|
35
|
+
}
|
|
36
|
+
const edge = (dir) => step(dir === 1 ? -1 : options.length, dir);
|
|
37
|
+
function matchTypeahead(key) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
typeahead.text = now - typeahead.at > TYPEAHEAD_RESET_MS ? key : typeahead.text + key;
|
|
40
|
+
typeahead.at = now;
|
|
41
|
+
const needle = typeahead.text.toLowerCase();
|
|
42
|
+
const start = Math.max(active, 0);
|
|
43
|
+
for (let i = 0; i < options.length; i++) {
|
|
44
|
+
const idx = (start + (typeahead.text.length === 1 ? i + 1 : i)) % options.length;
|
|
45
|
+
const o = options[idx];
|
|
46
|
+
if (!isOff(idx) && labelOf(o).toLowerCase().startsWith(needle)) return idx;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
const labelOf = (o) => o.label || o.text;
|
|
51
|
+
const isOff = (i) => options[i].disabled || options[i].parentElement?.disabled === true;
|
|
52
|
+
function place() {
|
|
53
|
+
if (!select || !list) return;
|
|
54
|
+
if (!select.isConnected) {
|
|
55
|
+
close(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const p = placeMenu(select.getBoundingClientRect(), list.scrollHeight);
|
|
59
|
+
list.style.left = `${p.left}px`;
|
|
60
|
+
list.style.minWidth = `${p.width}px`;
|
|
61
|
+
list.style.top = p.top === void 0 ? "" : `${p.top}px`;
|
|
62
|
+
list.style.bottom = p.bottom === void 0 ? "" : `${p.bottom}px`;
|
|
63
|
+
list.style.maxHeight = `${p.maxHeight}px`;
|
|
64
|
+
list.style.visibility = "";
|
|
65
|
+
}
|
|
66
|
+
function open(el, start) {
|
|
67
|
+
close(false);
|
|
68
|
+
const scope = themeScopeOf(el);
|
|
69
|
+
if (!scope) return;
|
|
70
|
+
select = el;
|
|
71
|
+
options = Array.from(el.options).filter((o) => !o.hidden);
|
|
72
|
+
if (options.length === 0) return;
|
|
73
|
+
list = document.createElement("ul");
|
|
74
|
+
list.id = LIST_ID;
|
|
75
|
+
list.setAttribute("role", "listbox");
|
|
76
|
+
list.tabIndex = -1;
|
|
77
|
+
list.className = "hdk-select__menu hdk-scroll hdk-scroll--thin";
|
|
78
|
+
const name = el.getAttribute("aria-label") ?? el.labels?.[0]?.textContent?.trim();
|
|
79
|
+
if (name) list.setAttribute("aria-label", name);
|
|
80
|
+
const cs = getComputedStyle(el);
|
|
81
|
+
list.style.fontSize = cs.fontSize;
|
|
82
|
+
list.style.fontFamily = cs.fontFamily;
|
|
83
|
+
list.style.visibility = "hidden";
|
|
84
|
+
let group = null;
|
|
85
|
+
items = options.map((o, i) => {
|
|
86
|
+
const g = o.parentElement instanceof HTMLOptGroupElement ? o.parentElement : null;
|
|
87
|
+
if (g && g !== group) {
|
|
88
|
+
const head = document.createElement("li");
|
|
89
|
+
head.setAttribute("role", "presentation");
|
|
90
|
+
head.className = "hdk-select__group";
|
|
91
|
+
head.textContent = g.label;
|
|
92
|
+
list?.append(head);
|
|
93
|
+
}
|
|
94
|
+
group = g;
|
|
95
|
+
const item = document.createElement("li");
|
|
96
|
+
item.id = `${LIST_ID}-${i}`;
|
|
97
|
+
item.setAttribute("role", "option");
|
|
98
|
+
const selected = o.index === el.selectedIndex;
|
|
99
|
+
item.setAttribute("aria-selected", String(selected));
|
|
100
|
+
if (o.disabled || g?.disabled) item.setAttribute("aria-disabled", "true");
|
|
101
|
+
item.className = `hdk-select__option${selected ? " hdk-select__option--selected" : ""}`;
|
|
102
|
+
const text = document.createElement("span");
|
|
103
|
+
text.className = "hdk-select__option-label";
|
|
104
|
+
text.textContent = labelOf(o);
|
|
105
|
+
const wrap = document.createElement("span");
|
|
106
|
+
wrap.className = "hdk-select__option-text";
|
|
107
|
+
wrap.append(text);
|
|
108
|
+
item.append(wrap);
|
|
109
|
+
if (selected) item.insertAdjacentHTML("beforeend", CHECK_SVG);
|
|
110
|
+
item.addEventListener("pointermove", () => {
|
|
111
|
+
if (!isOff(i) && i !== active) setActive(i);
|
|
112
|
+
});
|
|
113
|
+
item.addEventListener("click", () => choose(i));
|
|
114
|
+
list?.append(item);
|
|
115
|
+
return item;
|
|
116
|
+
});
|
|
117
|
+
list.addEventListener("keydown", onListKeyDown);
|
|
118
|
+
scope.appendChild(list);
|
|
119
|
+
el.setAttribute("aria-expanded", "true");
|
|
120
|
+
el.setAttribute("aria-controls", LIST_ID);
|
|
121
|
+
const current = options.findIndex((o) => o.index === el.selectedIndex);
|
|
122
|
+
setActive(start === "last" ? edge(-1) : current >= 0 && !isOff(current) ? current : edge(1));
|
|
123
|
+
place();
|
|
124
|
+
list.focus({ preventScroll: true });
|
|
125
|
+
window.addEventListener("scroll", onScroll, true);
|
|
126
|
+
window.addEventListener("resize", place);
|
|
127
|
+
}
|
|
128
|
+
function close(refocus) {
|
|
129
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
130
|
+
window.removeEventListener("resize", place);
|
|
131
|
+
list?.remove();
|
|
132
|
+
list = null;
|
|
133
|
+
if (!select) return;
|
|
134
|
+
select.removeAttribute("aria-expanded");
|
|
135
|
+
select.removeAttribute("aria-controls");
|
|
136
|
+
if (refocus) select.focus({ preventScroll: true });
|
|
137
|
+
select = null;
|
|
138
|
+
items = [];
|
|
139
|
+
options = [];
|
|
140
|
+
active = -1;
|
|
141
|
+
}
|
|
142
|
+
function choose(i) {
|
|
143
|
+
const el = select;
|
|
144
|
+
const o = options[i];
|
|
145
|
+
if (!el || !o || isOff(i)) return;
|
|
146
|
+
const changed = o.index !== el.selectedIndex;
|
|
147
|
+
close(true);
|
|
148
|
+
if (!changed) return;
|
|
149
|
+
el.selectedIndex = o.index;
|
|
150
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
151
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
152
|
+
}
|
|
153
|
+
const onScroll = (e) => {
|
|
154
|
+
if (list && e.target instanceof Node && list.contains(e.target)) return;
|
|
155
|
+
place();
|
|
156
|
+
};
|
|
157
|
+
function onListKeyDown(e) {
|
|
158
|
+
switch (e.key) {
|
|
159
|
+
case "ArrowDown":
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
setActive(step(active, 1));
|
|
162
|
+
break;
|
|
163
|
+
case "ArrowUp":
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
setActive(step(active, -1));
|
|
166
|
+
break;
|
|
167
|
+
case "Home":
|
|
168
|
+
case "PageUp":
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
setActive(edge(1));
|
|
171
|
+
break;
|
|
172
|
+
case "End":
|
|
173
|
+
case "PageDown":
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
setActive(edge(-1));
|
|
176
|
+
break;
|
|
177
|
+
case "Enter":
|
|
178
|
+
case " ":
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
choose(active);
|
|
181
|
+
break;
|
|
182
|
+
case "Escape":
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
close(true);
|
|
186
|
+
break;
|
|
187
|
+
case "Tab":
|
|
188
|
+
close(true);
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
192
|
+
const idx = matchTypeahead(e.key);
|
|
193
|
+
if (idx >= 0) setActive(idx);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const onPointerDown = (e) => {
|
|
198
|
+
lastPointer = e.pointerType;
|
|
199
|
+
const target = e.target;
|
|
200
|
+
if (list && !list.contains(target) && target !== select) close(false);
|
|
201
|
+
};
|
|
202
|
+
const onMouseDown = (e) => {
|
|
203
|
+
if (e.button !== 0 || lastPointer === "touch") return;
|
|
204
|
+
const el = e.target?.closest?.("select") ?? null;
|
|
205
|
+
if (!eligible(el)) return;
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
if (select === el) {
|
|
208
|
+
close(true);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
el.focus({ preventScroll: true });
|
|
212
|
+
open(el);
|
|
213
|
+
};
|
|
214
|
+
const onKeyDown = (e) => {
|
|
215
|
+
const el = e.target;
|
|
216
|
+
if (!eligible(el) || select === el) return;
|
|
217
|
+
if (!OPEN_KEYS.has(e.key) || e.ctrlKey || e.metaKey) return;
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
open(el, e.key === "ArrowUp" && !e.altKey ? "last" : void 0);
|
|
220
|
+
};
|
|
221
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
222
|
+
document.addEventListener("mousedown", onMouseDown, true);
|
|
223
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
224
|
+
uninstallCurrent = () => {
|
|
225
|
+
close(false);
|
|
226
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
227
|
+
document.removeEventListener("mousedown", onMouseDown, true);
|
|
228
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
229
|
+
uninstallCurrent = null;
|
|
230
|
+
};
|
|
231
|
+
return uninstallCurrent;
|
|
232
|
+
}
|
|
233
|
+
export {
|
|
234
|
+
installSelectMenus
|
|
235
|
+
};
|
|
236
|
+
//# sourceMappingURL=selectMenus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectMenus.js","sources":["../../src/lib/selectMenus.ts"],"sourcesContent":["/**\n * The themed list for every native <select> on a page, with no change to the\n * markup.\n *\n * import { installSelectMenus } from '@wolffm/task-ui-components/select-menus'\n * import '@wolffm/task-ui-components/select.css'\n * installSelectMenus()\n *\n * hadoku_site calls this once from its shell, which covers every\n * micro-frontend; an app does not call it itself. Framework-free and\n * React-free, like installTitleTooltips, for the same reason.\n *\n * A native select's closed box takes CSS, but its open list is drawn by the\n * OS and takes almost none. So a mouse press or an opening key on the select\n * is intercepted (preventDefault on mousedown is what stops the OS list) and\n * the same list <Select> renders opens instead, built from the select's own\n * <option>s and <optgroup>s at that moment.\n *\n * The <select> stays the source of truth. Choosing sets its selectedIndex and\n * fires `input` and `change` exactly as the OS list would, so every framework\n * (React's onChange included) and every plain listener sees an ordinary\n * change, and a form still submits the select's value.\n *\n * Left native on purpose:\n * - touch: a phone's own picker is the better control there\n * - `multiple` or `size > 1`, which render as an inline list, not a popup\n * - anything under `data-native-select`\n */\n\nimport { themeScopeOf } from './themeScope'\nimport { placeMenu } from './menuPlacement'\n\nconst TYPEAHEAD_RESET_MS = 600\nconst LIST_ID = 'hdk-select-menu'\n// `check` from the @wolffm/themes registry (Lucide circle-check), inlined so\n// this entry does not pull the whole registry into the shell.\nconst CHECK_SVG =\n '<svg class=\"hdk-icon hdk-select__check\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m9 12 2 2 4-4\" /></svg>'\n\nconst OPEN_KEYS = new Set(['ArrowDown', 'ArrowUp', ' ', 'F4'])\n\nlet uninstallCurrent: (() => void) | null = null\n\nfunction eligible(el: Element | null): el is HTMLSelectElement {\n return (\n el instanceof HTMLSelectElement &&\n !el.multiple &&\n el.size <= 1 &&\n !el.disabled &&\n !el.closest('[data-native-select]')\n )\n}\n\nexport function installSelectMenus(): () => void {\n if (uninstallCurrent) return uninstallCurrent\n\n let lastPointer = 'mouse'\n let select: HTMLSelectElement | null = null\n let list: HTMLUListElement | null = null\n let options: HTMLOptionElement[] = []\n let items: HTMLLIElement[] = []\n let active = -1\n const typeahead = { text: '', at: 0 }\n\n function setActive(i: number) {\n if (items[active]) items[active].classList.remove('hdk-select__option--active')\n active = i\n const item = items[i]\n if (!item || !list) return\n item.classList.add('hdk-select__option--active')\n list.setAttribute('aria-activedescendant', item.id)\n item.scrollIntoView({ block: 'nearest' })\n }\n\n function step(from: number, dir: 1 | -1) {\n for (let i = 1; i <= options.length; i++) {\n const next = (from + dir * i + options.length) % options.length\n if (!isOff(next)) return next\n }\n return from\n }\n\n const edge = (dir: 1 | -1) => step(dir === 1 ? -1 : options.length, dir)\n\n function matchTypeahead(key: string) {\n const now = Date.now()\n typeahead.text = now - typeahead.at > TYPEAHEAD_RESET_MS ? key : typeahead.text + key\n typeahead.at = now\n const needle = typeahead.text.toLowerCase()\n const start = Math.max(active, 0)\n for (let i = 0; i < options.length; i++) {\n // A single repeated key cycles; a longer prefix stays on the current match.\n const idx = (start + (typeahead.text.length === 1 ? i + 1 : i)) % options.length\n const o = options[idx]\n if (!isOff(idx) && labelOf(o).toLowerCase().startsWith(needle)) return idx\n }\n return -1\n }\n\n const labelOf = (o: HTMLOptionElement) => o.label || o.text\n // A disabled <optgroup> disables its options too.\n const isOff = (i: number) =>\n options[i].disabled ||\n (options[i].parentElement as HTMLOptGroupElement | null)?.disabled === true\n\n function place() {\n if (!select || !list) return\n if (!select.isConnected) {\n close(false)\n return\n }\n const p = placeMenu(select.getBoundingClientRect(), list.scrollHeight)\n list.style.left = `${p.left}px`\n list.style.minWidth = `${p.width}px`\n list.style.top = p.top === undefined ? '' : `${p.top}px`\n list.style.bottom = p.bottom === undefined ? '' : `${p.bottom}px`\n list.style.maxHeight = `${p.maxHeight}px`\n list.style.visibility = ''\n }\n\n function open(el: HTMLSelectElement, start?: 'last') {\n close(false)\n const scope = themeScopeOf(el)\n if (!scope) return\n select = el\n options = Array.from(el.options).filter(o => !o.hidden)\n if (options.length === 0) return\n\n list = document.createElement('ul')\n list.id = LIST_ID\n list.setAttribute('role', 'listbox')\n list.tabIndex = -1\n list.className = 'hdk-select__menu hdk-scroll hdk-scroll--thin'\n const name = el.getAttribute('aria-label') ?? el.labels?.[0]?.textContent?.trim()\n if (name) list.setAttribute('aria-label', name)\n // The list is portalled, so it cannot inherit the select's type size.\n const cs = getComputedStyle(el)\n list.style.fontSize = cs.fontSize\n list.style.fontFamily = cs.fontFamily\n list.style.visibility = 'hidden'\n\n let group: HTMLOptGroupElement | null = null\n items = options.map((o, i) => {\n const g = o.parentElement instanceof HTMLOptGroupElement ? o.parentElement : null\n if (g && g !== group) {\n const head = document.createElement('li')\n head.setAttribute('role', 'presentation')\n head.className = 'hdk-select__group'\n head.textContent = g.label\n list?.append(head)\n }\n group = g\n const item = document.createElement('li')\n item.id = `${LIST_ID}-${i}`\n item.setAttribute('role', 'option')\n const selected = o.index === el.selectedIndex\n item.setAttribute('aria-selected', String(selected))\n if (o.disabled || g?.disabled) item.setAttribute('aria-disabled', 'true')\n item.className = `hdk-select__option${selected ? ' hdk-select__option--selected' : ''}`\n const text = document.createElement('span')\n text.className = 'hdk-select__option-label'\n text.textContent = labelOf(o)\n const wrap = document.createElement('span')\n wrap.className = 'hdk-select__option-text'\n wrap.append(text)\n item.append(wrap)\n if (selected) item.insertAdjacentHTML('beforeend', CHECK_SVG)\n item.addEventListener('pointermove', () => {\n if (!isOff(i) && i !== active) setActive(i)\n })\n item.addEventListener('click', () => choose(i))\n list?.append(item)\n return item\n })\n list.addEventListener('keydown', onListKeyDown)\n scope.appendChild(list)\n el.setAttribute('aria-expanded', 'true')\n el.setAttribute('aria-controls', LIST_ID)\n\n const current = options.findIndex(o => o.index === el.selectedIndex)\n setActive(start === 'last' ? edge(-1) : current >= 0 && !isOff(current) ? current : edge(1))\n place()\n list.focus({ preventScroll: true })\n window.addEventListener('scroll', onScroll, true)\n window.addEventListener('resize', place)\n }\n\n function close(refocus: boolean) {\n window.removeEventListener('scroll', onScroll, true)\n window.removeEventListener('resize', place)\n list?.remove()\n list = null\n if (!select) return\n select.removeAttribute('aria-expanded')\n select.removeAttribute('aria-controls')\n if (refocus) select.focus({ preventScroll: true })\n select = null\n items = []\n options = []\n active = -1\n }\n\n function choose(i: number) {\n const el = select\n const o = options[i]\n if (!el || !o || isOff(i)) return\n const changed = o.index !== el.selectedIndex\n close(true)\n if (!changed) return\n el.selectedIndex = o.index\n el.dispatchEvent(new Event('input', { bubbles: true }))\n el.dispatchEvent(new Event('change', { bubbles: true }))\n }\n\n // Scrolling the list itself must not re-place (or close) it.\n const onScroll = (e: Event) => {\n if (list && e.target instanceof Node && list.contains(e.target)) return\n place()\n }\n\n function onListKeyDown(e: KeyboardEvent) {\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault()\n setActive(step(active, 1))\n break\n case 'ArrowUp':\n e.preventDefault()\n setActive(step(active, -1))\n break\n case 'Home':\n case 'PageUp':\n e.preventDefault()\n setActive(edge(1))\n break\n case 'End':\n case 'PageDown':\n e.preventDefault()\n setActive(edge(-1))\n break\n case 'Enter':\n case ' ':\n e.preventDefault()\n choose(active)\n break\n case 'Escape':\n // Stop here so an enclosing modal does not close along with the list.\n e.preventDefault()\n e.stopPropagation()\n close(true)\n break\n case 'Tab':\n close(true)\n break\n default:\n if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {\n const idx = matchTypeahead(e.key)\n if (idx >= 0) setActive(idx)\n }\n }\n }\n\n const onPointerDown = (e: PointerEvent) => {\n lastPointer = e.pointerType\n const target = e.target as Node\n if (list && !list.contains(target) && target !== select) close(false)\n }\n\n const onMouseDown = (e: MouseEvent) => {\n if (e.button !== 0 || lastPointer === 'touch') return\n const el = (e.target as Element | null)?.closest?.('select') ?? null\n if (!eligible(el)) return\n // This is what keeps the OS list from opening.\n e.preventDefault()\n if (select === el) {\n close(true)\n return\n }\n el.focus({ preventScroll: true })\n open(el)\n }\n\n const onKeyDown = (e: KeyboardEvent) => {\n const el = e.target as Element | null\n if (!eligible(el) || select === el) return\n if (!OPEN_KEYS.has(e.key) || e.ctrlKey || e.metaKey) return\n e.preventDefault()\n open(el, e.key === 'ArrowUp' && !e.altKey ? 'last' : undefined)\n }\n\n document.addEventListener('pointerdown', onPointerDown, true)\n document.addEventListener('mousedown', onMouseDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n\n uninstallCurrent = () => {\n close(false)\n document.removeEventListener('pointerdown', onPointerDown, true)\n document.removeEventListener('mousedown', onMouseDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n uninstallCurrent = null\n }\n return uninstallCurrent\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the unified user-settings modal (ConnectedSettings).
|
|
3
|
+
*
|
|
4
|
+
* Two concerns, both cookie-authed against the hadoku.me edge-router, which
|
|
5
|
+
* resolves the `hadoku_session` cookie / `X-Session-Id` header → X-User-Key +
|
|
6
|
+
* X-User-Id for the backend:
|
|
7
|
+
* - display name → POST /session/name (edge-router, self rename)
|
|
8
|
+
* - content level → GET/PUT /prefs/api/v1/content-level (prefs-api)
|
|
9
|
+
*
|
|
10
|
+
* Switching keys goes through the SITE's sign-in client, never a call of ours:
|
|
11
|
+
* see `loadSignIn()`, with `switchKeyHref()` as the fallback.
|
|
12
|
+
*
|
|
13
|
+
* These are same-origin relative paths, so ConnectedSettings is portable to any
|
|
14
|
+
* app served under hadoku.me with zero wiring. Off-origin (e.g. a Storybook or
|
|
15
|
+
* a non-hadoku host) every call resolves to null and the UI degrades to a
|
|
16
|
+
* read-only shell — it never throws.
|
|
17
|
+
*
|
|
18
|
+
* The raw key is never held in the browser (the auth page deliberately drops
|
|
19
|
+
* it), so name + content-level changes go through session identity, not a key
|
|
20
|
+
* header, and no app ever asks for a key at all.
|
|
21
|
+
*
|
|
22
|
+
* WHEN THESE RUN
|
|
23
|
+
* --------------
|
|
24
|
+
* Everything the popout displays is resolved at PAGE LOAD, not on gear-click —
|
|
25
|
+
* see `prefetchSettings()`. Opening settings must cost zero requests. Pinned by
|
|
26
|
+
* `e2e/settings-prefetch.spec.ts` in hadoku-task.
|
|
27
|
+
*/
|
|
28
|
+
export interface ContentLevelState {
|
|
29
|
+
/** Current global content-visibility level (1 = safest default). */
|
|
30
|
+
level: number;
|
|
31
|
+
/** Highest level this caller's tier may select (friend=3, admin=4). */
|
|
32
|
+
maxLevel: number;
|
|
33
|
+
}
|
|
34
|
+
/** GET the caller's current content level + tier ceiling. null on error. */
|
|
35
|
+
export declare function getContentLevel(): Promise<ContentLevelState | null>;
|
|
36
|
+
/** PUT a new content level. Server clamps to the tier ceiling → 400 if out of
|
|
37
|
+
* range. Returns the stored state on success, null on any failure. */
|
|
38
|
+
export declare function setContentLevel(level: number): Promise<ContentLevelState | null>;
|
|
39
|
+
/** POST a new display name for the signed-in user. Returns the stored name
|
|
40
|
+
* (may differ from the request for wp-* protected keys) or null on failure. */
|
|
41
|
+
export declare function setDisplayName(name: string): Promise<string | null>;
|
|
42
|
+
/**
|
|
43
|
+
* The hadoku tier ladder, LOW to HIGH: `public < friend < service < wife < admin`.
|
|
44
|
+
* Mirrors TIER_RANK in @wolffm/worker-utils, which this browser bundle cannot
|
|
45
|
+
* import (it would pull hono into the client). A tier missing here does not
|
|
46
|
+
* error — it fails the union and, worse, drops out of every `Record<Tier, …>`
|
|
47
|
+
* lookup, so the caller renders as if unrecognised. Keep it complete.
|
|
48
|
+
*/
|
|
49
|
+
export type Tier = 'public' | 'friend' | 'service' | 'wife' | 'admin';
|
|
50
|
+
export interface Identity {
|
|
51
|
+
userType: Tier;
|
|
52
|
+
name: string | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the signed-in caller's tier + display name from the edge-router.
|
|
56
|
+
* Returns { userType: 'public', name: null } for anonymous / off-origin / any
|
|
57
|
+
* failure — never throws, so ConnectedSettings can render unconditionally.
|
|
58
|
+
*/
|
|
59
|
+
export declare function whoami(): Promise<Identity>;
|
|
60
|
+
/**
|
|
61
|
+
* Where a "Switch key" control sends the user when the inline sign-in client
|
|
62
|
+
* (`loadSignIn()`) can't be loaded: the site's sign-in page, which brings them
|
|
63
|
+
* back here afterwards.
|
|
64
|
+
*
|
|
65
|
+
* A browser session is created ONLY by /auth (hadoku_site, 2026-10-25): every
|
|
66
|
+
* render of that page carries an edge-signed funnel token and the device
|
|
67
|
+
* fingerprint, and /session/create alerts on — and will refuse — a sign-in
|
|
68
|
+
* without them. So an app never posts a key itself.
|
|
69
|
+
*
|
|
70
|
+
* `switch=1` stops /auth from sending an already signed-in visitor straight
|
|
71
|
+
* back, which would make switching impossible. `return` is this page's path,
|
|
72
|
+
* query and hash; /auth redirects there with the new session cookie, and the
|
|
73
|
+
* shell's mf-loader resyncs the localStorage mirror (hadoku_session_id /
|
|
74
|
+
* hadoku_user_type) from /session/whoami on that load, so nothing here writes it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function switchKeyHref(): string;
|
|
77
|
+
/** What the site's sign-in client resolves to on success. */
|
|
78
|
+
export interface SignInResult {
|
|
79
|
+
valid: boolean;
|
|
80
|
+
userType?: string;
|
|
81
|
+
sessionId?: string;
|
|
82
|
+
}
|
|
83
|
+
/** `signIn` from hadoku.me's `/auth/sign-in.js`. Resolves on success; throws an
|
|
84
|
+
* `Error` carrying the server's message on a refused or empty key. */
|
|
85
|
+
export type SignIn = (key: string) => Promise<SignInResult>;
|
|
86
|
+
/**
|
|
87
|
+
* The whole sign-in client. The passkey half is optional: a site build from
|
|
88
|
+
* before it exports only `signIn`, and the passkey controls then fall back to
|
|
89
|
+
* the /auth page (`passkeyHref()`), as a missing module does.
|
|
90
|
+
*/
|
|
91
|
+
export interface SignInClient {
|
|
92
|
+
signIn: SignIn;
|
|
93
|
+
/** Like `signIn`, with a passkey in place of the key. Throws Error(message). */
|
|
94
|
+
signInWithPasskey?: () => Promise<SignInResult>;
|
|
95
|
+
/** Saves a passkey for the signed-in person on this device. Throws Error(message). */
|
|
96
|
+
addPasskey?: () => Promise<{
|
|
97
|
+
saved: boolean;
|
|
98
|
+
passkeys: number;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
export declare function loadSignInClient(): Promise<SignInClient | null>;
|
|
102
|
+
export declare function loadSignIn(): Promise<SignIn | null>;
|
|
103
|
+
/** Whether this browser can use a passkey at all (WebAuthn present). */
|
|
104
|
+
export declare function passkeysSupported(): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The /auth page's passkey flows, for when the sign-in client can't be
|
|
107
|
+
* loaded: `add` saves one for the signed-in visitor, `signin` is the page with
|
|
108
|
+
* its passkey button. Both come back here afterwards.
|
|
109
|
+
*/
|
|
110
|
+
export declare function passkeyHref(mode: 'add' | 'signin'): string;
|
|
111
|
+
/** Everything the settings popout displays, resolved together. */
|
|
112
|
+
export interface SettingsSnapshot {
|
|
113
|
+
identity: Identity;
|
|
114
|
+
/** null = public caller (no pill) or the request failed. */
|
|
115
|
+
content: ContentLevelState | null;
|
|
116
|
+
}
|
|
117
|
+
/** Identity the host already knows, so we don't ask the server for it again. */
|
|
118
|
+
export interface SettingsPrefetchHints {
|
|
119
|
+
/** Caller's tier. When supplied, whoami is skipped entirely. */
|
|
120
|
+
userType?: Tier;
|
|
121
|
+
/** Caller's display name. Only meaningful alongside `userType`. */
|
|
122
|
+
name?: string | null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve — once per page load — everything the settings popout shows.
|
|
126
|
+
*
|
|
127
|
+
* This is THE entry point, and it is called from ConnectedSettings' MOUNT, not
|
|
128
|
+
* from its open handler. Settings data is user identity: it is known at boot,
|
|
129
|
+
* it doesn't change while the page is up, and making the gear the trigger left
|
|
130
|
+
* the panel on screen but blank for a round trip (measured on prod: whoami
|
|
131
|
+
* 167ms and content-level 186ms, issued concurrently). It now rides along with
|
|
132
|
+
* the app's own boot traffic and the click costs nothing.
|
|
133
|
+
*
|
|
134
|
+
* Idempotent and shared: every caller after the first gets the same promise, so
|
|
135
|
+
* N mounted consumers still produce at most one whoami for the whole page.
|
|
136
|
+
* Mutations below write through to it rather than invalidating, so a remount
|
|
137
|
+
* never shows a value the user just changed.
|
|
138
|
+
*
|
|
139
|
+
* Against a current edge-router this issues NO requests of its own at all — the
|
|
140
|
+
* level rides on the whoami the shell already had in flight. See
|
|
141
|
+
* resolveSettings for the fallback when it doesn't.
|
|
142
|
+
*/
|
|
143
|
+
export declare function prefetchSettings(hints?: SettingsPrefetchHints): Promise<SettingsSnapshot>;
|
|
144
|
+
//# sourceMappingURL=settingsClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settingsClient.d.ts","sourceRoot":"","sources":["../../src/lib/settingsClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAmDH,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,4EAA4E;AAC5E,wBAAsB,eAAe,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAYzE;AAED;uEACuE;AACvE,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAgBtF;AAED;gFACgF;AAChF,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBzE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;AAErE,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,IAAI,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAyBD;;;;GAIG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,CAShD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;uEACuE;AACvE,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;AAyB3D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/C,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuB/D;AAED,wBAAgB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnD;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAM1D;AAMD,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,QAAQ,CAAA;IAClB,4DAA4D;IAC5D,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAClC;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,IAAI,CAAA;IACf,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,qBAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAG7F"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const CONTENT_LEVEL_PATH = "/prefs/api/v1/content-level";
|
|
2
|
+
const SESSION_NAME = "/session/name";
|
|
3
|
+
const SESSION_WHOAMI = "/session/whoami";
|
|
4
|
+
const WHOAMI_GLOBAL = "__hadokuWhoami";
|
|
5
|
+
const REQUEST_TIMEOUT_MS = 8e3;
|
|
6
|
+
function sessionHeaders(extra) {
|
|
7
|
+
const headers = { "Content-Type": "application/json", ...extra };
|
|
8
|
+
const sid = typeof window !== "undefined" ? localStorage.getItem("hadoku_session_id") : null;
|
|
9
|
+
if (sid) headers["X-Session-Id"] = sid;
|
|
10
|
+
return headers;
|
|
11
|
+
}
|
|
12
|
+
async function getContentLevel() {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(CONTENT_LEVEL_PATH, {
|
|
15
|
+
credentials: "same-origin",
|
|
16
|
+
headers: sessionHeaders(),
|
|
17
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) return null;
|
|
20
|
+
return await res.json();
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function setContentLevel(level) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(CONTENT_LEVEL_PATH, {
|
|
28
|
+
method: "PUT",
|
|
29
|
+
credentials: "same-origin",
|
|
30
|
+
headers: sessionHeaders(),
|
|
31
|
+
body: JSON.stringify({ level }),
|
|
32
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) return null;
|
|
35
|
+
const stored = await res.json();
|
|
36
|
+
patchSnapshot((prev) => ({ ...prev, content: stored }));
|
|
37
|
+
return stored;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function setDisplayName(name) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(SESSION_NAME, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
credentials: "same-origin",
|
|
47
|
+
headers: sessionHeaders(),
|
|
48
|
+
body: JSON.stringify({ name }),
|
|
49
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) return null;
|
|
52
|
+
const body = await res.json();
|
|
53
|
+
patchSnapshot((prev) => ({ ...prev, identity: { ...prev.identity, name: body.name } }));
|
|
54
|
+
return body.name;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sharedWhoami() {
|
|
60
|
+
const g = globalThis;
|
|
61
|
+
g[WHOAMI_GLOBAL] ??= fetch(SESSION_WHOAMI, {
|
|
62
|
+
credentials: "same-origin",
|
|
63
|
+
headers: sessionHeaders(),
|
|
64
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
65
|
+
}).then((res) => res.ok ? res.json() : null).catch(() => null);
|
|
66
|
+
return g[WHOAMI_GLOBAL];
|
|
67
|
+
}
|
|
68
|
+
async function whoami() {
|
|
69
|
+
if (typeof window === "undefined") return { userType: "public", name: null };
|
|
70
|
+
try {
|
|
71
|
+
const body = await sharedWhoami();
|
|
72
|
+
if (!body?.valid) return { userType: "public", name: null };
|
|
73
|
+
return { userType: body.userType ?? "public", name: body.name ?? null };
|
|
74
|
+
} catch {
|
|
75
|
+
return { userType: "public", name: null };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function switchKeyHref() {
|
|
79
|
+
return "/auth?switch=1&return=" + encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
|
|
80
|
+
}
|
|
81
|
+
const SIGN_IN_MODULE = "/auth/sign-in.js";
|
|
82
|
+
let signInModule = null;
|
|
83
|
+
function loadSignInClient() {
|
|
84
|
+
signInModule ??= import(
|
|
85
|
+
/* @vite-ignore */
|
|
86
|
+
SIGN_IN_MODULE
|
|
87
|
+
).then(
|
|
88
|
+
(m) => typeof m.signIn === "function" ? {
|
|
89
|
+
signIn: m.signIn,
|
|
90
|
+
signInWithPasskey: typeof m.signInWithPasskey === "function" ? m.signInWithPasskey : void 0,
|
|
91
|
+
addPasskey: typeof m.addPasskey === "function" ? m.addPasskey : void 0
|
|
92
|
+
} : null,
|
|
93
|
+
() => {
|
|
94
|
+
signInModule = null;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
return signInModule;
|
|
99
|
+
}
|
|
100
|
+
function loadSignIn() {
|
|
101
|
+
return loadSignInClient().then((c) => c?.signIn ?? null);
|
|
102
|
+
}
|
|
103
|
+
function passkeysSupported() {
|
|
104
|
+
return typeof window !== "undefined" && "PublicKeyCredential" in window;
|
|
105
|
+
}
|
|
106
|
+
function passkeyHref(mode) {
|
|
107
|
+
if (mode === "signin") return switchKeyHref();
|
|
108
|
+
return "/auth?passkey=add&return=" + encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
|
|
109
|
+
}
|
|
110
|
+
let snapshot = null;
|
|
111
|
+
function prefetchSettings(hints = {}) {
|
|
112
|
+
snapshot ??= resolveSettings(hints);
|
|
113
|
+
return snapshot;
|
|
114
|
+
}
|
|
115
|
+
async function resolveSettings(hints) {
|
|
116
|
+
const selfResolving = hints.userType === void 0 && typeof window !== "undefined";
|
|
117
|
+
const bodyPromise = selfResolving ? sharedWhoami() : Promise.resolve(null);
|
|
118
|
+
const identityPromise = hints.userType !== void 0 ? Promise.resolve({ userType: hints.userType, name: hints.name ?? null }) : whoami();
|
|
119
|
+
const contentPromise = bodyPromise.then((body) => {
|
|
120
|
+
const carried = contentFromWhoami(body);
|
|
121
|
+
if (carried) return carried;
|
|
122
|
+
if (looksSignedIn(hints.userType)) return getContentLevel();
|
|
123
|
+
return identityPromise.then((id) => id.userType === "public" ? null : getContentLevel());
|
|
124
|
+
});
|
|
125
|
+
const [identity, content] = await Promise.all([identityPromise, contentPromise]);
|
|
126
|
+
return { identity, content };
|
|
127
|
+
}
|
|
128
|
+
function contentFromWhoami(body) {
|
|
129
|
+
if (!body?.valid) return null;
|
|
130
|
+
const { contentLevel, maxContentLevel } = body;
|
|
131
|
+
if (typeof contentLevel !== "number" || typeof maxContentLevel !== "number") return null;
|
|
132
|
+
if (maxContentLevel < 1) return null;
|
|
133
|
+
return { level: contentLevel, maxLevel: maxContentLevel };
|
|
134
|
+
}
|
|
135
|
+
function looksSignedIn(hinted) {
|
|
136
|
+
if (hinted !== void 0) return hinted !== "public";
|
|
137
|
+
if (typeof window === "undefined") return false;
|
|
138
|
+
const cached = localStorage.getItem("hadoku_user_type");
|
|
139
|
+
return cached !== null && cached !== "public";
|
|
140
|
+
}
|
|
141
|
+
function patchSnapshot(patch) {
|
|
142
|
+
if (!snapshot) return;
|
|
143
|
+
snapshot = snapshot.then(patch);
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
getContentLevel,
|
|
147
|
+
loadSignIn,
|
|
148
|
+
loadSignInClient,
|
|
149
|
+
passkeyHref,
|
|
150
|
+
passkeysSupported,
|
|
151
|
+
prefetchSettings,
|
|
152
|
+
setContentLevel,
|
|
153
|
+
setDisplayName,
|
|
154
|
+
switchKeyHref,
|
|
155
|
+
whoami
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=settingsClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settingsClient.js","sources":["../../src/lib/settingsClient.ts"],"sourcesContent":["/**\n * Client for the unified user-settings modal (ConnectedSettings).\n *\n * Two concerns, both cookie-authed against the hadoku.me edge-router, which\n * resolves the `hadoku_session` cookie / `X-Session-Id` header → X-User-Key +\n * X-User-Id for the backend:\n * - display name → POST /session/name (edge-router, self rename)\n * - content level → GET/PUT /prefs/api/v1/content-level (prefs-api)\n *\n * Switching keys goes through the SITE's sign-in client, never a call of ours:\n * see `loadSignIn()`, with `switchKeyHref()` as the fallback.\n *\n * These are same-origin relative paths, so ConnectedSettings is portable to any\n * app served under hadoku.me with zero wiring. Off-origin (e.g. a Storybook or\n * a non-hadoku host) every call resolves to null and the UI degrades to a\n * read-only shell — it never throws.\n *\n * The raw key is never held in the browser (the auth page deliberately drops\n * it), so name + content-level changes go through session identity, not a key\n * header, and no app ever asks for a key at all.\n *\n * WHEN THESE RUN\n * --------------\n * Everything the popout displays is resolved at PAGE LOAD, not on gear-click —\n * see `prefetchSettings()`. Opening settings must cost zero requests. Pinned by\n * `e2e/settings-prefetch.spec.ts` in hadoku-task.\n */\n\n/** Edge-router / prefs-api endpoints (same-origin relative). */\nconst CONTENT_LEVEL_PATH = '/prefs/api/v1/content-level'\nconst SESSION_NAME = '/session/name'\nconst SESSION_WHOAMI = '/session/whoami'\n\n/**\n * The ecosystem's shared boot-whoami promise. hadoku_site's `mf-loader.js`\n * kicks `GET /session/whoami` in parallel with the micro-frontend's module\n * import and parks the promise here precisely so the app it loads doesn't\n * resolve identity a second time; @wolffm/prefs-client already consumes it\n * under the same name. Reading it is what makes settings' identity free.\n *\n * Absent outside that shell (standalone vite, Capacitor, Storybook) — there we\n * fetch, and publish the result under the same key so the next reader is free\n * too.\n */\nconst WHOAMI_GLOBAL = '__hadokuWhoami'\n\n/**\n * Raw whoami body, as both mf-loader and this module see it off the wire.\n *\n * `contentLevel` / `maxContentLevel` are OPTIONAL because the shape is\n * versioned by whatever edge-router is deployed, not by this bundle. They\n * arrive from edge-router ≥ the 2026-08-07 change, which reports the level\n * `authGate` had already resolved to stamp X-Hadoku-Content-Level. When they\n * are absent we fall back to the standalone GET — see resolveSettings.\n */\ninterface WhoamiBody {\n valid?: boolean\n userType?: Tier\n name?: string | null\n contentLevel?: number\n maxContentLevel?: number\n}\n\n// Hard timeout on every settings request. Without it, a request throttled by\n// the browser (e.g. the tab going to the background) leaves the caller's\n// \"saving\" flag stuck true — which disabled the content-level pills until the\n// tab regained focus. AbortSignal.timeout guarantees the promise settles.\nconst REQUEST_TIMEOUT_MS = 8000\n\n/** Session-id header the same way whoami() sends it (cookie is the fallback). */\nfunction sessionHeaders(extra?: Record<string, string>): Record<string, string> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json', ...extra }\n const sid = typeof window !== 'undefined' ? localStorage.getItem('hadoku_session_id') : null\n if (sid) headers['X-Session-Id'] = sid\n return headers\n}\n\nexport interface ContentLevelState {\n /** Current global content-visibility level (1 = safest default). */\n level: number\n /** Highest level this caller's tier may select (friend=3, admin=4). */\n maxLevel: number\n}\n\n/** GET the caller's current content level + tier ceiling. null on error. */\nexport async function getContentLevel(): Promise<ContentLevelState | null> {\n try {\n const res = await fetch(CONTENT_LEVEL_PATH, {\n credentials: 'same-origin',\n headers: sessionHeaders(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n })\n if (!res.ok) return null\n return (await res.json()) as ContentLevelState\n } catch {\n return null\n }\n}\n\n/** PUT a new content level. Server clamps to the tier ceiling → 400 if out of\n * range. Returns the stored state on success, null on any failure. */\nexport async function setContentLevel(level: number): Promise<ContentLevelState | null> {\n try {\n const res = await fetch(CONTENT_LEVEL_PATH, {\n method: 'PUT',\n credentials: 'same-origin',\n headers: sessionHeaders(),\n body: JSON.stringify({ level }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n })\n if (!res.ok) return null\n const stored = (await res.json()) as ContentLevelState\n patchSnapshot(prev => ({ ...prev, content: stored }))\n return stored\n } catch {\n return null\n }\n}\n\n/** POST a new display name for the signed-in user. Returns the stored name\n * (may differ from the request for wp-* protected keys) or null on failure. */\nexport async function setDisplayName(name: string): Promise<string | null> {\n try {\n const res = await fetch(SESSION_NAME, {\n method: 'POST',\n credentials: 'same-origin',\n headers: sessionHeaders(),\n body: JSON.stringify({ name }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n })\n if (!res.ok) return null\n const body = (await res.json()) as { ok: boolean; name: string | null }\n patchSnapshot(prev => ({ ...prev, identity: { ...prev.identity, name: body.name } }))\n return body.name\n } catch {\n return null\n }\n}\n\n/**\n * The hadoku tier ladder, LOW to HIGH: `public < friend < service < wife < admin`.\n * Mirrors TIER_RANK in @wolffm/worker-utils, which this browser bundle cannot\n * import (it would pull hono into the client). A tier missing here does not\n * error — it fails the union and, worse, drops out of every `Record<Tier, …>`\n * lookup, so the caller renders as if unrecognised. Keep it complete.\n */\nexport type Tier = 'public' | 'friend' | 'service' | 'wife' | 'admin'\n\nexport interface Identity {\n userType: Tier\n name: string | null\n}\n\n/**\n * The page's one whoami body, resolved at most once no matter how many callers.\n *\n * Under the hadoku.me shell this costs NO request: mf-loader kicks whoami in\n * parallel with the micro-frontend's module import and parks the promise on\n * WHOAMI_GLOBAL for exactly this reason (@wolffm/prefs-client reads the same\n * key). Elsewhere — standalone vite, Capacitor, Storybook — we fetch once and\n * publish under the same key so the next reader is free too.\n *\n * Resolves to null rather than throwing on any failure.\n */\nfunction sharedWhoami(): Promise<WhoamiBody | null> {\n const g = globalThis as { [WHOAMI_GLOBAL]?: Promise<WhoamiBody | null> }\n g[WHOAMI_GLOBAL] ??= fetch(SESSION_WHOAMI, {\n credentials: 'same-origin',\n headers: sessionHeaders(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n })\n .then(res => (res.ok ? (res.json() as Promise<WhoamiBody>) : null))\n .catch(() => null)\n return g[WHOAMI_GLOBAL]\n}\n\n/**\n * Resolve the signed-in caller's tier + display name from the edge-router.\n * Returns { userType: 'public', name: null } for anonymous / off-origin / any\n * failure — never throws, so ConnectedSettings can render unconditionally.\n */\nexport async function whoami(): Promise<Identity> {\n if (typeof window === 'undefined') return { userType: 'public', name: null }\n try {\n const body = await sharedWhoami()\n if (!body?.valid) return { userType: 'public', name: null }\n return { userType: body.userType ?? 'public', name: body.name ?? null }\n } catch {\n return { userType: 'public', name: null }\n }\n}\n\n/**\n * Where a \"Switch key\" control sends the user when the inline sign-in client\n * (`loadSignIn()`) can't be loaded: the site's sign-in page, which brings them\n * back here afterwards.\n *\n * A browser session is created ONLY by /auth (hadoku_site, 2026-10-25): every\n * render of that page carries an edge-signed funnel token and the device\n * fingerprint, and /session/create alerts on — and will refuse — a sign-in\n * without them. So an app never posts a key itself.\n *\n * `switch=1` stops /auth from sending an already signed-in visitor straight\n * back, which would make switching impossible. `return` is this page's path,\n * query and hash; /auth redirects there with the new session cookie, and the\n * shell's mf-loader resyncs the localStorage mirror (hadoku_session_id /\n * hadoku_user_type) from /session/whoami on that load, so nothing here writes it.\n */\nexport function switchKeyHref(): string {\n return (\n '/auth?switch=1&return=' +\n encodeURIComponent(window.location.pathname + window.location.search + window.location.hash)\n )\n}\n\n/** What the site's sign-in client resolves to on success. */\nexport interface SignInResult {\n valid: boolean\n userType?: string\n sessionId?: string\n}\n\n/** `signIn` from hadoku.me's `/auth/sign-in.js`. Resolves on success; throws an\n * `Error` carrying the server's message on a refused or empty key. */\nexport type SignIn = (key: string) => Promise<SignInResult>\n\n/**\n * The site serves its sign-in page's own client as a module, so an app can sign\n * in INLINE on the same code path as /auth: a fresh funnel token per call, the\n * device fingerprint, and the `/session/create` post that sets the cookie (and\n * writes the hadoku_session_id / hadoku_user_type mirror). This package never\n * posts /session/create itself — a direct post would skip the fingerprint and\n * read as off-funnel — and it does not vendor a copy.\n *\n * The specifier is a VARIABLE on purpose. A literal `import('/auth/sign-in.js')`\n * would have every bundler that re-bundles this package (hadoku_site's build\n * among them) try to resolve the path at build time; this one is only ever\n * resolved by the browser, against whatever origin the page is on. For the\n * same reason it is typed here (`SignIn`) rather than by an ambient module\n * declaration, which only a literal specifier would pick up.\n *\n * The module starts collecting the fingerprint as soon as it evaluates, so call\n * this when a sign-in form OPENS, not when it submits. Memoised; resolves to\n * null when the module can't be loaded (a dev server that isn't hadoku.me),\n * which is the caller's cue to fall back to `switchKeyHref()`.\n */\nconst SIGN_IN_MODULE = '/auth/sign-in.js'\nlet signInModule: Promise<SignInClient | null> | null = null\n\n/**\n * The whole sign-in client. The passkey half is optional: a site build from\n * before it exports only `signIn`, and the passkey controls then fall back to\n * the /auth page (`passkeyHref()`), as a missing module does.\n */\nexport interface SignInClient {\n signIn: SignIn\n /** Like `signIn`, with a passkey in place of the key. Throws Error(message). */\n signInWithPasskey?: () => Promise<SignInResult>\n /** Saves a passkey for the signed-in person on this device. Throws Error(message). */\n addPasskey?: () => Promise<{ saved: boolean; passkeys: number }>\n}\n\nexport function loadSignInClient(): Promise<SignInClient | null> {\n signInModule ??= import(/* @vite-ignore */ SIGN_IN_MODULE).then(\n (m: Partial<Record<keyof SignInClient, unknown>>) =>\n typeof m.signIn === 'function'\n ? {\n signIn: m.signIn as SignIn,\n signInWithPasskey:\n typeof m.signInWithPasskey === 'function'\n ? (m.signInWithPasskey as SignInClient['signInWithPasskey'])\n : undefined,\n addPasskey:\n typeof m.addPasskey === 'function'\n ? (m.addPasskey as SignInClient['addPasskey'])\n : undefined\n }\n : null,\n () => {\n // Let a later open retry: a failed fetch is not cached by the browser.\n signInModule = null\n return null\n }\n )\n return signInModule\n}\n\nexport function loadSignIn(): Promise<SignIn | null> {\n return loadSignInClient().then(c => c?.signIn ?? null)\n}\n\n/** Whether this browser can use a passkey at all (WebAuthn present). */\nexport function passkeysSupported(): boolean {\n return typeof window !== 'undefined' && 'PublicKeyCredential' in window\n}\n\n/**\n * The /auth page's passkey flows, for when the sign-in client can't be\n * loaded: `add` saves one for the signed-in visitor, `signin` is the page with\n * its passkey button. Both come back here afterwards.\n */\nexport function passkeyHref(mode: 'add' | 'signin'): string {\n if (mode === 'signin') return switchKeyHref()\n return (\n '/auth?passkey=add&return=' +\n encodeURIComponent(window.location.pathname + window.location.search + window.location.hash)\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Prefetch\n * ------------------------------------------------------------------ */\n\n/** Everything the settings popout displays, resolved together. */\nexport interface SettingsSnapshot {\n identity: Identity\n /** null = public caller (no pill) or the request failed. */\n content: ContentLevelState | null\n}\n\n/** Identity the host already knows, so we don't ask the server for it again. */\nexport interface SettingsPrefetchHints {\n /** Caller's tier. When supplied, whoami is skipped entirely. */\n userType?: Tier\n /** Caller's display name. Only meaningful alongside `userType`. */\n name?: string | null\n}\n\nlet snapshot: Promise<SettingsSnapshot> | null = null\n\n/**\n * Resolve — once per page load — everything the settings popout shows.\n *\n * This is THE entry point, and it is called from ConnectedSettings' MOUNT, not\n * from its open handler. Settings data is user identity: it is known at boot,\n * it doesn't change while the page is up, and making the gear the trigger left\n * the panel on screen but blank for a round trip (measured on prod: whoami\n * 167ms and content-level 186ms, issued concurrently). It now rides along with\n * the app's own boot traffic and the click costs nothing.\n *\n * Idempotent and shared: every caller after the first gets the same promise, so\n * N mounted consumers still produce at most one whoami for the whole page.\n * Mutations below write through to it rather than invalidating, so a remount\n * never shows a value the user just changed.\n *\n * Against a current edge-router this issues NO requests of its own at all — the\n * level rides on the whoami the shell already had in flight. See\n * resolveSettings for the fallback when it doesn't.\n */\nexport function prefetchSettings(hints: SettingsPrefetchHints = {}): Promise<SettingsSnapshot> {\n snapshot ??= resolveSettings(hints)\n return snapshot\n}\n\nasync function resolveSettings(hints: SettingsPrefetchHints): Promise<SettingsSnapshot> {\n // The whoami body, not just the adapted identity — edge-router carries the\n // content level on it (see below), and reading the raw body is how we get at\n // it without a second request.\n //\n // Skipped entirely when the host supplied identity: that prop exists for apps\n // that CANNOT reach /session/whoami (conjure sits behind a path-prefixed shim\n // where it 404s), so asking anyway would spend a request to learn nothing.\n const selfResolving = hints.userType === undefined && typeof window !== 'undefined'\n const bodyPromise = selfResolving ? sharedWhoami() : Promise.resolve(null)\n\n const identityPromise: Promise<Identity> =\n hints.userType !== undefined\n ? Promise.resolve({ userType: hints.userType, name: hints.name ?? null })\n : whoami()\n\n // PREFERRED PATH: take the level straight off whoami.\n //\n // edge-router's authGate has already resolved it (registry key → userId →\n // prefs D1) to stamp X-Hadoku-Content-Level, so reporting it on whoami costs\n // it nothing — and it saves us a proxied round trip to prefs-api that\n // measured 132-420ms against prod. Better still, whoami is in flight before\n // this bundle has even loaded, so the value is here the moment we ask.\n //\n // FALLBACK: the standalone GET, for any edge-router older than the\n // 2026-08-07 change, and for callers not behind it at all (Capacitor,\n // Storybook, a non-hadoku host). Gated on identity because the pill is\n // hidden for public callers — fetching an anonymous visitor's level is a\n // guaranteed-wasted request on every page view. `hadoku_user_type` is\n // written by the loader before anything mounts, so where it already says\n // \"signed in\" the fallback goes out concurrently rather than behind whoami.\n const contentPromise = bodyPromise.then(body => {\n const carried = contentFromWhoami(body)\n if (carried) return carried\n if (looksSignedIn(hints.userType)) return getContentLevel()\n return identityPromise.then(id => (id.userType === 'public' ? null : getContentLevel()))\n })\n\n const [identity, content] = await Promise.all([identityPromise, contentPromise])\n return { identity, content }\n}\n\n/**\n * Adapt the content level off a whoami body, or null if that edge-router\n * doesn't report it.\n *\n * Both fields are required, and both must be numbers: a body carrying a level\n * without its ceiling would render a pill with zero segments — worse than the\n * fallback fetch, because it looks like a working control that offers nothing.\n */\nfunction contentFromWhoami(body: WhoamiBody | null): ContentLevelState | null {\n if (!body?.valid) return null\n const { contentLevel, maxContentLevel } = body\n if (typeof contentLevel !== 'number' || typeof maxContentLevel !== 'number') return null\n if (maxContentLevel < 1) return null\n return { level: contentLevel, maxLevel: maxContentLevel }\n}\n\n/** Best-effort \"is this caller signed in?\" from state available synchronously. */\nfunction looksSignedIn(hinted?: Tier): boolean {\n if (hinted !== undefined) return hinted !== 'public'\n if (typeof window === 'undefined') return false\n const cached = localStorage.getItem('hadoku_user_type')\n return cached !== null && cached !== 'public'\n}\n\n/** Keep the prefetched snapshot in step with a successful mutation. */\nfunction patchSnapshot(patch: (prev: SettingsSnapshot) => SettingsSnapshot) {\n if (!snapshot) return\n snapshot = snapshot.then(patch)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a popup (tooltip bubble, dropdown list) should be portalled.
|
|
3
|
+
*
|
|
4
|
+
* Theme tokens are scoped: HadokuThemeRoot puts `data-theme` on the app
|
|
5
|
+
* container, and a nested `[data-theme]` scope (a theme preview, an embedded
|
|
6
|
+
* board) can sit inside that. A popup must render inside the SAME scope as its
|
|
7
|
+
* anchor, or it paints in the wrong theme — portalled to <body>, it loses the
|
|
8
|
+
* container's tokens entirely.
|
|
9
|
+
*
|
|
10
|
+
* Rendering it in place keeps the theme but breaks on any ancestor with a
|
|
11
|
+
* `transform`, which becomes the containing block for `position: fixed` and
|
|
12
|
+
* drags the popup off its anchor. The nearest `[data-theme]` ancestor is the
|
|
13
|
+
* best of both: the right tokens, and above every transform between the
|
|
14
|
+
* anchor and the theme root. When the only match is <html>, <body> takes it —
|
|
15
|
+
* :root tokens reach body, and a portal into <html> itself would sit outside
|
|
16
|
+
* the document's body.
|
|
17
|
+
*/
|
|
18
|
+
export declare function themeScopeOf(anchor: Element | null): HTMLElement | null;
|
|
19
|
+
//# sourceMappingURL=themeScope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeScope.d.ts","sourceRoot":"","sources":["../../src/lib/themeScope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,WAAW,GAAG,IAAI,CAKvE"}
|