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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
@ -13,6 +14,7 @@ import (
|
||||||
"verstak/internal/core/files"
|
"verstak/internal/core/files"
|
||||||
"verstak/internal/core/notes"
|
"verstak/internal/core/notes"
|
||||||
"verstak/internal/core/nodes"
|
"verstak/internal/core/nodes"
|
||||||
|
"verstak/internal/core/search"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
"verstak/internal/core/worklog"
|
"verstak/internal/core/worklog"
|
||||||
)
|
)
|
||||||
|
|
@ -26,6 +28,7 @@ type App struct {
|
||||||
notes *notes.Service
|
notes *notes.Service
|
||||||
actions *actions.Service
|
actions *actions.Service
|
||||||
worklog *worklog.Service
|
worklog *worklog.Service
|
||||||
|
search *search.Service
|
||||||
vault string
|
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) {
|
func (a *App) Search(query string) ([]SearchResultDTO, error) {
|
||||||
if strings.TrimSpace(query) == "" {
|
if strings.TrimSpace(query) == "" {
|
||||||
return []SearchResultDTO{}, nil
|
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 {
|
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 {
|
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()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,7 +401,4 @@ func toNodeDTOs(list []nodes.Node) []NodeDTO {
|
||||||
return result
|
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;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-COs6tJEl.js"></script>
|
<script type="module" crossorigin src="/assets/main-BqdVWy5o.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-ClxkTvdE.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-D8LYjC_e.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ func main() {
|
||||||
worklogSvc := worklog.NewService(db)
|
worklogSvc := worklog.NewService(db)
|
||||||
searchSvc := search.NewService(db)
|
searchSvc := search.NewService(db)
|
||||||
plugins.NewManager(abs).Discover()
|
plugins.NewManager(abs).Discover()
|
||||||
_ = searchSvc
|
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
db: db,
|
db: db,
|
||||||
|
|
@ -58,6 +57,7 @@ func main() {
|
||||||
notes: noteSvc,
|
notes: noteSvc,
|
||||||
actions: actionSvc,
|
actions: actionSvc,
|
||||||
worklog: worklogSvc,
|
worklog: worklogSvc,
|
||||||
|
search: searchSvc,
|
||||||
vault: abs,
|
vault: abs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,214 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
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 sections = []
|
||||||
let nodes = []
|
let nodes = []
|
||||||
let version = ''
|
let version = ''
|
||||||
let error = ''
|
let error = ''
|
||||||
let selectedSection = ''
|
let selectedSection = ''
|
||||||
let selectedNode = null
|
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 () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
version = 'verstak-gui'
|
version = await wailsCall('VerstakVersion') || 'verstak-gui/v2'
|
||||||
if (window.go && window.go.main && window.go.main.App) {
|
sections = await wailsCall('ListSections') || []
|
||||||
version = await window.go.main.App.VerstakVersion()
|
|
||||||
sections = await window.go.main.App.ListSections()
|
|
||||||
nodes = await window.go.main.App.ListRootNodes()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(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
|
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
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-top">
|
<div class="sidebar-brand">
|
||||||
<span class="logo">⚒</span>
|
<span class="logo">⚒</span>
|
||||||
<span class="app-name">Верстак</span>
|
<span class="brand-name">Верстак</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<div class="nav-group">
|
<div class="nav-group">
|
||||||
<div class="nav-label">Разделы</div>
|
<div class="nav-label">Разделы</div>
|
||||||
|
|
@ -48,278 +219,341 @@
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if selectedSection}
|
||||||
<div class="nav-group">
|
<div class="nav-group">
|
||||||
<div class="nav-label">Корневые дела</div>
|
<div class="nav-label">Дела {#if nodes.length > 0}({nodes.length}){/if}</div>
|
||||||
{#each nodes as node}
|
{#each nodes as node}
|
||||||
<button class="nav-item {selectedNode && selectedNode.id === node.id ? 'selected' : ''}"
|
<button class="nav-item {selectedNode && selectedNode.id === node.id ? 'selected' : ''}"
|
||||||
on:click={() => selectNode(node)}>
|
on:click={() => selectNode(node)}>
|
||||||
{node.title}
|
{node.title}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if nodes.length === 0 && sections.length > 0}
|
{#if nodes.length === 0}<div class="nav-empty">Нет дел</div>{/if}
|
||||||
<div class="nav-empty">Нет дел</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="sidebar-footer"><span class="version">{version}</span></div>
|
||||||
<div class="sidebar-bottom">
|
|
||||||
<span class="version">{version}</span>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main area -->
|
<!-- Main -->
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<!-- Header -->
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
{#if selectedNode}
|
{#if selectedNode}
|
||||||
<span class="crumb">{selectedNode.title}</span>
|
<span class="crumb">{selectedNode.title}</span>
|
||||||
|
<span class="crumb-type">{selectedNode.type}</span>
|
||||||
{:else if selectedSection}
|
{: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}
|
{:else}
|
||||||
<span class="crumb placeholder">Выберите раздел или дело</span>
|
<span class="crumb placeholder">Выберите раздел или дело</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
|
||||||
<!-- Search placeholder -->
|
|
||||||
<div class="search-hint">Поиск...</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Error banner -->
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error-banner">
|
<div class="error-banner" on:click={() => error = ''}>
|
||||||
Wails bindings: {error}
|
{error} <span class="dismiss">✕</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Content -->
|
{#if noteEditor}
|
||||||
<div class="content">
|
<!-- Note editor -->
|
||||||
{#if selectedNode}
|
<div class="note-editor">
|
||||||
<div class="node-view">
|
<div class="note-editor-header">
|
||||||
<h2>{selectedNode.title}</h2>
|
<span class="note-title">{noteEditor.title}</span>
|
||||||
<div class="node-meta">
|
{#if noteEditor.dirty}<span class="dirty-mark">●</span>{/if}
|
||||||
<span>ID: {selectedNode.id}</span>
|
<div class="note-editor-actions">
|
||||||
<span>Type: {selectedNode.type}</span>
|
<button class="btn btn-primary" on:click={saveCurrentNote}>Сохранить</button>
|
||||||
|
<button class="btn" on:click={closeNoteEditor}>Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if sections.length > 0}
|
<textarea class="note-textarea" bind:value={noteEditor.content}
|
||||||
<div class="welcome">
|
on:input={updateNoteContent} placeholder="Начните писать..."></textarea>
|
||||||
<h2>Верстак</h2>
|
</div>
|
||||||
<p>Разделы: {sections.length} · Дел: {nodes.length}</p>
|
|
||||||
{#if error}
|
{:else if selectedNode}
|
||||||
<p class="error-text">Go bindings не подключены: {error}</p>
|
<!-- 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}
|
{/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>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
<div class="loading">Загрузка...</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Reset */
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
*, *::before, *::after {
|
.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; }
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* App shell — full viewport */
|
/* Sidebar */
|
||||||
.app {
|
.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; }
|
||||||
display: flex;
|
.sidebar-brand { padding: 16px 20px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; }
|
||||||
width: 100vw;
|
.logo { font-size: 20px; line-height: 1; }
|
||||||
height: 100vh;
|
.brand-name { font-size: 16px; font-weight: 600; }
|
||||||
overflow: hidden;
|
.sidebar-nav { flex: 1; overflow-y: auto; padding: 12px 0; }
|
||||||
background: #13131f;
|
.nav-group { margin-bottom: 16px; }
|
||||||
color: #e4e4ef;
|
.nav-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; padding: 4px 20px; margin-bottom: 4px; }
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
.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; }
|
||||||
font-size: 14px;
|
.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 ===== */
|
/* Main */
|
||||||
.sidebar {
|
.main { flex: 1; display: flex; flex-direction: column; height: 100vh; min-width: 0; overflow: hidden; background: #13131f; }
|
||||||
width: 260px;
|
.header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; flex-shrink: 0; min-height: 48px; }
|
||||||
min-width: 200px;
|
.crumb { font-size: 14px; font-weight: 500; }
|
||||||
height: 100vh;
|
.crumb.placeholder { color: #666; }
|
||||||
display: flex;
|
.crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
||||||
flex-direction: column;
|
.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; }
|
||||||
background: #1a1a28;
|
.dismiss { opacity: 0.6; }
|
||||||
border-right: 1px solid #2a2a3c;
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-top {
|
/* Tabs */
|
||||||
padding: 16px 20px;
|
.tabs { display: flex; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; padding: 0 24px; }
|
||||||
display: flex;
|
.tab { padding: 10px 16px; border: none; background: none; color: #888; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; }
|
||||||
align-items: center;
|
.tab:hover { color: #ccc; }
|
||||||
gap: 10px;
|
.tab.active { color: #e4e4ef; border-bottom-color: #6366f1; }
|
||||||
border-bottom: 1px solid #2a2a3c;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
/* Tab content */
|
||||||
font-size: 20px;
|
.tab-content { flex: 1; overflow-y: auto; }
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-name {
|
/* Note editor */
|
||||||
font-size: 16px;
|
.note-editor { flex: 1; display: flex; flex-direction: column; height: 100%; }
|
||||||
font-weight: 600;
|
.note-editor-header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||||
color: #e4e4ef;
|
.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 {
|
/* Overview */
|
||||||
flex: 1;
|
.overview { padding: 24px; }
|
||||||
overflow-y: auto;
|
.overview h2 { font-size: 24px; margin-bottom: 16px; }
|
||||||
padding: 12px 0;
|
.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 {
|
/* Notes tab */
|
||||||
margin-bottom: 16px;
|
.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 {
|
/* Worklog tab */
|
||||||
font-size: 10px;
|
.worklog-tab { padding: 24px; }
|
||||||
text-transform: uppercase;
|
.worklog-form { display: flex; gap: 8px; margin-bottom: 24px; align-items: center; }
|
||||||
letter-spacing: 0.5px;
|
.worklog-form input { padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; }
|
||||||
color: #666;
|
.worklog-form input:focus { outline: none; border-color: #6366f1; }
|
||||||
padding: 4px 20px;
|
.worklog-form input[type="text"] { flex: 1; }
|
||||||
margin-bottom: 4px;
|
.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 {
|
/* Actions */
|
||||||
display: block;
|
.action-card { background: #1a1a28; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||||
width: 100%;
|
.action-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; }
|
||||||
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 {
|
/* Empty states */
|
||||||
background: #222233;
|
.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 {
|
/* Welcome */
|
||||||
background: #2a2a4a;
|
.welcome { padding: 48px 24px; text-align: center; }
|
||||||
color: #fff;
|
.welcome h2 { font-size: 32px; font-weight: 300; color: #8888a4; margin-bottom: 16px; }
|
||||||
font-weight: 500;
|
.welcome p { color: #666; font-size: 14px; }
|
||||||
}
|
.error-text { color: #ff8888; }
|
||||||
|
|
||||||
.nav-empty {
|
/* FAB */
|
||||||
padding: 8px 20px;
|
.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); }
|
||||||
color: #555;
|
.fab:hover { background: #4f46e5; }
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-bottom {
|
/* Modal */
|
||||||
padding: 12px 20px;
|
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||||
border-top: 1px solid #2a2a3c;
|
.modal { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 12px; padding: 24px; width: 400px; max-width: 90vw; }
|
||||||
flex-shrink: 0;
|
.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 {
|
/* Buttons */
|
||||||
font-size: 11px;
|
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; }
|
||||||
color: #555;
|
.btn:hover { background: #222233; }
|
||||||
}
|
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||||
|
.btn-primary:hover { background: #4f46e5; }
|
||||||
/* ===== MAIN AREA ===== */
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
.main {
|
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</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) {
|
// Re-export all methods
|
||||||
if (window.go && window.go.main && window.go.main.App) {
|
export const listSections = () => App.ListSections()
|
||||||
return window.go.main.App[method](...args)
|
export const listNodesBySection = (section) => App.ListNodesBySection(section)
|
||||||
}
|
export const listChildren = (parentID) => App.ListChildren(parentID)
|
||||||
return Promise.reject(new Error('Wails bindings not loaded'))
|
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 listNotes = (nodeID) => App.ListNotes(nodeID)
|
||||||
export const listSections = () => wailsCall('ListSections')
|
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 listFiles = (nodeID) => App.ListFiles(nodeID)
|
||||||
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)
|
|
||||||
|
|
||||||
// Notes
|
export const listActions = (nodeID) => App.ListActions(nodeID)
|
||||||
export const listNotes = (nodeID) => wailsCall('ListNotes', nodeID)
|
export const runAction = (id) => App.RunAction(id)
|
||||||
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)
|
|
||||||
|
|
||||||
// Files
|
export const listWorklog = (nodeID) => App.ListWorklog(nodeID)
|
||||||
export const listFiles = (nodeID) => wailsCall('ListFiles', nodeID)
|
export const createWorklog = (nodeID, summary, minutes) => App.CreateWorklog(nodeID, summary, minutes)
|
||||||
|
|
||||||
// Actions
|
export const search = (query) => App.Search(query)
|
||||||
export const listActions = (nodeID) => wailsCall('ListActions', nodeID)
|
|
||||||
export const runAction = (id) => wailsCall('RunAction', id)
|
|
||||||
|
|
||||||
// Worklog
|
export const verstakVersion = () => App.VerstakVersion()
|
||||||
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')
|
|
||||||
|
|
|
||||||
|
|
@ -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 { defineConfig } from "vite";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
server: {
|
server: {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 3001,
|
port: 3001,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
},
|
},
|
||||||
plugins: [svelte()],
|
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
publicDir: "public",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue