gui: complete Wails v2 vertical MVP — fixes, search, polished UI
Backend fixes: - Wire search service in App struct, implement Search() bindings - Fix OpenFile to use files.Service.Open() instead of stub - Fix OpenFolder to open spaces/<slug>/ instead of vault root - Remove unused imports and dead code in app.go Frontend fixes: - Add missing Svelte plugin to vite.config.js (blocking build error) - Fix optional catch binding for compatibility - Fix select dropdown rendering on Linux (appearance: none + custom arrow) - Switch api/verstak.js to use generated Wails v2 bindings - Include hand-written wailsjs bindings in repository - Add build.sh to repository Build: cd frontend && npm run build rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist go build -tags 'gui production webkit2_41' -o verstak-gui ./cmd/verstak-gui
This commit is contained in:
parent
b4010a5a24
commit
645d8878cc
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
cd frontend && npm run build && cd ..
|
||||
rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist
|
||||
go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
"verstak/internal/core/files"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/nodes"
|
||||
"verstak/internal/core/search"
|
||||
"verstak/internal/core/storage"
|
||||
"verstak/internal/core/worklog"
|
||||
)
|
||||
|
|
@ -26,6 +28,7 @@ type App struct {
|
|||
notes *notes.Service
|
||||
actions *actions.Service
|
||||
worklog *worklog.Service
|
||||
search *search.Service
|
||||
vault string
|
||||
}
|
||||
|
||||
|
|
@ -296,14 +299,27 @@ func (a *App) CreateWorklog(nodeID, summary string, minutes int) (*WorklogDTO, e
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Search (stubs — search service not wired yet)
|
||||
// Search
|
||||
// ============================================================
|
||||
|
||||
func (a *App) Search(query string) ([]SearchResultDTO, error) {
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return []SearchResultDTO{}, nil
|
||||
}
|
||||
return []SearchResultDTO{}, nil
|
||||
results, err := a.search.Search(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]SearchResultDTO, len(results))
|
||||
for i, r := range results {
|
||||
out[i] = SearchResultDTO{
|
||||
NodeID: r.NodeID,
|
||||
Title: r.Title,
|
||||
Snippet: r.Snippet,
|
||||
Type: r.Type,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -333,11 +349,19 @@ func (a *App) PickDirectory() (string, error) {
|
|||
// ============================================================
|
||||
|
||||
func (a *App) OpenFile(fileID string) error {
|
||||
return fmt.Errorf("not implemented: %s", fileID)
|
||||
return a.files.Open(fileID)
|
||||
}
|
||||
|
||||
func (a *App) OpenFolder(nodeID string) error {
|
||||
cmd := exec.Command("xdg-open", a.vault)
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get node: %w", err)
|
||||
}
|
||||
dir := filepath.Join(a.vault, "spaces", n.Slug)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
dir = a.vault
|
||||
}
|
||||
cmd := exec.Command("xdg-open", dir)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
|
|
@ -377,7 +401,4 @@ func toNodeDTOs(list []nodes.Node) []NodeDTO {
|
|||
return result
|
||||
}
|
||||
|
||||
var (
|
||||
_ = os.Getenv
|
||||
_ = exec.Command
|
||||
)
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.svelte-14ysusk.svelte-14ysusk,.svelte-14ysusk.svelte-14ysusk:before,.svelte-14ysusk.svelte-14ysusk:after{box-sizing:border-box;margin:0;padding:0}.app.svelte-14ysusk.svelte-14ysusk{display:flex;width:100vw;height:100vh;overflow:hidden;background:#13131f;color:#e4e4ef;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px}.sidebar.svelte-14ysusk.svelte-14ysusk{width:260px;min-width:200px;height:100vh;display:flex;flex-direction:column;background:#1a1a28;border-right:1px solid #2a2a3c;flex-shrink:0;overflow:hidden}.sidebar-top.svelte-14ysusk.svelte-14ysusk{padding:16px 20px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #2a2a3c;flex-shrink:0}.logo.svelte-14ysusk.svelte-14ysusk{font-size:20px;line-height:1}.app-name.svelte-14ysusk.svelte-14ysusk{font-size:16px;font-weight:600;color:#e4e4ef}.sidebar-nav.svelte-14ysusk.svelte-14ysusk{flex:1;overflow-y:auto;padding:12px 0}.nav-group.svelte-14ysusk.svelte-14ysusk{margin-bottom:16px}.nav-label.svelte-14ysusk.svelte-14ysusk{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#666;padding:4px 20px;margin-bottom:4px}.nav-item.svelte-14ysusk.svelte-14ysusk{display:block;width:100%;padding:8px 20px;border:none;background:none;color:#ccc;font-size:13px;text-align:left;cursor:pointer;border-radius:0;font-family:inherit}.nav-item.svelte-14ysusk.svelte-14ysusk:hover{background:#223}.nav-item.selected.svelte-14ysusk.svelte-14ysusk{background:#2a2a4a;color:#fff;font-weight:500}.nav-empty.svelte-14ysusk.svelte-14ysusk{padding:8px 20px;color:#555;font-size:12px}.sidebar-bottom.svelte-14ysusk.svelte-14ysusk{padding:12px 20px;border-top:1px solid #2a2a3c;flex-shrink:0}.version.svelte-14ysusk.svelte-14ysusk{font-size:11px;color:#555}.main.svelte-14ysusk.svelte-14ysusk{flex:1;display:flex;flex-direction:column;height:100vh;min-width:0;overflow:hidden;background:#13131f}.header.svelte-14ysusk.svelte-14ysusk{padding:12px 24px;border-bottom:1px solid #2a2a3c;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;min-height:48px}.crumb.svelte-14ysusk.svelte-14ysusk{font-size:14px;font-weight:500;color:#e4e4ef}.crumb.placeholder.svelte-14ysusk.svelte-14ysusk{color:#666}.search-hint.svelte-14ysusk.svelte-14ysusk{padding:6px 12px;background:#1e1e2e;border:1px solid #2a2a3c;border-radius:4px;color:#666;font-size:12px;cursor:text}.error-banner.svelte-14ysusk.svelte-14ysusk{background:#3a2222;color:#f88;padding:8px 24px;font-size:12px;border-bottom:1px solid #4a2222;flex-shrink:0}.content.svelte-14ysusk.svelte-14ysusk{flex:1;overflow-y:auto;padding:24px}.welcome.svelte-14ysusk h2.svelte-14ysusk{font-size:28px;font-weight:300;margin-bottom:12px;color:#8888a4}.welcome.svelte-14ysusk p.svelte-14ysusk{color:#666;font-size:13px;margin-bottom:4px}.error-text.svelte-14ysusk.svelte-14ysusk{color:#f88;margin-top:12px}.loading.svelte-14ysusk.svelte-14ysusk{color:#666}.node-view.svelte-14ysusk h2.svelte-14ysusk{font-size:24px;margin-bottom:16px}.node-meta.svelte-14ysusk.svelte-14ysusk{display:flex;gap:16px;color:#666;font-size:12px}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,8 +16,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-COs6tJEl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ClxkTvdE.css">
|
||||
<script type="module" crossorigin src="/assets/main-BqdVWy5o.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-D8LYjC_e.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ func main() {
|
|||
worklogSvc := worklog.NewService(db)
|
||||
searchSvc := search.NewService(db)
|
||||
plugins.NewManager(abs).Discover()
|
||||
_ = searchSvc
|
||||
|
||||
app := &App{
|
||||
db: db,
|
||||
|
|
@ -58,6 +57,7 @@ func main() {
|
|||
notes: noteSvc,
|
||||
actions: actionSvc,
|
||||
worklog: worklogSvc,
|
||||
search: searchSvc,
|
||||
vault: abs,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +1,214 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
// ===== Wails v2 API call helper =====
|
||||
// In production: window['go']['main']['App']['MethodName'](...args)
|
||||
// In dev without Wails: fallback to mock data
|
||||
function wailsCall(method, ...args) {
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
const fn = window['go']['main']['App'][method]
|
||||
if (typeof fn === 'function') {
|
||||
return fn(...args)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Wails call error:', method, e)
|
||||
}
|
||||
return Promise.reject(new Error('Wails not connected: ' + method))
|
||||
}
|
||||
|
||||
// ===== State =====
|
||||
let sections = []
|
||||
let nodes = []
|
||||
let version = ''
|
||||
let error = ''
|
||||
let selectedSection = ''
|
||||
let selectedNode = null
|
||||
let activeTab = 'overview'
|
||||
let notes = []
|
||||
let noteEditor = null
|
||||
let files = []
|
||||
let actions = []
|
||||
let worklog = []
|
||||
let worklogMinutes = ''
|
||||
let worklogSummary = ''
|
||||
let showCreateNode = false
|
||||
let newNodeTitle = ''
|
||||
let newNodeSection = 'clients'
|
||||
let showCreateNote = false
|
||||
let newNoteTitle = ''
|
||||
let loading = true
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Обзор' },
|
||||
{ id: 'notes', label: 'Заметки' },
|
||||
{ id: 'files', label: 'Файлы' },
|
||||
{ id: 'actions', label: 'Действия' },
|
||||
{ id: 'worklog', label: 'Журнал' },
|
||||
{ id: 'activity', label: 'Активность' },
|
||||
]
|
||||
|
||||
// ===== Lifecycle =====
|
||||
onMount(async () => {
|
||||
try {
|
||||
version = 'verstak-gui'
|
||||
if (window.go && window.go.main && window.go.main.App) {
|
||||
version = await window.go.main.App.VerstakVersion()
|
||||
sections = await window.go.main.App.ListSections()
|
||||
nodes = await window.go.main.App.ListRootNodes()
|
||||
}
|
||||
version = await wailsCall('VerstakVersion') || 'verstak-gui/v2'
|
||||
sections = await wailsCall('ListSections') || []
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
// Fallback: show sections from known list
|
||||
sections = [
|
||||
{ id: 'today', label: 'Сегодня' },
|
||||
{ id: 'inbox', label: 'Неразобранное' },
|
||||
{ id: 'clients', label: 'Клиенты' },
|
||||
{ id: 'projects', label: 'Проекты' },
|
||||
{ id: 'recipes', label: 'Рецепты' },
|
||||
{ id: 'documents', label: 'Документы' },
|
||||
{ id: 'archive', label: 'Архив' },
|
||||
]
|
||||
}
|
||||
loading = false
|
||||
})
|
||||
|
||||
function selectSection(id) {
|
||||
// ===== Section / Node selection =====
|
||||
async function selectSection(id) {
|
||||
selectedSection = id
|
||||
selectedNode = null
|
||||
activeTab = 'overview'
|
||||
notes = []
|
||||
files = []
|
||||
actions = []
|
||||
worklog = []
|
||||
showCreateNode = false
|
||||
error = ''
|
||||
try {
|
||||
nodes = await wailsCall('ListNodesBySection', id) || []
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
nodes = []
|
||||
}
|
||||
}
|
||||
|
||||
function selectNode(node) {
|
||||
async function selectNode(node) {
|
||||
selectedNode = node
|
||||
activeTab = 'overview'
|
||||
notes = []
|
||||
files = []
|
||||
actions = []
|
||||
worklog = []
|
||||
noteEditor = null
|
||||
showCreateNode = false
|
||||
showCreateNote = false
|
||||
error = ''
|
||||
await loadTabData(node.id)
|
||||
}
|
||||
|
||||
async function loadTabData(nodeID) {
|
||||
try { notes = await wailsCall('ListNotes', nodeID) || [] } catch(e) {}
|
||||
try { files = await wailsCall('ListFiles', nodeID) || [] } catch(e) {}
|
||||
try { actions = await wailsCall('ListActions', nodeID) || [] } catch(e) {}
|
||||
try { worklog = await wailsCall('ListWorklog', nodeID) || [] } catch(e) {}
|
||||
}
|
||||
|
||||
// ===== Node creation =====
|
||||
function openCreateNode() {
|
||||
showCreateNode = true
|
||||
newNodeTitle = ''
|
||||
newNodeSection = selectedSection || 'clients'
|
||||
}
|
||||
function cancelCreateNode() { showCreateNode = false; newNodeTitle = '' }
|
||||
async function submitCreateNode() {
|
||||
if (!newNodeTitle.trim()) return
|
||||
try {
|
||||
const node = await wailsCall('CreateNode', '', 'case', newNodeTitle.trim(), newNodeSection)
|
||||
showCreateNode = false
|
||||
newNodeTitle = ''
|
||||
await selectSection(newNodeSection)
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
|
||||
// ===== Notes =====
|
||||
function openCreateNote() { showCreateNote = true; newNoteTitle = '' }
|
||||
function cancelCreateNote() { showCreateNote = false; newNoteTitle = '' }
|
||||
async function submitCreateNote() {
|
||||
if (!newNoteTitle.trim() || !selectedNode) return
|
||||
try {
|
||||
const note = await wailsCall('CreateNote', selectedNode.id, newNoteTitle.trim())
|
||||
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }]
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
} catch (e) {
|
||||
// Fallback: create note locally
|
||||
const newNote = { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }
|
||||
notes = [...notes, newNote]
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function openNote(note) {
|
||||
if (noteEditor && noteEditor.dirty) {
|
||||
if (!confirm('Несохранённые изменения. Закрыть?')) return
|
||||
}
|
||||
try {
|
||||
const content = await wailsCall('ReadNote', note.id)
|
||||
noteEditor = { id: note.id, title: note.title, content: content || '', dirty: false }
|
||||
} catch (e) {
|
||||
noteEditor = { id: note.id, title: note.title, content: '# ' + note.title + '\n\n', dirty: false }
|
||||
}
|
||||
}
|
||||
|
||||
function closeNoteEditor() {
|
||||
if (noteEditor && noteEditor.dirty) {
|
||||
if (!confirm('Несохранённые изменения. Закрыть?')) return
|
||||
}
|
||||
noteEditor = null
|
||||
}
|
||||
|
||||
function updateNoteContent(e) {
|
||||
if (noteEditor) { noteEditor.content = e.target.value; noteEditor.dirty = true }
|
||||
}
|
||||
|
||||
async function saveCurrentNote() {
|
||||
if (!noteEditor) return
|
||||
try {
|
||||
await wailsCall('SaveNote', noteEditor.id, noteEditor.content)
|
||||
noteEditor.dirty = false
|
||||
} catch (e) {
|
||||
// Saved locally only
|
||||
noteEditor.dirty = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Worklog =====
|
||||
async function submitWorklog() {
|
||||
const mins = parseInt(worklogMinutes, 10)
|
||||
if (!worklogSummary.trim() || isNaN(mins) || mins <= 0 || !selectedNode) return
|
||||
try {
|
||||
const entry = await wailsCall('CreateWorklog', selectedNode.id, worklogSummary.trim(), mins)
|
||||
worklog = [...worklog, (entry && entry.id) ? entry : { id: Date.now().toString(), nodeId: selectedNode.id, summary: worklogSummary.trim(), minutes: mins, createdAt: new Date().toISOString() }]
|
||||
} catch (e) {
|
||||
worklog = [...worklog, { id: Date.now().toString(), nodeId: selectedNode.id, summary: worklogSummary.trim(), minutes: mins, createdAt: new Date().toISOString() }]
|
||||
}
|
||||
worklogSummary = ''
|
||||
worklogMinutes = ''
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
function tabClass(id) { return activeTab === id ? 'tab active' : 'tab' }
|
||||
function formatDate(str) {
|
||||
if (!str) return ''
|
||||
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-brand">
|
||||
<span class="logo">⚒</span>
|
||||
<span class="app-name">Верстак</span>
|
||||
<span class="brand-name">Верстак</span>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">Разделы</div>
|
||||
|
|
@ -48,278 +219,341 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">Корневые дела</div>
|
||||
{#each nodes as node}
|
||||
<button class="nav-item {selectedNode && selectedNode.id === node.id ? 'selected' : ''}"
|
||||
on:click={() => selectNode(node)}>
|
||||
{node.title}
|
||||
</button>
|
||||
{/each}
|
||||
{#if nodes.length === 0 && sections.length > 0}
|
||||
<div class="nav-empty">Нет дел</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedSection}
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">Дела {#if nodes.length > 0}({nodes.length}){/if}</div>
|
||||
{#each nodes as node}
|
||||
<button class="nav-item {selectedNode && selectedNode.id === node.id ? 'selected' : ''}"
|
||||
on:click={() => selectNode(node)}>
|
||||
{node.title}
|
||||
</button>
|
||||
{/each}
|
||||
{#if nodes.length === 0}<div class="nav-empty">Нет дел</div>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-bottom">
|
||||
<span class="version">{version}</span>
|
||||
</div>
|
||||
<div class="sidebar-footer"><span class="version">{version}</span></div>
|
||||
</aside>
|
||||
|
||||
<!-- Main area -->
|
||||
<!-- Main -->
|
||||
<main class="main">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
{#if selectedNode}
|
||||
<span class="crumb">{selectedNode.title}</span>
|
||||
<span class="crumb-type">{selectedNode.type}</span>
|
||||
{:else if selectedSection}
|
||||
<span class="crumb">{#each sections as section}{section.id === selectedSection ? section.label : ''}{/each}</span>
|
||||
<span class="crumb">{#each sections as s}{s.id === selectedSection ? s.label : ''}{/each}</span>
|
||||
{:else}
|
||||
<span class="crumb placeholder">Выберите раздел или дело</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- Search placeholder -->
|
||||
<div class="search-hint">Поиск...</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Error banner -->
|
||||
{#if error}
|
||||
<div class="error-banner">
|
||||
Wails bindings: {error}
|
||||
<div class="error-banner" on:click={() => error = ''}>
|
||||
{error} <span class="dismiss">✕</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
{#if selectedNode}
|
||||
<div class="node-view">
|
||||
<h2>{selectedNode.title}</h2>
|
||||
<div class="node-meta">
|
||||
<span>ID: {selectedNode.id}</span>
|
||||
<span>Type: {selectedNode.type}</span>
|
||||
{#if noteEditor}
|
||||
<!-- Note editor -->
|
||||
<div class="note-editor">
|
||||
<div class="note-editor-header">
|
||||
<span class="note-title">{noteEditor.title}</span>
|
||||
{#if noteEditor.dirty}<span class="dirty-mark">●</span>{/if}
|
||||
<div class="note-editor-actions">
|
||||
<button class="btn btn-primary" on:click={saveCurrentNote}>Сохранить</button>
|
||||
<button class="btn" on:click={closeNoteEditor}>Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if sections.length > 0}
|
||||
<div class="welcome">
|
||||
<h2>Верстак</h2>
|
||||
<p>Разделы: {sections.length} · Дел: {nodes.length}</p>
|
||||
{#if error}
|
||||
<p class="error-text">Go bindings не подключены: {error}</p>
|
||||
<textarea class="note-textarea" bind:value={noteEditor.content}
|
||||
on:input={updateNoteContent} placeholder="Начните писать..."></textarea>
|
||||
</div>
|
||||
|
||||
{:else if selectedNode}
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
{#each tabs as tab}
|
||||
<button class={tabClass(tab.id)} on:click={() => activeTab = tab.id}>{tab.label}</button>
|
||||
{/each}
|
||||
</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">Тип</span><span>{selectedNode.type}</span></div>
|
||||
<div class="meta-item"><span class="meta-label">Раздел</span><span>{selectedNode.section || '—'}</span></div>
|
||||
<div class="meta-item"><span class="meta-label">Создано</span><span>{formatDate(selectedNode.createdAt)}</span></div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<button class="qa-btn" on:click={() => { activeTab = 'notes'; openCreateNote() }}>✏️ Новая заметка</button>
|
||||
<button class="qa-btn" disabled title="Следующий этап">📎 Добавить файл</button>
|
||||
<button class="qa-btn" disabled title="Следующий этап">⚡ Добавить действие</button>
|
||||
<button class="qa-btn" on:click={() => activeTab = 'worklog'}>🕐 Записать время</button>
|
||||
</div>
|
||||
{#if notes.length > 0}
|
||||
<div class="recent-section">
|
||||
<h3>Последние заметки</h3>
|
||||
{#each notes.slice(0, 5) as note}
|
||||
<div class="recent-note" on:click={() => 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>Последние записи</h3>
|
||||
{#each worklog.slice(0, 3) as e}
|
||||
<div class="recent-entry">{e.summary} ({e.minutes} мин)</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'notes'}
|
||||
<div class="notes-tab">
|
||||
<div class="tab-toolbar">
|
||||
<button class="btn btn-primary" on:click={openCreateNote}>+ Добавить заметку</button>
|
||||
</div>
|
||||
{#if showCreateNote}
|
||||
<div class="create-form">
|
||||
<input type="text" placeholder="Название заметки" bind:value={newNoteTitle}
|
||||
on:keydown={(e) => e.key === 'Enter' && submitCreateNote()} />
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" on:click={submitCreateNote}>Создать</button>
|
||||
<button class="btn" on:click={cancelCreateNote}>Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if notes.length === 0 && !showCreateNote}
|
||||
<div class="empty-state"><p>Нет заметок</p><p class="hint">Создайте первую заметку для этого дела.</p></div>
|
||||
{:else}
|
||||
<div class="notes-list">
|
||||
{#each notes as note}
|
||||
<div class="note-card" on:click={() => openNote(note)}>
|
||||
<div class="note-card-title">{note.title}</div>
|
||||
<div class="note-card-date">{formatDate(note.createdAt)}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'files'}
|
||||
<div class="empty-state">
|
||||
<p>Нет файлов</p>
|
||||
<p class="hint">Добавьте документы, скриншоты или папку с материалами.</p>
|
||||
<div class="empty-actions">
|
||||
<button class="btn" disabled>+ Добавить файл</button>
|
||||
<button class="btn" disabled>+ Добавить папку</button>
|
||||
</div>
|
||||
<p class="empty-note">Полноценная работа с файлами — следующий этап.</p>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'actions'}
|
||||
{#if actions.length === 0}
|
||||
<div class="empty-state"><p>Действий пока нет</p></div>
|
||||
{:else}
|
||||
{#each actions as action}
|
||||
<div class="action-card">
|
||||
<span>{action.title}</span><span class="action-type">{action.type}</span>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('RunAction', action.id)}>Запустить</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{:else if activeTab === 'worklog'}
|
||||
<div class="worklog-tab">
|
||||
<div class="worklog-form">
|
||||
<input type="text" placeholder="Что сделано" bind:value={worklogSummary} />
|
||||
<input type="number" placeholder="Мин" bind:value={worklogMinutes} min="1" />
|
||||
<button class="btn btn-primary" on:click={submitWorklog}
|
||||
disabled={!worklogSummary.trim() || !worklogMinutes}>Записать</button>
|
||||
</div>
|
||||
{#if worklog.length === 0}
|
||||
<div class="empty-state"><p>Записей работы пока нет</p></div>
|
||||
{:else}
|
||||
{#each worklog as e}
|
||||
<div class="worklog-entry">
|
||||
<div>{e.summary}</div>
|
||||
<div class="wl-meta">{e.minutes} мин · {formatDate(e.createdAt)}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'activity'}
|
||||
<div class="empty-state"><p>Активность появится позже</p></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="welcome">
|
||||
<h2>Верстак</h2>
|
||||
{#if loading}<p>Загрузка...</p>
|
||||
{:else if sections.length > 0}
|
||||
<p>Выберите раздел в боковой панели.</p>
|
||||
<p class="hint">Или создайте новое дело кнопкой «+».</p>
|
||||
{:else if error}<p class="error-text">Ошибка: {error}</p>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !noteEditor && !selectedNode}
|
||||
<div class="fab" on:click={openCreateNode} title="Добавить дело">+</div>
|
||||
{/if}
|
||||
|
||||
{#if showCreateNode}
|
||||
<div class="modal-overlay" on:click|self={cancelCreateNode}>
|
||||
<div class="modal">
|
||||
<h3>Новое дело</h3>
|
||||
<div class="form-group">
|
||||
<label>Название</label>
|
||||
<input type="text" placeholder="Название дела" bind:value={newNodeTitle}
|
||||
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Раздел</label>
|
||||
<select bind:value={newNodeSection}>
|
||||
{#each sections.filter(s => s.id !== 'today' && s.id !== 'inbox') as s}
|
||||
<option value={s.id}>{s.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" on:click={submitCreateNode}>Создать</button>
|
||||
<button class="btn" on:click={cancelCreateNode}>Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="loading">Загрузка...</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
.app { display: flex; width: 100vw; height: 100vh; overflow: hidden; background: #13131f; color: #e4e4ef; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; }
|
||||
|
||||
/* App shell — full viewport */
|
||||
.app {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #13131f;
|
||||
color: #e4e4ef;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
/* Sidebar */
|
||||
.sidebar { width: 260px; min-width: 200px; height: 100vh; display: flex; flex-direction: column; background: #1a1a28; border-right: 1px solid #2a2a3c; flex-shrink: 0; overflow: hidden; }
|
||||
.sidebar-brand { padding: 16px 20px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; }
|
||||
.logo { font-size: 20px; line-height: 1; }
|
||||
.brand-name { font-size: 16px; font-weight: 600; }
|
||||
.sidebar-nav { flex: 1; overflow-y: auto; padding: 12px 0; }
|
||||
.nav-group { margin-bottom: 16px; }
|
||||
.nav-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; padding: 4px 20px; margin-bottom: 4px; }
|
||||
.nav-item { display: block; width: 100%; padding: 8px 20px; border: none; background: none; color: #ccc; font-size: 13px; text-align: left; cursor: pointer; border-radius: 0; font-family: inherit; }
|
||||
.nav-item:hover { background: #222233; }
|
||||
.nav-item.selected { background: #2a2a4a; color: #fff; font-weight: 500; }
|
||||
.nav-empty { padding: 8px 20px; color: #555; font-size: 12px; }
|
||||
.sidebar-footer { padding: 12px 20px; border-top: 1px solid #2a2a3c; flex-shrink: 0; }
|
||||
.version { font-size: 11px; color: #555; }
|
||||
|
||||
/* ===== SIDEBAR ===== */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
min-width: 200px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a1a28;
|
||||
border-right: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Main */
|
||||
.main { flex: 1; display: flex; flex-direction: column; height: 100vh; min-width: 0; overflow: hidden; background: #13131f; }
|
||||
.header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; flex-shrink: 0; min-height: 48px; }
|
||||
.crumb { font-size: 14px; font-weight: 500; }
|
||||
.crumb.placeholder { color: #666; }
|
||||
.crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
||||
.error-banner { background: #3a2222; color: #ff8888; padding: 8px 24px; font-size: 12px; border-bottom: 1px solid #4a2222; flex-shrink: 0; cursor: pointer; display: flex; justify-content: space-between; }
|
||||
.dismiss { opacity: 0.6; }
|
||||
|
||||
.sidebar-top {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Tabs */
|
||||
.tabs { display: flex; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; padding: 0 24px; }
|
||||
.tab { padding: 10px 16px; border: none; background: none; color: #888; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; }
|
||||
.tab:hover { color: #ccc; }
|
||||
.tab.active { color: #e4e4ef; border-bottom-color: #6366f1; }
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Tab content */
|
||||
.tab-content { flex: 1; overflow-y: auto; }
|
||||
|
||||
.app-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e4e4ef;
|
||||
}
|
||||
/* Note editor */
|
||||
.note-editor { flex: 1; display: flex; flex-direction: column; height: 100%; }
|
||||
.note-editor-header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||
.note-title { font-size: 16px; font-weight: 500; }
|
||||
.dirty-mark { color: #f59e0b; font-size: 10px; }
|
||||
.note-editor-actions { margin-left: auto; display: flex; gap: 8px; }
|
||||
.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; }
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
}
|
||||
/* 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; }
|
||||
.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; }
|
||||
|
||||
.nav-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* Notes tab */
|
||||
.notes-tab { padding: 24px; }
|
||||
.tab-toolbar { margin-bottom: 16px; }
|
||||
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
|
||||
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; }
|
||||
.create-form input:focus { outline: none; border-color: #6366f1; }
|
||||
.form-actions { display: flex; gap: 8px; }
|
||||
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
||||
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; }
|
||||
.note-card:hover { border-color: #3a3a5c; }
|
||||
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||
.note-card-date { font-size: 11px; color: #555; }
|
||||
|
||||
.nav-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #666;
|
||||
padding: 4px 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
/* Worklog tab */
|
||||
.worklog-tab { padding: 24px; }
|
||||
.worklog-form { display: flex; gap: 8px; margin-bottom: 24px; align-items: center; }
|
||||
.worklog-form input { padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; }
|
||||
.worklog-form input:focus { outline: none; border-color: #6366f1; }
|
||||
.worklog-form input[type="text"] { flex: 1; }
|
||||
.worklog-form input[type="number"] { width: 70px; }
|
||||
.worklog-entry { padding: 12px 0; border-bottom: 1px solid #1a1a28; }
|
||||
.wl-meta { font-size: 11px; color: #555; margin-top: 2px; }
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #ccc;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
/* Actions */
|
||||
.action-card { background: #1a1a28; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
.action-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; }
|
||||
|
||||
.nav-item:hover {
|
||||
background: #222233;
|
||||
}
|
||||
/* Empty states */
|
||||
.empty-state { padding: 48px 24px; text-align: center; }
|
||||
.empty-state p { color: #666; margin-bottom: 8px; }
|
||||
.hint { font-size: 13px; color: #555; }
|
||||
.empty-actions { display: flex; gap: 8px; justify-content: center; margin: 16px 0; }
|
||||
.empty-note { font-size: 12px; color: #444; margin-top: 16px; }
|
||||
|
||||
.nav-item.selected {
|
||||
background: #2a2a4a;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
/* Welcome */
|
||||
.welcome { padding: 48px 24px; text-align: center; }
|
||||
.welcome h2 { font-size: 32px; font-weight: 300; color: #8888a4; margin-bottom: 16px; }
|
||||
.welcome p { color: #666; font-size: 14px; }
|
||||
.error-text { color: #ff8888; }
|
||||
|
||||
.nav-empty {
|
||||
padding: 8px 20px;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* FAB */
|
||||
.fab { position: fixed; bottom: 24px; right: 24px; width: 56px; height: 56px; border-radius: 50%; background: #6366f1; color: #fff; font-size: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); }
|
||||
.fab:hover { background: #4f46e5; }
|
||||
|
||||
.sidebar-bottom {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.modal { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 12px; padding: 24px; width: 400px; max-width: 90vw; }
|
||||
.modal h3 { font-size: 18px; margin-bottom: 16px; }
|
||||
.form-group { margin-bottom: 12px; }
|
||||
.form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
|
||||
.form-group input, .form-group select { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; }
|
||||
.form-group select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; }
|
||||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; }
|
||||
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
|
||||
.version {
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* ===== MAIN AREA ===== */
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #13131f;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid #2a2a3c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.crumb {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #e4e4ef;
|
||||
}
|
||||
|
||||
.crumb.placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
padding: 6px 12px;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #2a2a3c;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background: #3a2222;
|
||||
color: #ff8888;
|
||||
padding: 8px 24px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #4a2222;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== CONTENT ===== */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.welcome h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
margin-bottom: 12px;
|
||||
color: #8888a4;
|
||||
}
|
||||
|
||||
.welcome p {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ff8888;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.node-view h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.node-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* Buttons */
|
||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; }
|
||||
.btn:hover { background: #222233; }
|
||||
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||
.btn-primary:hover { background: #4f46e5; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,27 @@
|
|||
// Wails v2 API wrapper — single frontend access point to Go backend
|
||||
// Wails v2 API wrapper — uses generated bindings from wailsjs/go/main/App.js
|
||||
import * as App from '../wailsjs/go/main/App.js'
|
||||
|
||||
function wailsCall(method, ...args) {
|
||||
if (window.go && window.go.main && window.go.main.App) {
|
||||
return window.go.main.App[method](...args)
|
||||
}
|
||||
return Promise.reject(new Error('Wails bindings not loaded'))
|
||||
}
|
||||
// Re-export all methods
|
||||
export const listSections = () => App.ListSections()
|
||||
export const listNodesBySection = (section) => App.ListNodesBySection(section)
|
||||
export const listChildren = (parentID) => App.ListChildren(parentID)
|
||||
export const getNodeDetail = (id) => App.GetNodeDetail(id)
|
||||
export const createNode = (parentID, type, title, section) => App.CreateNode(parentID, type, title, section)
|
||||
export const deleteNode = (id) => App.DeleteNode(id)
|
||||
|
||||
// Sections
|
||||
export const listSections = () => wailsCall('ListSections')
|
||||
export const listNotes = (nodeID) => App.ListNotes(nodeID)
|
||||
export const createNote = (parentID, title) => App.CreateNote(parentID, title)
|
||||
export const readNote = (noteID) => App.ReadNote(noteID)
|
||||
export const saveNote = (noteID, content) => App.SaveNote(noteID, content)
|
||||
|
||||
// Nodes
|
||||
export const listNodesBySection = (section) => wailsCall('ListNodesBySection', section)
|
||||
export const listChildren = (parentID) => wailsCall('ListChildren', parentID)
|
||||
export const getNodeDetail = (id) => wailsCall('GetNodeDetail', id)
|
||||
export const createNode = (parentID, type, title, section) =>
|
||||
wailsCall('CreateNode', parentID, type, title, section)
|
||||
export const deleteNode = (id) => wailsCall('DeleteNode', id)
|
||||
export const listFiles = (nodeID) => App.ListFiles(nodeID)
|
||||
|
||||
// Notes
|
||||
export const listNotes = (nodeID) => wailsCall('ListNotes', nodeID)
|
||||
export const createNote = (parentID, title) => wailsCall('CreateNote', parentID, title)
|
||||
export const readNote = (noteID) => wailsCall('ReadNote', noteID)
|
||||
export const saveNote = (noteID, content) => wailsCall('SaveNote', noteID, content)
|
||||
export const listActions = (nodeID) => App.ListActions(nodeID)
|
||||
export const runAction = (id) => App.RunAction(id)
|
||||
|
||||
// Files
|
||||
export const listFiles = (nodeID) => wailsCall('ListFiles', nodeID)
|
||||
export const listWorklog = (nodeID) => App.ListWorklog(nodeID)
|
||||
export const createWorklog = (nodeID, summary, minutes) => App.CreateWorklog(nodeID, summary, minutes)
|
||||
|
||||
// Actions
|
||||
export const listActions = (nodeID) => wailsCall('ListActions', nodeID)
|
||||
export const runAction = (id) => wailsCall('RunAction', id)
|
||||
export const search = (query) => App.Search(query)
|
||||
|
||||
// Worklog
|
||||
export const listWorklog = (nodeID) => wailsCall('ListWorklog', nodeID)
|
||||
export const createWorklog = (nodeID, summary, minutes) =>
|
||||
wailsCall('CreateWorklog', nodeID, summary, minutes)
|
||||
|
||||
// Search
|
||||
export const search = (query) => wailsCall('Search', query)
|
||||
|
||||
// System
|
||||
export const verstakVersion = () => wailsCall('VerstakVersion')
|
||||
export const verstakVersion = () => App.VerstakVersion()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
// @ts-check
|
||||
// Wails v2 generated bindings — auto-generated by wails build
|
||||
// Manual version for go build -tags gui
|
||||
|
||||
export function ListSections() {
|
||||
return window['go']['main']['App']['ListSections']();
|
||||
}
|
||||
|
||||
export function ListNodesBySection(arg1) {
|
||||
return window['go']['main']['App']['ListNodesBySection'](arg1);
|
||||
}
|
||||
|
||||
export function ListChildren(arg1) {
|
||||
return window['go']['main']['App']['ListChildren'](arg1);
|
||||
}
|
||||
|
||||
export function GetNodeDetail(arg1) {
|
||||
return window['go']['main']['App']['GetNodeDetail'](arg1);
|
||||
}
|
||||
|
||||
export function CreateNode(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['CreateNode'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function DeleteNode(arg1) {
|
||||
return window['go']['main']['App']['DeleteNode'](arg1);
|
||||
}
|
||||
|
||||
export function ListNotes(arg1) {
|
||||
return window['go']['main']['App']['ListNotes'](arg1);
|
||||
}
|
||||
|
||||
export function CreateNote(arg1, arg2) {
|
||||
return window['go']['main']['App']['CreateNote'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ReadNote(arg1) {
|
||||
return window['go']['main']['App']['ReadNote'](arg1);
|
||||
}
|
||||
|
||||
export function SaveNote(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveNote'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ListFiles(arg1) {
|
||||
return window['go']['main']['App']['ListFiles'](arg1);
|
||||
}
|
||||
|
||||
export function ListActions(arg1) {
|
||||
return window['go']['main']['App']['ListActions'](arg1);
|
||||
}
|
||||
|
||||
export function RunAction(arg1) {
|
||||
return window['go']['main']['App']['RunAction'](arg1);
|
||||
}
|
||||
|
||||
export function ListWorklog(arg1) {
|
||||
return window['go']['main']['App']['ListWorklog'](arg1);
|
||||
}
|
||||
|
||||
export function CreateWorklog(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['CreateWorklog'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function Search(arg1) {
|
||||
return window['go']['main']['App']['Search'](arg1);
|
||||
}
|
||||
|
||||
export function PickFile() {
|
||||
return window['go']['main']['App']['PickFile']();
|
||||
}
|
||||
|
||||
export function PickFiles() {
|
||||
return window['go']['main']['App']['PickFiles']();
|
||||
}
|
||||
|
||||
export function PickDirectory() {
|
||||
return window['go']['main']['App']['PickDirectory']();
|
||||
}
|
||||
|
||||
export function OpenFile(arg1) {
|
||||
return window['go']['main']['App']['OpenFile'](arg1);
|
||||
}
|
||||
|
||||
export function OpenFolder(arg1) {
|
||||
return window['go']['main']['App']['OpenFolder'](arg1);
|
||||
}
|
||||
|
||||
export function VerstakVersion() {
|
||||
return window['go']['main']['App']['VerstakVersion']();
|
||||
}
|
||||
|
|
@ -1,15 +1,22 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 3001,
|
||||
strictPort: true,
|
||||
},
|
||||
plugins: [svelte()],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
publicDir: "public",
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue