mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
feat(web): give pressable surfaces a quick press response
Buttons (every variant except link, skipped while loading), chips, tiles, nav items, rail and tab-bar icons now dip to 0.97 on press; document, template, application and suggestion cards dip to 0.98. Full-width settings and conversation rows darken to the press colour instead of shrinking. Joined button groups cancel the scale so their seams stay closed, and the switch thumb stretches toward its travel while held. Document and template cards transitioned transform, but Tailwind v4 lifts with the translate property, so their hover lift jumped; they now list translate and scale. A new document card keeps its accent ring while hovered, and letter template cards lift like resume template cards.
This commit is contained in:
@@ -21,7 +21,7 @@ export function ApplicationCard({ application, onClick, withMenu = false, draggi
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative grid gap-2 rounded-[10px] border border-line bg-surface p-3 shadow-e1 transition-colors duration-quick hover:border-line-2",
|
||||
"group relative grid gap-2 rounded-[10px] border border-line bg-surface p-3 shadow-e1 transition-[border-color,scale] duration-quick ease-enter hover:border-line-2 active:not-has-[[data-slot=button]:active]:scale-[0.98]",
|
||||
dragging && "rotate-1 shadow-e3",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -358,7 +358,7 @@ function EmptyState({ document, disabled, onPick }: EmptyStateProps) {
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onPick(suggestion.label)}
|
||||
className="flex min-h-11 w-full items-center gap-3 rounded-lg border border-line px-3 py-2 text-start transition-colors duration-quick hover:bg-hover disabled:opacity-60"
|
||||
className="flex min-h-11 w-full items-center gap-3 rounded-lg border border-line px-3 py-2 text-start transition-[background-color,scale] duration-quick ease-enter hover:bg-hover enabled:active:scale-[0.98] disabled:opacity-60"
|
||||
>
|
||||
<Icon name={suggestion.icon} size={20} className="shrink-0 text-ink-2" />
|
||||
<span className="grid min-w-0">
|
||||
@@ -489,7 +489,10 @@ function ConversationRow({ thread, current, detail, onOpen, onDelete }: Conversa
|
||||
type="button"
|
||||
aria-current={current ? "true" : undefined}
|
||||
onClick={onOpen}
|
||||
className={cn("grid min-w-0 flex-1 rounded-lg px-3 py-2 text-start", current && "bg-sunken")}
|
||||
className={cn(
|
||||
"grid min-w-0 flex-1 rounded-lg px-3 py-2 text-start transition-colors duration-quick ease-enter active:bg-press",
|
||||
current && "bg-sunken",
|
||||
)}
|
||||
>
|
||||
<span className="truncate font-medium text-sm">{thread.title}</span>
|
||||
<span className="truncate text-ink-3 text-xs">{detail}</span>
|
||||
@@ -498,7 +501,7 @@ function ConversationRow({ thread, current, detail, onOpen, onDelete }: Conversa
|
||||
icon="close"
|
||||
label={t`Delete ${thread.title}`}
|
||||
size="icon-sm"
|
||||
className="me-1 text-ink-3 opacity-0 transition-[opacity,background-color,border-color,color,filter] focus-visible:opacity-100 group-hover/row:opacity-100"
|
||||
className="me-1 text-ink-3 opacity-0 transition-[opacity,background-color,border-color,color,filter,scale] focus-visible:opacity-100 group-hover/row:opacity-100"
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -669,7 +669,7 @@ export function Composer(props: ComposerProps) {
|
||||
disabled={!streaming && (!hasText || disabled)}
|
||||
onClick={streaming ? props.onStop : submit}
|
||||
className={cn(
|
||||
"grid size-[34px] shrink-0 place-items-center rounded-lg transition-colors duration-quick",
|
||||
"grid size-[34px] shrink-0 place-items-center rounded-lg transition-[background-color,color,scale] duration-quick ease-enter enabled:active:scale-[0.97]",
|
||||
streaming ? "bg-ink text-bg" : hasText ? "bg-accent text-on-accent" : "bg-sunken text-ink-3",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -125,8 +125,9 @@ export function DocumentCard({ document, onTags, onLink }: DocumentItemProps) {
|
||||
document={document}
|
||||
label={document.name}
|
||||
className={cn(
|
||||
"relative block aspect-page overflow-hidden rounded-[6px] shadow-[0_0_0_1px_var(--line),var(--shadow-1)] transition-[transform,box-shadow] duration-quick hover:-translate-y-0.5 hover:shadow-[0_0_0_1px_var(--line),var(--shadow-2)]",
|
||||
isNew && "shadow-[0_0_0_2px_var(--accent),var(--shadow-1)]",
|
||||
"relative block aspect-page overflow-hidden rounded-[6px] shadow-[0_0_0_1px_var(--line),var(--shadow-1)] transition-[translate,scale,box-shadow] duration-quick ease-enter hover:-translate-y-0.5 hover:shadow-[0_0_0_1px_var(--line),var(--shadow-2)] active:scale-[0.98]",
|
||||
isNew &&
|
||||
"shadow-[0_0_0_2px_var(--accent),var(--shadow-1)] hover:shadow-[0_0_0_2px_var(--accent),var(--shadow-2)]",
|
||||
document.trashedAt && "pointer-events-none",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -160,7 +160,7 @@ export function DocumentsPage({ search, onSearchChange }: DocumentsPageProps) {
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"h-7 rounded-full border px-3 text-[13px] transition-colors duration-quick",
|
||||
"h-7 rounded-full border px-3 text-[13px] transition-[background-color,border-color,color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
active
|
||||
? "border-accent bg-accent-soft text-accent-text"
|
||||
: "border-line-2 text-ink-2 hover:bg-hover",
|
||||
|
||||
@@ -282,7 +282,7 @@ export function NewDocumentDialog({ data }: { data?: NewDocumentDialogData | und
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file) void importFile(file);
|
||||
}}
|
||||
className="flex items-start gap-4 rounded-xl border-[1.5px] border-line-2 border-dashed p-5 text-start transition-colors duration-quick hover:border-accent hover:bg-accent-soft"
|
||||
className="flex items-start gap-4 rounded-xl border-[1.5px] border-line-2 border-dashed p-5 text-start transition-[background-color,border-color,scale] duration-quick ease-enter hover:border-accent hover:bg-accent-soft active:scale-[0.98]"
|
||||
>
|
||||
<span className="grid size-11 shrink-0 place-items-center rounded-[10px] bg-sunken text-ink-2">
|
||||
<Icon name="upload_file" size={24} />
|
||||
@@ -420,7 +420,7 @@ function ChoiceTile({ icon, title, description, onClick, disabled }: ChoiceTileP
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className="flex items-start gap-3 rounded-xl border border-line p-4 text-start transition-colors duration-quick hover:border-line-2 hover:bg-hover disabled:opacity-60"
|
||||
className="flex items-start gap-3 rounded-xl border border-line p-4 text-start transition-[background-color,border-color,scale] duration-quick ease-enter hover:border-line-2 hover:bg-hover enabled:active:scale-[0.97] disabled:opacity-60"
|
||||
>
|
||||
<Icon name={icon} size={22} className="mt-0.5 text-ink-2" />
|
||||
<span className="grid gap-0.5">
|
||||
@@ -578,7 +578,7 @@ function CopyForJob({ initialSourceId, initialJobId, onBack, onCreated }: CopyFo
|
||||
setName(null);
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-1.5 rounded-full border px-3 text-[13px] transition-colors duration-quick",
|
||||
"flex h-8 items-center gap-1.5 rounded-full border px-3 text-[13px] transition-[background-color,border-color,color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
jobId === option.id
|
||||
? "border-accent bg-accent-soft text-accent-text"
|
||||
: "border-line-2 text-ink-2 hover:bg-hover",
|
||||
|
||||
@@ -182,7 +182,7 @@ export function ColorGroup() {
|
||||
value={accent.hex}
|
||||
aria-label={accent.label}
|
||||
title={accent.label}
|
||||
className="aspect-square cursor-pointer rounded-full transition-shadow duration-quick data-checked:shadow-[0_0_0_2px_var(--surface),0_0_0_4px_var(--swatch)]"
|
||||
className="aspect-square cursor-pointer rounded-full transition-[box-shadow,scale] duration-quick ease-enter active:scale-[0.97] data-checked:shadow-[0_0_0_2px_var(--surface),0_0_0_4px_var(--swatch)]"
|
||||
style={{ backgroundColor: accent.hex, "--swatch": accent.hex } as CSSProperties}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -190,7 +190,7 @@ function TemplateCard({ id, data, selected, onPreview }: TemplateCardProps) {
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative block aspect-page overflow-hidden rounded-md border border-line bg-white shadow-e1 transition-[transform,box-shadow] duration-quick group-hover/card:-translate-y-0.5 group-hover/card:shadow-e2",
|
||||
"relative block aspect-page overflow-hidden rounded-md border border-line bg-white shadow-e1 transition-[translate,scale,box-shadow] duration-quick ease-enter group-hover/card:-translate-y-0.5 group-hover/card:shadow-e2 group-active/card:scale-[0.98]",
|
||||
selected && "ring-2 ring-accent ring-offset-2 ring-offset-surface",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ function ThemeTiles() {
|
||||
<label
|
||||
key={option.value}
|
||||
className={cn(
|
||||
"flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-xl border border-line-2 bg-surface font-medium text-sm transition-[border-color,box-shadow] duration-quick hover:bg-hover has-focus-visible:outline-2 has-focus-visible:outline-accent",
|
||||
"flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-xl border border-line-2 bg-surface font-medium text-sm transition-[background-color,border-color,box-shadow,scale] duration-quick ease-enter hover:bg-hover active:scale-[0.97] has-focus-visible:outline-2 has-focus-visible:outline-accent",
|
||||
checked && "border-accent shadow-[0_0_0_3px_var(--accent-soft)]",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,8 @@ function Row({ icon, label, value }: RowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const rowClass = "flex h-14 items-center gap-3 px-3.5 font-medium text-base";
|
||||
const rowClass =
|
||||
"flex h-14 items-center gap-3 px-3.5 font-medium text-base transition-colors duration-quick ease-enter active:bg-press";
|
||||
|
||||
/** Phones: the Account tab is the settings root, three rows showing their current values. */
|
||||
export function SettingsRoot() {
|
||||
@@ -66,7 +67,7 @@ export function SettingsRoot() {
|
||||
href="https://docs.rxresu.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex h-12 items-center rounded-[14px] border border-line bg-surface px-3.5 font-medium"
|
||||
className="flex h-12 items-center rounded-[14px] border border-line bg-surface px-3.5 font-medium transition-colors duration-quick ease-enter active:bg-press"
|
||||
>
|
||||
<Trans>Help & docs</Trans>
|
||||
</a>
|
||||
|
||||
@@ -152,7 +152,7 @@ export function LetterDesignPanel() {
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative block aspect-[1/1.414] overflow-hidden rounded-md border bg-white transition-shadow duration-quick",
|
||||
"relative block aspect-[1/1.414] overflow-hidden rounded-md border bg-white transition-[translate,scale,box-shadow] duration-quick ease-enter group-hover/template:-translate-y-0.5 group-active/template:scale-[0.98]",
|
||||
selected
|
||||
? "border-accent shadow-[0_0_0_2px_var(--accent)]"
|
||||
: "border-line group-hover/template:shadow-e2",
|
||||
|
||||
@@ -182,7 +182,7 @@ function NavLink({ item, current }: { item: NavItem; current: boolean }) {
|
||||
to={item.to}
|
||||
aria-current={current ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex h-[34px] items-center gap-2.5 rounded-lg px-2.5 text-sm transition-colors duration-quick",
|
||||
"flex h-[34px] items-center gap-2.5 rounded-lg px-2.5 text-sm transition-[background-color,color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
current ? "bg-sunken font-medium text-ink" : "text-ink-2 hover:bg-hover hover:text-ink",
|
||||
)}
|
||||
>
|
||||
@@ -214,7 +214,7 @@ function Rail() {
|
||||
type="button"
|
||||
aria-label={t`Search or run…`}
|
||||
onClick={() => openPalette(true)}
|
||||
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-colors hover:bg-hover"
|
||||
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-[background-color,scale] duration-quick ease-enter hover:bg-hover active:scale-[0.97]"
|
||||
/>
|
||||
</RailTip>
|
||||
<nav aria-label={t`App`} className="grid gap-1">
|
||||
@@ -225,7 +225,7 @@ function Rail() {
|
||||
aria-label={item.label}
|
||||
aria-current={isCurrent(item.to) ? "page" : undefined}
|
||||
className={cn(
|
||||
"grid size-10 place-items-center rounded-lg transition-colors duration-quick",
|
||||
"grid size-10 place-items-center rounded-lg transition-[background-color,color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
isCurrent(item.to) ? "bg-sunken text-ink" : "text-ink-2 hover:bg-hover",
|
||||
)}
|
||||
/>
|
||||
@@ -238,7 +238,7 @@ function Rail() {
|
||||
<Link
|
||||
to={trash.to}
|
||||
aria-label={t`Trash`}
|
||||
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-colors hover:bg-hover"
|
||||
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-[background-color,scale] duration-quick ease-enter hover:bg-hover active:scale-[0.97]"
|
||||
/>
|
||||
</RailTip>
|
||||
)}
|
||||
@@ -302,21 +302,21 @@ function MobileTabs() {
|
||||
<Link
|
||||
to="/dashboard"
|
||||
aria-current={isCurrent("/dashboard") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5"
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("description", t`Documents`, isCurrent("/dashboard"))}
|
||||
</Link>
|
||||
<Link
|
||||
to="/dashboard/applications"
|
||||
aria-current={isCurrent("/dashboard/applications") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5"
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("work", t`Applications`, isCurrent("/dashboard/applications"))}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openDialog("document.new", undefined)}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5"
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
<span className="grid h-[26px] w-[34px] place-items-center rounded-full bg-accent text-on-accent">
|
||||
<Icon name="add" size={20} />
|
||||
@@ -329,7 +329,7 @@ function MobileTabs() {
|
||||
<Link
|
||||
to="/dashboard/settings"
|
||||
aria-current={isCurrent("/dashboard/settings") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5"
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("account_circle", t`Account`, isCurrent("/dashboard/settings"))}
|
||||
</Link>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cva } from "class-variance-authority";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 *:active:scale-100! has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button touch-target relative inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border border-transparent font-medium text-sm transition-[background-color,border-color,color,filter] duration-quick ease-enter disabled:pointer-events-none aria-busy:cursor-progress [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group/button touch-target relative inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border border-transparent font-medium text-sm transition-[background-color,border-color,color,filter,scale] duration-quick ease-enter disabled:pointer-events-none aria-busy:cursor-progress [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -27,7 +27,11 @@ const buttonVariants = cva(
|
||||
"icon-lg": "size-11 rounded-lg",
|
||||
},
|
||||
},
|
||||
compoundVariants: [{ variant: "ghost", size: "default", className: "px-3" }],
|
||||
compoundVariants: [
|
||||
{ variant: "ghost", size: "default", className: "px-3" },
|
||||
// Press feedback; a text link doesn't press, and a loading button blocks activation.
|
||||
{ variant: ["primary", "secondary", "ghost", "danger"], className: "not-aria-busy:active:scale-[0.97]" },
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "default",
|
||||
|
||||
@@ -9,7 +9,7 @@ const trackClassName =
|
||||
"relative inline-flex shrink-0 items-center rounded-full bg-line-2 transition-colors duration-quick group-data-checked/switch:bg-accent group-data-disabled/switch:opacity-50 data-[size=default]:h-5 data-[size=default]:w-8 data-[size=touch]:h-[26px] data-[size=touch]:w-11";
|
||||
|
||||
const thumbClassName =
|
||||
"pointer-events-none block rounded-full bg-white shadow-e1 transition-transform duration-quick ease-enter group-data-[size=default]/track:ms-0.5 group-data-[size=touch]/track:ms-0.5 group-data-[size=default]/track:size-4 group-data-[size=touch]/track:size-[22px] group-data-[size=default]/track:data-checked:translate-x-3 group-data-[size=touch]/track:data-checked:translate-x-[18px] rtl:group-data-[size=default]/track:data-checked:-translate-x-3 rtl:group-data-[size=touch]/track:data-checked:-translate-x-[18px]";
|
||||
"pointer-events-none block origin-left data-checked:origin-right rtl:origin-right rtl:data-checked:origin-left group-active/switch:not-data-disabled:scale-x-[1.15] rounded-full bg-white shadow-e1 transition-transform duration-quick ease-enter group-data-[size=default]/track:ms-0.5 group-data-[size=touch]/track:ms-0.5 group-data-[size=default]/track:size-4 group-data-[size=touch]/track:size-[22px] group-data-[size=default]/track:data-checked:translate-x-3 group-data-[size=touch]/track:data-checked:translate-x-[18px] rtl:group-data-[size=default]/track:data-checked:-translate-x-3 rtl:group-data-[size=touch]/track:data-checked:-translate-x-[18px]";
|
||||
|
||||
function SwitchTrack({ size }: { size: SwitchSize }) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user