import { createSignal } from "solid-js"; import type { VaultEntry } from "../../shared/types"; function vibrate(ms: number) { try { navigator.vibrate?.(ms); } catch { /* unsupported */ } } function toRoman(n: number): string { const VALS: [number, string][] = [ [1000,"M"],[900,"CM"],[500,"D"],[400,"CD"],[100,"C"],[90,"XC"], [50,"L"],[40,"XL"],[10,"X"],[9,"IX"],[5,"V"],[4,"IV"],[1,"I"], ]; let result = ""; for (const [val, sym] of VALS) { while (n >= val) { result += sym; n -= val; } } return result; } function formatNoteDate(ts: number): string { const now = Date.now(); const diff = now - ts; const DAY = 86_400_000; if (diff < DAY) return "Today"; if (diff < 2 * DAY) return "Yesterday"; const d = new Date(ts); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } const SWIPE_THRESHOLD = 80; const SWIPE_CONFIRM_THRESHOLD = 120; interface NoteRowProps { note: VaultEntry; index: number; searchQuery?: string; onTap: (note: VaultEntry, el: HTMLElement) => void; onDelete: (note: VaultEntry) => void; onArchive: (note: VaultEntry) => void; fadingClass?: string; } export function NoteRow(props: NoteRowProps) { let rowEl!: HTMLDivElement; const [swipeX, setSwipeX] = createSignal(0); const [swiping, setSwping] = createSignal(false); const [confirmDelete, setConfirmDelete] = createSignal(false); let touchStartX = 0; let touchStartY = 0; let captured = false; let suppressNextClick = false; let touchTapPending = false; function handleTouchStart(e: TouchEvent) { const t = e.touches[0]; if (!t) return; touchStartX = t.clientX; touchStartY = t.clientY; captured = false; suppressNextClick = false; touchTapPending = false; } function handleTouchMove(e: TouchEvent) { const t = e.touches[0]; if (!t) return; const dx = t.clientX - touchStartX; const dy = Math.abs(t.clientY - touchStartY); if (!captured) { if (dy > 8 && dy > Math.abs(dx)) { // vertical scroll — abort return; } if (Math.abs(dx) > 8) { captured = true; setSwping(true); } return; } setSwipeX(dx); } function handleTouchEnd(_e: TouchEvent) { if (!captured) { // Let the browser's click event handle taps and mouse input alike. touchTapPending = true; setSwping(false); return; } suppressNextClick = true; const dx = swipeX(); setSwping(false); if (dx < -SWIPE_CONFIRM_THRESHOLD) { // Committed delete swipe vibrate(40); setConfirmDelete(true); setSwipeX(0); setTimeout(() => setConfirmDelete(false), 4000); } else if (dx > SWIPE_CONFIRM_THRESHOLD) { // Archive vibrate(12); props.onArchive(props.note); setSwipeX(0); } else { // Snap back setSwipeX(0); } captured = false; } function handleTouchCancel() { setSwipeX(0); setSwping(false); captured = false; suppressNextClick = false; touchTapPending = false; } // Highlight search match in title function renderTitle() { const title = props.note.title || "Untitled"; const q = props.searchQuery?.trim().toLowerCase(); if (!q) return {title}; const idx = title.toLowerCase().indexOf(q); if (idx === -1) return {title}; return ( {title.slice(0, idx)} {title.slice(idx, idx + q.length)} {title.slice(idx + q.length)} ); } return (