1import { createSignal, onCleanup, onMount, Show } from "solid-js";2import type { VaultEntry } from "../../shared/types";3import { sendMessage } from "../../shared/bridge";4import { isNativePlatform } from "../../shared/native";5import { NoteToolbar, type ToolbarExec } from "../components/NoteToolbar";67const HEADER_HEIGHT = 52;8const AUTOSAVE_MS = 800;9const SAVED_DISPLAY_MS = 1500;1011interface NotesEditorProps {12 note: VaultEntry | null; 13 onBack: () => void;14 onSaved: (note: VaultEntry) => void;15 onDelete: (note: VaultEntry) => void;16 autoFocusTitle?: boolean;17}1819export function NotesEditor(props: NotesEditorProps) {20 let titleRef!: HTMLDivElement;21 let bodyRef!: HTMLDivElement;22 let contentScrollRef!: HTMLDivElement;2324 const [noteId, setNoteId] = createSignal<string | undefined>(props.note?.id);25 const [title, setTitle] = createSignal(props.note?.title ?? "");26 const [body, setBody] = createSignal(props.note?.notes ?? "");27 const [saved, setSaved] = createSignal(false);28 const [keyboardVisible, setKeyboardVisible] = createSignal(false);29 const [contentHeight, setContentHeight] = createSignal<number | null>(null);30 const [dirty, setDirty] = createSignal(false);3132 let saveTimer: number | null = null;33 let savedFadeTimer: number | null = null;3435 function scheduleAutosave() {36 setDirty(true);37 if (saveTimer) clearTimeout(saveTimer);38 saveTimer = window.setTimeout(save, AUTOSAVE_MS);39 }4041 async function save() {42 const t = title();43 const b = body();44 45 if (!t.trim() && !b.trim()) return;46 const res = await sendMessage({47 type: "SAVE_NOTE",48 payload: { id: noteId(), title: t || "Untitled", body: b },49 }) as { ok: boolean; data?: VaultEntry };50 if (res?.ok && res.data) {51 setNoteId(res.data.id);52 setDirty(false);53 showSaved();54 props.onSaved(res.data);55 }56 }5758 function showSaved() {59 setSaved(true);60 if (savedFadeTimer) clearTimeout(savedFadeTimer);61 savedFadeTimer = window.setTimeout(() => setSaved(false), SAVED_DISPLAY_MS);62 }6364 function exec(cmd: string, val?: string) {65 document.execCommand(cmd, false, val);66 bodyRef?.focus();67 scheduleAutosave();68 }6970 async function handleBack() {71 72 73 if (dirty() && (title().trim() || body().trim())) {74 if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }75 await save();76 }77 props.onBack();78 }7980 function handleKeyboardResize() {81 if (!isNativePlatform()) return;82 const vp = window.visualViewport;83 const viewportH = vp?.height ?? window.innerHeight;84 const kbH = window.innerHeight - viewportH;85 if (kbH > 50) {86 setKeyboardVisible(true);87 setContentHeight(viewportH - HEADER_HEIGHT);88 if (document.activeElement) {89 (document.activeElement as HTMLElement).scrollIntoView?.({ behavior: "smooth", block: "nearest" });90 }91 } else {92 setKeyboardVisible(false);93 setContentHeight(null);94 }95 }9697 onMount(() => {98 99 if (titleRef) titleRef.textContent = props.note?.title ?? "";100 if (bodyRef) bodyRef.innerHTML = props.note?.notes ?? "";101102 if (props.autoFocusTitle && titleRef) {103 setTimeout(() => {104 titleRef.focus();105 106 const range = document.createRange();107 const sel = window.getSelection();108 range.selectNodeContents(titleRef);109 range.collapse(false);110 sel?.removeAllRanges();111 sel?.addRange(range);112 }, 300);113 }114115 if (isNativePlatform()) {116 window.addEventListener("resize", handleKeyboardResize);117 }118119 onCleanup(() => {120 if (saveTimer) clearTimeout(saveTimer);121 if (savedFadeTimer) clearTimeout(savedFadeTimer);122 if (isNativePlatform()) window.removeEventListener("resize", handleKeyboardResize);123 });124 });125126 127 let swipeStartX = 0;128 let swipeStartY = 0;129 let swipeCaptured = false;130131 function handleSwipeStart(e: TouchEvent) {132 const t = e.touches[0];133 if (!t) return;134 swipeStartX = t.clientX;135 swipeStartY = t.clientY;136 swipeCaptured = false;137 }138139 function handleSwipeMove(e: TouchEvent) {140 const t = e.touches[0];141 if (!t) return;142 const dx = t.clientX - swipeStartX;143 const dy = Math.abs(t.clientY - swipeStartY);144 if (!swipeCaptured && swipeStartX < 30 && dx > 40 && dy < dx) {145 swipeCaptured = true;146 }147 }148149 function handleSwipeEnd() {150 if (swipeCaptured) {151 handleBack();152 }153 swipeCaptured = false;154 }155156 const execFn: ToolbarExec = exec;157158 return (159 <div160 class="peach-note-editor-surface"161 onTouchStart={handleSwipeStart}162 onTouchMove={handleSwipeMove}163 onTouchEnd={handleSwipeEnd}164 >165 {}166 <div class="peach-notes-header">167 <button class="peach-notes-back" onClick={handleBack} aria-label="Back to notes list">‹</button>168 <span class="peach-notes-header-title is-breadcrumb" onClick={handleBack}>notes</span>169 <div class="peach-notes-header-actions">170 <Show when={noteId()}>171 <button172 class="peach-notes-header-btn"173 aria-label="Delete note"174 onClick={() => {175 const id = noteId();176 if (!id) return;177 178 props.onDelete({ id, title: title(), notes: body(), type: "note", createdAt: 0, modifiedAt: Date.now() } as VaultEntry);179 }}180 title="Delete note"181 >182 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">183 <polyline points="1,3.5 13,3.5" />184 <path d="M5 3.5V2h4v1.5" />185 <rect x="2.5" y="3.5" width="9" height="9" rx="1.5" />186 <line x1="5.5" y1="6.5" x2="5.5" y2="10" />187 <line x1="8.5" y1="6.5" x2="8.5" y2="10" />188 </svg>189 </button>190 </Show>191 </div>192 </div>193194 {}195 <div196 ref={contentScrollRef}197 class="peach-note-editor-scroll"198 style={contentHeight() !== null ? { height: `${contentHeight()}px` } : undefined}199 >200 {}201 <Show when={!isNativePlatform()}>202 <NoteToolbar exec={execFn} />203 </Show>204205 <div206 ref={titleRef}207 class="peach-note-title-field"208 contenteditable="true"209 data-placeholder="Title"210 onInput={() => {211 setTitle(titleRef.textContent ?? "");212 scheduleAutosave();213 }}214 onKeyDown={(e) => {215 if (e.key === "Enter") {216 e.preventDefault();217 bodyRef?.focus();218 }219 }}220 aria-label="Note title"221 />222223 <div class="peach-note-hairline" />224225 <div226 ref={bodyRef}227 class="peach-note-body-field"228 contenteditable="true"229 data-placeholder="Begin writing…"230 onInput={() => {231 setBody(bodyRef.innerHTML ?? "");232 scheduleAutosave();233 }}234 onKeyDown={(e) => {235 const mod = e.metaKey || e.ctrlKey;236 if (mod && e.key === "b") { e.preventDefault(); exec("bold"); }237 if (mod && e.key === "i") { e.preventDefault(); exec("italic"); }238 if (mod && e.key === "u") { e.preventDefault(); exec("underline"); }239 }}240 aria-label="Note body"241 aria-multiline="true"242 />243 </div>244245 {}246 <div class="peach-note-saved-indicator" style={{ opacity: saved() ? "1" : "0" }}>247 saved.248 </div>249250 {}251 <Show when={isNativePlatform()}>252 <NoteToolbar253 exec={execFn}254 keyboardVisible={keyboardVisible()}255 onDismissKeyboard={() => {256 titleRef?.blur();257 bodyRef?.blur();258 }}259 />260 </Show>261 </div>262 );263}