feat(design): surface phase PR 1 — Lucide icon migration + scale enforcement

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), &uarr; → ArrowUp
(ChatInputBar Send), &times; → 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>
This commit is contained in:
2026-04-27 19:56:01 -04:00
parent 54710b35e9
commit 93a3beb5d6
20 changed files with 164 additions and 131 deletions
+6 -6
View File
@@ -7,6 +7,7 @@ import { useAuthStore } from "@/stores/auth";
import { useChatStore } from "@/stores/chat";
import AppLogo from "@/components/AppLogo.vue";
import NotificationBell from "@/components/NotificationBell.vue";
import { Sun, Moon, Settings } from "lucide-vue-next";
const { theme, toggleTheme } = useTheme();
const { toggleShortcuts } = useShortcuts();
@@ -94,15 +95,13 @@ router.afterEach(() => {
<button class="btn-icon" @click="toggleShortcuts" title="Keyboard shortcuts (?)">?</button>
<button class="btn-icon" @click="toggleTheme" :title="`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`">
{{ theme === "dark" ? "\u2600" : "\u263E" }}
<Sun v-if="theme === 'dark'" :size="16" />
<Moon v-else :size="16" />
</button>
<!-- Settings link -->
<router-link to="/settings" class="btn-icon" aria-label="Settings" title="Settings">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<Settings :size="16" />
</router-link>
<div class="user-info">
@@ -137,7 +136,8 @@ router.afterEach(() => {
<span class="status-text">{{ statusShortLabel }}</span>
</span>
<button class="btn-icon" @click="toggleShortcuts">?</button>
<button class="btn-icon" @click="toggleTheme">{{ theme === "dark" ? "\u2600" : "\u263E" }}</button>
<button class="btn-icon" @click="toggleTheme"><Sun v-if="theme === 'dark'" :size="16" />
<Moon v-else :size="16" /></button>
</div>
<div class="mobile-user">
<span class="username">{{ authStore.user?.username }}</span>
+19 -18
View File
@@ -10,6 +10,15 @@ import { useChatStore } from '@/stores/chat'
import { useSettingsStore } from '@/stores/settings'
import { useToastStore } from '@/stores/toast'
import type { Note } from '@/types/note'
import {
Paperclip,
Volume2,
VolumeX,
Square,
Mic,
Loader2,
ArrowUp,
} from 'lucide-vue-next'
const props = withDefaults(defineProps<{
/** Textarea placeholder */
@@ -277,9 +286,7 @@ defineExpose({ focus, prefill })
:disabled="!store.chatReady"
title="Attach a note"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
</svg>
<Paperclip :size="16" />
</button>
<div v-if="showNotePicker" class="note-picker-dropdown">
<input
@@ -322,12 +329,8 @@ defineExpose({ focus, prefill })
:title="listenMode ? 'Listen mode on' : 'Listen / volume'"
aria-label="Listen and volume settings"
>
<svg v-if="!tts.speaking.value" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C21.8 14.82 22 13.43 22 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
</svg>
<Volume2 v-if="!tts.speaking.value" :size="16" />
<VolumeX v-else :size="16" />
</button>
<div v-if="speakerPopoverOpen" class="speaker-popover">
<button
@@ -354,7 +357,7 @@ defineExpose({ focus, prefill })
class="speaker-row speaker-row--stop"
@click="tts.stop()"
>
<svg width="12" height="12" viewBox="0 0 14 14" fill="currentColor"><rect width="14" height="14" rx="2"/></svg>
<Square :size="16" fill="currentColor" />
<span>Stop playback</span>
</button>
</div>
@@ -371,12 +374,8 @@ defineExpose({ focus, prefill })
:title="recorder.recording.value ? 'Click to stop (or wait for silence)' : 'Click to speak'"
:aria-label="recorder.recording.value ? 'Stop recording' : 'Start recording'"
>
<svg v-if="!transcribingVoice" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="8" opacity="0.35"/><circle cx="12" cy="12" r="4"/>
</svg>
<Mic v-if="!transcribingVoice" :size="16" />
<Loader2 v-else :size="16" class="mic-loader" />
</button>
</div>
@@ -387,14 +386,14 @@ defineExpose({ focus, prefill })
@click="emit('abort')"
title="Stop generation"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><rect width="14" height="14" rx="2"/></svg>
<Square :size="16" fill="currentColor" />
</button>
<button
v-else
class="btn-send"
@click="onSubmit"
:disabled="!messageInput.trim() || !store.chatReady"
>&uarr;</button>
><ArrowUp :size="16" /></button>
</div>
</div>
</template>
@@ -484,6 +483,8 @@ defineExpose({ focus, prefill })
z-index: 1;
}
.btn-mic.mic-transcribing { opacity: 0.5; }
.mic-loader { animation: mic-spin 1s linear infinite; }
@keyframes mic-spin { to { transform: rotate(360deg); } }
/* Mic wrapper + live amplitude halo. The glow is a filled red disc
absolutely positioned behind the button, scaled/faded by the
+7 -6
View File
@@ -6,6 +6,7 @@ import ChatStreamingBubble from '@/components/ChatStreamingBubble.vue'
import ChatInputBar from '@/components/ChatInputBar.vue'
import ToolCallCard from '@/components/ToolCallCard.vue'
import type { Message, ToolCallRecord } from '@/types/chat'
import { Mic, ChevronDown, X } from 'lucide-vue-next'
const props = withDefaults(defineProps<{
variant: 'full' | 'widget'
@@ -372,7 +373,7 @@ defineExpose({ focus, prefill, send, contextCount, sidebarOpen, toggleContextSid
</router-link>
</div>
<button class="empty-voice-btn" @click="startVoiceMode" title="Start in voice mode">
<span class="voice-icon">🎤</span>
<Mic class="voice-icon" :size="16" />
<span>Start in voice mode</span>
</button>
</div>
@@ -391,7 +392,7 @@ defineExpose({ focus, prefill, send, contextCount, sidebarOpen, toggleContextSid
:class="{ collapsed: collapsedSections.has('auto') }"
@click="toggleSection('auto')"
>
<svg class="chev" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M2 3l3 4 3-4z"/></svg>
<ChevronDown class="chev" :size="16" />
<span>Auto-included</span>
<span class="section-count">{{ autoInjectedNotes.length }}</span>
</button>
@@ -404,7 +405,7 @@ defineExpose({ focus, prefill, send, contextCount, sidebarOpen, toggleContextSid
class="context-note-score"
:class="{ 'score-high': note.score >= 0.75, 'score-medium': note.score >= 0.60 && note.score < 0.75, 'score-low': note.score < 0.60 }"
>{{ Math.round(note.score * 100) }}%</span>
<button class="context-note-remove" @click="excludeAutoNote(note.id)" title="Remove from context">&times;</button>
<button class="context-note-remove" @click="excludeAutoNote(note.id)" title="Remove from context"><X :size="16" /></button>
</div>
</div>
</template>
@@ -414,7 +415,7 @@ defineExpose({ focus, prefill, send, contextCount, sidebarOpen, toggleContextSid
:class="{ collapsed: collapsedSections.has('suggested'), 'context-sidebar-header-gap': autoInjectedNotes.length }"
@click="toggleSection('suggested')"
>
<svg class="chev" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M2 3l3 4 3-4z"/></svg>
<ChevronDown class="chev" :size="16" />
<span>Suggested</span>
<span class="section-count">{{ suggestedNotes.length }}</span>
</button>
@@ -436,14 +437,14 @@ defineExpose({ focus, prefill, send, contextCount, sidebarOpen, toggleContextSid
:class="{ collapsed: collapsedSections.has('included'), 'context-sidebar-header-gap': autoInjectedNotes.length || suggestedNotes.length }"
@click="toggleSection('included')"
>
<svg class="chev" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M2 3l3 4 3-4z"/></svg>
<ChevronDown class="chev" :size="16" />
<span>In Context</span>
<span class="section-count">{{ includedNotes.length }}</span>
</button>
<div v-show="!collapsedSections.has('included')">
<div v-for="note in includedNotes" :key="note.id" class="context-note context-note-included">
<router-link :to="`/notes/${note.id}`" class="context-note-name">{{ note.title }}</router-link>
<button class="context-note-remove" @click="removeIncludedNote(note.id)" title="Remove from context">&times;</button>
<button class="context-note-remove" @click="removeIncludedNote(note.id)" title="Remove from context"><X :size="16" /></button>
</div>
</div>
</template>
+3 -2
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { X } from "lucide-vue-next";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { createEvent, updateEvent, deleteEvent, type EventEntry, type EventCreatePayload, type EventUpdatePayload } from "@/api/client";
import ProjectSelector from "@/components/ProjectSelector.vue";
@@ -259,7 +260,7 @@ async function doDelete() {
<div class="slide-over-panel" role="dialog" aria-modal="true">
<div class="so-header">
<h2 class="so-title">{{ isEditMode ? "Edit Event" : "New Event" }}</h2>
<button class="so-close" @click="emit('close')" aria-label="Close"></button>
<button class="so-close" @click="emit('close')" aria-label="Close"><X :size="16" /></button>
</div>
<form class="so-form" @submit.prevent="save">
@@ -316,7 +317,7 @@ async function doDelete() {
<div class="color-row">
<input v-model="color" type="color" class="color-picker" title="Pick event color" />
<input v-model="color" class="so-input color-hex" placeholder="#5B4A8A" />
<button v-if="color" type="button" class="btn-clear-color" @click="color = ''"></button>
<button v-if="color" type="button" class="btn-clear-color" @click="color = ''"><X :size="16" /></button>
</div>
</div>
+33 -17
View File
@@ -1,24 +1,40 @@
<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 }>();
// 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 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 = [
@@ -81,7 +97,7 @@ const groups = [
tabindex="-1"
@mousedown.prevent="btn.command()"
>
<span class="btn-icon" v-html="ICONS[btn.id]" />
<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" />
+2 -4
View File
@@ -2,6 +2,7 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { useNotificationsStore } from '@/stores/notifications'
import NotificationsPanel from './NotificationsPanel.vue'
import { Bell } from 'lucide-vue-next'
const store = useNotificationsStore()
const open = ref(false)
@@ -45,10 +46,7 @@ onUnmounted(() => {
aria-label="Notifications"
:title="`${store.count} unread notification${store.count !== 1 ? 's' : ''}`"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
<Bell :size="16" aria-hidden="true" />
<span v-if="store.count > 0" class="bell-badge" aria-live="polite">{{ store.count > 99 ? '99+' : store.count }}</span>
</button>
<NotificationsPanel v-if="open" @close="close" />
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { X } from "lucide-vue-next";
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useNotificationsStore } from '@/stores/notifications'
@@ -56,7 +57,7 @@ onMounted(() => store.fetchAll())
</p>
<span class="notif-time">{{ relativeTime(n.created_at) }}</span>
</div>
<button class="btn-notif-close" @click.stop="store.markRead(n.id)" aria-label="Dismiss"></button>
<button class="btn-notif-close" @click.stop="store.markRead(n.id)" aria-label="Dismiss"><X :size="16" /></button>
</li>
</ul>
<div v-else class="notif-empty">No unread notifications</div>
+3 -2
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { X } from "lucide-vue-next";
import { ref, onMounted } from 'vue'
import {
searchUsers,
@@ -116,7 +117,7 @@ onMounted(async () => {
<div class="share-dialog" role="dialog" :aria-label="`Share ${resourceTitle}`">
<header class="share-header">
<h2 class="share-title">Share "{{ resourceTitle }}"</h2>
<button class="btn-close" @click="emit('close')" aria-label="Close"></button>
<button class="btn-close" @click="emit('close')" aria-label="Close"><X :size="16" /></button>
</header>
<!-- Add share form -->
@@ -183,7 +184,7 @@ onMounted(async () => {
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<button class="btn-remove-share" @click="removeShare(share)" aria-label="Remove"></button>
<button class="btn-remove-share" @click="removeShare(share)" aria-label="Remove"><X :size="16" /></button>
</li>
<li v-if="!shares.length" class="shares-empty">Not shared with anyone yet</li>
</ul>
@@ -3,6 +3,7 @@ import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue';
import { useChatStore } from '@/stores/chat';
import ChatPanel from '@/components/ChatPanel.vue';
import ChatInputBar from '@/components/ChatInputBar.vue';
import { RotateCcw, ChevronDown, ChevronUp } from 'lucide-vue-next';
const props = defineProps<{ projectId: number }>();
@@ -195,11 +196,11 @@ defineExpose({ prefill });
</button>
</div>
</div>
<button class="btn-icon-sm" title="Restart conversation" @click="restart"></button>
<button class="btn-icon-sm" title="Restart conversation" @click="restart">
<RotateCcw :size="16" />
</button>
<button class="btn-icon-sm" title="Collapse" @click="collapse">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="6 9 12 15 18 9"/>
</svg>
<ChevronDown :size="16" />
</button>
</div>
</div>
@@ -217,9 +218,7 @@ defineExpose({ prefill });
<!-- Collapsed: standalone input bar with expand affordance -->
<div v-else class="ws-chat-collapsed">
<button class="ws-chat-expand-btn" title="Expand chat" @click="expand">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="18 15 12 9 6 15"/>
</svg>
<ChevronUp :size="16" />
</button>
<div class="ws-chat-input-row">
<ChatInputBar
@@ -10,6 +10,7 @@ import TiptapEditor from "@/components/TiptapEditor.vue";
import TagInput from "@/components/TagInput.vue";
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
import WordCount from "@/components/WordCount.vue";
import { Trash2, X } from "lucide-vue-next";
const props = defineProps<{
projectId: number;
@@ -316,7 +317,7 @@ defineExpose({ reload: loadProjectNotes });
<button class="btn-confirm" :disabled="creatingNote || !newNoteTitle.trim()" @click="createNote">
{{ creatingNote ? '' : '+' }}
</button>
<button class="btn-cancel" aria-label="Cancel new note" @click="cancelNewNote"></button>
<button class="btn-cancel" aria-label="Cancel new note" @click="cancelNewNote"><X :size="16" /></button>
</template>
<template v-else>
<span class="rail-title">Notes</span>
@@ -332,7 +333,7 @@ defineExpose({ reload: loadProjectNotes });
type="search"
aria-label="Search notes"
/>
<button v-if="searchQuery" class="btn-search-clear" aria-label="Clear search" @click="searchQuery = ''"></button>
<button v-if="searchQuery" class="btn-search-clear" aria-label="Clear search" @click="searchQuery = ''"><X :size="16" /></button>
</div>
<div v-if="listLoading" class="rail-state">Loading</div>
@@ -360,10 +361,10 @@ defineExpose({ reload: loadProjectNotes });
<button class="btn-confirm-delete" :disabled="pendingDelete === note.id" @click="requestDelete(note.id, $event)">
{{ pendingDelete === note.id ? '' : 'Delete?' }}
</button>
<button class="btn-cancel-delete" aria-label="Cancel delete" @click="cancelDelete($event)"></button>
<button class="btn-cancel-delete" aria-label="Cancel delete" @click="cancelDelete($event)"><X :size="16" /></button>
</template>
<button v-else class="btn-delete" title="Delete note" @click="requestDelete(note.id, $event)">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
<Trash2 :size="16" />
</button>
</div>
</li>
@@ -416,7 +417,7 @@ defineExpose({ reload: loadProjectNotes });
:class="['btn-tag-suggestion', { applied: tagSuggestions.appliedTags.value.has(tag) }]"
@click="tagSuggestions.applyTagSuggestion(tag)"
>#{{ tag }}{{ tagSuggestions.appliedTags.value.has(tag) ? ' ✓' : '' }}</button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss tag suggestions" @click="tagSuggestions.dismissTagSuggestions()"></button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss tag suggestions" @click="tagSuggestions.dismissTagSuggestions()"><X :size="16" /></button>
</div>
<div class="toolbar-row">
@@ -429,7 +430,7 @@ defineExpose({ reload: loadProjectNotes });
<button class="btn-chip-link" @click="applyLink(s)">[[{{ s.title }}]]</button>
</span>
<button class="btn-link-all" @click="applyAllLinks" title="Link all suggestions">All</button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"></button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"><X :size="16" /></button>
</div>
<div class="editor-area" @keydown.ctrl.s.prevent="saveNote" @keydown.ctrl.e.prevent="editorRef?.editor?.commands.focus()">
@@ -5,6 +5,7 @@ import { apiGet, apiPatch, apiPost, apiDelete } from "@/api/client";
import { useToastStore } from "@/stores/toast";
import TaskLogSection from "@/components/TaskLogSection.vue";
import { renderMarkdown } from "@/utils/markdown";
import { Trash2, X } from "lucide-vue-next";
const props = defineProps<{ projectId: number }>();
@@ -298,12 +299,12 @@ defineExpose({ reload: loadAll });
</span>
<template v-if="deleteConfirmPending">
<button class="btn-delete-confirm" :disabled="deletingTask" @click="deleteActiveTask">{{ deletingTask ? '...' : 'Delete?' }}</button>
<button class="btn-delete-cancel" aria-label="Cancel delete" @click="cancelDeleteTask"></button>
<button class="btn-delete-cancel" aria-label="Cancel delete" @click="cancelDeleteTask"><X :size="16" /></button>
</template>
<button v-else class="btn-delete-task" title="Delete task" @click="deleteActiveTask">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
<Trash2 :size="16" />
</button>
<button class="btn-close-detail" @click="closeTask" aria-label="Close detail"></button>
<button class="btn-close-detail" @click="closeTask" aria-label="Close detail"><X :size="16" /></button>
</div>
<h3 class="detail-title">{{ activeTask.title }}</h3>