fix: polish workspace files and editor shell
This commit is contained in:
@@ -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'}
|
||||
<div class="host-state error">
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<Icon name="warning" size={24} class="error-icon" />
|
||||
<p class="error-title">Plugin View Error</p>
|
||||
<div class="error-details">
|
||||
<p><strong>Plugin:</strong> {currentPluginId || 'unknown'}</p>
|
||||
@@ -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;"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.plugin-bundle-host {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.host-state {
|
||||
@@ -305,7 +312,12 @@
|
||||
}
|
||||
|
||||
.plugin-mount-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
{@const isDangerous = dangerousPermissions.includes(perm)}
|
||||
<span class="tag" class:dangerous={isDangerous}>
|
||||
{perm}
|
||||
{#if isDangerous}<Icon name="warning" size={12} className="danger-icon" />{/if}
|
||||
{#if isDangerous}<Icon name="warning" size={12} class="danger-icon" />{/if}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -362,7 +362,7 @@
|
||||
}
|
||||
|
||||
.check { color: #4ecca3; margin-left: 2px; }
|
||||
.danger-icon { color: #e94560; margin-left: 2px; vertical-align: middle; }
|
||||
:global(.danger-icon) { color: #e94560; margin-left: 2px; vertical-align: middle; }
|
||||
|
||||
.info {
|
||||
color: #ffc857;
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
<div class="loading">Scanning plugin directories...</div>
|
||||
{:else if error}
|
||||
<div class="error">
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<Icon name="warning" size={24} class="error-icon" />
|
||||
<div class="error-message">{error}</div>
|
||||
<button class="retry-btn" on:click={loadAll} type="button">⟳ Retry</button>
|
||||
</div>
|
||||
@@ -454,7 +454,7 @@
|
||||
padding: 2rem; text-align: center; color: #a0a0b8;
|
||||
}
|
||||
.error { color: #e94560; }
|
||||
.error-icon { color: #e94560; margin-bottom: 0.5rem; }
|
||||
:global(.error-icon) { color: #e94560; margin-bottom: 0.5rem; }
|
||||
.error-message {
|
||||
font-family: monospace; font-size: 0.85rem; margin-bottom: 1rem; word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<Icon name="logo" size={20} className="sidebar-logo" />
|
||||
<Icon name="logo" size={20} class="sidebar-logo" />
|
||||
<span class="sidebar-title">Verstak</span>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
on:click={() => handleNav(item.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name={item.icon} size={16} className="nav-icon" />
|
||||
<Icon name={item.icon} size={16} class="nav-icon" />
|
||||
<span class="nav-label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -107,7 +107,7 @@
|
||||
on:click={() => handleSidebarItem(item)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name={item.icon || 'plugin'} size={16} className="nav-icon icon-plugin" />
|
||||
<Icon name={item.icon || 'plugin'} size={16} class="nav-icon icon-plugin" />
|
||||
<span class="nav-label">{item.title || item.id}</span>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -121,7 +121,7 @@
|
||||
<div class="sidebar-footer">
|
||||
{#if errorMessage}
|
||||
<span class="sidebar-error">
|
||||
<Icon name="warning" size={10} className="sidebar-error-icon" />
|
||||
<Icon name="warning" size={10} class="sidebar-error-icon" />
|
||||
Plugin UI error
|
||||
</span>
|
||||
{/if}
|
||||
@@ -152,7 +152,7 @@
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
:global(.sidebar-logo) {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: #4ecca3;
|
||||
@@ -218,13 +218,13 @@
|
||||
color: #e0e0f0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
:global(.nav-icon) {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
color: currentColor;
|
||||
}
|
||||
.nav-icon.icon-plugin {
|
||||
:global(.nav-icon.icon-plugin) {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
color: #e94560;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.sidebar-error-icon {
|
||||
:global(.sidebar-error-icon) {
|
||||
color: #e94560;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
{#if error}
|
||||
<div class="error-box">
|
||||
<Icon name="warning" size={14} className="error-icon" />
|
||||
<Icon name="warning" size={14} class="error-icon" />
|
||||
<span class="error-text">{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -191,7 +191,7 @@
|
||||
{#each recentVaults as path}
|
||||
<li>
|
||||
<button class="recent-item" on:click={() => openRecent(path)} type="button" disabled={opening}>
|
||||
<Icon name="vault" size={16} className="recent-icon" />
|
||||
<Icon name="vault" size={16} class="recent-icon" />
|
||||
<span class="recent-path">{path}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -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;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="view-container">
|
||||
<div class="error-boundary">
|
||||
<div class="error-fallback">
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<Icon name="warning" size={24} class="error-icon" />
|
||||
<p class="error-title">Plugin UI failed</p>
|
||||
<p class="error-text">{renderError}</p>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="view-container">
|
||||
<div class="view" class:degraded={pluginStatus === 'degraded'}>
|
||||
<div class="view-header">
|
||||
<Icon name={currentView.icon || 'logo'} size={20} className="view-icon" />
|
||||
<Icon name={currentView.icon || 'logo'} size={20} class="view-icon" />
|
||||
<h2>{currentView.title}</h2>
|
||||
{#if hasFrontend}
|
||||
<span class="frontend-badge">frontend bundle</span>
|
||||
@@ -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 {
|
||||
|
||||
@@ -54,8 +54,10 @@
|
||||
|
||||
<style>
|
||||
.workbench-host {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a1a2e;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<PluginBundleHost
|
||||
pluginId={tool.pluginId}
|
||||
componentId={tool.component}
|
||||
componentProps={{ workspaceNodeId: currentNodeId, workspaceNode: currentNode }}
|
||||
componentProps={{ workspaceNodeId: currentNodeId, workspaceNode: currentNode, workspaceRootPath: currentNode.path || '' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
export let nodes = [];
|
||||
export let node = null;
|
||||
@@ -66,11 +67,11 @@
|
||||
return nodes.some(n => n.parentId === id);
|
||||
}
|
||||
|
||||
function icon(type) {
|
||||
if (type === 'space') return '\u{1F310}';
|
||||
if (type === 'case') return '\u{1F4CB}';
|
||||
if (type === 'folder') return '\u{1F4C1}';
|
||||
return '\u{1F4C4}';
|
||||
function iconName(type) {
|
||||
if (type === 'space') return 'space';
|
||||
if (type === 'case') return 'case';
|
||||
if (type === 'folder') return 'folder';
|
||||
return 'dot';
|
||||
}
|
||||
|
||||
async function selectNode(id) {
|
||||
@@ -121,7 +122,7 @@
|
||||
<div class="wt-error">{localError}</div>
|
||||
{:else}
|
||||
{#each roots() as node (node.id)}
|
||||
<svelte:self {node} {nodes} {currentNodeId} {expandedNodes} depth={1} {icon} {toggle} {hasKids} {selectNode} {openCreate} />
|
||||
<svelte:self {node} {nodes} {currentNodeId} {expandedNodes} depth={1} {toggle} {hasKids} {selectNode} {openCreate} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -129,7 +130,7 @@
|
||||
<div class="wt-create">
|
||||
<div class="wt-create-header">
|
||||
<span>New {newNodeType}</span>
|
||||
<button class="wt-btn" on:click={cancelCreate} type="button">\u2715</button>
|
||||
<button class="wt-btn" on:click={cancelCreate} type="button">x</button>
|
||||
</div>
|
||||
<input type="text" bind:value={newNodeTitle} placeholder="Name..." disabled={creating} />
|
||||
<div class="wt-create-actions">
|
||||
@@ -143,11 +144,13 @@
|
||||
<div class="wt-node" class:selected={node.id === $activeWorkspaceNodeId} class:archived={node.status === 'archived'} class:sleeping={node.status === 'sleeping'}>
|
||||
<div class="wt-row" style="padding-left: {depth * 1.0 + 0.4}rem;">
|
||||
{#if hasKids(node.id)}
|
||||
<button class="wt-expand" on:click={() => toggle(node.id)} type="button">{expandedNodes[node.id] ? '\u25BE' : '\u25B8'}</button>
|
||||
<button class="wt-expand" on:click={() => toggle(node.id)} type="button" aria-label={expandedNodes[node.id] ? 'Collapse' : 'Expand'}>
|
||||
<Icon name={expandedNodes[node.id] ? 'chevronDown' : 'chevronRight'} size={12} class="wt-expand-icon" />
|
||||
</button>
|
||||
{:else}
|
||||
<span class="wt-expand-spacer"></span>
|
||||
{/if}
|
||||
<span class="wt-icon">{icon(node.type)}</span>
|
||||
<span class="wt-icon"><Icon name={iconName(node.type)} size={13} class="wt-node-icon" /></span>
|
||||
<button class="wt-label" on:click={() => selectNode(node.id)} type="button">{node.title}</button>
|
||||
{#if node.type !== 'case'}
|
||||
<button class="wt-btn wt-btn-small" on:click={() => openCreate(node.id, 'case')} title="Add child" type="button">+</button>
|
||||
@@ -155,7 +158,7 @@
|
||||
</div>
|
||||
{#if expandedNodes[node.id]}
|
||||
{#each childrenOf(node.id) as child (child.id)}
|
||||
<svelte:self node={child} {nodes} {currentNodeId} {expandedNodes} depth={depth + 1} {icon} {toggle} {hasKids} {selectNode} {openCreate} />
|
||||
<svelte:self node={child} {nodes} {currentNodeId} {expandedNodes} depth={depth + 1} {toggle} {hasKids} {selectNode} {openCreate} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -178,7 +181,8 @@
|
||||
.wt-expand { width: 1rem; height: 1rem; min-height: 0; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; color: #666; background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; }
|
||||
.wt-expand:hover { color: #e0e0f0; }
|
||||
.wt-expand-spacer { width: 1rem; flex-shrink: 0; }
|
||||
.wt-icon { font-size: 0.8rem; flex-shrink: 0; }
|
||||
.wt-icon { width: 0.9rem; height: 0.9rem; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; color: #a0a0b8; }
|
||||
:global(.wt-node-icon), :global(.wt-expand-icon) { display: block; }
|
||||
.wt-label { flex: 1; min-height: 0; justify-content: flex-start; background: none; border: none; color: #e0e0f0; font-size: 0.78rem; text-align: left; cursor: pointer; padding: 0.1rem 0.2rem; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wt-label:hover { color: #4ecca3; }
|
||||
.wt-node.archived .wt-label { text-decoration: line-through; opacity: 0.5; }
|
||||
|
||||
@@ -154,9 +154,9 @@
|
||||
status: 'initialized',
|
||||
currentNodeId: 'case-alpha',
|
||||
nodes: [
|
||||
{ id: 'space-main', parentId: '', type: 'space', title: 'Main Space', status: 'active', order: 1 },
|
||||
{ id: 'case-alpha', parentId: 'space-main', type: 'case', title: 'Alpha Case', status: 'active', order: 1 },
|
||||
{ id: 'case-beta', parentId: 'space-main', type: 'case', title: 'Beta Case', status: 'active', order: 2 }
|
||||
{ id: 'space-main', parentId: '', type: 'space', title: 'Main Space', path: 'Main Space', status: 'active', order: 1 },
|
||||
{ id: 'case-alpha', parentId: 'space-main', type: 'case', title: 'Alpha Case', path: 'Main Space/Alpha Case', status: 'active', order: 1 },
|
||||
{ id: 'case-beta', parentId: 'space-main', type: 'case', title: 'Beta Case', path: 'Main Space/Beta Case', status: 'active', order: 2 }
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -176,7 +176,12 @@
|
||||
'Docs/todo.txt': { type: 'file', content: 'Buy groceries\nWrite tests', modifiedAt: new Date().toISOString() },
|
||||
'Docs/readme.md': { type: 'file', content: '# Hello World\n\nThis is a **test** document.\n\n- item 1\n- item 2', modifiedAt: new Date().toISOString() },
|
||||
'Notes': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Notes/Overview.md': { type: 'file', content: '# Notes Overview\n\nMy notes content here.', modifiedAt: new Date().toISOString() }
|
||||
'Notes/Overview.md': { type: 'file', content: '# Notes Overview\n\nMy notes content here.', modifiedAt: new Date().toISOString() },
|
||||
'Main Space': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Main Space/Alpha Case': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Main Space/Alpha Case/alpha-only.txt': { type: 'file', content: 'alpha file', modifiedAt: new Date().toISOString() },
|
||||
'Main Space/Beta Case': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Main Space/Beta Case/beta-only.txt': { type: 'file', content: 'beta file', modifiedAt: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -379,6 +384,12 @@
|
||||
'(function(){',
|
||||
'var DefaultEditor={',
|
||||
'mount:function(c,p,api){',
|
||||
'if(!document.getElementById("mock-default-editor-styles")){',
|
||||
'var style=document.createElement("style");',
|
||||
'style.id="mock-default-editor-styles";',
|
||||
'style.textContent=".de-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.de-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a}.de-toolbar-mode{font-size:.75rem;color:#4ecca3;padding:.15rem .5rem;border-radius:3px;background:#1a2a3a}.de-toolbar-context{font-size:.7rem;color:#8b8ba8}.de-editor-wrap{flex:1;display:flex;min-height:0;overflow:hidden}.de-textarea{flex:1;width:100%;height:100%;resize:none;border:0;outline:0;padding:.75rem;font-family:monospace;font-size:.85rem;line-height:1.6;background:#0d0d1a;color:#e0e0e0}.de-preview{flex:1;height:100%;padding:.75rem 1rem;overflow-y:auto;background:#0d0d1a;line-height:1.7;font-size:.9rem}.de-notes-badge{font-size:.65rem;padding:.1rem .4rem;border-radius:3px;background:#2a1a3a;color:#b388ff}";',
|
||||
'document.head.appendChild(style);',
|
||||
'}',
|
||||
'c.innerHTML="";',
|
||||
'c.className="de-root";',
|
||||
'var req=p.request||{};',
|
||||
@@ -442,13 +453,14 @@
|
||||
"c.innerHTML='';",
|
||||
"c.className='files-root';",
|
||||
"c.setAttribute('data-plugin-id','verstak.files');",
|
||||
"var root=String((p&&(p.workspaceRootPath||(p.workspaceNode&&p.workspaceNode.path)))||'').split('/').filter(Boolean).join('/');",
|
||||
"var list=document.createElement('div');",
|
||||
"list.className='files-list';",
|
||||
"list.setAttribute('data-files-list','');",
|
||||
"c.appendChild(list);",
|
||||
"function load(){",
|
||||
"list.textContent='Loading...';",
|
||||
"api.files.list('').then(function(entries){",
|
||||
"api.files.list(root).then(function(entries){",
|
||||
"list.innerHTML='';",
|
||||
"if(!entries||!entries.length){list.textContent='Empty folder';return;}",
|
||||
"entries.forEach(function(e){",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
/**
|
||||
* Icon.svelte — Renders inline SVG icons.
|
||||
* Icon.svelte — Renders bundled Lucide SVG icons.
|
||||
*
|
||||
* Usage:
|
||||
* <Icon name="puzzle" size={18} />
|
||||
@@ -8,26 +8,60 @@
|
||||
*
|
||||
* Rules:
|
||||
* - NO emoji, NO unicode pictographic symbols
|
||||
* - ALL icons must be SVG registered in ../ui/icons.js
|
||||
* - If name is not found, renders a default dot icon
|
||||
* - NO dependency on system icon fonts
|
||||
* - If name is not found, renders a default circle icon
|
||||
*/
|
||||
import { iconPaths } from './icons.js';
|
||||
import Briefcase from 'lucide-svelte/icons/briefcase';
|
||||
import ChevronDown from 'lucide-svelte/icons/chevron-down';
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
import Circle from 'lucide-svelte/icons/circle';
|
||||
import FlaskConical from 'lucide-svelte/icons/flask-conical';
|
||||
import Folder from 'lucide-svelte/icons/folder';
|
||||
import LayoutGrid from 'lucide-svelte/icons/layout-grid';
|
||||
import PanelsTopLeft from 'lucide-svelte/icons/panels-top-left';
|
||||
import Pencil from 'lucide-svelte/icons/pencil';
|
||||
import Plug from 'lucide-svelte/icons/plug';
|
||||
import Puzzle from 'lucide-svelte/icons/puzzle';
|
||||
import Settings from 'lucide-svelte/icons/settings';
|
||||
import Shield from 'lucide-svelte/icons/shield';
|
||||
import TriangleAlert from 'lucide-svelte/icons/triangle-alert';
|
||||
|
||||
export let name = 'dot';
|
||||
export let size = 16;
|
||||
export let className = '';
|
||||
|
||||
$: icon = iconPaths[name] || iconPaths.dot;
|
||||
const icons = {
|
||||
case: Briefcase,
|
||||
chevronDown: ChevronDown,
|
||||
chevronRight: ChevronRight,
|
||||
dot: Circle,
|
||||
edit: Pencil,
|
||||
flask: FlaskConical,
|
||||
folder: Folder,
|
||||
gear: Settings,
|
||||
logo: PanelsTopLeft,
|
||||
plugin: Plug,
|
||||
puzzle: Puzzle,
|
||||
space: LayoutGrid,
|
||||
vault: Shield,
|
||||
warning: TriangleAlert,
|
||||
};
|
||||
|
||||
const aliases = {
|
||||
'🧪': 'flask',
|
||||
danger: 'warning',
|
||||
settings: 'gear',
|
||||
};
|
||||
|
||||
$: iconKey = aliases[name] || name || 'dot';
|
||||
$: IconComponent = icons[iconKey] || icons.dot;
|
||||
$: iconClass = [className, $$restProps.class].filter(Boolean).join(' ');
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={icon.viewBox}
|
||||
width={size}
|
||||
height={size}
|
||||
class={className}
|
||||
>
|
||||
{#each icon.paths as p}
|
||||
<path d={p.d} {...p.attrs} />
|
||||
{/each}
|
||||
</svg>
|
||||
<svelte:component
|
||||
this={IconComponent}
|
||||
size={size}
|
||||
{...$$restProps}
|
||||
class={iconClass}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* icons.js — Centralised SVG icon set for Verstak.
|
||||
*
|
||||
* RULE: Icons MUST be SVG only. No emoji, no unicode symbols, no font-icons.
|
||||
* The Wails WebKitGTK webview does NOT render colour emoji.
|
||||
*
|
||||
* Each icon is an object: { viewBox, paths }
|
||||
* - viewBox: string, default '0 0 24 24'
|
||||
* - paths: array of <path> attributes or objects with { d, ...attrs }
|
||||
*
|
||||
* Usage in Svelte:
|
||||
* import { iconPaths } from '../lib/ui/icons.js';
|
||||
* <svg viewBox={iconPaths.puzzle.viewBox} width="16" height="16">
|
||||
* {#each iconPaths.puzzle.paths as p}
|
||||
* <path d={p.d} {...p.attrs} />
|
||||
* {/each}
|
||||
* </svg>
|
||||
*/
|
||||
|
||||
export const iconPaths = {
|
||||
/** Plugin Manager — puzzle piece */
|
||||
puzzle: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M4 5a3 3 0 0 1 3-3h4v2a2 2 0 0 0 4 0V2h4a2 2 0 0 1 2 2v4h-2a2 2 0 0 0 0 4h2v4a2 2 0 0 1-2 2h-4v-2a2 2 0 0 0-4 0v2H7a3 3 0 0 1-3-3v-3h2a2 2 0 0 0 0-4H4V5Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Platform Test / Diagnostics — flask/test-tube */
|
||||
flask: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M9 2v5.586l-5.293 5.293A1 1 0 0 0 4 14v4a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4a1 1 0 0 0-.293-.707L15 7.586V2H9Zm2 0v6a1 1 0 0 0 .293.707l5 5V14a1 1 0 0 1-1 1h-1l-2-2-2 2H8a1 1 0 0 1-1-1v-.293l3-3V2h3Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Verstak logo — stack/tray */
|
||||
logo: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M5 3h14a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Zm0 2v3h14V5H5Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
{ d: 'M5 12h14a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2Zm0 2v3h14v-3H5Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Default fallback — circle */
|
||||
dot: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 6a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Vault — safe / shield */
|
||||
vault: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4Zm0 2.18L19 6.4v4.6c0 4.5-3.07 8.68-7 9.82-3.93-1.14-7-5.32-7-9.82V6.4l7-3.22ZM11 8v2H9v2h2v4h2v-4h2v-2h-2V8h-2Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Settings — gear */
|
||||
gear: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M19.14 13l.57-1.43 1.79-.5-1-2.29-1.64.73-.29-.28-.73-1.64 2.29-1-1-2.29-1.79.5-.57 1.43-2.17.17-.57-1.43-1.79-.5-1 2.29 1.64.73-.29.28-.73 1.64-2.29-1-1 2.29 1.79.5.57 1.43-.57 1.43-1.79.5 1 2.29 1.64-.73.29.28.73 1.64-2.29 1 1 2.29 1.79-.5.57-1.43 2.17-.17.57 1.43 1.79.5 1-2.29-1.64-.73.29-.28.73-1.64 2.29 1 1-2.29-1.79-.5-.57-1.43ZM12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** Warning / error — triangle */
|
||||
warning: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M1 21h22L12 2 1 21Zm12-3h-2v-2h2v2Zm0-4h-2v-4h2v4Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/** General plugin — extension/puzzle alternative */
|
||||
plugin: {
|
||||
viewBox: '0 0 24 24',
|
||||
paths: [
|
||||
{ d: 'M11 2H5v6.172a3 3 0 0 0 0 5.656V20a1 1 0 0 0 1 1h4v-2H7v-4a1 1 0 0 0-1-1H5v-2h1a1 1 0 0 0 1-1V8h2V4h2a1 1 0 0 0 1-1V2h-1Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
{ d: 'M17 2a3 3 0 0 1 3 3v1h-4V5a1 1 0 0 0-1-1h-2v2h2v2h-2v2h4V8h2v2a3 3 0 0 1-3 3h-1v2h1a2 2 0 0 1 2 2v2h2v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2h2v-2h-2v-2h4a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-2v2h-2V4a2 2 0 0 1 2-2h3Z',
|
||||
attrs: { fill: 'currentColor', stroke: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an SVG icon string inline.
|
||||
* Returns an SVG string suitable for {@html } or innerHTML.
|
||||
* @param {string} name - icon key
|
||||
* @param {number|string} size - width/height
|
||||
* @param {string} className - optional CSS class
|
||||
*/
|
||||
export function svgIcon(name, size = 16, className = '') {
|
||||
const icon = iconPaths[name];
|
||||
if (!icon) return svgIcon('dot', size, className);
|
||||
const paths = icon.paths
|
||||
.map(p => {
|
||||
const attrs = p.attrs
|
||||
? Object.entries(p.attrs).map(([k, v]) => `${k}="${v}"`).join(' ')
|
||||
: '';
|
||||
return `<path d="${p.d}"${attrs ? ' ' + attrs : ''}/>`;
|
||||
})
|
||||
.join('');
|
||||
const cls = className ? ` class="${className}"` : '';
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${icon.viewBox}" width="${size}" height="${size}"${cls}>${paths}</svg>`;
|
||||
}
|
||||
Reference in New Issue
Block a user