refactor(frontend): extract OverviewTab safely

- New component: frontend/src/lib/components/OverviewTab.svelte
  - Props: selectedNode, notes, worklog, formatDate, nodeKindLabel
  - Events: createNote, addFile, createAction, switchTab, openNote
  - Pure display component, no internal state

- App.svelte changes:
  - Replaced inline overview markup with <OverviewTab>
  - Removed overview-related CSS (overview, meta-grid, qa-btn, etc.)
  - openCreateNote() call replaced with setActiveTab('notes') only
    (create note is now handled by NotesTab component)

- Build: npm run build , go test ./... 
This commit is contained in:
mirivlad 2026-06-16 03:29:18 +08:00
parent 58cdd61d27
commit 208cd970d7
6 changed files with 199 additions and 151 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,8 +19,8 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-nSPvOkeD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CUROQg6p.css">
<script type="module" crossorigin src="/assets/main-C0_3Dz8C.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-t8j1WzN6.css">
</head>
<body>
<div id="app"></div>

View File

@ -19,6 +19,7 @@
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
import NotesTab from './lib/components/notes/NotesTab.svelte'
import OverviewTab from './lib/components/OverviewTab.svelte'
import FilesTab from './lib/components/files/FilesTab.svelte'
// ===== Wails v2 API call helper =====
@ -2560,50 +2561,17 @@
</div>
<div class="tab-content">
{#if activeTab === 'overview'}
<div class="overview">
<h2>{selectedNode.title}</h2>
<div class="meta-grid">
<div class="meta-item"><span class="meta-label">{t('overview.type')}</span><span>{nodeKindLabel(selectedNode.type)}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.section')}</span><span>{selectedNode.section || '—'}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.created')}</span><span>{formatDate(selectedNode.createdAt)}</span></div>
</div>
<div class="quick-actions">
<button class="qa-btn" on:click={() => { setActiveTab('notes'); openCreateNote() }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
{t('overview.newNote')}
</button>
<button class="qa-btn" on:click={() => { setActiveTab('files'); if (filesTabRef) filesTabRef.addFile(); }}>
<svg width="16" height="16" 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 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
{t('overview.addFile')}
</button>
<button class="qa-btn" on:click={openCreateAction}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
{t('overview.addAction')}
</button>
<button class="qa-btn" on:click={() => setActiveTab('worklog')}>
<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="10"/><polyline points="12 6 12 12 16 14"/></svg>
{t('overview.logTime')}
</button>
</div>
{#if notes.length > 0}
<div class="recent-section">
<h3>{t('overview.recentNotes')}</h3>
{#each notes.slice(0, 5) as note}
<div class="recent-note" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span>
</div>
{/each}
</div>
{/if}
{#if worklog.length > 0}
<div class="recent-section">
<h3>{t('overview.recentEntries')}</h3>
{#each worklog.slice(0, 3) as e}
<div class="recent-entry">{e.summary} ({e.minutes} {t('worklog.min')})</div>
{/each}
</div>
{/if}
</div>
<OverviewTab
{selectedNode}
{notes}
{worklog}
{formatDate}
{nodeKindLabel}
on:createNote={() => { setActiveTab('notes'); }}
on:addFile={() => { setActiveTab('files'); if (filesTabRef) filesTabRef.addFile(); }}
on:createAction={openCreateAction}
on:openNote={(e) => openNote(e.detail.note)}
/>
{:else if activeTab === 'notes'}
<NotesTab
@ -3685,23 +3653,6 @@
.form-group input:focus { border-color: #818cf8; }
.note-textarea { flex: 1; width: 100%; border: none; outline: none; background: #13131f; color: #e4e4ef; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 14px; line-height: 1.6; padding: 24px; resize: none; }
/* Overview */
.overview { padding: 24px; }
.overview h2 { font-size: 24px; margin-bottom: 16px; }
.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
.meta-item { background: #1a1a28; padding: 12px 16px; border-radius: 8px; }
.meta-label { display: block; font-size: 11px; color: #666; margin-bottom: 4px; text-transform: uppercase; }
.quick-actions { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.qa-btn { padding: 10px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
.qa-btn:hover { background: #222233; }
.qa-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.recent-section { margin-bottom: 24px; }
.recent-section h3 { font-size: 13px; color: #666; text-transform: uppercase; margin-bottom: 8px; }
.recent-note { padding: 8px 12px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; }
.recent-note:hover { background: #1a1a28; }
.recent-date { font-size: 11px; color: #555; }
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
/* Worklog tab */
.worklog-tab { padding: 24px; }
.worklog-toolbar { margin-bottom: 16px; }

View File

@ -0,0 +1,97 @@
<script>
import { createEventDispatcher } from 'svelte'
import { t } from '../i18n'
// ===== Props =====
export let selectedNode = null
export let notes = []
export let worklog = []
export let formatDate = (str) => ''
export let nodeKindLabel = (kind) => kind || ''
// ===== Events =====
const dispatch = createEventDispatcher()
function createNote() {
dispatch('createNote')
}
function addFile() {
dispatch('addFile')
}
function createAction() {
dispatch('createAction')
}
function logTime() {
dispatch('switchTab', { tab: 'worklog' })
}
function openNoteHandler(note) {
dispatch('openNote', { note })
}
</script>
<div class="overview">
<h2>{selectedNode.title}</h2>
<div class="meta-grid">
<div class="meta-item"><span class="meta-label">{t('overview.type')}</span><span>{nodeKindLabel(selectedNode.type)}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.section')}</span><span>{selectedNode.section || '—'}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.created')}</span><span>{formatDate(selectedNode.createdAt)}</span></div>
</div>
<div class="quick-actions">
<button class="qa-btn" on:click={createNote}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
{t('overview.newNote')}
</button>
<button class="qa-btn" on:click={addFile}>
<svg width="16" height="16" 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 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
{t('overview.addFile')}
</button>
<button class="qa-btn" on:click={createAction}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
{t('overview.addAction')}
</button>
<button class="qa-btn" on:click={logTime}>
<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="10"/><polyline points="12 6 12 12 16 14"/></svg>
{t('overview.logTime')}
</button>
</div>
{#if notes.length > 0}
<div class="recent-section">
<h3>{t('overview.recentNotes')}</h3>
{#each notes.slice(0, 5) as note}
<div class="recent-note" role="button" tabindex="0" on:click={() => openNoteHandler(note)} on:keydown={(e) => e.key === 'Enter' && openNoteHandler(note)}>
<span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span>
</div>
{/each}
</div>
{/if}
{#if worklog.length > 0}
<div class="recent-section">
<h3>{t('overview.recentEntries')}</h3>
{#each worklog.slice(0, 3) as e}
<div class="recent-entry">{e.summary} ({e.minutes} {t('worklog.min')})</div>
{/each}
</div>
{/if}
</div>
<style>
.overview { padding: 24px; }
.overview h2 { font-size: 24px; margin-bottom: 16px; }
.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
.meta-item { background: #1a1a28; padding: 12px 16px; border-radius: 8px; }
.meta-label { display: block; font-size: 11px; color: #666; margin-bottom: 4px; text-transform: uppercase; }
.quick-actions { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.qa-btn { padding: 10px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
.qa-btn:hover { background: #222233; }
.qa-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.recent-section { margin-bottom: 24px; }
.recent-section h3 { font-size: 13px; color: #666; text-transform: uppercase; margin-bottom: 8px; }
.recent-note { padding: 8px 12px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; }
.recent-note:hover { background: #1a1a28; }
.recent-date { font-size: 11px; color: #555; }
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
</style>