feat: ui completion — VaultSelection, Sidebar navigation, layout fixes

This commit is contained in:
mirivlad 2026-06-17 07:28:00 +08:00
parent 6202157cbf
commit 7530e21dfd
7 changed files with 301 additions and 53 deletions

Binary file not shown.

View File

@ -33,9 +33,14 @@
vaultStatus = { status: 'open', path: '', vaultId: '' }; vaultStatus = { status: 'open', path: '', vaultId: '' };
} }
function onNav(e) {
currentView = e.detail.viewId;
}
// Listen for vault-opened event from VaultSelection // Listen for vault-opened event from VaultSelection
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('verstak:vault-opened', onVaultOpened); window.addEventListener('verstak:vault-opened', onVaultOpened);
window.addEventListener('verstak:nav', onNav);
} }
checkVault(); checkVault();
@ -62,6 +67,19 @@
{/if} {/if}
<style> <style>
:global(*) {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0f0;
overflow: hidden;
}
.app-loading { .app-loading {
display: flex; display: flex;
align-items: center; align-items: center;
@ -71,11 +89,13 @@
color: #a0a0b8; color: #a0a0b8;
font-size: 1rem; font-size: 1rem;
} }
main { main {
display: flex; display: flex;
height: 100vh; height: 100vh;
background: #1a1a2e; background: #1a1a2e;
} }
.content { .content {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@ -254,12 +254,15 @@
<style> <style>
.plugin-manager { .plugin-manager {
max-width: 900px; max-width: 900px;
padding-top: 0.5rem;
} }
header { header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 1rem; margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #0f3460;
} }
.header-left { .header-left {
display: flex; display: flex;

View File

@ -2,74 +2,191 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import * as App from '../../../wailsjs/go/api/App'; import * as App from '../../../wailsjs/go/api/App';
let sidebarItems = [];
let activeView = '';
let plugins = []; let plugins = [];
let contributions = { sidebarItems: [], views: [], commands: [], settingsPanels: [] }; let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
let sidebarItems = [];
let navItems = [
{ id: 'plugin-manager', label: 'Plugin Manager', icon: '🧩' },
];
$: vaultOpen = vaultStatus.status === 'open';
onMount(async () => { onMount(async () => {
try { try {
const [contribs, pluginList] = await Promise.all([ const [p, v, contribs] = await Promise.all([
App.GetContributions(), App.GetPlugins().catch(() => []),
App.GetPlugins(), App.GetVaultStatus().catch(() => ({ status: 'unknown', path: '', vaultId: '' })),
App.GetContributions().catch(() => ({})),
]); ]);
contributions = contribs; plugins = p || [];
plugins = pluginList; vaultStatus = v;
const pluginMap = new Map(pluginList.map(p => [p.manifest.id, p]));
sidebarItems = (contribs.sidebarItems || []).filter(item => { sidebarItems = (contribs.sidebarItems || []).filter(item => {
const plugin = pluginMap.get(item.pluginId); const plugin = plugins.find(p => p.manifest?.id === item.pluginId);
return plugin && plugin.manifest.permissions.includes('ui.register'); if (!plugin) return false;
return plugin.status !== 'disabled' && plugin.status !== 'failed' && plugin.status !== 'incompatible' && plugin.status !== 'missing-required-capability';
}); });
} catch (e) { } catch (e) {
console.error('[Sidebar] load error:', e); console.error('[Sidebar] load error:', e);
} }
}); });
function openView(viewId) { function handleNav(id) {
activeView = viewId; window.dispatchEvent(new CustomEvent('verstak:nav', { detail: { viewId: id } }));
window.dispatchEvent(new CustomEvent('verstak:open-view', { detail: { viewId } })); }
function handleSidebarItem(item) {
window.dispatchEvent(new CustomEvent('verstak:open-view', { detail: { viewId: item.id } }));
} }
</script> </script>
<nav class="sidebar"> <aside class="sidebar">
{#each sidebarItems as item} <div class="sidebar-header">
<button <span class="sidebar-logo">📦</span>
class="sidebar-item" <span class="sidebar-title">Verstak</span>
class:active={activeView === item.item.view} </div>
on:click={() => openView(item.item.view)}
type="button" <nav class="sidebar-nav">
> {#each navItems as item}
{#if item.item.icon}<span class="icon">{item.item.icon}</span>{/if} <button
<span class="label">{item.item.title}</span> class="nav-item"
</button> on:click={() => handleNav(item.id)}
{/each} type="button"
</nav> >
<span class="nav-icon">{item.icon}</span>
<span class="nav-label">{item.label}</span>
</button>
{/each}
</nav>
{#if sidebarItems.length > 0}
<div class="sidebar-section">
<span class="section-label">Plugins</span>
{#each sidebarItems as item}
<button
class="nav-item plugin-item"
on:click={() => handleSidebarItem(item)}
type="button"
>
<span class="nav-icon">{item.icon || '📌'}</span>
<span class="nav-label">{item.label || item.id}</span>
</button>
{/each}
</div>
{/if}
<div class="sidebar-footer">
{#if vaultStatus.status !== 'unknown'}
<span class="vault-indicator" class:vault-open={vaultStatus.status === 'open'} class:vault-closed={vaultStatus.status !== 'open'}>
● Vault: {vaultStatus.status}
</span>
{/if}
</div>
</aside>
<style> <style>
.sidebar { .sidebar {
width: 200px; width: 220px;
min-width: 220px;
background: #16213e; background: #16213e;
border-right: 1px solid #0f3460;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5rem 0; border-right: 1px solid #0f3460;
overflow-y: auto; overflow: hidden;
} }
.sidebar-item {
.sidebar-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem 1rem; padding: 1rem 1.25rem;
border-bottom: 1px solid #0f3460;
}
.sidebar-logo {
font-size: 1.2rem;
}
.sidebar-title {
color: #e0e0f0;
font-size: 1rem;
font-weight: 600;
}
.sidebar-nav {
display: flex;
flex-direction: column;
padding: 0.5rem 0.75rem;
gap: 0.15rem;
}
.sidebar-section {
display: flex;
flex-direction: column;
padding: 0.5rem 0.75rem;
gap: 0.15rem;
border-top: 1px solid #0f3460;
margin-top: 0.25rem;
}
.section-label {
color: #666;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.25rem 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.45rem 0.75rem;
background: none; background: none;
border: none; border: none;
color: #a0a0b0; color: #a0a0b8;
font-size: 0.85rem;
cursor: pointer; cursor: pointer;
border-radius: 6px;
text-align: left; text-align: left;
font-size: 0.9rem;
width: 100%; width: 100%;
transition: background 0.15s, color 0.15s;
}
.nav-item:hover {
background: #0f3460;
color: #e0e0f0;
}
.nav-icon {
font-size: 1rem;
flex-shrink: 0;
width: 1.2rem;
text-align: center;
}
.nav-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-footer {
margin-top: auto;
padding: 0.75rem 1.25rem;
border-top: 1px solid #0f3460;
}
.vault-indicator {
font-size: 0.7rem;
color: #666;
}
.vault-indicator.vault-open {
color: #4ecca3;
}
.vault-indicator.vault-closed {
color: #a0a0b8;
} }
.sidebar-item:hover { background: #0f3460; color: #e0e0f0; }
.sidebar-item.active { background: #0f3460; color: #4ecca3; }
.icon { font-size: 1.1rem; }
.label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
</style> </style>

View File

@ -16,7 +16,8 @@
appSettings = await App.GetAppSettings() || {}; appSettings = await App.GetAppSettings() || {};
recentVaults = appSettings.recentVaults || []; recentVaults = appSettings.recentVaults || [];
} catch (e) { } catch (e) {
error = 'Failed to load app settings: ' + String(e); // App settings might fail if backend not ready — show selection anyway
console.error('[VaultSelection] load settings:', e);
} }
loading = false; loading = false;
}); });
@ -29,21 +30,27 @@
} }
creating = true; creating = true;
try { try {
// Create the vault // Step 1: Create the vault directory + metadata
const createErr = await App.CreateVault(newVaultPath.trim()); const createErr = await App.CreateVault(newVaultPath.trim());
if (createErr) { if (createErr) {
error = 'Create vault: ' + createErr; error = 'Create vault: ' + createErr;
creating = false; creating = false;
return; return;
} }
// Open it and save to app settings // Step 2: Open it (registers capabilities, loads plugin state)
const setErr = await App.SetCurrentVault(newVaultPath.trim()); const openErr = await App.OpenVault(newVaultPath.trim());
if (setErr) { if (openErr) {
error = 'Set current vault: ' + setErr; error = 'Open vault: ' + openErr;
creating = false; creating = false;
return; return;
} }
// Success — dispatch event for app to transition // Step 3: Save to app settings (set current + add to recent)
const setErr = await App.SetCurrentVault(newVaultPath.trim());
if (setErr) {
// Vault is open but settings save failed — still proceed
console.warn('[VaultSelection] SetCurrentVault:', setErr);
}
// Success — notify app to transition to main UI
window.dispatchEvent(new CustomEvent('verstak:vault-opened')); window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
} catch (e) { } catch (e) {
error = String(e); error = String(e);
@ -59,12 +66,18 @@
} }
opening = true; opening = true;
try { try {
const setErr = await App.SetCurrentVault(openVaultPath.trim()); // Step 1: Open the vault
if (setErr) { const openErr = await App.OpenVault(openVaultPath.trim());
error = setErr; if (openErr) {
error = 'Open vault: ' + openErr;
opening = false; opening = false;
return; return;
} }
// Step 2: Save to app settings
const setErr = await App.SetCurrentVault(openVaultPath.trim());
if (setErr) {
console.warn('[VaultSelection] SetCurrentVault:', setErr);
}
window.dispatchEvent(new CustomEvent('verstak:vault-opened')); window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
} catch (e) { } catch (e) {
error = String(e); error = String(e);
@ -76,12 +89,16 @@
error = ''; error = '';
opening = true; opening = true;
try { try {
const setErr = await App.SetCurrentVault(path); const openErr = await App.OpenVault(path);
if (setErr) { if (openErr) {
error = setErr; error = 'Open vault: ' + openErr;
opening = false; opening = false;
return; return;
} }
const setErr = await App.SetCurrentVault(path);
if (setErr) {
console.warn('[VaultSelection] SetCurrentVault:', setErr);
}
window.dispatchEvent(new CustomEvent('verstak:vault-opened')); window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
} catch (e) { } catch (e) {
error = String(e); error = String(e);
@ -90,6 +107,13 @@
} }
</script> </script>
{#if loading}
<div class="vault-selection">
<div class="vault-selection-inner">
<p class="loading-text">Loading...</p>
</div>
</div>
{:else}
<div class="vault-selection"> <div class="vault-selection">
<div class="vault-selection-inner"> <div class="vault-selection-inner">
<div class="logo"> <div class="logo">
@ -160,6 +184,7 @@
{/if} {/if}
</div> </div>
</div> </div>
{/if}
<style> <style>
.vault-selection { .vault-selection {
@ -174,6 +199,11 @@
max-width: 520px; max-width: 520px;
width: 100%; width: 100%;
} }
.loading-text {
color: #a0a0b8;
text-align: center;
font-size: 1rem;
}
.logo { .logo {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;

View File

@ -9,6 +9,12 @@ export function CloseVault():Promise<void>;
export function CreateVault(arg1:string):Promise<void>; export function CreateVault(arg1:string):Promise<void>;
export function DisablePlugin(arg1:string):Promise<string>;
export function EnablePlugin(arg1:string):Promise<string>;
export function GetAppSettings():Promise<Record<string, any>>;
export function GetCapabilities():Promise<Array<capability.Entry>>; export function GetCapabilities():Promise<Array<capability.Entry>>;
export function GetContributions():Promise<api.ContributionSummary>; export function GetContributions():Promise<api.ContributionSummary>;
@ -17,10 +23,30 @@ export function GetPermissions():Promise<Array<permissions.Entry>>;
export function GetPlugins():Promise<Array<plugin.Plugin>>; export function GetPlugins():Promise<Array<plugin.Plugin>>;
export function GetVaultPluginState():Promise<Record<string, any>>;
export function GetVaultStatus():Promise<Record<string, string>>; export function GetVaultStatus():Promise<Record<string, string>>;
export function OpenVault(arg1:string):Promise<void>; export function OpenVault(arg1:string):Promise<void>;
export function ReadPluginDataJSON(arg1:string,arg2:string):Promise<Record<string, any>>;
export function ReadPluginSetting(arg1:string,arg2:string):Promise<any>;
export function ReadPluginSettings(arg1:string):Promise<Record<string, any>>;
export function RecordDesiredPlugin(arg1:string,arg2:string,arg3:string):Promise<string>;
export function ReloadPlugins():Promise<number|string>; export function ReloadPlugins():Promise<number|string>;
export function SetCurrentVault(arg1:string):Promise<string>;
export function Startup():Promise<void>; export function Startup():Promise<void>;
export function UpdateAppSettings(arg1:Record<string, any>):Promise<string>;
export function WritePluginDataJSON(arg1:string,arg2:string,arg3:Record<string, any>):Promise<string>;
export function WritePluginSetting(arg1:string,arg2:string,arg3:any):Promise<string>;
export function WritePluginSettings(arg1:string,arg2:Record<string, any>):Promise<string>;

View File

@ -10,6 +10,18 @@ export function CreateVault(arg1) {
return window['go']['api']['App']['CreateVault'](arg1); return window['go']['api']['App']['CreateVault'](arg1);
} }
export function DisablePlugin(arg1) {
return window['go']['api']['App']['DisablePlugin'](arg1);
}
export function EnablePlugin(arg1) {
return window['go']['api']['App']['EnablePlugin'](arg1);
}
export function GetAppSettings() {
return window['go']['api']['App']['GetAppSettings']();
}
export function GetCapabilities() { export function GetCapabilities() {
return window['go']['api']['App']['GetCapabilities'](); return window['go']['api']['App']['GetCapabilities']();
} }
@ -26,6 +38,10 @@ export function GetPlugins() {
return window['go']['api']['App']['GetPlugins'](); return window['go']['api']['App']['GetPlugins']();
} }
export function GetVaultPluginState() {
return window['go']['api']['App']['GetVaultPluginState']();
}
export function GetVaultStatus() { export function GetVaultStatus() {
return window['go']['api']['App']['GetVaultStatus'](); return window['go']['api']['App']['GetVaultStatus']();
} }
@ -34,10 +50,46 @@ export function OpenVault(arg1) {
return window['go']['api']['App']['OpenVault'](arg1); return window['go']['api']['App']['OpenVault'](arg1);
} }
export function ReadPluginDataJSON(arg1, arg2) {
return window['go']['api']['App']['ReadPluginDataJSON'](arg1, arg2);
}
export function ReadPluginSetting(arg1, arg2) {
return window['go']['api']['App']['ReadPluginSetting'](arg1, arg2);
}
export function ReadPluginSettings(arg1) {
return window['go']['api']['App']['ReadPluginSettings'](arg1);
}
export function RecordDesiredPlugin(arg1, arg2, arg3) {
return window['go']['api']['App']['RecordDesiredPlugin'](arg1, arg2, arg3);
}
export function ReloadPlugins() { export function ReloadPlugins() {
return window['go']['api']['App']['ReloadPlugins'](); return window['go']['api']['App']['ReloadPlugins']();
} }
export function SetCurrentVault(arg1) {
return window['go']['api']['App']['SetCurrentVault'](arg1);
}
export function Startup() { export function Startup() {
return window['go']['api']['App']['Startup'](); return window['go']['api']['App']['Startup']();
} }
export function UpdateAppSettings(arg1) {
return window['go']['api']['App']['UpdateAppSettings'](arg1);
}
export function WritePluginDataJSON(arg1, arg2, arg3) {
return window['go']['api']['App']['WritePluginDataJSON'](arg1, arg2, arg3);
}
export function WritePluginSetting(arg1, arg2, arg3) {
return window['go']['api']['App']['WritePluginSetting'](arg1, arg2, arg3);
}
export function WritePluginSettings(arg1, arg2) {
return window['go']['api']['App']['WritePluginSettings'](arg1, arg2);
}