1import { createSignal } from "solid-js";2import type { VaultEntry } from "../../shared/types";34function vibrate(ms: number) {5 try { navigator.vibrate?.(ms); } catch { }6}78function toRoman(n: number): string {9 const VALS: [number, string][] = [10 [1000,"M"],[900,"CM"],[500,"D"],[400,"CD"],[100,"C"],[90,"XC"],11 [50,"L"],[40,"XL"],[10,"X"],[9,"IX"],[5,"V"],[4,"IV"],[1,"I"],12 ];13 let result = "";14 for (const [val, sym] of VALS) {15 while (n >= val) { result += sym; n -= val; }16 }17 return result;18}1920function formatNoteDate(ts: number): string {21 const now = Date.now();22 const diff = now - ts;23 const DAY = 86_400_000;24 if (diff < DAY) return "Today";25 if (diff < 2 * DAY) return "Yesterday";26 const d = new Date(ts);27 return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });28}2930const SWIPE_THRESHOLD = 80;31const SWIPE_CONFIRM_THRESHOLD = 120;3233interface NoteRowProps {34 note: VaultEntry;35 index: number;36 searchQuery?: string;37 onTap: (note: VaultEntry, el: HTMLElement) => void;38 onDelete: (note: VaultEntry) => void;39 onArchive: (note: VaultEntry) => void;40 fadingClass?: string;41}4243export function NoteRow(props: NoteRowProps) {44 let rowEl!: HTMLDivElement;45 const [swipeX, setSwipeX] = createSignal(0);46 const [swiping, setSwping] = createSignal(false);47 const [confirmDelete, setConfirmDelete] = createSignal(false);4849 let touchStartX = 0;50 let touchStartY = 0;51 let captured = false;5253 function handleTouchStart(e: TouchEvent) {54 const t = e.touches[0];55 if (!t) return;56 touchStartX = t.clientX;57 touchStartY = t.clientY;58 captured = false;59 }6061 function handleTouchMove(e: TouchEvent) {62 const t = e.touches[0];63 if (!t) return;64 const dx = t.clientX - touchStartX;65 const dy = Math.abs(t.clientY - touchStartY);6667 if (!captured) {68 if (dy > 8 && dy > Math.abs(dx)) {69 70 return;71 }72 if (Math.abs(dx) > 8) {73 captured = true;74 setSwping(true);75 }76 return;77 }7879 setSwipeX(dx);80 }8182 function handleTouchEnd(_e: TouchEvent) {83 if (!captured) {84 85 props.onTap(props.note, rowEl);86 vibrate(12);87 setSwping(false);88 return;89 }9091 const dx = swipeX();92 setSwping(false);9394 if (dx < -SWIPE_CONFIRM_THRESHOLD) {95 96 vibrate(40);97 setConfirmDelete(true);98 setSwipeX(0);99 setTimeout(() => setConfirmDelete(false), 4000);100 } else if (dx > SWIPE_CONFIRM_THRESHOLD) {101 102 vibrate(12);103 props.onArchive(props.note);104 setSwipeX(0);105 } else {106 107 setSwipeX(0);108 }109 captured = false;110 }111112 function handleTouchCancel() {113 setSwipeX(0);114 setSwping(false);115 captured = false;116 }117118 119 function renderTitle() {120 const title = props.note.title || "Untitled";121 const q = props.searchQuery?.trim().toLowerCase();122 if (!q) return <span class="peach-note-title">{title}</span>;123 const idx = title.toLowerCase().indexOf(q);124 if (idx === -1) return <span class="peach-note-title">{title}</span>;125 return (126 <span class="peach-note-title">127 {title.slice(0, idx)}128 <span class="peach-note-title-match">{title.slice(idx, idx + q.length)}</span>129 {title.slice(idx + q.length)}130 </span>131 );132 }133134 return (135 <div136 ref={rowEl}137 class={`peach-note-row${props.fadingClass ? " " + props.fadingClass : ""}`}138 style={(() => {139 const dx = swipeX();140 if (dx === 0) return undefined;141 return { transform: `translateX(${dx}px)`, transition: swiping() ? "none" : "transform 250ms ease" };142 })()}143 onTouchStart={handleTouchStart}144 onTouchMove={handleTouchMove}145 onTouchEnd={handleTouchEnd}146 onTouchCancel={handleTouchCancel}147 onClick={() => {148 if (!("ontouchstart" in window)) {149 props.onTap(props.note, rowEl);150 }151 }}152 >153 {}154 <div class="peach-note-row-underlay peach-note-row-underlay--delete" style={{ opacity: Math.min(1, Math.max(0, -swipeX() / SWIPE_THRESHOLD)) }}>155 DELETE156 </div>157 {}158 <div class="peach-note-row-underlay peach-note-row-underlay--archive" style={{ opacity: Math.min(1, Math.max(0, swipeX() / SWIPE_THRESHOLD)) }}>159 ARCHIVE160 </div>161162 <span class="peach-note-numeral">{toRoman(props.index + 1)}.</span>163 <span class="peach-note-title-gap" />164 {renderTitle()}165 <span class="peach-note-date">{formatNoteDate(props.note.modifiedAt)}</span>166167 {confirmDelete() && (168 <div class="peach-note-delete-confirm">169 <span>Move to trash?</span>170 <button class="peach-note-delete-confirm-btn" onClick={(e) => {171 e.stopPropagation();172 vibrate(60);173 props.onDelete(props.note);174 setConfirmDelete(false);175 }}>Delete</button>176 <button class="peach-note-delete-cancel-btn" onClick={(e) => {177 e.stopPropagation();178 setConfirmDelete(false);179 }}>Cancel</button>180 </div>181 )}182 </div>183 );184}