Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions components/dashboard/DashboardModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@
color: var(--primary-text);
}

/* Slide the drawer in from the right on phone so it reads as the same sidebar
* the burger menu opened from, rather than popping in place. */
/* Slide the drawer in from the right on phone when it genuinely opens (the home
* Settings button, a tool sheet's link) rather than popping in place. */
@keyframes dashboardDrawerIn {
from {
transform: translateX(100%);
Expand All @@ -260,10 +260,10 @@
}
}

/* Phone: the sidebar nav lives in the burger menu, so the modal drops it and
* renders as a right-side drawer matching that menu (same width and position
* below the navbar) instead of a full-screen pane. Tabs are switched from the
* burger, which re-opens the dashboard on the chosen tab. Content is tightened
/* Phone: on a project the sidebar nav lives in the burger menu, so the modal
* drops it and renders as a right-side drawer matching that menu (same width and
* position below the navbar) instead of a full-screen pane. Tabs are switched from
* the burger, which swaps this drawer in for it on the chosen tab. Content is tightened
* and slightly scaled down so the settings sit comfortably in the narrow width. */
@media (max-width: 767px) {
.overlay {
Expand Down Expand Up @@ -291,6 +291,14 @@
animation: dashboardDrawerIn 0.25s ease;
}

/* Swapped in for the burger menu, which sits in this exact rect: the drawer
* is already on screen, so it must not slide in again — only its content
* changes. Same in-place replacement the sections list ⇄ section switch does
* below (see [DashboardContext].drawerSwap). */
.modal.instant {
animation: none;
}

/* The nav sidebar and the content share the drawer, one screen at a time.
* Default screen is content; .mobileSections flips to the sections list (used
* when the dashboard is opened without a burger menu behind it — e.g. the
Expand Down
11 changes: 7 additions & 4 deletions components/dashboard/DashboardModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import DashboardAuth from "./account/DashboardAuth";
import AboutSettings from "./AboutSettings";

const DashboardModal = () => {
const { isOpen, closeDashboard, activeTab, setActiveTab, openedFromMenu, setMobileMenuOpen } =
const { isOpen, closeDashboard, activeTab, setActiveTab, openedFromMenu, swapDrawerScreen, drawerSwap } =
useContext(DashboardContext);
const t = useTranslations("modal");
const tSidebar = useTranslations("sidebar");
Expand All @@ -40,10 +40,13 @@ const DashboardModal = () => {
// so preferences/account tabs are reachable, and the back arrow returns to it.
const [mobileShowSections, setMobileShowSections] = useState(false);

// Either way this is the same gesture — step back to the list of sections —
// so it looks the same either way: the drawer's content is replaced, with no
// slide. Only the list itself differs (the burger menu in a project, this
// modal's own sidebar elsewhere).
const handleBack = () => {
if (openedFromMenu) {
closeDashboard();
setMobileMenuOpen(true);
swapDrawerScreen("menu");
} else {
setMobileShowSections(true);
}
Expand Down Expand Up @@ -133,7 +136,7 @@ const DashboardModal = () => {
return (
<div className={styles.overlay} onClick={closeDashboard}>
<div
className={`${styles.modal} ${isPhone && mobileShowSections ? styles.mobileSections : ""}`}
className={`${styles.modal} ${isPhone && mobileShowSections ? styles.mobileSections : ""} ${drawerSwap ? styles.instant : ""}`}
onClick={(e) => e.stopPropagation()}
>
<SidebarMenu structure={menuStructure} activeTab={activeTab} onTabChange={handleTabChange} />
Expand Down
16 changes: 0 additions & 16 deletions components/editor/EditorPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,6 @@
display: block;
}

@media (pointer: coarse) {
.container,
.editor_wrapper,
.page_shift {
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}

.container :global(.ProseMirror) {
-webkit-user-select: text;
user-select: text;
-webkit-touch-callout: default;
}
}

.editor_shadow {
position: sticky;
top: 0;
Expand Down
33 changes: 20 additions & 13 deletions components/editor/sidebar/EditorSidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,17 @@
overflow: hidden;
}

.collapsed .element {
visibility: hidden;
}

/* NB: this module has no `.element` rule of its own on purpose. EditorSidebarFormat
* renders the *navigation* module's `.element` inside this module's
* `.sidebar_content`, and CSS Modules scope the two independently — so a
* `.collapsed .element` selector written here could never match the panel it was
* aimed at, and this drawer went on rendering its full contents while shut.
* `visibility` inherits, so hiding the drawer itself covers whatever panel the
* markup puts inside it. */
.collapsed {
width: 0;
padding: 0;
}

.element {
background-color: var(--editor-sidebar);
padding-top: 10px;
padding-bottom: 38px;
border-radius: 35px;
box-shadow: var(--panel-shadow);
visibility: hidden;
}

/* Phone: the format sidebar becomes a right overlay drawer, mirroring the
Expand All @@ -53,7 +49,11 @@
/* No drawer background — only the rounded .element panel shows, floating
* over the (backdrop-dimmed) editor, matching the desktop look. */
transform: translateX(0);
transition: transform 0.25s ease;
/* No delay on the visibility flip coming back in: the drawer has to be
* rendered before it can slide into view. */
transition:
transform 0.25s ease,
visibility 0s;
pointer-events: auto;
}

Expand All @@ -62,6 +62,13 @@
padding: 12px;
transform: translateX(100%);
pointer-events: none;
/* Hold the hide until the slide-out has played, then stop rendering the
* drawer entirely — see the matching block in
* EditorSidebarNavigation.module.css for why an off-screen translate is
* not enough on its own. */
transition:
transform 0.25s ease,
visibility 0s linear 0.25s;
}
}

Expand Down
40 changes: 37 additions & 3 deletions components/editor/sidebar/EditorSidebarNavigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@
/* No drawer background — only the rounded .element panel shows, floating
* over the (backdrop-dimmed) editor, matching the desktop look. */
transform: translateX(0);
transition: transform 0.25s ease;
/* visibility flips back with no delay on the way in: the drawer has to be
* rendered before it can slide into view. */
transition:
transform 0.25s ease,
visibility 0s;
pointer-events: auto;
}

Expand All @@ -79,11 +83,41 @@
padding: 12px;
transform: translateX(-100%);
pointer-events: none;
/*
* Stop rendering the drawer once it has finished sliding out.
*
* Translating it off-screen is not enough on iOS. Running the transform
* transition promotes this fixed, full-height element to its own
* compositing layer with a real backing store (the panel, its text, its
* box-shadow), and WebKit keeps that layer after the transition ends —
* a `position: fixed` element with a non-identity transform and a
* declared transition is exactly what it holds on to. From then on the
* layer sits permanently over the panel area at z-index 60, so every
* pan of the board and every scroll of the editor has to recomposite
* against it: smooth until the drawer is first opened, unusable after.
*
* `visibility: hidden` drops it out of rendering entirely and the layer
* with it, while leaving the subtree laid out — the marker gutter is
* measured against the scene list with getBoundingClientRect (see
* EditorSidebarNavigation), which `display: none` would zero.
*
* The 0.25s delay is what lets the slide-out still be seen: visibility
* is not interpolated, so it simply holds `visible` for the length of the
* transform and then flips. The format drawer on the right needs the same
* treatment and now carries it (EditorSidebar.module.css).
*/
visibility: hidden;
transition:
transform 0.25s ease,
visibility 0s linear 0.25s;
}

/* Defeat the desktop hide above so the slide-out is actually seen. */
/* Follow the drawer's own delayed visibility rather than the desktop rule's
* immediate hide, so the slide-out is actually seen. Must be `inherit` and
* not `visible`: an explicit `visible` here would keep the panel rendered
* after the drawer above it has gone hidden, which is the whole problem. */
.collapsed .element {
visibility: visible;
visibility: inherit;
}
}

Expand Down
30 changes: 14 additions & 16 deletions components/navbar/ProjectNavbarMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ const ProjectNavbarMobile = () => {
const tSidebar = useTranslations("sidebar");

const {
openDashboard,
closeDashboard,
isDashboardOpen,
mobileMenuOpen,
setMobileMenuOpen,
swapDrawerScreen,
drawerSwap,
membership,
userCtx,
canUploadToCloud,
Expand Down Expand Up @@ -297,7 +298,11 @@ const ProjectNavbarMobile = () => {
</nav>

{isInProject && projectId && (
<ProjectNavbarMobileMenu isOpen={mobileMenuOpen} onClose={() => setMobileMenuOpen(false)}>
<ProjectNavbarMobileMenu
isOpen={mobileMenuOpen}
instant={drawerSwap}
onClose={() => setMobileMenuOpen(false)}
>
<div className={mobileMenu.title_row}>
{membership ? (
<StatusIndicator />
Expand Down Expand Up @@ -329,19 +334,18 @@ const ProjectNavbarMobile = () => {
<div className={mobileMenu.separator} />

{/* Dashboard settings — the modal drops its sidebar on phone, so
its grouped tabs are navigated from here; each opens the
dashboard directly on that tab. */}
this menu *is* its sections list; each row swaps the dashboard
drawer in for this one on that tab, in place and without a
slide, the way the home dashboard swaps its own list for a
section (see [DashboardContext].swapDrawerScreen). */}
{dashboardMenu.map((section) => (
<div key={section.group} className={mobileMenu.section}>
<div className={mobileMenu.group_label}>{section.group}</div>
{section.items.map((tab) => (
<button
key={tab.id}
className={mobileMenu.item}
onClick={() => {
openDashboard(tab.id, { fromMenu: true });
setMobileMenuOpen(false);
}}
onClick={() => swapDrawerScreen("dashboard", tab.id)}
>
{tab.icon}
<span>{tab.label}</span>
Expand Down Expand Up @@ -369,10 +373,7 @@ const ProjectNavbarMobile = () => {
) : (
<button
className={mobileMenu.item}
onClick={() => {
openDashboard("Auth", { fromMenu: true });
setMobileMenuOpen(false);
}}
onClick={() => swapDrawerScreen("dashboard", "Auth")}
>
<LogIn size={18} />
<span>{tSidebar("auth")}</span>
Expand All @@ -381,10 +382,7 @@ const ProjectNavbarMobile = () => {

<button
className={mobileMenu.item}
onClick={() => {
openDashboard("About", { fromMenu: true });
setMobileMenuOpen(false);
}}
onClick={() => swapDrawerScreen("dashboard", "About")}
>
<Info size={18} />
<span>{tModal("tabs.About")}</span>
Expand Down
24 changes: 18 additions & 6 deletions components/navbar/ProjectNavbarMobileMenu.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px calc(12px + var(--safe-right)) calc(20px + var(--safe-bottom)) 12px;
/* Same horizontal gutter as the dashboard drawer's --drawer-gutter: picking a
* section swaps that drawer in for this one in place, so the close button and
* the row labels must land on the same lines on both screens or they visibly
* jump across the swap. */
padding: 12px calc(16px + var(--safe-right)) calc(20px + var(--safe-bottom)) 16px;
overflow-y: auto;

background-color: var(--main-bg);
Expand All @@ -36,6 +40,14 @@
transform: translateX(100%);
}

/* Handing over to (or taking back from) the dashboard drawer, which covers this
* exact rect: sliding out would only animate something nobody can see, and would
* drag this drawer's shadow across the strip beside it. Snap instead, so the two
* read as one drawer replacing its content. See [DashboardContext].drawerSwap. */
.drawer_instant {
transition: none;
}

.header {
display: flex;
flex-direction: row;
Expand All @@ -44,8 +56,8 @@
gap: 8px;
margin-bottom: 8px;
/* Match the left inset of the group labels / items / title row below (all
* padding-left: 10px) so the "Menu" title lines up with them. */
padding-left: 10px;
* padding-left: 12px) so the "Menu" title lines up with them. */
padding-left: 12px;
}

.header_title {
Expand Down Expand Up @@ -84,7 +96,7 @@
align-items: center;
gap: 14px;
width: 100%;
padding: 9px 10px;
padding: 9px 12px;
border: none;
border-radius: 10px;
background: none;
Expand All @@ -101,7 +113,7 @@

/* Group heading above a run of dashboard tabs (Project / Preferences / Account). */
.group_label {
padding: 10px 10px 2px;
padding: 10px 12px 2px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
Expand All @@ -128,7 +140,7 @@
flex-direction: row;
align-items: center;
gap: 10px;
padding: 6px 10px 12px;
padding: 6px 12px 12px;
}

.title_input {
Expand Down
16 changes: 14 additions & 2 deletions components/navbar/ProjectNavbarMobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import menu from "./ProjectNavbarMobileMenu.module.css";
interface ProjectNavbarMobileMenuProps {
isOpen: boolean;
onClose: () => void;
/**
* Skip the slide when the dashboard drawer is taking this drawer's place (or
* handing it back): the two share the same rect, so the swap has to read as one
* drawer changing screens — see [DashboardContext].drawerSwap.
*/
instant?: boolean;
children: ReactNode;
}

Expand All @@ -25,7 +31,7 @@ interface ProjectNavbarMobileMenuProps {
* above the editor's floating chrome (scroll drag-handle, edit FAB) no matter its
* z-index. At the body level its z-index applies against the whole page instead.
*/
const ProjectNavbarMobileMenu = ({ isOpen, onClose, children }: ProjectNavbarMobileMenuProps) => {
const ProjectNavbarMobileMenu = ({ isOpen, onClose, instant, children }: ProjectNavbarMobileMenuProps) => {
const t = useTranslations("navbar");

// Portals need `document`, which doesn't exist during SSR — render nothing
Expand All @@ -38,7 +44,13 @@ const ProjectNavbarMobileMenu = ({ isOpen, onClose, children }: ProjectNavbarMob
return createPortal(
<>
{isOpen && <div className={menu.backdrop} onClick={onClose} />}
<div className={join(menu.drawer, !isOpen ? menu.drawer_closed : "")}>
<div
className={join(
menu.drawer,
!isOpen ? menu.drawer_closed : "",
instant ? menu.drawer_instant : "",
)}
>
<div className={menu.header}>
<span className={menu.header_title}>{t("menu")}</span>
<button className={menu.close_btn} onClick={onClose} aria-label={t("close")}>
Expand Down
Loading
Loading