1import { onMount } from "solid-js";23export function toHtml(raw: string): string {4 if (/<\/?[a-z]/i.test(raw)) return raw;5 return raw6 .replace(/&/g, "&")7 .replace(/</g, "<")8 .replace(/>/g, ">")9 .replace(/\n/g, "<br>");10}1112function Toolbar(props: { exec: (cmd: string, val?: string) => void }) {13 const e = props.exec;14 return (15 <div class="peach-note-toolbar">16 <button class="peach-note-tool-btn peach-note-tool-bold"17 onMouseDown={(ev) => { ev.preventDefault(); e("bold"); }}18 title="Bold (⌘B)">B</button>19 <button class="peach-note-tool-btn peach-note-tool-italic"20 onMouseDown={(ev) => { ev.preventDefault(); e("italic"); }}21 title="Italic (⌘I)">I</button>22 <div class="peach-note-tool-divider" />23 <button class="peach-note-tool-btn"24 onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "1"); }}25 title="Small">sm</button>26 <button class="peach-note-tool-btn"27 onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "3"); }}28 title="Normal">md</button>29 <button class="peach-note-tool-btn"30 onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "5"); }}31 title="Large">lg</button>32 </div>33 );34}35363738interface NoteEditorProps {39 initialContent: string;40 saving: boolean;41 onSave: (html: string) => Promise<void>;42 onCancel: () => void;43}4445export function NoteEditor(props: NoteEditorProps) {46 let editorRef: HTMLDivElement | undefined;4748 onMount(() => {49 if (!editorRef) return;50 editorRef.innerHTML = toHtml(props.initialContent);51 editorRef.focus();52 const sel = window.getSelection();53 if (sel) {54 const range = document.createRange();55 range.selectNodeContents(editorRef);56 range.collapse(false);57 sel.removeAllRanges();58 sel.addRange(range);59 }60 });6162 function exec(cmd: string, value?: string) {63 document.execCommand(cmd, false, value);64 editorRef?.focus();65 }6667 function handleKeyDown(e: KeyboardEvent) {68 const mod = e.metaKey || e.ctrlKey;69 if (mod && e.key === "b") { e.preventDefault(); exec("bold"); return; }70 if (mod && e.key === "i") { e.preventDefault(); exec("italic"); return; }71 if (e.key === "Escape") { e.preventDefault(); props.onCancel(); }72 }7374 return (75 <div class="peach-note-editor">76 <div class="peach-note-editor-header">77 <button class="peach-note-editor-back" onClick={props.onCancel}78 disabled={props.saving} aria-label="Cancel">79 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor"80 stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">81 <path d="M6 1L1 6L6 11" />82 </svg>83 </button>84 <span class="peach-note-editor-label">note</span>85 <button class="peach-note-editor-save-btn"86 onClick={() => void props.onSave(editorRef?.innerHTML ?? "")}87 disabled={props.saving}>88 {props.saving ? "saving…" : "save"}89 </button>90 </div>9192 <Toolbar exec={exec} />9394 <div ref={editorRef} class="peach-note-editor-body"95 contenteditable="true" onKeyDown={handleKeyDown}96 aria-multiline="true" aria-label="Note content" />97 </div>98 );99}100101102103interface NoteEditorInlineProps {104 initialContent: string;105 onContentChange: (html: string) => void;106}107108export function NoteEditorInline(props: NoteEditorInlineProps) {109 let editorRef: HTMLDivElement | undefined;110111 onMount(() => {112 if (!editorRef) return;113 editorRef.innerHTML = toHtml(props.initialContent);114 editorRef.focus();115 });116117 function exec(cmd: string, value?: string) {118 document.execCommand(cmd, false, value);119 editorRef?.focus();120 }121122 function handleKeyDown(e: KeyboardEvent) {123 const mod = e.metaKey || e.ctrlKey;124 if (mod && e.key === "b") { e.preventDefault(); exec("bold"); }125 if (mod && e.key === "i") { e.preventDefault(); exec("italic"); }126 }127128 return (129 <div class="peach-note-editor-inline">130 <Toolbar exec={exec} />131 <div ref={editorRef}132 class="peach-note-editor-body peach-note-editor-body--inline"133 contenteditable="true"134 onKeyDown={handleKeyDown}135 onInput={() => props.onContentChange(editorRef?.innerHTML ?? "")}136 aria-multiline="true"137 aria-label="Note content"138 />139 </div>140 );141}