mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-20 05:42:20 +02:00
fix(ui): respect iOS safe areas, screen height and landscape layout
This commit is contained in:
@@ -190,7 +190,7 @@ export function createPanel({
|
||||
// which left it as a side panel squeezing the chat).
|
||||
if (window.innerWidth <= 768) {
|
||||
pane.style.position = 'fixed';
|
||||
pane.style.inset = '0';
|
||||
pane.style.inset = 'var(--sheet-top) 0 0';
|
||||
pane.style.width = '100%';
|
||||
pane.style.maxWidth = '100%';
|
||||
pane.style.zIndex = '170';
|
||||
|
||||
@@ -82,6 +82,28 @@
|
||||
/* Warm accent — used by the Goals/Today UI in Notes. Lives as a token so
|
||||
themes can override without touching the goal CSS. */
|
||||
--accent-warm: #d19a66;
|
||||
|
||||
/* Device safe areas (notch, Dynamic Island, home indicator) */
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-right: env(safe-area-inset-right, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-left: env(safe-area-inset-left, 0px);
|
||||
/* Full-height sheets start clear of the status-bar blur; flush when there is no inset */
|
||||
--sheet-top: calc(var(--safe-top) + min(var(--safe-top), 24px));
|
||||
--app-height: 100dvh;
|
||||
}
|
||||
/* The on-screen keyboard covers the home indicator */
|
||||
:root.keyboard-open { --safe-bottom: 0px; }
|
||||
/* iOS standalone web apps report dvh (and the viewport) short by the status bar
|
||||
height; lvh still matches the screen. Body becomes the containing block for
|
||||
fixed elements so overlays reach the real bottom edge as well. Containment
|
||||
stops body's background reaching the canvas, so html paints it instead. */
|
||||
@media (display-mode: standalone) {
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
:root { --app-height: 100lvh; }
|
||||
html { height: var(--app-height); background-color: var(--bg); }
|
||||
body { contain: layout; overflow: clip; }
|
||||
}
|
||||
}
|
||||
|
||||
:root.light {
|
||||
@@ -121,9 +143,12 @@ body {
|
||||
font-family: var(--font-family, 'Fira Code', monospace);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
height: 100dvh; /* dynamic viewport height — adapts when mobile keyboard opens */
|
||||
height: var(--app-height);
|
||||
padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
|
||||
overflow: hidden;
|
||||
}
|
||||
/* iOS keeps the full height when the keyboard opens; size the shell to what is visible */
|
||||
:root.keyboard-open body { height: var(--keyboard-viewport-height, var(--app-height)); }
|
||||
|
||||
/* Self-hosted Fira Code font */
|
||||
@font-face { font-family: 'Fira Code'; font-weight: 300; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Light.woff2') format('woff2'); }
|
||||
|
||||
@@ -562,8 +562,8 @@ body.bg-pattern-ascii-fireflies {
|
||||
/* Fixed hamburger — always visible, toggles sidebar */
|
||||
.hamburger-btn {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 9px;
|
||||
top: calc(12px + var(--safe-top));
|
||||
left: calc(9px + var(--safe-left));
|
||||
right: auto;
|
||||
z-index: 210;
|
||||
width: 30px;
|
||||
@@ -583,7 +583,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
}
|
||||
body.hamburger-right .hamburger-btn {
|
||||
left: auto;
|
||||
right: 9px;
|
||||
right: calc(9px + var(--safe-right));
|
||||
}
|
||||
.mobile-new-chat-btn {
|
||||
display: none;
|
||||
@@ -655,6 +655,25 @@ body.bg-pattern-ascii-fireflies {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
/* Edge columns bleed into the safe-area strips instead of leaving bare background */
|
||||
@media (min-width: 769px) {
|
||||
.sidebar:not(.hidden) {
|
||||
margin-bottom: calc(-1 * var(--safe-bottom));
|
||||
padding-bottom: var(--safe-bottom);
|
||||
}
|
||||
.sidebar:not(.hidden):not(.right-side) {
|
||||
margin-left: calc(-1 * var(--safe-left));
|
||||
padding-left: var(--safe-left);
|
||||
}
|
||||
.sidebar.right-side:not(.hidden) {
|
||||
margin-right: calc(-1 * var(--safe-right));
|
||||
padding-right: var(--safe-right);
|
||||
}
|
||||
.icon-rail {
|
||||
margin-bottom: calc(-1 * var(--safe-bottom));
|
||||
padding-bottom: calc(8px + var(--safe-bottom));
|
||||
}
|
||||
}
|
||||
.icon-rail-divider {
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
@@ -4800,7 +4819,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
|
||||
.toast {
|
||||
position:fixed;
|
||||
top: 16px; right: 16px;
|
||||
top: calc(16px + var(--safe-top)); right: calc(16px + var(--safe-right));
|
||||
left: auto; bottom: auto;
|
||||
background:var(--panel); color:var(--fg);
|
||||
border:1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
@@ -4984,8 +5003,8 @@ body.bg-pattern-ascii-fireflies {
|
||||
.toast.toast-panel-offset.exiting { transform: translateX(-120%); }
|
||||
@media (max-width: 768px) {
|
||||
.toast {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
top: calc(12px + var(--safe-top));
|
||||
right: calc(12px + var(--safe-right));
|
||||
max-width: calc(100vw - 24px);
|
||||
/* Receive touches so the swipe-to-dismiss gesture works (desktop keeps
|
||||
pointer-events:none so the toast never blocks clicks). Horizontal pan
|
||||
@@ -5109,6 +5128,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
--sidebar-bg. */
|
||||
background: var(--sidebar-bg, var(--panel)) !important;
|
||||
backdrop-filter: none !important;
|
||||
padding: var(--safe-top) 0 var(--safe-bottom) var(--safe-left);
|
||||
}
|
||||
.sidebar.hidden {
|
||||
width: 80% !important;
|
||||
@@ -5122,6 +5142,8 @@ body.bg-pattern-ascii-fireflies {
|
||||
.sidebar.right-side {
|
||||
left: auto; right: 0;
|
||||
box-shadow: -4px 0 20px rgba(0,0,0,0.5);
|
||||
padding-left: 0;
|
||||
padding-right: var(--safe-right);
|
||||
}
|
||||
html.ody-sidebar-off .sidebar,
|
||||
html.ody-mobile-startup-sidebar-hidden .sidebar {
|
||||
@@ -5315,9 +5337,9 @@ body.bg-pattern-ascii-fireflies {
|
||||
.hamburger-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
top: 6px;
|
||||
top: calc(6px + var(--safe-top));
|
||||
left: auto !important;
|
||||
right: 4px !important;
|
||||
right: calc(4px + var(--safe-right)) !important;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.hamburger-btn:hover,
|
||||
@@ -5330,8 +5352,8 @@ body.bg-pattern-ascii-fireflies {
|
||||
.mobile-new-chat-btn {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 8px;
|
||||
top: calc(12px + var(--safe-top));
|
||||
left: calc(8px + var(--safe-left));
|
||||
z-index: 210;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -5400,10 +5422,6 @@ body.bg-pattern-ascii-fireflies {
|
||||
padding: 6px !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
/* Safe area padding for notched devices */
|
||||
.chat-input-bar {
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
/* Mode toggle — larger touch targets */
|
||||
.mode-toggle {
|
||||
height: 34px;
|
||||
@@ -5447,15 +5465,15 @@ body.bg-pattern-ascii-fireflies {
|
||||
URL-bar show/hide. Order matters: later same-specificity rule
|
||||
wins, so dvh must come after vh. */
|
||||
max-height: 100vh !important;
|
||||
max-height: 100dvh !important;
|
||||
max-height: calc(var(--app-height) - var(--sheet-top)) !important;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
height: calc(var(--app-height) - var(--sheet-top));
|
||||
border-radius: 14px 14px 0 0 !important;
|
||||
border: none !important;
|
||||
border-top: 1px solid var(--border) !important;
|
||||
box-shadow: none !important;
|
||||
padding: 6px !important;
|
||||
padding-bottom: env(safe-area-inset-bottom, 6px) !important;
|
||||
padding-bottom: var(--safe-bottom) !important;
|
||||
margin: 0 !important;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -5536,7 +5554,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
(against the flex-sized grid) as a definite height, so grid/flex
|
||||
children never filled. An explicit px height is unambiguous. */
|
||||
margin: 0 !important;
|
||||
padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px)) !important;
|
||||
padding: 8px 10px calc(8px + var(--safe-bottom)) !important;
|
||||
background: var(--bg) !important;
|
||||
/* Flex column. JS (_fillSkillCardHeight) sets EXPLICIT px heights on
|
||||
the preview + <pre>; in a flex column an explicit-height item
|
||||
@@ -5615,7 +5633,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
/* Small bottom padding now that the load-more no longer steals space —
|
||||
the action bar sits low, just clearing the home-indicator safe area. */
|
||||
#doclib-modal [data-doclib-panel="documents"] .doclib-card.doclib-card-expanded .doclib-card-preview {
|
||||
padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px)) !important;
|
||||
padding-bottom: calc(18px + var(--safe-bottom)) !important;
|
||||
}
|
||||
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
|
||||
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .doclib-card-pdf-frame,
|
||||
@@ -6127,6 +6145,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
z-index:250;
|
||||
backdrop-filter:none;
|
||||
pointer-events:none;
|
||||
padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
|
||||
}
|
||||
/* Cookbook always sits above Gallery so the "Serve a model in
|
||||
Cookbook…" flow (and any other "open Cookbook from inside another
|
||||
@@ -6160,6 +6179,17 @@ body.bg-pattern-ascii-fireflies {
|
||||
centered window glides instead of jumping when the nav toggles. */
|
||||
transition: left 0.25s ease, width 0.25s ease;
|
||||
}
|
||||
/* A right-side nav reserves its width on the right instead */
|
||||
body.hamburger-right :is(#calendar-modal, #gallery-modal, #tasks-modal, #memory-modal, #doclib-modal,
|
||||
#compare-model-overlay, #research-overlay, #theme-modal, #settings-modal, #email-lib-modal) {
|
||||
left: 0;
|
||||
}
|
||||
/* Windows never grow past the area between the nav and the safe areas */
|
||||
:is(#calendar-modal, #gallery-modal, #tasks-modal, #memory-modal, #doclib-modal,
|
||||
#compare-model-overlay, #research-overlay, #theme-modal, #settings-modal, #email-lib-modal)
|
||||
> :is(.modal-content, .memory-modal-content, .settings-modal-content, .doclib-modal-content) {
|
||||
max-width: calc(100vw - var(--safe-left) - var(--safe-right) - var(--icon-rail-w, 48px) - var(--sidebar-w, 0px));
|
||||
}
|
||||
}
|
||||
.modal-content {
|
||||
background:var(--panel);
|
||||
@@ -6689,8 +6719,8 @@ body.bg-pattern-ascii-fireflies {
|
||||
/* Agent indicator */
|
||||
#agent-indicator {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: calc(20px + var(--safe-top));
|
||||
right: calc(20px + var(--safe-right));
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
@@ -7016,7 +7046,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
#compare-model-overlay .modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(720px, calc(100dvh - 48px));
|
||||
max-height: min(720px, calc(var(--app-height) - 48px));
|
||||
overflow: hidden;
|
||||
min-height: 180px;
|
||||
}
|
||||
@@ -7433,7 +7463,7 @@ body.bg-pattern-ascii-fireflies {
|
||||
|
||||
#recording-indicator {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
top: calc(10px + var(--safe-top));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
|
||||
@@ -172,9 +172,8 @@
|
||||
position:fixed; height:100% = the layout viewport, whose bottom sits
|
||||
UNDER Firefox Android's bottom URL bar — so flex-end parked the sheet
|
||||
(and its footer / Start button) beneath the bar. dvh excludes the bar
|
||||
so the footer lands above it. Extra safe-area pad for iOS home bar. */
|
||||
height: 100dvh;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
so the footer lands above it. */
|
||||
height: var(--app-height);
|
||||
}
|
||||
/* Confirm dialog stays centered, not a bottom sheet */
|
||||
#styled-confirm-overlay {
|
||||
@@ -204,7 +203,7 @@
|
||||
border-radius: 14px 14px 0 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 6px 12px 12px;
|
||||
padding: 6px 12px calc(12px + var(--safe-bottom));
|
||||
animation: sheet-enter 0.2s ease-out forwards;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
@@ -252,12 +251,12 @@
|
||||
#email-lib-modal .modal-content,
|
||||
#doclib-modal .doclib-modal-content {
|
||||
max-height: 90vh !important;
|
||||
max-height: 90dvh !important;
|
||||
max-height: calc(var(--app-height) * 0.9) !important;
|
||||
height: auto !important;
|
||||
/* Reserve the iOS home-indicator / bottom-bar strip so an expanded
|
||||
skill card's footer (Delete / Edit / Run) isn't hidden under it.
|
||||
dvh already excludes the URL bar; this handles the safe area. */
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
|
||||
padding-bottom: var(--safe-bottom) !important;
|
||||
}
|
||||
/* Grab handle pill on the non-standard content classes too. Memory's
|
||||
desktop rule defines a radial-glow ::before later in the file —
|
||||
@@ -305,7 +304,7 @@
|
||||
visible viewport on Chrome/Safari mobile (URL bar covered the
|
||||
action buttons row). */
|
||||
max-height: 90vh !important;
|
||||
max-height: 90dvh !important;
|
||||
max-height: calc(var(--app-height) * 0.9) !important;
|
||||
height: auto !important;
|
||||
}
|
||||
/* Anchor those modals to the top so the full height is usable */
|
||||
@@ -314,8 +313,11 @@
|
||||
#email-lib-modal,
|
||||
#doclib-modal,
|
||||
#gallery-modal,
|
||||
#tasks-modal {
|
||||
padding-top: 0 !important;
|
||||
#tasks-modal,
|
||||
#memory-modal,
|
||||
#settings-modal {
|
||||
padding-top: var(--sheet-top) !important;
|
||||
padding-bottom: 0 !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
/* Deep Research already gets the swipe grab-handle pill from the
|
||||
|
||||
@@ -410,8 +410,8 @@ button.hamburger {
|
||||
/* Agent indicator */
|
||||
#agent-indicator {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: calc(20px + var(--safe-top));
|
||||
right: calc(20px + var(--safe-right));
|
||||
background: var(--sidebar-bg, var(--panel));
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
@@ -894,7 +894,7 @@ button.hamburger {
|
||||
/* Recording indicator */
|
||||
#recording-indicator {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
top: calc(10px + var(--safe-top));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
|
||||
@@ -2666,11 +2666,13 @@ mark.doc-find-mark.current {
|
||||
render it as a narrow side pane ("sidebar") on a phone. */
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
top: 0 !important; right: 0 !important; bottom: 0 !important; left: 0 !important;
|
||||
top: var(--sheet-top) !important; right: 0 !important; bottom: 0 !important; left: 0 !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
flex: none !important;
|
||||
z-index: 170;
|
||||
padding-bottom: var(--safe-bottom);
|
||||
/* Stroke the top edge so the rounded corners read as a curved sheet edge. */
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
|
||||
@@ -1181,8 +1181,8 @@ span[id$="-logo"] svg:not([width]) {
|
||||
opacity: 0;
|
||||
transform-origin: center center;
|
||||
position: absolute;
|
||||
right: calc(16px + env(safe-area-inset-right, 0px));
|
||||
bottom: calc(18px + env(safe-area-inset-bottom, 0px));
|
||||
right: calc(16px + var(--safe-right));
|
||||
bottom: calc(18px + var(--safe-bottom));
|
||||
height: var(--fab-size);
|
||||
min-width: var(--fab-size);
|
||||
padding: 0 20px 0 18px;
|
||||
@@ -1298,17 +1298,23 @@ span[id$="-logo"] svg:not([width]) {
|
||||
instead of becoming a side panel. */
|
||||
#doclib-modal.doclib-fullscreen:not(.modal-right-docked) .doclib-modal-content {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
top: var(--safe-top) !important;
|
||||
left: calc(var(--safe-left) + var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
right: var(--safe-right) !important;
|
||||
bottom: var(--safe-bottom) !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
border-radius: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
body.hamburger-right #doclib-modal.doclib-fullscreen:not(.modal-right-docked) .doclib-modal-content {
|
||||
left: var(--safe-left) !important;
|
||||
right: calc(var(--safe-right) + var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
}
|
||||
}
|
||||
#doclib-modal.doclib-fullscreen .modal-header,
|
||||
#doclib-modal.doclib-fullscreen .lib-tabs,
|
||||
#doclib-modal.modal-right-docked .modal-header,
|
||||
@@ -1415,10 +1421,10 @@ span[id$="-logo"] svg:not([width]) {
|
||||
panes reserve room with margins so left + right docks can be active at the
|
||||
same time without skewing the entire body box. */
|
||||
body.right-dock-active {
|
||||
padding-right: 0;
|
||||
padding-right: var(--safe-right);
|
||||
}
|
||||
body.left-dock-active {
|
||||
padding-left: 0;
|
||||
padding-left: var(--safe-left);
|
||||
}
|
||||
body.left-dock-active:not(.email-doc-split-active) .chat-container {
|
||||
margin-left: var(--left-dock-w, 0px);
|
||||
|
||||
+25
-19
@@ -447,13 +447,11 @@
|
||||
edges and let the inner body absorb the remaining height. */
|
||||
body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .modal-content {
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
inset: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left) !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
border-radius: 0 !important;
|
||||
transform: none !important;
|
||||
display: flex !important;
|
||||
@@ -472,17 +470,23 @@ body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .e
|
||||
one of the two CSS vars is 0 at any given time, set by init.js). */
|
||||
body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
top: var(--safe-top) !important;
|
||||
left: calc(var(--safe-left) + var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
right: var(--safe-right) !important;
|
||||
bottom: var(--safe-bottom) !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
border-radius: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
body.hamburger-right:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content {
|
||||
left: var(--safe-left) !important;
|
||||
right: calc(var(--safe-right) + var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
|
||||
}
|
||||
}
|
||||
/* Mobile: the /email route adds .email-lib-fullscreen, but the desktop full-
|
||||
bleed rule above squares off the corners and offsets the email by the icon
|
||||
rail. On phones the email should be the normal bottom-sheet — full width,
|
||||
@@ -490,12 +494,14 @@ body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.mod
|
||||
so it wins on mobile. */
|
||||
@media (max-width: 768px) {
|
||||
body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content {
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
height: 100dvh !important;
|
||||
max-height: 100dvh !important;
|
||||
top: var(--sheet-top) !important;
|
||||
left: var(--safe-left) !important;
|
||||
right: var(--safe-right) !important;
|
||||
bottom: 0 !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
border-radius: 14px 14px 0 0 !important;
|
||||
border-top: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
@@ -4502,7 +4502,7 @@ button .spinner-whirlpool {
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
top: auto !important;
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px) !important;
|
||||
bottom: calc(var(--safe-bottom) + 8px) !important;
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
max-width: calc(100vw - 16px) !important;
|
||||
|
||||
@@ -4638,13 +4638,14 @@ body.notes-view .chat-container { flex: 1; min-width: 0; }
|
||||
@media (max-width: 768px) {
|
||||
body.notes-view .notes-pane {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
inset: var(--sheet-top) 0 0;
|
||||
max-width: 100%;
|
||||
width: 100% !important;
|
||||
/* Desktop sets height: min(80vh, 820px) which wins over inset:0's bottom:0
|
||||
and leaves a gap at the bottom of the viewport. Force full height here. */
|
||||
height: 100dvh !important;
|
||||
max-height: 100dvh !important;
|
||||
height: calc(var(--app-height) - var(--sheet-top)) !important;
|
||||
max-height: calc(var(--app-height) - var(--sheet-top)) !important;
|
||||
padding-bottom: var(--safe-bottom);
|
||||
z-index: 170;
|
||||
/* Bottom-sheet treatment like the document editor: stroked, rounded top,
|
||||
slides up from the bottom. */
|
||||
|
||||
@@ -1439,7 +1439,7 @@ body.theme-frosted .modal {
|
||||
/* Bottom-sheet: 90dvh tall (a touch smaller than full-screen) anchored to the
|
||||
bottom, so there's a small gap at the top and it reads as a sheet. The body
|
||||
still scrolls within the bounded height. */
|
||||
#research-overlay { align-items: flex-end !important; justify-content: stretch !important; }
|
||||
#research-overlay { align-items: flex-end !important; justify-content: stretch !important; padding-bottom: 0 !important; }
|
||||
#research-pane {
|
||||
height: 90dvh !important;
|
||||
max-height: 90dvh !important;
|
||||
@@ -1451,7 +1451,7 @@ body.theme-frosted .modal {
|
||||
browser's bottom toolbar / safe area and can always be scrolled into view
|
||||
(otherwise the form fits the viewport and there's no room to reach them). */
|
||||
#research-pane .research-pane-body {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 40px) !important;
|
||||
padding-bottom: calc(var(--safe-bottom) + 40px) !important;
|
||||
touch-action: pan-y;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
window._odysseusLoadTime = Date.now();
|
||||
// Stops iOS focus-zoom on every field; iOS still allows pinch-zoom, other platforms would not.
|
||||
(function(){
|
||||
var ua = navigator.userAgent || '';
|
||||
var isIOS = /iPad|iPhone|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
|
||||
var vp = document.querySelector('meta[name="viewport"]');
|
||||
if (isIOS && vp) vp.setAttribute('content', vp.getAttribute('content') + ', maximum-scale=1');
|
||||
})();
|
||||
(function(){
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem('odysseus-theme'));
|
||||
|
||||
@@ -6929,13 +6929,6 @@ import { invalidateSettings } from './appConfig.js';
|
||||
}
|
||||
});
|
||||
}
|
||||
// Smooth viewport resize when keyboard opens/closes
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
document.documentElement.style.setProperty('--vh', window.visualViewport.height + 'px');
|
||||
});
|
||||
document.documentElement.style.setProperty('--vh', window.visualViewport.height + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// If the browser discarded and restored this tab, reload the current session
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from './utils.js';
|
||||
import { state } from './state.js';
|
||||
import { getSettings } from '../appConfig.js';
|
||||
import { collapseSidebarToRail } from '../modalSnap.js';
|
||||
import { clampWindowLeft, collapseSidebarToRail, leftNavRight, safeInsetPx } from '../modalSnap.js';
|
||||
import { emailApiUrl } from '../emailShared.js';
|
||||
import {
|
||||
bindMenuDismiss,
|
||||
@@ -823,7 +823,7 @@ function _readCssPx(name) {
|
||||
}
|
||||
|
||||
function _emailSplitLeftEdge() {
|
||||
return _readCssPx('--icon-rail-w') + _readCssPx('--sidebar-w');
|
||||
return leftNavRight();
|
||||
}
|
||||
|
||||
function _setEmailDocumentSplit(leftEdge, emailWidth) {
|
||||
@@ -899,7 +899,8 @@ function _clearEmailDocumentSplit() {
|
||||
// rail. Used by the "try collapsing the sidebar first" path so we can decide
|
||||
// whether collapsing recovers enough room before minimizing email.
|
||||
function _emailSplitLeftEdgeIfSidebarCollapsed() {
|
||||
return _readCssPx('--icon-rail-w');
|
||||
const navOnRight = document.getElementById('sidebar')?.classList.contains('right-side');
|
||||
return safeInsetPx('left') + (navOnRight ? 0 : _readCssPx('--icon-rail-w'));
|
||||
}
|
||||
|
||||
function _hasDesktopRoomForEmailAndDocument(modal, opts = {}) {
|
||||
@@ -2415,7 +2416,7 @@ export function openEmailLibrary(opts = {}) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth;
|
||||
const refH = window.innerHeight * 0.85;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - refH) / 2) + 'px';
|
||||
content.style.transform = 'none';
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import spinnerModule from '../spinner.js';
|
||||
import * as Modals from '../modalManager.js';
|
||||
import { clampWindowLeft } from '../modalSnap.js';
|
||||
import { showToast } from '../ui.js?v=20260916largetoolscroll1';
|
||||
import { state } from './state.js';
|
||||
import { _esc, _extractName, _renderEmailSummaryError } from './utils.js';
|
||||
@@ -428,7 +429,7 @@ export async function _openEmailWindow(em, folder) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth, h = content.offsetHeight;
|
||||
const off = (_emailWindowSeq % 6) * 28;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2 + off) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2 + off), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - h) / 3 + off) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export function createPanel({
|
||||
// which left it as a side panel squeezing the chat).
|
||||
if (window.innerWidth <= 768) {
|
||||
pane.style.position = 'fixed';
|
||||
pane.style.inset = '0';
|
||||
pane.style.inset = 'var(--sheet-top) 0 0';
|
||||
pane.style.width = '100%';
|
||||
pane.style.maxWidth = '100%';
|
||||
pane.style.zIndex = '170';
|
||||
|
||||
@@ -369,6 +369,31 @@ document.addEventListener('DOMContentLoaded', markComposerUserEdited, { once: tr
|
||||
/* ---- Mobile viewport fix — keep chat visible when virtual keyboard opens ---- */
|
||||
{
|
||||
if (window.visualViewport) {
|
||||
const root = document.documentElement;
|
||||
// iOS overlays the keyboard without resizing the layout viewport and scrolls
|
||||
// the page to reveal the field, leaving it shifted after the keyboard closes.
|
||||
const _syncKeyboardViewport = function() {
|
||||
const vv = window.visualViewport;
|
||||
if (vv.scale > 1.01) return;
|
||||
const keyboardOpen = window.innerHeight - vv.height > 120;
|
||||
root.classList.toggle('keyboard-open', keyboardOpen);
|
||||
if (keyboardOpen) root.style.setProperty('--keyboard-viewport-height', vv.height + 'px');
|
||||
else root.style.removeProperty('--keyboard-viewport-height');
|
||||
if (window.scrollY) window.scrollTo(0, 0);
|
||||
};
|
||||
// iOS fires visualViewport resize only once, mid-animation, and no focusin when
|
||||
// the tapped field already had focus (e.g. the composer after a new chat)
|
||||
const _syncThroughKeyboardAnimation = () => {
|
||||
_syncKeyboardViewport();
|
||||
for (const delay of [100, 300, 600, 1000]) setTimeout(_syncKeyboardViewport, delay);
|
||||
};
|
||||
_syncKeyboardViewport();
|
||||
window.visualViewport.addEventListener('resize', _syncThroughKeyboardAnimation);
|
||||
window.visualViewport.addEventListener('scroll', _syncKeyboardViewport);
|
||||
document.addEventListener('focusin', _syncThroughKeyboardAnimation);
|
||||
document.addEventListener('focusout', _syncThroughKeyboardAnimation);
|
||||
window.addEventListener('scroll', () => { if (window.scrollY) window.scrollTo(0, 0); }, { passive: true });
|
||||
|
||||
let _lastVVHeight = window.visualViewport.height;
|
||||
window.visualViewport.addEventListener('resize', function() {
|
||||
const vv = window.visualViewport;
|
||||
|
||||
+45
-7
@@ -110,7 +110,7 @@ function _clampRightDockWidth(width) {
|
||||
const min = _minEdgeDockWidth();
|
||||
const navRight = _leftNavRight();
|
||||
const leftDockW = _activeDockWidth('left');
|
||||
const maxByChat = window.innerWidth - navRight - leftDockW - MIN_CHAT_WIDTH;
|
||||
const maxByChat = window.innerWidth - navRight - _rightNavInset() - leftDockW - MIN_CHAT_WIDTH;
|
||||
const max = Math.min(Math.round(window.innerWidth * 0.82), maxByChat);
|
||||
return _clampDockWidthToSpace(width, min, max);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function _clampRightDockWidth(width) {
|
||||
function _clampLeftDockWidth(width, left = _leftNavRight()) {
|
||||
const min = _minEdgeDockWidth();
|
||||
const rightDockW = _activeDockWidth('right');
|
||||
const available = Math.max(0, window.innerWidth - left - rightDockW);
|
||||
const available = Math.max(0, window.innerWidth - left - _rightNavInset() - rightDockW);
|
||||
const max = Math.min(Math.round(available * 0.82), available - MIN_CHAT_WIDTH);
|
||||
return _clampDockWidthToSpace(width, min, max);
|
||||
}
|
||||
@@ -170,13 +170,23 @@ function _shouldAutoCollapseSidebar(dockW) {
|
||||
return remaining < MIN_CHAT_WIDTH;
|
||||
}
|
||||
|
||||
function _navOnRight() {
|
||||
return !!document.getElementById('sidebar')?.classList.contains('right-side');
|
||||
}
|
||||
|
||||
// Body padding carries the device safe-area insets (notch, Dynamic Island).
|
||||
function _bodyPaddingPx(side) {
|
||||
return parseFloat(window.getComputedStyle(document.body)[side === 'left' ? 'paddingLeft' : 'paddingRight']) || 0;
|
||||
}
|
||||
|
||||
// Right edge (px) of whatever left navigation is currently showing — the
|
||||
// expanded sidebar if visible, otherwise the icon rail. Used to anchor the
|
||||
// left dock so it always sits flush to the right of the nav.
|
||||
function _leftNavRight() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rail = document.getElementById('icon-rail');
|
||||
let x = 0;
|
||||
let x = _bodyPaddingPx('left');
|
||||
if (_navOnRight()) return x;
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
const r = sidebar.getBoundingClientRect();
|
||||
if (r.width) x = Math.max(x, r.right);
|
||||
@@ -188,8 +198,36 @@ function _leftNavRight() {
|
||||
return x;
|
||||
}
|
||||
|
||||
// Distance (px) from the viewport's right edge that a right-side nav and the
|
||||
// safe area occupy. The right dock sits flush to the left of it.
|
||||
function _rightNavInset() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rail = document.getElementById('icon-rail');
|
||||
let inset = _bodyPaddingPx('right');
|
||||
if (!_navOnRight()) return inset;
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
const r = sidebar.getBoundingClientRect();
|
||||
if (r.width) inset = Math.max(inset, window.innerWidth - r.left);
|
||||
}
|
||||
if (rail && window.getComputedStyle(rail).display !== 'none') {
|
||||
const r = rail.getBoundingClientRect();
|
||||
if (r.width) inset = Math.max(inset, window.innerWidth - r.left);
|
||||
}
|
||||
return inset;
|
||||
}
|
||||
|
||||
export function leftNavRight() { return _leftNavRight(); }
|
||||
export function rightNavInset() { return _rightNavInset(); }
|
||||
export function safeInsetPx(side) { return _bodyPaddingPx(side); }
|
||||
|
||||
// Keeps a floating window between the nav and the safe areas
|
||||
export function clampWindowLeft(left, width) {
|
||||
const max = window.innerWidth - _rightNavInset() - width;
|
||||
return Math.max(_leftNavRight(), Math.min(left, max));
|
||||
}
|
||||
|
||||
function _clampEmailDocSplitWidth(width, left = _leftNavRight()) {
|
||||
const available = Math.max(0, window.innerWidth - left);
|
||||
const available = Math.max(0, window.innerWidth - left - _rightNavInset());
|
||||
if (!available) return 0;
|
||||
const compact = available < 760;
|
||||
const minEmail = compact ? 260 : 340;
|
||||
@@ -235,7 +273,7 @@ function _applyEmailDocSplitGeometry(left, emailWidth) {
|
||||
if (!docPane || window.innerWidth <= 768) return;
|
||||
docPane.style.setProperty('position', 'fixed', 'important');
|
||||
docPane.style.setProperty('left', `${x}px`, 'important');
|
||||
docPane.style.setProperty('right', 'var(--right-dock-w, 0px)', 'important');
|
||||
docPane.style.setProperty('right', `calc(var(--right-dock-w, 0px) + ${_rightNavInset()}px)`, 'important');
|
||||
docPane.style.setProperty('top', '0px', 'important');
|
||||
docPane.style.setProperty('bottom', '0px', 'important');
|
||||
docPane.style.setProperty('width', 'auto', 'important');
|
||||
@@ -497,7 +535,7 @@ function _applyDockInternal(modal, side, dockClass) {
|
||||
} else {
|
||||
w = _resolveRightDockWidth(modal, content);
|
||||
content.style.left = 'auto';
|
||||
content.style.right = '0';
|
||||
content.style.right = _rightNavInset() + 'px';
|
||||
content.style.width = w + 'px';
|
||||
content.style.maxWidth = w + 'px';
|
||||
document.body.classList.add('right-dock-active');
|
||||
@@ -872,7 +910,7 @@ export function makeEdgeDockController(modal, side = 'right', dockClass) {
|
||||
w = _clampRightDockWidth(window.innerWidth - clientX);
|
||||
content._userDockWidth = w;
|
||||
content.style.left = 'auto';
|
||||
content.style.right = '0';
|
||||
content.style.right = _rightNavInset() + 'px';
|
||||
content.style.width = w + 'px';
|
||||
content.style.maxWidth = w + 'px';
|
||||
document.body.classList.add('right-dock-active');
|
||||
|
||||
@@ -75,12 +75,15 @@ export function initSidebarLayout(Storage, opts) {
|
||||
_setSidebarModeClasses(mode);
|
||||
}
|
||||
|
||||
// Phones in landscape get the desktop layout, but the icon rail is too small to use there
|
||||
const _phoneLandscape = window.matchMedia('(pointer: coarse) and (max-height: 500px)');
|
||||
|
||||
function _syncRailSideCore() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!iconRail) return;
|
||||
const isRight = sidebar.classList.contains('right-side');
|
||||
const sidebarHidden = sidebar.classList.contains('hidden');
|
||||
const railHidden = iconRail.classList.contains('rail-hidden');
|
||||
const railHidden = iconRail.classList.contains('rail-hidden') || _phoneLandscape.matches;
|
||||
const isMobileMini = iconRail.classList.contains('mobile-mini');
|
||||
iconRail.classList.toggle('right-side', isRight);
|
||||
// On mobile mini mode, JS already set inline styles — don't touch
|
||||
@@ -118,6 +121,7 @@ export function initSidebarLayout(Storage, opts) {
|
||||
// Set initial reference and expose globally
|
||||
_syncRailSideFn = _syncRailSideCore;
|
||||
window.syncRailSide = syncRailSide;
|
||||
_phoneLandscape.addEventListener('change', () => syncRailSide());
|
||||
|
||||
// Restore sidebar side preference
|
||||
if (Storage.get(Storage.KEYS.SIDEBAR_SIDE) === 'right') {
|
||||
|
||||
@@ -257,12 +257,19 @@ export function makeWindowResizable(content, options = {}) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||
if (saved && saved.w && saved.h) {
|
||||
const w = Math.max(minW, Math.min(saved.w, window.innerWidth));
|
||||
const bodyStyle = getComputedStyle(document.body);
|
||||
const padLeft = parseFloat(bodyStyle.paddingLeft) || 0;
|
||||
const padRight = parseFloat(bodyStyle.paddingRight) || 0;
|
||||
const w = Math.max(minW, Math.min(saved.w, window.innerWidth - padLeft - padRight));
|
||||
const h = Math.max(minH, Math.min(saved.h, window.innerHeight));
|
||||
content.style.width = w + 'px';
|
||||
content.style.height = h + 'px';
|
||||
content.style.maxWidth = 'none';
|
||||
content.style.maxHeight = 'none';
|
||||
const left = parseFloat(content.style.left);
|
||||
if (Number.isFinite(left) && left + w > window.innerWidth - padRight) {
|
||||
content.style.left = Math.max(padLeft, window.innerWidth - padRight - w) + 'px';
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user