diff --git a/apps/web/src/features/resume/editor/write/entry-card.tsx b/apps/web/src/features/resume/editor/write/entry-card.tsx
index 5a531c45d..5cdab8a6a 100644
--- a/apps/web/src/features/resume/editor/write/entry-card.tsx
+++ b/apps/web/src/features/resume/editor/write/entry-card.tsx
@@ -2,11 +2,13 @@ import type { MessageDescriptor } from "@lingui/core";
import type { KeyboardEvent, ReactNode } from "react";
import type { PageSettings } from "./entries";
import type { Entry, WriteSection } from "./model";
+import { useDirection } from "@base-ui/react/direction-provider";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { msg, t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
+import { AnimatePresence, m } from "motion/react";
import { useEffect, useMemo } from "react";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Collapsible, CollapsibleContent } from "@reactive-resume/ui/components/collapsible";
@@ -26,7 +28,7 @@ import { toast } from "@reactive-resume/ui/components/toast";
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft";
-import { DRAG_SETTLE } from "@/libs/motion";
+import { D3, DRAG_SETTLE, EASE, EXIT } from "@/libs/motion";
import { getCompatibleMoveTargets, getSourceSectionTitle, moveItem } from "@/libs/resume/move-item";
import { useEditorStore } from "../store";
import { EntryFields } from "./entries";
@@ -177,17 +179,20 @@ export function EntryCard({ section, entryId, index, count, page, locked, onMove
)}
- {open && isPhone && (
- deleteEntry(section, entryId)}
- onBack={() => select(null)}
- >
- {fields}
-
- )}
+
+ {open && isPhone && (
+ deleteEntry(section, entryId)}
+ onBack={() => select(null)}
+ >
+ {fields}
+
+ )}
+
);
}
@@ -204,12 +209,17 @@ type PhoneEntryScreenProps = {
/** Phones: an open entry pushes in full screen, with a back label naming its section and 44px fields. */
function PhoneEntryScreen({ section, title, locked, onBack, onDelete, children }: PhoneEntryScreenProps) {
const sectionTitle = useSectionTitle(section);
+ // Off-screen at the end edge: the screen pushes in from there and goes back there.
+ const offscreen = useDirection() === "rtl" ? "translateX(-100%)" : "translateX(100%)";
return (
-
+
);
}
diff --git a/apps/web/src/features/settings/root.tsx b/apps/web/src/features/settings/root.tsx
index 6c3783823..90dd711ed 100644
--- a/apps/web/src/features/settings/root.tsx
+++ b/apps/web/src/features/settings/root.tsx
@@ -12,6 +12,12 @@ import { orpc } from "@/libs/orpc/client";
import { themeMap } from "@/libs/theme";
import { SignOutButton } from "./account/page";
+// Phones: a section pushes in from the end and Back returns from the start (styles in index.css, after the page
+// transition). Browsers without view-transition types swap instantly, as for every other navigation.
+const supportsTransitionTypes = globalThis.CSS?.supports?.("selector(:active-view-transition-type(a))") === true;
+export const pushTransition = supportsTransitionTypes && { types: ["page", "forward"] };
+export const popTransition = supportsTransitionTypes && { types: ["page", "back"] };
+
type RowProps = { icon: IconName; label: ReactNode; value?: ReactNode };
function Row({ icon, label, value }: RowProps) {
@@ -52,13 +58,13 @@ export function SettingsRoot() {
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index f22e52ee3..4c0df7910 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -91,3 +91,40 @@ html:active-view-transition-type(page)::view-transition-old(*):only-child {
html:active-view-transition-type(page)::view-transition-new(root) {
animation: var(--d2) var(--ease) both vt-page-in;
}
+
+/*
+ * Phones, Settings: a section pushes in from the end and Back returns to the list from the start. The links add a
+ * `forward` or `back` type next to `page`, so the page rules above still fade the old page out; these replace only
+ * the new page's entry. Same specificity as the page rules, so they must stay after them.
+ */
+@keyframes vt-push-in {
+ from {
+ opacity: 0;
+ translate: var(--vt-push) 0;
+ }
+}
+
+@keyframes vt-pop-in {
+ from {
+ opacity: 0;
+ translate: calc(var(--vt-push) * -1) 0;
+ }
+}
+
+html {
+ --vt-push: 24px;
+}
+
+html[dir="rtl"] {
+ --vt-push: -24px;
+}
+
+@media (width < 640px) {
+ html:active-view-transition-type(forward)::view-transition-new(root) {
+ animation: var(--d2) var(--ease) both vt-push-in;
+ }
+
+ html:active-view-transition-type(back)::view-transition-new(root) {
+ animation: var(--d2) var(--ease) both vt-pop-in;
+ }
+}
diff --git a/apps/web/src/routes/builder/$resumeId/-components/design-panel.tsx b/apps/web/src/routes/builder/$resumeId/-components/design-panel.tsx
index 2b09f276b..68e05c2e5 100644
--- a/apps/web/src/routes/builder/$resumeId/-components/design-panel.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-components/design-panel.tsx
@@ -102,7 +102,8 @@ type SheetTab = "template" | "type" | "color" | "page";
/**
* Phones: Design is a sheet over the lower half of the page, so every change shows above it. The handle
- * raises it to full height. Tabs replace the group nav; Advanced sits under Page.
+ * raises it to full height. It keeps its full height and moves by `translate` (half out of view when lowered),
+ * and rises into view when Design opens. Tabs replace the group nav; Advanced sits under Page.
*/
export function DesignSheet() {
const locked = useIsResumeLocked();
@@ -113,8 +114,8 @@ export function DesignSheet() {
)}
+ {/* Lowered, the sheet's lower half is out of view: this keeps the end of the list scrollable into sight. */}
+ {!expanded && }
diff --git a/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx b/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
index 33de65898..ce2a6585b 100644
--- a/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
@@ -238,7 +238,7 @@ function MobileBody({ mode, onModeChange }: MobileBodyProps) {
return (