@hadoku/task-ui-components 0.0.0-stage → 4.20.3
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 +302 -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 +72 -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 +120 -0
- package/dist/lib/settingsClient.d.ts.map +1 -0
- package/dist/lib/settingsClient.js +140 -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 +465 -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 +140 -0
- package/src/lib/menuPlacement.ts +31 -0
- package/src/lib/selectMenus.ts +303 -0
- package/src/lib/settingsClient.ts +377 -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
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { HadokuThemePicker } from "./components/HadokuThemePicker.js";
|
|
2
|
+
import { AppHeader } from "./components/AppHeader.js";
|
|
3
|
+
import { ConnectedSettings } from "./components/ConnectedSettings.js";
|
|
4
|
+
import { CoffeeIcon, FlowerIcon, HadokuThemeProvider, HeartIcon, LanternIcon, LeafIcon, MoonIcon, StrawberryIcon, SunIcon, WaveIcon, ZapIcon, useHadokuTheme, useHadokuThemeOptional } from "@wolffm/themes";
|
|
5
|
+
import { Toast } from "./components/Toast.js";
|
|
6
|
+
import { Toaster } from "./components/Toaster.js";
|
|
7
|
+
import { Modal } from "./components/Modal.js";
|
|
8
|
+
import { ContextMenu } from "./components/ContextMenu.js";
|
|
9
|
+
import { LoadingSkeleton } from "./components/LoadingSkeleton.js";
|
|
10
|
+
import { SettingsModal } from "./components/SettingsModal.js";
|
|
11
|
+
import { Button } from "./components/Button.js";
|
|
12
|
+
import { NativeSelect, Select } from "./components/Select.js";
|
|
13
|
+
import { NumberInput } from "./components/NumberInput.js";
|
|
14
|
+
import { Tooltip } from "./components/Tooltip.js";
|
|
15
|
+
import { installTitleTooltips } from "./lib/titleTooltips.js";
|
|
16
|
+
import { installSelectMenus } from "./lib/selectMenus.js";
|
|
17
|
+
import { BentoGrid } from "./components/BentoGrid.js";
|
|
18
|
+
import { BentoCard } from "./components/BentoCard.js";
|
|
19
|
+
import { useToast } from "./hooks/useToast.js";
|
|
20
|
+
import { useReducedMotion } from "./hooks/useReducedMotion.js";
|
|
21
|
+
import { CircleIcon, DiamondIcon, FALLBACK_ICONS, HexagonIcon, OctagonIcon, PentagonIcon, SettingsIcon, SquareIcon, StarIcon, TagIcon, TriangleIcon, getFallbackIcon } from "./components/ThemeIcons.js";
|
|
22
|
+
import { loadSignIn, switchKeyHref } from "./lib/settingsClient.js";
|
|
23
|
+
export {
|
|
24
|
+
AppHeader,
|
|
25
|
+
BentoCard,
|
|
26
|
+
BentoGrid,
|
|
27
|
+
Button,
|
|
28
|
+
CircleIcon,
|
|
29
|
+
CoffeeIcon,
|
|
30
|
+
ConnectedSettings,
|
|
31
|
+
ContextMenu,
|
|
32
|
+
DiamondIcon,
|
|
33
|
+
FALLBACK_ICONS,
|
|
34
|
+
FlowerIcon,
|
|
35
|
+
HadokuThemePicker,
|
|
36
|
+
HadokuThemeProvider,
|
|
37
|
+
HeartIcon,
|
|
38
|
+
HexagonIcon,
|
|
39
|
+
LanternIcon,
|
|
40
|
+
LeafIcon,
|
|
41
|
+
LoadingSkeleton,
|
|
42
|
+
Modal,
|
|
43
|
+
MoonIcon,
|
|
44
|
+
NativeSelect,
|
|
45
|
+
NumberInput,
|
|
46
|
+
OctagonIcon,
|
|
47
|
+
PentagonIcon,
|
|
48
|
+
Select,
|
|
49
|
+
SettingsIcon,
|
|
50
|
+
SettingsModal,
|
|
51
|
+
SquareIcon,
|
|
52
|
+
StarIcon,
|
|
53
|
+
StrawberryIcon,
|
|
54
|
+
SunIcon,
|
|
55
|
+
TagIcon,
|
|
56
|
+
Toast,
|
|
57
|
+
Toaster,
|
|
58
|
+
Tooltip,
|
|
59
|
+
TriangleIcon,
|
|
60
|
+
WaveIcon,
|
|
61
|
+
ZapIcon,
|
|
62
|
+
getFallbackIcon,
|
|
63
|
+
installSelectMenus,
|
|
64
|
+
installTitleTooltips,
|
|
65
|
+
loadSignIn,
|
|
66
|
+
switchKeyHref,
|
|
67
|
+
useHadokuTheme,
|
|
68
|
+
useHadokuThemeOptional,
|
|
69
|
+
useReducedMotion,
|
|
70
|
+
useToast
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a dropdown list goes under (or over) its trigger, shared by <Select>
|
|
3
|
+
* and installSelectMenus so both lists sit and flip identically.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MENU_GAP = 4;
|
|
6
|
+
export declare const MENU_MAX_HEIGHT = 280;
|
|
7
|
+
export interface MenuPosition {
|
|
8
|
+
left: number;
|
|
9
|
+
width: number;
|
|
10
|
+
top?: number;
|
|
11
|
+
bottom?: number;
|
|
12
|
+
maxHeight: number;
|
|
13
|
+
}
|
|
14
|
+
/** `listHeight` is the list's full scrollHeight, so a short list never flips. */
|
|
15
|
+
export declare function placeMenu(rect: DOMRect, listHeight: number): MenuPosition;
|
|
16
|
+
//# sourceMappingURL=menuPlacement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menuPlacement.d.ts","sourceRoot":"","sources":["../../src/lib/menuPlacement.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,QAAQ,IAAI,CAAA;AACzB,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,iFAAiF;AACjF,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,CAazE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const MENU_GAP = 4;
|
|
2
|
+
const MENU_MAX_HEIGHT = 280;
|
|
3
|
+
function placeMenu(rect, listHeight) {
|
|
4
|
+
const below = window.innerHeight - rect.bottom - MENU_GAP;
|
|
5
|
+
const above = rect.top - MENU_GAP;
|
|
6
|
+
const flip = below < Math.min(MENU_MAX_HEIGHT, listHeight) && above > below;
|
|
7
|
+
return {
|
|
8
|
+
left: rect.left,
|
|
9
|
+
width: rect.width,
|
|
10
|
+
...flip ? { bottom: window.innerHeight - rect.top + MENU_GAP } : { top: rect.bottom + MENU_GAP },
|
|
11
|
+
maxHeight: Math.max(120, Math.min(MENU_MAX_HEIGHT, flip ? above : below) - 8)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
MENU_GAP,
|
|
16
|
+
MENU_MAX_HEIGHT,
|
|
17
|
+
placeMenu
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=menuPlacement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menuPlacement.js","sources":["../../src/lib/menuPlacement.ts"],"sourcesContent":["/**\n * Where a dropdown list goes under (or over) its trigger, shared by <Select>\n * and installSelectMenus so both lists sit and flip identically.\n */\n\nexport const MENU_GAP = 4\nexport const MENU_MAX_HEIGHT = 280\n\nexport interface MenuPosition {\n left: number\n width: number\n top?: number\n bottom?: number\n maxHeight: number\n}\n\n/** `listHeight` is the list's full scrollHeight, so a short list never flips. */\nexport function placeMenu(rect: DOMRect, listHeight: number): MenuPosition {\n const below = window.innerHeight - rect.bottom - MENU_GAP\n const above = rect.top - MENU_GAP\n // Flip up only when the list does not fit below AND there is more room above.\n const flip = below < Math.min(MENU_MAX_HEIGHT, listHeight) && above > below\n return {\n left: rect.left,\n width: rect.width,\n ...(flip\n ? { bottom: window.innerHeight - rect.top + MENU_GAP }\n : { top: rect.bottom + MENU_GAP }),\n maxHeight: Math.max(120, Math.min(MENU_MAX_HEIGHT, flip ? above : below) - 8)\n }\n}\n"],"names":[],"mappings":"AAKO,MAAM,WAAW;AACjB,MAAM,kBAAkB;AAWxB,SAAS,UAAU,MAAe,YAAkC;AACzE,QAAM,QAAQ,OAAO,cAAc,KAAK,SAAS;AACjD,QAAM,QAAQ,KAAK,MAAM;AAEzB,QAAM,OAAO,QAAQ,KAAK,IAAI,iBAAiB,UAAU,KAAK,QAAQ;AACtE,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,GAAI,OACA,EAAE,QAAQ,OAAO,cAAc,KAAK,MAAM,SAAA,IAC1C,EAAE,KAAK,KAAK,SAAS,SAAA;AAAA,IACzB,WAAW,KAAK,IAAI,KAAK,KAAK,IAAI,iBAAiB,OAAO,QAAQ,KAAK,IAAI,CAAC;AAAA,EAAA;AAEhF;"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The themed list for every native <select> on a page, with no change to the
|
|
3
|
+
* markup.
|
|
4
|
+
*
|
|
5
|
+
* import { installSelectMenus } from '@wolffm/task-ui-components/select-menus'
|
|
6
|
+
* import '@wolffm/task-ui-components/select.css'
|
|
7
|
+
* installSelectMenus()
|
|
8
|
+
*
|
|
9
|
+
* hadoku_site calls this once from its shell, which covers every
|
|
10
|
+
* micro-frontend; an app does not call it itself. Framework-free and
|
|
11
|
+
* React-free, like installTitleTooltips, for the same reason.
|
|
12
|
+
*
|
|
13
|
+
* A native select's closed box takes CSS, but its open list is drawn by the
|
|
14
|
+
* OS and takes almost none. So a mouse press or an opening key on the select
|
|
15
|
+
* is intercepted (preventDefault on mousedown is what stops the OS list) and
|
|
16
|
+
* the same list <Select> renders opens instead, built from the select's own
|
|
17
|
+
* <option>s and <optgroup>s at that moment.
|
|
18
|
+
*
|
|
19
|
+
* The <select> stays the source of truth. Choosing sets its selectedIndex and
|
|
20
|
+
* fires `input` and `change` exactly as the OS list would, so every framework
|
|
21
|
+
* (React's onChange included) and every plain listener sees an ordinary
|
|
22
|
+
* change, and a form still submits the select's value.
|
|
23
|
+
*
|
|
24
|
+
* Left native on purpose:
|
|
25
|
+
* - touch: a phone's own picker is the better control there
|
|
26
|
+
* - `multiple` or `size > 1`, which render as an inline list, not a popup
|
|
27
|
+
* - anything under `data-native-select`
|
|
28
|
+
*/
|
|
29
|
+
export declare function installSelectMenus(): () => void;
|
|
30
|
+
//# sourceMappingURL=selectMenus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectMenus.d.ts","sourceRoot":"","sources":["../../src/lib/selectMenus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA0BH,wBAAgB,kBAAkB,IAAI,MAAM,IAAI,CAyP/C"}
|
|
@@ -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,120 @@
|
|
|
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
|
+
export declare function loadSignIn(): Promise<SignIn | null>;
|
|
87
|
+
/** Everything the settings popout displays, resolved together. */
|
|
88
|
+
export interface SettingsSnapshot {
|
|
89
|
+
identity: Identity;
|
|
90
|
+
/** null = public caller (no pill) or the request failed. */
|
|
91
|
+
content: ContentLevelState | null;
|
|
92
|
+
}
|
|
93
|
+
/** Identity the host already knows, so we don't ask the server for it again. */
|
|
94
|
+
export interface SettingsPrefetchHints {
|
|
95
|
+
/** Caller's tier. When supplied, whoami is skipped entirely. */
|
|
96
|
+
userType?: Tier;
|
|
97
|
+
/** Caller's display name. Only meaningful alongside `userType`. */
|
|
98
|
+
name?: string | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve — once per page load — everything the settings popout shows.
|
|
102
|
+
*
|
|
103
|
+
* This is THE entry point, and it is called from ConnectedSettings' MOUNT, not
|
|
104
|
+
* from its open handler. Settings data is user identity: it is known at boot,
|
|
105
|
+
* it doesn't change while the page is up, and making the gear the trigger left
|
|
106
|
+
* the panel on screen but blank for a round trip (measured on prod: whoami
|
|
107
|
+
* 167ms and content-level 186ms, issued concurrently). It now rides along with
|
|
108
|
+
* the app's own boot traffic and the click costs nothing.
|
|
109
|
+
*
|
|
110
|
+
* Idempotent and shared: every caller after the first gets the same promise, so
|
|
111
|
+
* N mounted consumers still produce at most one whoami for the whole page.
|
|
112
|
+
* Mutations below write through to it rather than invalidating, so a remount
|
|
113
|
+
* never shows a value the user just changed.
|
|
114
|
+
*
|
|
115
|
+
* Against a current edge-router this issues NO requests of its own at all — the
|
|
116
|
+
* level rides on the whoami the shell already had in flight. See
|
|
117
|
+
* resolveSettings for the fallback when it doesn't.
|
|
118
|
+
*/
|
|
119
|
+
export declare function prefetchSettings(hints?: SettingsPrefetchHints): Promise<SettingsSnapshot>;
|
|
120
|
+
//# 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,wBAAgB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUnD;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"}
|