GUI: fix sidebar icons and create modal with template selection

- Add TemplateIcon.svelte with SVG icons for folder/project/client/document/recipe
- TreeNode.svelte: render type-based icon before each node label
- Create modal: show template selection cards (translate titles via t())
- Context menu: use TemplateIcon + translated template names
- i18n (ru/en): add template.* translation keys
- Fix white capsule issue: tree-toggle arrow at 10px rendered oddly
  without icon presence; replaced with proper SVG icons
This commit is contained in:
mirivlad 2026-06-03 02:29:11 +08:00
parent d285f9ad8b
commit b26b757d80
5 changed files with 114 additions and 5 deletions

View File

@ -4,6 +4,7 @@
import FilePreviewModal from './lib/FilePreviewModal.svelte'
import ConfirmModal from './lib/ConfirmModal.svelte'
import TreeNode from './TreeNode.svelte'
import TemplateIcon from './lib/TemplateIcon.svelte'
import { onMount, onDestroy } from 'svelte'
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
import { t } from './lib/i18n'
@ -1491,10 +1492,27 @@
{#if showCreateNode}
<div class="modal-overlay" on:click|self={cancelCreateNode}>
<div class="modal">
<h3>{createWithTemplate ? createWithTemplate.title : (createInNode ? t('nav.createInside') : t('case.new'))}</h3>
<h3>{createInNode ? t('nav.createInside') : t('case.new')}</h3>
{#if createInNode}
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
{/if}
<div class="form-group">
<label>{t('template.select')}</label>
<div class="template-cards">
<button class="template-card" class:selected={!createWithTemplate}
on:click={() => createWithTemplate = null}>
<TemplateIcon kind="folder" size={22} />
<span>{t('template.optionNone')}</span>
</button>
{#each enabledTemplates as tpl}
<button class="template-card" class:selected={createWithTemplate?.id === tpl.id}
on:click={() => createWithTemplate = tpl}>
<TemplateIcon kind={tpl.icon || 'generic'} size={22} />
<span>{t(tpl.title)}</span>
</button>
{/each}
</div>
</div>
<div class="form-group">
<label>{t('common.name')}</label>
<input type="text" placeholder={t('case.namePlaceholder')} bind:value={newNodeTitle}
@ -1513,9 +1531,10 @@
<div class="context-menu" style="left: {contextMenu.x}px; top: {contextMenu.y}px">
{#if contextMenu.node}
<div class="context-menu-section">{t('common.create')}</div>
{#each (enabledTemplates.length > 0 ? enabledTemplates : [{ id: '', title: t('template.optionNone'), icon: '' }]) as tpl}
{#each (enabledTemplates.length > 0 ? enabledTemplates : [{ id: '', title: 'template.optionNone', icon: 'folder' }]) as tpl}
<button class="context-menu-item" on:click={() => openCreateInNode(tpl)}>
{tpl.icon || '📄'} {tpl.title}
<TemplateIcon kind={tpl.icon || 'folder'} size={16} />
<span>{t(tpl.title)}</span>
</button>
{/each}
<div class="context-menu-divider"></div>
@ -1734,7 +1753,7 @@
.context-menu-backdrop { position: fixed; inset: 0; z-index: 200; }
.context-menu { position: fixed; background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 4px; min-width: 180px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
.context-menu-section { padding: 6px 12px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; }
.context-menu-item { display: block; width: 100%; padding: 6px 12px; border: none; background: none; color: #ccc; font-size: 13px; text-align: left; cursor: pointer; border-radius: 4px; font-family: inherit; }
.context-menu-item { display: flex; width: 100%; padding: 6px 12px; border: none; background: none; color: #ccc; font-size: 13px; text-align: left; cursor: pointer; border-radius: 4px; font-family: inherit; align-items: center; gap: 6px; }
.context-menu-item:hover { background: #222233; color: #fff; }
.context-menu-item.danger { color: #ff6b6b; }
.context-menu-item.danger:hover { background: #3a2222; color: #ff6b6b; }
@ -1881,6 +1900,12 @@
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
/* Template cards */
.template-cards { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; }
.template-card { display: flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; }
.template-card:hover { background: #1e1e30; border-color: #3a3a5c; }
.template-card.selected { background: #2a2a50; border-color: #6366f1; color: #e4e4ef; }
/* Today Dashboard */
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }

View File

@ -1,4 +1,6 @@
<script>
import TemplateIcon from './lib/TemplateIcon.svelte'
export let nodes = []
export let expanded = {}
export let selectedNodeId = ''
@ -6,6 +8,17 @@
export let onSelect
export let onToggle
export let onContextMenu
function iconKind(node) {
if (node.type === 'folder' && node.template_id) {
const m = { 'folder.default': 'folder', 'project.default': 'project', 'client.default': 'client', 'document.default': 'document', 'recipe.default': 'recipe' }
if (m[node.template_id]) return m[node.template_id]
}
if (node.type === 'project' || node.type === 'folder') return 'folder'
if (node.type === 'note') return 'note'
if (node.type === 'file') return 'file'
return 'generic'
}
</script>
{#each nodes as node}
@ -21,6 +34,7 @@
<span class="tree-spacer"></span>
{/if}
</button>
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
<span class="tree-label" on:click={() => onSelect && onSelect(node)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{node.title}
@ -31,3 +45,18 @@
{onSelect} {onToggle} {onContextMenu} />
{/if}
{/each}
<style>
.tree-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
flex-shrink: 0;
color: #888;
margin-right: 2px;
}
.tree-item.selected .tree-icon {
color: #a5b4fc;
}
</style>

View File

@ -0,0 +1,40 @@
<script>
export let kind = 'generic'
export let size = 18
</script>
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
{#if kind === 'folder'}
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
{:else if kind === 'project'}
<rect x="3" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="3" width="7" height="4" rx="1"/>
<rect x="14" y="10" width="7" height="11" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/>
{:else if kind === 'client'}
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
{:else if kind === 'document'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
{:else if kind === 'recipe'}
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
<line x1="8" y1="7" x2="16" y2="7"/>
<line x1="8" y1="11" x2="14" y2="11"/>
<line x1="8" y1="15" x2="12" y2="15"/>
{:else if kind === 'note'}
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
{:else if kind === 'file'}
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
<polyline points="13 2 13 9 20 9"/>
{:else}
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
{/if}
</svg>

View File

@ -80,6 +80,15 @@ export default {
'sync.notConnected': 'Not connected',
'sync.disabled': 'Disabled',
'template.optionNone': 'No template',
'template.optional': 'Template (optional)',
'template.folder': 'Folder',
'template.project': 'Project',
'template.client': 'Client',
'template.document': 'Document',
'template.recipe': 'Recipe',
'template.select': 'Select template',
'error.generic': 'An error occurred',
'error.invalidCredentials': 'Invalid username or password',
}

View File

@ -216,6 +216,12 @@ export default {
'template.optionNone': 'Без шаблона',
'template.optional': 'Шаблон (опционально)',
'template.folder': 'Папка',
'template.project': 'Проект',
'template.client': 'Клиент',
'template.document': 'Документ',
'template.recipe': 'Рецепт',
'template.select': 'Выберите шаблон',
'mime.jpeg': 'Изображение JPEG',
'mime.png': 'Изображение PNG',