n.
diff --git a/apps/web/src/features/homepage/languages-showcase.tsx b/apps/web/src/features/homepage/languages-showcase.tsx
index 31f7de866..3f5463cbf 100644
--- a/apps/web/src/features/homepage/languages-showcase.tsx
+++ b/apps/web/src/features/homepage/languages-showcase.tsx
@@ -30,7 +30,7 @@ const headingClass = `${wordClass} mb-[15px] font-semibold text-[13px] leading-[
const paperClass =
"language-paper relative mx-auto w-full max-w-[440px] rotate-2 rounded border border-[#e0dcd5] bg-[#efede7] font-[Arial,Noto_Sans,sans-serif] text-[#29272b] text-start shadow-[4px_5px_0_#d5d0c8,5px_6px_0_#77716b,11px_16px_0_-4px_#b9b1a7,10px_28px_42px_#0005]";
const optionClass =
- "group/option flex min-h-[62px] items-center justify-between gap-2 rounded border border-[#454345] bg-[#222224] px-[17px] py-[14px] text-[#e3dfda] text-[clamp(16px,1.6vw,21px)] shadow-[0_3px_0_#101011] transition-[background,color,transform,box-shadow] duration-[160ms] ease-[ease] hover:not-aria-pressed:-translate-y-[2px] hover:not-aria-pressed:bg-[#343031] hover:not-aria-pressed:shadow-[0_5px_0_#101011] focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-(--home-accent) active:translate-y-[2px] active:shadow-[0_1px_0_#101011] aria-pressed:translate-y-[2px] aria-pressed:border-[#d0b9a5] aria-pressed:bg-[#d0b9a5] aria-pressed:text-[#24201d] aria-pressed:shadow-[0_1px_0_#101011] group-data-[instant=true]/languages:transition-none max-[420px]:min-h-[56px] max-[420px]:px-[11px] max-[420px]:py-[10px] max-[420px]:text-[16px]";
+ "group/option flex min-h-[62px] items-center justify-between gap-2 rounded border border-[#454345] bg-[#222224] px-[17px] py-[14px] text-[#e3dfda] text-[clamp(16px,1.6vw,21px)] shadow-[0_3px_0_#101011] transition-[background-color,color,translate,box-shadow] duration-[160ms] ease-[ease] hover:not-aria-pressed:-translate-y-[2px] hover:not-aria-pressed:bg-[#343031] hover:not-aria-pressed:shadow-[0_5px_0_#101011] focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-(--home-accent) active:translate-y-[2px] active:shadow-[0_1px_0_#101011] aria-pressed:translate-y-[2px] aria-pressed:border-[#d0b9a5] aria-pressed:bg-[#d0b9a5] aria-pressed:text-[#24201d] aria-pressed:shadow-[0_1px_0_#101011] group-data-[instant=true]/languages:transition-none max-[420px]:min-h-[56px] max-[420px]:px-[11px] max-[420px]:py-[10px] max-[420px]:text-[16px]";
type LinesProps = { count: 2 | 3; className?: string };
diff --git a/apps/web/src/features/homepage/page.tsx b/apps/web/src/features/homepage/page.tsx
index 5b05ba075..ad164bddb 100644
--- a/apps/web/src/features/homepage/page.tsx
+++ b/apps/web/src/features/homepage/page.tsx
@@ -23,12 +23,12 @@ const ExportPlayground = lazy(() => import("./export-playground"));
const AtsPlayground = lazy(() => import("./ats-playground"));
const githubUrl = "https://github.com/reactive-resume/reactive-resume";
const buttonClass =
- "inline-flex min-h-[52px] items-center justify-center gap-3 rounded-[4px] border border-[#f1f0eb] bg-[#f1f0eb] px-[19px] py-[14px] text-[14px] font-[550] text-[#151516] [transition:background-color_150ms_ease,transform_150ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-[#d9d8d2] active:transform-[scale(0.97)] max-[540px]:min-h-[49px] max-[540px]:gap-[18px] max-[540px]:px-[15px] max-[540px]:py-3 max-[540px]:text-[13px]";
+ "inline-flex min-h-[52px] items-center justify-center gap-3 rounded-[4px] border border-[#f1f0eb] bg-[#f1f0eb] px-[19px] py-[14px] text-[14px] font-[550] text-[#151516] [transition:background-color_150ms_ease,transform_150ms_var(--ease-out-strong)] hover:bg-[#d9d8d2] active:transform-[scale(0.97)] max-[540px]:min-h-[49px] max-[540px]:gap-[18px] max-[540px]:px-[15px] max-[540px]:py-3 max-[540px]:text-[13px]";
const brandClass = "inline-flex shrink-0 items-center gap-[11px] font-[550] tracking-[-0.04em] max-[540px]:gap-2";
const paperLinkClass =
"group/link flex min-h-[65px] items-center gap-[14px] border-t border-[#2d2e3038] text-[14px] [transition:background-color_160ms_ease] focus-visible:outline-[#37383a]";
const paperArrowClass =
- "ml-auto [transition:transform_180ms_cubic-bezier(0.23,1,0.32,1)] group-hover/link:transform-[translate(2px,-2px)]";
+ "ml-auto [transition:transform_180ms_var(--ease-out-strong)] group-hover/link:transform-[translate(2px,-2px)]";
const contributeLinkClass =
"inline-flex min-h-11 items-center gap-2 text-(--home-ink) underline-offset-4 hover:underline";
type DeferredDemoProps = { children: ReactNode };
@@ -91,7 +91,7 @@ export function Homepage() {
>
Docs
-
+
diff --git a/apps/web/src/features/homepage/resume-sculpture.tsx b/apps/web/src/features/homepage/resume-sculpture.tsx
index ffd8a8d57..9683647df 100644
--- a/apps/web/src/features/homepage/resume-sculpture.tsx
+++ b/apps/web/src/features/homepage/resume-sculpture.tsx
@@ -1,7 +1,7 @@
import type { CSSProperties, PointerEvent } from "react";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
-import { motion, useMotionTemplate, useReducedMotion, useSpring } from "motion/react";
+import { m, useMotionTemplate, useReducedMotion, useSpring } from "motion/react";
import { useId, useRef, useState } from "react";
import { cn } from "@reactive-resume/utils/style";
import "./resume-sculpture.css";
@@ -16,7 +16,7 @@ const focusRing =
const dockLabel = "mb-[7px] block text-[11px] leading-[1.4] text-[#a5a5ab]";
const dockButton =
- "relative grid min-h-[44px] min-w-[44px] cursor-pointer place-items-center bg-transparent text-[12px] text-[#a5a5ab] [transition:transform_140ms_cubic-bezier(0.23,1,0.32,1),color_150ms_ease]";
+ "relative grid min-h-[44px] min-w-[44px] cursor-pointer place-items-center bg-transparent text-[12px] text-[#a5a5ab] [transition:transform_140ms_var(--ease-out-strong),color_150ms_ease]";
const templateIcon: Record
= {
ditgar: "bg-[linear-gradient(to_right,#aa8ebe_34%,transparent_34%)]",
@@ -110,7 +110,7 @@ export function ResumeSculpture({
onPointerCancel={endDrag}
onLostPointerCapture={endDrag}
>
-
);
})}
-
+
@@ -351,7 +351,7 @@ function ResumePaper({ name, accent, typeface, template, position }: ResumePaper
return (
[
const linkClass =
"group/link inline-flex min-h-9 items-center gap-1 text-(--home-muted) text-[14px] [transition:color_150ms_ease] hover:text-(--home-ink)";
const arrowClass =
- "opacity-0 [transition:opacity_180ms_ease,transform_180ms_cubic-bezier(0.23,1,0.32,1)] group-hover/link:transform-[translate(1px,-1px)] group-hover/link:opacity-60";
+ "opacity-0 [transition:opacity_180ms_ease,transform_180ms_var(--ease-out-strong)] group-hover/link:transform-[translate(1px,-1px)] group-hover/link:opacity-60";
const socialClass =
"inline-grid size-10 place-items-center text-(--home-muted) [transition:color_160ms_ease] hover:text-(--home-ink)";
const metaLinkClass = "text-(--home-ink) underline-offset-[3px] hover:underline";
@@ -201,7 +202,7 @@ export function SiteFooter() {
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
- transition={{ duration: 0.7, ease: [0.23, 1, 0.32, 1] }}
+ transition={{ duration: 0.7, ease: EASE_OUT_STRONG }}
>
Reactive Resume
diff --git a/apps/web/src/features/homepage/styles.css b/apps/web/src/features/homepage/styles.css
index 7875f3f73..a29819659 100644
--- a/apps/web/src/features/homepage/styles.css
+++ b/apps/web/src/features/homepage/styles.css
@@ -49,13 +49,3 @@
transform: translateX(100%);
}
}
-
-@media (prefers-reduced-motion: reduce) {
- .homepage *,
- .homepage *::before,
- .homepage *::after {
- animation: none;
- transition: none;
- scroll-behavior: auto;
- }
-}
diff --git a/apps/web/src/features/homepage/template-shelf.tsx b/apps/web/src/features/homepage/template-shelf.tsx
index f10cb63f5..cb4f0d721 100644
--- a/apps/web/src/features/homepage/template-shelf.tsx
+++ b/apps/web/src/features/homepage/template-shelf.tsx
@@ -11,7 +11,7 @@ type TemplateShelfProps = { template: Template; onChange: (template: Template) =
const templates = templateSchema.options;
const displayName = (name: string) => name[0].toUpperCase() + name.slice(1);
const roundLinkClass =
- "inline-flex size-[46px] shrink-0 items-center justify-center rounded-full border border-[#444447] bg-transparent text-(--home-ink) [transition:background-color_150ms_ease,transform_150ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-[#2c2c2f] active:transform-[scale(0.97)] max-[540px]:size-11";
+ "inline-flex size-[46px] shrink-0 items-center justify-center rounded-full border border-[#444447] bg-transparent text-(--home-ink) [transition:background-color_150ms_ease,transform_150ms_var(--ease-out-strong)] hover:bg-[#2c2c2f] active:transform-[scale(0.97)] max-[540px]:size-11";
export function TemplateShelf({ template, onChange }: TemplateShelfProps) {
const index = templates.indexOf(template);
@@ -35,7 +35,7 @@ export function TemplateShelf({ template, onChange }: TemplateShelfProps) {
diff --git a/apps/web/src/features/resume/preview/preview.tsx b/apps/web/src/features/resume/preview/preview.tsx
index 231b15efa..a6bbf299a 100644
--- a/apps/web/src/features/resume/preview/preview.tsx
+++ b/apps/web/src/features/resume/preview/preview.tsx
@@ -1,6 +1,5 @@
import type { ResumePreviewProps } from "./preview.shared";
import { lazy, Suspense } from "react";
-import { useIsClient } from "usehooks-ts";
import { useResumeData } from "../builder/draft";
import { ResumePreviewLoader } from "./preview.shared";
import { getResumePreviewPageCount } from "./preview.shared.utils";
@@ -19,13 +18,10 @@ export function ResumePreview({
showPageNumbers = false,
...rest
}: ResumePreviewProps) {
- const isClient = useIsClient();
const builderResumeData = useResumeData();
const resumeData = rest.data ?? builderResumeData;
const pageCount = getResumePreviewPageCount(resumeData);
- if (!isClient) return null;
-
const resolvedProps = { ...rest, pageGap, pageLayout, pageScale, showPageNumbers };
return (
diff --git a/apps/web/src/features/resume/public/public-resume.tsx b/apps/web/src/features/resume/public/public-resume.tsx
index 451c91586..0356d582c 100644
--- a/apps/web/src/features/resume/public/public-resume.tsx
+++ b/apps/web/src/features/resume/public/public-resume.tsx
@@ -89,7 +89,7 @@ export function PublicResumePage({ resume, username, slug, flags, isRoot = false
onClick={() => void onDownloadPDF()}
aria-label={t`Download PDF`}
title={t`Download PDF`}
- className="fixed right-6 bottom-6 z-50 rounded-full bg-background/95 opacity-70 shadow-lg backdrop-blur transition-opacity hover:opacity-100 print:hidden"
+ className="fixed right-6 bottom-6 z-50 rounded-full bg-background/95 opacity-70 shadow-lg backdrop-blur transition-[opacity,scale] duration-150 ease-out-strong hover:opacity-100 print:hidden"
>
{isExporting ? (
diff --git a/apps/web/src/features/resume/stylesheet/editor.test.tsx b/apps/web/src/features/resume/stylesheet/editor.test.tsx
index d046a5381..95c07caf3 100644
--- a/apps/web/src/features/resume/stylesheet/editor.test.tsx
+++ b/apps/web/src/features/resume/stylesheet/editor.test.tsx
@@ -28,9 +28,8 @@ const builder = vi.hoisted(() => ({
redo: vi.fn(),
}));
-vi.mock("usehooks-ts", async (importOriginal) => ({
- ...(await importOriginal
()),
- useMediaQuery: () => media.mobile,
+vi.mock("@reactive-resume/ui/hooks/use-mobile", () => ({
+ useIsMobile: () => media.mobile,
}));
vi.mock("@/features/theme/provider", () => ({
diff --git a/apps/web/src/features/resume/stylesheet/editor.tsx b/apps/web/src/features/resume/stylesheet/editor.tsx
index 88da18522..2ad26ac06 100644
--- a/apps/web/src/features/resume/stylesheet/editor.tsx
+++ b/apps/web/src/features/resume/stylesheet/editor.tsx
@@ -20,7 +20,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { BookOpenIcon } from "@phosphor-icons/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useMediaQuery } from "usehooks-ts";
import { convertLegacyStyleRules } from "@reactive-resume/pdf/semantic-legacy";
import {
buildSemanticTree,
@@ -31,6 +30,7 @@ import {
import { isFatalStylesheetDiagnostic } from "@reactive-resume/resume/stylesheet";
import { PopoverTrigger } from "@reactive-resume/ui/components/popover";
import { Sheet, SheetContent, SheetTitle } from "@reactive-resume/ui/components/sheet";
+import { useIsMobile } from "@reactive-resume/ui/hooks/use-mobile";
import { ColorPicker } from "@/components/input/color-picker";
import { useIsResumeLocked, useResumeData, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft";
import { useTheme } from "@/features/theme/provider";
@@ -382,7 +382,7 @@ const createEditorMetadata = (data: ResumeData): SemanticCssEditorMetadata => {
function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) {
const { theme } = useTheme();
- const isMobile = useMediaQuery("(max-width: 767px)", { initializeWithValue: false });
+ const isMobile = useIsMobile();
const [focusOpen, setFocusOpen] = useState(false);
const [diagnostics, setDiagnostics] = useState([]);
const [colorTokens, setColorTokens] = useState([]);
diff --git a/apps/web/src/features/settings/authentication/components/action-button.tsx b/apps/web/src/features/settings/authentication/components/action-button.tsx
deleted file mode 100644
index 78eb2a04b..000000000
--- a/apps/web/src/features/settings/authentication/components/action-button.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { ReactNode } from "react";
-import { m } from "motion/react";
-
-type ActionButtonProps = {
- children: ReactNode;
-};
-
-export function ActionButton({ children }: ActionButtonProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/apps/web/src/features/settings/authentication/components/passkeys.tsx b/apps/web/src/features/settings/authentication/components/passkeys.tsx
index 6032eebe9..acec15ba3 100644
--- a/apps/web/src/features/settings/authentication/components/passkeys.tsx
+++ b/apps/web/src/features/settings/authentication/components/passkeys.tsx
@@ -2,7 +2,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { KeyIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { m } from "motion/react";
import { Button } from "@reactive-resume/ui/components/button";
import { Separator } from "@reactive-resume/ui/components/separator";
import { toast } from "@reactive-resume/ui/components/toast";
@@ -112,12 +111,7 @@ export function PasskeysSection() {
};
return (
-
+
@@ -166,6 +160,6 @@ export function PasskeysSection() {
)}
-
+
);
}
diff --git a/apps/web/src/features/settings/authentication/components/password.tsx b/apps/web/src/features/settings/authentication/components/password.tsx
index 80f0919be..5f797b505 100644
--- a/apps/web/src/features/settings/authentication/components/password.tsx
+++ b/apps/web/src/features/settings/authentication/components/password.tsx
@@ -1,10 +1,8 @@
import { Trans } from "@lingui/react/macro";
import { PasswordIcon, PencilSimpleLineIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
-import { m } from "motion/react";
import { Button } from "@reactive-resume/ui/components/button";
import { useDialogStore } from "@/dialogs/store";
-import { ActionButton } from "./action-button";
import { useAuthAccounts } from "./hooks";
export function PasswordSection() {
@@ -14,35 +12,28 @@ export function PasswordSection() {
const hasPassword = hasAccount("credential");
return (
-