Responsive Foundation
Overview
The mobile responsive contract establishes a consistent layout baseline for the zudotext iOS build and the web build viewed on mobile browsers. It defines the shared 640 px CSS breakpoint, the separate compact-shell eligibility policy, viewport hooks, global CSS rules that prevent common mobile layout pitfalls (keyboard displacement and bounce scrolling), and platform-detection utilities. Together these pieces give UI components a stable surface to build on without per-component workarounds.
Breakpoint
The single breakpoint token lives in packages/:
:root {
--breakpoint-mobile: 640px;
}Anything at or below 640 px wide is treated as a mobile viewport. Components use this token in media queries to switch between desktop and mobile layouts. This remains the global dialog/fullscreen breakpoint; it is not the only signal used by the compact editing shell:
@media (max-width: 640px) {
/* mobile layout */
}Compact mobile shell
The compact editing shell is enabled by the editKeyBar.compactMobile preference, which defaults to true. It is eligible when either the CSS width is at most 640px, or the primary pointer is coarse and the CSS width is at most 1024px:
const compact = compactMobile &&
(width <= 640 || (primaryPointer === "coarse" && width <= 1024));Do not use this predicate to change the shared dialog breakpoint. The shell's fixed palette and its app/frame controls use the Visual Viewport API and safe-area insets. Empty directional palette slots have no overlay, so an empty corner still passes pointer events to the editor. If the visible area is too small for the requested directional footprint, the shell keeps the fixed More control and temporarily exposes the favorites from its Keys panel.
Compact mobile chrome is an opt-out, not a second editor setting: when it is false, the normal toolbar, navigation, and frame strip remain in use. When editKeyBar.enabled is false while compact mode is active, the fixed More control remains so app and frame navigation cannot become unreachable; only the directional/secondary editing keys are hidden.
The palette also follows editor focus: favorites appear while editing, and opening More tools chooses Keys for an editing session or App with no Keys selector otherwise. That choice is latched while the panel is open. App actions are icon-and-label rows; editing keys use shared icon groups. Move palette opens a modal snap-grid overlay with drag, arrow/HJKL, Shift, Enter, Esc, and reset controls.
editKeyBar.placement selects the desktop surface when compact mode is inactive: dock-top, dock-bottom, float-bar, or float-rail. Compact mode wins over all four — resolveEditKeyBarSurface returns compact-palette before it looks at placement, so a floating surface never mounts alongside the compact palette and placement never changes compact eligibility. placement syncs with workspace settings; floating positions, the snapped rail edge, and the collapsed state are local to the device/window. See Edit key bar for components, render rules, and persistence contracts.
The compact palette's keys are the one place in the app that uses the raised keycap button treatment (PaletteKeyButton's appearance="keycap"): a lighter bg-keycap fill over a --spacing-keycap-edge bottom edge that collapses and sinks when pressed, driven by both :active and aria-pressed so a latched Control chord reads as held down. prefers-reduced-motion drops the sink and keeps the color change. The desktop docked bar and the floating bar/rail deliberately keep the quiet toolbar treatment — the keycap is a touch affordance, not the app's default button. See Buttons and context menus.
Hooks
useIsMobileViewport
Imported from @takazudo/ui-components. Returns true when the viewport width is at or below MOBILE_BREAKPOINT_PX.
import { useIsMobileViewport, MOBILE_BREAKPOINT_PX } from "@takazudo/ui-components";
function MyComponent() {
const isMobile = useIsMobileViewport();
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}MOBILE_BREAKPOINT_PX is the numeric constant (640) exported alongside the hook. Use it when you need the value in JavaScript (e.g., a ResizeObserver threshold) rather than in CSS.
useKeyboardInset
Tracks the on-screen keyboard height on iOS using the Visual Viewport API and writes it to the --keyboard-inset CSS custom property on document.documentElement. This lets any element in the tree respond to the keyboard without JavaScript:
import { useKeyboardInset } from "@takazudo/ui-components";
function App() {
useKeyboardInset(); // call once near the root
return <Layout />;
}CSS usage pattern — keep interactive elements above the keyboard and above the device safe area:
.bottom-bar {
bottom: calc(var(--keyboard-inset, 0px) + var(--safe-bottom));
}var(--keyboard-inset, 0px) falls back to 0px when the keyboard is closed or the Visual Viewport API is unavailable (non-iOS browsers).
On Android the WebView itself is resized above the keyboard (see Android Build § WebView runtime), so both viewports shrink together and --keyboard-inset stays 0px — the keyboard is counted once, by the layout, never twice.
Safe-area tokens
tokens.css resolves one token per side: --safe-top, --safe-right, --safe-bottom, --safe-left. Each is var(--native-safe-area-inset-<side>, env(safe-area-inset-<side>, 0px)). A host whose WebView cannot report env() reliably (the Android edge-to-edge WebView) injects the --native-* values through installNativeInsets() (packages/); everywhere else they stay unset and env() applies unchanged. Use the tokens rather than env() directly.
CSS Base
The renderer ships these global rules in tauri-. The viewport-only rules are scoped under @media (max-width: 640px) so the desktop build is unaffected by the app's --display-scale token system.
@media (max-width: 640px) {
/* Full viewport height with dvh — avoids address-bar collapse and
kills WKWebView outer bounce on overscroll. */
html,
body {
height: 100dvh;
overscroll-behavior: none;
}
/* Prevent iOS Safari from zooming when an input is focused.
iOS only triggers auto-zoom below 16px, so the floor matters
on mobile only. */
input,
textarea,
select {
font-size: max(16px, 1rem);
}
/* Keep focused inputs visible above the on-screen keyboard. */
input,
textarea,
select {
scroll-margin-bottom: 120px;
}
}
/* Safe-area token + opt-in utility. Apply .has-safe-top to toolbars
and floating elements at the top of the screen — NOT to the editor. */
:root {
--safe-top: var(--native-safe-area-inset-top, env(safe-area-inset-top, 0px));
}
.has-safe-top {
padding-top: var(--safe-top);
}Platform Flag
Use these runtime utilities to branch behavior by host. They live in @takazudo/backend-bridge (next to the adapter / capability layer):
| Utility | Returns true when… |
|---|---|
isTauriIOS() | Running inside Tauri on an iOS device (iPhone, iPad, or iPadOS desktop-mode UA with touch points) |
isTauriMacOS() | Running inside Tauri on macOS (positively identified — Windows/Linux Tauri builds return false) |
isBrowser() | Running in a plain browser (no Tauri internals — covers the web build, the zudo-sg preview, MockAdapter, RestAdapter) |
Example — apply a mobile CSS class only in the Tauri iOS build:
import { isTauriIOS } from "@takazudo/backend-bridge";
if (isTauriIOS()) {
document.documentElement.classList.add("is-tauri-ios");
}Accessibility and viewport evidence
The Tauri and web entry points use width=device-width, initial-scale=1.0, viewport-fit=cover; they do not set maximum-scale or user-scalable=no. Pinch-zoom therefore remains available to the host. While visualViewport.scale is not 1, the editor shell retains its last natural-scale origin and size, while the palette's safe bounds continue to follow the visible viewport; the palette is clamped and can collapse to More when that visible area is too small. At natural scale, a resize or scroll event recomputes both the shell and the fixed palette coordinates.
The input font-size floor and --keyboard-inset still address the common focus/keyboard cases, but they are not evidence of an iOS or Android IME session. Unit tests and browser tests can simulate a non-zero Visual Viewport pan or resize and an offline banner; they cannot certify physical keyboard animation, IME composition, pinch gestures, or device safe-area values. Those behaviors require a real iOS or Android device run.