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:
parent
58cdd61d27
commit
208cd970d7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,8 +19,8 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-nSPvOkeD.js"></script>
|
<script type="module" crossorigin src="/assets/main-C0_3Dz8C.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-CUROQg6p.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-t8j1WzN6.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
||||||
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
||||||
import NotesTab from './lib/components/notes/NotesTab.svelte'
|
import NotesTab from './lib/components/notes/NotesTab.svelte'
|
||||||
|
import OverviewTab from './lib/components/OverviewTab.svelte'
|
||||||
import FilesTab from './lib/components/files/FilesTab.svelte'
|
import FilesTab from './lib/components/files/FilesTab.svelte'
|
||||||
|
|
||||||
// ===== Wails v2 API call helper =====
|
// ===== Wails v2 API call helper =====
|
||||||
|
|
@ -2560,50 +2561,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
{#if activeTab === 'overview'}
|
{#if activeTab === 'overview'}
|
||||||
<div class="overview">
|
<OverviewTab
|
||||||
<h2>{selectedNode.title}</h2>
|
{selectedNode}
|
||||||
<div class="meta-grid">
|
{notes}
|
||||||
<div class="meta-item"><span class="meta-label">{t('overview.type')}</span><span>{nodeKindLabel(selectedNode.type)}</span></div>
|
{worklog}
|
||||||
<div class="meta-item"><span class="meta-label">{t('overview.section')}</span><span>{selectedNode.section || '—'}</span></div>
|
{formatDate}
|
||||||
<div class="meta-item"><span class="meta-label">{t('overview.created')}</span><span>{formatDate(selectedNode.createdAt)}</span></div>
|
{nodeKindLabel}
|
||||||
</div>
|
on:createNote={() => { setActiveTab('notes'); }}
|
||||||
<div class="quick-actions">
|
on:addFile={() => { setActiveTab('files'); if (filesTabRef) filesTabRef.addFile(); }}
|
||||||
<button class="qa-btn" on:click={() => { setActiveTab('notes'); openCreateNote() }}>
|
on:createAction={openCreateAction}
|
||||||
<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>
|
on:openNote={(e) => openNote(e.detail.note)}
|
||||||
{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>
|
|
||||||
|
|
||||||
{:else if activeTab === 'notes'}
|
{:else if activeTab === 'notes'}
|
||||||
<NotesTab
|
<NotesTab
|
||||||
|
|
@ -3685,23 +3653,6 @@
|
||||||
.form-group input:focus { border-color: #818cf8; }
|
.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; }
|
.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 */
|
||||||
.worklog-tab { padding: 24px; }
|
.worklog-tab { padding: 24px; }
|
||||||
.worklog-toolbar { margin-bottom: 16px; }
|
.worklog-toolbar { margin-bottom: 16px; }
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue