93a3beb5d6
Replaces every hand-inlined SVG in the chrome (60 across 15 files) with Lucide components. Snaps icon sizes to the doc's 16/24 scale — all icons in this pass land at 16. AppLogo wordmark stays as the legitimate "custom app icon" exception per the doc; GraphView's <svg> mount point for D3 stays as well. Replaces emoji-as-icons in clear button-affordance cases with their Lucide equivalents: ✕ → X (close/dismiss buttons across 8 files), ✓ → Check (ProjectView task-advance button), 🎤 → Mic (ChatPanel voice CTA), 📎 → Paperclip (ChatView context toggle), ↑ → ArrowUp (ChatInputBar Send), × → X (ChatPanel context-note remove), ☀/☾ → Sun/Moon (AppHeader theme toggle). Inline emoji punctuation in textual confirmation copy ("Saved ✓", "Created ✓", `done: "✓"` status maps, `'✓' : '📄'` interpolations) is left for surface-phase voice/tone touchups — replacing requires structural template changes and is best done per-component. Stroke weight stays at Lucide's default 2px; tightening to the doc's 1.5/1 is deferred per the surface-phase spec (option ii: drop-in + scale enforcement). Adds lucide-vue-next ^0.469.0 to frontend/package.json. Docker rebuild handles the install. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
175 lines
5.4 KiB
Vue
175 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import type { Editor } from "@tiptap/vue-3";
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Strikethrough,
|
|
Heading1,
|
|
Heading2,
|
|
Heading3,
|
|
List,
|
|
ListOrdered,
|
|
ListChecks,
|
|
Code,
|
|
SquareCode,
|
|
Quote,
|
|
Link as LinkIcon,
|
|
Brackets,
|
|
} from "lucide-vue-next";
|
|
import type { Component } from "vue";
|
|
|
|
const props = defineProps<{ editor: Editor | null }>();
|
|
|
|
const ICONS: Record<string, Component> = {
|
|
bold: Bold,
|
|
italic: Italic,
|
|
strike: Strikethrough,
|
|
h1: Heading1,
|
|
h2: Heading2,
|
|
h3: Heading3,
|
|
ul: List,
|
|
ol: ListOrdered,
|
|
task: ListChecks,
|
|
code: Code,
|
|
codeblock: SquareCode,
|
|
quote: Quote,
|
|
link: LinkIcon,
|
|
wikilink: Brackets,
|
|
};
|
|
|
|
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"
|
|
tabindex="-1"
|
|
@mousedown.prevent="btn.command()"
|
|
>
|
|
<component :is="ICONS[btn.id]" class="btn-icon" :size="16" />
|
|
</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>
|