diff --git a/src/App.css b/src/App.css
index 0cf7a01..433fb2b 100644
--- a/src/App.css
+++ b/src/App.css
@@ -2503,3 +2503,101 @@ a.statusbar-legal-link {
transform: scale(1);
}
}
+
+/* ---- Responsive shell: status bar, toolbars and short screens ---- */
+
+.statusbar {
+ white-space: nowrap;
+}
+
+.statusbar-zoom {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 6px;
+ font: inherit;
+ color: var(--text-h);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.statusbar-zoom:hover:not(:disabled),
+.statusbar-zoom:focus-visible {
+ background: var(--code-bg);
+ border-color: var(--border);
+ outline: none;
+}
+
+.statusbar-zoom:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+.app-shell[data-layout='touch'] .statusbar {
+ gap: 12px;
+}
+
+.style-sheet-text-size {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 0;
+ font: 13px var(--mono);
+ color: var(--text);
+}
+
+.style-sheet-text-size .statusbar-font-slider {
+ flex: 1;
+ width: auto;
+}
+
+@media (max-width: 420px) {
+ .bottom-toolbar {
+ gap: 2px;
+ padding: 4px;
+ }
+
+ .bottom-tool-button {
+ min-width: 40px;
+ padding: 4px 2px;
+ }
+
+ .bottom-tool-label {
+ font-size: 9px;
+ }
+}
+
+/* Landscape phones and other short screens: give the canvas the height */
+@media (max-height: 500px) {
+ .app-shell[data-layout='touch'] .touch-hints {
+ display: none;
+ }
+
+ .app-shell[data-layout='touch'] .bottom-tool-button {
+ flex-direction: row;
+ min-height: 40px;
+ }
+
+ .app-shell[data-layout='touch'] .bottom-tool-label {
+ display: none;
+ }
+
+ .app-shell[data-layout='touch'] .statusbar {
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+}
+
+/* Keep the rotation slider from stretching across the status bar */
+.statusbar .canvas-rotation-control-compact {
+ flex: 0 1 260px;
+ width: auto;
+ min-width: 140px;
+}
+
+.statusbar .canvas-rotation-control-compact .canvas-rotation-slider {
+ min-width: 0;
+ flex: 1;
+}
diff --git a/src/components/BottomToolBar.tsx b/src/components/BottomToolBar.tsx
index 265d83d..3caa532 100644
--- a/src/components/BottomToolBar.tsx
+++ b/src/components/BottomToolBar.tsx
@@ -5,6 +5,7 @@ import { useAppStore } from '../store'
import BottomSheet from './BottomSheet'
import StrokeSettings from './StrokeSettings'
import CanvasRotationControl from './CanvasRotationControl'
+import TextSizeSlider from './TextSizeSlider'
export default function BottomToolBar() {
const activeTool = useAppStore((s) => s.activeTool)
@@ -65,6 +66,7 @@ export default function BottomToolBar() {
{settingsOpen && (
setSettingsOpen(false)}>
+
)}
diff --git a/src/components/CanvasArea.tsx b/src/components/CanvasArea.tsx
index 568766c..9fedb9a 100644
--- a/src/components/CanvasArea.tsx
+++ b/src/components/CanvasArea.tsx
@@ -74,10 +74,14 @@ export default function CanvasArea({
const [loadingSample, setLoadingSample] = useState(false)
const mapInputRef = useRef(null)
const lastFittedSrc = useRef(null)
+ /** True until the user zooms or pans, so resizing keeps the map fitted */
+ const autoFitRef = useRef(true)
+ const fitRequest = useAppStore((s) => s.fitRequest)
const erasingRef = useRef(false)
const commitViewport = useCallback(
(viewport: Viewport) => {
+ autoFitRef.current = false
if (zoomFrameRef.current != null) {
cancelAnimationFrame(zoomFrameRef.current)
zoomFrameRef.current = null
@@ -98,6 +102,7 @@ export default function CanvasArea({
)
const previewZoom = useCallback((viewport: Viewport) => {
+ autoFitRef.current = false
const next = clampViewport(viewport)
gestureRef.current = true
viewportRef.current = next
@@ -177,13 +182,13 @@ export default function CanvasArea({
})
}, [])
- useEffect(() => {
- if (!mapImage || size.width === 0 || size.height === 0) return
- if (lastFittedSrc.current === mapImage.src) return
- lastFittedSrc.current = mapImage.src
+ /** Scales and centres the map so it fills the canvas, keeping the current rotation. */
+ const fitToView = useCallback(() => {
+ const image = useAppStore.getState().mapImage
+ if (!image || size.width === 0 || size.height === 0) return
const rotation = useAppStore.getState().viewport.rotation
- const center = { x: mapImage.width / 2, y: mapImage.height / 2 }
- const bounds = rotatedBounds(mapImage.width, mapImage.height, rotation)
+ const center = { x: image.width / 2, y: image.height / 2 }
+ const bounds = rotatedBounds(image.width, image.height, rotation)
const scale =
Math.min(size.width / bounds.width, size.height / bounds.height) * 0.95
commitViewport({
@@ -192,7 +197,21 @@ export default function CanvasArea({
x: size.width / 2 - center.x * scale,
y: size.height / 2 - center.y * scale,
})
- }, [mapImage, size, commitViewport])
+ autoFitRef.current = true
+ }, [size, commitViewport])
+
+ // Fit a newly loaded map, and re-fit when the canvas resizes unless the user has zoomed
+ useEffect(() => {
+ if (!mapImage || size.width === 0 || size.height === 0) return
+ const mapChanged = lastFittedSrc.current !== mapImage.src
+ if (!mapChanged && !autoFitRef.current) return
+ lastFittedSrc.current = mapImage.src
+ fitToView()
+ }, [mapImage, size, fitToView])
+
+ useEffect(() => {
+ if (fitRequest > 0) fitToView()
+ }, [fitRequest, fitToView])
useEffect(() => {
const isEditableTarget = (e: KeyboardEvent) => {
diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx
index 39117b0..e0053d7 100644
--- a/src/components/StatusBar.tsx
+++ b/src/components/StatusBar.tsx
@@ -1,17 +1,13 @@
import { useState } from 'react'
-import { Minus, Plus } from 'lucide-react'
+import { Maximize2, Minus, Plus } from 'lucide-react'
import { getToolLabel } from '../config/tools'
import type { LegalDocumentId } from '../content/legal'
import type { LayoutMode } from '../hooks/useLayoutMode'
import { useAppStore } from '../store'
import type { SaveStatus } from '../saveStatus'
-import {
- ANNOTATION_TEXT_SCALE_MAX,
- ANNOTATION_TEXT_SCALE_MIN,
- ANNOTATION_TEXT_SCALE_STEP,
-} from '../utils/annotationTextScale'
import { stageRef } from '../stageRef'
import { useSaveState } from '../saveStatus'
+import TextSizeSlider from './TextSizeSlider'
import CanvasRotationControl from './CanvasRotationControl'
import LegalModal from './LegalModal'
@@ -54,10 +50,8 @@ export default function StatusBar({
const scale = useAppStore((s) => s.viewport.scale)
const setViewport = useAppStore((s) => s.setViewport)
const pointer = useAppStore((s) => s.pointer)
- const textScalePct = useAppStore((s) => s.annotationTextScalePct)
- const setAnnotationTextScalePct = useAppStore(
- (s) => s.setAnnotationTextScalePct,
- )
+ const hasMap = useAppStore((s) => s.mapImage !== null)
+ const requestFitToView = useAppStore((s) => s.requestFitToView)
const saveState = useSaveState()
const [legalDoc, setLegalDoc] = useState(null)
@@ -66,6 +60,9 @@ export default function StatusBar({
const showRotation = layoutMode !== 'touch'
const showLegalLinks = layoutMode !== 'touch'
const showCredit = layoutMode === 'desktop'
+ // Narrower layouts keep the bar to the essentials; the Style sheet holds the rest on touch
+ const showToolName = layoutMode === 'desktop'
+ const showTextSize = layoutMode !== 'touch'
const zoomBy = (direction: 1 | -1) => {
const viewport = useAppStore.getState().viewport
@@ -88,9 +85,17 @@ export default function StatusBar({
return (