[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
523
components/notes/NoteToolbar.tsx
Normal file
523
components/notes/NoteToolbar.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
import {
|
||||
Bold,
|
||||
Check,
|
||||
CheckSquare,
|
||||
Code,
|
||||
Eye,
|
||||
FileCode,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading,
|
||||
Image as ImageIcon,
|
||||
Italic,
|
||||
Link as LinkIcon,
|
||||
List,
|
||||
ListOrdered,
|
||||
Minus,
|
||||
PencilLine,
|
||||
Quote,
|
||||
Redo2,
|
||||
Search,
|
||||
Sigma,
|
||||
SquareCode,
|
||||
Strikethrough,
|
||||
Table as TableIcon,
|
||||
Type,
|
||||
Underline,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { type MarkdownActionType } from "../../domain/notes";
|
||||
import { useAvailableFonts } from "../../application/state/fontStore";
|
||||
import { useI18n } from "../../application/i18n/I18nProvider";
|
||||
import type { ActiveTextFormats, NoteEditorMode } from "./noteEditorTypes";
|
||||
import { EMPTY_ACTIVE_FORMATS } from "./noteEditorTypes";
|
||||
import { Dropdown, DropdownContent, DropdownTrigger } from "../ui/dropdown";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface NoteToolbarProps {
|
||||
editorMode: NoteEditorMode;
|
||||
onAction?: (action: MarkdownActionType) => void;
|
||||
onOpenHostPicker?: () => void;
|
||||
className?: string;
|
||||
noteFontFamily?: string;
|
||||
onChangeNoteFontFamily?: (font: string) => void;
|
||||
noteFontSize?: number;
|
||||
onChangeNoteFontSize?: (size: number) => void;
|
||||
noteCodeFontSize?: number;
|
||||
onChangeNoteCodeFontSize?: (size: number) => void;
|
||||
/** Active text-format toggles at the current selection (button highlight). */
|
||||
activeFormats?: ActiveTextFormats;
|
||||
}
|
||||
|
||||
export interface NoteModeDropdownProps {
|
||||
editorMode: NoteEditorMode;
|
||||
onChangeMode: (mode: NoteEditorMode) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FONT_SIZES = [12, 13, 14, 15, 16, 18, 20];
|
||||
const CODE_FONT_SIZES = [11, 12, 13, 14, 15, 16, 18];
|
||||
|
||||
export const NoteModeDropdown: React.FC<NoteModeDropdownProps> = ({
|
||||
editorMode,
|
||||
onChangeMode,
|
||||
className = "",
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const normalizedMode: "edit" | "source" | "preview" =
|
||||
editorMode === "live" ? "edit" : editorMode;
|
||||
const options = [
|
||||
{
|
||||
mode: "edit" as const,
|
||||
label: t("notes.toolbar.modeLive"),
|
||||
title: t("notes.toolbar.modeLiveTitle"),
|
||||
icon: PencilLine,
|
||||
},
|
||||
{
|
||||
mode: "source" as const,
|
||||
label: t("notes.toolbar.modeSource"),
|
||||
title: t("notes.toolbar.modeSourceTitle"),
|
||||
icon: SquareCode,
|
||||
},
|
||||
{
|
||||
mode: "preview" as const,
|
||||
label: t("notes.toolbar.modePreview"),
|
||||
title: t("notes.toolbar.modePreviewTitle"),
|
||||
icon: Eye,
|
||||
},
|
||||
];
|
||||
const activeOption = options.find((option) => option.mode === normalizedMode) ?? options[0];
|
||||
const ActiveIcon = activeOption.icon;
|
||||
|
||||
return (
|
||||
<Select value={normalizedMode} onValueChange={(mode) => onChangeMode(mode as NoteEditorMode)}>
|
||||
<SelectTrigger
|
||||
data-note-mode-dropdown-trigger
|
||||
aria-label={activeOption.title}
|
||||
className={cn(
|
||||
"app-no-drag h-8 w-auto shrink-0 gap-1.5 border-0 bg-transparent px-2 text-xs text-muted-foreground shadow-none hover:bg-secondary/70 hover:text-foreground focus:ring-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActiveIcon size={15} />
|
||||
<span>{activeOption.label}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" className="w-max min-w-[10rem]">
|
||||
{options.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<SelectItem
|
||||
key={option.mode}
|
||||
value={option.mode}
|
||||
data-note-mode-option={option.mode}
|
||||
className="h-9 whitespace-nowrap"
|
||||
>
|
||||
<span className="flex items-center gap-2 whitespace-nowrap"><Icon size={14} />{option.label}</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoteToolbar: React.FC<NoteToolbarProps> = ({
|
||||
editorMode,
|
||||
onAction,
|
||||
className = "",
|
||||
noteFontFamily = "",
|
||||
onChangeNoteFontFamily,
|
||||
noteFontSize = 14,
|
||||
onChangeNoteFontSize,
|
||||
noteCodeFontSize = 13,
|
||||
onChangeNoteCodeFontSize,
|
||||
activeFormats = EMPTY_ACTIVE_FORMATS,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [fontSearch, setFontSearch] = useState("");
|
||||
|
||||
// The font tool only controls code block / inline code fonts, so it lists
|
||||
// system monospace fonts (fontStore) rather than the UI font set.
|
||||
const availableSystemFonts = useAvailableFonts();
|
||||
|
||||
const systemFontList = useMemo(() => {
|
||||
const defaultOption = { label: t("notes.toolbar.defaultFont"), value: "" };
|
||||
const list = availableSystemFonts.map((f) => ({
|
||||
label: f.name,
|
||||
value: f.family,
|
||||
}));
|
||||
return [defaultOption, ...list];
|
||||
}, [availableSystemFonts, t]);
|
||||
|
||||
const filteredFonts = useMemo(() => {
|
||||
if (!fontSearch.trim()) return systemFontList;
|
||||
const query = fontSearch.trim().toLowerCase();
|
||||
return systemFontList.filter(
|
||||
(f) => f.label.toLowerCase().includes(query) || f.value.toLowerCase().includes(query),
|
||||
);
|
||||
}, [fontSearch, systemFontList]);
|
||||
|
||||
const isEditing = editorMode === "edit" || editorMode === "live" || editorMode === "source";
|
||||
|
||||
if (!isEditing) return null;
|
||||
|
||||
// Highlight style for toggles that are active at the current selection.
|
||||
const formatButtonClass = (active: boolean) =>
|
||||
cn(
|
||||
"p-1.5 rounded-md transition-colors",
|
||||
active
|
||||
? "bg-primary/15 text-primary hover:bg-primary/20 hover:text-primary"
|
||||
: "hover:bg-muted text-muted-foreground hover:text-foreground",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 border-b border-border/70 bg-card/40 text-xs select-none min-w-0 ${className}`}
|
||||
>
|
||||
{/* Formatting Tools (Available in Live Preview & Source Mode) */}
|
||||
<div className="flex flex-1 items-center gap-0.5 min-w-0 overflow-x-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar-track]:bg-transparent">
|
||||
|
||||
{/* Undo / Redo */}
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("undo")}
|
||||
title={t("notes.toolbar.undo")}
|
||||
>
|
||||
<Undo2 size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("redo")}
|
||||
title={t("notes.toolbar.redo")}
|
||||
>
|
||||
<Redo2 size={14} />
|
||||
</button>
|
||||
|
||||
<div className="h-4 w-px bg-border mx-1 shrink-0" />
|
||||
|
||||
{/* Code Font & Typography Settings Dropdown */}
|
||||
<Dropdown>
|
||||
<DropdownTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
||||
title={t("notes.toolbar.typography")}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<Type size={14} />
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownContent align="start" className="w-64 p-2.5 space-y-2.5 z-50 text-xs shadow-lg">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-[11px] font-medium text-muted-foreground">
|
||||
<span>{t("notes.toolbar.customCodeFont")}</span>
|
||||
<span className="text-[10px] opacity-70">
|
||||
{t("notes.toolbar.fontsAvailable", { count: systemFontList.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative flex items-center">
|
||||
<Search size={12} className="absolute left-2 text-muted-foreground pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("notes.toolbar.searchFont")}
|
||||
value={fontSearch}
|
||||
onChange={(e) => setFontSearch(e.target.value)}
|
||||
className="w-full pl-6 pr-2 py-1 rounded border border-border bg-background text-[11px] text-foreground outline-none focus:border-primary placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Font List */}
|
||||
<div className="space-y-0.5 max-h-52 overflow-y-auto pr-1">
|
||||
{filteredFonts.length === 0 ? (
|
||||
<div className="py-2 text-center text-muted-foreground text-[11px]">
|
||||
{t("notes.toolbar.noFontsFound")}
|
||||
</div>
|
||||
) : (
|
||||
filteredFonts.map((f) => (
|
||||
<button
|
||||
key={f.value}
|
||||
type="button"
|
||||
style={{ fontFamily: f.value || undefined }}
|
||||
className={cn(
|
||||
"w-full px-2 py-1 rounded text-left text-xs transition-colors flex items-center justify-between gap-1.5",
|
||||
(noteFontFamily || "") === f.value
|
||||
? "bg-primary text-primary-foreground font-medium"
|
||||
: "hover:bg-secondary text-foreground",
|
||||
)}
|
||||
onClick={() => onChangeNoteFontFamily?.(f.value)}
|
||||
>
|
||||
<span className="truncate">{f.label}</span>
|
||||
{(noteFontFamily || "") === f.value && <Check size={12} className="shrink-0" />}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-2">
|
||||
<div className="text-[11px] font-medium text-muted-foreground mb-1">{t("notes.toolbar.bodyFontSize")}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{FONT_SIZES.map((sz) => (
|
||||
<button
|
||||
key={sz}
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-2 py-0.5 rounded text-xs border transition-colors",
|
||||
(noteFontSize || 14) === sz
|
||||
? "bg-primary text-primary-foreground border-primary font-medium"
|
||||
: "border-border hover:bg-secondary text-foreground",
|
||||
)}
|
||||
onClick={() => onChangeNoteFontSize?.(sz)}
|
||||
>
|
||||
{sz}px
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-2">
|
||||
<div className="text-[11px] font-medium text-muted-foreground mb-1">{t("notes.toolbar.codeFontSize")}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{CODE_FONT_SIZES.map((sz) => (
|
||||
<button
|
||||
key={sz}
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-2 py-0.5 rounded text-xs border transition-colors font-mono",
|
||||
(noteCodeFontSize || 13) === sz
|
||||
? "bg-primary text-primary-foreground border-primary font-medium"
|
||||
: "border-border hover:bg-secondary text-foreground",
|
||||
)}
|
||||
onClick={() => onChangeNoteCodeFontSize?.(sz)}
|
||||
>
|
||||
{sz}px
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
|
||||
{/* Heading Dropdown (Using Portal Dropdown to avoid clipping) */}
|
||||
<Dropdown>
|
||||
<DropdownTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title={t("notes.toolbar.headingLevel")}
|
||||
>
|
||||
<Heading size={14} />
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownContent align="start" className="w-32 py-1 z-50 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-1.5 flex items-center gap-2 hover:bg-secondary text-foreground transition-colors text-left"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("h1")}
|
||||
>
|
||||
<Heading1 size={14} className="text-primary" />
|
||||
<span>{t("notes.toolbar.h1")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-1.5 flex items-center gap-2 hover:bg-secondary text-foreground transition-colors text-left"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("h2")}
|
||||
>
|
||||
<Heading2 size={14} className="text-primary" />
|
||||
<span>{t("notes.toolbar.h2")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-1.5 flex items-center gap-2 hover:bg-secondary text-foreground transition-colors text-left"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("h3")}
|
||||
>
|
||||
<Heading3 size={14} className="text-primary" />
|
||||
<span>{t("notes.toolbar.h3")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-1.5 flex items-center gap-2 hover:bg-secondary text-foreground transition-colors text-left"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("h4")}
|
||||
>
|
||||
<Heading4 size={14} className="text-primary" />
|
||||
<span>{t("notes.toolbar.h4")}</span>
|
||||
</button>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
|
||||
{/* Inline Styles */}
|
||||
<button
|
||||
type="button"
|
||||
className={formatButtonClass(activeFormats.bold)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("bold")}
|
||||
title={t("notes.toolbar.bold")}
|
||||
>
|
||||
<Bold size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={formatButtonClass(activeFormats.italic)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("italic")}
|
||||
title={t("notes.toolbar.italic")}
|
||||
>
|
||||
<Italic size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={formatButtonClass(activeFormats.strikethrough)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("strikethrough")}
|
||||
title={t("notes.toolbar.strikethrough")}
|
||||
>
|
||||
<Strikethrough size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={formatButtonClass(activeFormats.underline)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("underline")}
|
||||
title={t("notes.toolbar.underline")}
|
||||
>
|
||||
<Underline size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={formatButtonClass(activeFormats.code)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("code")}
|
||||
title={t("notes.toolbar.inlineCode")}
|
||||
>
|
||||
<Code size={14} />
|
||||
</button>
|
||||
|
||||
<div className="h-4 w-px bg-border mx-1 shrink-0" />
|
||||
|
||||
{/* Lists */}
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("bullet")}
|
||||
title={t("notes.toolbar.bulletList")}
|
||||
>
|
||||
<List size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("number")}
|
||||
title={t("notes.toolbar.orderedList")}
|
||||
>
|
||||
<ListOrdered size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("task")}
|
||||
title={t("notes.toolbar.taskList")}
|
||||
>
|
||||
<CheckSquare size={14} />
|
||||
</button>
|
||||
|
||||
<div className="h-4 w-px bg-border mx-1 shrink-0" />
|
||||
|
||||
{/* Blocks */}
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("quote")}
|
||||
title={t("notes.toolbar.quote")}
|
||||
>
|
||||
<Quote size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("codeblock")}
|
||||
title={t("notes.toolbar.codeBlock")}
|
||||
>
|
||||
<FileCode size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("math")}
|
||||
title={t("notes.toolbar.mathFormula")}
|
||||
>
|
||||
<Sigma size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("table")}
|
||||
title={t("notes.toolbar.table")}
|
||||
>
|
||||
<TableIcon size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("divider")}
|
||||
title={t("notes.toolbar.divider")}
|
||||
>
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("link")}
|
||||
title={t("notes.toolbar.link")}
|
||||
>
|
||||
<LinkIcon size={14} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onAction?.("image")}
|
||||
title={t("notes.toolbar.image")}
|
||||
>
|
||||
<ImageIcon size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user