refactor(web): put app transitions on the house motion tokens

Set Tailwind's default transition duration and curve to --d1/--ease so
bare transition-* utilities run quick on the house curve. Mirror the
motion tokens in libs/motion.ts (EASE, EASE_MOVE, D1-D3, EXIT) and move
app UI off ease-out-strong and hand-typed 150/300ms durations; the
landing page keeps --ease-out-strong.

Give snapping hover and focus states a transition, slide progress fills
with translate instead of animating width, keep selection outlines'
width and style constant so they fade from transparent instead of
flashing dark, and drop the unused agent-prompt-marquee keyframes.
This commit is contained in:
Amruth Pillai
2026-09-29 12:32:45 +02:00
parent 741b080296
commit 982e688e65
25 changed files with 82 additions and 56 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProp
<span className="max-w-32 truncate sm:max-w-44">{chip}</span>
<div
className={cn(
"ms-1.5 flex shrink-0 items-center gap-x-0.5 transition-opacity duration-150 group-focus-within/chip:opacity-100 group-hover/chip:opacity-100",
"ms-1.5 flex shrink-0 items-center gap-x-0.5 transition-opacity duration-quick group-focus-within/chip:opacity-100 group-hover/chip:opacity-100",
isEditing ? "opacity-100" : "opacity-65",
)}
>
@@ -64,7 +64,7 @@ export function ColorPicker({
{trigger ?? (
<PopoverTrigger>
<div
className="size-6 shrink-0 cursor-pointer rounded-full border border-ink/60 transition-transform duration-150 ease-out-strong focus-visible:outline-hidden active:scale-[0.97]"
className="size-6 shrink-0 cursor-pointer rounded-full border border-ink/60 transition-transform duration-quick ease-enter focus-visible:outline-hidden active:scale-[0.97]"
style={{ backgroundColor: currentValue }}
/>
</PopoverTrigger>
@@ -94,7 +94,7 @@ export function ColorPicker({
aria-pressed={currentValue === color}
onClick={() => setCurrentValue(color)}
className={cn(
"size-5 shrink-0 cursor-pointer rounded-full transition-transform duration-150 ease-out-strong focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent active:scale-[0.97]",
"size-5 shrink-0 cursor-pointer rounded-full transition-transform duration-quick ease-enter focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent active:scale-[0.97]",
currentValue === color && "border border-ink/60",
)}
/>
@@ -34,7 +34,7 @@ export function FontDisplay({ family, label, type, url }: FontDisplayProps) {
<div ref={containerRef} className="inline-flex items-baseline gap-2">
<span
style={{ fontFamily: isLoaded ? `'${previewName}', sans-serif` : "sans-serif" }}
className={cn(isLoaded ? "opacity-100" : "opacity-50", "transition-opacity duration-150 ease-out-strong")}
className={cn(isLoaded ? "opacity-100" : "opacity-50", "transition-opacity duration-quick ease-enter")}
>
{label}
</span>
@@ -104,7 +104,7 @@ function Column({ stage, applications, onOpen }: ColumnProps) {
<button
type="button"
onClick={() => setVisible((count) => count + COLUMN_PAGE_SIZE)}
className="rounded-lg border border-line border-dashed py-2 text-ink-3 text-xs hover:bg-hover"
className="rounded-lg border border-line border-dashed py-2 text-ink-3 text-xs transition-colors hover:bg-hover"
>
{t`Show ${Math.min(remaining, COLUMN_PAGE_SIZE)} more`}
</button>
@@ -177,7 +177,7 @@ export function ApplicationCalendar({ applications, allApplications, onOpen }: A
render={
<button
type="button"
className="self-start rounded-md px-1.5 py-0.5 text-[11px] text-ink-3 hover:bg-sunken hover:text-ink"
className="self-start rounded-md px-1.5 py-0.5 text-[11px] text-ink-3 transition-colors hover:bg-sunken hover:text-ink"
/>
}
>
@@ -320,7 +320,7 @@ function InterviewChip({ item, time, onOpen }: InterviewChipProps) {
<button
type="button"
title={`${time} · ${kind?.label ?? item.interview.kind} · ${item.application.company} — ${item.application.role}`}
className="flex min-w-0 items-center gap-1.5 rounded-md border-s-2 bg-sunken/60 px-1.5 py-0.5 text-start text-[11px] leading-tight hover:bg-sunken max-sm:justify-center max-sm:border-s-0 max-sm:bg-transparent"
className="flex min-w-0 items-center gap-1.5 rounded-md border-s-2 bg-sunken/60 px-1.5 py-0.5 text-start text-[11px] leading-tight transition-colors hover:bg-sunken max-sm:justify-center max-sm:border-s-0 max-sm:bg-transparent"
style={{ borderInlineStartColor: kind?.color }}
onClick={onOpen}
>
@@ -43,7 +43,10 @@ export function CloseDialog({ open, onOpenChange, onClose }: CloseDialogProps) {
className="grid gap-1"
>
{CLOSED_REASONS.map((value) => (
<div key={value} className="flex h-10 items-center gap-2.5 rounded-lg px-2 hover:bg-hover">
<div
key={value}
className="flex h-10 items-center gap-2.5 rounded-lg px-2 transition-colors hover:bg-hover"
>
<RadioGroupItem id={`${id}-${value}`} value={value} />
<label htmlFor={`${id}-${value}`} className="flex-1 cursor-pointer text-sm">
{getClosedReasonLabel(value)}
@@ -82,7 +82,7 @@ export function FileAttachmentField({ value, onChange, attachLabel, disabled }:
type="button"
disabled={disabled || upload.isPending}
onClick={() => inputRef.current?.click()}
className="flex w-full items-center gap-2 rounded-lg border border-line border-dashed p-2.5 text-ink-3 text-sm hover:bg-sunken/50 disabled:opacity-60"
className="flex w-full items-center gap-2 rounded-lg border border-line border-dashed p-2.5 text-ink-3 text-sm transition-[background-color,opacity] hover:bg-sunken/50 disabled:opacity-60"
>
<Icon name="upload" size={16} />
{upload.isPending ? <Trans>Uploading…</Trans> : attachLabel}
@@ -111,7 +111,7 @@ export function ApplicationList({ applications, showClosed, selectedId, onOpen }
<button
type="button"
onClick={() => sortBy(key)}
className="inline-flex h-9 items-center gap-1 rounded-md hover:text-ink"
className="inline-flex h-9 items-center gap-1 rounded-md transition-colors hover:text-ink"
>
{label}
{sort.key === key && <Icon name={sort.direction === "asc" ? "arrow_upward" : "arrow_downward"} size={14} />}
@@ -157,7 +157,7 @@ export function ApplicationList({ applications, showClosed, selectedId, onOpen }
id={groupId}
aria-expanded={open}
onClick={() => toggleGroup(status)}
className="flex h-9 items-center gap-2 rounded-md px-1.5 font-semibold text-sm hover:bg-hover"
className="flex h-9 items-center gap-2 rounded-md px-1.5 font-semibold text-sm transition-colors hover:bg-hover"
>
<Icon
name="chevron_right"
@@ -130,7 +130,7 @@ export function AssistantPanel({ document, onClose }: AssistantPanelProps) {
<button
type="button"
aria-label={t`Model: ${providerLabel}`}
className="flex h-7 max-w-[140px] items-center gap-0.5 rounded-md px-2 text-ink-2 text-xs hover:bg-hover"
className="flex h-7 max-w-[140px] items-center gap-0.5 rounded-md px-2 text-ink-2 text-xs transition-colors hover:bg-hover"
/>
}
>
@@ -484,7 +484,7 @@ type ConversationRowProps = {
function ConversationRow({ thread, current, detail, onOpen, onDelete }: ConversationRowProps) {
return (
<li className="group/row flex items-center gap-1 rounded-lg hover:bg-hover">
<li className="group/row flex items-center gap-1 rounded-lg transition-colors hover:bg-hover">
<button
type="button"
aria-current={current ? "true" : undefined}
@@ -498,7 +498,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 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] focus-visible:opacity-100 group-hover/row:opacity-100"
onClick={onDelete}
/>
</li>
@@ -482,7 +482,7 @@ function QuestionCard({ part, readOnly, onAnswer }: QuestionCardProps) {
value={other}
onChange={(event) => setOther(event.target.value)}
placeholder={t`Or in your own words…`}
className="h-8 min-w-0 flex-1 rounded-md border border-line-2 bg-raised px-2 text-ink text-sm outline-none focus:border-accent"
className="h-8 min-w-0 flex-1 rounded-md border border-line-2 bg-raised px-2 text-ink text-sm outline-none transition-[border-color,box-shadow] focus:border-accent"
/>
<Button type="submit" size="sm" disabled={!other.trim()}>
<Trans>Send</Trans>
@@ -595,7 +595,7 @@ export function Composer(props: ComposerProps) {
type="button"
aria-label={t`Don't send ${chip.label}`}
onClick={() => onContextChange({ ...context, [chip.key]: false })}
className="grid size-5 place-items-center rounded text-ink-3 hover:bg-hover hover:text-ink"
className="grid size-5 place-items-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink"
>
<Icon name="close" size={14} />
</button>
@@ -612,7 +612,7 @@ export function Composer(props: ComposerProps) {
type="button"
aria-label={t`Remove ${attachment.filename}`}
onClick={() => setAttachments((current) => current.filter((item) => item.id !== attachment.id))}
className="grid size-5 place-items-center rounded text-ink-3 hover:bg-hover hover:text-ink"
className="grid size-5 place-items-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink"
>
<Icon name="close" size={14} />
</button>
@@ -621,7 +621,7 @@ export function Composer(props: ComposerProps) {
</div>
)}
<div className="flex items-end gap-1.5 rounded-xl border border-line-2 bg-raised p-1.5 focus-within:border-accent focus-within:ring-3 focus-within:ring-accent-soft">
<div className="flex items-end gap-1.5 rounded-xl border border-line-2 bg-raised p-1.5 transition-[border-color,box-shadow] focus-within:border-accent focus-within:ring-3 focus-within:ring-accent-soft">
<label htmlFor={id} className="sr-only">
<Trans>Message the assistant</Trans>
</label>
@@ -32,7 +32,7 @@ export function ScoreHeader({ report }: ScoreHeaderProps) {
<div className="h-1.5 overflow-hidden rounded-full bg-sunken">
<div
className={cn(
"h-full translate-x-(--fill) rounded-full transition-[translate] duration-300 ease-out-strong rtl:-translate-x-(--fill)",
"h-full translate-x-(--fill) rounded-full transition-[translate] duration-standard ease-enter rtl:-translate-x-(--fill)",
tone.bar,
)}
style={{ "--fill": `${report.score - 100}%` } as CSSProperties}
+1 -1
View File
@@ -3,7 +3,7 @@ import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
export function AuthLayout() {
return (
<div className="fade-in-0 slide-in-from-top-4 mx-auto flex h-svh w-dvw max-w-sm animate-in flex-col justify-center gap-y-6 px-4 xs:px-0 duration-300 ease-(--ease-out-strong)">
<div className="fade-in-0 slide-in-from-top-4 mx-auto flex h-svh w-dvw max-w-sm animate-in flex-col justify-center gap-y-6 px-4 xs:px-0 duration-emphasized ease-enter">
<BrandIcon className="mb-4 size-20 self-center" />
<Outlet />
@@ -1,3 +1,4 @@
import type { CSSProperties } from "react";
import type { ImportKind } from "@/features/resume/import/read-file";
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
@@ -467,8 +468,8 @@ function ImportProgress({ stage, notes }: { stage: number; notes: string[] }) {
className="h-1 overflow-hidden rounded-full bg-sunken"
>
<div
className="h-full bg-accent transition-[width] duration-standard"
style={{ width: `${(stage / 3) * 100}%` }}
className="h-full translate-x-(--fill) bg-accent transition-[translate] duration-standard ease-enter rtl:-translate-x-(--fill)"
style={{ "--fill": `${(stage / 3) * 100 - 100}%` } as CSSProperties}
/>
</div>
</div>
@@ -1,5 +1,6 @@
import type { JdTermMatch } from "@reactive-resume/resume/ats-pdf";
import type { ResumeData, SkillItem } from "@reactive-resume/schema/resume/data";
import type { CSSProperties } from "react";
import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -155,8 +156,8 @@ export function JobMatchTab({ match, data }: JobMatchTabProps) {
className="h-1.5 overflow-hidden rounded-full bg-sunken"
>
<div
className="h-full rounded-full bg-accent transition-[width] duration-emphasized ease-enter"
style={{ width: `${(result.found.length / result.total) * 100}%` }}
className="h-full translate-x-(--fill) rounded-full bg-accent transition-[translate] duration-standard ease-enter rtl:-translate-x-(--fill)"
style={{ "--fill": `${(result.found.length / result.total) * 100 - 100}%` } as CSSProperties}
/>
</div>
</div>
@@ -59,8 +59,8 @@ export function PageOverlay({ pageIndex, pageMap, onSelect }: PageOverlayProps)
data-section-id={node.kind === "header" ? undefined : node.sectionId}
data-item-id={node.kind === "item" ? node.itemId : undefined}
className={cn(
"absolute cursor-pointer rounded-[4px] transition-[background-color,outline-color] duration-quick hover:bg-[oklch(0.5_0.1_150/0.06)]",
selected && "outline-[1.5px] outline-accent outline-solid",
"absolute cursor-pointer rounded-[4px] outline-[1.5px] outline-solid transition-[background-color,outline-color] duration-quick hover:bg-[oklch(0.5_0.1_150/0.06)]",
selected ? "outline-accent" : "outline-transparent",
)}
style={{
left: `calc(${(node.x / page.width) * 100}% - 4px)`,
@@ -10,7 +10,7 @@ import { toast } from "@reactive-resume/ui/components/toast";
import { isRTL } from "@reactive-resume/utils/locale";
import { cn } from "@reactive-resume/utils/style";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
import { EASE_OUT_STRONG } from "@/libs/motion";
import { EASE } from "@/libs/motion";
import { isEditableElementFocused, usePreviewPausedStore, useResumeData } from "../builder/draft";
import { PdfCanvasDocument, PdfCanvasPage } from "./pdf-canvas";
import { ResumePreviewLoader } from "./preview.shared";
@@ -34,7 +34,7 @@ const UPDATE_DEBOUNCE_MS = 100;
const TYPING_DEBOUNCE_MS = 250;
// Incoming layer fades in over the old one; the old layer holds at full opacity until the incoming one is opaque,
// then drops out. Fading both at once dips the page towards the background mid-swap.
const INCOMING_TRANSITION = { duration: 0.15, ease: EASE_OUT_STRONG };
const INCOMING_TRANSITION = { duration: 0.15, ease: EASE };
const EXITING_TRANSITION = { duration: 0.1, delay: 0.18 };
// Motion animates in JS, so the CSS reduced-motion rule doesn't reach it: with reduced motion a new render replaces
// the old one at once, and no half-faded page is ever on screen.
-6
View File
@@ -1,11 +1,5 @@
@import "@reactive-resume/ui/globals.css";
@keyframes agent-prompt-marquee {
to {
transform: translateX(-33.333%);
}
}
/* Check's score ring: the arc eases to a new score, so a fix visibly counts. */
@property --ring-value {
syntax: "<percentage>";
+17 -2
View File
@@ -1,3 +1,18 @@
// Mirrors `--ease-out-strong` in packages/ui/src/styles/globals.css for Motion (JS) animations.
// Mirror the Desk & Paper motion tokens (--d1/--d2/--d3, --ease) here as Motion animations start using them.
// Motion (JS) mirrors of the CSS motion tokens in packages/ui/src/styles/globals.css (--d1/--d2/--d3, --ease,
// --ease-in-out-strong). Durations are in seconds, Motion's unit; multiply by 1000 for APIs that take ms (dnd-kit).
/** --ease / `ease-enter`: everything entering, exiting or changing state. */
export const EASE = [0.2, 0.8, 0.2, 1] as const;
/** --ease-in-out-strong / `ease-in-out-strong`: things moving across the screen (indicators, reorder, settling). */
export const EASE_MOVE = [0.77, 0, 0.175, 1] as const;
/** --d1 / `duration-quick`: hover, press, toggle, checkbox, focus ring. */
export const D1 = 0.12;
/** --d2 / `duration-standard`: menus, popovers, expand/collapse, content swaps, dialogs. */
export const D2 = 0.2;
/** --d3 / `duration-emphasized`: side and bottom sheets, toasts, the assistant. */
export const D3 = 0.32;
/** Exits run at 70% of the enter duration, e.g. `D2 * EXIT`. */
export const EXIT = 0.7;
/** --ease-out-strong. Landing page only (features/homepage/site-footer.tsx); app code uses EASE. */
export const EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const;
@@ -17,7 +17,7 @@ import { LevelTypeCombobox } from "@/components/level/combobox";
import { LevelDisplay } from "@/components/level/display";
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { EASE_OUT_STRONG } from "@/libs/motion";
import { D1, EASE } from "@/libs/motion";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
@@ -180,7 +180,7 @@ function QuickColorCircle({ color, active, onSelect, className, ...props }: Quic
onClick={() => onSelect(color)}
className={cn(
"relative flex size-8 items-center justify-center rounded-md bg-transparent",
"transition-[scale,background-color] duration-150 ease-out-strong hover:bg-sunken/80 active:scale-[0.97]",
"transition-[scale,background-color] duration-quick ease-enter hover:bg-sunken/80 active:scale-[0.97]",
className,
)}
{...props}
@@ -193,7 +193,7 @@ function QuickColorCircle({ color, active, onSelect, className, ...props }: Quic
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.16, ease: EASE_OUT_STRONG }}
transition={{ duration: D1, ease: EASE }}
className="absolute inset-0 flex size-8 items-center justify-center"
>
<div className="size-4 rounded-md bg-ink" />
@@ -33,7 +33,7 @@ import { Icon } from "@reactive-resume/ui/components/icon";
import { Switch } from "@reactive-resume/ui/components/switch";
import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
import { EASE_OUT_STRONG } from "@/libs/motion";
import { EASE } from "@/libs/motion";
import { resolveLayoutSectionTitle } from "./title";
import { filterVisibleLayoutSectionIds } from "./visibility";
@@ -263,7 +263,7 @@ export function LayoutPages() {
</Button>
</div>
<DragOverlay dropAnimation={{ duration: 150, easing: `cubic-bezier(${EASE_OUT_STRONG})` }}>
<DragOverlay dropAnimation={{ duration: 150, easing: `cubic-bezier(${EASE})` }}>
{activeId ? <LayoutItemContent id={activeId} isOverlay /> : null}
</DragOverlay>
</DndContext>
@@ -612,7 +612,7 @@ function LayoutItemContent({
data-overlay={isOverlay ? "true" : undefined}
data-dragging={isDragging ? "true" : undefined}
className={cn(
"group/item flex cursor-grab touch-none select-none items-center gap-x-2 rounded-md border border-line bg-bg px-2 py-1.5 font-medium text-sm transition-[background-color,border-color] duration-150",
"group/item flex cursor-grab touch-none select-none items-center gap-x-2 rounded-md border border-line bg-bg px-2 py-1.5 font-medium text-sm transition-[background-color,border-color] duration-quick",
"hover:bg-sunken/40 active:cursor-grabbing active:border-accent/60 active:bg-sunken/40",
"data-[overlay=true]:cursor-grabbing data-[overlay=true]:border-accent/60 data-[overlay=true]:bg-bg data-[overlay=true]:shadow-md",
"data-[dragging=true]:cursor-grabbing data-[dragging=true]:opacity-40",
@@ -215,10 +215,10 @@ function LetterOverlay({ pageIndex, pageMap, itemId, washed }: LetterOverlayProp
key={`${node.key}:${node.y}`}
onClick={() => reveal(node.kind === "header" ? "from" : "body")}
className={cn(
"absolute cursor-pointer rounded-[4px] transition-[background-color,outline-color] duration-standard",
"absolute cursor-pointer rounded-[4px] outline-[1.5px] outline-solid transition-[background-color,outline-color] duration-standard",
washed && node.kind === "item"
? "bg-[oklch(0.93_0.05_150/0.55)] mix-blend-multiply outline-[1.5px] outline-[oklch(0.5_0.1_150)] outline-solid"
: "hover:bg-[oklch(0.5_0.1_150/0.06)]",
? "bg-[oklch(0.93_0.05_150/0.55)] mix-blend-multiply outline-[oklch(0.5_0.1_150)]"
: "outline-transparent hover:bg-[oklch(0.5_0.1_150/0.06)]",
)}
style={{
left: `calc(${(node.x / page.width) * 100}% - 4px)`,
@@ -1,5 +1,5 @@
import type { CoverLetter } from "@reactive-resume/schema/cover-letter/data";
import type { ReactElement, ReactNode } from "react";
import type { CSSProperties, ReactElement, ReactNode } from "react";
import type { Application } from "@/features/applications/types";
import type { LetterDraft } from "@/features/letters/store";
import { t } from "@lingui/core/macro";
@@ -553,8 +553,8 @@ function DraftBox({ draft, letter, application, onKeep }: DraftBoxProps) {
aria-live="polite"
aria-busy={streaming}
className={cn(
"min-h-[120px] whitespace-pre-wrap rounded-lg bg-accent-soft px-3 py-2.5 text-sm leading-relaxed transition-[outline-color] duration-standard",
draft.phase === "ready" ? "outline-[1.5px] outline-accent outline-solid" : "outline-transparent",
"min-h-[120px] whitespace-pre-wrap rounded-lg bg-accent-soft px-3 py-2.5 text-sm leading-relaxed outline-[1.5px] outline-solid transition-[outline-color] duration-standard",
draft.phase === "ready" ? "outline-accent" : "outline-transparent",
)}
>
{text}
@@ -600,8 +600,15 @@ function LengthSection({ content }: { content: string }) {
}}
/>
<span
className={cn("absolute inset-y-0 start-0 rounded-full transition-[width] duration-standard", fill)}
style={{ width: percent(Math.min(words, LETTER_LENGTH.scale)) }}
className={cn(
"absolute inset-y-0 start-0 w-full translate-x-(--fill) rounded-full transition-[translate] duration-quick ease-enter rtl:-translate-x-(--fill)",
fill,
)}
style={
{
"--fill": `${(Math.min(words, LETTER_LENGTH.scale) / LETTER_LENGTH.scale) * 100 - 100}%`,
} as CSSProperties
}
/>
</div>
<p className="text-ink-3 text-xs">
@@ -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 hover:bg-hover"
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-colors hover:bg-hover"
/>
</RailTip>
<nav aria-label={t`App`} className="grid gap-1">
@@ -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 hover:bg-hover"
className="grid size-10 place-items-center rounded-lg text-ink-2 transition-colors hover:bg-hover"
/>
</RailTip>
)}