/** * Mobile stylesheet for the DSH web shell. * * Hooks are the stable framework attributes only — no hashed classes: * - `[data-mobile-nav="frame"]` our marker on the AppFrame element (value-scoped so * it never matches the plugin's own controls) * - `[data-sidebar-collapsed]` AppFrame: sidebar is in the compact rail state * - `[data-side="sidebar"|"details"]` AppFrame drag handles * - `[data-shell-overlay]` AppFrame overlay layer (used to locate the frame) * - `[data-phase]` conversation root phase (hero|active|settling) * * Below the official auto-collapse breakpoint (1024px) the rail is removed * from the grid entirely; the sidebar column becomes an overlay drawer that * slides in when the frame leaves the collapsed state (narrowExpanded). */ export const MOBILE_CSS = ` /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */ [data-mobile-nav="toggle"], [data-mobile-nav="files"] { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex: none; padding: 0; border: none; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; -webkit-tap-highlight-color: transparent; } [data-mobile-nav="toggle"]:hover, [data-mobile-nav="files"]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)); } [data-mobile-nav="toggle"]:focus-visible, [data-mobile-nav="files"]:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7); outline-offset: 1px; } /* Drawer footer actions: the relocated Session log download plus the Files action that opens the dsh-web-ui explorer sheet. */ [data-mobile-nav="drawer-actions"] { display: inline-flex; align-items: center; gap: 8px; } /* 宿主没有 aionui explorer 列(官方 DSH 无 dsh-web-ui,issue #48)时隐藏 移动端「文件浏览」入口(header 图标 + drawer footer 项)——不然点了没反应。 */ [data-mobile-nav-explorer="0"] [data-mobile-nav="files"], [data-mobile-nav-explorer="0"] [data-mobile-nav="explorer"] { display: none !important; } [data-mobile-nav="session-log"], [data-mobile-nav="explorer"] { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 34px; padding: 0 12px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)); border-radius: 12px; background: transparent; color: var(--dsw-alias-label-primary, inherit); font-family: inherit; font-size: 13px; line-height: 20px; cursor: pointer; -webkit-tap-highlight-color: transparent; } [data-mobile-nav="session-log"]:hover:not(:disabled), [data-mobile-nav="explorer"]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)); } [data-mobile-nav="session-log"]:disabled { color: var(--dsw-alias-label-dimmed, rgba(0, 0, 0, .35)); cursor: default; } /* Floating fallback button (hero / blank phases without a session header). The top clears the camera band below the status bar; when the client has set viewport-fit=cover the safe-area inset moves it below the notch too. */ [data-mobile-nav="fab"] { position: absolute; top: calc(env(safe-area-inset-top, 0px) + 72px); left: 10px; z-index: 21; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)); border-radius: 50%; background: var(--dsw-alias-button-floating-fill, #ffffff); color: var(--dsw-alias-label-primary, inherit); cursor: pointer; box-shadow: 0 2px 12px rgba(0, 0, 0, .18); -webkit-tap-highlight-color: transparent; } [data-mobile-nav="fab"]:hover { background: var(--dsw-alias-button-floating-hover, rgba(0, 0, 0, .08)); } [data-mobile-nav="fab"]:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7); outline-offset: 2px; } /* Dimmed backdrop under the open drawer; above every column, below the drawer. pointer-events: none —— 点击穿透(issue #38):backdrop 只负责视觉压暗, 不抢点击。关闭抽屉改由 MobileNavOverlay 的 document 级「抽屉外点击」监听处理 (等价于原来的点击遮罩关闭,且抽屉内点击不再被 backdrop 吃掉)。 */ [data-mobile-nav="backdrop"] { position: absolute; inset: 0; z-index: 30; background: rgba(0, 0, 0, .45); pointer-events: none; animation: dsh-mobile-nav-fade .2s var(--ds-ease-in-out, ease-in-out); -webkit-tap-highlight-color: transparent; } @keyframes dsh-mobile-nav-fade { from { opacity: 0; } to { opacity: 1; } } /* Settings sheet entrance: the official dialog mounts with no animation at all, so it snaps in. Fade + slight rise/scale reads as a proper sheet. */ @keyframes dsh-mobile-nav-sheet-in { from { opacity: 0; transform: translateY(14px) scale(.98); } to { opacity: 1; transform: none; } } /* Preview sheet rise: the aionui preview column opens as a bottom sheet. */ @keyframes dsh-mobile-nav-sheet-up { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } } /* ---------- mobile-only layout ---------- */ @media (max-width: 1023px) { /* --- Phone chrome --- The system status bar stays visible (no fullscreen). Two adjustments make it behave: - touch-action: manipulation kills double-tap-to-zoom (and the 300ms tap delay) while keeping pan and pinch zoom; the client also suppresses legacy-iOS gesturestart as a fallback. - With the client's viewport-fit=cover, env(safe-area-inset-top) is the status bar / notch height; the rules below push the app content below it so the status bar never covers anything. Off notched phones (or in a normal browser tab where the layout viewport already sits below the status bar) the inset is 0 and nothing shifts. */ html, body { touch-action: manipulation !important; } /* AppFrame: the drawer takes the sidebar column out of grid flow, so the remaining in-flow items (center, details) land in tracks 1..2: give the center every pixel and keep the details track at zero. The top padding clears the status bar / notch for every in-flow surface (session header, messages, composer); the absolutely-positioned drawer is unaffected (its containing block is the frame's padding box, i.e. still the frame top). */ [data-mobile-nav="frame"] { position: relative !important; grid-template-columns: minmax(0, 1fr) 0 0 !important; padding-top: env(safe-area-inset-top, 0px) !important; } /* 主内容列(第 2 个网格子元素)在官方样式里有显式 grid-column: 2—— 网格被压缩成 [1fr, 0, 0] 后它会落在 0px 的第 2 轨,整个主界面被挤出 视口(只剩背景图)。必须显式把它拉回第 1 轨(issue #5)。 第 3 列(details)保持 0 轨即可,无需处理。 */ [data-mobile-nav="frame"] > :nth-child(2) { grid-column: 1 !important; grid-row: 1 !important; min-width: 0 !important; } /* The sidebar column (first grid child) becomes a left drawer. The drawer hugs the sidebar content exactly (the wide sidebar carries an inline width, ~280px): a fixed 92vw box would leave a white strip where the container background shows beside the content. Closed state: translateX(-110%) — more than -100% of the max-content width — guarantees the whole drawer (and its shadow, had it one) leaves the viewport. A mere -100% leaves a sliver on screen; -105% (as used before) left 14px of the drawer plus a long 32px-blur shadow gradient visible along the left edge of the main UI. No box-shadow at all: the dimmed backdrop already separates drawer from content. Z-index note: the backdrop renders inside the shell's overlay layer ([data-shell-overlay]), which forms its own stacking context. Third-party plugins can force that layer up with !important (dsh-update-checker sets it to 500), and when the layer outranks the drawer, the backdrop paints ABOVE the drawer and swallows every tap — the drawer opens but no row can be pressed (every tap just closes it). The drawer must therefore outrank any such raise. 1200 (was 600) clears the mobile layers shipped by @linxin666/dsh-web-ui-all — its sidebar pane is z-index 1100, its details pane 1000 and its full-screen frame ::after mask 1050 (issue #67: that mask sat on top of the 600 drawer and ate every tap). Still far under the fixed-position banners/toasts (z 9999) that float at the viewport level. */ [data-mobile-nav="frame"] > :first-child { position: absolute !important; inset: 0 auto 0 0 !important; width: max-content !important; max-width: 92vw !important; z-index: 1200 !important; transform: translateX(-110%); transition: transform .28s var(--ds-ease-in-out, ease-in-out); background: var(--dsw-alias-bg-base, #ffffff); /* Keep the drawer's own content below the status bar / notch: the drawer spans the full frame height (its absolute containing block is the frame's padding box, so the frame's own safe-area padding does NOT reach it). The drawer background paints the status-bar strip, which the client's theme-color meta matches, so the strip reads seamless. */ padding-top: env(safe-area-inset-top, 0px) !important; /* Kill the official sidebarCol right border: with the backdrop the edge reads cleanly, and the settings dialog (width:100% of this box) stays pixel-flush with the drawer. */ border-right: none !important; } /* Expanded state (frame without data-sidebar-collapsed) slides the drawer in. The open state must be transform:none — NOT translateX(0): an identity transform still makes the drawer the containing block for fixed-position descendants (the settings dialog's .VOzbGW_overlay is portaled into the sidebar DOM). With the identity transform the wide settings sheet (100vw-16) overflows the 280px drawer, the dialog's focus scrolls the overflow:hidden drawer to scrollLeft=102, and every static child (plus the fixed overlay) shifts 102px off-screen. With transform:none the overlay is viewport-anchored: it dims the full screen and the sheet sits at left:8. */ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child { transform: none !important; } /* Kill a competing full-screen mask (issue #67). @linxin666/dsh-web-ui-all ships its own mobile drawer, and part of it is [data-dsh-frame]:not([data-sidebar-collapsed])::after { content: ""; position: fixed; inset: 0; z-index: 1050; background: rgb(0 0 0 / 24%); } The pseudo-element belongs to the frame we already mark, and the frame carries only "position: relative" with z-index auto — no stacking context — so this mask competes with the drawer in the parent stacking context and, at 1050, paints over it. It covers the whole viewport, so every tap on a session row lands on the mask instead: the drawer opens but nothing inside it can be pressed, and the page behind cannot be scrolled. Removing it is safe: the mobile stylesheet already renders its own backdrop, and tapping outside the drawer is handled in JS. The attribute selector is repeated on purpose. Their rule has the same specificity (0,2,1) once ours is written the obvious way, and plugin stylesheets are injected in load order, so a tie would be decided by whichever plugin happened to load last. Doubling the attribute makes it (0,3,1) and deterministic. */ [data-mobile-nav="frame"][data-mobile-nav="frame"]:not([data-sidebar-collapsed])::after { content: none !important; } /* Drag handles are useless on touch and would float over the drawer. */ [data-side="sidebar"], [data-side="details"] { display: none !important; } /* --- Conversation text on mobile --- The official message flow keeps desktop's 32px side gutters and 16px type. On a phone: shrink the type a notch and widen the lines by trimming the gutters (the sidebar drawer list keeps its size). The flow's scroll container is the only _scroll element holding markdown

paragraphs — the composer's own scroll (textarea) is excluded via :has(p). */ /* The official main scroll body reserves scrollbar-gutter for desktop scrollbars (8px), which shoves every column off-center on a phone. Classic desktop scrollbars (Edge/Chrome) also occupy ~8-17px in a phone-sized viewport, shifting the column further. Mobile scrolling is touch/wheel, so remove the scrollbar entirely on phones: the column is then exactly centered in every browser. */ [data-phase] [class$="_scrollBody"] { scrollbar-gutter: auto !important; scrollbar-width: none !important; } [data-phase] [class$="_scrollBody"]::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; } /* Message action rows (copy / run-time badges) can overflow the right edge on narrow screens — keep them inside the message width. */ [data-phase] [class$="_actions"] { overflow: hidden !important; } [data-phase] [class$="_actions"] [class$="_timeEnd"] { flex: 0 1 auto !important; min-width: 0 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } [data-phase] [class$="_scroll"]:has(p) { padding-left: 20px !important; padding-right: 20px !important; font-size: 15px !important; } /* The official markdown styles set an explicit 16px on paragraphs and list items, so the container's inherited 15px is not enough. User messages render their text in a div whose class carries _text_ (16px too) — cover it as well. */ [data-phase] [class$="_scroll"]:has(p) p, [data-phase] [class$="_scroll"]:has(p) li, [data-phase] [class$="_scroll"]:has(p) [class*="_text_"] { font-size: 15px !important; } /* Keep DSH's own process disclosures and their expand/collapse behaviour, but remove desktop-sized vertical breathing room between consecutive context, Skill, and system-prompt entries. */ [data-phase] [data-turn-process] { height: 28px !important; padding-bottom: 4px !important; margin-bottom: 4px !important; } [data-phase] [data-turn-process][data-open] { margin-bottom: 4px !important; } [data-phase] [data-disclosure-row] { min-height: 24px !important; } [data-phase] :is([data-context-injection-body], [data-system-prompt-body]) { margin-top: 2px !important; } /* --- Composer bottom row on mobile --- Keep add, permission, model, reasoning and send controls on one line at the Honor 50's 360px CSS viewport. DSH's stable data-composer-card hook survives the editor's textarea -> contenteditable migration. */ [data-phase] [data-composer-card="true"] > [class$="_row"] { flex-wrap: nowrap !important; gap: 6px !important; } [data-phase] [data-composer-card="true"] > [class$="_row"] > :first-child { gap: 8px !important; min-width: 0 !important; } [data-phase] [data-composer-card="true"] > [class$="_row"] > :first-child > :nth-child(2) { flex: 0 1 auto !important; min-width: 0 !important; } [data-phase] [data-composer-card="true"] > [class$="_row"] > :last-child { flex: 1 1 0 !important; min-width: 0 !important; gap: 6px !important; } /* --- Composer popups as bottom sheets on mobile --- Two composer-anchored popups break on phones (field: bottom + side cut, only part of the popup visible): 1. the model pill menu ([role=menu], max 360px, opens upward from the pill inside the composer card); 2. the "/" command palette (max 320px card with the search box — it hosts the /model popupSelect list: search + provider-grouped rows). Both are position:absolute INSIDE the conversation scrollBody (overflow:hidden) and the shell center column (overflow:hidden), so the scroll containers clip them mid-list. Forensics showed neither layer creates a containing block (no transform/contain/will-change), so on mobile we snap whichever popup is open to a viewport-anchored sheet: fixed positioning escapes the scroll clip entirely, width is deterministic, safe-area keeps it off the gesture bar. A transient picker covering the composer is standard mobile UX; selection or an outside tap still dismisses it. */ [data-phase] [class$="_root"]:has(> [aria-haspopup="menu"]) > [role="menu"], [data-phase] [class$="_card"]:has(> [class$="_search"]) { position: fixed !important; left: 12px !important; right: 12px !important; top: auto !important; bottom: calc(env(safe-area-inset-bottom, 0px) + 12px) !important; width: auto !important; min-width: 0 !important; max-width: none !important; max-height: min(65vh, 480px) !important; max-height: min(65dvh, 480px) !important; z-index: 130 !important; border-radius: 14px !important; box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important; } /* --- Session header on mobile --- Layout goal: [toggle] [session title] [mode badge] in a row, with the Session log capsule removed from the header (relocated to the drawer footer). Stable structural hooks only: [data-phase] header the session header element header > :first-child titleRow (titleCluster + utilities) header > :first-child > :last-child headerUtilities (Session log seat) */ [data-phase] header { padding: 8px 12px 0 !important; } /* The directory and Files controls are absolutely positioned, so reserve their lanes and let the title use the remaining width without squeezing. */ [data-phase] header > :first-child { min-height: 36px !important; padding: 0 32px !important; } [data-phase] header [class$="_titleCluster"], [data-phase] header [class$="_crumbs"] { min-width: 0 !important; } [data-phase] header button[class*="_crumb"] { max-width: calc(100vw - 104px) !important; padding-left: 0 !important; padding-right: 0 !important; } /* The directory toggle sits at the far left of the header (the header is position:relative; the data-slot wrappers are display:contents). */ [data-mobile-nav="toggle"] { position: absolute !important; left: 8px !important; top: 12px !important; z-index: 2 !important; } /* The Files action sits at the FAR RIGHT of the header so it reads as a distinct control from the directory toggle on the left (which opens the history sidebar). */ [data-mobile-nav="files"] { position: absolute !important; left: auto !important; right: 8px !important; top: 12px !important; z-index: 2 !important; } /* Session log download: gone from the header row on mobile (the utilities seat holds only the session-log-export capsule). */ [data-phase] header > :first-child > :last-child { display: none !important; } /* --- Settings dialog on mobile --- Desktop: 800px two-column flex (188px nav + content). Mobile: a near-full-width sheet — nav tabs wrap into rows on top, option rows stay horizontal (title+description left, control right). Structural selectors are scoped to the unique aria-modal dialog; every settings-specific rule is gated with :has(> :first-child > :last-child > button) — the settings nav tab list holds