feat: restore global search in app header
This commit is contained in:
+94
-45
@@ -10,6 +10,7 @@
|
||||
import SettingsWindow from './lib/SettingsWindow.svelte'
|
||||
import SyncStatus from './lib/SyncStatus.svelte'
|
||||
import TodayScreen from './lib/TodayScreen.svelte'
|
||||
import AppHeader from './lib/AppHeader.svelte'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { actionIcon } from './lib/actionIcons.js'
|
||||
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
||||
@@ -207,6 +208,12 @@
|
||||
let unlistenDrop = null
|
||||
|
||||
$: visibleTrashNodes = computeVisibleTrashNodes(trashInfo?.nodes || [], trashFolderId)
|
||||
$: currentViewTitle = selectedNode
|
||||
? selectedNode.title
|
||||
: selectedSection
|
||||
? (systemViews.find(view => view.id === selectedSection)?.label || '')
|
||||
: t('nav.selectPrompt')
|
||||
$: currentViewSubtitle = selectedNode ? nodeKindLabel(selectedNode.type) : ''
|
||||
|
||||
$: sortedInboxNodes = sortInboxNodes(inboxNodes, inboxSortBy, inboxSortDir)
|
||||
$: groupedInboxNodes = inboxGroupByContext ? groupInboxByContext(sortedInboxNodes) : null
|
||||
@@ -1805,6 +1812,10 @@
|
||||
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'), 'link': t('kind.link') }
|
||||
return labels[kind] || kind || t('kind.case')
|
||||
}
|
||||
function searchResultTypeLabel(kind) {
|
||||
if (kind === 'action') return t('kind.action')
|
||||
return nodeKindLabel(kind)
|
||||
}
|
||||
function captureKindLabel(kind) {
|
||||
if (!kind) return ''
|
||||
return t('capture.kind.' + kind)
|
||||
@@ -2384,6 +2395,65 @@
|
||||
return openActivityTarget(ev)
|
||||
}
|
||||
|
||||
async function openSearchResult(result) {
|
||||
if (!result || !result.nodeId) return
|
||||
try {
|
||||
if (result.type === 'link') {
|
||||
const node = await wailsCall('GetNodeDetail', result.nodeId)
|
||||
if (node) {
|
||||
await selectNode(node)
|
||||
setActiveTab('links')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (result.type === 'action') {
|
||||
const node = await wailsCall('GetNodeDetail', result.nodeId)
|
||||
if (node) {
|
||||
await selectNode(node)
|
||||
setActiveTab('actions')
|
||||
}
|
||||
return
|
||||
}
|
||||
const detail = await wailsCall('GetNodeDetail', result.nodeId)
|
||||
if (!detail) return
|
||||
if (result.type === 'note') {
|
||||
const parentId = detail.parent_id || detail.parentId || ''
|
||||
const parent = parentId ? await wailsCall('GetNodeDetail', parentId) : null
|
||||
if (parent) {
|
||||
await selectNode(parent)
|
||||
setActiveTab('notes')
|
||||
notes = await wailsCall('ListNotes', parent.id) || []
|
||||
const note = notes.find(item => item.id === detail.id)
|
||||
if (note) await openNote(note)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (result.type === 'file') {
|
||||
const parentId = detail.parent_id || detail.parentId || ''
|
||||
const parent = parentId ? await wailsCall('GetNodeDetail', parentId) : null
|
||||
if (parent) {
|
||||
await selectNode(parent)
|
||||
setActiveTab('files')
|
||||
await loadFolder(parent.id)
|
||||
const fileItem = fileItems.find(item => item.id === detail.id)
|
||||
if (fileItem && canPreviewFile(fileItem)) {
|
||||
await openPreview(fileItem)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (result.type === 'folder') {
|
||||
await selectNode(detail)
|
||||
setActiveTab('files')
|
||||
await loadFolder(detail.id)
|
||||
return
|
||||
}
|
||||
await selectNode(detail)
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Sync =====
|
||||
async function loadSyncStatus() {
|
||||
try {
|
||||
@@ -2537,20 +2607,31 @@
|
||||
|
||||
<!-- Main -->
|
||||
<main class="main">
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
{#if selectedNode}
|
||||
<span class="crumb">{selectedNode.title}</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}
|
||||
<span class="crumb placeholder">{t('nav.selectPrompt')}</span>
|
||||
<AppHeader
|
||||
title={currentViewTitle}
|
||||
subtitle={currentViewSubtitle}
|
||||
{wailsCall}
|
||||
typeLabel={searchResultTypeLabel}
|
||||
onOpenSearchResult={openSearchResult}
|
||||
>
|
||||
<div slot="actions" class="header-actions">
|
||||
{#if selectedSection === 'inbox'}
|
||||
<button class="btn btn-primary" on:click={captureClipboard} disabled={inboxCaptureBusy}>
|
||||
{inboxCaptureBusy ? t('common.loading') : t('inbox.pasteClipboard')}
|
||||
</button>
|
||||
{#if inboxCaptureStatus}
|
||||
<span class="inbox-capture-status">{inboxCaptureStatus}</span>
|
||||
{/if}
|
||||
{:else if selectedSection === 'trash'}
|
||||
{#if trashSelectedIds.length > 0}
|
||||
<button class="btn btn-sm btn-primary" on:click={() => restoreTrash(trashSelectedIds)}>{@html actionIcon('restore', 14)} {t('trash.restore')} ({trashSelectedIds.length})</button>
|
||||
<button class="btn btn-sm btn-danger" on:click={() => purgeTrash(trashSelectedIds)}>{@html actionIcon('purge', 14)} {t('common.delete')} ({trashSelectedIds.length})</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-danger" on:click={emptyTrash}>{@html actionIcon('emptyTrash', 14)} {t('trash.emptyTrash')}</button>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenTrashFolder')}>{t('trash.openFolder')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
</div>
|
||||
</header>
|
||||
</AppHeader>
|
||||
|
||||
{#if error}
|
||||
<div class="error-banner" role="button" tabindex="0" on:click={() => error = ''} on:keydown={onKeyActivate(() => error = '')}>
|
||||
@@ -2985,17 +3066,8 @@
|
||||
on:drop={handleInboxDrop}>
|
||||
<div class="inbox-header">
|
||||
<div>
|
||||
<h2>{t('nav.inbox')}</h2>
|
||||
<p>{t('inbox.subtitle')}</p>
|
||||
</div>
|
||||
<div class="inbox-header-actions">
|
||||
<button class="btn btn-primary" on:click={captureClipboard} disabled={inboxCaptureBusy}>
|
||||
{inboxCaptureBusy ? t('common.loading') : t('inbox.pasteClipboard')}
|
||||
</button>
|
||||
{#if inboxCaptureStatus}
|
||||
<span class="inbox-capture-status">{inboxCaptureStatus}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if inboxNodes.length > 0}
|
||||
@@ -3102,17 +3174,8 @@
|
||||
<div class="trash-screen">
|
||||
<div class="trash-header">
|
||||
<div>
|
||||
<h2>{t('nav.trash')}</h2>
|
||||
<p>{currentTrashPath}</p>
|
||||
</div>
|
||||
<div class="trash-actions">
|
||||
{#if trashSelectedIds.length > 0}
|
||||
<button class="btn btn-sm btn-primary" on:click={() => restoreTrash(trashSelectedIds)}>{@html actionIcon('restore', 14)} {t('trash.restore')} ({trashSelectedIds.length})</button>
|
||||
<button class="btn btn-sm btn-danger" on:click={() => purgeTrash(trashSelectedIds)}>{@html actionIcon('purge', 14)} {t('common.delete')} ({trashSelectedIds.length})</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-danger" on:click={emptyTrash}>{@html actionIcon('emptyTrash', 14)} {t('trash.emptyTrash')}</button>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenTrashFolder')}>{t('trash.openFolder')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !trashInfo || (trashInfo.nodes || []).length === 0}
|
||||
<div class="empty-state">
|
||||
@@ -3434,9 +3497,6 @@
|
||||
|
||||
{:else if selectedSection === 'activity'}
|
||||
<div class="activity-feed">
|
||||
<div class="activity-feed-header">
|
||||
<h2>{t('activity.title')}</h2>
|
||||
</div>
|
||||
{#if activityFeed.length === 0}
|
||||
<div class="empty-state"><p>{t('activity.empty')}</p></div>
|
||||
{:else}
|
||||
@@ -3803,16 +3863,11 @@
|
||||
|
||||
/* Main */
|
||||
.main { flex: 1; display: flex; flex-direction: column; height: 100vh; min-width: 0; overflow: hidden; background: #13131f; }
|
||||
.header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; flex-shrink: 0; min-height: 48px; }
|
||||
.header-left { display: flex; align-items: center; gap: 8px; flex: 1; }
|
||||
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.sidebar-settings-btn { background: transparent; border: none; border-radius: 6px; padding: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #666; font-family: inherit; width: 32px; height: 32px; }
|
||||
.sidebar-settings-btn:hover { background: #1e1e38; color: #a5b4fc; }
|
||||
.sidebar-settings-btn:active { background: #252545; color: #818cf8; }
|
||||
|
||||
.crumb { font-size: 14px; font-weight: 500; }
|
||||
.crumb.placeholder { color: #666; }
|
||||
.crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
||||
.error-banner { background: #3a2222; color: #ff8888; padding: 8px 24px; font-size: 12px; border-bottom: 1px solid #4a2222; flex-shrink: 0; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
|
||||
.dismiss-btn { background: none; border: none; color: #ff6666; cursor: pointer; padding: 2px; display: flex; align-items: center; border-radius: 2px; }
|
||||
.dismiss-btn:hover { color: #ff4444; }
|
||||
@@ -3900,9 +3955,7 @@
|
||||
.inbox-screen { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.inbox-screen.drop-valid { outline: 2px dashed #4ade80; outline-offset: -4px; background: rgba(74, 222, 128, 0.04); }
|
||||
.inbox-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.inbox-header h2 { margin: 0 0 6px; }
|
||||
.inbox-header p { margin: 0; color: #a0a0b8; font-size: 13px; }
|
||||
.inbox-header-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.inbox-capture-status { color: #8ee6b1; font-size: 12px; }
|
||||
.inbox-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.inbox-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #2a2a3c; border-radius: 8px; background: #1a1a28; cursor: pointer; }
|
||||
@@ -3948,9 +4001,7 @@
|
||||
/* Trash screen */
|
||||
.trash-screen { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.trash-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.trash-header h2 { margin: 0 0 6px; }
|
||||
.trash-header p { margin: 0; color: #8888a0; font-size: 12px; }
|
||||
.trash-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.trash-browser { max-width: 980px; }
|
||||
.trash-section { min-width: 0; }
|
||||
.trash-section h3 { margin: 0 0 10px; font-size: 13px; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
@@ -4169,8 +4220,6 @@
|
||||
|
||||
/* Activity feed (global section) */
|
||||
.activity-feed { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.activity-feed-header { margin-bottom: 20px; }
|
||||
.activity-feed-header h2 { font-size: 24px; }
|
||||
.activity-feed-events { display: flex; flex-direction: column; gap: 2px; }
|
||||
.activity-feed-event { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
|
||||
.activity-feed-event:hover { background: #1a1a28; color: #e4e4ef; }
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import GlobalSearch from './GlobalSearch.svelte'
|
||||
|
||||
export let title = ''
|
||||
export let subtitle = ''
|
||||
export let wailsCall = async () => []
|
||||
export let typeLabel = (type) => type || ''
|
||||
export let onOpenSearchResult = () => {}
|
||||
</script>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="header-title" class:placeholder={!title}>{title || ''}</span>
|
||||
{#if subtitle}
|
||||
<span class="header-subtitle">{subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<GlobalSearch {wailsCall} {typeLabel} on:open={(e) => onOpenSearchResult(e.detail)} />
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header { padding: 10px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; gap: 16px; flex-shrink: 0; min-height: 52px; }
|
||||
.header-left { display: flex; align-items: center; gap: 8px; min-width: 120px; flex: 0 1 auto; overflow: hidden; }
|
||||
.header-title { font-size: 15px; font-weight: 600; color: #e4e4ef; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.header-title.placeholder { color: #666; font-weight: 500; }
|
||||
.header-subtitle { font-size: 11px; color: #8888a0; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; white-space: nowrap; }
|
||||
.header-right { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
||||
@media (max-width: 760px) {
|
||||
.header { padding: 8px 12px; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
|
||||
.header-left { flex: 1 1 100%; }
|
||||
.header-right { flex: 1 1 100%; justify-content: stretch; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte'
|
||||
import { t } from './i18n'
|
||||
|
||||
export let wailsCall = async () => []
|
||||
export let typeLabel = (type) => type || ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let query = ''
|
||||
let results = []
|
||||
let open = false
|
||||
let loading = false
|
||||
let selectedIndex = 0
|
||||
let inputEl
|
||||
let searchTimer
|
||||
|
||||
function clean(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function resultPath(result) {
|
||||
return clean(result.path)
|
||||
}
|
||||
|
||||
function resultMeta(result) {
|
||||
const parts = []
|
||||
const type = typeLabel(result.type)
|
||||
const path = resultPath(result)
|
||||
if (type) parts.push(type)
|
||||
if (path) parts.push(path)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
async function runSearch() {
|
||||
const q = clean(query)
|
||||
if (q.length < 2) {
|
||||
results = []
|
||||
open = false
|
||||
loading = false
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
try {
|
||||
results = await wailsCall('Search', q) || []
|
||||
selectedIndex = 0
|
||||
open = true
|
||||
} catch (e) {
|
||||
results = []
|
||||
open = false
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSearch() {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(runSearch, 180)
|
||||
}
|
||||
|
||||
function closeResults() {
|
||||
open = false
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
query = ''
|
||||
results = []
|
||||
closeResults()
|
||||
}
|
||||
|
||||
function openResult(result) {
|
||||
if (!result) return
|
||||
dispatch('open', result)
|
||||
clearSearch()
|
||||
inputEl?.blur()
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
scheduleSearch()
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
clearSearch()
|
||||
inputEl?.blur()
|
||||
return
|
||||
}
|
||||
if (!open || results.length === 0) return
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
selectedIndex = Math.min(results.length - 1, selectedIndex + 1)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
selectedIndex = Math.max(0, selectedIndex - 1)
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
openResult(results[selectedIndex])
|
||||
}
|
||||
}
|
||||
|
||||
async function focusSearch() {
|
||||
await tick()
|
||||
inputEl?.focus()
|
||||
inputEl?.select()
|
||||
}
|
||||
|
||||
function handleWindowKeydown(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault()
|
||||
focusSearch()
|
||||
return
|
||||
}
|
||||
if (e.key === '/' && !isEditableTarget(e.target)) {
|
||||
e.preventDefault()
|
||||
focusSearch()
|
||||
}
|
||||
}
|
||||
|
||||
function isEditableTarget(target) {
|
||||
if (!target || !(target instanceof Element)) return false
|
||||
return !!target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleWindowKeydown)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleWindowKeydown)
|
||||
clearTimeout(searchTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="global-search">
|
||||
<input
|
||||
class="global-search-input"
|
||||
bind:this={inputEl}
|
||||
bind:value={query}
|
||||
type="search"
|
||||
placeholder={t('search.placeholder')}
|
||||
aria-label={t('search.placeholder')}
|
||||
autocomplete="off"
|
||||
on:input={handleInput}
|
||||
on:focus={() => { if (results.length > 0) open = true }}
|
||||
on:keydown={handleKeydown}
|
||||
on:blur={() => setTimeout(closeResults, 160)}
|
||||
/>
|
||||
{#if open}
|
||||
<div class="global-search-results">
|
||||
{#if loading}
|
||||
<div class="global-search-empty">{t('common.loading')}</div>
|
||||
{:else if results.length === 0}
|
||||
<div class="global-search-empty">{t('search.noResults')}</div>
|
||||
{:else}
|
||||
{#each results as result, index}
|
||||
<button
|
||||
class="global-search-result"
|
||||
class:selected={index === selectedIndex}
|
||||
on:mousedown|preventDefault={() => openResult(result)}
|
||||
>
|
||||
<span class="global-search-title">{result.title}</span>
|
||||
{#if resultMeta(result)}
|
||||
<span class="global-search-meta">{resultMeta(result)}</span>
|
||||
{/if}
|
||||
{#if result.snippet}
|
||||
<span class="global-search-snippet">{result.snippet}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.global-search { position: relative; flex: 1 1 360px; min-width: 180px; max-width: 520px; }
|
||||
.global-search-input { width: 100%; height: 32px; padding: 0 12px; border: 1px solid #2a2a3c; border-radius: 6px; background: #10101a; color: #e4e4ef; font: inherit; font-size: 13px; }
|
||||
.global-search-input:focus { outline: none; border-color: #6366f1; box-shadow: 0 0 0 2px rgba(99,102,241,0.18); }
|
||||
.global-search-results { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 160; max-height: min(360px, calc(100vh - 88px)); overflow-y: auto; background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; box-shadow: 0 14px 36px rgba(0,0,0,0.38); padding: 4px; }
|
||||
.global-search-result { display: flex; flex-direction: column; gap: 2px; width: 100%; border: 0; background: transparent; color: #e4e4ef; text-align: left; padding: 8px 10px; border-radius: 6px; cursor: pointer; font: inherit; }
|
||||
.global-search-result:hover, .global-search-result.selected { background: #252545; }
|
||||
.global-search-title { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.global-search-meta { font-size: 11px; color: #a0a0bc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.global-search-snippet { font-size: 12px; color: #767690; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.global-search-empty { padding: 10px 12px; color: #8888a0; font-size: 12px; }
|
||||
</style>
|
||||
@@ -154,7 +154,6 @@
|
||||
|
||||
<div class="today-screen">
|
||||
<div class="today-header">
|
||||
<h2>{t('today.title')}</h2>
|
||||
<span class="today-date">{todayDashboard?.date || ''}</span>
|
||||
</div>
|
||||
|
||||
@@ -334,8 +333,7 @@
|
||||
|
||||
<style>
|
||||
.today-screen { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
||||
.today-header h2 { font-size: 24px; margin: 0; }
|
||||
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; min-height: 20px; }
|
||||
.today-date { font-size: 13px; color: #666; }
|
||||
|
||||
.today-tabs { display: flex; gap: 0; border-bottom: 1px solid #2a2a3c; margin-bottom: 16px; }
|
||||
|
||||
@@ -108,6 +108,8 @@ export default {
|
||||
'common.created': 'Created',
|
||||
'common.empty': 'None',
|
||||
'common.newName': 'New name',
|
||||
'search.placeholder': 'Search Verstak...',
|
||||
'search.noResults': 'No results found',
|
||||
'welcome.title': 'Verstak',
|
||||
'welcome.selectSection': 'Select a section in the sidebar.',
|
||||
'welcome.createCase': 'Or create a new case with the + button.',
|
||||
@@ -140,6 +142,7 @@ export default {
|
||||
'kind.archive': 'Archive',
|
||||
'kind.case': 'Case',
|
||||
'kind.link': 'Link',
|
||||
'kind.action': 'Action',
|
||||
'links.empty': 'No links yet',
|
||||
'links.editTitle': 'Edit link',
|
||||
'links.note': 'Note',
|
||||
|
||||
@@ -113,6 +113,8 @@ export default {
|
||||
'common.created': 'Создано',
|
||||
'common.empty': 'Нет',
|
||||
'common.newName': 'Новое имя',
|
||||
'search.placeholder': 'Поиск по Верстаку...',
|
||||
'search.noResults': 'Ничего не найдено',
|
||||
|
||||
'welcome.title': 'Верстак',
|
||||
'welcome.selectSection': 'Выберите раздел в боковой панели.',
|
||||
@@ -148,6 +150,7 @@ export default {
|
||||
'kind.archive': 'Архив',
|
||||
'kind.case': 'Дело',
|
||||
'kind.link': 'Ссылка',
|
||||
'kind.action': 'Действие',
|
||||
|
||||
'links.empty': 'Ссылок пока нет',
|
||||
'links.editTitle': 'Редактировать ссылку',
|
||||
|
||||
Reference in New Issue
Block a user