1import { createEffect, on, 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}1112const BoldIcon = () => <span style="font-family:Inter,system-ui,sans-serif;font-weight:700;font-size:11px;">B</span>;13const ItalicIcon = () => <span style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;">I</span>;14const UnderlineIcon = () => <span style="text-decoration:underline;font-size:10px;font-family:'IBM Plex Mono',monospace;">U</span>;15const StrikeIcon = () => <span style="text-decoration:line-through;font-size:10px;font-family:'IBM Plex Mono',monospace;">S</span>;16const HeadingIcon = () => <span style="font-family:Inter,system-ui,sans-serif;font-weight:600;font-size:10px;letter-spacing:0.03em;">H</span>;1718const BulletIcon = () => (19 <svg width="12" height="11" viewBox="0 0 12 11" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">20 <circle cx="1.5" cy="2.5" r="1" fill="currentColor" stroke="none"/>21 <line x1="4" y1="2.5" x2="11" y2="2.5"/>22 <circle cx="1.5" cy="6" r="1" fill="currentColor" stroke="none"/>23 <line x1="4" y1="6" x2="11" y2="6"/>24 <circle cx="1.5" cy="9.5" r="1" fill="currentColor" stroke="none"/>25 <line x1="4" y1="9.5" x2="11" y2="9.5"/>26 </svg>27);2829const NumberedIcon = () => (30 <svg width="12" height="11" viewBox="0 0 12 11" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">31 <text x="0" y="4" font-size="4" fill="currentColor" stroke="none" font-family="monospace">1.</text>32 <line x1="4" y1="2.5" x2="11" y2="2.5"/>33 <text x="0" y="7.5" font-size="4" fill="currentColor" stroke="none" font-family="monospace">2.</text>34 <line x1="4" y1="6" x2="11" y2="6"/>35 <text x="0" y="11" font-size="4" fill="currentColor" stroke="none" font-family="monospace">3.</text>36 <line x1="4" y1="9.5" x2="11" y2="9.5"/>37 </svg>38);3940const RuleIcon = () => (41 <svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">42 <line x1="0" y1="4" x2="12" y2="4"/>43 <line x1="2" y1="1.5" x2="2" y2="6.5" stroke-width="0.6" opacity="0.5"/>44 <line x1="10" y1="1.5" x2="10" y2="6.5" stroke-width="0.6" opacity="0.5"/>45 </svg>46);4748export type ExecFn = (cmd: string, val?: string) => void;4950export function Toolbar(props: { exec: ExecFn; class?: string }) {51 const e = props.exec;52 function toggleHeading() {53 const val = document.queryCommandValue("formatBlock").toLowerCase();54 e("formatBlock", val === "h2" ? "p" : "h2");55 }56 return (57 <div class={props.class ?? "peach-note-toolbar"}>58 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("bold"); }} title="Bold (⌘B)"><BoldIcon /></button>59 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("italic"); }} title="Italic (⌘I)"><ItalicIcon /></button>60 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("underline"); }} title="Underline (⌘U)"><UnderlineIcon /></button>61 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("strikeThrough"); }} title="Strikethrough"><StrikeIcon /></button>62 <div class="peach-note-tool-divider" />63 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); toggleHeading(); }} title="Heading"><HeadingIcon /></button>64 <button class="peach-note-tool-btn peach-note-tool-icon" onMouseDown={(ev) => { ev.preventDefault(); e("insertUnorderedList"); }} title="Bullet list"><BulletIcon /></button>65 <button class="peach-note-tool-btn peach-note-tool-icon" onMouseDown={(ev) => { ev.preventDefault(); e("insertOrderedList"); }} title="Numbered list"><NumberedIcon /></button>66 <div class="peach-note-tool-divider" />67 <button class="peach-note-tool-btn peach-note-tool-icon" onMouseDown={(ev) => { ev.preventDefault(); e("insertHorizontalRule"); }} title="Divider"><RuleIcon /></button>68 </div>69 );70}71727374interface NoteEditorInlineProps {75 initialContent: string;76 onContentChange?: (html: string) => void;77 onBlur?: (html: string) => void;78 onReady?: (el: HTMLDivElement) => void;79 autoFocus?: boolean;80 81 entryKey?: string;82}8384export function NoteEditorInline(props: NoteEditorInlineProps) {85 let editorRef: HTMLDivElement | undefined;8687 onMount(() => {88 if (!editorRef) return;89 editorRef.innerHTML = toHtml(props.initialContent);90 if (props.autoFocus !== false) editorRef.focus();91 props.onReady?.(editorRef);92 });9394 createEffect(on(() => props.entryKey, (key, prevKey) => {95 if (prevKey === undefined || key === prevKey || !editorRef) return;96 editorRef.innerHTML = toHtml(props.initialContent);97 props.onReady?.(editorRef);98 }));99100 function exec(cmd: string, value?: string) {101 document.execCommand(cmd, false, value);102 editorRef?.focus();103 }104105 function handleKeyDown(e: KeyboardEvent) {106 const mod = e.metaKey || e.ctrlKey;107 108 109 const isLetterKey = e.key.length === 1;110 if (isLetterKey) return;111 if (mod && e.key === "b") { e.preventDefault(); exec("bold"); }112 if (mod && e.key === "i") { e.preventDefault(); exec("italic"); }113 if (mod && e.key === "u") { e.preventDefault(); exec("underline"); }114 }115116 return (117 <div class="peach-note-editor-inline">118 <div ref={editorRef}119 class="peach-note-editor-body peach-note-editor-body--inline"120 contenteditable="true"121 onKeyDown={handleKeyDown}122 onInput={() => props.onContentChange?.(editorRef?.innerHTML ?? "")}123 onBlur={() => props.onBlur?.(editorRef?.innerHTML ?? "")}124 aria-multiline="true"125 aria-label="Note content"126 />127 </div>128 );129}