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;
}
</style>
<script type="module" crossorigin src="/assets/main-DqItQmr8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-SsPgaNKa.css">
<script type="module" crossorigin src="/assets/main-BLPyZvHh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-DxSaNTHX.css">
</head>
<body>
<div id="app"></div>

View File

@ -623,28 +623,31 @@
function openCreateRoot() {
createInNode = null
createWithTemplate = null
createWithTemplate = undefined
newNodeTitle = ''
showCreateNode = true
}
function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = null }
function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = undefined }
async function submitCreateNode() {
if (!newNodeTitle.trim()) return
if (!newNodeTitle.trim() || createWithTemplate === undefined) return
try {
const parentID = createInNode ? createInNode.id : ''
const templateID = createWithTemplate ? createWithTemplate.id : ''
await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID)
const created = await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID)
showCreateNode = false
newNodeTitle = ''
const createdId = created ? created.id : null
createInNode = null
createWithTemplate = null
createWithTemplate = undefined
workspaceTree = await wailsCall('ListWorkspaceTree') || workspaceTree
if (!parentID) {
// If root node created, select it
const updated = await wailsCall('ListWorkspaceTree') || workspaceTree
workspaceTree = updated
if (createdId) {
const node = await wailsCall('GetNodeDetail', createdId)
if (node) {
selectedSection = ''
selectNode(node)
}
}
} 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 }
}
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')
}
function pluralize(n, one, few, many) {
@ -1102,7 +1105,7 @@
<div class="header-left">
{#if selectedNode}
<span class="crumb">{selectedNode.title}</span>
<span class="crumb-type">{selectedNode.type}</span>
<span class="crumb-type">{nodeKindLabel(selectedNode.type)}</span>
{:else if selectedSection}
<span class="crumb">{#each systemViews as v}{v.id === selectedSection ? v.label : ''}{/each}</span>
{:else}
@ -1491,7 +1494,7 @@
{#if showCreateNode}
<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>
{#if createInNode}
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
@ -1499,16 +1502,22 @@
<div class="form-group">
<label>{t('template.select')}</label>
<div class="template-cards">
<button class="template-card" class:selected={!createWithTemplate}
<button class="template-card" class:selected={createWithTemplate === null}
on:click={() => createWithTemplate = null}>
<TemplateIcon kind="folder" size={22} />
<span>{t('template.optionNone')}</span>
<TemplateIcon kind="folder" size={24} />
<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>
{#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>
<TemplateIcon kind={tpl.icon || 'generic'} size={24} />
<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>
{/each}
</div>
@ -1519,7 +1528,8 @@
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus />
</div>
<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>
</div>
</div>
@ -1901,10 +1911,13 @@
.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-cards { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.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.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 { padding: 24px; overflow-y: auto; flex: 1; }

View File

@ -10,53 +10,108 @@
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 === 'client' || node.template_id === 'client.default') return 'client'
if (node.type === 'project' || node.template_id === 'project.default') return 'project'
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 === 'folder' || node.template_id === 'folder.default') return 'folder'
if (node.type === 'note') return 'note'
if (node.type === 'file') return 'file'
return 'generic'
}
function hasChildren(node) {
return node.children && node.children.length > 0
}
</script>
{#each nodes as node}
<div class="tree-item"
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)}>
<button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{#if node.children && node.children.length > 0}
<span class="tree-arrow">{expanded[node.id] ? '▼' : '▶'}</span>
{:else}
<span class="tree-spacer"></span>
{/if}
</button>
{#if hasChildren(node)}
<button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
<span class="tree-arrow">{expanded[node.id] ? '▾' : '▸'}</span>
</button>
{:else}
<span class="tree-toggle-placeholder"></span>
{/if}
<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}
</span>
</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}
{onSelect} {onToggle} {onContextMenu} />
{/if}
{/each}
<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 {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 32px;
flex-shrink: 0;
color: #888;
margin-right: 2px;
margin-right: 4px;
}
.tree-item.selected .tree-icon {
color: #a5b4fc;
}
.tree-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
line-height: 32px;
}
</style>

View File

@ -7,11 +7,16 @@ export default {
'nav.recipes': 'Recipes',
'nav.documents': 'Documents',
'nav.archive': 'Archive',
'nav.sections': 'Sections',
'nav.cases': 'Cases',
'nav.noCases': 'No cases',
'nav.system': 'System',
'nav.workspace': 'Workspace',
'nav.noNodes': 'No nodes',
'nav.openFolder': 'Open folder',
'nav.createInside': 'Create inside',
'nav.selectPrompt': 'Select a section or case',
'nav.brand': 'Verstak',
'tab.overview': 'Overview',
'tab.notes': 'Notes',
@ -80,14 +85,35 @@ export default {
'sync.notConnected': 'Not connected',
'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.none.desc': 'No template, simple container node',
'template.folder': 'Folder',
'template.folder.desc': 'A folder to group items inside a workspace',
'template.project': 'Project',
'template.project.desc': 'A distinct project or task with files, notes and work log',
'template.client': 'Client',
'template.client.desc': 'An organization or person for whom work is performed',
'template.document': 'Document',
'template.document.desc': 'A document with description, notes and files',
'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.invalidCredentials': 'Invalid username or password',

View File

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