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:
parent
d285f9ad8b
commit
b26b757d80
|
|
@ -4,6 +4,7 @@
|
||||||
import FilePreviewModal from './lib/FilePreviewModal.svelte'
|
import FilePreviewModal from './lib/FilePreviewModal.svelte'
|
||||||
import ConfirmModal from './lib/ConfirmModal.svelte'
|
import ConfirmModal from './lib/ConfirmModal.svelte'
|
||||||
import TreeNode from './TreeNode.svelte'
|
import TreeNode from './TreeNode.svelte'
|
||||||
|
import TemplateIcon from './lib/TemplateIcon.svelte'
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
||||||
import { t } from './lib/i18n'
|
import { t } from './lib/i18n'
|
||||||
|
|
@ -1491,10 +1492,27 @@
|
||||||
{#if showCreateNode}
|
{#if showCreateNode}
|
||||||
<div class="modal-overlay" on:click|self={cancelCreateNode}>
|
<div class="modal-overlay" on:click|self={cancelCreateNode}>
|
||||||
<div class="modal">
|
<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}
|
{#if createInNode}
|
||||||
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
|
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
|
||||||
{/if}
|
{/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">
|
<div class="form-group">
|
||||||
<label>{t('common.name')}</label>
|
<label>{t('common.name')}</label>
|
||||||
<input type="text" placeholder={t('case.namePlaceholder')} bind:value={newNodeTitle}
|
<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">
|
<div class="context-menu" style="left: {contextMenu.x}px; top: {contextMenu.y}px">
|
||||||
{#if contextMenu.node}
|
{#if contextMenu.node}
|
||||||
<div class="context-menu-section">{t('common.create')}</div>
|
<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)}>
|
<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>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="context-menu-divider"></div>
|
<div class="context-menu-divider"></div>
|
||||||
|
|
@ -1734,7 +1753,7 @@
|
||||||
.context-menu-backdrop { position: fixed; inset: 0; z-index: 200; }
|
.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 { 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-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:hover { background: #222233; color: #fff; }
|
||||||
.context-menu-item.danger { color: #ff6b6b; }
|
.context-menu-item.danger { color: #ff6b6b; }
|
||||||
.context-menu-item.danger:hover { background: #3a2222; 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; }
|
.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 */
|
||||||
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
|
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
|
||||||
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
|
import TemplateIcon from './lib/TemplateIcon.svelte'
|
||||||
|
|
||||||
export let nodes = []
|
export let nodes = []
|
||||||
export let expanded = {}
|
export let expanded = {}
|
||||||
export let selectedNodeId = ''
|
export let selectedNodeId = ''
|
||||||
|
|
@ -6,6 +8,17 @@
|
||||||
export let onSelect
|
export let onSelect
|
||||||
export let onToggle
|
export let onToggle
|
||||||
export let onContextMenu
|
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>
|
</script>
|
||||||
|
|
||||||
{#each nodes as node}
|
{#each nodes as node}
|
||||||
|
|
@ -21,6 +34,7 @@
|
||||||
<span class="tree-spacer"></span>
|
<span class="tree-spacer"></span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
|
||||||
<span class="tree-label" on:click={() => onSelect && onSelect(node)}
|
<span class="tree-label" on:click={() => onSelect && onSelect(node)}
|
||||||
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
||||||
{node.title}
|
{node.title}
|
||||||
|
|
@ -31,3 +45,18 @@
|
||||||
{onSelect} {onToggle} {onContextMenu} />
|
{onSelect} {onToggle} {onContextMenu} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -80,6 +80,15 @@ export default {
|
||||||
'sync.notConnected': 'Not connected',
|
'sync.notConnected': 'Not connected',
|
||||||
'sync.disabled': 'Disabled',
|
'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.generic': 'An error occurred',
|
||||||
'error.invalidCredentials': 'Invalid username or password',
|
'error.invalidCredentials': 'Invalid username or password',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,12 @@ export default {
|
||||||
|
|
||||||
'template.optionNone': 'Без шаблона',
|
'template.optionNone': 'Без шаблона',
|
||||||
'template.optional': 'Шаблон (опционально)',
|
'template.optional': 'Шаблон (опционально)',
|
||||||
|
'template.folder': 'Папка',
|
||||||
|
'template.project': 'Проект',
|
||||||
|
'template.client': 'Клиент',
|
||||||
|
'template.document': 'Документ',
|
||||||
|
'template.recipe': 'Рецепт',
|
||||||
|
'template.select': 'Выберите шаблон',
|
||||||
|
|
||||||
'mime.jpeg': 'Изображение JPEG',
|
'mime.jpeg': 'Изображение JPEG',
|
||||||
'mime.png': 'Изображение PNG',
|
'mime.png': 'Изображение PNG',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue