feat: add workspace header search slot
This commit is contained in:
parent
8f6559cdb6
commit
874b7c7ffb
|
|
@ -144,6 +144,15 @@ export function createPluginAPI(pluginId) {
|
||||||
return {
|
return {
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
openSettings: function(panelId) {
|
||||||
|
assertActive('ui.openSettings');
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:open-settings', {
|
||||||
|
detail: { pluginId: pluginId, panelId: panelId || '' }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
capabilities: {
|
capabilities: {
|
||||||
has: async function(capId) {
|
has: async function(capId) {
|
||||||
const info = await callBackend(pluginId, 'capabilities.has(' + capId + ')', function() {
|
const info = await callBackend(pluginId, 'capabilities.has(' + capId + ')', function() {
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,10 @@
|
||||||
export let activeSettingsPluginId = '';
|
export let activeSettingsPluginId = '';
|
||||||
export let activeSettingsPanelId = '';
|
export let activeSettingsPanelId = '';
|
||||||
|
|
||||||
$: if (activeSettingsPluginId && activeSettingsPanelId) {
|
$: if (activeSettingsPluginId) {
|
||||||
const key = `${activeSettingsPluginId}:${activeSettingsPanelId}`;
|
const settingsPanelCount = (contributions.settingsPanels || []).length;
|
||||||
if (key !== lastOpenedKey) {
|
const key = `${activeSettingsPluginId}:${activeSettingsPanelId || '*'}`;
|
||||||
lastOpenedKey = key;
|
if (key !== lastOpenedKey || settingsPanelCount === 0) {
|
||||||
openSettingsFromProps(activeSettingsPluginId, activeSettingsPanelId);
|
openSettingsFromProps(activeSettingsPluginId, activeSettingsPanelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +71,7 @@
|
||||||
async function openSettingsFromProps(pluginId, panelId) {
|
async function openSettingsFromProps(pluginId, panelId) {
|
||||||
const panel = (contributions.settingsPanels || []).find(sp => sp.pluginId === pluginId && (!panelId || sp.id === panelId));
|
const panel = (contributions.settingsPanels || []).find(sp => sp.pluginId === pluginId && (!panelId || sp.id === panelId));
|
||||||
if (panel) {
|
if (panel) {
|
||||||
|
lastOpenedKey = `${pluginId}:${panelId || '*'}`;
|
||||||
settingsPanel = panel;
|
settingsPanel = panel;
|
||||||
settingsPluginId = pluginId;
|
settingsPluginId = pluginId;
|
||||||
settingsError = null;
|
settingsError = null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import * as App from '../../../wailsjs/go/api/App';
|
import * as App from '../../../wailsjs/go/api/App';
|
||||||
|
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||||
import Icon from '../ui/Icon.svelte';
|
import Icon from '../ui/Icon.svelte';
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
|
|
@ -65,6 +66,10 @@
|
||||||
settingsOpen = false;
|
settingsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusItemProps(item) {
|
||||||
|
return { statusBarItem: item };
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadStatusBar();
|
loadStatusBar();
|
||||||
window.addEventListener('verstak:plugins-changed', loadStatusBar);
|
window.addEventListener('verstak:plugins-changed', loadStatusBar);
|
||||||
|
|
@ -92,21 +97,33 @@
|
||||||
</span>
|
</span>
|
||||||
{#each leftItems as item}
|
{#each leftItems as item}
|
||||||
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
||||||
|
{#if item.handler}
|
||||||
|
<PluginBundleHost pluginId={item.pluginId} componentId={item.handler} componentProps={statusItemProps(item)} />
|
||||||
|
{:else}
|
||||||
{item.label || item.id}
|
{item.label || item.id}
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="status-bar-group status-center">
|
<div class="status-bar-group status-center">
|
||||||
{#each centerItems as item}
|
{#each centerItems as item}
|
||||||
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
||||||
|
{#if item.handler}
|
||||||
|
<PluginBundleHost pluginId={item.pluginId} componentId={item.handler} componentProps={statusItemProps(item)} />
|
||||||
|
{:else}
|
||||||
{item.label || item.id}
|
{item.label || item.id}
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="status-bar-group status-right">
|
<div class="status-bar-group status-right">
|
||||||
{#each rightItems as item}
|
{#each rightItems as item}
|
||||||
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
<span class="status-bar-item" data-status-item-id={item.id} title={item.pluginId}>
|
||||||
|
{#if item.handler}
|
||||||
|
<PluginBundleHost pluginId={item.pluginId} componentId={item.handler} componentProps={statusItemProps(item)} />
|
||||||
|
{:else}
|
||||||
{item.label || item.id}
|
{item.label || item.id}
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="settings-menu-wrap">
|
<div class="settings-menu-wrap">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,15 @@
|
||||||
let workspaceTools = [];
|
let workspaceTools = [];
|
||||||
let activeToolKey = '';
|
let activeToolKey = '';
|
||||||
|
|
||||||
|
const toolOrder = new Map([
|
||||||
|
['notes', 10],
|
||||||
|
['files', 20],
|
||||||
|
['activity', 40],
|
||||||
|
['browser', 50],
|
||||||
|
['inbox', 50],
|
||||||
|
['search', 90],
|
||||||
|
]);
|
||||||
|
|
||||||
$: selectedWorkspace = nodes.find(n => n.id === selectedWorkspaceName || n.name === selectedWorkspaceName || n.rootPath === selectedWorkspaceName) || null;
|
$: selectedWorkspace = nodes.find(n => n.id === selectedWorkspaceName || n.name === selectedWorkspaceName || n.rootPath === selectedWorkspaceName) || null;
|
||||||
$: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || '';
|
$: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || '';
|
||||||
$: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName;
|
$: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName;
|
||||||
|
|
@ -24,6 +33,22 @@
|
||||||
return `${tool?.pluginId || ''}:${tool?.id || ''}`;
|
return `${tool?.pluginId || ''}:${tool?.id || ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toolRank(tool) {
|
||||||
|
const text = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
|
||||||
|
for (const [needle, rank] of toolOrder.entries()) {
|
||||||
|
if (text.includes(needle)) return rank;
|
||||||
|
}
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortWorkspaceTools(tools) {
|
||||||
|
return [...tools].sort((a, b) => {
|
||||||
|
const rankDiff = toolRank(a) - toolRank(b);
|
||||||
|
if (rankDiff !== 0) return rankDiff;
|
||||||
|
return String(a.title || a.id).localeCompare(String(b.title || b.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTools() {
|
async function loadTools() {
|
||||||
try {
|
try {
|
||||||
const [c, p] = await Promise.all([
|
const [c, p] = await Promise.all([
|
||||||
|
|
@ -37,7 +62,7 @@
|
||||||
plugins.filter(pl => pl.enabled && (pl.status === 'loaded' || pl.status === 'degraded')).map(pl => pl.manifest?.id)
|
plugins.filter(pl => pl.enabled && (pl.status === 'loaded' || pl.status === 'degraded')).map(pl => pl.manifest?.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
workspaceTools = (contributions.workspaceItems || []).filter(tool => enabledIds.has(tool.pluginId));
|
workspaceTools = sortWorkspaceTools((contributions.workspaceItems || []).filter(tool => enabledIds.has(tool.pluginId)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[WorkspaceHost] loadTools error:', e);
|
console.error('[WorkspaceHost] loadTools error:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -47,9 +72,14 @@
|
||||||
<div class="workspace-host">
|
<div class="workspace-host">
|
||||||
{#if selectedWorkspace}
|
{#if selectedWorkspace}
|
||||||
<div class="workspace-header">
|
<div class="workspace-header">
|
||||||
|
<div class="workspace-title-group">
|
||||||
<span class="workspace-title">{workspaceTitle}</span>
|
<span class="workspace-title">{workspaceTitle}</span>
|
||||||
<span class="workspace-type">{workspaceType}</span>
|
<span class="workspace-type">{workspaceType}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="workspace-search" data-workspace-search>
|
||||||
|
<input type="search" placeholder="Search workspace" aria-label="Search workspace" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if workspaceTools.length > 0}
|
{#if workspaceTools.length > 0}
|
||||||
<div class="workspace-tabs" role="tablist" aria-label="Workspace tools">
|
<div class="workspace-tabs" role="tablist" aria-label="Workspace tools">
|
||||||
|
|
@ -101,12 +131,20 @@
|
||||||
.workspace-header {
|
.workspace-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid #16213e;
|
border-bottom: 1px solid #16213e;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-title-group {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-title {
|
.workspace-title {
|
||||||
color: #e0e0f0;
|
color: #e0e0f0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
|
@ -121,6 +159,40 @@
|
||||||
background: #1a2a3a;
|
background: #1a2a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-search {
|
||||||
|
flex: 0 1 22rem;
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-search input {
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
|
border: 1px solid #283653;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #101626;
|
||||||
|
color: #e0e0f0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-search input:focus {
|
||||||
|
border-color: #4ecca3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.workspace-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-search {
|
||||||
|
flex-basis: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-tabs {
|
.workspace-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
settingsPanels: [{ id: 'verstak.sync.settings', title: 'Sync', component: 'SyncSettings' }],
|
settingsPanels: [{ id: 'verstak.sync.settings', title: 'Sync', component: 'SyncSettings' }],
|
||||||
statusBarItems: [{ id: 'verstak.sync.status', label: 'Sync', position: 'right' }]
|
statusBarItems: [{ id: 'verstak.sync.status', label: 'Sync', position: 'right', handler: 'SyncStatusBar' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rootPath: '/tmp/verstak-test/plugins/sync',
|
rootPath: '/tmp/verstak-test/plugins/sync',
|
||||||
|
|
@ -1703,7 +1703,7 @@
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
settingsPanels: [{ id: 'verstak.sync.settings', title: 'Sync', component: 'SyncSettings' }],
|
settingsPanels: [{ id: 'verstak.sync.settings', title: 'Sync', component: 'SyncSettings' }],
|
||||||
statusBarItems: [{ id: 'verstak.sync.status', label: 'Sync', position: 'right' }]
|
statusBarItems: [{ id: 'verstak.sync.status', label: 'Sync', position: 'right', handler: 'SyncStatusBar' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rootPath: '/tmp/verstak-test/plugins/sync',
|
rootPath: '/tmp/verstak-test/plugins/sync',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = path.resolve('.');
|
||||||
|
|
||||||
|
function read(relativePath) {
|
||||||
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIncludes(source, needle, message) {
|
||||||
|
if (!source.includes(needle)) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceHost = read('frontend/src/lib/shell/WorkspaceHost.svelte');
|
||||||
|
const statusBar = read('frontend/src/lib/shell/StatusBar.svelte');
|
||||||
|
const pluginManager = read('frontend/src/lib/plugin-manager/PluginManager.svelte');
|
||||||
|
const syncManifest = JSON.parse(read('../verstak-official-plugins/plugins/sync/plugin.json'));
|
||||||
|
|
||||||
|
assertIncludes(
|
||||||
|
workspaceHost,
|
||||||
|
'data-workspace-search',
|
||||||
|
'WorkspaceHost should expose a stable workspace header search slot'
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
workspaceHost,
|
||||||
|
'toolOrder',
|
||||||
|
'WorkspaceHost should define usage-based workspace tool ordering'
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
workspaceHost,
|
||||||
|
'sortWorkspaceTools',
|
||||||
|
'WorkspaceHost should sort workspace tools by expected usage'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertIncludes(
|
||||||
|
statusBar,
|
||||||
|
"import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';",
|
||||||
|
'StatusBar should mount plugin-provided status bar components'
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
statusBar,
|
||||||
|
'componentId={item.handler}',
|
||||||
|
'StatusBar should use statusBarItems.handler as the component id'
|
||||||
|
);
|
||||||
|
|
||||||
|
const syncStatus = syncManifest.contributes.statusBarItems.find((item) => item.id === 'verstak.sync.status');
|
||||||
|
if (!syncStatus || syncStatus.handler !== 'SyncStatusBar') {
|
||||||
|
throw new Error('Sync statusBarItem should declare handler "SyncStatusBar"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/lastOpenedKey\s*=\s*key;\s*openSettingsFromProps\(activeSettingsPluginId,\s*activeSettingsPanelId\)/s.test(pluginManager)) {
|
||||||
|
throw new Error('PluginManager should not mark settings panel as opened before resolving contributions');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('shell source contract smoke passed');
|
||||||
Loading…
Reference in New Issue