dsh-mobile 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,20 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ // src/mobile-layout-attributes.ts
9
+ /**
10
+ * Root marker the dedicated layout publishes for its wide/narrow band.
11
+ *
12
+ * The band depends on the device screen size, which a media query cannot read,
13
+ * so the layout decides it in JS and writes this attribute on `.dshm-shell`:
14
+ * `"1"` docks the sidebar, anything else — `"0"`, or no marker — is the overlay
15
+ * drawer. The stylesheet and the sidebar scroll adapter both follow the marker,
16
+ * so the band has exactly one owner.
17
+ *
18
+ * This module stays free of imports: the dedicated layout bundle and the native
19
+ * client bundle both use it, and neither should pull the other in.
20
+ */
21
+ const WIDE_LAYOUT_ATTRIBUTE = "data-dshm-wide";
8
22
  // src/mobile-layout-messages.ts
9
23
  const MOBILE_LAYOUT_MESSAGES = Object.freeze({
10
24
  it: Object.freeze({
@@ -36,24 +50,62 @@ window.__ModuleLoader__.load({
36
50
  * keep the overlay behavior byte-for-byte.
37
51
  */
38
52
  const WIDE_LAYOUT_MIN_WIDTH_PX = 900;
53
+ /**
54
+ * Shorter-side floor for the persistent sidebar, measured on the *device*: a
55
+ * landscape phone is wider than the width threshold while its screen's short
56
+ * side is still a phone's. Docking a 932x430 phone hid the conversation behind a
57
+ * permanent rail and removed the panel return bar, so width alone is not enough
58
+ * evidence of a tablet-sized viewport.
59
+ *
60
+ * The measurement must come from `screen`, not the viewport: a Pad 9 Pro Max in
61
+ * landscape (1363x860 viewport, 1364x909 screen) reports `innerHeight: 441`
62
+ * while the soft keyboard is open, and a viewport-height floor would flip a
63
+ * docked tablet into the drawer layout mid-sentence.
64
+ */
65
+ const WIDE_LAYOUT_MIN_SHORTER_SIDE_PX = 600;
66
+ /**
67
+ * Narrow band that still has room for a folded 56px rail beside the
68
+ * conversation: the pre-existing 721-899px phone rule plus any narrow viewport
69
+ * that is wide enough for the rail, such as a landscape phone.
70
+ */
71
+ const FOLDED_RAIL_OFFSET_QUERY = "(min-width:721px)";
39
72
  const RIGHTBAR_MIN_WIDTH_PX = 300;
40
73
  const RIGHTBAR_MAX_WIDTH_PX = 460;
41
74
  const RIGHTBAR_CENTER_MIN_WIDTH_PX = 400;
42
75
  const RIGHTBAR_DEFAULT_RATIO = .45;
43
76
  const SIDEBAR_EXPANDED_WIDTH_PX = 340;
44
77
  const SIDEBAR_COLLAPSED_WIDTH_PX = 56;
45
- function isWideViewportLayout(viewportWidth) {
46
- return viewportWidth >= 900;
78
+ /**
79
+ * The device's shorter side in CSS pixels, independent of rotation and of the
80
+ * soft keyboard. Falls back to the viewport only where `screen` reports nothing
81
+ * usable (SSR, minimal DOM stubs).
82
+ * @param screenWidth - `screen.width`, or `undefined` without a screen.
83
+ * @param screenHeight - `screen.height`, or `undefined` without a screen.
84
+ * @param viewportWidth - the viewport width, used for the fallback.
85
+ * @param viewportHeight - the viewport height, used for the fallback.
86
+ * @returns the shorter side in CSS pixels.
87
+ */
88
+ function resolveDeviceShortSide(screenWidth, screenHeight, viewportWidth, viewportHeight) {
89
+ return typeof screenWidth === "number" && typeof screenHeight === "number" && screenWidth > 0 && screenHeight > 0 ? Math.min(screenWidth, screenHeight) : Math.min(viewportWidth, viewportHeight);
90
+ }
91
+ /**
92
+ * Whether the dedicated layout should dock the sidebar as a persistent column.
93
+ * @param viewportWidth - the viewport width in CSS pixels; a narrow window is
94
+ * never wide, whatever the screen behind it measures.
95
+ * @param deviceShortSide - the device's shorter side, from {@link resolveDeviceShortSide}.
96
+ * @returns whether the viewport and the device are both large enough.
97
+ */
98
+ function isWideViewportLayout(viewportWidth, deviceShortSide) {
99
+ return viewportWidth >= 900 && deviceShortSide >= 600;
47
100
  }
48
101
  /** Preserve a readable conversation width before turning a requested right track into a docked column. */
49
- function resolveMobileRightbarLayout(viewportWidth, sidebarOpen, track, fullscreen) {
50
- const overlayWidth = isWideViewportLayout(viewportWidth) ? Math.min(viewportWidth * .94, RIGHTBAR_MAX_WIDTH_PX) : viewportWidth;
51
- if (!track || fullscreen || !isWideViewportLayout(viewportWidth) && sidebarOpen) return Object.freeze({
102
+ function resolveMobileRightbarLayout(viewportWidth, wideViewport, sidebarOpen, track, fullscreen) {
103
+ const overlayWidth = wideViewport ? Math.min(viewportWidth * .94, RIGHTBAR_MAX_WIDTH_PX) : viewportWidth;
104
+ if (!track || fullscreen || !wideViewport && sidebarOpen) return Object.freeze({
52
105
  docked: false,
53
106
  width: overlayWidth
54
107
  });
55
- const sidebarWidth = isWideViewportLayout(viewportWidth) && sidebarOpen ? SIDEBAR_EXPANDED_WIDTH_PX : SIDEBAR_COLLAPSED_WIDTH_PX;
56
- const available = Math.floor(viewportWidth - sidebarWidth - RIGHTBAR_CENTER_MIN_WIDTH_PX);
108
+ const available = Math.floor(viewportWidth - (wideViewport && sidebarOpen ? SIDEBAR_EXPANDED_WIDTH_PX : SIDEBAR_COLLAPSED_WIDTH_PX) - RIGHTBAR_CENTER_MIN_WIDTH_PX);
57
109
  if (available < RIGHTBAR_MIN_WIDTH_PX) return Object.freeze({
58
110
  docked: false,
59
111
  width: overlayWidth
@@ -249,10 +301,26 @@ window.__ModuleLoader__.load({
249
301
  if (isSidebarRightControl(sidebarRight) && sidebarRight.isExpanded()) sidebarRight.toggleExpanded();
250
302
  closeDetails();
251
303
  }
252
- /** Reads the live viewport; unknown environments (SSR, tests) stay narrow. */
304
+ /** The device and viewport facts the layout decision needs, read live. */
305
+ function readViewport() {
306
+ const width = window.innerWidth;
307
+ const height = window.innerHeight;
308
+ const screen = window.screen;
309
+ return {
310
+ width,
311
+ height,
312
+ shortSide: resolveDeviceShortSide(screen?.width, screen?.height, width, height)
313
+ };
314
+ }
315
+ /**
316
+ * Reads the live device and viewport; unknown environments (SSR, tests) stay
317
+ * narrow. `screen` is what distinguishes a tablet from a landscape phone and
318
+ * what keeps a docked tablet docked while its soft keyboard is open.
319
+ */
253
320
  function viewportIsWide() {
254
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
255
- return window.matchMedia(`(min-width: 900px)`).matches;
321
+ if (typeof window === "undefined") return false;
322
+ const viewport = readViewport();
323
+ return isWideViewportLayout(viewport.width, viewport.shortSide);
256
324
  }
257
325
  /** Resolve the supported language used by the dedicated mobile layout. */
258
326
  function resolveMobileLayoutLanguage(documentLanguage, browserLanguages) {
@@ -407,10 +475,12 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
407
475
  shell descendants below positive-z-index portals attached to document.body. */
408
476
  .dshm-shell{-webkit-text-size-adjust:100%;text-size-adjust:100%;position:relative;isolation:isolate;display:grid;grid-template-rows:minmax(0,1fr);width:100%;height:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}
409
477
  /* The mobile root owns its Grid geometry. Layered important rules keep broad
410
- unlayered skin rules from collapsing virtualized views to the header height. */
478
+ unlayered skin rules from collapsing virtualized views to the header height.
479
+ The wide/narrow band is the root attribute, not a media query: the decision
480
+ needs the device screen size, which a media query cannot read. */
411
481
  @layer dsh-mobile-layout{
412
482
  .dshm-shell{display:grid!important;grid-template-rows:minmax(0,1fr)!important;grid-template-columns:minmax(0,1fr)!important}
413
- @media(min-width:900px){.dshm-shell{grid-template-columns:auto minmax(0,1fr)!important}}
483
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"]{grid-template-columns:auto minmax(0,1fr)!important}
414
484
  }
415
485
  .dshm-main{grid-area:1/1;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;margin-right:0;overflow:hidden;transition:margin-right var(--ds-transition-duration-slow,190ms) var(--ds-ease-in-out,ease)}
416
486
  .dshm-mainContent{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}
@@ -432,14 +502,20 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
432
502
  .dshm-drawer[data-open=true]>*{transform:translateX(0);opacity:1}
433
503
  .dshm-drawer[data-open=false]>*{width:56px!important}
434
504
  /* Folded icon rail: on narrow viewports the drawer is fixed, so give the main
435
- content the same 56px offset the wide grid breakpoint already provides. */
436
- @media(min-width:721px) and (max-width:899px){.dshm-shell:has(.dshm-drawer[data-sidebar-collapsed]:not([data-right-modal=true])) .dshm-main{margin-left:56px}}
505
+ content the same 56px offset the wide grid already provides. The width floor
506
+ keeps phone-width headers on their own inset; the band itself is the root
507
+ attribute, so a landscape phone gets the offset too. The drawer's folded state
508
+ owns this offset independently of nested-pane markers. */
509
+ @media${FOLDED_RAIL_OFFSET_QUERY}{.dshm-shell:not([${WIDE_LAYOUT_ATTRIBUTE}="1"]):has(.dshm-drawer[data-open=false]:not([data-right-modal=true])) .dshm-main{margin-left:56px}}
437
510
  /* display:none also suppresses fixed descendants installed by the phone adapter. */
438
511
  .dshm-drawer[data-right-modal=true]{display:none!important}
439
512
  .dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-1,#fff),var(--dsw-alias-bg-layer-1,#fff));box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}
440
513
  .dshm-details[data-open=true]{transform:translateX(0)}
441
514
  .dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}
442
- @media(max-width:899px){.dshm-details{width:100%;max-width:100%;box-shadow:none}.dshm-details [data-sidebar-right-toggle],.dshm-details [data-sidebar-right-mode]{min-width:48px;min-height:48px}}
515
+ /* An absent marker is the narrow default: static fixtures and the first paint
516
+ before React mounts keep the overlay geometry rather than a half-docked shell. */
517
+ .dshm-shell:not([${WIDE_LAYOUT_ATTRIBUTE}="1"]) .dshm-details{width:100%;max-width:100%;box-shadow:none}
518
+ .dshm-shell:not([${WIDE_LAYOUT_ATTRIBUTE}="1"]) .dshm-details [data-sidebar-right-toggle],.dshm-shell:not([${WIDE_LAYOUT_ATTRIBUTE}="1"]) .dshm-details [data-sidebar-right-mode]{min-width:48px;min-height:48px}
443
519
  .dshm-scrim{position:fixed;z-index:65;inset:0;border:0;background:rgb(15 23 42 / 40%);opacity:0;pointer-events:none;transition:opacity 180ms ease-out}
444
520
  .dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}
445
521
  .dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}
@@ -451,7 +527,9 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
451
527
  .dshm-panelBack:disabled{opacity:.5;cursor:default}
452
528
  .dshm-panelBack:focus-visible{outline:2px solid var(--dsw-alias-label-primary,#171a21);outline-offset:2px}
453
529
  .dshm-panelBack svg{width:20px;height:20px;flex-shrink:0}
454
- .dshm-shell header{min-width:0;padding-left:52px}
530
+ .dshm-shell header{min-width:0}
531
+ /* A panel owns its header padding; only conversation content clears the rail. */
532
+ .dshm-shell :where(.dshm-main:not([data-dsh-panel="1"])) header{padding-left:52px}
455
533
  /* Keep focused editables above Safari's 16px zoom threshold while retaining
456
534
  the DSH content-font preference and larger inherited text. Stock menus
457
535
  portal into document.body, so the floor scopes from the body itself. */
@@ -480,30 +558,33 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
480
558
  .dshm-shell [data-plan-review-key]>section>div:last-child>div:last-child>button:first-child{grid-column:1/-1}
481
559
  .dshm-shell [data-plan-review-key]>section>div:last-child button{width:100%;min-height:44px}
482
560
  }
483
- @media(min-width:900px){
484
- .dshm-shell{grid-template-columns:auto minmax(0,1fr)}
561
+ /* Wide band: the shell carries the decision, so these rules follow the same
562
+ device+viewport test as the JS. An absent attribute is the narrow default. */
563
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"]{grid-template-columns:auto minmax(0,1fr)}
485
564
  /* The wide drawer is a static grid column, not an overlay: it occupies its own
486
565
  track and never overlaps the conversation. Keeping a z-index here made it
487
566
  outrank the conversation column, so a dialog a plugin mounts inside the
488
567
  composer's own stacking context lost its left edge behind the opaque rail.
489
568
  A static column needs no layer, so it stays in normal flow order. */
490
- .dshm-drawer{position:static;grid-area:1/1;box-shadow:none;z-index:auto}
491
- .dshm-drawer[data-open=true]{width:340px;box-shadow:none}
492
- .dshm-drawer[data-open=false]{width:56px}
569
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"] .dshm-drawer{position:static;grid-area:1/1;box-shadow:none;z-index:auto}
570
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"] .dshm-drawer[data-open=true]{width:340px;box-shadow:none}
571
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"] .dshm-drawer[data-open=false]{width:56px}
493
572
  /* The conversation column outranks that column so an in-shell modal opened from
494
573
  it is not clipped by the navigation rail. It stays below the scrim (65), the
495
574
  detail panel (80) and the shell overlay (90), so a modal shell surface still
496
575
  dims and covers it. Only the wide grid needs this: on a phone the drawer is a
497
576
  floating overlay and must keep covering the conversation. */
498
- .dshm-main{grid-area:1/2;z-index:1}
499
- }
577
+ .dshm-shell[${WIDE_LAYOUT_ATTRIBUTE}="1"] .dshm-main{grid-area:1/2;z-index:1}
500
578
  @media(prefers-reduced-motion:reduce){.dshm-drawer,.dshm-details,.dshm-scrim,.dshm-drawer>*,.dshm-main{transition:none!important}}
501
579
  /* Touch adaptation changes control boxes, not invisible hit regions. Root opt-out
502
- retains pane geometry too; nested opt-out preserves a widget's own controls. */
580
+ retains pane geometry too; nested opt-out preserves a widget's own controls.
581
+ DSH's plugin cards open through a cardTitle/cardOpen button whose ::after
582
+ overlay already owns the whole card hit area; the floor only inflated that
583
+ title box and pushed each description away from its card title. */
503
584
  @media(pointer:coarse){
504
585
  .dshm-main[data-dsh-panel="1"] .dshm-mainContent:has(>[data-slot="main"]>:not([data-dsh-adapt="off"])):not(:has(>[data-slot="main"]>[data-dsh-adapt="off"])){overflow-y:auto;overscroll-behavior:contain}
505
586
  .dshm-main[data-dsh-panel="1"] .dshm-mainContent>[data-slot="main"]>:not([data-dsh-adapt="off"]){max-width:100%;box-sizing:border-box}
506
- .dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],select,input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]):not([type=color]):not([type=file]):not([type=image])):not([role="switch"],[role="checkbox"],[role="radio"],[role="slider"],[data-dsh-adapt="off"],[data-dsh-adapt="off"] *){min-width:44px!important;min-height:44px!important}
587
+ .dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],select,input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]):not([type=color]):not([type=file]):not([type=image])):not([role="switch"],[role="checkbox"],[role="radio"],[role="slider"],button[class*="_cardTitle"][class*="_cardOpen"],[data-dsh-adapt="off"],[data-dsh-adapt="off"] *){min-width:44px!important;min-height:44px!important}
507
588
  .dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],a):not([data-dsh-adapt="off"],[data-dsh-adapt="off"] *){touch-action:manipulation}
508
589
  }
509
590
  `;
@@ -530,14 +611,14 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
530
611
  const active = document.activeElement;
531
612
  if (active instanceof HTMLElement && active.matches(COMPOSER_EDITOR_SELECTOR)) active.blur();
532
613
  }, [state.panelInfo.activePanelId]);
533
- const [viewportWidth, setViewportWidth] = (0, react.useState)(window.innerWidth);
614
+ const [viewport, setViewport] = (0, react.useState)(readViewport);
534
615
  (0, react.useEffect)(() => {
535
616
  let frame;
536
617
  const resize = () => {
537
618
  if (frame !== void 0) return;
538
619
  frame = window.requestAnimationFrame(() => {
539
620
  frame = void 0;
540
- setViewportWidth(window.innerWidth);
621
+ setViewport(readViewport());
541
622
  });
542
623
  };
543
624
  window.addEventListener("resize", resize);
@@ -546,8 +627,9 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
546
627
  if (frame !== void 0) window.cancelAnimationFrame(frame);
547
628
  };
548
629
  }, []);
549
- const wideViewport = isWideViewportLayout(viewportWidth);
550
- const rightbar = resolveMobileRightbarLayout(viewportWidth, state.sidebarOpen, state.rightbarTrack, state.rightbarFullscreen);
630
+ const viewportWidth = viewport.width;
631
+ const wideViewport = isWideViewportLayout(viewport.width, viewport.shortSide);
632
+ const rightbar = resolveMobileRightbarLayout(viewport.width, wideViewport, state.sidebarOpen, state.rightbarTrack, state.rightbarFullscreen);
551
633
  const rightbarDocked = state.detailsOpen && rightbar.docked;
552
634
  const rightbarFullscreen = state.detailsOpen && state.rightbarFullscreen;
553
635
  const rightbarFullWidth = state.detailsOpen && !rightbarDocked && (!wideViewport || rightbarFullscreen);
@@ -664,6 +746,7 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
664
746
  return (0, react.createElement)("div", {
665
747
  className: "dshm-shell",
666
748
  lang: language,
749
+ [WIDE_LAYOUT_ATTRIBUTE]: wideViewport ? "1" : "0",
667
750
  "data-rightbar-docked": rightbarDocked,
668
751
  "data-rightbar-fullscreen": rightbarFullscreen,
669
752
  style: { "--dshm-rightbar-width": `${rightbar.width}px` }
@@ -836,6 +919,8 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
836
919
  exports.MOBILE_LAYOUT_MESSAGES = MOBILE_LAYOUT_MESSAGES;
837
920
  exports.MOBILE_LAYOUT_STYLES = MOBILE_LAYOUT_STYLES;
838
921
  exports.TOUCH_PRIMARY_QUERY = TOUCH_PRIMARY_QUERY;
922
+ exports.WIDE_LAYOUT_ATTRIBUTE = WIDE_LAYOUT_ATTRIBUTE;
923
+ exports.WIDE_LAYOUT_MIN_SHORTER_SIDE_PX = WIDE_LAYOUT_MIN_SHORTER_SIDE_PX;
839
924
  exports.WIDE_LAYOUT_MIN_WIDTH_PX = WIDE_LAYOUT_MIN_WIDTH_PX;
840
925
  exports.apply = apply;
841
926
  exports.closeDetailsFromScrim = closeDetailsFromScrim;
@@ -848,6 +933,7 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
848
933
  exports.isSidebarRightControl = isSidebarRightControl;
849
934
  exports.isWideViewportLayout = isWideViewportLayout;
850
935
  exports.resolveComposerImePolicy = resolveComposerImePolicy;
936
+ exports.resolveDeviceShortSide = resolveDeviceShortSide;
851
937
  exports.resolveMobileLayoutLanguage = resolveMobileLayoutLanguage;
852
938
  exports.resolveMobileRightbarLayout = resolveMobileRightbarLayout;
853
939
  return module.exports;