diff --git a/.changeset/frontend-mobile-layout.md b/.changeset/frontend-mobile-layout.md new file mode 100644 index 0000000..d2034f7 --- /dev/null +++ b/.changeset/frontend-mobile-layout.md @@ -0,0 +1,7 @@ +--- +"frontend": patch +--- + +The wizard now fits a phone screen. It previously had no width breakpoints at all, so the fixed 210px step rail left roughly 165px of usable content on a 375px device. Below 700px the rail becomes a horizontal top bar — the same nodes and connector rotated a quarter turn, each label under its node — so every step stays visible without hiding navigation behind a menu. The remaining layouts follow: the landing cards stack and scroll from the top instead of centering and clipping on a short screen, the JSON preview drawer takes the full width now that there is no side rail to sit beside, the Variables description/type pair and the Authors paired fields drop to one column, picker and action rows wrap, and the JSON viewer's per-level indent tightens so deep nesting no longer runs off the screen. + +Form controls are also held at 16px on small screens: iOS Safari auto-zooms whenever a focused control is smaller than that, and every field here was around 14px, so tapping one zoomed the page in and left it scrolled sideways. diff --git a/packages/frontend/src/components/AppShell.module.css b/packages/frontend/src/components/AppShell.module.css index 4f8c2db..fe84c27 100644 --- a/packages/frontend/src/components/AppShell.module.css +++ b/packages/frontend/src/components/AppShell.module.css @@ -11,6 +11,23 @@ background-color: var(--c-bg); } +/* Phone: the sidebar becomes a top bar (see Sidebar.module.css), so the shell + stacks and the content takes the full width. `min-height: 0` lets the content + scroll inside the shell rather than pushing it taller than the viewport — + without it a flex column child refuses to shrink below its content height. */ +@media (max-width: 700px) { + .shell { + flex-direction: column; + } + + .content { + min-height: 0; + /* Bottom padding clears the fixed Preview pill so it can't cover the last + control on the page (usually Continue). */ + padding: 1.5rem 1rem 4.5rem; + } +} + .previewPill { position: fixed; bottom: 1.5rem; @@ -30,6 +47,15 @@ transition: border-color 0.15s, color 0.15s, background-color 0.15s; } +/* Phone: tuck the pill closer into the corner. This has to sit *after* the base + rule above — same specificity, so source order is what decides. */ +@media (max-width: 700px) { + .previewPill { + bottom: 1rem; + right: 1rem; + } +} + .previewPill:hover { color: var(--c-ink); border-color: var(--c-ink-3); diff --git a/packages/frontend/src/components/JsonViewer.module.css b/packages/frontend/src/components/JsonViewer.module.css index 23cc0ce..505cd6c 100644 --- a/packages/frontend/src/components/JsonViewer.module.css +++ b/packages/frontend/src/components/JsonViewer.module.css @@ -19,6 +19,19 @@ padding-left: 1.5rem; } +/* Phone: 1.5rem per nesting level eats a 375px screen within a few levels, so + tighten the indent (and the gutters) rather than force sideways scrolling. */ +@media (max-width: 700px) { + .viewer { + padding: 0.85rem 0.75rem; + font-size: 0.78rem; + } + + .children { + padding-left: 0.9rem; + } +} + /* Syntax token colors — light mode (jsPsych logo palette, darkened for contrast on white) */ .key { color: #006838; } /* logo dark green — 7:1 on white */ .str { color: #b06210; } /* orange-amber (hue ~31°) — 4.5:1 on white */ diff --git a/packages/frontend/src/components/PreviewDrawer.module.css b/packages/frontend/src/components/PreviewDrawer.module.css index 980638e..42375bb 100644 --- a/packages/frontend/src/components/PreviewDrawer.module.css +++ b/packages/frontend/src/components/PreviewDrawer.module.css @@ -23,6 +23,16 @@ background: rgba(0, 0, 0, 0.35); } +/* Phone: there's no side rail to sit beside, so the drawer takes the screen. + (The --sidebar-w allowance above is meaningless once the nav is a top bar.) */ +@media (max-width: 700px) { + .drawer { + width: 100%; + max-width: 100%; + border-left: none; + } +} + @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } diff --git a/packages/frontend/src/components/Sidebar.module.css b/packages/frontend/src/components/Sidebar.module.css index c50a3fd..3272788 100644 --- a/packages/frontend/src/components/Sidebar.module.css +++ b/packages/frontend/src/components/Sidebar.module.css @@ -216,6 +216,79 @@ :global([data-theme="dark"]) .psychdsLink { color: var(--c-ink-2); } :global([data-theme="dark"]) .psychdsLink:hover { color: var(--c-forest-bright); } +/* ─── Phone: the step nav becomes a top bar ────────────────────────────────── + A 210px side rail leaves ~165px of content on a 375px screen, so below 700px + the nav turns horizontal and sits above the content: the same nodes and + connector, rotated a quarter turn, with each label under its node. + Progress stays visible at all times — no hamburger, nothing hidden. */ + +@media (max-width: 700px) { + .sidebar { + width: 100%; + min-width: 0; + padding: 0.75rem 0 0.5rem; + border-right: none; + border-bottom: 1px solid var(--c-border-sub); + } + + .header { + padding: 0 1rem 0.75rem; + } + + /* Steps run left-to-right, each an equal-width column (node above label). */ + .stepList { + flex-direction: row; + flex: 0 0 auto; + gap: 0; + padding: 0 0.5rem; + } + + .stepList li { + flex: 1 1 0; + min-width: 0; + } + + .step { + flex-direction: column; + gap: 0.3rem; + padding: 0.4rem 0.15rem; + font-size: 0.68rem; + line-height: 1.25; + text-align: center; + } + + /* The connector rotates with the flow: it now runs from this node's center + across to the next node's. Equal-width columns make width:100% land exactly + on the next node, the same trick the vertical rule uses with its height. */ + .step::before { + left: 50%; + top: calc(0.4rem + 11px); /* step padding-top + node radius = node center */ + width: 100%; + height: 2px; + transform: none; + } + + /* Footer flattens into a single row under the steps so it costs one line of + height instead of two stacked blocks. */ + .footer { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.6rem 1rem 0; + margin-top: 0.4rem; + } + + .psychdsLink { + padding: 0; + } + + .startOver { + width: auto; + flex-shrink: 0; + } +} + /* Start over confirmation modal — native for free focus trap + Escape */ .dialog { @@ -223,7 +296,9 @@ border: 1px solid var(--c-border); border-radius: 12px; padding: 1.75rem; - width: 320px; + /* Never wider than the viewport it's centred in (320px overflows a 375px + phone once the UA's dialog margins are counted). */ + width: min(320px, calc(100vw - 2rem)); display: flex; flex-direction: column; gap: 0.75rem; diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css index cc60063..352efc4 100644 --- a/packages/frontend/src/index.css +++ b/packages/frontend/src/index.css @@ -236,6 +236,23 @@ a:hover { outline-offset: 2px; } +/* Phone: iOS Safari auto-zooms the page whenever a focused form control has a + font-size under 16px — every field here is ~14px, so tapping one would zoom + in and leave the layout scrolled sideways with no way back short of a pinch. + Holding the controls at 16px on small screens is the standard prevention. + Only the controls you actually type into: a checkbox or radio can't be zoomed + into, and forcing a font-size on one only risks disturbing a styled control + that sizes itself from it. !important because each page's CSS module sets its + own font-size on these controls by class, which outranks a bare element + selector. */ +@media (max-width: 700px) { + input:not([type="checkbox"]):not([type="radio"]), + select, + textarea { + font-size: 16px !important; + } +} + /* Respect users who ask for less motion: collapse transitions and the preview drawer's slide-in to a near-instant, non-animated state change. */ @media (prefers-reduced-motion: reduce) { diff --git a/packages/frontend/src/pages/Authors.module.css b/packages/frontend/src/pages/Authors.module.css index 9692cd1..21c5569 100644 --- a/packages/frontend/src/pages/Authors.module.css +++ b/packages/frontend/src/pages/Authors.module.css @@ -410,3 +410,26 @@ .continueBtn:hover { background-color: var(--c-cta-hover); } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── */ + +@media (max-width: 700px) { + /* Paired fields (name/email, etc.) stack — two columns on a phone leaves each + input too narrow to read what's typed in it. */ + .fieldRow { + grid-template-columns: 1fr; + gap: 0.7rem; + } + + .card { + padding: 0.85rem 0.85rem 0.75rem; + } + + .bulkActions { + flex-wrap: wrap; + } + + .continueBtn { + width: 100%; + } +} diff --git a/packages/frontend/src/pages/DataUpload.module.css b/packages/frontend/src/pages/DataUpload.module.css index c7daedb..de2312e 100644 --- a/packages/frontend/src/pages/DataUpload.module.css +++ b/packages/frontend/src/pages/DataUpload.module.css @@ -485,3 +485,42 @@ .replaceConfirmYes:hover { filter: brightness(0.94); } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── */ + +@media (max-width: 700px) { + /* Button + "or" + folder name is too much for one phone line; let it wrap, + and let the (ellipsised) folder name shrink rather than push the row wide. */ + .pickerRow { + flex-wrap: wrap; + gap: 0.6rem 0.85rem; + } + + .folderName { + min-width: 0; + flex: 1 1 100%; + } + + /* Action rows keep their spacing but drop to a second line when needed. */ + .joinKeyActions, + .replaceConfirmBtns { + flex-wrap: wrap; + gap: 0.75rem 1rem; + } + + /* Size the lists to the viewport rather than to a fixed 220/260px. In portrait + that lands near the desktop cap; in landscape, where 40% of the height is + well under it, the list gives the rest of the page room instead of filling + the screen. dvh so the retractable URL bar doesn't inflate it; the vh line + is the fallback for browsers without dvh. */ + .fileList, + .statusList { + max-height: 40vh; + max-height: 40dvh; + } + + .continueBtn, + .processBtn { + width: 100%; + } +} diff --git a/packages/frontend/src/pages/Landing.module.css b/packages/frontend/src/pages/Landing.module.css index b3d2a28..7c66625 100644 --- a/packages/frontend/src/pages/Landing.module.css +++ b/packages/frontend/src/pages/Landing.module.css @@ -180,3 +180,40 @@ padding: 0.1em 0.3em; border-radius: 3px; } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── + Two 260px cards side by side need 544px, so they stack. Once stacked the + screen can no longer centre everything vertically without clipping the top on + a short phone, so the column scrolls from the top instead. */ + +@media (max-width: 700px) { + .landing { + justify-content: flex-start; + overflow-y: auto; + padding: 2rem 1rem; + } + + .logo { + height: 44px; + } + + .title { + font-size: 1.75rem; + } + + .description { + font-size: 0.95rem; + } + + .cards { + flex-direction: column; + gap: 1rem; + width: 100%; + max-width: 400px; + } + + .card { + width: 100%; + padding: 1.25rem; + } +} diff --git a/packages/frontend/src/pages/ProjectInfo.module.css b/packages/frontend/src/pages/ProjectInfo.module.css index a1b4201..ab24306 100644 --- a/packages/frontend/src/pages/ProjectInfo.module.css +++ b/packages/frontend/src/pages/ProjectInfo.module.css @@ -401,3 +401,37 @@ .continueBtn:hover { background-color: var(--c-cta-hover); } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── */ + +@media (max-width: 700px) { + .form { + gap: 1.25rem; + } + + /* "Upload JSON" + its hint stop sharing one line. */ + .uploadRow { + flex-wrap: wrap; + gap: 0.5rem 0.75rem; + } + + /* The 4.5rem label gutter leaves too little for the value, so the conflict + rows read as label-above-value instead. */ + .conflictDetailRow { + flex-direction: column; + gap: 0.15rem; + } + + .conflictDetailLabel { + width: auto; + } + + .conflictBtns { + flex-wrap: wrap; + } + + .continueBtn { + align-self: stretch; + width: 100%; + } +} diff --git a/packages/frontend/src/pages/Review.module.css b/packages/frontend/src/pages/Review.module.css index b47562b..ea01727 100644 --- a/packages/frontend/src/pages/Review.module.css +++ b/packages/frontend/src/pages/Review.module.css @@ -246,3 +246,20 @@ white-space: pre-wrap; word-break: break-word; } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── */ + +@media (max-width: 700px) { + /* Full-width so the primary action is an easy thumb target. */ + .downloadBtn, + .saveJsonBtn, + .validateBtn { + align-self: stretch; + width: 100%; + text-align: center; + } + + .validatorNote { + padding: 0.85rem 0.9rem; + } +} diff --git a/packages/frontend/src/pages/Variables.module.css b/packages/frontend/src/pages/Variables.module.css index 562cffb..c83b990 100644 --- a/packages/frontend/src/pages/Variables.module.css +++ b/packages/frontend/src/pages/Variables.module.css @@ -374,3 +374,42 @@ .continueBtn:hover { background-color: var(--c-cta-hover); } + +/* ─── Phone ─────────────────────────────────────────────────────────────────── */ + +@media (max-width: 700px) { + /* Subtext and the "show system variables" switch stop competing for one line. */ + .introRow { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + margin-bottom: 1.25rem; + } + + /* A long column name plus its badges no longer forces the row wider than the + screen — the name wraps and the badges keep their own line if they must. */ + .rowHeader { + flex-wrap: wrap; + gap: 0.4rem 0.6rem; + padding: 0.6rem 0.75rem; + } + + .rowName { + min-width: 0; + overflow-wrap: anywhere; + } + + /* Description over type, rather than a 140px column squeezing both. */ + .bodyMain { + grid-template-columns: 1fr; + gap: 0.85rem; + } + + .rowBody { + padding: 0.6rem 0.75rem 0.85rem; + } + + .continueBtn { + width: 100%; + } +} diff --git a/website/src/pages/contact.module.css b/website/src/pages/contact.module.css index 6ae7590..a017e73 100644 --- a/website/src/pages/contact.module.css +++ b/website/src/pages/contact.module.css @@ -3,9 +3,18 @@ align-items: center; justify-content: center; min-height: calc(100vh - var(--ifm-navbar-height) - 6rem); + /* dvh so the retractable URL bar on mobile doesn't push the page taller than + the screen; the vh line above is the fallback. */ + min-height: calc(100dvh - var(--ifm-navbar-height) - 6rem); padding: 3rem 1.5rem; } +@media (max-width: 700px) { + .wrap { + padding: 2rem 1.15rem; + } +} + .inner { max-width: 720px; text-align: left; diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css index 6e3becb..f01b8d2 100644 --- a/website/src/pages/index.module.css +++ b/website/src/pages/index.module.css @@ -232,3 +232,55 @@ font-size: 0.95rem; margin: 0; } + +/* --- Phone -------------------------------------------------------------- */ + +/* The 900px step above already stacks the hero; this one is about the phone + itself — trimming the outer gutters and the display type, which still + overhangs a 375px screen at 2.4rem. */ +@media (max-width: 600px) { + .hero { + padding: 2.25rem 1.15rem 1.5rem; + } + + .heroLogo { + height: 60px; + margin-bottom: 1.15rem; + } + + .heroTitle { + font-size: 1.9rem; + letter-spacing: -0.02em; + } + + .heroLede { + font-size: 1.05rem; + } + + .pathCard { + padding: 1.15rem; + } + + /* Side-by-side button + link would leave the button too narrow to read, so + they stack full-width — the link centres to stay aligned with the button + above it rather than hanging off its left edge. */ + .pathCardActions { + align-items: stretch; + flex-direction: column; + gap: 0.85rem; + } + + .pathLink { + text-align: center; + } + + .features, + .showcase { + padding-left: 1.15rem; + padding-right: 1.15rem; + } + + .showcaseCard { + padding: 1.25rem 1.15rem; + } +} diff --git a/website/src/pages/wizard.module.css b/website/src/pages/wizard.module.css index 2f9e791..85b59de 100644 --- a/website/src/pages/wizard.module.css +++ b/website/src/pages/wizard.module.css @@ -6,5 +6,20 @@ 4.5rem is the footer's single-row height; it wraps taller on narrow screens, where a small scroll is fine. */ height: calc(100vh - var(--ifm-navbar-height) - 4.5rem); + /* Mobile browsers count their retractable URL bar in 100vh, so the frame + would hang below the fold until the bar hides. dvh tracks the bar as it + moves; the vh line above stays as the fallback for browsers without it. */ + height: calc(100dvh - var(--ifm-navbar-height) - 4.5rem); border: 0; } + +/* Phone: the footer wraps to three rows here, so the 4.5rem single-row + allowance is far too small — subtracting it would still leave the frame + overhanging. Give the wizard the whole viewport below the navbar and let the + footer live below the fold; the wizard scrolls internally either way. */ +@media (max-width: 700px) { + .wizardFrame { + height: calc(100vh - var(--ifm-navbar-height)); + height: calc(100dvh - var(--ifm-navbar-height)); + } +} diff --git a/website/src/theme/Footer/styles.module.css b/website/src/theme/Footer/styles.module.css index 17da2c3..30686ce 100644 --- a/website/src/theme/Footer/styles.module.css +++ b/website/src/theme/Footer/styles.module.css @@ -64,3 +64,23 @@ color: var(--ifm-color-primary); text-decoration: none; } + +/* Phone: the row already wraps, but a 1.75rem link gap plus space-between + scatters the wrapped items across the width. Centre everything into a tidy + stack and close the gaps up. */ +@media (max-width: 700px) { + .footer { + padding: 1.15rem 1.15rem; + } + + .inner { + justify-content: center; + text-align: center; + gap: 0.9rem 1.25rem; + } + + .links { + justify-content: center; + gap: 0.75rem 1.25rem; + } +}