gui: fix sidebar icons, create modal, and type display

- TreeNode.svelte: no white spacer for leaf nodes, iconKind maps node.type
  directly, 32px rows with hover/selected states
- App.svelte: header shows localized nodeKindLabel, auto-select created
  node, create modal with Пустое дело card + template descriptions,
  disable button until name+type chosen
- i18n: add kind.folder/note/file, template descriptions,
  template.optionNone → Пустое дело / Empty case
This commit is contained in:
mirivlad 2026-06-03 02:58:27 +08:00
parent a6b0f9d7e6
commit f022f46909
9 changed files with 151 additions and 46 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,8 +16,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-DqItQmr8.js"></script> <script type="module" crossorigin src="/assets/main-BLPyZvHh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-SsPgaNKa.css"> <link rel="stylesheet" crossorigin href="/assets/main-DxSaNTHX.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -623,28 +623,31 @@
function openCreateRoot() { function openCreateRoot() {
createInNode = null createInNode = null
createWithTemplate = null createWithTemplate = undefined
newNodeTitle = '' newNodeTitle = ''
showCreateNode = true showCreateNode = true
} }
function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = null } function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = undefined }
async function submitCreateNode() { async function submitCreateNode() {
if (!newNodeTitle.trim()) return if (!newNodeTitle.trim() || createWithTemplate === undefined) return
try { try {
const parentID = createInNode ? createInNode.id : '' const parentID = createInNode ? createInNode.id : ''
const templateID = createWithTemplate ? createWithTemplate.id : '' const templateID = createWithTemplate ? createWithTemplate.id : ''
await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID) const created = await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID)
showCreateNode = false showCreateNode = false
newNodeTitle = '' newNodeTitle = ''
const createdId = created ? created.id : null
createInNode = null createInNode = null
createWithTemplate = null createWithTemplate = undefined
workspaceTree = await wailsCall('ListWorkspaceTree') || workspaceTree workspaceTree = await wailsCall('ListWorkspaceTree') || workspaceTree
if (!parentID) { if (createdId) {
// If root node created, select it const node = await wailsCall('GetNodeDetail', createdId)
const updated = await wailsCall('ListWorkspaceTree') || workspaceTree if (node) {
workspaceTree = updated selectedSection = ''
selectNode(node)
}
} }
} catch (e) { error = String(e) } } catch (e) { error = String(e) }
} }
@ -906,7 +909,7 @@
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str } try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
} }
function nodeKindLabel(kind) { function nodeKindLabel(kind) {
const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'archive': t('kind.archive'), 'case': t('kind.case') } const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'folder': t('kind.folder'), 'note': t('kind.note'), 'file': t('kind.file'), 'archive': t('kind.archive'), 'case': t('kind.case') }
return labels[kind] || kind || t('kind.case') return labels[kind] || kind || t('kind.case')
} }
function pluralize(n, one, few, many) { function pluralize(n, one, few, many) {
@ -1102,7 +1105,7 @@
<div class="header-left"> <div class="header-left">
{#if selectedNode} {#if selectedNode}
<span class="crumb">{selectedNode.title}</span> <span class="crumb">{selectedNode.title}</span>
<span class="crumb-type">{selectedNode.type}</span> <span class="crumb-type">{nodeKindLabel(selectedNode.type)}</span>
{:else if selectedSection} {:else if selectedSection}
<span class="crumb">{#each systemViews as v}{v.id === selectedSection ? v.label : ''}{/each}</span> <span class="crumb">{#each systemViews as v}{v.id === selectedSection ? v.label : ''}{/each}</span>
{:else} {:else}
@ -1491,7 +1494,7 @@
{#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 modal-create">
<h3>{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>
@ -1499,16 +1502,22 @@
<div class="form-group"> <div class="form-group">
<label>{t('template.select')}</label> <label>{t('template.select')}</label>
<div class="template-cards"> <div class="template-cards">
<button class="template-card" class:selected={!createWithTemplate} <button class="template-card" class:selected={createWithTemplate === null}
on:click={() => createWithTemplate = null}> on:click={() => createWithTemplate = null}>
<TemplateIcon kind="folder" size={22} /> <TemplateIcon kind="folder" size={24} />
<span>{t('template.optionNone')}</span> <div class="template-card-text">
<span class="template-card-title">{t('template.optionNone')}</span>
<span class="template-card-desc">{t('template.none.desc')}</span>
</div>
</button> </button>
{#each enabledTemplates as tpl} {#each enabledTemplates as tpl}
<button class="template-card" class:selected={createWithTemplate?.id === tpl.id} <button class="template-card" class:selected={createWithTemplate?.id === tpl.id}
on:click={() => createWithTemplate = tpl}> on:click={() => createWithTemplate = tpl}>
<TemplateIcon kind={tpl.icon || 'generic'} size={22} /> <TemplateIcon kind={tpl.icon || 'generic'} size={24} />
<span>{t(tpl.title)}</span> <div class="template-card-text">
<span class="template-card-title">{t(tpl.title)}</span>
<span class="template-card-desc">{t(tpl.title + '.desc')}</span>
</div>
</button> </button>
{/each} {/each}
</div> </div>
@ -1519,7 +1528,8 @@
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus /> on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus />
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-primary" on:click={submitCreateNode}>{t('common.create')}</button> <button class="btn btn-primary" on:click={submitCreateNode}
disabled={!newNodeTitle.trim() || createWithTemplate === undefined}>{t('common.create')}</button>
<button class="btn" on:click={cancelCreateNode}>{t('common.cancel')}</button> <button class="btn" on:click={cancelCreateNode}>{t('common.cancel')}</button>
</div> </div>
</div> </div>
@ -1901,10 +1911,13 @@
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; } .rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
/* Template cards */ /* Template cards */
.template-cards { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; } .template-cards { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.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 { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid #2a2a3c; background: #13131f; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; width: 100%; text-align: left; }
.template-card:hover { background: #1e1e30; border-color: #3a3a5c; } .template-card:hover { background: #1e1e30; border-color: #3a3a5c; }
.template-card.selected { background: #2a2a50; border-color: #6366f1; color: #e4e4ef; } .template-card.selected { background: #2a2a50; border-color: #6366f1; color: #e4e4ef; }
.template-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.template-card-title { font-weight: 500; }
.template-card-desc { font-size: 11px; color: #888; }
/* Today Dashboard */ /* Today Dashboard */
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; } .today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }

View File

@ -10,53 +10,108 @@
export let onContextMenu export let onContextMenu
function iconKind(node) { function iconKind(node) {
if (node.type === 'folder' && node.template_id) { if (node.type === 'client' || node.template_id === 'client.default') return 'client'
const m = { 'folder.default': 'folder', 'project.default': 'project', 'client.default': 'client', 'document.default': 'document', 'recipe.default': 'recipe' } if (node.type === 'project' || node.template_id === 'project.default') return 'project'
if (m[node.template_id]) return m[node.template_id] if (node.type === 'document' || node.template_id === 'document.default') return 'document'
} if (node.type === 'recipe' || node.template_id === 'recipe.default') return 'recipe'
if (node.type === 'project' || node.type === 'folder') return 'folder' if (node.type === 'folder' || node.template_id === 'folder.default') return 'folder'
if (node.type === 'note') return 'note' if (node.type === 'note') return 'note'
if (node.type === 'file') return 'file' if (node.type === 'file') return 'file'
return 'generic' return 'generic'
} }
function hasChildren(node) {
return node.children && node.children.length > 0
}
</script> </script>
{#each nodes as node} {#each nodes as node}
<div class="tree-item" <div class="tree-item"
class:selected={selectedNodeId === node.id} class:selected={selectedNodeId === node.id}
style="padding-left: {level * 16 + 8}px" style="padding-left: {level * 16 + 4}px"
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}> on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
{#if hasChildren(node)}
<button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)} <button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}> on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{#if node.children && node.children.length > 0} <span class="tree-arrow">{expanded[node.id] ? '▾' : '▸'}</span>
<span class="tree-arrow">{expanded[node.id] ? '▼' : '▶'}</span>
{:else}
<span class="tree-spacer"></span>
{/if}
</button> </button>
{:else}
<span class="tree-toggle-placeholder"></span>
{/if}
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span> <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}
</span> </span>
</div> </div>
{#if expanded[node.id] && node.children && node.children.length > 0} {#if expanded[node.id] && hasChildren(node)}
<svelte:self nodes={node.children} {expanded} {selectedNodeId} level={level + 1} <svelte:self nodes={node.children} {expanded} {selectedNodeId} level={level + 1}
{onSelect} {onToggle} {onContextMenu} /> {onSelect} {onToggle} {onContextMenu} />
{/if} {/if}
{/each} {/each}
<style> <style>
.tree-item {
display: flex;
align-items: center;
height: 32px;
padding-right: 8px;
cursor: default;
font-size: 13px;
color: #ccc;
user-select: none;
}
.tree-item:hover {
background: #222233;
}
.tree-item.selected {
background: #2a2a4a;
color: #fff;
font-weight: 500;
}
.tree-toggle {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 0;
width: 20px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-family: inherit;
line-height: 1;
font-size: 12px;
}
.tree-toggle:hover {
color: #888;
}
.tree-toggle-placeholder {
display: inline-block;
width: 20px;
flex-shrink: 0;
}
.tree-icon { .tree-icon {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 20px; width: 20px;
height: 32px;
flex-shrink: 0; flex-shrink: 0;
color: #888; color: #888;
margin-right: 2px; margin-right: 4px;
} }
.tree-item.selected .tree-icon { .tree-item.selected .tree-icon {
color: #a5b4fc; color: #a5b4fc;
} }
.tree-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
line-height: 32px;
}
</style> </style>

View File

@ -7,11 +7,16 @@ export default {
'nav.recipes': 'Recipes', 'nav.recipes': 'Recipes',
'nav.documents': 'Documents', 'nav.documents': 'Documents',
'nav.archive': 'Archive', 'nav.archive': 'Archive',
'nav.sections': 'Sections',
'nav.cases': 'Cases',
'nav.noCases': 'No cases',
'nav.system': 'System', 'nav.system': 'System',
'nav.workspace': 'Workspace', 'nav.workspace': 'Workspace',
'nav.noNodes': 'No nodes', 'nav.noNodes': 'No nodes',
'nav.openFolder': 'Open folder', 'nav.openFolder': 'Open folder',
'nav.createInside': 'Create inside', 'nav.createInside': 'Create inside',
'nav.selectPrompt': 'Select a section or case',
'nav.brand': 'Verstak',
'tab.overview': 'Overview', 'tab.overview': 'Overview',
'tab.notes': 'Notes', 'tab.notes': 'Notes',
@ -80,14 +85,35 @@ export default {
'sync.notConnected': 'Not connected', 'sync.notConnected': 'Not connected',
'sync.disabled': 'Disabled', 'sync.disabled': 'Disabled',
'template.optionNone': 'No template', 'kind.project': 'Project',
'kind.client': 'Client',
'kind.document': 'Document',
'kind.recipe': 'Recipe',
'kind.folder': 'Folder',
'kind.note': 'Note',
'kind.file': 'File',
'kind.archive': 'Archive',
'kind.case': 'Case',
'template.optionNone': 'Empty case',
'template.optional': 'Template (optional)', 'template.optional': 'Template (optional)',
'template.none.desc': 'No template, simple container node',
'template.folder': 'Folder', 'template.folder': 'Folder',
'template.folder.desc': 'A folder to group items inside a workspace',
'template.project': 'Project', 'template.project': 'Project',
'template.project.desc': 'A distinct project or task with files, notes and work log',
'template.client': 'Client', 'template.client': 'Client',
'template.client.desc': 'An organization or person for whom work is performed',
'template.document': 'Document', 'template.document': 'Document',
'template.document.desc': 'A document with description, notes and files',
'template.recipe': 'Recipe', 'template.recipe': 'Recipe',
'template.select': 'Select template', 'template.recipe.desc': 'A repeatable procedure or instruction',
'template.note': 'Note',
'template.file': 'File',
'template.select': 'Select type',
'case.new': 'New case',
'case.namePlaceholder': 'Case name',
'error.generic': 'An error occurred', 'error.generic': 'An error occurred',
'error.invalidCredentials': 'Invalid username or password', 'error.invalidCredentials': 'Invalid username or password',

View File

@ -79,6 +79,9 @@ export default {
'kind.client': 'Клиент', 'kind.client': 'Клиент',
'kind.document': 'Документ', 'kind.document': 'Документ',
'kind.recipe': 'Рецепт', 'kind.recipe': 'Рецепт',
'kind.folder': 'Папка',
'kind.note': 'Заметка',
'kind.file': 'Файл',
'kind.archive': 'Архив', 'kind.archive': 'Архив',
'kind.case': 'Дело', 'kind.case': 'Дело',
@ -214,14 +217,22 @@ export default {
'delete.folder': 'папку', 'delete.folder': 'папку',
'delete.file': 'файл', 'delete.file': 'файл',
'template.optionNone': 'Без шаблона', 'template.optionNone': 'Пустое дело',
'template.optional': 'Шаблон (опционально)', 'template.optional': 'Шаблон (опционально)',
'template.none.desc': 'Без шаблона, простой узел-контейнер',
'template.folder': 'Папка', 'template.folder': 'Папка',
'template.folder.desc': 'Папка для группировки элементов внутри рабочего пространства',
'template.project': 'Проект', 'template.project': 'Проект',
'template.project.desc': 'Отдельный проект или задача с файлами, заметками и журналом',
'template.client': 'Клиент', 'template.client': 'Клиент',
'template.client.desc': 'Организация или человек, для которых ведутся работы',
'template.document': 'Документ', 'template.document': 'Документ',
'template.document.desc': 'Документ с описанием, заметками и файлами',
'template.recipe': 'Рецепт', 'template.recipe': 'Рецепт',
'template.select': 'Выберите шаблон', 'template.recipe.desc': 'Повторяемая процедура или инструкция',
'template.note': 'Заметка',
'template.file': 'Файл',
'template.select': 'Выберите тип',
'mime.jpeg': 'Изображение JPEG', 'mime.jpeg': 'Изображение JPEG',
'mime.png': 'Изображение PNG', 'mime.png': 'Изображение PNG',