978a2627fb
MarkdownToolbar: - Replace text labels with SVG icons (Material Design paths) - Group buttons with visual separators: text / headings / lists / code / links - Add Strikethrough and Blockquote buttons (previously missing) - Active state: tinted bg + ring instead of solid fill; hover lift shadow - Toolbar container: pill shape with border (matches rest of UI) editor-shared.css: - btn-save: gradient + glow (linear-gradient(135deg, #6366f1, #4f46e5)) - title-input: borderless with Fraunces font and focus underline NoteEditorView: - Back button label standardized to "← Notes" (was "Back") - Write/Preview tabs and toolbar now share the same row (flex-direction row) NoteViewerView: - Shimmer skeleton loader replaces plain "Loading..." text - Note title uses Fraunces font at 2rem - Edit button becomes primary gradient CTA - Meta line shows clock/pencil SVG icons alongside timestamps - Backlinks section: card grid with colored type badges (note=indigo, task=amber) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
8.2 KiB
Vue
158 lines
8.2 KiB
Vue
<script setup lang="ts">
|
|
import type { Editor } from "@tiptap/vue-3";
|
|
|
|
const props = defineProps<{ editor: Editor | null }>();
|
|
|
|
// SVG icons — Material Design paths (24x24) or custom SVG
|
|
const ICONS: Record<string, string> = {
|
|
bold: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>`,
|
|
italic: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/></svg>`,
|
|
strike: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"/></svg>`,
|
|
h1: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">1</text></svg>`,
|
|
h2: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">2</text></svg>`,
|
|
h3: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">3</text></svg>`,
|
|
ul: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`,
|
|
ol: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2zm1-9h1V4H2v1h1zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2zm5-3h13V6H7v2zm0 4h13v-2H7v2zm0 4h13v-2H7v2z"/></svg>`,
|
|
task: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>`,
|
|
code: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`,
|
|
codeblock: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/></svg>`,
|
|
quote: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`,
|
|
link: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>`,
|
|
wikilink: `<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3"/><path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"/></svg>`,
|
|
};
|
|
|
|
const groups = [
|
|
[
|
|
{ id: "bold", title: "Bold (Ctrl+B)", isActive: () => props.editor?.isActive("bold") ?? false, command: () => props.editor?.chain().focus().toggleBold().run() },
|
|
{ id: "italic", title: "Italic (Ctrl+I)", isActive: () => props.editor?.isActive("italic") ?? false, command: () => props.editor?.chain().focus().toggleItalic().run() },
|
|
{ id: "strike", title: "Strikethrough", isActive: () => props.editor?.isActive("strike") ?? false, command: () => props.editor?.chain().focus().toggleStrike().run() },
|
|
],
|
|
[
|
|
{ id: "h1", title: "Heading 1", isActive: () => props.editor?.isActive("heading", { level: 1 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 1 }).run() },
|
|
{ id: "h2", title: "Heading 2", isActive: () => props.editor?.isActive("heading", { level: 2 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 2 }).run() },
|
|
{ id: "h3", title: "Heading 3", isActive: () => props.editor?.isActive("heading", { level: 3 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 3 }).run() },
|
|
],
|
|
[
|
|
{ id: "ul", title: "Bullet List", isActive: () => props.editor?.isActive("bulletList") ?? false, command: () => props.editor?.chain().focus().toggleBulletList().run() },
|
|
{ id: "ol", title: "Ordered List", isActive: () => props.editor?.isActive("orderedList") ?? false, command: () => props.editor?.chain().focus().toggleOrderedList().run() },
|
|
{ id: "task", title: "Task List", isActive: () => props.editor?.isActive("taskList") ?? false, command: () => props.editor?.chain().focus().toggleTaskList().run() },
|
|
],
|
|
[
|
|
{ id: "code", title: "Inline Code", isActive: () => props.editor?.isActive("code") ?? false, command: () => props.editor?.chain().focus().toggleCode().run() },
|
|
{ id: "codeblock", title: "Code Block", isActive: () => props.editor?.isActive("codeBlock") ?? false, command: () => props.editor?.chain().focus().toggleCodeBlock().run() },
|
|
{ id: "quote", title: "Blockquote", isActive: () => props.editor?.isActive("blockquote") ?? false, command: () => props.editor?.chain().focus().toggleBlockquote().run() },
|
|
],
|
|
[
|
|
{
|
|
id: "link",
|
|
title: "Link",
|
|
isActive: () => props.editor?.isActive("link") ?? false,
|
|
command: () => {
|
|
const ed = props.editor;
|
|
if (!ed) return;
|
|
if (ed.isActive("link")) {
|
|
ed.chain().focus().unsetLink().run();
|
|
} else {
|
|
const url = prompt("URL:");
|
|
if (url) ed.chain().focus().setLink({ href: url }).run();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: "wikilink",
|
|
title: "Insert wikilink [[ ]]",
|
|
isActive: () => false,
|
|
command: () => props.editor?.chain().focus().insertContent("[[").run(),
|
|
},
|
|
],
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<div class="md-toolbar" role="toolbar" aria-label="Text formatting">
|
|
<template v-for="(group, gi) in groups" :key="gi">
|
|
<div class="toolbar-group">
|
|
<button
|
|
v-for="btn in group"
|
|
:key="btn.id"
|
|
:class="['md-btn', { active: btn.isActive() }]"
|
|
:title="btn.title"
|
|
type="button"
|
|
@mousedown.prevent="btn.command()"
|
|
>
|
|
<span class="btn-icon" v-html="ICONS[btn.id]" />
|
|
</button>
|
|
</div>
|
|
<span v-if="gi < groups.length - 1" class="toolbar-sep" aria-hidden="true" />
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.md-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
flex-wrap: wrap;
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
padding: 3px 4px;
|
|
}
|
|
|
|
.toolbar-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1px;
|
|
}
|
|
|
|
.toolbar-sep {
|
|
display: block;
|
|
width: 1px;
|
|
height: 18px;
|
|
background: var(--color-border);
|
|
flex-shrink: 0;
|
|
margin: 0 3px;
|
|
}
|
|
|
|
.md-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: transparent;
|
|
color: var(--color-text-secondary);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: background 0.12s, color 0.12s, box-shadow 0.12s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.md-btn:hover {
|
|
background: var(--color-bg-card);
|
|
color: var(--color-text);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.md-btn.active {
|
|
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
|
|
color: var(--color-primary);
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 35%, transparent);
|
|
}
|
|
|
|
.md-btn.active:hover {
|
|
background: color-mix(in srgb, var(--color-primary) 22%, transparent);
|
|
}
|
|
|
|
.btn-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|