Files
openchamber/packages/ui/src/styles/mobile.css
T

600 lines
21 KiB
CSS
Raw Normal View History

/* Mobile Adaptations - All Devices */
/* Set global device type variables */
:root {
--is-mobile: 0;
--device-type: 'desktop';
--font-scale: 1;
}
@media (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) {
--is-mobile: 1;
--device-type: 'mobile';
--font-scale: 0.9;
}
}
/* Utility classes for device-specific styling */
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
@media (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
display: none !important;
}
:root.desktop-runtime .mobile-only {
display: none !important;
}
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
display: block !important;
}
}
/* General mobile improvements for all mobile devices */
@media (max-width: 1024px) {
/* Override CSS custom properties for mobile */
:root.mobile-pointer:not(.desktop-runtime) {
--text-markdown: 1rem;
--text-code: 0.875rem;
--text-ui-header: 0.9375rem;
--text-ui-label: 0.875rem;
--text-meta: 0.875rem;
--text-micro: 0.8125rem;
}
/* Force override with higher specificity */
:root.mobile-pointer:not(.desktop-runtime) * {
--text-markdown: 1rem !important;
--text-code: 0.875rem !important;
--text-ui-header: 0.9375rem !important;
--text-ui-label: 0.875rem !important;
--text-meta: 0.875rem !important;
--text-micro: 0.8125rem !important;
}
/* Additional override for elements using typography classes */
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
font-size: unset !important;
}
/* Fix font size for tool displays */
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
--text-code: 0.875rem !important;
font-size: var(--text-code) !important;
}
/* Improve touch targets for mobile */
:root.mobile-pointer:not(.desktop-runtime) button:not([role="radio"]):not([role="checkbox"]):not([role="switch"]),
:root.mobile-pointer:not(.desktop-runtime) .btn,
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
min-height: 36px;
min-width: 36px;
}
/* Composer footer action buttons (sessions / attach / auto-accept): hug the
icon so the group stays tight. The container only renders in the mobile
JSX, so no pointer/runtime gating is needed; !important overrides both the
w-8 utility and the global mobile touch-target min-width regardless of
cascade layer or specificity. */
.composer-mobile-actions button {
width: 1.5rem !important;
min-width: 0 !important;
}
/* Improve input field touch targets */
:root.mobile-pointer:not(.desktop-runtime) input,
:root.mobile-pointer:not(.desktop-runtime) textarea,
:root.mobile-pointer:not(.desktop-runtime) select {
min-height: 36px;
}
/* Fix mobile text inputs */
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
:root.mobile-pointer:not(.desktop-runtime) textarea {
font-size: 16px !important; /* Prevents iOS zoom */
}
/* Prevent keyboard on non-input elements */
:root.mobile-pointer:not(.desktop-runtime)
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
:root.mobile-pointer:not(.desktop-runtime) .btn,
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
/* Specific fix for session buttons */
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
pointer-events: auto;
}
/* Prevent system context menu on message content when using custom selection menu */
:root.mobile-pointer:not(.desktop-runtime) .message-content-text {
-webkit-touch-callout: none;
-webkit-user-select: text;
user-select: text;
}
/* Allow system context menu only when explicitly requested via the "More" button */
:root.mobile-pointer:not(.desktop-runtime) .message-content-text.show-system-menu {
-webkit-touch-callout: default;
}
/* Improve mobile spacing */
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
padding-top: 0.75rem !important;
padding-bottom: 0.75rem !important;
}
/* Fix mobile scroll containers */
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
overflow-x: hidden !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
:root.mobile-pointer:not(.desktop-runtime) [data-page-scroll-lock="true"] {
overflow: hidden !important;
overscroll-behavior: none;
}
2026-04-05 19:24:23 +03:00
/* Keep markdown code blocks horizontally scrollable on mobile. */
:root.mobile-pointer:not(.desktop-runtime) .markdown-content [data-markdown="code-block-body"] {
overflow-x: auto !important;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch;
}
/* Fix mobile viewport height */
:root.mobile-pointer:not(.desktop-runtime) {
height: 100%;
height: -webkit-fill-available;
overflow: hidden;
}
/* Fix main layout container */
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
height: 100vh;
height: -webkit-fill-available;
min-height: 0;
}
/* Fix main content area */
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
min-height: 0;
flex: 1;
}
/* Fix chat container */
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
height: 100%;
min-height: 0;
}
/* Ensure proper flex behavior */
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
min-height: 0;
}
/* Mobile sidebar positioning */
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
top: var(--header-height, 3rem);
height: calc(100vh - var(--header-height, 3rem));
}
/* Set header height variable */
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3rem;
}
/* For mobile devices with larger header */
@media (max-width: 768px) {
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3.5rem;
}
}
/* For tablet devices */
@media (min-width: 769px) and (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3.25rem;
}
}
}
/* iOS PWA Adaptations */
/* iOS safe area defaults (applies to PWA + Safari) */
@supports (-webkit-touch-callout: none) {
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime),
:root.device-tablet:not(.desktop-runtime),
:root.mobile-pointer:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top);
}
:root.oc-capacitor-app [data-sonner-toaster][data-y-position='top'] {
top: calc(env(safe-area-inset-top, 0px) + 16px) !important;
}
}
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
@media (display-mode: standalone) {
/* iOS-specific safe area handling with CSS variable support */
@supports (-webkit-touch-callout: none) {
/* Safe area handling for fixed positioned elements */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime),
:root.device-tablet:not(.desktop-runtime),
:root.mobile-pointer:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
padding-top: 0;
padding-bottom: 0;
padding-left: var(--oc-safe-area-left);
padding-right: var(--oc-safe-area-right);
}
/* Safe area for bottom fixed/bottom-0 elements */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
}
/* Drawer safe area - top already offset by header height */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .drawer-safe-area,
:root.device-tablet:not(.desktop-runtime) .drawer-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .drawer-safe-area {
padding-top: 0;
padding-bottom: var(--oc-safe-area-bottom-visual);
}
/* Fix iOS viewport issues */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen,
:root.device-tablet:not(.desktop-runtime) .flex.flex-col.h-screen,
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
min-height: 100vh;
min-height: -webkit-fill-available;
}
/* Prevent content overlap in iOS */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .flex-1.overflow-hidden,
:root.device-tablet:not(.desktop-runtime) .flex-1.overflow-hidden,
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
position: relative;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Update header height for iOS */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 4.5rem;
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .mobile-sidebar-top,
:root.device-tablet:not(.desktop-runtime) .mobile-sidebar-top,
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
--header-height: 4.5rem;
}
/* Mobile typography improvements */
.typography-markdown {
font-size: 1rem !important;
}
.typography-code {
font-size: 0.875rem !important;
}
.typography-ui-header {
font-size: 0.9375rem !important;
}
.typography-ui-label {
font-size: 0.875rem !important;
}
.typography-meta {
font-size: 0.875rem !important;
}
.typography-micro {
font-size: 0.8125rem !important;
}
/* Fix font size for tool displays */
[class*="tool"], [class*="code"] {
font-size: 0.875rem !important;
}
}
/* Fallback for non-iOS standalone mode */
@supports not (-webkit-touch-callout: none) {
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime),
:root.device-tablet:not(.desktop-runtime),
:root.mobile-pointer:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
padding-top: 0;
padding-bottom: 0;
padding-left: var(--oc-safe-area-left);
padding-right: var(--oc-safe-area-right);
}
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
}
}
}
/* Phase 6: Home indicator blur gradient fix */
@media (display-mode: standalone) and (max-width: 768px) {
.pwa-dialog-content {
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
top: auto;
/* Browsers that don't understand dvh ignore the second declaration and
keep the vh-based fallback. Newer browsers use 100dvh so Android
Chrome's collapsible URL bar doesn't push the dialog footer off-screen. */
max-height: calc(
100vh
- var(--oc-safe-area-top, 0px)
- var(--oc-safe-area-bottom-visual, 0px)
- 20px
);
max-height: calc(
100dvh
- var(--oc-safe-area-top, 0px)
- var(--oc-safe-area-bottom-visual, 0px)
- 20px
);
padding: var(--pwa-dialog-padding);
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
row-gap: clamp(0.75rem, 2vw, 1.25rem);
}
.pwa-dialog-content.pwa-compact {
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
}
.pwa-dialog-content [data-slot="dialog-close"] {
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
right: var(--pwa-dialog-padding);
}
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
}
.pwa-dialog-content .settings-page-body {
max-width: 100%;
padding: 0.75rem 1rem;
row-gap: 0;
}
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
margin-top: 0.85rem;
}
.pwa-dialog-content .directory-dialog-body {
padding: 0.5rem 0.75rem;
}
.pwa-dialog-content .directory-dialog-body .directory-grid {
gap: 0.6rem;
}
.pwa-overlay-panel {
margin-bottom: 0;
}
.pwa-overlay-panel .pwa-overlay-scroll {
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
}
}
@media (display-mode: standalone) {
/* Home indicator overlay */
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) body::after,
:root.device-tablet:not(.desktop-runtime) body::after,
:root.mobile-pointer:not(.desktop-runtime) body::after {
content: '';
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
background: linear-gradient(
to top,
var(--background) 0%,
var(--background) 30%,
rgba(from var(--background) r g b / 0.8) 60%,
rgba(from var(--background) r g b / 0.3) 85%,
transparent 100%
);
z-index: 1000;
pointer-events: none;
}
/* Fallback for browsers without rgba(from ...) support */
@supports not (color: rgba(from white r g b / 0.5)) {
2026-05-04 00:04:29 +03:00
:root.device-mobile:not(.desktop-runtime) body::after,
:root.device-tablet:not(.desktop-runtime) body::after,
:root.mobile-pointer:not(.desktop-runtime) body::after {
background: linear-gradient(
to top,
var(--background) 0%,
var(--background) 30%,
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
rgba(21, 19, 19, 0.3) 85%,
transparent 100%
);
}
/* Light theme fallback */
:root body::after {
background: linear-gradient(
to top,
var(--background) 0%,
var(--background) 30%,
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
rgba(248, 247, 243, 0.3) 85%,
transparent 100%
);
}
}
}
/* Small app-wide bottom safe area for the native shell. The phone's rounded hardware
corners clip controls flush against the bottom edge, and the PWA's own safe-area
padding is gated behind display-mode: standalone — which the Capacitor WebView does
not match — so nothing reserves bottom room in the native app. Expose it as a token
so any native surface can consume it; the chat shell does so below. */
:root.oc-capacitor-app {
--oc-app-bottom-safe: max(16px, calc(env(safe-area-inset-bottom, 0px) * 0.5));
}
/* Paint the document canvas with the theme background in the native app — the same
thing .desktop-runtime does for body/#root, which the Capacitor shell never got.
The status bar is overlaid (transparent), and in dark mode `color-scheme: dark`
makes the bare UA canvas dark, so any sliver not covered by content (notably the
area behind the status bar) bled through as a dark band at the top. It only showed
in dark mode, which is why it tracked the system theme. */
:root.oc-capacitor-app,
:root.oc-capacitor-app body,
:root.oc-capacitor-app #root {
background: var(--background) !important;
background-color: var(--background) !important;
}
/* Native (Capacitor) keyboard handling.
The Keyboard plugin runs in `resize: 'none'` mode so the WebView keeps its full
height; instead we shrink the app shell by the keyboard frame height, exposed as
--oc-keyboard-inset and set once from `keyboardWillShow` (see useNativeMobileChrome).
Scoped to .oc-capacitor-app so the browser PWA keeps its dvh / interactive-widget
behaviour untouched.
`keyboardWillShow` fires at the start of the iOS keyboard animation, so the inset
is set once and the transition carries the rise. The duration/curve are tuned to
mimic the native iOS keyboard (≈0.25s, cubic-bezier(0.38, 0.7, 0.125, 1)) so our
layout and the keyboard move together. (visualViewport live-tracking would be exact
but doesn't report under WKWebView's `resize: 'none'`, so this is the best signal.) */
:root.oc-capacitor-app .oc-mobile-app-shell {
height: calc(100dvh - var(--oc-keyboard-inset, 0px));
/* Reserve the bottom safe area only while the keyboard is down — when it's up the
inset cancels it out (the home indicator is hidden and the composer should sit
flush above the keyboard). The shell keeps its own bg behind this padding. */
padding-bottom: max(0px, calc(var(--oc-app-bottom-safe, 0px) - var(--oc-keyboard-inset, 0px)));
transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1),
padding-bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
}
/* Android resizes the window for the keyboard natively (no manual --oc-keyboard-inset),
so 100dvh changes instantly. Animating height against that instant resize makes the
header/content bounce on keyboard open — disable the transition on Android. */
:root.oc-capacitor-app.oc-platform-android .oc-mobile-app-shell {
transition: none;
}
/* Portal surfaces (bottom sheets, overlay panels) render at <body> level, outside
the app shell, so they don't inherit the shell's keyboard inset. They're full-
height `fixed inset-0` scrims with a bottom-anchored (`mt-auto`) sheet, so raising
their bottom edge by the keyboard height shrinks scrim + sheet together and lifts
any input above the keyboard instead of hiding it underneath. The opacity term
preserves the scrim's enter fade (Tailwind's `transition-opacity` would otherwise
be overridden by this rule's `transition` shorthand). */
:root.oc-capacitor-app .oc-keyboard-inset-surface {
bottom: var(--oc-keyboard-inset, 0px);
transition: bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1), opacity 0.2s ease-out;
}
/* Full-screen scroll views (e.g. the connect/login screen) live outside the app
shell, so shrink them by the keyboard height the same way the shell does. Capping
the height (instead of min-height: 100dvh) is what makes overflow-y-auto actually
scroll, so a field near the bottom lifts above the keyboard rather than staying
hidden behind it. min-height: 0 neutralises the Tailwind min-h-dvh baseline. */
:root.oc-capacitor-app .oc-keyboard-fill-screen {
height: calc(100dvh - var(--oc-keyboard-inset, 0px));
min-height: 0;
transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
}
/* The composer keeps its 1rem bottom padding while the keyboard is down (breathing
room above the home indicator), but that gap looks artificial sitting above the
keyboard's accessory bar — so tighten it while the keyboard is open. Animated to
match the keyboard motion. */
:root.oc-capacitor-app .oc-mobile-composer {
transition: padding-bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
}
:root.oc-capacitor-app.oc-keyboard-open .oc-mobile-composer {
padding-bottom: 6px;
}