NoteToolbar.tsx

6.9 KB · raw

1import { Show } from "solid-js";2import { isNativePlatform } from "../../shared/native";34export type ToolbarExec = (cmd: string, val?: string) => void;56const BoldIcon = () => (7  <svg viewBox="0 0 14 14" aria-hidden="true">8    <path d="M4 2h3.75a2.5 2.5 0 0 1 0 5H4V2Zm0 5h4.25a2.5 2.5 0 0 1 0 5H4V7Z" />9  </svg>10);1112const ItalicIcon = () => (13  <svg viewBox="0 0 14 14" aria-hidden="true">14    <path d="M6 2h5M3 12h5M8.5 2l-3 10" />15  </svg>16);1718const UnderlineIcon = () => (19  <svg viewBox="0 0 14 14" aria-hidden="true">20    <path d="M3.5 2v4.25a3.5 3.5 0 0 0 7 0V2M2.5 12h9" />21  </svg>22);2324const StrikeIcon = () => (25  <svg viewBox="0 0 14 14" aria-hidden="true">26    <path d="M10.5 3.25C9.75 2.4 8.65 2 7.2 2 5.35 2 4 2.8 4 4.2c0 1.2.95 1.75 2.55 2.15M3.5 10.5c.85 1 2.1 1.5 3.75 1.5 1.9 0 3.25-.8 3.25-2.2 0-1.15-.9-1.75-2.45-2.15M2 7h10" />27  </svg>28);2930const HeadingIcon = () => (31  <svg viewBox="0 0 14 14" aria-hidden="true">32    <path d="M2 2.5v9M8 2.5v9M2 7h6M10 9.25c.25-.6.7-.9 1.35-.9.8 0 1.3.45 1.3 1.1 0 .55-.35.95-1.2 1.55L10.2 12h2.65" />33  </svg>34);3536const QuoteIcon = () => (37  <svg viewBox="0 0 14 14" aria-hidden="true">38    <path d="M2.25 4.25h3v3h-2c0 1.4.55 2.45 1.65 3.15M8.25 4.25h3v3h-2c0 1.4.55 2.45 1.65 3.15" />39  </svg>40);4142const BulletIcon = () => (43  <svg viewBox="0 0 14 14" aria-hidden="true">44    <path d="M5 3h7M5 7h7M5 11h7" />45    <circle cx="2" cy="3" r=".75" />46    <circle cx="2" cy="7" r=".75" />47    <circle cx="2" cy="11" r=".75" />48  </svg>49);5051const NumberedIcon = () => (52  <svg viewBox="0 0 14 14" aria-hidden="true">53    <path d="M5 3h7M5 7h7M5 11h7M1.25 2.25h.8V4M1.2 6.15c.2-.35.5-.55.95-.55.55 0 .9.3.9.75 0 .4-.25.65-.8 1.05l-1 .7h1.9M1.2 10.15c.25-.3.55-.45.95-.45.55 0 .9.25.9.65 0 .35-.25.6-.7.65.5.05.8.3.8.7 0 .45-.4.75-1 .75-.45 0-.8-.15-1.05-.45" />54  </svg>55);5657const RuleIcon = () => (58  <svg viewBox="0 0 14 14" aria-hidden="true">59    <path d="M1.5 7h11" />60  </svg>61);6263function applyBold(exec: ToolbarExec) { exec("bold"); }64function applyItalic(exec: ToolbarExec) { exec("italic"); }65function applyUnderline(exec: ToolbarExec) { exec("underline"); }66function applyStrike(exec: ToolbarExec) { exec("strikeThrough"); }67function applyHeading(exec: ToolbarExec) {68  const val = document.queryCommandValue("formatBlock").toLowerCase();69  exec("formatBlock", val === "h2" ? "p" : "h2");70}71function applyQuote(exec: ToolbarExec) {72  const val = document.queryCommandValue("formatBlock").toLowerCase();73  exec("formatBlock", val === "blockquote" ? "p" : "blockquote");74}75function applyBullet(exec: ToolbarExec) { exec("insertUnorderedList"); }76function applyNumbered(exec: ToolbarExec) { exec("insertOrderedList"); }77function applyRule(exec: ToolbarExec) { exec("insertHorizontalRule"); }7879interface ToolbarProps {80  exec: ToolbarExec;81  keyboardVisible?: boolean;82  onDismissKeyboard?: () => void;83  class?: string;84}8586export function NoteToolbar(props: ToolbarProps) {87  return (88    <Show89      when={isNativePlatform()}90      fallback={91        // Desktop: toolbar rendered inline above the body, shown always92        <div class={props.class ?? "peach-note-toolbar-desktop-inline"}>93          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyBold(props.exec); }} title="Bold" aria-label="Bold"><BoldIcon /></button>94          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyItalic(props.exec); }} title="Italic" aria-label="Italic"><ItalicIcon /></button>95          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyUnderline(props.exec); }} title="Underline" aria-label="Underline"><UnderlineIcon /></button>96          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyStrike(props.exec); }} title="Strikethrough" aria-label="Strikethrough"><StrikeIcon /></button>97          <div class="peach-note-toolbar-divider" />98          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyHeading(props.exec); }} title="Heading" aria-label="Heading"><HeadingIcon /></button>99          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyQuote(props.exec); }} title="Quote" aria-label="Quote"><QuoteIcon /></button>100          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyBullet(props.exec); }} title="Bullet list" aria-label="Bullet list"><BulletIcon /></button>101          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyNumbered(props.exec); }} title="Numbered list" aria-label="Numbered list"><NumberedIcon /></button>102          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyRule(props.exec); }} title="Divider" aria-label="Divider"><RuleIcon /></button>103        </div>104      }105    >106      {/* Mobile: sits above keyboard when keyboard is visible */}107      <Show when={props.keyboardVisible}>108        <div class={props.class ?? "peach-note-toolbar-mobile"}>109          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyBold(props.exec); }} aria-label="Bold"><BoldIcon /></button>110          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyItalic(props.exec); }} aria-label="Italic"><ItalicIcon /></button>111          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyUnderline(props.exec); }} aria-label="Underline"><UnderlineIcon /></button>112          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyStrike(props.exec); }} aria-label="Strikethrough"><StrikeIcon /></button>113          <div class="peach-note-toolbar-divider" />114          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyHeading(props.exec); }} aria-label="Heading"><HeadingIcon /></button>115          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyQuote(props.exec); }} aria-label="Quote"><QuoteIcon /></button>116          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyBullet(props.exec); }} aria-label="Bullet list"><BulletIcon /></button>117          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyNumbered(props.exec); }} aria-label="Numbered list"><NumberedIcon /></button>118          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyRule(props.exec); }} aria-label="Divider"><RuleIcon /></button>119          <div class="peach-note-toolbar-spacer" />120          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); props.onDismissKeyboard?.(); }} aria-label="Dismiss keyboard">121            <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M1.5 3h11v7h-11V3ZM3 5h.01M5 5h.01M7 5h.01M9 5h.01M11 5h.01M3 7h.01M5 7h.01M7 7h.01M9 7h2M4 9h6M5 12l2 1.5L9 12" /></svg>122          </button>123        </div>124      </Show>125    </Show>126  );127}