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
+19 -8
View File
@@ -5,6 +5,16 @@ import { apiGet, apiPatch, apiDelete, apiPost } from "@/api/client";
import { useToastStore } from "@/stores/toast";
import { relativeTime } from "@/composables/useRelativeTime";
import ShareDialog from "@/components/ShareDialog.vue";
import {
LayoutGrid,
Clock,
FileText,
ChevronRight,
ChevronDown,
Pencil,
Trash2,
Check,
} from "lucide-vue-next";
interface Milestone {
id: number;
@@ -330,7 +340,7 @@ async function confirmDelete() {
<router-link to="/projects" class="btn-back"> Projects</router-link>
<div class="page-header-actions">
<router-link v-if="project" :to="`/workspace/${project.id}`" class="btn-workspace">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
<LayoutGrid :size="16" />
Workspace
</router-link>
<button v-if="project" class="btn-share" @click="showShare = true">Share</button>
@@ -359,7 +369,7 @@ async function confirmDelete() {
</div>
<p v-if="project.goal" class="project-goal">{{ project.goal }}</p>
<p v-if="project.summary?.last_activity" class="project-activity">
<svg viewBox="0 0 24 24" width="11" height="11" fill="currentColor" aria-hidden="true"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67V7z"/></svg>
<Clock :size="16" />
Active {{ relativeTime(project.summary.last_activity) }}
</p>
</div>
@@ -382,7 +392,7 @@ async function confirmDelete() {
<span class="stat-label">done</span>
</div>
<div class="stat-chip stat-notes">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true" style="opacity:0.6"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
<FileText :size="16" style="opacity:0.6" />
<span class="stat-val">{{ project.summary.note_count }}</span>
<span class="stat-label">notes</span>
</div>
@@ -463,7 +473,8 @@ async function confirmDelete() {
@click="group.milestone && renamingMilestoneId !== group.milestone.id && toggleMilestoneCollapse(group.milestone.id)"
>
<span class="ms-chevron" v-if="group.milestone">
<svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor"><path :d="collapsedMilestones.has(group.milestone.id) ? 'M8 5l8 7-8 7V5z' : 'M5 8l7 8 7-8H5z'"/></svg>
<ChevronRight v-if="collapsedMilestones.has(group.milestone.id)" :size="16" />
<ChevronDown v-else :size="16" />
</span>
<template v-if="group.milestone && renamingMilestoneId === group.milestone.id">
<input
@@ -485,10 +496,10 @@ async function confirmDelete() {
<span class="ms-pct">{{ group.milestone.pct }}%</span>
<div class="ms-actions" @click.stop>
<button class="ms-action-btn" title="Rename" @click="startRenameMilestone(group.milestone)">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
<Pencil :size="16" />
</button>
<button class="ms-action-btn ms-action-delete" title="Delete" @click="deletingMilestone = group.milestone">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<Trash2 :size="16" />
</button>
</div>
</template>
@@ -554,7 +565,7 @@ async function confirmDelete() {
title="Mark as Done"
:disabled="advancingTaskId === task.id"
@click="advanceTaskStatus(task, $event)"
></button>
><Check :size="16" /></button>
</div>
</router-link>
<p v-if="!group.tasks.filter(t => t.status === 'in_progress').length" class="col-empty">No tasks</p>
@@ -598,7 +609,7 @@ async function confirmDelete() {
</div>
<template v-else>
<router-link v-for="note in notes" :key="note.id" :to="`/notes/${note.id}`" class="note-row">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" class="note-icon" aria-hidden="true"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
<FileText class="note-icon" :size="16" />
<span class="note-title">{{ note.title || "Untitled" }}</span>
<span class="note-date">{{ relativeTime(note.updated_at) }}</span>
</router-link>