fix: replace emoji icons with inline SVG (Icon.svelte + icons.js)
All emoji characters replaced with inline SVG icons: - Plugin Manager sidebar: puzzle SVG icon - Verstak logo: stack/tray SVG icon - Plugin icons: flask SVG (from plugin manifest) - Warning/error indicators: warning triangle SVG - Settings button: gear SVG - Vault recent: vault/shield SVG - Fallback: dot SVG New components: - frontend/src/lib/ui/icons.js — SVG path map - frontend/src/lib/ui/Icon.svelte — reusable SVG icon component Icon policy: NO emoji or unicode pictographic symbols in the app. Only SVG icons registered in icons.js are allowed. Wails WebKitGTK does not render colour emoji.
This commit is contained in:
parent
c2e14cae69
commit
6d2f7858eb
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
// Import the VerstakPluginAPI contract
|
||||
import './VerstakPluginAPI.js';
|
||||
|
|
@ -168,7 +169,7 @@
|
|||
|
||||
{:else if loadState === 'error'}
|
||||
<div class="host-state error">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<p class="error-title">Plugin View Error</p>
|
||||
<div class="error-details">
|
||||
<p><strong>Plugin:</strong> {currentPluginId || 'unknown'}</p>
|
||||
|
|
@ -238,7 +239,7 @@
|
|||
color: #e94560;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
.host-state.error :global(.error-icon) {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
export let p = {};
|
||||
export let capabilities = [];
|
||||
export let permissions = [];
|
||||
|
|
@ -127,7 +128,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{#if missingRequired.length > 0}
|
||||
<p class="warning">⚠ Missing required capabilities: {missingRequired.join(', ')}</p>
|
||||
<p class="warning"><Icon name="warning" size={12} /> Missing required capabilities: {missingRequired.join(', ')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -145,7 +146,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{#if missingOptional.length > 0}
|
||||
<p class="info">ℹ Optional capabilities not available — plugin running in degraded mode</p>
|
||||
<p class="info"><Icon name="warning" size={12} /> Optional capabilities not available — plugin running in degraded mode</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -159,7 +160,7 @@
|
|||
{@const isDangerous = dangerousPermissions.includes(perm)}
|
||||
<span class="tag" class:dangerous={isDangerous}>
|
||||
{perm}
|
||||
{#if isDangerous}<span class="danger-icon">⚠</span>{/if}
|
||||
{#if isDangerous}<Icon name="warning" size={12} className="danger-icon" />{/if}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -175,7 +176,7 @@
|
|||
<div class="card-actions">
|
||||
{#if hasSettingsPanel}
|
||||
<button class="btn-settings" on:click={() => window.dispatchEvent(new CustomEvent('verstak:open-settings', { detail: { pluginId: m.id, panelId: settingsPanels[0]?.id } }))} type="button" disabled={isDisabled || p.status === 'failed'}>
|
||||
⚙ Settings
|
||||
<Icon name="gear" size={14} /> Settings
|
||||
</button>
|
||||
{/if}
|
||||
{#if vaultOpen && canToggle}
|
||||
|
|
@ -196,7 +197,7 @@
|
|||
|
||||
<!-- Permission warnings -->
|
||||
{#if !hasUIPermission && (m.contributes && (m.contributes.views || m.contributes.sidebarItems || m.contributes.settingsPanels).length > 0)}
|
||||
<p class="warning">⚠ Plugin has UI contributions but lacks ui.register permission</p>
|
||||
<p class="warning"><Icon name="warning" size={12} /> Plugin has UI contributions but lacks ui.register permission</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -341,7 +342,7 @@
|
|||
}
|
||||
|
||||
.check { color: #4ecca3; margin-left: 2px; }
|
||||
.danger-icon { color: #e94560; margin-left: 2px; }
|
||||
.danger-icon { color: #e94560; margin-left: 2px; vertical-align: middle; }
|
||||
|
||||
.info {
|
||||
color: #ffc857;
|
||||
|
|
@ -376,6 +377,9 @@
|
|||
}
|
||||
|
||||
.btn-settings {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background: #0f3460;
|
||||
border: 1px solid #1a3a5c;
|
||||
color: #e0e0f0;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
import PluginCard from './PluginCard.svelte';
|
||||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -151,7 +152,7 @@
|
|||
<div class="loading">Scanning plugin directories...</div>
|
||||
{:else if error}
|
||||
<div class="error">
|
||||
<div class="error-icon">⚠</div>
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<div class="error-message">{error}</div>
|
||||
<button class="retry-btn" on:click={loadAll} type="button">⟳ Retry</button>
|
||||
</div>
|
||||
|
|
@ -329,7 +330,7 @@
|
|||
padding: 2rem; text-align: center; color: #a0a0b8;
|
||||
}
|
||||
.error { color: #e94560; }
|
||||
.error-icon { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.error-icon { color: #e94560; margin-bottom: 0.5rem; }
|
||||
.error-message {
|
||||
font-family: monospace; font-size: 0.85rem; margin-bottom: 1rem; word-break: break-word;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import WorkspaceTree from './WorkspaceTree.svelte';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
let plugins = [];
|
||||
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
||||
|
|
@ -9,7 +10,7 @@
|
|||
let errorMessage = '';
|
||||
|
||||
let navItems = [
|
||||
{ id: 'plugin-manager', label: 'Plugin Manager', icon: '🧩' },
|
||||
{ id: 'plugin-manager', label: 'Plugin Manager', icon: 'puzzle' },
|
||||
];
|
||||
|
||||
$: vaultOpen = vaultStatus.status === 'open';
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">📦</span>
|
||||
<Icon name="logo" size={20} className="sidebar-logo" />
|
||||
<span class="sidebar-title">Verstak</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -61,7 +62,7 @@
|
|||
on:click={() => handleNav(item.id)}
|
||||
type="button"
|
||||
>
|
||||
<span class="nav-icon">{item.icon}</span>
|
||||
<Icon name={item.icon} size={16} className="nav-icon" />
|
||||
<span class="nav-label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -76,7 +77,7 @@
|
|||
on:click={() => handleSidebarItem(item)}
|
||||
type="button"
|
||||
>
|
||||
<span class="nav-icon">{item.icon || '📌'}</span>
|
||||
<Icon name={item.icon || 'plugin'} size={16} className="nav-icon icon-plugin" />
|
||||
<span class="nav-label">{item.title || item.id}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -89,7 +90,10 @@
|
|||
|
||||
<div class="sidebar-footer">
|
||||
{#if errorMessage}
|
||||
<span class="sidebar-error">⚠️ Plugin UI error</span>
|
||||
<span class="sidebar-error">
|
||||
<Icon name="warning" size={10} className="sidebar-error-icon" />
|
||||
Plugin UI error
|
||||
</span>
|
||||
{/if}
|
||||
{#if vaultStatus.status !== 'unknown'}
|
||||
<span class="vault-indicator" class:vault-open={vaultStatus.status === 'open'} class:vault-closed={vaultStatus.status !== 'open'}>
|
||||
|
|
@ -119,7 +123,10 @@
|
|||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 1.2rem;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: #4ecca3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
|
|
@ -181,10 +188,13 @@
|
|||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
width: 1.2rem;
|
||||
text-align: center;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
color: currentColor;
|
||||
}
|
||||
.nav-icon.icon-plugin {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
|
|
@ -213,9 +223,14 @@
|
|||
}
|
||||
|
||||
.sidebar-error {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
color: #e94560;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.sidebar-error-icon {
|
||||
color: #e94560;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
let appSettings = {};
|
||||
let recentVaults = [];
|
||||
|
|
@ -134,7 +135,7 @@
|
|||
|
||||
{#if error}
|
||||
<div class="error-box">
|
||||
<span class="error-icon">⚠</span>
|
||||
<Icon name="warning" size={14} className="error-icon" />
|
||||
<span class="error-text">{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -190,7 +191,7 @@
|
|||
{#each recentVaults as path}
|
||||
<li>
|
||||
<button class="recent-item" on:click={() => openRecent(path)} type="button" disabled={opening}>
|
||||
<span class="recent-icon">📁</span>
|
||||
<Icon name="vault" size={16} className="recent-icon" />
|
||||
<span class="recent-path">{path}</span>
|
||||
</button>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
export let activeView = null;
|
||||
export let activeViewPluginId = null;
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
<div class="view-container">
|
||||
<div class="error-boundary">
|
||||
<div class="error-fallback">
|
||||
<span class="error-icon">⚠</span>
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
<p class="error-title">Plugin UI failed</p>
|
||||
<p class="error-text">{renderError}</p>
|
||||
</div>
|
||||
|
|
@ -57,7 +58,7 @@
|
|||
<div class="view-container">
|
||||
<div class="view" class:degraded={pluginStatus === 'degraded'}>
|
||||
<div class="view-header">
|
||||
<span class="view-icon">{currentView.icon || '📦'}</span>
|
||||
<Icon name={currentView.icon || 'logo'} size={20} className="view-icon" />
|
||||
<h2>{currentView.title}</h2>
|
||||
{#if hasFrontend}
|
||||
<span class="frontend-badge">frontend bundle</span>
|
||||
|
|
@ -126,7 +127,12 @@
|
|||
color: #e0e0f0;
|
||||
flex: 1;
|
||||
}
|
||||
.view-icon { font-size: 1.3rem; }
|
||||
.view-icon {
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
color: #a78bfa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.frontend-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
|
|
@ -206,7 +212,6 @@
|
|||
padding: 2rem;
|
||||
}
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
color: #e94560;
|
||||
}
|
||||
.error-title {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
/**
|
||||
* Icon.svelte — Renders inline SVG icons.
|
||||
*
|
||||
* Usage:
|
||||
* <Icon name="puzzle" size={18} />
|
||||
* <Icon name="flask" class="my-icon" />
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
import { iconPaths } from './icons.js';
|
||||
|
||||
export let name = 'dot';
|
||||
export let size = 16;
|
||||
export let className = '';
|
||||
|
||||
$: icon = iconPaths[name] || iconPaths.dot;
|
||||
</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>
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* 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>`;
|
||||
}
|
||||
Loading…
Reference in New Issue