From 5c979174f1882c1f1edb69520b300d1641885e99 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 19 Jun 2026 23:37:10 +0800 Subject: [PATCH] fix: polish workspace files and editor shell --- docs/NOTES_FILES_PLUGIN_PLAN.md | 8 +- docs/PLUGIN_RUNTIME.md | 40 +++- frontend/e2e/files-plugin.spec.js | 23 ++ frontend/e2e/plugin-manager-layout.spec.js | 11 + frontend/package-lock.json | 11 + frontend/package.json | 1 + frontend/package.json.md5 | 2 +- .../lib/plugin-host/PluginBundleHost.svelte | 22 +- .../src/lib/plugin-manager/PluginCard.svelte | 4 +- .../lib/plugin-manager/PluginManager.svelte | 4 +- frontend/src/lib/shell/Sidebar.svelte | 16 +- frontend/src/lib/shell/VaultSelection.svelte | 8 +- frontend/src/lib/shell/ViewContainer.svelte | 8 +- frontend/src/lib/shell/WorkbenchHost.svelte | 2 + frontend/src/lib/shell/WorkspaceHost.svelte | 2 +- frontend/src/lib/shell/WorkspaceTree.svelte | 26 ++- frontend/src/lib/test/wails-mock.js | 22 +- frontend/src/lib/ui/Icon.svelte | 66 ++++-- frontend/src/lib/ui/icons.js | 128 ----------- internal/core/workspace/manager.go | 211 +++++++++++++++++- internal/core/workspace/manager_test.go | 61 +++++ scripts/install-dev-plugins.sh | 32 ++- scripts/update-and-build-all.sh | 38 +--- 23 files changed, 497 insertions(+), 249 deletions(-) delete mode 100644 frontend/src/lib/ui/icons.js diff --git a/docs/NOTES_FILES_PLUGIN_PLAN.md b/docs/NOTES_FILES_PLUGIN_PLAN.md index d9d5e15..9879e2f 100644 --- a/docs/NOTES_FILES_PLUGIN_PLAN.md +++ b/docs/NOTES_FILES_PLUGIN_PLAN.md @@ -54,9 +54,11 @@ Canonical rules: Canonical scoped paths: -- Workspace/root overview notes live under `Notes/`. -- Case/project/folder scoped notes live under `/Notes/`. -- The default overview note is `/Notes/Overview.md`. +- Workspace/root overview notes live under `/Notes/`. +- Case/project/folder scoped notes live under `/Notes/`. +- The default overview note is `/Notes/Overview.md`. +- `workspace-node-path` is a normal vault-relative folder path stored on the + workspace node. Files plugin workspace views are scoped to this path. Visibility requirements: diff --git a/docs/PLUGIN_RUNTIME.md b/docs/PLUGIN_RUNTIME.md index 4543753..ec69d8a 100644 --- a/docs/PLUGIN_RUNTIME.md +++ b/docs/PLUGIN_RUNTIME.md @@ -146,7 +146,7 @@ foreach plugin: |---|---|---| | `description` | string | Описание плагина | | `source` | string | `"official"`, `"local"`, `"third-party"` | -| `icon` | string | Иконка (emoji или имя) | +| `icon` | string | Имя иконки из встроенного Lucide-набора shell | | `requires` | string[] | Жёзкие capability-зависимости | | `optionalRequires` | string[] | Мягкие capability-зависимости | | `frontend` | object | `{ "entry": "path/to/index.js", "style": "path/to/style.css" }` | @@ -180,6 +180,8 @@ foreach plugin: Плагины могут регистрировать UI contributions через поле `contributes` в `plugin.json`. +Icon fields use shell icon names rendered through the bundled Lucide SVG wrapper. Plugins must not rely on emoji, Unicode pictographs, or system icon fonts; if a plugin needs its own icon font, it must bundle the font and reference it from its own frontend bundle. + ### Реализованные contribution points (Milestone 5a) | Тип | Поле manifest | Описание | Frontend host | @@ -210,7 +212,7 @@ foreach plugin: { "id": "mypanel.sidebar", "title": "My Panel", - "icon": "📌", + "icon": "puzzle", "view": "mypanel.view", "position": 100 } @@ -219,7 +221,7 @@ foreach plugin: { "id": "mypanel.view", "title": "My Panel View", - "icon": "📌", + "icon": "puzzle", "component": "MyPanelComponent" } ], @@ -234,7 +236,7 @@ foreach plugin: { "id": "mypanel.cmd", "title": "Do Something", - "icon": "⚡", + "icon": "gear", "handler": "doSomething" } ] @@ -717,12 +719,13 @@ Vault plugin state хранится **внутри vault** в `.verstak/plugins. │ │ │ │ │ │ │ Verstak │ PluginManager | ViewContainer │ │ │ │ │ │ │ -│ │ 🧩 Plugin│ (padding: 1.5rem) │ │ +│ │ [icon] │ (padding: 1.5rem) │ │ +│ │ Plugin │ │ │ │ │ Manager│ │ │ │ │ │ │ │ │ │ Plugins │ │ │ -│ │ 📌 item1 │ │ │ -│ │ 📌 item2 │ │ │ +│ │ [icon] item1 │ │ +│ │ [icon] item2 │ │ │ │ │ │ │ │ │ ● Vault │ │ │ │ └──────────┴──────────────────────────────────────┘ │ @@ -759,6 +762,7 @@ Workspace — центральная модель Верстака вокруг | `parentId` | string | ID родителя (пусто для root) | | `type` | space/case/folder | Тип ноды | | `title` | string | Название | +| `path` | string | Vault-relative папка ноды | | `status` | active/sleeping/archived | Жизненный цикл | | `tags` | string[] | Теги | | `order` | int | Порядок среди siblings | @@ -767,7 +771,20 @@ Workspace — центральная модель Верстака вокруг ### Хранение -`/.verstak/workspace.json` — атомарная запись (temp + rename). +`/.verstak/workspace.json` — атомарная запись metadata (temp + rename). +Каждая workspace node также имеет user-visible folder inside vault. `path` +хранит canonical vault-relative folder path. Имена папок читаемые: берутся из +title, очищаются от запрещённых символов, сохраняют Unicode/кириллицу, а при +коллизии получают suffix ` (2)`, ` (3)`, ... + +Пример: + +``` +/ + My Workspace/ + Test/ + test/ +``` ### API @@ -786,8 +803,15 @@ Workspace — центральная модель Верстака вокруг ### Правила - Root node создаётся при создании vault +- Для каждой node создаётся обычная папка внутри vault +- WorkspaceItems получают выбранную node и `workspaceRootPath`; Files plugin + показывает именно эту папку, а не общий root vault - Порядок children стабилен (sort by order) - Нельзя переместить ноду в себя или в своего потомка +- `MoveWorkspaceNode` переносит physical folder subtree and updates descendant + paths +- `RenameWorkspaceNode` меняет display title; physical folder rename/UI для этого + остаётся отдельным действием - Архивирование — soft delete (status = archived) - Corrupt JSON → backup + defaults diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js index c1d9c82..04cc7cb 100644 --- a/frontend/e2e/files-plugin.spec.js +++ b/frontend/e2e/files-plugin.spec.js @@ -27,6 +27,19 @@ test.describe('G: Files Plugin', () => { await expect(sidebarItem).toHaveCount(0); }); + test('workspace Files view is scoped to selected workspace folder', async ({ page }) => { + await page.locator('.wt-label').filter({ hasText: 'Alpha Case' }).click(); + + await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.files-item-name').filter({ hasText: 'alpha-only.txt' })).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.files-item-name').filter({ hasText: 'beta-only.txt' })).toHaveCount(0); + + await page.locator('.wt-label').filter({ hasText: 'Beta Case' }).click(); + + await expect(page.locator('.files-item-name').filter({ hasText: 'beta-only.txt' })).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.files-item-name').filter({ hasText: 'alpha-only.txt' })).toHaveCount(0); + }); + test('open .txt via workbench from files context shows default-editor', async ({ page }) => { await page.evaluate(async () => { const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', { @@ -42,6 +55,11 @@ test.describe('G: Files Plugin', () => { const editor = page.locator('[data-editor-mode="text"]'); await expect(editor).toBeVisible({ timeout: 10000 }); await expect(editor).toHaveAttribute('data-resource-path', 'Docs/todo.txt'); + + const textarea = page.locator('.de-textarea'); + await expect(textarea).toBeVisible({ timeout: 10000 }); + const textareaBox = await textarea.boundingBox(); + expect(textareaBox.height).toBeGreaterThan(300); }); test('open .md via workbench from files context shows generic-markdown', async ({ page }) => { @@ -59,6 +77,11 @@ test.describe('G: Files Plugin', () => { const workbench = page.locator('.workbench-host'); await expect(workbench).toBeVisible({ timeout: 10000 }); await expect(workbench.locator('.workbench-title')).toHaveText('Docs/readme.md'); + + const preview = page.locator('.de-preview'); + await expect(preview).toBeVisible({ timeout: 10000 }); + const previewBox = await preview.boundingBox(); + expect(previewBox.height).toBeGreaterThan(300); }); test('open notes markdown via workbench from files context shows notes-markdown', async ({ page }) => { diff --git a/frontend/e2e/plugin-manager-layout.spec.js b/frontend/e2e/plugin-manager-layout.spec.js index 7edcfd3..57bf7f4 100644 --- a/frontend/e2e/plugin-manager-layout.spec.js +++ b/frontend/e2e/plugin-manager-layout.spec.js @@ -81,4 +81,15 @@ test.describe('E: Plugin Manager layout', () => { await expect(selected).toHaveCount(1); await expect(selected).toHaveText('Beta Case'); }); + + test('shell icons render through bundled Lucide SVG components', async ({ page }) => { + const logo = page.locator('.sidebar-logo'); + await expect(logo).toBeVisible(); + await expect(logo).toHaveClass(/lucide/); + + await page.locator('.wt-label').filter({ hasText: 'Alpha Case' }).click(); + const workspaceIcon = page.locator('.wt-node-icon').first(); + await expect(workspaceIcon).toBeVisible(); + await expect(workspaceIcon).toHaveClass(/lucide/); + }); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c2c709e..ec1add8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@playwright/test": "^1.61.0", "@sveltejs/vite-plugin-svelte": "^3.1.0", + "lucide-svelte": "^0.577.0", "svelte": "^4.2.0", "vite": "^5.4.0" } @@ -982,6 +983,16 @@ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true }, + "node_modules/lucide-svelte": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz", + "integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==", + "deprecated": "Package deprecated. Please use @lucide/svelte instead.", + "dev": true, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 664699a..9fbe43c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@playwright/test": "^1.61.0", "@sveltejs/vite-plugin-svelte": "^3.1.0", + "lucide-svelte": "^0.577.0", "svelte": "^4.2.0", "vite": "^5.4.0" } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index f7b5fea..c720b76 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -43be2fbdf6ba6ca9504a7c4b0ac32ae0 \ No newline at end of file +c02aa6458981565332c3b0bc49516b42 \ No newline at end of file diff --git a/frontend/src/lib/plugin-host/PluginBundleHost.svelte b/frontend/src/lib/plugin-host/PluginBundleHost.svelte index 7289b3d..5f33949 100644 --- a/frontend/src/lib/plugin-host/PluginBundleHost.svelte +++ b/frontend/src/lib/plugin-host/PluginBundleHost.svelte @@ -17,13 +17,15 @@ let currentPluginId = null; let currentComponent = null; let currentAPI = null; + let currentPropsKey = ''; $: activePluginId = pluginId || viewPluginId; $: activeComponent = componentId; + $: propsKey = JSON.stringify(componentProps || {}); // React to changes — reload on view change $: if (activePluginId && activeComponent) { - loadAndMount(activePluginId, activeComponent); + loadAndMount(activePluginId, activeComponent, propsKey); } else if (!activePluginId) { cleanup(); loadState = 'idle'; @@ -58,6 +60,7 @@ currentPluginId = null; currentComponent = null; currentAPI = null; + currentPropsKey = ''; } function unpackBackendResult(result) { @@ -67,9 +70,9 @@ return { value: result, error: '' }; } - async function loadAndMount(pId, compId) { + async function loadAndMount(pId, compId, nextPropsKey) { // If same plugin+component and already mounted, skip - if (currentPluginId === pId && currentComponent === compId && loadState === 'loaded') { + if (currentPluginId === pId && currentComponent === compId && currentPropsKey === nextPropsKey && loadState === 'loaded') { return; } @@ -80,6 +83,7 @@ errorText = ''; currentPluginId = pId; currentComponent = compId; + currentPropsKey = nextPropsKey; try { // Get plugin frontend info @@ -181,7 +185,7 @@ {:else if loadState === 'error'}
- +

Plugin View Error

Plugin: {currentPluginId || 'unknown'}

@@ -209,17 +213,20 @@ bind:this={mountContainer} data-plugin-id={currentPluginId} data-component={currentComponent} + style="flex:1;min-width:0;min-height:0;height:100%;display:flex;flex-direction:column;position:relative;" >
{/if}
diff --git a/frontend/src/lib/shell/VaultSelection.svelte b/frontend/src/lib/shell/VaultSelection.svelte index 489e4b1..a5c0c1f 100644 --- a/frontend/src/lib/shell/VaultSelection.svelte +++ b/frontend/src/lib/shell/VaultSelection.svelte @@ -135,7 +135,7 @@ {#if error}
- + {error}
{/if} @@ -191,7 +191,7 @@ {#each recentVaults as path}
  • @@ -247,7 +247,7 @@ font-size: 0.85rem; color: #e94560; } - .error-icon { flex-shrink: 0; } + :global(.error-icon) { flex-shrink: 0; } .error-text { word-break: break-word; } .actions { display: flex; @@ -369,7 +369,7 @@ opacity: 0.5; cursor: not-allowed; } - .recent-icon { flex-shrink: 0; } + :global(.recent-icon) { flex-shrink: 0; } .recent-path { overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/src/lib/shell/ViewContainer.svelte b/frontend/src/lib/shell/ViewContainer.svelte index 744377a..ed974be 100644 --- a/frontend/src/lib/shell/ViewContainer.svelte +++ b/frontend/src/lib/shell/ViewContainer.svelte @@ -48,7 +48,7 @@
    - +

    Plugin UI failed

    {renderError}

    @@ -58,7 +58,7 @@
    - +

    {currentView.title}

    {#if hasFrontend} frontend bundle @@ -128,7 +128,7 @@ color: #e0e0f0; flex: 1; } - .view-icon { + :global(.view-icon) { width: 1.3rem; height: 1.3rem; color: #a78bfa; @@ -212,7 +212,7 @@ text-align: center; padding: 2rem; } - .error-icon { + :global(.error-icon) { color: #e94560; } .error-title { diff --git a/frontend/src/lib/shell/WorkbenchHost.svelte b/frontend/src/lib/shell/WorkbenchHost.svelte index 9628791..016272b 100644 --- a/frontend/src/lib/shell/WorkbenchHost.svelte +++ b/frontend/src/lib/shell/WorkbenchHost.svelte @@ -54,8 +54,10 @@