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() {
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 (
-
+
@@ -303,7 +303,7 @@ function SelectionBar({ onEdit }: { onEdit: () => void }) { if (!selection) return null; return ( -
+