mirror of
https://github.com/ThisTine/Snip.git
synced 2026-10-18 02:51:38 +07:00
- Ink-first palette, drop gradients/glow; tabular numbers
- Home: form stays put, results stack below and auto-copy; paste shortens instantly
- Hold-to-delete replaces the delete confirm modal
- Copy buttons morph to a check instead of toasting
- Motion: CSS press feedback, custom ease-out, origin-aware menu, faster exits,
no animation on frequent/keyboard actions, reduced-motion respected
- Fix: scheme-less URLs ("example.com") were blocked by native validation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import type { ShortLink } from "../lib/types";
|
|
import { compactNumber, fullShortUrl, prettyHost, shortUrl } from "../lib/format";
|
|
import { Sparkline } from "./Sparkline";
|
|
import { CopyButton } from "./ui/CopyButton";
|
|
import { HoldToDelete } from "./ui/HoldToDelete";
|
|
import { Qr, Lock, Edit } from "./icons";
|
|
|
|
export interface LinkItemProps {
|
|
link: ShortLink;
|
|
onEdit: (link: ShortLink) => void;
|
|
onDelete: (link: ShortLink) => void;
|
|
onDeleteTap: () => void;
|
|
onManagePin: (link: ShortLink) => void;
|
|
onShowQr: (link: ShortLink) => void;
|
|
}
|
|
|
|
export const iconBtn =
|
|
"press focusable grid h-8 w-8 place-items-center rounded-lg text-muted hover:bg-surface-2 hover:text-ink";
|
|
|
|
export function UrlRow({
|
|
link,
|
|
onEdit,
|
|
onDelete,
|
|
onDeleteTap,
|
|
onManagePin,
|
|
onShowQr,
|
|
}: LinkItemProps) {
|
|
const weekTotal = link.last7Days.reduce((s, d) => s + d.count, 0);
|
|
|
|
return (
|
|
<div className="card flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:gap-4">
|
|
{/* identity */}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<p className="truncate font-mono text-sm font-semibold text-ink">
|
|
{shortUrl(link.code)}
|
|
</p>
|
|
{link.hasPin && <Lock size={12} className="shrink-0 text-muted" />}
|
|
</div>
|
|
<p className="truncate text-[12px] text-muted" title={link.longUrl}>
|
|
→ {prettyHost(link.longUrl)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
{/* stats */}
|
|
<div className="flex shrink-0 items-center gap-3 tabular-nums">
|
|
<div className="hidden sm:block">
|
|
<Sparkline data={link.last7Days} />
|
|
</div>
|
|
<p className="text-sm font-semibold text-ink sm:w-14 sm:text-right">
|
|
{compactNumber(link.totalClicks)}
|
|
{weekTotal > 0 && (
|
|
<span className="block text-[11px] font-medium text-accent">
|
|
+{compactNumber(weekTotal)}
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* actions */}
|
|
<div className="-mr-1.5 flex shrink-0 items-center">
|
|
<CopyButton text={fullShortUrl(link.code)} className={iconBtn} />
|
|
<button onClick={() => onShowQr(link)} aria-label="Show QR code" className={iconBtn}>
|
|
<Qr size={15} />
|
|
</button>
|
|
<button onClick={() => onManagePin(link)} aria-label="Manage PIN" className={iconBtn}>
|
|
<Lock size={15} />
|
|
</button>
|
|
<button onClick={() => onEdit(link)} aria-label="Edit destination" className={iconBtn}>
|
|
<Edit size={15} />
|
|
</button>
|
|
<HoldToDelete
|
|
onConfirm={() => onDelete(link)}
|
|
onTap={onDeleteTap}
|
|
className="h-8 w-8 rounded-lg hover:bg-surface-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|