clio.css 0.9.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +10 -6
  2. package/EVIDENCE.md +130 -9
  3. package/README.md +96 -3
  4. package/THIRD-PARTY.md +37 -3
  5. package/dist/art/cursors/arrow-2x.png +0 -0
  6. package/dist/art/cursors/arrow-3x.png +0 -0
  7. package/dist/art/cursors/arrow.png +0 -0
  8. package/dist/art/cursors/ibeam-2x.png +0 -0
  9. package/dist/art/cursors/ibeam-3x.png +0 -0
  10. package/dist/art/cursors/ibeam.png +0 -0
  11. package/dist/art/cursors/watch-2x.png +0 -0
  12. package/dist/art/cursors/watch-3x.png +0 -0
  13. package/dist/art/cursors/watch.png +0 -0
  14. package/dist/art/drawingboard/help-button.png +0 -0
  15. package/dist/art/drawingboard/parts/chasing-arrows.png +0 -0
  16. package/dist/art/drawingboard/parts/disclosure-open.png +0 -0
  17. package/dist/art/drawingboard/parts/disclosure.png +0 -0
  18. package/dist/art/drawingboard/parts/hpair-dec-on.png +0 -0
  19. package/dist/art/drawingboard/parts/hpair-inc-on.png +0 -0
  20. package/dist/art/drawingboard/parts/hpair-off.png +0 -0
  21. package/dist/art/drawingboard/parts/hpair.png +0 -0
  22. package/dist/art/drawingboard/parts/little-arrows-disabled.png +0 -0
  23. package/dist/art/drawingboard/parts/little-arrows-down.png +0 -0
  24. package/dist/art/drawingboard/parts/little-arrows-up.png +0 -0
  25. package/dist/art/drawingboard/parts/little-arrows.png +0 -0
  26. package/dist/art/drawingboard/parts/menu-scroll-down.png +0 -0
  27. package/dist/art/drawingboard/parts/menu-scroll-up.png +0 -0
  28. package/dist/art/drawingboard/parts/slider-thumb.png +0 -0
  29. package/dist/art/drawingboard/parts/vpair-dec-on.png +0 -0
  30. package/dist/art/drawingboard/parts/vpair-inc-on.png +0 -0
  31. package/dist/art/drawingboard/parts/vpair-off.png +0 -0
  32. package/dist/art/drawingboard/parts/vpair.png +0 -0
  33. package/dist/art/drawingboard/slider-track.png +0 -0
  34. package/dist/art/drawingboard/window-bottom-inactive-tiled.svg +1 -0
  35. package/dist/art/drawingboard/window-bottom-tiled.svg +1 -0
  36. package/dist/art/drawingboard/window-top-inactive-tiled.svg +1 -0
  37. package/dist/art/drawingboard/window-top-tiled.svg +1 -0
  38. package/dist/art/platinum/balloon-tail.png +0 -0
  39. package/dist/art/platinum/balloon.png +0 -0
  40. package/dist/art/platinum/bevel-held.png +0 -0
  41. package/dist/art/platinum/bevel-menu-arrow.png +0 -0
  42. package/dist/art/platinum/bevel.png +0 -0
  43. package/dist/art/platinum/chasing-arrows.png +0 -0
  44. package/dist/art/platinum/disclosure-open.png +0 -0
  45. package/dist/art/platinum/disclosure.png +0 -0
  46. package/dist/art/platinum/help-button.png +0 -0
  47. package/dist/art/platinum/little-arrows-disabled.png +0 -0
  48. package/dist/art/platinum/little-arrows.png +0 -0
  49. package/dist/art/platinum/slider-thumb.png +0 -0
  50. package/dist/art/platinum/slider-track.png +0 -0
  51. package/dist/art/platinum/sort-direction.png +0 -0
  52. package/dist/art/platinum/utility-close.png +0 -0
  53. package/dist/art/platinum/utility-collapse-side.png +0 -0
  54. package/dist/art/platinum/utility-collapse.png +0 -0
  55. package/dist/art/platinum/utility-dots.png +0 -0
  56. package/dist/art/platinum/utility-zoom.png +0 -0
  57. package/dist/art/system7/disclosure-open.png +0 -0
  58. package/dist/art/system7/disclosure.png +0 -0
  59. package/dist/art/system7/little-arrows.png +0 -0
  60. package/dist/art/system7/menu-scroll-down.png +0 -0
  61. package/dist/art/system7/menu-scroll-up.png +0 -0
  62. package/dist/clio.css +3632 -1044
  63. package/dist/clio.drawingboard.css +2650 -324
  64. package/dist/clio.drawingboard.min.css +2 -2
  65. package/dist/clio.js +1024 -0
  66. package/dist/clio.min.css +2 -2
  67. package/dist/clio.min.js +13 -0
  68. package/dist/clio.platinum.css +3096 -856
  69. package/dist/clio.platinum.min.css +2 -2
  70. package/dist/clio.system6.css +1640 -174
  71. package/dist/clio.system6.min.css +2 -2
  72. package/dist/clio.system7.css +1685 -181
  73. package/dist/clio.system7.min.css +2 -2
  74. package/dist/components.json +1102 -21
  75. package/dist/fonts/ClioSans97Plain.woff2 +0 -0
  76. package/dist/fonts/ClioSans97PlainExact.woff2 +0 -0
  77. package/dist/fonts/ClioSans97TitlePlain.woff2 +0 -0
  78. package/dist/fonts/ClioSans97TitlePlainExact.woff2 +0 -0
  79. package/dist/fonts/FusionPixel12-JA.woff2 +0 -0
  80. package/dist/fonts/FusionPixel12-KO.woff2 +0 -0
  81. package/dist/fonts/FusionPixel12-SC.woff2 +0 -0
  82. package/dist/fonts/FusionPixel12-TC.woff2 +0 -0
  83. package/dist/fonts/OFL-FusionPixel.txt +399 -0
  84. package/dist/fonts/Virtue.woff2 +0 -0
  85. package/llms.txt +14 -1
  86. package/package.json +6 -3
package/dist/clio.js ADDED
@@ -0,0 +1,1024 @@
1
+ /*! clio.js — optional behaviour for clio.css | MIT | https://surfine.github.io/clio.css/
2
+ * Load after clio.css; nothing in clio.css needs it. It adds what CSS cannot do:
3
+ * - a pressed control lets go while the pointer is off it and presses again on return
4
+ * - Return/Enter clicks a dialog's default button, Esc or Cmd-. its cancel button,
5
+ * each after flashing it pressed
6
+ * - scroll bars with the era's fixed-size scroll box, arrows that repeat while held,
7
+ * paging from the lane, and System 6/7's dragged outline (Mac OS 8/9 drag live)
8
+ * - menus that track the pointer (drag and release, blink, submenus, keys) and
9
+ * <select> opening as the era's pop-up menu
10
+ * - window dragging with a grey outline, the collapse box, clio.zoomRects()
11
+ * - the era's insertion point in text fields (XOR, measured blink)
12
+ * - optional sounds you supply (clio.sounds, data-clio-sounds)
13
+ * Opt out per element with data-clio-js="off". */
14
+ (() => {
15
+ "use strict";
16
+ if (typeof document === "undefined" || window.clio) return;
17
+
18
+ const TICK = 1000 / 60;
19
+ const OFF = "data-clio-off";
20
+ const optedOut = (el) => !!el.closest('[data-clio-js="off"]');
21
+
22
+ /* ---- Held controls: let go off the control, press again on return ------- */
23
+ // The Control Manager tracks a press until the button comes up and draws the
24
+ // control pressed only while the pointer is inside it (Inside Macintosh:
25
+ // Macintosh Toolbox Essentials, TrackControl). Browsers keep :active wherever
26
+ // the pointer goes; clio.css's held rules skip [data-clio-off].
27
+ const PRESSABLE = [
28
+ "button", "input[type=button]", "input[type=submit]", "input[type=reset]",
29
+ "input[type=checkbox]", "input[type=radio]", '[role="button"]', '[role="tab"]',
30
+ ".title-bar > *", ".bevel", ".little-arrows > *", ".control-strip-module", ".control-strip-scroll",
31
+ ].join(",");
32
+
33
+ document.addEventListener("pointerdown", (e) => {
34
+ if (e.button !== 0 || !(e.target instanceof Element)) return;
35
+ const el = e.target.closest(PRESSABLE);
36
+ if (!el || el.disabled || optedOut(el) || el.closest(".clio-sb-anchor, .menu-bar, [role=menubar], [role=menu-bar], .menu, [role=menu]")) return;
37
+ const r = el.getBoundingClientRect(); // once per press; the control does not move while held
38
+ const inside = (ev) => ev.clientX >= r.left && ev.clientX < r.right && ev.clientY >= r.top && ev.clientY < r.bottom;
39
+ const move = (ev) => { if (ev.pointerId === e.pointerId) el.toggleAttribute(OFF, !inside(ev)); };
40
+ const end = (ev) => {
41
+ if (ev.pointerId !== e.pointerId) return;
42
+ el.removeAttribute(OFF);
43
+ document.removeEventListener("pointermove", move, true);
44
+ document.removeEventListener("pointerup", end, true);
45
+ document.removeEventListener("pointercancel", end, true);
46
+ };
47
+ document.addEventListener("pointermove", move, true);
48
+ document.addEventListener("pointerup", end, true);
49
+ document.addEventListener("pointercancel", end, true);
50
+ }, true);
51
+
52
+ /* ---- Default and cancel buttons from the keyboard ----------------------- */
53
+ // Return or Enter is the default button; Esc and Cmd-. are Cancel. The button
54
+ // shows pressed for 8 ticks first, as the Dialog Manager flashes it.
55
+ const DIALOG = ".modal-dialog, .alert-box, .standard-dialog, .modeless-dialog, .window.movable-modal, dialog, [role=dialog], [role=alertdialog]";
56
+ const DEFAULT = ".default:not(:disabled), .btn-default:not(:disabled), [data-clio-default]:not(:disabled)";
57
+ const CANCEL = "[data-clio-cancel]:not(:disabled), button[value=cancel]:not(:disabled), .cancel:not(:disabled)";
58
+ const flash = (button) => {
59
+ const had = button.classList.contains("active");
60
+ button.classList.add("active");
61
+ setTimeout(() => { if (!had) button.classList.remove("active"); button.click(); }, 8 * TICK);
62
+ };
63
+ document.addEventListener("keydown", (e) => {
64
+ if (e.defaultPrevented || e.isComposing || e.repeat) return;
65
+ const enter = (e.key === "Enter") && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey;
66
+ const cancel = e.key === "Escape" || (e.key === "." && (e.metaKey || e.ctrlKey));
67
+ if (!enter && !cancel) return;
68
+ const focus = document.activeElement instanceof Element ? document.activeElement : null;
69
+ const scope = (focus && focus.closest(DIALOG)) || [...document.querySelectorAll("dialog[open]")].pop();
70
+ if (!scope || optedOut(scope)) return;
71
+ if (enter && focus && (focus.matches("textarea, button, select, [role=button], [contenteditable], a[href], summary") || focus.isContentEditable)) return;
72
+ const button = scope.querySelector(enter ? DEFAULT : CANCEL);
73
+ if (!button) return;
74
+ e.preventDefault();
75
+ flash(button);
76
+ });
77
+
78
+ /* ---- Menus -------------------------------------------------------------- */
79
+ // Press a title and drag: the menu follows the pointer and the item under it
80
+ // is chosen when the button comes up, blinking first. Mac OS 8 and 9 also
81
+ // keep a clicked menu open (--clio-menu-sticky: 1). Measured in Mac OS 9.0:
82
+ // 3 blinks of 3 ticks off and 3 on; a submenu opens 9 ticks after its item
83
+ // lights. Leaving the item towards the submenu keeps it as the Menu Manager
84
+ // did (System 6, 7 and Mac OS 9 ROMs: MenuDelay 8 ticks, MenuDrag 60 ticks).
85
+ const BAR = ".menu-bar, [role=menubar], [role=menu-bar]";
86
+ const MENU = ".menu, [role=menu]";
87
+ const TITLE = ":scope > li > :is(button, a, span)";
88
+ const BLINK = "data-clio-blink";
89
+ const SUBMENU_DELAY = 9 * TICK;
90
+ const DRAG_HOLD = 60 * TICK; // MenuDrag: counted from leaving the item, never extended
91
+ const reduced = () => matchMedia("(prefers-reduced-motion: reduce)").matches;
92
+ const sticky = (el) => getComputedStyle(el).getPropertyValue("--clio-menu-sticky").trim() === "1";
93
+ const usable = (li) => li && li.matches(`:is(${MENU}) > li`) && !li.matches(".disabled, [aria-disabled=true], .separator, [role=separator], .scroll-up, .scroll-down") && !li.querySelector(":scope > :is(a, button):is(:disabled, [aria-disabled=true])");
94
+ const submenuOf = (li) => li && li.querySelector(`:scope > :is(${MENU})`);
95
+ const control = (li) => li.querySelector(":scope > :is(a, button)") || li;
96
+
97
+ let track = null; // { bar, title, li, mode: "press" | "sticky", hot, timer, hold }
98
+
99
+ function titleOf(li) { return li.querySelector(":scope > :is(button, a, span)"); }
100
+ function openTitle(bar, li) {
101
+ for (const other of bar.querySelectorAll(":scope > li.open")) if (other !== li) closeTitle(other);
102
+ li.classList.add("open");
103
+ const t = titleOf(li); if (t && t.hasAttribute("aria-expanded")) t.setAttribute("aria-expanded", "true");
104
+ }
105
+ function closeTitle(li) {
106
+ li.classList.remove("open");
107
+ const t = titleOf(li); if (t && t.hasAttribute("aria-expanded")) t.setAttribute("aria-expanded", "false");
108
+ for (const x of li.querySelectorAll("li.open, li.active")) x.classList.remove("open", "active");
109
+ }
110
+ function closeAll() {
111
+ if (!track) return;
112
+ clearTimeout(track.timer); clearTimeout(track.hold);
113
+ for (const li of track.bar.querySelectorAll(":scope > li.open")) closeTitle(li);
114
+ if (document.activeElement && track.bar.contains(document.activeElement)) document.activeElement.blur();
115
+ track = null;
116
+ }
117
+ // Light an item; open its submenu after the delay, close siblings' submenus.
118
+ function light(li) {
119
+ if (!track || track.hot === li) return;
120
+ const menu = li ? li.parentElement : null;
121
+ if (menu) for (const sib of menu.children) if (sib !== li) { sib.classList.remove("active"); if (sib.classList.contains("open")) closeSub(sib); }
122
+ if (track.hot && track.hot !== li && (!li || !track.hot.contains(li))) track.hot.classList.remove("active");
123
+ track.hot = li;
124
+ clearTimeout(track.timer);
125
+ if (!li || !usable(li)) return;
126
+ li.classList.add("active");
127
+ if (submenuOf(li)) track.timer = setTimeout(() => { li.classList.add("open"); }, SUBMENU_DELAY);
128
+ }
129
+ function closeSub(li) { li.classList.remove("open", "active"); for (const x of li.querySelectorAll("li.open, li.active")) x.classList.remove("open", "active"); }
130
+ // Leaving an open parent item keeps its submenu while the pointer stays inside
131
+ // the wedge from the last point in the item (the anchor) to the submenu's two
132
+ // near corners, angles compared in whole degrees as AngleFromSlope gave them,
133
+ // and never moves back away from the submenu.
134
+ const degrees = (dx, dy) => Math.round(Math.atan2(Math.abs(dx), -dy) * 180 / Math.PI);
135
+ function inWedge(parent, anchor, to) {
136
+ const sub = submenuOf(parent); if (!sub || !parent.classList.contains("open")) return false;
137
+ const r = sub.getBoundingClientRect();
138
+ const right = r.left >= anchor.x, edge = right ? r.left : r.right;
139
+ if (right ? to.x < anchor.x : to.x > anchor.x) return false;
140
+ const a = degrees(to.x - anchor.x, to.y - anchor.y);
141
+ return a >= degrees(edge - anchor.x, r.top - anchor.y) && a <= degrees(edge - anchor.x, r.bottom - anchor.y);
142
+ }
143
+ function itemAt(x, y) {
144
+ const el = document.elementFromPoint(x, y);
145
+ return el ? el.closest(`:is(${MENU}) > li`) : null;
146
+ }
147
+ function choose(li, after) {
148
+ const done = () => {
149
+ li.removeAttribute(BLINK); const c = control(li); c.removeAttribute(BLINK);
150
+ closeAll();
151
+ if (after) after();
152
+ play("menu");
153
+ c.click();
154
+ };
155
+ track.mode = "blink";
156
+ const c = control(li);
157
+ if (reduced()) return done();
158
+ let n = 0;
159
+ const step = () => { // off, on, off, on, off, on: 3 ticks each
160
+ const off = n % 2 === 0;
161
+ li.toggleAttribute(BLINK, off); c.toggleAttribute(BLINK, off);
162
+ if (++n === 7) return done();
163
+ setTimeout(step, 3 * TICK);
164
+ };
165
+ step();
166
+ }
167
+
168
+ document.addEventListener("pointerdown", (e) => {
169
+ if (e.button !== 0 || !(e.target instanceof Element)) return;
170
+ const target = e.target;
171
+ if (track && track.mode === "sticky") {
172
+ const li = target.closest(`:is(${MENU}) > li`);
173
+ const tli = target.closest(`:is(${BAR}) > li`);
174
+ if (li && track.bar.contains(li)) { track.mode = "press"; e.preventDefault(); return; }
175
+ if (tli && tli.parentElement === track.bar && !li) { e.preventDefault(); if (tli.classList.contains("open")) { closeAll(); return; } openTitle(track.bar, tli); track.li = tli; track.mode = "press"; track.fresh = false; return; }
176
+ closeAll(); e.preventDefault(); e.stopPropagation();
177
+ document.addEventListener("click", (c) => { c.preventDefault(); c.stopPropagation(); }, { capture: true, once: true });
178
+ return;
179
+ }
180
+ const tli = target.closest(`:is(${BAR}) > li`);
181
+ if (!tli || optedOut(tli) || target.closest(MENU)) return;
182
+ e.preventDefault(); // the title neither takes focus nor starts a selection
183
+ const bar = tli.parentElement;
184
+ track = { bar, li: tli, mode: "press", hot: null, fresh: true, last: { x: e.clientX, y: e.clientY } };
185
+ openTitle(bar, tli);
186
+ }, true);
187
+
188
+ document.addEventListener("pointermove", (e) => {
189
+ if (!track || track.mode === "blink") return;
190
+ const at = { x: e.clientX, y: e.clientY };
191
+ const from = track.last; track.last = at;
192
+ const el = document.elementFromPoint(at.x, at.y);
193
+ if (!el) return;
194
+ const tli = el.closest(`:is(${BAR}) > li`);
195
+ if (tli && tli.parentElement === track.bar && !el.closest(MENU) && tli !== track.li && (track.mode === "press" || track.mode === "sticky")) {
196
+ openTitle(track.bar, tli); track.li = tli; track.fresh = false; light(null); return;
197
+ }
198
+ const li = el.closest(`:is(${MENU}) > li`);
199
+ if (li && !track.bar.contains(li)) return;
200
+ // keep a parent's submenu while heading for it, for MenuDrag from leaving the item
201
+ const parent = track.hot && submenuOf(track.hot) ? track.hot : null;
202
+ if (parent && li !== parent && !(li && parent.contains(li)) && parent.classList.contains("open")) {
203
+ const now = performance.now();
204
+ if (track.left == null) {
205
+ track.left = now;
206
+ track.hold = setTimeout(() => { if (track) { track.left = null; light(itemAt(track.last.x, track.last.y)); } }, DRAG_HOLD);
207
+ }
208
+ if (now - track.left < DRAG_HOLD && inWedge(parent, track.anchor || from, at)) return;
209
+ }
210
+ clearTimeout(track.hold); track.left = null;
211
+ if (li) track.anchor = at; // the last point inside an item
212
+ if (li) light(li);
213
+ else if (track.hot && !submenuOf(track.hot)) light(null);
214
+ }, true);
215
+
216
+ document.addEventListener("pointerup", (e) => {
217
+ if (!track || track.mode !== "press" || e.button !== 0) return;
218
+ const el = document.elementFromPoint(e.clientX, e.clientY);
219
+ const li = el && el.closest(`:is(${MENU}) > li`);
220
+ if (li && track.bar.contains(li) && usable(li) && !submenuOf(li)) {
221
+ // the press and release were both on the item (a click-opened menu, or a tap): the
222
+ // browser's own click follows and must not run the item a second time
223
+ document.addEventListener("click", (c) => { if (c.isTrusted && c.target instanceof Node && li.contains(c.target)) { c.preventDefault(); c.stopPropagation(); } }, { capture: true, once: true });
224
+ e.preventDefault(); choose(li); return;
225
+ }
226
+ const tli = el && el.closest(`:is(${BAR}) > li`);
227
+ if (tli === track.li && track.fresh && !el.closest(MENU) && sticky(track.bar)) { track.mode = "sticky"; return; } // Mac OS 8: a click leaves the menu open
228
+ closeAll();
229
+ }, true);
230
+
231
+ // A click that ends a menu gesture must not reach the page.
232
+ document.addEventListener("click", (e) => {
233
+ if (e.isTrusted && e.target instanceof Element && e.target.closest(`:is(${BAR}) > li > :is(button, a, span)`) && !optedOut(e.target)) e.preventDefault();
234
+ }, true);
235
+
236
+ // Keyboard, as an application menu bar (WAI-ARIA APG): the bar is one Tab
237
+ // stop (roving tabindex), focusing a title previews its menu, Left and Right
238
+ // move between titles, Down/Return/Space/Home/End enter a menu, Up/Down/
239
+ // Home/End move inside it, Right and Left open and close submenus or move
240
+ // to the next title, Esc closes one level, Tab leaves and closes all. (Mac OS
241
+ // 8 and 9 had no keyboard menus; this is the web's.) Command-key
242
+ // equivalents (<kbd>⌘N</kbd>) work on menu bars marked data-clio-shortcuts.
243
+ const items = (menu) => [...menu.children].filter(usable);
244
+ const focusItem = (li) => { const c = control(li); if (c === li && !li.hasAttribute("tabindex")) li.tabIndex = -1; c.focus(); if (track) light(li); };
245
+ const titlesOf = (bar) => [...bar.children].filter((li) => titleOf(li));
246
+ const menuOf = (tli) => tli.querySelector(`:scope > :is(${MENU})`);
247
+ let quiet = false; // focus coming back to a title from its own menu: no preview
248
+ function rove(bar, tli) { for (const li of titlesOf(bar)) { const t = titleOf(li); t.tabIndex = li === tli ? 0 : -1; } }
249
+ function toTitle(bar, tli, opts = {}) {
250
+ if (track) closeAll();
251
+ rove(bar, tli);
252
+ quiet = !!opts.quiet;
253
+ const t = titleOf(tli); if (t) t.focus({ preventScroll: true });
254
+ quiet = false;
255
+ }
256
+ function enter(bar, tli, last) {
257
+ if (!track || track.li !== tli) { closeAll(); track = { bar, li: tli, mode: "sticky", hot: null, last: { x: 0, y: 0 } }; }
258
+ openTitle(bar, tli);
259
+ const m = menuOf(tli), list = m ? items(m) : [];
260
+ if (list.length) focusItem(list[last ? list.length - 1 : 0]);
261
+ }
262
+ document.addEventListener("focusin", (e) => {
263
+ const t = e.target instanceof Element ? e.target : null;
264
+ const tli = t && t.matches(`:is(${BAR}) > li > :is(button, a, span)`) ? t.parentElement : null;
265
+ if (!tli || optedOut(tli) || quiet || (e.relatedTarget instanceof Node && tli.contains(e.relatedTarget))) return;
266
+ if (track && track.mode !== "preview") return; // a pointer gesture is under way
267
+ const bar = tli.parentElement;
268
+ if (track) closeAll();
269
+ rove(bar, tli);
270
+ track = { bar, li: tli, mode: "preview", hot: null, last: { x: 0, y: 0 } };
271
+ openTitle(bar, tli);
272
+ });
273
+ document.addEventListener("focusout", (e) => {
274
+ if (!track || track.mode === "blink") return;
275
+ const to = e.relatedTarget;
276
+ if (to instanceof Node && track.bar.contains(to)) return;
277
+ if (track.mode === "preview" || track.mode === "sticky") closeAll();
278
+ });
279
+ document.addEventListener("keydown", (e) => {
280
+ if (e.defaultPrevented || e.isComposing) return;
281
+ const f = document.activeElement instanceof Element ? document.activeElement : null;
282
+ if (e.metaKey || e.ctrlKey) return shortcut(e);
283
+ if (!f) return;
284
+ const tli = f.closest(`:is(${BAR}) > li`);
285
+ if (!tli || optedOut(tli)) return;
286
+ const bar = tli.parentElement, titles = titlesOf(bar);
287
+ const inMenu = f.closest(`:is(${MENU}) > li`);
288
+ const go = (fn) => { e.preventDefault(); e.stopPropagation(); fn(); };
289
+ const next = (d) => titles[(titles.indexOf(tli) + d + titles.length) % titles.length];
290
+ if (!inMenu) {
291
+ if (["ArrowDown", "Enter", " ", "Home"].includes(e.key)) return go(() => enter(bar, tli, false));
292
+ if (e.key === "End" || e.key === "ArrowUp") return go(() => enter(bar, tli, true));
293
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") return go(() => toTitle(bar, next(e.key === "ArrowRight" ? 1 : -1)));
294
+ if (e.key === "Escape" && track) return go(() => toTitle(bar, tli, { quiet: true }));
295
+ return;
296
+ }
297
+ if (!track) track = { bar, li: tli, mode: "sticky", hot: null, last: { x: 0, y: 0 } };
298
+ track.mode = track.mode === "preview" ? "sticky" : track.mode;
299
+ const menu = inMenu.parentElement, list = items(menu), i = list.indexOf(inMenu);
300
+ const parentItem = menu.parentElement.closest(`:is(${MENU}) > li`);
301
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") return go(() => focusItem(list[(i + (e.key === "ArrowDown" ? 1 : list.length - 1)) % list.length]));
302
+ if (e.key === "Home" || e.key === "End") return go(() => focusItem(list[e.key === "Home" ? 0 : list.length - 1]));
303
+ if (e.key === "ArrowRight" && submenuOf(inMenu)) return go(() => { inMenu.classList.add("open"); const s = items(submenuOf(inMenu)); if (s[0]) focusItem(s[0]); });
304
+ if (e.key === "ArrowRight") return go(() => toTitle(bar, next(1)));
305
+ if ((e.key === "ArrowLeft" || e.key === "Escape") && parentItem) return go(() => { closeSub(parentItem); focusItem(parentItem); });
306
+ if (e.key === "ArrowLeft") return go(() => toTitle(bar, next(-1)));
307
+ if (e.key === "Escape") return go(() => toTitle(bar, tli, { quiet: true }));
308
+ if ((e.key === "Enter" || e.key === " ") && usable(inMenu) && !submenuOf(inMenu)) return go(() => choose(inMenu, () => toTitle(bar, tli, { quiet: true })));
309
+ }, true);
310
+
311
+ function shortcut(e) {
312
+ if (e.altKey || e.key.length !== 1) return;
313
+ const key = e.key.toLowerCase();
314
+ for (const bar of document.querySelectorAll(`:is(${BAR})[data-clio-shortcuts]`)) {
315
+ for (const kbd of bar.querySelectorAll(`:is(${MENU}) > li kbd`)) {
316
+ const m = kbd.textContent.trim().match(/^(⇧?)\s*⌘\s*(.)$/u); // "⌘N", "⇧⌘S"
317
+ const li = kbd.closest(`:is(${MENU}) > li`);
318
+ if (!m || m[2].toLowerCase() !== key || !!m[1] !== e.shiftKey || !usable(li)) continue;
319
+ e.preventDefault();
320
+ const tli = li.closest(`:is(${BAR}) > li`), t = titleOf(tli);
321
+ if (t) t.classList.add("active");
322
+ setTimeout(() => { if (t) t.classList.remove("active"); }, 8 * TICK);
323
+ control(li).click();
324
+ return;
325
+ }
326
+ }
327
+ }
328
+
329
+ /* ---- Pop-up menus (<select>) ------------------------------------------ */
330
+ // The open menu lies over the button with the current item on the button's
331
+ // row. Where the browser draws it (appearance: base-select) clio.js only
332
+ // lines it up; elsewhere it draws the era's .menu itself and tracks it like
333
+ // the menu bar's. Touch keeps the system's picker.
334
+ const POPUP = "select:not([multiple]):not([size]), select[size='1']";
335
+ const baseSelect = !!(window.CSS && CSS.supports && CSS.supports("appearance", "base-select")) && matchMedia("(pointer: fine)").matches; // as the stylesheet: touch keeps the system picker
336
+ // Drawing Board's menus need their pencil shade and corner pixels, which the
337
+ // browser's picker cannot carry: with clio.js there, the stylesheet hands those
338
+ // pop-ups back (data-clio-popups) and clio.js draws them as it does elsewhere.
339
+ if (baseSelect) document.documentElement.setAttribute("data-clio-popups", "");
340
+ const drawn = (sel) => baseSelect && getComputedStyle(sel).appearance === "base-select";
341
+ const rowOf = (el) => px(getComputedStyle(el).getPropertyValue("--clio-menu-item-height")) || 16;
342
+ const borderTop = (el) => px(getComputedStyle(el).getPropertyValue("--clio-menu-border-top-width")) || 1;
343
+ const blink = (li, done) => {
344
+ if (reduced()) return done();
345
+ let n = 0;
346
+ const step = () => { li.toggleAttribute(BLINK, n % 2 === 0); if (++n === 7) { li.removeAttribute(BLINK); return done(); } setTimeout(step, 3 * TICK); };
347
+ step();
348
+ };
349
+ const alignPicker = (sel) => {
350
+ const h = rowOf(sel), r = sel.getBoundingClientRect(), atTop = getComputedStyle(sel).getPropertyValue("--clio-popup-menu-align").trim() === "top";
351
+ sel.style.setProperty("--clio-picker-shift", `${(atTop ? 0 : Math.round((r.height - h) / 2) - borderTop(sel)) - Math.max(0, sel.selectedIndex) * h}px`);
352
+ };
353
+ let popup = null; // { sel, menu, mode, hot, opened }
354
+
355
+ function openPopup(sel) {
356
+ const r = sel.getBoundingClientRect(), h = rowOf(sel);
357
+ const menu = document.createElement("ul");
358
+ menu.className = "menu clio-popup-menu";
359
+ menu.setAttribute("role", "menu");
360
+ for (const o of sel.options) {
361
+ const li = document.createElement("li");
362
+ li.textContent = o.label || o.text;
363
+ // The menu hangs off the era's root, outside the select: carry each option's language
364
+ // so :lang() gives a Chinese, Japanese or Korean item its font (96-cjk.css).
365
+ const lang = o.closest("[lang]");
366
+ if (lang) li.lang = lang.lang;
367
+ li.dataset.index = o.index;
368
+ if (o.disabled || (o.parentElement && o.parentElement.disabled)) li.classList.add("disabled");
369
+ if (o.selected) li.classList.add("checked");
370
+ menu.appendChild(li);
371
+ }
372
+ const host = sel.closest("[data-era]") || document.body;
373
+ // Per era: the current item on the button's row (centred), or, Mac OS 9.0, the menu's
374
+ // top on the button's with the current item there; as wide as the button less
375
+ // --clio-popup-menu-trim (Mac OS 9.0: the arrows stay in sight).
376
+ const cs = getComputedStyle(sel), trim = px(cs.getPropertyValue("--clio-popup-menu-trim")), atTop = cs.getPropertyValue("--clio-popup-menu-align").trim() === "top";
377
+ const row = atTop ? Math.round(r.top) : Math.round(r.top + (r.height - h) / 2) - borderTop(sel);
378
+ Object.assign(menu.style, { position: "fixed", left: `${Math.round(r.left)}px`, top: `${row - Math.max(0, sel.selectedIndex) * h}px`, minWidth: `${Math.round(r.width) - trim}px`, zIndex: "2147483000" });
379
+ const phase = cs.getPropertyValue("--clio-db-phase").trim(); // the pens keep the button's place along them
380
+ if (phase) menu.style.setProperty("--clio-db-phase", phase);
381
+ host.appendChild(menu);
382
+ sel.classList.add("clio-open"); // the button draws its arrows pressed while the menu is open
383
+ // keep it on screen, as the Menu Manager does
384
+ const m = menu.getBoundingClientRect();
385
+ if (m.top < 0) menu.style.top = "0px";
386
+ else if (m.bottom > innerHeight) menu.style.top = `${Math.max(0, innerHeight - m.height)}px`;
387
+ popup = { sel, menu, mode: "press", hot: null, opened: performance.now(), moved: false, x: 0, y: 0 };
388
+ }
389
+ function closePopup() { if (popup) { popup.menu.remove(); popup.sel.classList.remove("clio-open"); popup = null; } }
390
+ function lightPopup(li) {
391
+ if (!popup || popup.hot === li) return;
392
+ if (popup.hot) popup.hot.classList.remove("active");
393
+ popup.hot = li && !li.classList.contains("disabled") ? li : null;
394
+ if (popup.hot) popup.hot.classList.add("active");
395
+ }
396
+ function choosePopup(li) {
397
+ const { sel } = popup; popup.mode = "blink";
398
+ blink(li, () => {
399
+ closePopup();
400
+ const i = +li.dataset.index;
401
+ if (sel.selectedIndex !== i) {
402
+ sel.selectedIndex = i;
403
+ sel.dispatchEvent(new Event("input", { bubbles: true }));
404
+ sel.dispatchEvent(new Event("change", { bubbles: true }));
405
+ }
406
+ });
407
+ }
408
+
409
+ document.addEventListener("pointerdown", (e) => {
410
+ if (e.button !== 0 || !(e.target instanceof Element)) return;
411
+ if (popup) {
412
+ const li = e.target.closest(".clio-popup-menu > li");
413
+ e.preventDefault();
414
+ if (li && popup.menu.contains(li) && popup.mode === "sticky") { popup.mode = "press"; popup.moved = true; lightPopup(li); return; }
415
+ if (popup.mode !== "blink") closePopup();
416
+ return;
417
+ }
418
+ const sel = e.target.closest(POPUP);
419
+ if (!sel || sel.disabled || optedOut(sel)) return;
420
+ if (drawn(sel)) { alignPicker(sel); return; }
421
+ if (e.pointerType === "touch") return;
422
+ e.preventDefault();
423
+ sel.focus({ preventScroll: true });
424
+ openPopup(sel);
425
+ popup.x = e.clientX; popup.y = e.clientY;
426
+ }, true);
427
+ document.addEventListener("keydown", (e) => {
428
+ const f = document.activeElement;
429
+ if (f instanceof Element && f.matches(POPUP) && drawn(f)) alignPicker(f);
430
+ if (popup && e.key === "Escape") { e.preventDefault(); closePopup(); }
431
+ }, true);
432
+ document.addEventListener("pointermove", (e) => {
433
+ if (!popup || popup.mode === "blink") return;
434
+ const el = document.elementFromPoint(e.clientX, e.clientY);
435
+ const li = el && el.closest(".clio-popup-menu > li");
436
+ if (Math.abs(e.clientX - popup.x) + Math.abs(e.clientY - popup.y) > 3) popup.moved = true;
437
+ lightPopup(li && popup.menu.contains(li) ? li : null);
438
+ }, true);
439
+ document.addEventListener("pointerup", (e) => {
440
+ if (!popup || popup.mode !== "press") return;
441
+ const el = document.elementFromPoint(e.clientX, e.clientY);
442
+ const li = el && el.closest(".clio-popup-menu > li");
443
+ const click = !popup.moved; // a click, not a drag: the item under the pointer is just the current one
444
+ if (li && popup.menu.contains(li) && !li.classList.contains("disabled") && !click) return choosePopup(li);
445
+ if ((el && el.closest(POPUP) === popup.sel) || (li && click)) {
446
+ if (sticky(popup.sel)) { popup.mode = "sticky"; return; }
447
+ }
448
+ closePopup();
449
+ }, true);
450
+ addEventListener("blur", closePopup);
451
+ addEventListener("resize", closePopup);
452
+
453
+ /* ---- Scroll bars -------------------------------------------------------- */
454
+ const HOSTS = ".window-pane, div.listbox, div[role=listbox], ul[role=listbox], .clio-scroll";
455
+ const LINE = 16; // one step of an arrow: one row of the Finder's list (System 6.0.8)
456
+ // A held arrow or lane steps at once, then after --clio-scrollbar-repeat-delay
457
+ // (0 in System 6 and 7, whose TrackControl had none; 0.2 s in Mac OS 9's Finder).
458
+ // The Mac then stepped as fast as the application could scroll: no fixed rate
459
+ // existed, so this one is chosen.
460
+ const REPEAT_EVERY = 50;
461
+ const px = (v) => parseFloat(v) || 0;
462
+ const make = (cls, parent) => { const d = document.createElement("div"); d.className = cls; parent.appendChild(d); return d; };
463
+ const hosts = new WeakMap();
464
+ const live = new Set(); // every Bars, to measure again when an era changes
465
+
466
+ class Bars {
467
+ constructor(host) {
468
+ this.host = host;
469
+ const cs = getComputedStyle(host);
470
+ for (const [v, side] of [["pt", "Top"], ["pr", "Right"], ["pb", "Bottom"], ["pl", "Left"]]) host.style.setProperty(`--clio-sb-${v}`, cs[`padding${side}`]);
471
+ this.pad = { t: px(cs.paddingTop), l: px(cs.paddingLeft) };
472
+ this.overflow = { x: cs.overflowX, y: cs.overflowY };
473
+ host.classList.add("clio-sb-host");
474
+ const a = this.anchor = document.createElement("div");
475
+ a.className = "clio-sb-anchor";
476
+ a.setAttribute("aria-hidden", "true");
477
+ this.v = this.bar("v");
478
+ this.h = this.bar("h");
479
+ this.corner = make("clio-sb-corner", a);
480
+ host.prepend(a);
481
+ this.need = { x: false, y: false };
482
+ this.queued = false;
483
+ this.update = () => { this.queued = false; this.layout(); };
484
+ this.schedule = () => { if (!this.queued) { this.queued = true; requestAnimationFrame(this.update); } };
485
+ host.addEventListener("scroll", this.schedule, { passive: true });
486
+ this.ro = new ResizeObserver(this.schedule);
487
+ this.ro.observe(host);
488
+ this.mo = new MutationObserver(this.schedule);
489
+ this.mo.observe(host, { childList: true, subtree: true, characterData: true });
490
+ this.layout();
491
+ }
492
+
493
+ bar(axis) {
494
+ const b = make(`clio-sb clio-sb-${axis}`, this.anchor);
495
+ const parts = { bar: b, axis, track: make("clio-sb-track", b), dec: make("clio-sb-button clio-sb-dec", b), inc: make("clio-sb-button clio-sb-inc", b), thumb: make("clio-sb-thumb", b), ghost: null };
496
+ b.addEventListener("pointerdown", (e) => this.press(parts, e));
497
+ return parts;
498
+ }
499
+
500
+ read() {
501
+ const h = this.host, cs = getComputedStyle(h); let r;
502
+ return {
503
+ cw: h.clientWidth, ch: h.clientHeight, sw: h.scrollWidth, sh: h.scrollHeight, sl: h.scrollLeft, st: h.scrollTop,
504
+ size: px(cs.getPropertyValue("--clio-scrollbar-size")) || 15,
505
+ len: px(cs.getPropertyValue("--clio-scrollbar-thumb-length")) || 16,
506
+ live: cs.getPropertyValue("--clio-scrollbar-thumb-tracking").trim() === "live",
507
+ delay: parseFloat(cs.getPropertyValue("--clio-scrollbar-repeat-delay")) || 0,
508
+ smart: !!h.closest('[data-clio-scrolling="smart"]'),
509
+ overlap: px(cs.getPropertyValue("--clio-scrollbar-thumb-overlap")),
510
+ // the padding box's place on the page: QuickDraw aligned a lane's pattern with the
511
+ // screen, so the lanes are aligned with the page (System 6.0.8, System 7.5.3)
512
+ ox: (r = h.getBoundingClientRect()).left + h.clientLeft + scrollX, oy: r.top + h.clientTop + scrollY,
513
+ };
514
+ }
515
+
516
+ layout() {
517
+ let m = this.read();
518
+ const want = {
519
+ y: this.overflow.y === "scroll" || (this.overflow.y === "auto" && m.sh > m.ch),
520
+ x: this.overflow.x === "scroll" || (this.overflow.x === "auto" && m.sw > m.cw),
521
+ };
522
+ if (want.x !== this.need.x || want.y !== this.need.y) {
523
+ this.need = want;
524
+ this.host.classList.toggle("clio-sb-y", want.y);
525
+ this.host.classList.toggle("clio-sb-x", want.x);
526
+ m = this.read(); // the padding changed: measure again, once
527
+ }
528
+ this.m = m;
529
+ const { cw, ch, size } = m, smart = m.smart && m.live; // Smart Scrolling is Mac OS 8/9's
530
+ this.place(this.v, this.need.y, cw - size - this.pad.l, -this.pad.t, ch - (this.need.x ? size : 0), m.st, m.sh - ch, ch / m.sh, smart);
531
+ this.place(this.h, this.need.x, -this.pad.l, ch - size - this.pad.t, cw - (this.need.y ? size : 0), m.sl, m.sw - cw, cw / m.sw, smart);
532
+ const c = this.corner.style;
533
+ c.display = this.need.x && this.need.y ? "block" : "none";
534
+ c.left = `${cw - size - this.pad.l}px`; c.top = `${ch - size - this.pad.t}px`; c.width = c.height = `${size}px`;
535
+ }
536
+
537
+ place(p, on, x, y, length, pos, max, visible, smart) {
538
+ const s = p.bar.style, { size } = this.m, vertical = p.axis === "v";
539
+ s.display = on ? "block" : "none";
540
+ if (!on) return;
541
+ s.left = `${x}px`; s.top = `${y}px`;
542
+ s.width = `${vertical ? size : length}px`; s.height = `${vertical ? length : size}px`;
543
+ // The lane's origin on the page (its line is the track's first pixel), as a pattern offset.
544
+ const mod = (v, n) => ((Math.round(-v) % n) + n) % n;
545
+ const tx = this.m.ox + this.pad.l + x + (vertical ? 1 : size), ty = this.m.oy + this.pad.t + y + (vertical ? size : 1);
546
+ s.setProperty("--clio-sb-px", `${mod(tx, 8)}px`); s.setProperty("--clio-sb-py", `${mod(ty, 8)}px`);
547
+ p.bar.classList.toggle("clio-sb-smart", smart);
548
+ p.bar.classList.toggle("clio-sb-off", max <= 0);
549
+ const ov = this.m.overlap; // Platinum's box shares its end lines with the arrows'
550
+ const track = length - 2 * size + 2 * ov;
551
+ const thumb = Math.min(track, smart ? Math.max(this.m.len, Math.round(track * visible)) : this.m.len);
552
+ const range = Math.max(0, track - thumb);
553
+ const at = max > 0 ? Math.round(range * Math.min(1, pos / max)) : 0;
554
+ Object.assign(p, { track: p.track, length: track, thumbLength: thumb, range, at, max });
555
+ p.thumb.style[vertical ? "height" : "width"] = `${thumb}px`;
556
+ p.thumb.style[vertical ? "minHeight" : "minWidth"] = "0"; // the browser bar's minimum is not the box's length
557
+ p.thumb.style[vertical ? "top" : "left"] = `${(smart ? 0 : size) - ov}px`;
558
+ p.thumb.style[vertical ? "width" : "height"] = "";
559
+ if (!p.dragging) p.thumb.style.transform = vertical ? `translateY(${at}px)` : `translateX(${at}px)`;
560
+ }
561
+
562
+ scrollBy(p, d) { this.host.scrollBy(p.axis === "v" ? { top: d, behavior: "instant" } : { left: d, behavior: "instant" }); }
563
+ scrollToThumb(p, at) { const v = p.range > 0 ? Math.round(at / p.range * p.max) : 0; if (p.axis === "v") this.host.scrollTop = v; else this.host.scrollLeft = v; }
564
+
565
+ press(p, e) {
566
+ if (e.button !== 0 || p.bar.classList.contains("clio-sb-off")) return;
567
+ e.preventDefault();
568
+ const vertical = p.axis === "v", m = this.m;
569
+ p.bar.setPointerCapture(e.pointerId);
570
+ const r = p.bar.getBoundingClientRect();
571
+ const along = (ev) => (vertical ? ev.clientY - r.top : ev.clientX - r.left);
572
+ const smart = p.bar.classList.contains("clio-sb-smart");
573
+ const trackStart = (smart ? 0 : m.size) - m.overlap;
574
+ const target = e.target;
575
+ let timer = 0, stop;
576
+
577
+ const repeat = (fn, test) => {
578
+ fn();
579
+ timer = setTimeout(function again() { if (test()) fn(); timer = setTimeout(again, REPEAT_EVERY); }, m.delay || REPEAT_EVERY);
580
+ };
581
+
582
+ if (target === p.dec || target === p.inc) {
583
+ const button = target, sign = button === p.dec ? -1 : 1;
584
+ let over = true;
585
+ button.classList.add("clio-held");
586
+ const inButton = (ev) => { const b = button.getBoundingClientRect(); return ev.clientX >= b.left && ev.clientX < b.right && ev.clientY >= b.top && ev.clientY < b.bottom; };
587
+ p.onmove = (ev) => { over = inButton(ev); button.classList.toggle("clio-held", over); };
588
+ repeat(() => this.scrollBy(p, sign * LINE), () => over);
589
+ stop = () => button.classList.remove("clio-held");
590
+ } else if (target === p.thumb) {
591
+ const grab = along(e) - trackStart - p.at;
592
+ const live = m.live;
593
+ p.dragging = true;
594
+ if (!live) {
595
+ p.ghost = make("clio-sb-ghost", p.bar);
596
+ p.ghost.style.cssText = vertical
597
+ ? `left:1px;right:0;top:${m.size}px;height:${p.thumbLength}px`
598
+ : `top:1px;bottom:0;left:${m.size}px;width:${p.thumbLength}px`; // System 7.5.3: 14x16, the box's own size inside the lane line
599
+ }
600
+ let at = p.at;
601
+ p.onmove = (ev) => {
602
+ at = Math.max(0, Math.min(p.range, Math.round(along(ev) - trackStart - grab)));
603
+ if (live) { this.scrollToThumb(p, at); p.thumb.style.transform = vertical ? `translateY(${at}px)` : `translateX(${at}px)`; return; }
604
+ const shift = `${-at}px`;
605
+ p.ghost.style.transform = vertical ? `translateY(${at}px)` : `translateX(${at}px)`;
606
+ p.ghost.style.setProperty(vertical ? "--clio-sb-gy" : "--clio-sb-gx", shift);
607
+ };
608
+ p.onmove(e);
609
+ stop = () => {
610
+ p.dragging = false;
611
+ if (p.ghost) { p.ghost.remove(); p.ghost = null; this.scrollToThumb(p, at); }
612
+ this.schedule();
613
+ };
614
+ } else {
615
+ // The lane pages towards the pointer until the box reaches it.
616
+ let point = along(e);
617
+ const page = (vertical ? m.ch : m.cw) - LINE;
618
+ p.onmove = (ev) => { point = along(ev); };
619
+ const beyond = () => { const a = trackStart + p.at; return point < a ? -1 : point >= a + p.thumbLength ? 1 : 0; };
620
+ repeat(() => { const d = beyond(); if (d) this.scrollBy(p, d * page); }, () => !!beyond());
621
+ }
622
+
623
+ const up = (ev) => {
624
+ if (ev.pointerId !== e.pointerId) return;
625
+ clearTimeout(timer);
626
+ p.bar.removeEventListener("pointermove", move);
627
+ p.bar.removeEventListener("pointerup", up);
628
+ p.bar.removeEventListener("pointercancel", up);
629
+ p.onmove = null;
630
+ if (stop) stop();
631
+ };
632
+ const move = (ev) => { if (ev.pointerId === e.pointerId && p.onmove) p.onmove(ev); };
633
+ p.bar.addEventListener("pointermove", move);
634
+ p.bar.addEventListener("pointerup", up);
635
+ p.bar.addEventListener("pointercancel", up);
636
+ }
637
+
638
+ destroy() {
639
+ this.ro.disconnect(); this.mo.disconnect();
640
+ this.host.removeEventListener("scroll", this.schedule);
641
+ this.anchor.remove();
642
+ this.host.classList.remove("clio-sb-host", "clio-sb-x", "clio-sb-y");
643
+ for (const v of ["pt", "pr", "pb", "pl"]) this.host.style.removeProperty(`--clio-sb-${v}`);
644
+ }
645
+ }
646
+
647
+ const blockish = new Set(["block", "flow-root", "list-item"]);
648
+ function scrollbars(root = document) {
649
+ const found = [];
650
+ const list = root instanceof Element && root.matches(HOSTS) ? [root] : [];
651
+ list.push(...(root.querySelectorAll ? root.querySelectorAll(HOSTS) : []));
652
+ for (const el of list) {
653
+ if (hosts.has(el) || optedOut(el) || el.closest("[data-clio-scrollbars=native]")) continue;
654
+ const cs = getComputedStyle(el);
655
+ if (!blockish.has(cs.display)) continue; // flex and grid scrollers keep the browser's bar
656
+ if (![cs.overflowX, cs.overflowY].some((o) => o === "auto" || o === "scroll")) continue;
657
+ const bars = new Bars(el);
658
+ hosts.set(el, bars);
659
+ live.add(bars);
660
+ found.push(el);
661
+ }
662
+ return found;
663
+ }
664
+
665
+ const claimBar = (b) => {
666
+ if (optedOut(b) || b.hasAttribute("data-clio-menus")) return;
667
+ b.setAttribute("data-clio-menus", "");
668
+ const ts = titlesOf(b), cur = ts.find((li) => titleOf(li).tabIndex === 0 && titleOf(li).hasAttribute("tabindex")) || ts[0];
669
+ if (cur) rove(b, cur); // one Tab stop for the whole bar
670
+ for (const c of b.querySelectorAll(`:is(${MENU}) > li > :is(a, button)`)) if (!c.hasAttribute("tabindex")) c.tabIndex = -1; // items are reached with the arrows
671
+ };
672
+ const claimBars = (root) => { for (const b of (root.querySelectorAll ? root.querySelectorAll(BAR) : [])) claimBar(b); if (root instanceof Element && root.matches(BAR)) claimBar(root); };
673
+ function start() {
674
+ liveRegion = document.createElement("div");
675
+ liveRegion.className = "clio-live"; liveRegion.setAttribute("aria-live", "polite");
676
+ document.body.appendChild(liveRegion);
677
+ claimWindows(document);
678
+ scrollbars(document);
679
+ claimBars(document); // clio.js opens these menus; focus alone no longer does
680
+ queueCentre();
681
+ new MutationObserver((records) => {
682
+ for (const r of records) for (const n of r.addedNodes) if (n instanceof Element && !n.classList.contains("clio-sb-anchor") && !n.closest(".clio-sb-anchor")) { scrollbars(n); claimBars(n); if (!n.classList.contains("clio-move-menu") && !n.classList.contains("clio-outline") && !n.classList.contains("clio-live")) claimWindows(n); }
683
+ }).observe(document.documentElement, { childList: true, subtree: true });
684
+ // An era or Smart Scrolling switched anywhere: the bars' sizes come from the era.
685
+ new MutationObserver(() => { queueCentre(); for (const b of live) if (b.host.isConnected) b.schedule(); })
686
+ .observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ["data-era", "data-clio-scrolling"] });
687
+ }
688
+
689
+ /* ---- Windows: dragged outline, zoom rects, collapse -------------------- */
690
+ // Dragging a title bar moves a grey outline (QuickDraw's DragGrayRgn: 1px in
691
+ // System 7.5.3, a 2px band in Mac OS 9.0, both measured) and the window
692
+ // follows when the button comes up. Only windows positioned absolute or
693
+ // fixed, or marked data-clio-drag, move.
694
+ const outlineBox = (r, width, margin = 0) => {
695
+ const d = document.createElement("div");
696
+ d.className = "clio-outline";
697
+ d.setAttribute("aria-hidden", "true");
698
+ Object.assign(d.style, { left: `${Math.round(r.left)}px`, top: `${Math.round(r.top)}px`, width: `${Math.round(r.width)}px`, height: `${Math.round(r.height)}px`, padding: `${width + margin}px` });
699
+ grayPhase(d, Math.round(r.left), Math.round(r.top));
700
+ return d;
701
+ };
702
+ // QuickDraw's gray kept to the screen as the outline moved: keep it to the page (a fixed
703
+ // background would not survive the transform that moves the outline).
704
+ const grayPhase = (d, x, y) => { d.style.backgroundPosition = `${Math.round(x + y + scrollX + scrollY) & 1}px 0`; };
705
+ const outlineWidth = (el) => px(getComputedStyle(el).getPropertyValue("--clio-drag-outline-width")) || 1;
706
+ document.addEventListener("pointerdown", (e) => {
707
+ if (e.button !== 0 || !(e.target instanceof Element)) return;
708
+ const bar = e.target.closest(".window > .title-bar");
709
+ if (!bar || e.target.closest("button, a, input, select, .close, .close-box, .resize, .resize-box, .zoom, .shade, .shade-box, .collapse") || optedOut(bar)) return;
710
+ const win = bar.parentElement, cs = getComputedStyle(win);
711
+ if (!(cs.position === "absolute" || cs.position === "fixed" || win.hasAttribute("data-clio-drag"))) return;
712
+ e.preventDefault();
713
+ // The dragged region is the window's structure: its frame and its drop shadow.
714
+ // (Drawing Board: a shadow 7px across and 8px down, and a margin for the corners' overrun.)
715
+ const ws = getComputedStyle(win), grow = px(ws.getPropertyValue("--clio-drag-outline-extra")), m = px(ws.getPropertyValue("--clio-drag-outline-margin"));
716
+ const growY = ws.getPropertyValue("--clio-drag-outline-extra-y").trim() ? px(ws.getPropertyValue("--clio-drag-outline-extra-y")) : grow;
717
+ const b = win.getBoundingClientRect(), r = { left: b.left - m, top: b.top - m, width: b.width + grow + 2 * m, height: b.height + growY + 2 * m };
718
+ const o = outlineBox(r, outlineWidth(win), m);
719
+ (win.closest("[data-era]") || document.body).appendChild(o);
720
+ const x0 = e.clientX, y0 = e.clientY; let dx = 0, dy = 0;
721
+ const move = (ev) => { if (ev.pointerId !== e.pointerId) return; dx = Math.round(ev.clientX - x0); dy = Math.round(ev.clientY - y0); /* a finger reports fractions */ o.style.transform = `translate(${dx}px, ${dy}px)`; grayPhase(o, Math.round(r.left) + dx, Math.round(r.top) + dy); };
722
+ const up = (ev) => {
723
+ if (ev.pointerId !== e.pointerId) return;
724
+ document.removeEventListener("pointermove", move, true); document.removeEventListener("pointerup", up, true); document.removeEventListener("pointercancel", up, true);
725
+ o.remove();
726
+ if (ev.type === "pointercancel" || (!dx && !dy)) return;
727
+ moveBy(win, dx, dy);
728
+ };
729
+ document.addEventListener("pointermove", move, true); document.addEventListener("pointerup", up, true); document.addEventListener("pointercancel", up, true);
730
+ });
731
+
732
+ // One way to move, for the pointer, the keyboard and the move menu: kept
733
+ // inside the viewport (fixed) or the window's container (absolute).
734
+ function moveBy(win, dx, dy, say) {
735
+ const cs = getComputedStyle(win);
736
+ let left = px(cs.left) + dx, top = px(cs.top) + dy;
737
+ const op = win.offsetParent;
738
+ const box = cs.position === "fixed" || !op || op === document.body || op === document.documentElement ? { w: document.documentElement.clientWidth, h: innerHeight } : { w: op.clientWidth, h: op.clientHeight };
739
+ left = Math.max(0, Math.min(left, box.w - win.offsetWidth));
740
+ top = Math.max(0, Math.min(top, box.h - win.offsetHeight));
741
+ win.style.left = `${Math.round(left)}px`; win.style.top = `${Math.round(top)}px`;
742
+ for (const b of live) if (win.contains(b.host)) b.schedule(); // the lanes' patterns keep to the page
743
+ if (say) announce(`${windowName(win)} ${say}`);
744
+ win.dispatchEvent(new CustomEvent("clio:move", { bubbles: true, detail: { dx, dy } }));
745
+ }
746
+ const windowName = (win) => { const t = win.querySelector(":scope > .title-bar :is(.title, h1, h2, h3)"); return (t && t.textContent.trim()) || "Window"; };
747
+ // A polite live region, there from the start so screen readers register it.
748
+ let liveRegion = null;
749
+ function announce(text) {
750
+ if (!liveRegion) return;
751
+ liveRegion.textContent = text;
752
+ clearTimeout(announce.t); announce.t = setTimeout(() => { liveRegion.textContent = ""; }, 1000);
753
+ }
754
+ // The keyboard and single-pointer way: a move button at the start of each
755
+ // movable window's title bar, out of sight until it has keyboard focus.
756
+ // Arrow keys move the window 16px (Shift: 1px); Return, Space, a click or a
757
+ // right click on the title bar open a menu of moves.
758
+ const STEP = 16;
759
+ const MOVES = [["Move left", -1, 0, "moved left"], ["Move right", 1, 0, "moved right"], ["Move up", 0, -1, "moved up"], ["Move down", 0, 1, "moved down"]];
760
+ function handleFor(win) {
761
+ const bar = win.querySelector(":scope > .title-bar");
762
+ if (!bar || bar.querySelector(":scope > .clio-move")) return;
763
+ bar.classList.add("clio-drag-bar"); // a finger on it drags the window, not the page
764
+ const b = document.createElement("button");
765
+ b.type = "button"; b.className = "clio-move";
766
+ b.setAttribute("aria-haspopup", "menu"); b.setAttribute("aria-expanded", "false");
767
+ b.setAttribute("aria-label", `Move ${windowName(win)}. Arrow keys move it.`);
768
+ bar.prepend(b);
769
+ }
770
+ const movable = (win) => { const p = getComputedStyle(win).position; return p === "absolute" || p === "fixed" || win.hasAttribute("data-clio-drag"); };
771
+ function claimWindows(root) {
772
+ const list = root instanceof Element && root.matches(".window") ? [root] : [];
773
+ if (root.querySelectorAll) list.push(...root.querySelectorAll(".window"));
774
+ for (const w of list) if (!optedOut(w) && movable(w)) handleFor(w);
775
+ }
776
+ let moveMenu = null;
777
+ function closeMoveMenu(refocus) {
778
+ if (!moveMenu) return;
779
+ const { menu, handle } = moveMenu; moveMenu = null;
780
+ menu.remove(); handle.setAttribute("aria-expanded", "false");
781
+ if (refocus) handle.focus({ preventScroll: true });
782
+ }
783
+ function openMoveMenu(win, at) {
784
+ closeMoveMenu(false);
785
+ const handle = win.querySelector(":scope > .title-bar > .clio-move");
786
+ const menu = document.createElement("ul");
787
+ menu.className = "menu clio-move-menu"; menu.setAttribute("role", "menu");
788
+ menu.setAttribute("aria-label", `Move ${windowName(win)}`);
789
+ for (const [label, x, y, say] of MOVES) {
790
+ const li = document.createElement("li"); li.setAttribute("role", "none");
791
+ const b = document.createElement("button"); b.type = "button"; b.setAttribute("role", "menuitem"); b.tabIndex = -1; b.textContent = label;
792
+ b.addEventListener("click", () => { closeMoveMenu(true); moveBy(win, x * STEP, y * STEP, say); });
793
+ li.appendChild(b); menu.appendChild(li);
794
+ }
795
+ const r = at || handle.getBoundingClientRect();
796
+ Object.assign(menu.style, { position: "fixed", left: `${Math.round(r.left)}px`, top: `${Math.round(r.top + (r.height || 0))}px`, zIndex: "2147483000" });
797
+ (win.closest("[data-era]") || document.body).appendChild(menu);
798
+ handle.setAttribute("aria-expanded", "true");
799
+ moveMenu = { menu, handle, win };
800
+ menu.querySelector("button").focus({ preventScroll: true });
801
+ menu.addEventListener("keydown", (e) => {
802
+ const bs = [...menu.querySelectorAll("button")], i = bs.indexOf(document.activeElement);
803
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); bs[(i + (e.key === "ArrowDown" ? 1 : bs.length - 1)) % bs.length].focus(); }
804
+ else if (e.key === "Home" || e.key === "End") { e.preventDefault(); bs[e.key === "Home" ? 0 : bs.length - 1].focus(); }
805
+ else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeMoveMenu(true); }
806
+ else if (e.key === "Tab") closeMoveMenu(false);
807
+ });
808
+ menu.addEventListener("focusout", (e) => { if (!(e.relatedTarget instanceof Node && menu.contains(e.relatedTarget))) setTimeout(() => { if (moveMenu && !moveMenu.menu.contains(document.activeElement)) closeMoveMenu(false); }); });
809
+ }
810
+ document.addEventListener("keydown", (e) => {
811
+ const h = e.target instanceof Element ? e.target.closest(".clio-move") : null;
812
+ if (!h || e.defaultPrevented) return;
813
+ const m = { ArrowLeft: [-1, 0, "moved left"], ArrowRight: [1, 0, "moved right"], ArrowUp: [0, -1, "moved up"], ArrowDown: [0, 1, "moved down"] }[e.key];
814
+ if (!m) return;
815
+ e.preventDefault();
816
+ const step = e.shiftKey ? 1 : STEP;
817
+ moveBy(h.closest(".window"), m[0] * step, m[1] * step, m[2]);
818
+ });
819
+ document.addEventListener("click", (e) => {
820
+ const h = e.target instanceof Element ? e.target.closest(".clio-move") : null;
821
+ if (h) { if (moveMenu && moveMenu.handle === h) closeMoveMenu(true); else openMoveMenu(h.closest(".window")); }
822
+ });
823
+ document.addEventListener("contextmenu", (e) => {
824
+ const bar = e.target instanceof Element ? e.target.closest(".window > .title-bar") : null;
825
+ if (!bar || !bar.querySelector(":scope > .clio-move") || e.target.closest("button:not(.clio-move)")) return;
826
+ e.preventDefault();
827
+ openMoveMenu(bar.parentElement, { left: e.clientX, top: e.clientY, height: 0 });
828
+ });
829
+ addEventListener("resize", () => requestAnimationFrame(() => { for (const h of document.querySelectorAll(".clio-move")) { const w = h.closest(".window"); if (w) moveBy(w, 0, 0); } }));
830
+
831
+ // Zoom rects (the Finder opening a folder, measured frame by frame): one
832
+ // outline per tick. System 7.5.3: 8, each edge eased by 1.6^k, the last four on
833
+ // screen at once. Mac OS 9.0: 6 in a 2px band at 1.4^(k-7) of the way, all kept
834
+ // until the window draws. clio.zoomRects(from, to) with elements or rects;
835
+ // { closing: true } runs it backwards. Opacity only; none under reduced motion.
836
+ function zoomRects(from, to, opts = {}) {
837
+ const rect = (x) => (x instanceof Element ? x.getBoundingClientRect() : x);
838
+ const a = rect(from), b = rect(to);
839
+ if (!a || !b || reduced()) return Promise.resolve();
840
+ const host = (to instanceof Element && to.closest("[data-era]")) || (from instanceof Element && from.closest("[data-era]")) || document.body;
841
+ // Per era (tokens): N outlines, the k-th at (r^k - c) / (r^M - c) of the way, `keep` on
842
+ // screen at once (0: all, until the last is gone), drawn with the drag outline's width.
843
+ const cs = getComputedStyle(host), num = (n, d) => { const v = parseFloat(cs.getPropertyValue(n)); return Number.isFinite(v) ? v : d; };
844
+ const N = num("--clio-zoom-count", 8), M = num("--clio-zoom-steps", N), Q = num("--clio-zoom-ratio", 1.6), C = num("--clio-zoom-offset", 1), keep = num("--clio-zoom-keep", 4) || Infinity;
845
+ const boxes = [];
846
+ for (let k = 1; k <= N; k++) {
847
+ const p = (Q ** k - C) / (Q ** M - C), l = (u, v) => u + (v - u) * p;
848
+ const o = outlineBox({ left: l(a.left, b.left), top: l(a.top, b.top), width: l(a.width, b.width), height: l(a.height, b.height) }, outlineWidth(host));
849
+ o.style.setProperty("--clio-drag-outline-extra", "0px"); // a plain rectangle
850
+ o.style.opacity = "0";
851
+ host.appendChild(o); boxes.push(o);
852
+ }
853
+ if (opts.closing) boxes.reverse();
854
+ return new Promise((done) => {
855
+ let t = 0;
856
+ const step = () => {
857
+ boxes.forEach((o, i) => { o.style.opacity = i <= t && i > t - keep ? "1" : "0"; });
858
+ if (++t < N + (Number.isFinite(keep) ? keep - 1 : 0)) setTimeout(step, TICK); else { boxes.forEach((o) => o.remove()); done(); }
859
+ };
860
+ step();
861
+ });
862
+ }
863
+
864
+ // The collapse box (WindowShade) folds a window to its title bar; with
865
+ // data-clio-shade="dblclick" on the window or an ancestor a double click on
866
+ // the title bar does too (Appearance > Options; off by default in Mac OS 9.0).
867
+ function collapse(win, on = !win.classList.contains("collapsed")) {
868
+ win.classList.toggle("collapsed", on);
869
+ const box = win.querySelector(":scope > .title-bar > :is(.shade, .shade-box, .collapse)");
870
+ if (box) box.setAttribute("aria-expanded", String(!on));
871
+ play(on ? "collapse" : "expand");
872
+ win.dispatchEvent(new CustomEvent("clio:collapse", { bubbles: true, detail: { collapsed: on } }));
873
+ }
874
+ document.addEventListener("click", (e) => {
875
+ if (!(e.target instanceof Element)) return;
876
+ const box = e.target.closest(".window > .title-bar > :is(.shade, .shade-box, .collapse)");
877
+ if (box && !optedOut(box) && !e.defaultPrevented) collapse(box.closest(".window"));
878
+ });
879
+ document.addEventListener("dblclick", (e) => {
880
+ if (!(e.target instanceof Element)) return;
881
+ const bar = e.target.closest(".window > .title-bar");
882
+ if (!bar || e.target.closest("button") || optedOut(bar)) return;
883
+ const opt = bar.closest("[data-clio-shade]");
884
+ if (opt && /\bdblclick\b/.test(opt.getAttribute("data-clio-shade"))) collapse(bar.parentElement);
885
+ });
886
+
887
+ /* ---- Centred labels ----------------------------------------------------- */
888
+ // QuickDraw centred a label at floor(spare / 2); a browser centres at spare / 2
889
+ // and, when the spare width is odd, puts the text on a half pixel, which each
890
+ // engine then rounds its own way (Chromium down, WebKit blends, Firefox up).
891
+ // Where a label's text starts on a half pixel, one more pixel at its end makes
892
+ // the spare even: buttons take it as padding, a title as margin. Measured when
893
+ // fonts load, on resize and when the page changes, never per frame; all reads
894
+ // come before all writes. data-clio-centre="off" (or data-clio-js="off") leaves
895
+ // a label alone.
896
+ const CENTRED = "button, .btn, .btn-default, .standard-button, .bevel, [role=tab], .title-bar > :is(.title, h1, h2, h3)";
897
+ const nudged = new Map(); // element -> the inline value it had
898
+ const isTitle = (el) => el.matches(".title-bar > *");
899
+ let centreQueued = false;
900
+ function centre() {
901
+ centreQueued = false;
902
+ // undo, so each label is measured as the stylesheet lays it out
903
+ for (const [el, was] of nudged) { if (isTitle(el)) el.style.marginRight = was; else el.style.paddingRight = was; }
904
+ nudged.clear();
905
+ const todo = [];
906
+ for (const el of document.querySelectorAll(CENTRED)) {
907
+ if (optedOut(el) || el.closest('[data-clio-centre="off"]') || !el.firstChild || !el.getClientRects().length) continue;
908
+ const cs = getComputedStyle(el);
909
+ if (cs.whiteSpace !== "nowrap" && el.getClientRects().length > 1) continue;
910
+ const range = document.createRange(); range.selectNodeContents(el);
911
+ const r = range.getBoundingClientRect();
912
+ if (!r.width) continue;
913
+ const frac = (r.left + scrollX) % 1;
914
+ if (Math.abs(frac - 0.5) < 0.26) todo.push([el, cs]);
915
+ }
916
+ for (const [el, cs] of todo) {
917
+ if (isTitle(el)) { nudged.set(el, el.style.marginRight); el.style.marginRight = `${(parseFloat(cs.marginRight) || 0) + 1}px`; }
918
+ else { nudged.set(el, el.style.paddingRight); el.style.paddingRight = `${(parseFloat(cs.paddingRight) || 0) + 1}px`; }
919
+ }
920
+ }
921
+ const queueCentre = () => { if (!centreQueued) { centreQueued = true; requestAnimationFrame(centre); } };
922
+ if (document.fonts) document.fonts.ready.then(queueCentre);
923
+ addEventListener("resize", queueCentre);
924
+ new MutationObserver((records) => {
925
+ for (const r of records) for (const n of r.addedNodes) if (n.nodeType === 3 || (n instanceof Element && !n.closest(".clio-sb-anchor, .clio-outline, .clio-popup-menu, .clio-live"))) return queueCentre();
926
+ for (const r of records) if (r.type === "characterData") return queueCentre();
927
+ }).observe(document.documentElement, { childList: true, subtree: true, characterData: true });
928
+
929
+ /* ---- Insertion point ---------------------------------------------------- */
930
+ // TextEdit's caret: a 1px line drawn in XOR (black on white, white on black)
931
+ // as tall as the line, on for GetCaretTime() ticks and off as long: 32 in
932
+ // System 6 and 7, about 48 in Mac OS 8 and 9 (--clio-caret-ticks; measured). clio.js draws it in text fields inside .window; the
933
+ // browser's own caret comes back while text is selected or an input method is
934
+ // composing, and the caret stays lit under reduced motion. Its place is
935
+ // measured when the text, the selection, a scroll or the size change, never
936
+ // per frame. data-clio-caret="off" (or data-clio-js="off") keeps the browser's.
937
+ const FIELD = "input:is([type=text], [type=search], [type=password], [type=email], [type=url], [type=tel], [type=number], :not([type])), textarea";
938
+ const MIRROR = ["boxSizing", "width", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontStretch", "letterSpacing", "wordSpacing", "lineHeight", "textTransform", "textIndent", "tabSize"];
939
+ let caretEl = null, caretField = null, caretTimer = 0, composing = false, caretQueued = false;
940
+ const caretWanted = (f) => f instanceof HTMLElement && f.matches(FIELD) && !!f.closest(".window") && !optedOut(f) && !f.closest('[data-clio-caret="off"]') && !f.readOnly && !f.disabled;
941
+ function caretStop() {
942
+ clearInterval(caretTimer); caretTimer = 0;
943
+ if (caretEl) caretEl.remove();
944
+ if (caretField) caretField.classList.remove("clio-caret-field");
945
+ caretEl = caretField = null;
946
+ }
947
+ function caretUpdate() {
948
+ caretQueued = false;
949
+ const f = document.activeElement;
950
+ if (!caretWanted(f) || composing || f.selectionStart == null || f.selectionStart !== f.selectionEnd) return caretStop();
951
+ // reads: the field, and a hidden copy of it holding the text before the caret
952
+ const cs = getComputedStyle(f), r = f.getBoundingClientRect(), area = f.tagName === "TEXTAREA";
953
+ const m = document.createElement("div");
954
+ for (const k of MIRROR) m.style[k] = cs[k];
955
+ Object.assign(m.style, { position: "absolute", visibility: "hidden", left: "-9999px", top: "0", borderStyle: "solid", whiteSpace: area ? "pre-wrap" : "pre", overflowWrap: area ? "break-word" : "normal", height: "auto" });
956
+ const text = f.type === "password" ? "•".repeat(f.value.length) : f.value;
957
+ m.textContent = text.slice(0, f.selectionStart);
958
+ const mark = document.createElement("span"); mark.textContent = "​"; m.appendChild(mark);
959
+ document.body.appendChild(m);
960
+ const mx = mark.offsetLeft, my = mark.offsetTop, glyph = mark.offsetHeight;
961
+ m.remove();
962
+ // the caret is a line tall, and TextEdit drew it in the pixel left of the character
963
+ const h = px(cs.getPropertyValue("--clio-caret-height")) || Math.round(parseFloat(cs.lineHeight)) || glyph;
964
+ const bt = parseFloat(cs.borderTopWidth) || 0, bb = parseFloat(cs.borderBottomWidth) || 0;
965
+ const y = area ? my - Math.round((h - glyph) / 2) - f.scrollTop : bt + Math.floor((r.height - bt - bb - h) / 2);
966
+ const x = mx - 1 - f.scrollLeft;
967
+ const inside = x >= 0 && x <= r.width && y + h > 0 && y < r.height;
968
+ // writes
969
+ if (!inside) { if (caretEl) caretEl.hidden = true; return; }
970
+ if (caretField !== f) { caretStop(); caretField = f; f.classList.add("clio-caret-field"); }
971
+ if (!caretEl) { caretEl = document.createElement("div"); caretEl.className = "clio-caret"; caretEl.setAttribute("aria-hidden", "true"); document.body.appendChild(caretEl); }
972
+ caretEl.hidden = false;
973
+ Object.assign(caretEl.style, { left: `${Math.round(r.left + scrollX + x)}px`, top: `${Math.round(r.top + scrollY + y)}px`, height: `${h}px` });
974
+ // each move shows the caret and starts the blink again (TEBlink after TEKey/TEClick)
975
+ caretEl.classList.remove("clio-caret-off");
976
+ clearInterval(caretTimer); caretTimer = 0;
977
+ const ticks = px(cs.getPropertyValue("--clio-caret-ticks")) || 32;
978
+ if (!reduced()) caretTimer = setInterval(() => caretEl && caretEl.classList.toggle("clio-caret-off"), ticks * TICK);
979
+ }
980
+ const queueCaret = () => { if (!caretQueued) { caretQueued = true; requestAnimationFrame(caretUpdate); } };
981
+ // (Firefox fires no selectionchange for a field's own selection: a click or a key moves it too)
982
+ for (const t of ["focusin", "selectionchange", "input", "keyup", "pointerup", "select"]) document.addEventListener(t, queueCaret);
983
+ matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", () => { if (caretField) queueCaret(); });
984
+ document.addEventListener("focusout", () => requestAnimationFrame(() => { if (!caretWanted(document.activeElement)) caretStop(); }));
985
+ document.addEventListener("compositionstart", () => { composing = true; caretStop(); });
986
+ document.addEventListener("compositionend", () => { composing = false; queueCaret(); });
987
+ addEventListener("scroll", () => { if (caretField) queueCaret(); }, { capture: true, passive: true });
988
+ addEventListener("resize", () => { if (caretField) queueCaret(); });
989
+
990
+ /* ---- Sounds ------------------------------------------------------------- */
991
+ // None ship (the Mac's are Apple's recordings). Give clio.sounds a URL per
992
+ // name and put data-clio-sounds on <html>: "alert" plays when an alert box
993
+ // appears, "menu" when a menu item is chosen, "collapse" and "expand" with
994
+ // the collapse box. clio.play(name) plays one yourself.
995
+ const sounds = {};
996
+ function play(name) {
997
+ const url = sounds[name];
998
+ if (!url || !document.documentElement.hasAttribute("data-clio-sounds")) return;
999
+ try { const a = new Audio(url); a.play().catch(() => {}); } catch {}
1000
+ }
1001
+ const ALERT = ".alert-box, [role=alertdialog]";
1002
+ const visible = (el) => el.isConnected && el.getClientRects().length > 0;
1003
+ new MutationObserver((records) => {
1004
+ if (!document.documentElement.hasAttribute("data-clio-sounds")) return;
1005
+ for (const r of records) {
1006
+ const els = r.type === "attributes" ? [r.target] : [...r.addedNodes].filter((n) => n instanceof Element);
1007
+ for (const el of els) if ((el.matches(ALERT) || el.querySelector(ALERT)) && visible(el.matches(ALERT) ? el : el.querySelector(ALERT))) return play("alert");
1008
+ }
1009
+ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["open", "hidden"] });
1010
+
1011
+ window.clio = {
1012
+ version: "0.12.0",
1013
+ zoomRects,
1014
+ collapse,
1015
+ sounds,
1016
+ play,
1017
+ scrollbars,
1018
+ unscroll(el) { const b = hosts.get(el); if (b) { b.destroy(); hosts.delete(el); live.delete(b); } },
1019
+ refresh(el) { const b = hosts.get(el); if (b) b.layout(); },
1020
+ centre, // align centred labels again (after a change clio.js cannot see)
1021
+ };
1022
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
1023
+ else start();
1024
+ })();