@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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +917 -2
  3. package/dist/app-header.css +288 -0
  4. package/dist/bento.css +143 -0
  5. package/dist/button.css +227 -0
  6. package/dist/components/AppHeader.d.ts +61 -0
  7. package/dist/components/AppHeader.d.ts.map +1 -0
  8. package/dist/components/AppHeader.js +23 -0
  9. package/dist/components/AppHeader.js.map +1 -0
  10. package/dist/components/BentoCard.d.ts +23 -0
  11. package/dist/components/BentoCard.d.ts.map +1 -0
  12. package/dist/components/BentoCard.js +49 -0
  13. package/dist/components/BentoCard.js.map +1 -0
  14. package/dist/components/BentoGrid.d.ts +24 -0
  15. package/dist/components/BentoGrid.d.ts.map +1 -0
  16. package/dist/components/BentoGrid.js +22 -0
  17. package/dist/components/BentoGrid.js.map +1 -0
  18. package/dist/components/Button.d.ts +28 -0
  19. package/dist/components/Button.d.ts.map +1 -0
  20. package/dist/components/Button.js +51 -0
  21. package/dist/components/Button.js.map +1 -0
  22. package/dist/components/ConnectedSettings.d.ts +53 -0
  23. package/dist/components/ConnectedSettings.d.ts.map +1 -0
  24. package/dist/components/ConnectedSettings.js +302 -0
  25. package/dist/components/ConnectedSettings.js.map +1 -0
  26. package/dist/components/ConnectedThemePicker.d.ts +52 -0
  27. package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
  28. package/dist/components/ConnectedThemePicker.js +32 -0
  29. package/dist/components/ConnectedThemePicker.js.map +1 -0
  30. package/dist/components/ContextMenu.d.ts +22 -0
  31. package/dist/components/ContextMenu.d.ts.map +1 -0
  32. package/dist/components/ContextMenu.js +23 -0
  33. package/dist/components/ContextMenu.js.map +1 -0
  34. package/dist/components/HadokuThemePicker.d.ts +27 -0
  35. package/dist/components/HadokuThemePicker.d.ts.map +1 -0
  36. package/dist/components/HadokuThemePicker.js +19 -0
  37. package/dist/components/HadokuThemePicker.js.map +1 -0
  38. package/dist/components/LoadingSkeleton.d.ts +14 -0
  39. package/dist/components/LoadingSkeleton.d.ts.map +1 -0
  40. package/dist/components/LoadingSkeleton.js +62 -0
  41. package/dist/components/LoadingSkeleton.js.map +1 -0
  42. package/dist/components/Modal.d.ts +39 -0
  43. package/dist/components/Modal.d.ts.map +1 -0
  44. package/dist/components/Modal.js +106 -0
  45. package/dist/components/Modal.js.map +1 -0
  46. package/dist/components/NumberInput.d.ts +26 -0
  47. package/dist/components/NumberInput.d.ts.map +1 -0
  48. package/dist/components/NumberInput.js +131 -0
  49. package/dist/components/NumberInput.js.map +1 -0
  50. package/dist/components/Select.d.ts +52 -0
  51. package/dist/components/Select.d.ts.map +1 -0
  52. package/dist/components/Select.js +281 -0
  53. package/dist/components/Select.js.map +1 -0
  54. package/dist/components/SettingsModal.d.ts +69 -0
  55. package/dist/components/SettingsModal.d.ts.map +1 -0
  56. package/dist/components/SettingsModal.js +141 -0
  57. package/dist/components/SettingsModal.js.map +1 -0
  58. package/dist/components/ThemeIcons.d.ts +26 -0
  59. package/dist/components/ThemeIcons.d.ts.map +1 -0
  60. package/dist/components/ThemeIcons.js +74 -0
  61. package/dist/components/ThemeIcons.js.map +1 -0
  62. package/dist/components/ThemePicker.d.ts +28 -0
  63. package/dist/components/ThemePicker.d.ts.map +1 -0
  64. package/dist/components/ThemePicker.js +101 -0
  65. package/dist/components/ThemePicker.js.map +1 -0
  66. package/dist/components/Toast.d.ts +13 -0
  67. package/dist/components/Toast.d.ts.map +1 -0
  68. package/dist/components/Toast.js +70 -0
  69. package/dist/components/Toast.js.map +1 -0
  70. package/dist/components/Toaster.d.ts +19 -0
  71. package/dist/components/Toaster.d.ts.map +1 -0
  72. package/dist/components/Toaster.js +19 -0
  73. package/dist/components/Toaster.js.map +1 -0
  74. package/dist/components/Tooltip.d.ts +49 -0
  75. package/dist/components/Tooltip.d.ts.map +1 -0
  76. package/dist/components/Tooltip.js +143 -0
  77. package/dist/components/Tooltip.js.map +1 -0
  78. package/dist/context-menu.css +11 -0
  79. package/dist/controls.css +137 -0
  80. package/dist/hooks/useReducedMotion.d.ts +7 -0
  81. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  82. package/dist/hooks/useReducedMotion.js +25 -0
  83. package/dist/hooks/useReducedMotion.js.map +1 -0
  84. package/dist/hooks/useToast.d.ts +18 -0
  85. package/dist/hooks/useToast.d.ts.map +1 -0
  86. package/dist/hooks/useToast.js +28 -0
  87. package/dist/hooks/useToast.js.map +1 -0
  88. package/dist/icon-btn.css +64 -0
  89. package/dist/index.d.ts +49 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +72 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/lib/menuPlacement.d.ts +16 -0
  94. package/dist/lib/menuPlacement.d.ts.map +1 -0
  95. package/dist/lib/menuPlacement.js +19 -0
  96. package/dist/lib/menuPlacement.js.map +1 -0
  97. package/dist/lib/selectMenus.d.ts +30 -0
  98. package/dist/lib/selectMenus.d.ts.map +1 -0
  99. package/dist/lib/selectMenus.js +236 -0
  100. package/dist/lib/selectMenus.js.map +1 -0
  101. package/dist/lib/settingsClient.d.ts +120 -0
  102. package/dist/lib/settingsClient.d.ts.map +1 -0
  103. package/dist/lib/settingsClient.js +140 -0
  104. package/dist/lib/settingsClient.js.map +1 -0
  105. package/dist/lib/themeScope.d.ts +19 -0
  106. package/dist/lib/themeScope.d.ts.map +1 -0
  107. package/dist/lib/themeScope.js +10 -0
  108. package/dist/lib/themeScope.js.map +1 -0
  109. package/dist/lib/titleTooltips.d.ts +38 -0
  110. package/dist/lib/titleTooltips.d.ts.map +1 -0
  111. package/dist/lib/titleTooltips.js +166 -0
  112. package/dist/lib/titleTooltips.js.map +1 -0
  113. package/dist/lib/tooltipPlacement.d.ts +14 -0
  114. package/dist/lib/tooltipPlacement.d.ts.map +1 -0
  115. package/dist/lib/tooltipPlacement.js +34 -0
  116. package/dist/lib/tooltipPlacement.js.map +1 -0
  117. package/dist/modal.css +129 -0
  118. package/dist/number-input.css +131 -0
  119. package/dist/scrollbar.css +165 -0
  120. package/dist/select.css +251 -0
  121. package/dist/styles.css +1866 -0
  122. package/dist/task-ui-components.css +1866 -0
  123. package/dist/theme-picker.css +219 -0
  124. package/dist/toaster.css +159 -0
  125. package/dist/tooltip.css +103 -0
  126. package/dist/types/index.d.ts +37 -0
  127. package/dist/types/index.d.ts.map +1 -0
  128. package/package.json +60 -4
  129. package/src/app-header.css +288 -0
  130. package/src/bento.css +143 -0
  131. package/src/button.css +227 -0
  132. package/src/components/AppHeader.tsx +87 -0
  133. package/src/components/BentoCard.tsx +72 -0
  134. package/src/components/BentoGrid.tsx +48 -0
  135. package/src/components/Button.tsx +81 -0
  136. package/src/components/ConnectedSettings.tsx +465 -0
  137. package/src/components/ConnectedThemePicker.tsx +79 -0
  138. package/src/components/ContextMenu.tsx +48 -0
  139. package/src/components/HadokuThemePicker.tsx +39 -0
  140. package/src/components/LoadingSkeleton.tsx +84 -0
  141. package/src/components/Modal.tsx +171 -0
  142. package/src/components/NumberInput.tsx +161 -0
  143. package/src/components/Select.tsx +359 -0
  144. package/src/components/SettingsModal.tsx +235 -0
  145. package/src/components/ThemeIcons.tsx +127 -0
  146. package/src/components/ThemePicker.tsx +149 -0
  147. package/src/components/Toast.tsx +92 -0
  148. package/src/components/Toaster.tsx +43 -0
  149. package/src/components/Tooltip.tsx +212 -0
  150. package/src/context-menu.css +11 -0
  151. package/src/controls.css +137 -0
  152. package/src/hooks/useReducedMotion.ts +36 -0
  153. package/src/hooks/useToast.ts +57 -0
  154. package/src/icon-btn.css +64 -0
  155. package/src/index.ts +140 -0
  156. package/src/lib/menuPlacement.ts +31 -0
  157. package/src/lib/selectMenus.ts +303 -0
  158. package/src/lib/settingsClient.ts +377 -0
  159. package/src/lib/themeScope.ts +23 -0
  160. package/src/lib/titleTooltips.ts +236 -0
  161. package/src/lib/tooltipPlacement.ts +59 -0
  162. package/src/modal.css +129 -0
  163. package/src/number-input.css +131 -0
  164. package/src/scrollbar.css +165 -0
  165. package/src/select.css +251 -0
  166. package/src/theme-picker.css +219 -0
  167. package/src/toaster.css +159 -0
  168. package/src/tooltip.css +103 -0
  169. 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":";;AAgCA,MAAM,qBAAqB;AAC3B,MAAM,UAAU;AAGhB,MAAM,YACJ;AAEF,MAAM,gCAAgB,IAAI,CAAC,aAAa,WAAW,KAAK,IAAI,CAAC;AAE7D,IAAI,mBAAwC;AAE5C,SAAS,SAAS,IAA6C;AAC7D,SACE,cAAc,qBACd,CAAC,GAAG,YACJ,GAAG,QAAQ,KACX,CAAC,GAAG,YACJ,CAAC,GAAG,QAAQ,sBAAsB;AAEtC;AAEO,SAAS,qBAAiC;AAC/C,MAAI,iBAAkB,QAAO;AAE7B,MAAI,cAAc;AAClB,MAAI,SAAmC;AACvC,MAAI,OAAgC;AACpC,MAAI,UAA+B,CAAA;AACnC,MAAI,QAAyB,CAAA;AAC7B,MAAI,SAAS;AACb,QAAM,YAAY,EAAE,MAAM,IAAI,IAAI,EAAA;AAElC,WAAS,UAAU,GAAW;AAC5B,QAAI,MAAM,MAAM,EAAG,OAAM,MAAM,EAAE,UAAU,OAAO,4BAA4B;AAC9E,aAAS;AACT,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,CAAC,QAAQ,CAAC,KAAM;AACpB,SAAK,UAAU,IAAI,4BAA4B;AAC/C,SAAK,aAAa,yBAAyB,KAAK,EAAE;AAClD,SAAK,eAAe,EAAE,OAAO,UAAA,CAAW;AAAA,EAC1C;AAEA,WAAS,KAAK,MAAc,KAAa;AACvC,aAAS,IAAI,GAAG,KAAK,QAAQ,QAAQ,KAAK;AACxC,YAAM,QAAQ,OAAO,MAAM,IAAI,QAAQ,UAAU,QAAQ;AACzD,UAAI,CAAC,MAAM,IAAI,EAAG,QAAO;AAAA,IAC3B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,CAAC,QAAgB,KAAK,QAAQ,IAAI,KAAK,QAAQ,QAAQ,GAAG;AAEvE,WAAS,eAAe,KAAa;AACnC,UAAM,MAAM,KAAK,IAAA;AACjB,cAAU,OAAO,MAAM,UAAU,KAAK,qBAAqB,MAAM,UAAU,OAAO;AAClF,cAAU,KAAK;AACf,UAAM,SAAS,UAAU,KAAK,YAAA;AAC9B,UAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;AAChC,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AAEvC,YAAM,OAAO,SAAS,UAAU,KAAK,WAAW,IAAI,IAAI,IAAI,MAAM,QAAQ;AAC1E,YAAM,IAAI,QAAQ,GAAG;AACrB,UAAI,CAAC,MAAM,GAAG,KAAK,QAAQ,CAAC,EAAE,cAAc,WAAW,MAAM,EAAG,QAAO;AAAA,IACzE;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,CAAC,MAAyB,EAAE,SAAS,EAAE;AAEvD,QAAM,QAAQ,CAAC,MACb,QAAQ,CAAC,EAAE,YACV,QAAQ,CAAC,EAAE,eAA8C,aAAa;AAEzE,WAAS,QAAQ;AACf,QAAI,CAAC,UAAU,CAAC,KAAM;AACtB,QAAI,CAAC,OAAO,aAAa;AACvB,YAAM,KAAK;AACX;AAAA,IACF;AACA,UAAM,IAAI,UAAU,OAAO,sBAAA,GAAyB,KAAK,YAAY;AACrE,SAAK,MAAM,OAAO,GAAG,EAAE,IAAI;AAC3B,SAAK,MAAM,WAAW,GAAG,EAAE,KAAK;AAChC,SAAK,MAAM,MAAM,EAAE,QAAQ,SAAY,KAAK,GAAG,EAAE,GAAG;AACpD,SAAK,MAAM,SAAS,EAAE,WAAW,SAAY,KAAK,GAAG,EAAE,MAAM;AAC7D,SAAK,MAAM,YAAY,GAAG,EAAE,SAAS;AACrC,SAAK,MAAM,aAAa;AAAA,EAC1B;AAEA,WAAS,KAAK,IAAuB,OAAgB;AACnD,UAAM,KAAK;AACX,UAAM,QAAQ,aAAa,EAAE;AAC7B,QAAI,CAAC,MAAO;AACZ,aAAS;AACT,cAAU,MAAM,KAAK,GAAG,OAAO,EAAE,OAAO,CAAA,MAAK,CAAC,EAAE,MAAM;AACtD,QAAI,QAAQ,WAAW,EAAG;AAE1B,WAAO,SAAS,cAAc,IAAI;AAClC,SAAK,KAAK;AACV,SAAK,aAAa,QAAQ,SAAS;AACnC,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,UAAM,OAAO,GAAG,aAAa,YAAY,KAAK,GAAG,SAAS,CAAC,GAAG,aAAa,KAAA;AAC3E,QAAI,KAAM,MAAK,aAAa,cAAc,IAAI;AAE9C,UAAM,KAAK,iBAAiB,EAAE;AAC9B,SAAK,MAAM,WAAW,GAAG;AACzB,SAAK,MAAM,aAAa,GAAG;AAC3B,SAAK,MAAM,aAAa;AAExB,QAAI,QAAoC;AACxC,YAAQ,QAAQ,IAAI,CAAC,GAAG,MAAM;AAC5B,YAAM,IAAI,EAAE,yBAAyB,sBAAsB,EAAE,gBAAgB;AAC7E,UAAI,KAAK,MAAM,OAAO;AACpB,cAAM,OAAO,SAAS,cAAc,IAAI;AACxC,aAAK,aAAa,QAAQ,cAAc;AACxC,aAAK,YAAY;AACjB,aAAK,cAAc,EAAE;AACrB,cAAM,OAAO,IAAI;AAAA,MACnB;AACA,cAAQ;AACR,YAAM,OAAO,SAAS,cAAc,IAAI;AACxC,WAAK,KAAK,GAAG,OAAO,IAAI,CAAC;AACzB,WAAK,aAAa,QAAQ,QAAQ;AAClC,YAAM,WAAW,EAAE,UAAU,GAAG;AAChC,WAAK,aAAa,iBAAiB,OAAO,QAAQ,CAAC;AACnD,UAAI,EAAE,YAAY,GAAG,SAAU,MAAK,aAAa,iBAAiB,MAAM;AACxE,WAAK,YAAY,qBAAqB,WAAW,kCAAkC,EAAE;AACrF,YAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,WAAK,YAAY;AACjB,WAAK,cAAc,QAAQ,CAAC;AAC5B,YAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,WAAK,YAAY;AACjB,WAAK,OAAO,IAAI;AAChB,WAAK,OAAO,IAAI;AAChB,UAAI,SAAU,MAAK,mBAAmB,aAAa,SAAS;AAC5D,WAAK,iBAAiB,eAAe,MAAM;AACzC,YAAI,CAAC,MAAM,CAAC,KAAK,MAAM,kBAAkB,CAAC;AAAA,MAC5C,CAAC;AACD,WAAK,iBAAiB,SAAS,MAAM,OAAO,CAAC,CAAC;AAC9C,YAAM,OAAO,IAAI;AACjB,aAAO;AAAA,IACT,CAAC;AACD,SAAK,iBAAiB,WAAW,aAAa;AAC9C,UAAM,YAAY,IAAI;AACtB,OAAG,aAAa,iBAAiB,MAAM;AACvC,OAAG,aAAa,iBAAiB,OAAO;AAExC,UAAM,UAAU,QAAQ,UAAU,OAAK,EAAE,UAAU,GAAG,aAAa;AACnE,cAAU,UAAU,SAAS,KAAK,EAAE,IAAI,WAAW,KAAK,CAAC,MAAM,OAAO,IAAI,UAAU,KAAK,CAAC,CAAC;AAC3F,UAAA;AACA,SAAK,MAAM,EAAE,eAAe,KAAA,CAAM;AAClC,WAAO,iBAAiB,UAAU,UAAU,IAAI;AAChD,WAAO,iBAAiB,UAAU,KAAK;AAAA,EACzC;AAEA,WAAS,MAAM,SAAkB;AAC/B,WAAO,oBAAoB,UAAU,UAAU,IAAI;AACnD,WAAO,oBAAoB,UAAU,KAAK;AAC1C,UAAM,OAAA;AACN,WAAO;AACP,QAAI,CAAC,OAAQ;AACb,WAAO,gBAAgB,eAAe;AACtC,WAAO,gBAAgB,eAAe;AACtC,QAAI,QAAS,QAAO,MAAM,EAAE,eAAe,MAAM;AACjD,aAAS;AACT,YAAQ,CAAA;AACR,cAAU,CAAA;AACV,aAAS;AAAA,EACX;AAEA,WAAS,OAAO,GAAW;AACzB,UAAM,KAAK;AACX,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,EAAG;AAC3B,UAAM,UAAU,EAAE,UAAU,GAAG;AAC/B,UAAM,IAAI;AACV,QAAI,CAAC,QAAS;AACd,OAAG,gBAAgB,EAAE;AACrB,OAAG,cAAc,IAAI,MAAM,SAAS,EAAE,SAAS,KAAA,CAAM,CAAC;AACtD,OAAG,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAA,CAAM,CAAC;AAAA,EACzD;AAGA,QAAM,WAAW,CAAC,MAAa;AAC7B,QAAI,QAAQ,EAAE,kBAAkB,QAAQ,KAAK,SAAS,EAAE,MAAM,EAAG;AACjE,UAAA;AAAA,EACF;AAEA,WAAS,cAAc,GAAkB;AACvC,YAAQ,EAAE,KAAA;AAAA,MACR,KAAK;AACH,UAAE,eAAA;AACF,kBAAU,KAAK,QAAQ,CAAC,CAAC;AACzB;AAAA,MACF,KAAK;AACH,UAAE,eAAA;AACF,kBAAU,KAAK,QAAQ,EAAE,CAAC;AAC1B;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,UAAE,eAAA;AACF,kBAAU,KAAK,CAAC,CAAC;AACjB;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,UAAE,eAAA;AACF,kBAAU,KAAK,EAAE,CAAC;AAClB;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,UAAE,eAAA;AACF,eAAO,MAAM;AACb;AAAA,MACF,KAAK;AAEH,UAAE,eAAA;AACF,UAAE,gBAAA;AACF,cAAM,IAAI;AACV;AAAA,MACF,KAAK;AACH,cAAM,IAAI;AACV;AAAA,MACF;AACE,YAAI,EAAE,IAAI,WAAW,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ;AAC/D,gBAAM,MAAM,eAAe,EAAE,GAAG;AAChC,cAAI,OAAO,EAAG,WAAU,GAAG;AAAA,QAC7B;AAAA,IAAA;AAAA,EAEN;AAEA,QAAM,gBAAgB,CAAC,MAAoB;AACzC,kBAAc,EAAE;AAChB,UAAM,SAAS,EAAE;AACjB,QAAI,QAAQ,CAAC,KAAK,SAAS,MAAM,KAAK,WAAW,OAAQ,OAAM,KAAK;AAAA,EACtE;AAEA,QAAM,cAAc,CAAC,MAAkB;AACrC,QAAI,EAAE,WAAW,KAAK,gBAAgB,QAAS;AAC/C,UAAM,KAAM,EAAE,QAA2B,UAAU,QAAQ,KAAK;AAChE,QAAI,CAAC,SAAS,EAAE,EAAG;AAEnB,MAAE,eAAA;AACF,QAAI,WAAW,IAAI;AACjB,YAAM,IAAI;AACV;AAAA,IACF;AACA,OAAG,MAAM,EAAE,eAAe,KAAA,CAAM;AAChC,SAAK,EAAE;AAAA,EACT;AAEA,QAAM,YAAY,CAAC,MAAqB;AACtC,UAAM,KAAK,EAAE;AACb,QAAI,CAAC,SAAS,EAAE,KAAK,WAAW,GAAI;AACpC,QAAI,CAAC,UAAU,IAAI,EAAE,GAAG,KAAK,EAAE,WAAW,EAAE,QAAS;AACrD,MAAE,eAAA;AACF,SAAK,IAAI,EAAE,QAAQ,aAAa,CAAC,EAAE,SAAS,SAAS,MAAS;AAAA,EAChE;AAEA,WAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,WAAS,iBAAiB,aAAa,aAAa,IAAI;AACxD,WAAS,iBAAiB,WAAW,WAAW,IAAI;AAEpD,qBAAmB,MAAM;AACvB,UAAM,KAAK;AACX,aAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,aAAS,oBAAoB,aAAa,aAAa,IAAI;AAC3D,aAAS,oBAAoB,WAAW,WAAW,IAAI;AACvD,uBAAmB;AAAA,EACrB;AACA,SAAO;AACT;"}
@@ -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"}