diff --git a/components/board/BoardCanvas.module.css b/components/board/BoardCanvas.module.css index c9403e6b..7481fe35 100644 --- a/components/board/BoardCanvas.module.css +++ b/components/board/BoardCanvas.module.css @@ -621,6 +621,15 @@ so typing/caret placement still works. The drag/resize/connection handles have no hover on touch, so surface them and enlarge them into comfortable targets. */ @media (max-width: 767px) { + /* The navbar is a fixed overlay pinned to the top (see ProjectNavbar). The + * board doesn't drive the scroll-hide, so the bar stays shown over it — push + * the whole canvas down by one bar height so its top strip isn't trapped + * behind the bar. The canvas measures its own rect for panning, so shifting + * the wrapper leaves the pan/zoom math correct. */ + .board_wrapper { + padding-top: var(--navbar-height); + } + .container { touch-action: none; /* The canvas owns all touch gestures. Without this, a double-tap or diff --git a/components/dashboard/DashboardModal.module.css b/components/dashboard/DashboardModal.module.css index 2bab6a0c..88038ab1 100644 --- a/components/dashboard/DashboardModal.module.css +++ b/components/dashboard/DashboardModal.module.css @@ -130,6 +130,12 @@ padding: 30px; background: var(--main-bg); min-height: 0; + /* Without this, a panel containing a child with a fixed minimum width wider + * than the narrow phone drawer (e.g. Keybinds' .keyArea, Layout's margin-input + * pills) forces .content past the modal's `overflow: hidden` edge, clipping the + * right gutter and the close button. min-width:0 pins .content to the drawer + * width so any overflow scrolls inside .scrollArea and the header stays aligned. */ + min-width: 0; } .contentHeader { diff --git a/components/editor/DocumentEditorPanel.tsx b/components/editor/DocumentEditorPanel.tsx index 9081cc16..d0bce908 100644 --- a/components/editor/DocumentEditorPanel.tsx +++ b/components/editor/DocumentEditorPanel.tsx @@ -26,6 +26,7 @@ import { DocumentEditorConfig, EDITOR_INPUT_ATTRIBUTES } from "@src/lib/editor/d import { useDocumentComments } from "@src/lib/editor/use-document-comments"; import { getNodeIdAtPos, transactionDeletesNode } from "@src/lib/screenplay/comment-anchors"; import { useDocumentEditor } from "@src/lib/editor/use-document-editor"; +import { focusEditorInViewport } from "@src/lib/editor/focus-in-viewport"; import { getSpellErrorAt } from "@src/lib/spellcheck/spellcheck-extension"; import type { SuggestionData } from "@components/editor/SuggestionMenu"; @@ -107,8 +108,14 @@ const DocumentEditorPanel = ({ // actively scrolling (or being dragged) and faded out shortly after, so it // never sits on top of the writing while at rest. const [showScrollThumb, setShowScrollThumb] = useState(false); - const [thumbTop, setThumbTop] = useState(0); const [canScrollThumb, setCanScrollThumb] = useState(false); + // The handle's position is written straight to the DOM (not React state) so + // tracking the scroll gesture never re-renders this (heavy) panel — that + // per-event re-render is what made the scroll-linked chrome-hide stutter + // against the finger. thumbTopRef seeds the transform when the handle + // (re)mounts; updateThumb writes it imperatively thereafter. + const scrollHandleRef = useRef(null); + const thumbTopRef = useRef(0); const scrollIdleTimer = useRef | null>(null); const isDraggingThumb = useRef(false); // The draggable scroll track element, measured to derive the handle's travel @@ -117,6 +124,10 @@ const DocumentEditorPanel = ({ // Last scrollTop, to derive scroll direction for hiding/showing the mobile // editor chrome (navbar + sidebar edge handles). const lastScrollTop = useRef(0); + // Coalesces all scroll-driven work into a single update per animation frame + // (see onScroll). A burst of scroll events then costs one layout, aligned to + // the paint cycle, so the chrome stays glued to the scroll instead of lagging. + const scrollRafRef = useRef(null); // Continuous 0→1 progress for hiding that chrome, tracked so it follows the // scroll gesture linearly rather than snapping at a threshold (see // applyChromeHide). Mirrored into the --chrome-hide CSS variable. @@ -211,19 +222,22 @@ const DocumentEditorPanel = ({ // size via a compact --display-margin-scale — no page rectangles, so nothing // shifts while writing. Paged (endless off): render the real fixed-size page — // page breaks, headers, footers, exactly like desktop — and scale the whole - // page down with `zoom` so it fits the viewport width. A fixed page rectangle - // means its boundaries never move as you type, so there are no layout shifts. - // `zoom` is a uniform visual scale and pagination is measured off-screen, so - // page count / numbering are unaffected either way. + // page down with transform: scale() so it fits the viewport width. A fixed + // page rectangle means its boundaries never move as you type, so there are no + // layout shifts. The scale is purely visual (NOT `zoom`: WebKit clamps + // zoom-shrunk fonts to a 9px rendered minimum, inflating the screenplay font + // — see the paged-mode rule in EditorPanel.module.css) and pagination is + // measured off-screen, so page count / numbering are unaffected either way. useEffect(() => { const container = containerEl; if (!container) return; const pageSize = SCREENPLAY_FORMATS[pageFormat as keyof typeof SCREENPLAY_FORMATS]; - // Only the phone paged view is zoomed. Endless reflows (no zoom); desktop + // Only the phone paged view is scaled. Endless reflows (no scale); desktop // shows the page at 1:1. if (!isPhone || isEndlessScroll || !pageSize) { container.style.removeProperty("--editor-zoom"); + container.style.removeProperty("--editor-layout-height"); return; } @@ -239,8 +253,28 @@ const DocumentEditorPanel = ({ apply(); const ro = new ResizeObserver(apply); ro.observe(container); - return () => ro.disconnect(); - }, [containerEl, isPhone, isEndlessScroll, pageFormat]); + + // transform: scale() doesn't shrink the layout box the way `zoom` did, so + // the CSS collapses the leftover (1 − scale) tail of the editor's layout + // height with a negative margin. Track the untransformed height here + // (offsetHeight ignores transforms) and expose it as a CSS var. + const editorDOM = editor?.view?.dom; + let heightObserver: ResizeObserver | undefined; + if (editorDOM) { + const applyHeight = () => { + container.style.setProperty("--editor-layout-height", `${editorDOM.offsetHeight}px`); + }; + applyHeight(); + heightObserver = new ResizeObserver(applyHeight); + heightObserver.observe(editorDOM); + } + + return () => { + ro.disconnect(); + heightObserver?.disconnect(); + container.style.removeProperty("--editor-layout-height"); + }; + }, [containerEl, isPhone, isEndlessScroll, pageFormat, editor]); // ---- Orphaned comment cleanup ---- // Comments anchor to a node's data-id. When that node is deleted the comment @@ -763,7 +797,10 @@ const DocumentEditorPanel = ({ // bottom inset keeps it off the very bottom of the screen. const HANDLE_HEIGHT = 44; const TRACK_INSET_TOP = 60; - const TRACK_INSET_BOTTOM = 24; + // Base bottom gap only; the CSS adds --safe-bottom on top, which can't be read + // here. This is just the first-frame travel-range fallback before the track + // element mounts and is measured directly, so the missing inset is harmless. + const TRACK_INSET_BOTTOM = 8; // How far the handle can travel down its track. Measured off the track element // itself, which is fixed to the viewport (see .scroll_track) so the range stays @@ -791,7 +828,10 @@ const DocumentEditorPanel = ({ return; } setCanScrollThumb(true); - setThumbTop((scrollTop / scrollable) * thumbTravel()); + const top = (scrollTop / scrollable) * thumbTravel(); + thumbTopRef.current = top; + const handle = scrollHandleRef.current; + if (handle) handle.style.transform = `translateY(${top}px)`; }, [containerEl, thumbTravel]); // Reveal the thumb and (re)arm the timer that hides it once scrolling has @@ -854,7 +894,10 @@ const DocumentEditorPanel = ({ const ed = editor; if (ed) { ed.setEditable(true); - ed.commands.focus(); + // Drop the caret on a character that's currently on screen rather + // than at the old selection, so entering edit mode doesn't scroll + // the view away to wherever the caret last was. + focusEditorInViewport(ed); } return; } @@ -867,25 +910,36 @@ const DocumentEditorPanel = ({ }, 280); }, [isPhone, mobileEditMode, isReadOnly, editor, setMobileEditMode, applyChromeHide]); - const onScroll = (e: React.UIEvent) => { + const onScroll = () => { if (suggestions.length > 0) updateSuggestions?.([]); - const el = e.currentTarget; - // Clamp to the real scroll range. iOS rubber-band overscroll reports a - // scrollTop below 0 (top) or beyond the maximum (bottom) and then springs - // back, which would otherwise feed spurious up/down deltas into the chrome - // hide and make the navbar flicker as the bounce settles. Clamping pins - // the delta to 0 while overscrolling, so the bounce leaves the bar alone. - const maxScroll = el.scrollHeight - el.clientHeight; - const scrollTop = Math.max(0, Math.min(el.scrollTop, maxScroll)); - setIsScrolled(scrollTop > 0); - if (isPhone) { + // Coalesce into one update per frame. iOS delivers scroll events on the + // main thread at an irregular cadence while the page scrolls on the + // compositor; doing the work per event (each a layout, plus a re-render) + // let the chrome drift behind the finger and stutter. Draining the latest + // scrollTop once per rAF keeps a single, paint-aligned update. + if (scrollRafRef.current != null) return; + scrollRafRef.current = requestAnimationFrame(() => { + scrollRafRef.current = null; + const el = containerEl; + if (!el) return; + // Clamp to the real scroll range. iOS rubber-band overscroll reports a + // scrollTop below 0 (top) or beyond the maximum (bottom) and then + // springs back, which would otherwise feed spurious up/down deltas + // into the chrome hide and make the navbar flicker as the bounce + // settles. Clamping pins the delta to 0 while overscrolling, so the + // bounce leaves the bar alone. + const maxScroll = el.scrollHeight - el.clientHeight; + const scrollTop = Math.max(0, Math.min(el.scrollTop, maxScroll)); + setIsScrolled(scrollTop > 0); + if (!isPhone) return; updateThumb(); revealScrollThumb(); // Accumulate scroll movement into the hide progress over - // CHROME_HIDE_RANGE px: scrolling down slides the chrome away, scrolling - // up brings it back. Snap fully open at the very top. In edit mode the - // navbar carries the exit/undo/redo controls, so keep it pinned open. + // CHROME_HIDE_RANGE px: scrolling down slides the chrome away, + // scrolling up brings it back (from anywhere in the doc). Snap fully + // open at the very top. In edit mode the navbar carries the + // exit/undo/redo controls, so keep it pinned open. const delta = scrollTop - lastScrollTop.current; if (mobileEditMode || scrollTop <= 4) { applyChromeHide(0); @@ -893,7 +947,7 @@ const DocumentEditorPanel = ({ applyChromeHide(chromeHideRef.current + delta / CHROME_HIDE_RANGE); } lastScrollTop.current = scrollTop; - } + }); }; // Drag the thumb to scroll: map vertical pointer movement onto scrollTop via @@ -938,6 +992,7 @@ const DocumentEditorPanel = ({ return () => { if (scrollIdleTimer.current) clearTimeout(scrollIdleTimer.current); if (tapTimer.current) clearTimeout(tapTimer.current); + if (scrollRafRef.current != null) cancelAnimationFrame(scrollRafRef.current); }; }, []); @@ -1031,8 +1086,9 @@ const DocumentEditorPanel = ({ )} >
diff --git a/components/editor/EditorPanel.module.css b/components/editor/EditorPanel.module.css index fed958b6..0635d477 100644 --- a/components/editor/EditorPanel.module.css +++ b/components/editor/EditorPanel.module.css @@ -134,11 +134,14 @@ position: fixed; /* --navbar-height positions the handle just below the (shown) navbar; the +60 * then clears the right sidebar edge toggle (top:8px, up to 44px tall on touch) - * plus a small gap. Bottom inset keeps it off the very bottom edge of the - * screen. Keep the +60 / bottom in sync with TRACK_INSET_TOP / - * TRACK_INSET_BOTTOM in DocumentEditorPanel (the travel-range fallback). */ + * plus a small gap. The bottom sits 8px above the safe-area inset, so the + * handle's lower end lines up with the MobileFormatToolbar (which floats 8px + * above the keyboard) and clears the home indicator instead of dipping into it. + * Keep the +60 / bottom base in sync with TRACK_INSET_TOP / TRACK_INSET_BOTTOM + * in DocumentEditorPanel (the travel-range fallback; the safe-area part is + * measured, not part of the constant). */ top: calc(var(--navbar-height) + 60px); - bottom: 24px; + bottom: calc(var(--safe-bottom) + 8px); /* Right offset matches .right_sidebar_toggle (ProjectWorkspace) so the handle * lines up horizontally with the sidebar edge toggle. */ right: 8px; @@ -210,6 +213,13 @@ * edge. Drop the reserved gutter too so the page uses the full width. */ scrollbar-gutter: auto; scrollbar-width: none; + /* The mobile navbar is a fixed overlay pinned to the top (see ProjectNavbar + * .container:not(.home_container)), so this scroller fills the full height + * *behind* it. Pad the top by one navbar height so the first line clears the + * bar at rest, while the content still scrolls up UNDER the bar and is + * revealed as it slides away — the Google-Docs reclaim. The pad is constant, + * so the scroll viewport never resizes and content tracks the finger 1:1. */ + padding-top: var(--navbar-height); } .container::-webkit-scrollbar { @@ -265,16 +275,51 @@ /* PAGED mode (endless off): render exactly like desktop — real fixed-size * pages, page breaks, headers, footers — and scale the whole page down to fit - * the viewport with `zoom` (--editor-zoom is computed in DocumentEditorPanel). - * Because the page is a fixed rectangle, its boundaries never move while - * writing, so there are no layout shifts — the tradeoff being smaller text - * than endless mode's reflow. The page keeps its canonical width (from the - * injected `.pagination` rule) and stays centred as it scales. */ + * the viewport with transform: scale() (--editor-zoom is computed in + * DocumentEditorPanel). Because the page is a fixed rectangle, its boundaries + * never move while writing, so there are no layout shifts — the tradeoff + * being smaller text than endless mode's reflow. + * + * transform, NOT `zoom`: WebKit multiplies computed font sizes by `zoom` + * (usedZoom) and then applies its "smart minimum font size" + * (minimumLogicalFontSize = 9, StyleFontSizeFunctions.cpp): any absolute + * font-size ≥ 9px whose zoomed result lands below 9px is clamped back up to + * a rendered 9px. On a phone (zoom ≈ 0.48) the 16px screenplay font came out + * as 9px rendered — reported as 9 / zoom ≈ 18.7px computed — cramming + * oversized glyphs into the fixed 16px line boxes and wrapping text + * differently than the offscreen pagination measurement. There is NO CSS + * opt-out (text-size-adjust and text autosizing are different code paths and + * do not participate). transform leaves computed font sizes at their + * canonical values — layout is pixel-identical to the measurement pass — and + * only scales at paint time, so the clamp never triggers. + * + * Unlike zoom, transform doesn't shrink the layout box, so two compensations + * are needed (both !important to beat the injected + * `.pagination { margin: 0 auto !important }`; this three-class selector + * then wins on specificity — same trick as the endless width override): + * - centring: equal negative side margins centre the canonical-width box in + * the wrapper (margin auto would pin an overflowing box to the left); + * transform-origin top center then scales it in place. + * - height: negative margin-bottom collapses the (1 − zoom) tail of the + * unscaled layout height so the scroll extent matches what is visible. + * --editor-layout-height (the editor's untransformed offsetHeight) is fed + * by a ResizeObserver in DocumentEditorPanel. */ .editor_wrapper:not(.endless_scroll) :global(.ProseMirror) { - zoom: var(--editor-zoom, 1); + transform: scale(var(--editor-zoom, 1)); + transform-origin: top center; + margin-left: calc((100% - var(--page-width)) / 2) !important; + margin-right: calc((100% - var(--page-width)) / 2) !important; + margin-bottom: calc((var(--editor-zoom, 1) - 1) * var(--editor-layout-height, 0px)) !important; } .editor_shadow { max-width: none; + /* Stick the top shadow to the navbar's current bottom edge. The scroller is + * padded by a full navbar height (see .container above), which is also the + * sticky baseline — so `top: 0` already parks the shadow right below the + * shown bar. As the fixed bar slides up (--chrome-hide 0 → 1), offset the + * sticky point up by that much so the shadow rides the bar to the very top + * instead of hanging a navbar height too low, below the edge toggles. */ + top: calc(var(--navbar-height) * var(--chrome-hide, 0) * -1); } } diff --git a/components/editor/timeline/TimelinePanel.module.css b/components/editor/timeline/TimelinePanel.module.css index d25aef85..66430164 100644 --- a/components/editor/timeline/TimelinePanel.module.css +++ b/components/editor/timeline/TimelinePanel.module.css @@ -19,6 +19,15 @@ -webkit-user-select: text; } +/* Phone: the navbar is a fixed overlay pinned to the top (see ProjectNavbar), so + * the timeline strip — first in the column — would otherwise sit behind it. Push + * it below the bar. */ +@media (max-width: 767px) { + .timeline { + margin-top: var(--navbar-height); + } +} + /* ---- Toolbar ---- */ .toolbar { diff --git a/components/navbar/ProjectNavbar.module.css b/components/navbar/ProjectNavbar.module.css index aa0d0c0a..17d6f771 100644 --- a/components/navbar/ProjectNavbar.module.css +++ b/components/navbar/ProjectNavbar.module.css @@ -57,14 +57,12 @@ border-radius: 999px; box-shadow: var(--panel-shadow); - /* Hide by sliding the pill straight up off the bar and fading in lockstep, - * both driven by --chrome-hide (0 shown → 1 hidden). The pill translates its - * FULL travel (a whole navbar height) so it keeps moving until it's gone, - * rather than collapsing in place and leaving a clipped sliver to fade out — - * translate and opacity now finish together. No transition: it tracks the - * scroll gesture directly (see applyChromeHide). On desktop these clusters - * don't render and --chrome-hide stays 0, so this is inert there. */ - transform: translateY(calc(var(--chrome-hide, 0) * -1 * var(--navbar-height))); + /* Fade the pill out as the chrome hides. The whole bar (.container) now slides + * up as one unit, so the pill no longer translates on its own — it just fades, + * in lockstep with the bar's slide, driven by --chrome-hide (0 shown → 1 + * hidden). No transition: it tracks the scroll gesture directly (see + * applyChromeHide). On desktop these clusters don't render and --chrome-hide + * stays 0, so this is inert there. */ opacity: calc(1 - var(--chrome-hide, 0)); } @@ -116,19 +114,31 @@ * clear of the bar's bottom edge. */ padding-block: 0; padding-top: var(--safe-top); - overflow: hidden; - /* Collapse the bar's height continuously as the reader scrolls down: - * --chrome-hide runs 0 (shown) → 1 (hidden), driven straight from - * scrollTop in DocumentEditorPanel. Height reclaims the space (the - * workspace is a flex column, so this reflows cleanly) and there is - * intentionally no transition — it tracks the gesture, not an ease. The - * fade lives on the pill clusters themselves (.mobile_left/.mobile_right) - * so it stays in lockstep with their slide-up rather than fading the - * whole row on a separate curve. */ - height: calc(var(--navbar-height) * (1 - var(--chrome-hide, 0))); + /* Hide the bar by sliding it straight up off the top as the reader scrolls + * down — a compositor-only transform, NOT a height collapse. --chrome-hide + * runs 0 (shown) → 1 (fully up), driven from scrollTop in + * DocumentEditorPanel; no transition so it tracks the gesture, not an ease. */ + transform: translateY(calc(var(--chrome-hide, 0) * -1 * var(--navbar-height))); + will-change: transform; } - /* Fully hidden: drop hit-testing so the collapsed sliver can't catch taps. */ + /* Project (reader) bar only — NOT the home bar (.home_container), which must + * stay in normal flow. Lift the bar OUT of the flex column into a fixed overlay + * pinned to the top. The editor scroll container then fills the full height + * behind it and pads its own top by --navbar-height (see EditorPanel + * .container), so the reader's content sits *under* the bar and is revealed as + * the bar slides away — the Google-Docs model. Because the bar no longer holds + * a slot in the column and the editor viewport never resizes, the content + * tracks the scroll 1:1 and the reclaimed space is filled by the content that + * was behind the bar, not by shifting the whole view. */ + .container:not(.home_container) { + position: fixed; + top: 0; + left: 0; + right: 0; + } + + /* Fully hidden: drop hit-testing so the off-screen bar can't catch taps. */ .container_hidden { pointer-events: none; } diff --git a/components/project/ProjectWorkspace.module.css b/components/project/ProjectWorkspace.module.css index 262c137e..516cff77 100644 --- a/components/project/ProjectWorkspace.module.css +++ b/components/project/ProjectWorkspace.module.css @@ -54,6 +54,16 @@ } } +/* Phone: the navbar is a fixed overlay (see ProjectNavbar), so panel_area now + * starts at the very top under the bar. Drop the edge toggle below the bar so it + * isn't hidden behind it at rest (it still slides off-right as the reader scrolls + * via --chrome-hide). */ +@media (max-width: 767px) { + .right_sidebar_toggle { + top: calc(var(--navbar-height) + 8px); + } +} + .right_sidebar_toggle:hover { opacity: 1; background-color: var(--tertiary-hover); diff --git a/components/project/ProjectWorkspace.tsx b/components/project/ProjectWorkspace.tsx index fffa3ebf..37b09fb9 100644 --- a/components/project/ProjectWorkspace.tsx +++ b/components/project/ProjectWorkspace.tsx @@ -4,6 +4,7 @@ import { useContext, useEffect, useState } from "react"; import { useViewContext } from "@src/context/ViewContext"; import { ProjectContext } from "@src/context/ProjectContext"; import { useActiveEditor } from "@src/lib/editor/use-active-editor"; +import { focusEditorInViewport } from "@src/lib/editor/focus-in-viewport"; import { useIsPhone } from "@src/lib/utils/hooks"; import EditorSidebarNavigation from "@components/editor/sidebar/EditorSidebarNavigation"; import EditorSidebarFormat from "@components/editor/sidebar/EditorSidebarFormat"; @@ -59,7 +60,9 @@ const ProjectWorkspace = () => { const editor = activeEditor; if (editor) { editor.setEditable(true); - editor.commands.focus(); + // Drop the caret on a character that's currently on screen rather than + // at the old selection, so the view doesn't jump when the keyboard rises. + focusEditorInViewport(editor); } }; diff --git a/components/project/SplitPanelContainer.module.css b/components/project/SplitPanelContainer.module.css index d38375ae..3d20d491 100644 --- a/components/project/SplitPanelContainer.module.css +++ b/components/project/SplitPanelContainer.module.css @@ -164,6 +164,16 @@ } } +/* Phone: the navbar is a fixed overlay (see ProjectNavbar), so the panel now + * starts at the very top under the bar. Drop the left edge handles below the bar + * so they aren't hidden behind it at rest (they still slide off-left as the + * reader scrolls via --chrome-hide). */ +@media (max-width: 767px) { + .panel_switcher_anchor { + top: calc(var(--navbar-height) + 8px); + } +} + .panel_switcher_separator { height: 1px; margin: 4px 8px; diff --git a/components/projects/stats/ProjectStatsContainer.module.css b/components/projects/stats/ProjectStatsContainer.module.css index f1c02a64..47a2aa4e 100644 --- a/components/projects/stats/ProjectStatsContainer.module.css +++ b/components/projects/stats/ProjectStatsContainer.module.css @@ -8,6 +8,16 @@ overflow-y: auto; } +/* Phone: the navbar is a fixed overlay pinned to the top (see ProjectNavbar) and + * stats don't drive the scroll-hide, so the bar stays shown over them. Pad the + * scroll root's top by one bar height so the content isn't trapped behind it. + * (Also the root for the no-stats empty state, which reuses this .container.) */ +@media (max-width: 767px) { + .container { + padding-top: var(--navbar-height); + } +} + .center_content { width: 100%; max-width: 1100px; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9b502f5d..ec5b1bf5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,8 +7,6 @@ name = "Scriptio" version = "0.0.0" dependencies = [ "log", - "objc2", - "objc2-foundation", "serde", "serde_json", "tauri", @@ -2596,7 +2594,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", "block2", - "libc", "objc2", "objc2-core-foundation", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1dadddc..3b809be7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,9 +28,3 @@ tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = "2.4.5" tauri-plugin-clipboard-manager = "2" tauri-plugin-opener = "2.5.3" - -# iOS: disable WKWebView text autosizing (see textAutosizing fix in lib.rs). -# Versions pinned to what Tauri already resolves so no extra tree is pulled. -[target.'cfg(target_os = "ios")'.dependencies] -objc2 = "0.6" -objc2-foundation = { version = "0.3", features = ["NSString", "NSValue"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bf5f3e15..73a96e51 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,36 +20,6 @@ pub fn run() { )?; } - // iOS WKWebView inflates ("text autosizing") the editor font: the - // screenplay page lays out at a fixed ~818px width and the phone CSS - // scales it down with `zoom`, but WKWebView then boosts the font by - // the column-width/viewport ratio (~2x) so 12pt renders at ~33px, - // overflowing the fixed 16px line box. CSS can't stop it here — - // `-webkit-text-size-adjust: none` is honoured in getComputedStyle - // yet ignored by the autosizer — so turn the feature off natively on - // WKPreferences via the private `textAutosizingEnabled` key (KVC). - #[cfg(target_os = "ios")] - { - use tauri::Manager; - if let Some(webview) = app.webview_windows().values().next().cloned() { - let _ = webview.with_webview(|wv| unsafe { - use objc2::msg_send; - use objc2::runtime::AnyObject; - use objc2_foundation::{NSNumber, NSString}; - - let view: *mut AnyObject = wv.inner().cast(); - if view.is_null() { - return; - } - let configuration: *mut AnyObject = msg_send![view, configuration]; - let preferences: *mut AnyObject = msg_send![configuration, preferences]; - let disabled = NSNumber::new_bool(false); - let key = NSString::from_str("textAutosizingEnabled"); - let _: () = msg_send![preferences, setValue: &*disabled, forKey: &*key]; - }); - } - } - Ok(()) }) .invoke_handler(tauri::generate_handler![some_noop_command]) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7e8abe41..6a1c889a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,38 +13,6 @@ const URL = "https://scriptio.app/"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {/* Disable WebKit text autosizing. - * - * MUST live here as a raw