Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/mobile.js

25.4 KiB, 8 runs

created by r2519314175:1399, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* ============================================================
2 Daimond — the phone shell controller
3 ------------------------------------------------------------
4 Two surfaces beyond the chat floor and the bottom bar:
5
6 the DRAWER the rail (Diamonds / Chats / Admin), slid in from
7 the left. Opened by the hamburger, closed by a
8 tap on the scrim, a swipe, or picking a chat.
9
10 the SHEET a "thing" (Web, Doc, Message, Compose, Tools)
11 raised over the chat at three detents. `half`
12 shows the thing and the tail of the conversation
13 together — the daimon beside the thing. An ask
14 pill at its foot forwards to the one composer, so
15 the thing can be talked over without leaving it.
16
17 `daimond.js` routes here: a stage guest reaching mshow() opens
18 the sheet; the rail opens the drawer. This file exposes
19 window.DaimondSheet (open/close/onEngineHide) and
20 window.DaimondShell (openDrawer/closeDrawer).
21 ============================================================ */
22(function () {
23 'use strict';
24
25 var mq = window.matchMedia('(max-width: 760px)');
26 function isPhone() { return mq.matches; }
27
28 // ── The drawer ─────────────────────────────────────────────
29 function openDrawer() { document.body.classList.add('drawer-open'); }
30 function closeDrawer() { document.body.classList.remove('drawer-open'); }
31 function toggleDrawer() { document.body.classList.toggle('drawer-open'); }
32
33 // ── The footer: the chip row ───────────────────────────────
34 //
35 // The bar carried four hard-wired destinations -- Chat, Email, Files, Agents
36 // -- and named four of the seventeen panels there are. It carries the chip
37 // row now, which is the row the desktop header carries, MOVED here rather
38 // than copied: `#panel-tags` is the one row the layout engine renders, and
39 // the one the gallery asks whether a panel is on. A second copy would be a
40 // second thing to keep in step with both, and the first to drift.
41
42 /// Put the chip row where this width wants it: the footer on a phone, the
43 /// top bar otherwise.
44 function placeChips() {
45 var row = document.getElementById('panel-tags');
46 var bar = document.getElementById('mnav');
47 var acts = document.querySelector('.top-actions');
48 if (!row || !bar || !acts) return;
49 var want = isPhone() ? bar : acts;
50 if (row.parentNode === want) return;
51 // FIRST in the top bar, which is where the markup has it: the icon buttons
52 // after it are what the row's left edge is measured against.
53 if (want === acts) acts.insertBefore(row, acts.firstChild);
54 else bar.appendChild(row);
55 bindScroll(row);
56 if (window.DaimondPanels) DaimondPanels.reflow();
57 markHere();
58 }
59
60 /// Which panel the user is actually looking at, or '' if that is the chat
61 /// floor with nothing over it.
62 ///
63 /// Three surfaces can be the answer and the phone shows one of them at a
64 /// time: the drawer (the rail), the sheet (a guest), and the destination on
65 /// the floor. Exported because the chip row's renderer has to fill in the
66 /// same chip this marks, and two answers to "where am I" would show as two
67 /// chips filled at once.
68 function here() {
69 if (document.body.classList.contains('drawer-open')) return 'rail';
70 if (guest) return guest;
71 return document.body.dataset.mpanel || '';
72 }
73
74 /// Fill in the chip for wherever we are, and scroll it into view.
75 ///
76 /// On a desktop a filled chip means "this panel is open", which is legible
77 /// because you can see the panel. On a phone one thing is on screen, so the
78 /// only useful meaning is "this is it" -- an `open` Email panel behind a
79 /// Terminal sheet is not where the user is.
80 var _here = null;
81 function markHere() {
82 if (!isPhone()) return;
83 var row = document.getElementById('panel-tags');
84 if (!row) return;
85 var id = here(), on = null;
86 row.querySelectorAll('.ptag[data-panel]').forEach(function (c) {
87 var is = c.dataset.panel === id;
88 c.classList.toggle('on', is);
89 c.setAttribute('aria-pressed', is ? 'true' : 'false');
90 if (is) on = c;
91 });
92 // SCROLLED TO ONLY WHEN IT CHANGES. This runs on every attribute change to
93 // `body` -- and `class` alone carries `resizing`, `sheet-open` and
94 // `drawer-open` -- so a scroll on every call would drag the strip back to
95 // wherever the user already is each time anything at all happened, while
96 // their thumb was on it looking for something else.
97 if (on && id !== _here && on.scrollIntoView) {
98 try { on.scrollIntoView({ inline: 'nearest', block: 'nearest' }); } catch (e) { /* old engine */ }
99 }
100 _here = id;
101 markScroll(row);
102 }
103
104 /// Say which way there is more of the row, so the fade at its ends is true.
105 function markScroll(row) {
106 var more = row.scrollWidth - row.clientWidth;
107 if (more <= 1) { row.removeAttribute('data-more'); return; }
108 var atStart = row.scrollLeft <= 1;
109 var atEnd = row.scrollLeft >= more - 1;
110 row.dataset.more = atStart ? 'end' : (atEnd ? 'start' : 'both');
111 }
112
113 function bindScroll(row) {
114 if (row._moreBound) return;
115 row._moreBound = true;
116 row.addEventListener('scroll', function () { markScroll(row); }, { passive: true });
117 if (typeof ResizeObserver !== 'undefined') {
118 new ResizeObserver(function () { markScroll(row); }).observe(row);
119 }
120 }
121
122 /// What a footer chip means: take me there.
123 ///
124 /// NOT the desktop row's toggle. A chip there is filled while its panel is
125 /// open and clicking it puts the panel away, which reads correctly because
126 /// you can see both states at once. Here the thing it would put away is the
127 /// whole screen -- and for the rail it is worse than that: `hide('rail')`
128 /// sets `display: none` on the element the drawer IS, and the hamburger only
129 /// toggles a class, so the drawer would never open again. See the capture
130 /// handler further down, which exists for the same trap on the rail's own
131 /// closer.
132 function goTo(id) {
133 if (guest && guest !== id) close(); // one thing up at a time
134 if (window.DaimondPanels) DaimondPanels.show(id);
135 markHere();
136 }
137
138 // ── The sheet ──────────────────────────────────────────────
139 // Guests that default to full (a thing you mostly read or write)
140 // versus half (a thing you glance at while talking to the daimon).
141 // A thing you mostly read or write, so it opens at full height rather than
142 // half. The terminal belongs here for the plainest reason: at the half detent
143 // it is eleven rows, and eleven rows is not a terminal.
144 var DEFAULT_FULL = { doc: 1, preview: 1, compose: 1, tools: 1, term: 1 };
145 // Guests with nothing to "ask about" hide the ask pill. The trash is one:
146 // asking the daimon about a list of things you have deleted would offer to
147 // spend money on the one surface whose whole subject is undoing a mistake.
148 // Social is another, for the opposite reason: it is ALREADY a box you write
149 // in, and a second box under it that sends what you write to a model is two
150 // boxes with opposite meanings.
151 var NO_ASK = { compose: 1, tools: 1, trash: 1, social: 1 };
152
153 var sheetEl, bodyEl, grabEl, titleEl, askWrap, askInput, askSend;
154 var guest = null; // the panel id currently in the sheet, or null
155 var detent = 'half'; // full | half | peek
156 var closing = false; // re-entrancy guard against DaimondPanels.hide
157 // What had the keyboard when the sheet went up, so the sheet can give it
158 // back. Without this a dismissal dropped focus on the document body and the
159 // next Tab started again from the top of the app.
160 var opener = null;
161
162 function t(k, v) { return window.DaimondI18n ? DaimondI18n.t(k, v) : k; }
163
164 function label(id) {
165 var el = document.getElementById('panel-' + id);
166 return (el && el.getAttribute('data-label')) || id;
167 }
168
169 // The top bar (~50px) and the bottom bar bound the room the sheet may take.
170 var BAR_FALLBACK = 62, TOPBAR = 50, PEEK = 56;
171
172 /// How tall the bottom bar actually is, home indicator included.
173 ///
174 /// MEASURED, not written down. This was the constant 58 while css/mobile.css
175 /// drew the bar from its own 58 and variables.css declared 54 -- three copies
176 /// of one number, and the sheet's foot is placed from one of them while its
177 /// height is worked out from another. A bar that changes (it did, on
178 /// 2026-08-28, when the four destinations became the chip row) then puts the
179 /// sheet's foot over it, which is what `dev/verify_sweep_mobile.mjs` measures
180 /// at every phone width and with a notch inset posed. The fallback is for a
181 /// call before the bar is laid out, and errs LARGE, which errs towards a
182 /// shorter sheet.
183 function barH() {
184 var b = document.getElementById('mnav');
185 var h = b ? Math.round(b.getBoundingClientRect().height) : 0;
186 return h > 0 ? h : BAR_FALLBACK;
187 }
188
189 /// The most a sheet may grow to: from just under the top bar to just above
190 /// the bottom bar. `full` stops a touch short so a sliver of chat stays.
191 function maxH() { return Math.max(PEEK, window.innerHeight - TOPBAR - barH()); }
192
193 /// The HEIGHT of the sheet at each detent (it is anchored to the bottom, so
194 /// a taller sheet reveals more of the thing and less of the chat).
195 function detentH(name) {
196 if (name === 'peek') return PEEK;
197 if (name === 'full') return maxH();
198 return Math.min(maxH(), Math.round(window.innerHeight * 0.52)); // half
199 }
200
201 function applyH(px) { sheetEl.style.height = px + 'px'; }
202
203 function snapTo(name) {
204 detent = name;
205 sheetEl.classList.remove('dragging');
206 applyH(detentH(name)); // height is instant; only the slide animates
207 }
208
209 /// Hide a guest's own title when the sheet has just said the same thing.
210 ///
211 /// A panel's `.ctitle` is its NAME while it is empty and a live title once it
212 /// holds something — a URL, a filename, a subject — so no CSS selector can
213 /// tell the two apart, and the stylesheet's attempt to name the offenders was
214 /// wrong: it claimed Tools was the only panel that repeated the sheet's label,
215 /// and rendering all nine showed Message over Message, Graph over Graph, and a
216 /// blank 44px band over Doc, whose title is empty and whose closer is hidden
217 /// on a phone. Compose ("New message") and Web (a live URL) are real subtitles
218 /// and are left alone.
219 ///
220 /// The test is what is on the screen, made when the guest is raised, so a
221 /// panel that later gains a real title keeps it. And a head is dropped only
222 /// when it says nothing new AND holds nothing but its own closer: the
223 /// Terminal's head repeats the label and carries Start and Stop, and a
224 /// terminal you cannot start is not a terminal.
225 function hideRedundantHead(el, sheetLabel) {
226 var head = el.querySelector('.chead, .railhead');
227 if (!head) return;
228 var title = head.querySelector('.ctitle');
229 var said = title ? (title.textContent || '').trim() : '';
230 var same = !said || said.toLowerCase() === String(sheetLabel || '').trim().toLowerCase();
231 var ctrls = head.querySelectorAll('button, a[href], input, select, [role="button"]');
232 var kept = 0;
233 for (var i = 0; i < ctrls.length; i++) {
234 // The panel's own closer is what the sheet's own ✕ already is.
235 if (!ctrls[i].classList.contains('panel-close')) kept++;
236 }
237 head.classList.toggle('head-said-twice', same && kept === 0);
238 }
239
240 /// Raise a guest. The element is MOVED into the sheet (the same idiom
241 /// the desktop layout engine uses to reorder panels); the desktop's
242 /// apply() skips reordering on a phone, so it stays put until closed.
243 function open(id) {
244 if (!sheetEl) return;
245 var el = document.getElementById('panel-' + id);
246 if (!el) return;
247 if (guest && guest !== id) stashBack(); // only one thing up at a time
248 // Captured before anything in the sheet takes the keyboard, and only on
249 // the FIRST raise: swapping one guest for another must not make the
250 // outgoing guest the thing focus goes home to.
251 if (!guest) opener = document.activeElement;
252 el.style.display = ''; // clear any inline none left by apply()
253 bodyEl.appendChild(el);
254 guest = id;
255 titleEl.textContent = label(id);
256 hideRedundantHead(el, label(id));
257 document.body.classList.add('sheet-open');
258 sheetEl.classList.add('open');
259 if (NO_ASK[id]) askWrap.classList.add('hidden');
260 else {
261 askWrap.classList.remove('hidden');
262 askInput.placeholder = t('sheet.ask_about', { thing: label(id).toLowerCase() });
263 }
264 // Size to the detent instantly (still slid off-screen), then add `.open`
265 // on the next frame so the transform slides it up into view.
266 snapTo(DEFAULT_FULL[id] ? 'full' : 'half');
267 requestAnimationFrame(function () { sheetEl.classList.add('open'); });
268 setTimeout(function () { sheetEl.classList.add('open'); }, 20); // headless-safe
269 }
270
271 /// Put the guest element back where the desktop engine expects it,
272 /// hidden, so a later resize to desktop reseats it correctly.
273 function stashBack() {
274 var el = document.getElementById('panel-' + guest);
275 var stage = document.getElementById('stage');
276 if (el && stage) { el.style.display = 'none'; stage.appendChild(el); }
277 }
278
279 function teardown() {
280 stashBack();
281 guest = null;
282 document.body.classList.remove('sheet-open');
283 sheetEl.classList.remove('open'); // slides down (transform), then rests
284 applyH(0);
285 // Give the keyboard back to whatever raised the sheet, if it is still on
286 // screen — a panel that redrew underneath may have taken it with it.
287 if (opener && opener.focus && opener.getClientRects && opener.getClientRects().length) {
288 try { opener.focus(); } catch (e) { /* gone with the redraw */ }
289 }
290 opener = null;
291 }
292
293 /// A user dismissal: tear the sheet down AND tell the engine the panel
294 /// is closed, so its state and (on desktop) its header tag stay honest.
295 function close() {
296 if (!guest) return;
297 var id = guest;
298 teardown();
299 closing = true;
300 try { if (window.DaimondPanels) DaimondPanels.hide(id); }
301 finally { closing = false; }
302 }
303
304 /// The engine closed a panel (e.g. its own close button was reached).
305 /// Mirror it in the sheet, unless we are the ones who asked for it.
306 function onEngineHide(id) {
307 if (closing) return;
308 if (guest === id) teardown();
309 }
310
311 // ── Dragging the grabber ───────────────────────────────────
312 // Drag UP grows the sheet (reveal more of the thing); drag DOWN shrinks it,
313 // and past peek it dismisses.
314 //
315 // THE SHEET MOVES FROM THE GRABBER AND FROM NOWHERE ELSE. THE GUEST OWNS
316 // EVERY DRAG INSIDE ITSELF.
317 //
318 // That is a rule and not a description, because a guest can now be a
319 // SCROLLER: the watched live document puts a tall column of typeset pages in
320 // `.tl-scroll` inside this sheet, and a nested scroller under a draggable
321 // surface is where gestures fight. Whichever of the two loses becomes
322 // impossible — a sheet that takes the drag makes the book unreadable, and a
323 // scroller that takes it makes the sheet unmovable — so the line is drawn at
324 // the grabber, which is the one strip of the sheet a guest never occupies.
325 //
326 // Measured at 390x844, with a six-page document in the sheet at `full`:
327 //
328 // drag up from the middle of the pages document +333px, sheet unmoved
329 // drag down near the top, page scrolled document back to +58, sheet unmoved
330 // drag down at the very top nothing moves at all: not the
331 // scroller, not the sheet body, not
332 // the page behind it
333 // drag down on the grabber sheet 736 -> 439, and the reader
334 // stays on page 1 +36pt
335 //
336 // The third line is the one worth keeping: an overscroll cannot escape,
337 // because `responsive.css` pins `html, body { overscroll-behavior: none }`
338 // under 760px and `#msheet-body` is not itself a scroller. So a drag that
339 // runs off the end of the book does nothing, rather than dragging the sheet
340 // or pulling the page down to refresh — and a refresh is the one thing a live
341 // compiler does not survive.
342 //
343 // The obvious "improvement" — letting a drag anywhere in the sheet move it,
344 // which is what several phone sheets do — would take the pages away. Do not.
345 function bindGrab() {
346 var startY = 0, startH = 0, startDetent = 'half', dragging = false;
347 grabEl.addEventListener('pointerdown', function (e) {
348 dragging = true;
349 startY = e.clientY;
350 startH = sheetEl.getBoundingClientRect().height;
351 startDetent = detent;
352 sheetEl.classList.add('dragging');
353 grabEl.setPointerCapture(e.pointerId);
354 });
355 grabEl.addEventListener('pointermove', function (e) {
356 if (!dragging) return;
357 var h = Math.min(maxH(), Math.max(0, startH + (startY - e.clientY)));
358 applyH(h);
359 });
360 grabEl.addEventListener('pointerup', function (e) {
361 if (!dragging) return;
362 dragging = false;
363 try { grabEl.releasePointerCapture(e.pointerId); } catch (x) {}
364 var h = Math.max(0, startH + (startY - e.clientY));
365 // Dragged well below peek: dismiss.
366 if (h < PEEK - 24) { close(); return; }
367 // Otherwise snap to the nearest of full/half/peek by height.
368 var opts = [['full', detentH('full')], ['half', detentH('half')], ['peek', detentH('peek')]];
369 var best = opts[0], bestD = Infinity;
370 opts.forEach(function (o) {
371 var d = Math.abs(o[1] - h);
372 if (d < bestD) { bestD = d; best = o; }
373 });
374 snapTo(best[0]);
375 });
376 // A POINTER STREAM CAN END IN `pointercancel` RATHER THAN `pointerup`, and
377 // on a phone it does: the browser claims a gesture mid-drag and takes the
378 // pointer with it. Nothing listened for that, so the drag never ended —
379 // `dragging` stayed true, `.dragging` stayed on the sheet, and the sheet was
380 // left stuck at whatever height the cancelled drag had reached. Worse, the
381 // `visualViewport` re-fit below skips a sheet that is `.dragging`, so from
382 // then on a keyboard coming up no longer re-fitted it either. Measured: a
383 // cancel at 299px left the sheet at 299px through a viewport change to 600.
384 //
385 // It goes back to the detent the drag STARTED at, not to the nearest one: a
386 // gesture the user did not finish decided nothing.
387 grabEl.addEventListener('pointercancel', function (e) {
388 if (!dragging) return;
389 dragging = false;
390 try { grabEl.releasePointerCapture(e.pointerId); } catch (x) {}
391 snapTo(startDetent);
392 });
393 }
394
395 // ── The ask pill: forward to the one composer ──────────────
396 function ask() {
397 var text = (askInput.value || '').trim();
398 if (!text) return;
399 if (!(window.DaimondCore && DaimondCore.ask)) return;
400 DaimondCore.ask(text);
401 askInput.value = '';
402 askInput.blur();
403 // Park the thing so the answer, which lands on the chat floor
404 // behind the sheet, comes fully into view. The peek bar taps back.
405 snapTo('peek');
406 }
407
408 // ── Init ───────────────────────────────────────────────────
409 function init() {
410 sheetEl = document.getElementById('msheet');
411 bodyEl = document.getElementById('msheet-body');
412 grabEl = document.getElementById('msheet-grab');
413 titleEl = document.getElementById('msheet-title');
414 askWrap = document.getElementById('msheet-ask');
415 askInput = document.getElementById('msheet-ask-input');
416 askSend = document.getElementById('msheet-ask-send');
417 if (!sheetEl) return;
418
419 bindGrab();
420 document.getElementById('msheet-close').addEventListener('click', close);
421 askSend.addEventListener('click', ask);
422 askInput.addEventListener('keydown', function (e) {
423 if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); ask(); }
424 });
425
426 // The chip row belongs to whichever bar this width uses.
427 placeChips();
428
429 // WHERE WE ARE IS WATCHED, NOT REPORTED. Six or more paths change it --
430 // `mshow`, the sheet opening and closing, the drawer, a panel closing
431 // itself, a saved layout being restored -- and every one of them ends in
432 // one of these two attributes. Watching them covers the paths nobody
433 // remembered to call from, which is exactly how the old bar's four buttons
434 // came to be marked in two places and not in the others.
435 var watch = new MutationObserver(markHere);
436 watch.observe(document.body, { attributes: true, attributeFilter: ['data-mpanel', 'class'] });
437
438 // The hamburger and the scrim.
439 var burger = document.getElementById('drawer-btn');
440 if (burger) burger.addEventListener('click', toggleDrawer);
441 var scrim = document.getElementById('scrim');
442 if (scrim) scrim.addEventListener('click', closeDrawer);
443
444 // Picking a chat or a Diamond is "go work on this" — the drawer's job
445 // is done, so it steps out of the way.
446 var rail = document.getElementById('panel-rail');
447 if (rail) rail.addEventListener('click', function (e) {
448 if (e.target.closest('.session-box, .diamond-box')) closeDrawer();
449 });
450
451 // The rail's own closer, which responsive.css leaves on screen here alone
452 // (see the note beside `.panel.rail .panel-close`). On a desktop it hides
453 // the rail panel; on a phone the rail IS the drawer, so it closes the
454 // drawer instead — a cross closes the ONE thing it sits on, and here that
455 // thing is the drawer.
456 //
457 // Caught in the CAPTURE phase and stopped, because the panel engine binds
458 // every `[data-close]` on the bubble: left to reach it, `hide('rail')`
459 // would set `display: none` on the drawer, and the hamburger — which only
460 // toggles `body.drawer-open` — would then open nothing at all.
461 if (rail) rail.addEventListener('click', function (e) {
462 var b = e.target.closest && e.target.closest('.panel-close');
463 if (!b || !isPhone()) return;
464 e.stopPropagation();
465 e.preventDefault();
466 closeDrawer();
467 var burger2 = document.getElementById('drawer-btn');
468 // The keyboard goes back to what opened the drawer.
469 if (burger2) { try { burger2.focus(); } catch (x) { /* not on screen */ } }
470 }, true);
471
472 // A left-edge swipe opens the drawer; a swipe on the open drawer's
473 // scrim is caught by the scrim tap. Gesture, always paired with the
474 // visible hamburger — never gesture-only.
475 bindEdgeSwipe();
476
477 // Escape, which neither of these two answered. They are the only surfaces
478 // in the app that cover it and had no key out at all — a phone shell run
479 // on a tablet with a keyboard, or on a desktop narrowed past 760px, left
480 // the keyboard with nothing to press.
481 //
482 // The innermost thing goes first, and a keystroke a dialog has already
483 // dealt with is left alone: the app's dialogs call `preventDefault` on the
484 // Escape they consume, so `defaultPrevented` is how one closed OVER the
485 // sheet avoids taking the sheet down with it.
486 document.addEventListener('keydown', function (e) {
487 if (e.key !== 'Escape' || e.defaultPrevented) return;
488 if (guest) { close(); return; }
489 if (document.body.classList.contains('drawer-open')) {
490 closeDrawer();
491 var burger3 = document.getElementById('drawer-btn');
492 if (burger3) { try { burger3.focus(); } catch (x) { /* not on screen */ } }
493 }
494 });
495
496 // Keep the sheet honest across a keyboard show/hide and rotation.
497 if (window.visualViewport) {
498 window.visualViewport.addEventListener('resize', function () {
499 if (guest && !sheetEl.classList.contains('dragging')) snapTo(detent);
500 });
501 }
502
503 // Crossing the phone boundary: fold the phone surfaces away when we
504 // grow to desktop, and let the engine reseat everything. Driven off
505 // `resize` (not only the media-query `change`, which some engines fire
506 // unreliably under a scripted viewport) so the desktop restore is sure.
507 if (mq.addEventListener) mq.addEventListener('change', scheduleMode);
508 else if (mq.addListener) mq.addListener(scheduleMode);
509 window.addEventListener('resize', scheduleMode);
510 }
511
512 // Debounced, and gated on the live width rather than the media query — some
513 // engines flip matchMedia a beat after the resize event, and the reseat must
514 // not miss that beat and leave a guest stranded in the sheet.
515 var modeTimer = null;
516 function scheduleMode() {
517 if (modeTimer) clearTimeout(modeTimer);
518 modeTimer = setTimeout(onMode, 60);
519 }
520 function onMode() {
521 // Either way the chip row has to be in the bar this width uses, so this
522 // comes before the desktop-only restore below it.
523 placeChips();
524 if (window.innerWidth <= 760) return;
525 closeDrawer();
526 if (guest) teardown();
527 if (window.DaimondPanels) DaimondPanels.reflow();
528 }
529
530 /// A drag that begins within 24px of the left edge opens the drawer.
531 function bindEdgeSwipe() {
532 var x0 = 0, y0 = 0, live = false;
533 document.addEventListener('touchstart', function (e) {
534 if (!isPhone() || document.body.classList.contains('drawer-open')) return;
535 var t = e.touches[0];
536 if (t.clientX <= 24) { live = true; x0 = t.clientX; y0 = t.clientY; }
537 }, { passive: true });
538 document.addEventListener('touchmove', function (e) {
539 if (!live) return;
540 var t = e.touches[0];
541 if (t.clientX - x0 > 46 && Math.abs(t.clientY - y0) < 40) { openDrawer(); live = false; }
542 }, { passive: true });
543 document.addEventListener('touchend', function () { live = false; }, { passive: true });
544 }
545
546 window.DaimondSheet = {
547 open: open, close: close, onEngineHide: onEngineHide,
548 isOpen: function () { return !!guest; },
549 guest: function () { return guest; },
550 };
551 window.DaimondShell = {
552 openDrawer: openDrawer, closeDrawer: closeDrawer, toggleDrawer: toggleDrawer,
553 /// What a footer chip does, and where the chip row's renderer asks which
554 /// chip to fill in. See `goTo` and `here`.
555 goTo: goTo, here: here, markHere: markHere,
556 isPhone: isPhone,
557 };
558
559 if (document.readyState === 'loading') {
560 document.addEventListener('DOMContentLoaded', init);
561 } else {
562 init();
563 }
564})();