UI refinement: toolbar icons, gradient CTA, Fraunces title, viewer polish
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>
This commit is contained in:
@@ -1,130 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import type { Editor } from "@tiptap/vue-3";
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor | null;
|
||||
}>();
|
||||
const props = defineProps<{ editor: Editor | null }>();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: "B",
|
||||
title: "Bold",
|
||||
command: () => props.editor?.chain().focus().toggleBold().run(),
|
||||
isActive: () => props.editor?.isActive("bold") ?? false,
|
||||
},
|
||||
{
|
||||
label: "I",
|
||||
title: "Italic",
|
||||
command: () => props.editor?.chain().focus().toggleItalic().run(),
|
||||
isActive: () => props.editor?.isActive("italic") ?? false,
|
||||
},
|
||||
{
|
||||
label: "H1",
|
||||
title: "Heading 1",
|
||||
command: () =>
|
||||
props.editor?.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => props.editor?.isActive("heading", { level: 1 }) ?? false,
|
||||
},
|
||||
{
|
||||
label: "H2",
|
||||
title: "Heading 2",
|
||||
command: () =>
|
||||
props.editor?.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => props.editor?.isActive("heading", { level: 2 }) ?? false,
|
||||
},
|
||||
{
|
||||
label: "Link",
|
||||
title: "Link",
|
||||
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();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
isActive: () => props.editor?.isActive("link") ?? false,
|
||||
},
|
||||
{
|
||||
label: "UL",
|
||||
title: "Bullet List",
|
||||
command: () => props.editor?.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => props.editor?.isActive("bulletList") ?? false,
|
||||
},
|
||||
{
|
||||
label: "OL",
|
||||
title: "Numbered List",
|
||||
command: () => props.editor?.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => props.editor?.isActive("orderedList") ?? false,
|
||||
},
|
||||
{
|
||||
label: "`",
|
||||
title: "Inline Code",
|
||||
command: () => props.editor?.chain().focus().toggleCode().run(),
|
||||
isActive: () => props.editor?.isActive("code") ?? false,
|
||||
},
|
||||
{
|
||||
label: "```",
|
||||
title: "Code Block",
|
||||
command: () => props.editor?.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => props.editor?.isActive("codeBlock") ?? false,
|
||||
},
|
||||
{
|
||||
label: "☑",
|
||||
title: "Task List",
|
||||
command: () => props.editor?.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => props.editor?.isActive("taskList") ?? false,
|
||||
},
|
||||
{
|
||||
label: "[[",
|
||||
title: "Insert wikilink (type note title to search)",
|
||||
command: () => props.editor?.chain().focus().insertContent("[[").run(),
|
||||
isActive: () => false,
|
||||
},
|
||||
{
|
||||
id: "wikilink",
|
||||
title: "Insert wikilink [[ ]]",
|
||||
isActive: () => false,
|
||||
command: () => props.editor?.chain().focus().insertContent("[[").run(),
|
||||
},
|
||||
],
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="md-toolbar">
|
||||
<button
|
||||
v-for="btn in buttons"
|
||||
:key="btn.label"
|
||||
:class="['md-btn', { active: btn.isActive() }]"
|
||||
:title="btn.title"
|
||||
@click="btn.command()"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
<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 {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: var(--radius-sm);
|
||||
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);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
.md-btn:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.md-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: var(--color-primary);
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user