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:
parent
a6b0f9d7e6
commit
f022f46909
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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue