Today screen: tabs (feed, suggestions, in-progress, captured) + inbox sort/group
- Новый экран 'Сегодня' разбит на 4 вкладки: Лента, Предложения, В работе, Захвачено - Лента отображает события за сегодня с кликабельными сущностями - Предложения вынесены в отдельную вкладку (только предложения) - В работе: изменённые файлы/заметки/действия за сегодня с сортировками - Захвачено: захваченные элементы за сегодня с сортировками - Неразобранное: сортировка по дате/имени/типу с направлением - Неразобранное: переключатель 'Группировать по месту захвата' - TodayScreen.svelte: новый компонент с 4 вкладками - Новые i18n ключи для вкладок и сортировок - Backend: ListTodayInProgress, ListTodayCaptures bindings - Все переходы из вкладок ведут в соответствующее место программы
This commit is contained in:
parent
c8aaf36533
commit
2ed2ecf77a
|
|
@ -220,3 +220,43 @@ func (a *App) eventDTOWithPath(e activity.Event) EventDTO {
|
|||
dto.NodePath = a.nodes.Path(e.NodeID)
|
||||
return dto
|
||||
}
|
||||
|
||||
// ListTodayInProgress returns today's modification events — items the user worked on.
|
||||
func (a *App) ListTodayInProgress() ([]EventDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := a.activity.ListTodayEvents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modTypes := map[string]bool{
|
||||
activity.TypeNoteCreated: true,
|
||||
activity.TypeNoteUpdated: true,
|
||||
activity.TypeNoteDeleted: true,
|
||||
activity.TypeFileAdded: true,
|
||||
activity.TypeFileDeleted: true,
|
||||
activity.TypeFileRenamed: true,
|
||||
activity.TypeFileCopied: true,
|
||||
activity.TypeFileMoved: true,
|
||||
activity.TypeFolderAdded: true,
|
||||
activity.TypeFolderDeleted: true,
|
||||
activity.TypeFolderRenamed: true,
|
||||
activity.TypeFolderMoved: true,
|
||||
activity.TypeNodeCreated: true,
|
||||
activity.TypeNodeUpdated: true,
|
||||
activity.TypeNodeDeleted: true,
|
||||
activity.TypeActionCreated: true,
|
||||
activity.TypeActionDone: true,
|
||||
}
|
||||
result := make([]EventDTO, 0, len(events))
|
||||
for _, e := range events {
|
||||
if modTypes[e.EventType] {
|
||||
result = append(result, a.eventDTOWithPath(e))
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].CreatedAt > result[j].CreatedAt
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"verstak/internal/core/activity"
|
||||
)
|
||||
|
||||
type InboxNodeDTO struct {
|
||||
NodeDTO
|
||||
|
|
@ -189,6 +194,34 @@ func (a *App) clearCaptureMeta(nodeID string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ListTodayCaptures returns inbox nodes captured today.
|
||||
func (a *App) ListTodayCaptures() ([]InboxNodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all, err := a.ListInboxNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start, end := activity.TodayBoundaries()
|
||||
startTime, _ := time.Parse(time.RFC3339, start)
|
||||
endTime, _ := time.Parse(time.RFC3339, end)
|
||||
result := make([]InboxNodeDTO, 0, len(all))
|
||||
for _, item := range all {
|
||||
if item.CapturedAt == "" {
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, item.CapturedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if (t.Equal(startTime) || t.After(startTime)) && t.Before(endTime) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) captureMeta(nodeID, key string) string {
|
||||
v, ok, err := a.nodes.MetaGet(nodeID, key)
|
||||
if err != nil || !ok {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,8 +19,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-DzZpCe3r.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BoZGEeiu.css">
|
||||
<script type="module" crossorigin src="/assets/main-DNvOo-CH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CXWRKHDk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import VaultRecovery from './lib/VaultRecovery.svelte'
|
||||
import SettingsWindow from './lib/SettingsWindow.svelte'
|
||||
import SyncStatus from './lib/SyncStatus.svelte'
|
||||
import TodayScreen from './lib/TodayScreen.svelte'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { actionIcon } from './lib/actionIcons.js'
|
||||
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
||||
|
|
@ -92,10 +93,15 @@
|
|||
let acceptingSuggestion = null
|
||||
let suggestions = []
|
||||
let suggestionCount = 0
|
||||
let inProgressItems = []
|
||||
let todayCaptures = []
|
||||
let inboxNodes = []
|
||||
let localInboxNodes = []
|
||||
let inboxCaptureBusy = false
|
||||
let inboxCaptureStatus = ''
|
||||
let inboxSortBy = 'date'
|
||||
let inboxSortDir = 'desc'
|
||||
let inboxGroupByContext = false
|
||||
let links = []
|
||||
let editingLink = null
|
||||
let linkTitle = ''
|
||||
|
|
@ -197,6 +203,42 @@
|
|||
let unlistenDrop = null
|
||||
|
||||
$: visibleTrashNodes = computeVisibleTrashNodes(trashInfo?.nodes || [], trashFolderId)
|
||||
|
||||
$: sortedInboxNodes = sortInboxNodes(inboxNodes, inboxSortBy, inboxSortDir)
|
||||
$: groupedInboxNodes = inboxGroupByContext ? groupInboxByContext(sortedInboxNodes) : null
|
||||
|
||||
function sortInboxNodes(nodes, sortBy, sortDir) {
|
||||
const sorted = [...nodes]
|
||||
sorted.sort((a, b) => {
|
||||
let cmp = 0
|
||||
if (sortBy === 'name') {
|
||||
cmp = (a.title || '').localeCompare(b.title || '')
|
||||
} else if (sortBy === 'type') {
|
||||
const aKind = a.captureKind || a.sourceKind || a.type || ''
|
||||
const bKind = b.captureKind || b.sourceKind || b.type || ''
|
||||
cmp = aKind.localeCompare(bKind)
|
||||
} else {
|
||||
cmp = (a.capturedAt || a.createdAt || '').localeCompare(b.capturedAt || b.createdAt || '')
|
||||
}
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
function groupInboxByContext(nodes) {
|
||||
const groups = {}
|
||||
for (const item of nodes) {
|
||||
const key = item.captureContextLabel || t('inbox.noContext')
|
||||
if (!groups[key]) groups[key] = []
|
||||
groups[key].push(item)
|
||||
}
|
||||
const result = []
|
||||
for (const [label, items] of Object.entries(groups)) {
|
||||
result.push({ label, items })
|
||||
}
|
||||
result.sort((a, b) => a.label.localeCompare(b.label))
|
||||
return result
|
||||
}
|
||||
$: currentTrashPath = trashFolderStack.length === 0 ? t('nav.trash') : [t('nav.trash'), ...trashFolderStack.map(node => node.title)].join(' / ')
|
||||
$: currentTrashTitle = currentTrashFolder()?.title || t('trash.deletedNodes')
|
||||
|
||||
|
|
@ -378,6 +420,8 @@
|
|||
showCreateNode = false
|
||||
error = ''
|
||||
todayDashboard = null
|
||||
inProgressItems = []
|
||||
todayCaptures = []
|
||||
activityFeed = []
|
||||
activityOffset = 0
|
||||
activityHasMore = true
|
||||
|
|
@ -388,6 +432,8 @@
|
|||
todayDashboard = await wailsCall('ListTodayView') || { cases: [] }
|
||||
suggestions = await wailsCall('GetSuggestions') || []
|
||||
suggestionCount = suggestions.length
|
||||
inProgressItems = await wailsCall('ListTodayInProgress') || []
|
||||
todayCaptures = await wailsCall('ListTodayCaptures') || []
|
||||
} else if (id === 'inbox') {
|
||||
inboxNodes = await wailsCall('ListInboxNodes') || []
|
||||
} else if (id === 'trash') {
|
||||
|
|
@ -1673,7 +1719,7 @@
|
|||
const labels = {
|
||||
'note_created': t('event.noteCreated'),
|
||||
'note_updated': t('event.noteUpdated'),
|
||||
'note_deleted': 'Заметка удалена',
|
||||
'note_deleted': t('event.noteDeleted'),
|
||||
'file_added': t('event.fileAdded'),
|
||||
'file_deleted': t('event.fileDeleted'),
|
||||
'file_renamed': t('event.fileRenamed'),
|
||||
|
|
@ -1682,13 +1728,13 @@
|
|||
'folder_added': t('event.folderAdded'),
|
||||
'folder_deleted': t('event.folderDeleted'),
|
||||
'folder_renamed': t('event.folderRenamed'),
|
||||
'folder_moved': 'Папка перемещена',
|
||||
'folder_moved': t('event.folderMoved'),
|
||||
'node_created': t('event.caseCreated'),
|
||||
'node_updated': t('event.caseUpdated'),
|
||||
'node_deleted': 'Узел удалён',
|
||||
'action_created': 'Действие создано',
|
||||
'action_done': 'Действие выполнено',
|
||||
'worklog_added': 'Запись времени добавлена',
|
||||
'node_deleted': t('event.nodeDeleted'),
|
||||
'action_created': t('event.actionCreated'),
|
||||
'action_done': t('event.actionDone'),
|
||||
'worklog_added': t('event.worklogAdded'),
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
|
@ -2844,13 +2890,71 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if inboxNodes.length > 0}
|
||||
<div class="inbox-controls">
|
||||
<div class="inbox-sort-row">
|
||||
<button class="sort-btn" class:active={inboxSortBy === 'date'} on:click={() => { inboxSortBy = 'date'; inboxSortDir = inboxSortBy === 'date' && inboxSortDir === 'desc' ? 'asc' : 'desc' }}>{t('inbox.sortByDate')}</button>
|
||||
<button class="sort-btn" class:active={inboxSortBy === 'name'} on:click={() => { inboxSortBy = 'name'; inboxSortDir = inboxSortBy === 'name' && inboxSortDir === 'desc' ? 'asc' : 'desc' }}>{t('inbox.sortByName')}</button>
|
||||
<button class="sort-btn" class:active={inboxSortBy === 'type'} on:click={() => { inboxSortBy = 'type'; inboxSortDir = inboxSortBy === 'type' && inboxSortDir === 'desc' ? 'asc' : 'desc' }}>{t('inbox.sortByType')}</button>
|
||||
<span class="sort-dir">{inboxSortDir === 'asc' ? '↑' : '↓'}</span>
|
||||
<span class="inbox-controls-divider"></span>
|
||||
<label class="checkbox-label-inline">
|
||||
<input type="checkbox" bind:checked={inboxGroupByContext} />
|
||||
<span>{t('inbox.groupByContext')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if inboxNodes.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>{t('inbox.empty')}</p>
|
||||
</div>
|
||||
{:else if groupedInboxNodes}
|
||||
{#each groupedInboxNodes as group}
|
||||
<div class="inbox-group">
|
||||
<div class="inbox-group-header">{group.label} <span class="inbox-group-count">{group.items.length}</span></div>
|
||||
<div class="inbox-list">
|
||||
{#each group.items as item}
|
||||
<div class="inbox-item" role="button" tabindex="0" on:click={() => openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
|
||||
<div class="inbox-item-main">
|
||||
<span class="inbox-item-title">{item.title}</span>
|
||||
<span class="inbox-item-meta">{inboxMetaText(item)}</span>
|
||||
</div>
|
||||
<div class="inbox-item-actions">
|
||||
{#if item.suggestedTargetNodeId}
|
||||
<button class="inbox-icon-btn inbox-icon-btn-primary" title={t('inbox.keepHere')} aria-label={t('inbox.keepHere')} on:click|stopPropagation={() => resolveInboxHere(item)}>
|
||||
{@html actionIcon('attach')}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="inbox-icon-slot" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<button class="inbox-icon-btn" title={t('inbox.assign')} aria-label={t('inbox.assign')} on:click|stopPropagation={() => openAssignInbox(item)}>
|
||||
{@html actionIcon('move')}
|
||||
</button>
|
||||
<button class="inbox-icon-btn" title={t('common.open')} aria-label={t('common.open')} on:click|stopPropagation={() => openInboxArtifact(item)}>
|
||||
{@html actionIcon('open')}
|
||||
</button>
|
||||
{#if canShowInboxArtifactInFolder(item)}
|
||||
<button class="inbox-icon-btn" title={t('file.showInExplorer')} aria-label={t('file.showInExplorer')} on:click|stopPropagation={() => openNodeFolder(item)}>
|
||||
{@html actionIcon('showInExplorer')}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="inbox-icon-slot" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<button class="inbox-icon-btn inbox-icon-btn-danger" title={t('common.delete')} aria-label={t('common.delete')} on:click|stopPropagation={() => confirmDeleteInbox(item)}>
|
||||
{@html actionIcon('delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="inbox-list">
|
||||
{#each inboxNodes as item}
|
||||
{#each sortedInboxNodes as item}
|
||||
<div class="inbox-item" role="button" tabindex="0" on:click={() => openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
|
||||
<div class="inbox-item-main">
|
||||
<span class="inbox-item-title">{item.title}</span>
|
||||
|
|
@ -3181,110 +3285,36 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if selectedSection === 'today' && todayDashboard}
|
||||
<div class="today-dashboard">
|
||||
<div class="today-header">
|
||||
<h2>{t('today.title')}</h2>
|
||||
<span class="today-date">{todayDashboard.date}</span>
|
||||
{:else if selectedSection === 'today'}
|
||||
{#if todayDashboard}
|
||||
<TodayScreen
|
||||
{todayDashboard}
|
||||
{suggestions}
|
||||
{inProgressItems}
|
||||
{todayCaptures}
|
||||
suggestionCount={suggestions.length}
|
||||
{formatTime}
|
||||
{formatDate}
|
||||
{eventLabel}
|
||||
{eventIcon}
|
||||
{nodeKindLabel}
|
||||
{captureKindLabel}
|
||||
{inboxMetaText}
|
||||
onOpenNodeById={(id) => openNodeById(id)}
|
||||
onOpenActivityEvent={(ev) => openActivityEvent(ev)}
|
||||
onOpenActivityTarget={(ev) => openActivityTarget(ev)}
|
||||
onOpenSuggestionWorklog={(s) => openSuggestionWorklogModal(s)}
|
||||
onAcceptSuggestion={(s) => acceptTodaySuggestion(s)}
|
||||
onDeleteSuggestion={(s) => deleteSuggestion(s)}
|
||||
onOpenNodeFolder={(id) => openNodeFolder(id)}
|
||||
onOpenInboxArtifact={(item) => openInboxArtifact(item)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="today-empty">
|
||||
<p>{t('today.empty')}</p>
|
||||
<p class="hint">{t('today.emptyHint')}</p>
|
||||
</div>
|
||||
{#if todayDashboard.summary}
|
||||
<div class="today-summary">
|
||||
{#if todayDashboard.summary.changedCases > 0}<span class="summary-chip">{todayDashboard.summary.changedCases} {pluralize(todayDashboard.summary.changedCases, t('today.plural.case_one'), t('today.plural.case_few'), t('today.plural.case_many'))}</span>{/if}
|
||||
{#if todayDashboard.summary.notes > 0}<span class="summary-chip">{todayDashboard.summary.notes} {pluralize(todayDashboard.summary.notes, t('today.plural.note_one'), t('today.plural.note_few'), t('today.plural.note_many'))}</span>{/if}
|
||||
{#if todayDashboard.summary.files > 0}<span class="summary-chip">{todayDashboard.summary.files} {pluralize(todayDashboard.summary.files, t('today.plural.file_one'), t('today.plural.file_few'), t('today.plural.file_many'))}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if suggestions.length > 0}
|
||||
<div class="today-suggestions">
|
||||
<div class="suggestions-title">{t('suggest.title')}</div>
|
||||
{#each suggestions as s}
|
||||
<div class="suggestion-card" class:expanded={s._expanded}>
|
||||
<div class="suggestion-main" on:click={() => s._expanded = !s._expanded} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && (s._expanded = !s._expanded)}>
|
||||
<div class="suggestion-info">
|
||||
<button class="suggestion-node link-btn" on:click|stopPropagation={() => openNodeById(s.nodeId)}>{s.nodeTitle}</button>
|
||||
<span class="suggestion-summary">{s.summary}</span>
|
||||
<span class="suggestion-meta">{s.suggestedMin} {t('worklog.min')}{#if s.events && s.events.length > 0} · {s.events.length} {t('suggest.detectedEvents')}{/if}</span>
|
||||
</div>
|
||||
<div class="suggestion-actions">
|
||||
<span class="suggestion-confidence-dot" class:low={s.confidence === 'low'} class:medium={s.confidence === 'medium'} class:high={s.confidence === 'high'} title={t('suggest.confidence.' + s.confidence)} aria-label={t('suggest.confidence.' + s.confidence)}></span>
|
||||
<input type="number" class="suggestion-min-input" value={s.suggestedMin} min="1" max="480"
|
||||
on:change|stopPropagation={(e) => s.suggestedMin = parseInt(e.target.value)} />
|
||||
<span class="suggestion-min-label">{t('suggest.minutes')}</span>
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openSuggestionWorklogModal(s)}>{t('suggest.edit')}</button>
|
||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => acceptTodaySuggestion(s)}>{t('suggest.apply')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => deleteSuggestion(s)}>{t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if s._expanded && s.events && s.events.length > 0}
|
||||
<div class="suggestion-detail">
|
||||
<div class="suggestion-detail-title">{t('suggest.detectedEvents')}</div>
|
||||
{#each s.events as ev}
|
||||
<div class="suggestion-detail-event">
|
||||
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
||||
{#if ev.nodePath}<span class="suggestion-event-path">{ev.nodePath}</span>{/if}
|
||||
<span class="suggestion-event-type">{eventLabel(ev.eventType)}</span>
|
||||
<span class="suggestion-event-title">{ev.title}</span>
|
||||
<button class="link-btn" on:click={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if todayDashboard.groups && todayDashboard.groups.length > 0}
|
||||
{#each todayDashboard.groups as group}
|
||||
<div class="today-case">
|
||||
<div class="today-case-header" role="button" tabindex="0" on:click={() => openNodeById(group.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(group.nodeId)}>
|
||||
<span class="today-case-title">{group.nodeTitle}</span>
|
||||
<span class="today-case-type">{nodeKindLabel(group.nodeKind)}</span>
|
||||
{#if group.events}<span class="today-case-count">{group.events.length} {pluralize(group.events.length, t('today.plural.event_one'), t('today.plural.event_few'), t('today.plural.event_many'))}</span>{/if}
|
||||
<span class="today-case-time">{formatTime(group.lastActivityAt)}</span>
|
||||
</div>
|
||||
{#if group.events && group.events.length > 0}
|
||||
<div class="today-events">
|
||||
{#each group.events as ev}
|
||||
<div class="today-event" role="button" tabindex="0" on:click={() => openNodeById(group.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(group.nodeId)}>
|
||||
<span class="today-event-icon">{eventIcon(ev.eventType)}</span>
|
||||
<span class="today-event-title">{ev.title}</span>
|
||||
<span class="today-event-type">{eventLabel(ev.eventType)}</span>
|
||||
{#if ev.targetType}<span class="activity-event-target">{ev.targetType}</span>{/if}
|
||||
<span class="today-event-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="today-events-empty">{t('today.changedCases')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if todayDashboard.events && todayDashboard.events.length > 0}
|
||||
<div class="today-timeline">
|
||||
<h3>{t('today.timeline')}</h3>
|
||||
{#each todayDashboard.events as ev}
|
||||
<div class="timeline-event" role="button" tabindex="0" on:click={() => openActivityEvent(ev)} on:keydown={(e) => e.key === 'Enter' && openActivityEvent(ev)}>
|
||||
<span class="timeline-dot"></span>
|
||||
<span class="timeline-title">{ev.title}</span>
|
||||
<span class="timeline-type">{eventLabel(ev.eventType)}</span>
|
||||
<span class="timeline-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="today-empty">
|
||||
<p>{t('today.empty')}</p>
|
||||
<p class="hint">{t('today.emptyHint')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else if selectedSection === 'activity'}
|
||||
<div class="activity-feed">
|
||||
|
|
@ -3773,6 +3803,18 @@
|
|||
.inbox-icon-btn-danger:hover { color: #fff; border-color: #dc2626; background: #3a1f24; }
|
||||
.inbox-tab { padding: 24px; }
|
||||
|
||||
/* Inbox sort/group controls */
|
||||
.inbox-controls { margin-bottom: 12px; }
|
||||
.inbox-sort-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.sort-btn { padding: 4px 10px; border: 1px solid #2a2a3c; background: #13131f; color: #888; border-radius: 4px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
||||
.sort-btn:hover { border-color: #3a3a5c; color: #ccc; }
|
||||
.sort-btn.active { border-color: #6366f1; color: #a5b4fc; background: rgba(99,102,241,0.08); }
|
||||
.sort-dir { font-size: 12px; color: #8888a0; margin-left: 2px; }
|
||||
.inbox-controls-divider { width: 1px; height: 20px; background: #2a2a3c; margin: 0 4px; }
|
||||
.inbox-group { margin-bottom: 16px; }
|
||||
.inbox-group-header { font-size: 12px; font-weight: 600; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 0 6px; margin-bottom: 6px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; gap: 8px; }
|
||||
.inbox-group-count { font-size: 10px; color: #8888a0; background: #1e1e30; padding: 1px 6px; border-radius: 8px; }
|
||||
|
||||
/* Links tab */
|
||||
.links-tab { padding: 24px; }
|
||||
.links-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,433 @@
|
|||
<script>
|
||||
import { t } from './i18n'
|
||||
|
||||
export let todayDashboard = null
|
||||
export let suggestions = []
|
||||
export let inProgressItems = []
|
||||
export let todayCaptures = []
|
||||
export let suggestionCount = 0
|
||||
|
||||
export let formatTime = (iso) => ''
|
||||
export let formatDate = (iso) => ''
|
||||
export let eventLabel = (type) => type
|
||||
export let eventIcon = (type) => ''
|
||||
export let nodeKindLabel = (kind) => kind
|
||||
export let captureKindLabel = (kind) => kind
|
||||
export let inboxMetaText = (item) => ''
|
||||
|
||||
export let onOpenNodeById = (id) => {}
|
||||
export let onOpenActivityEvent = (ev) => {}
|
||||
export let onOpenActivityTarget = (ev) => {}
|
||||
export let onOpenSuggestionWorklog = (s) => {}
|
||||
export let onAcceptSuggestion = (s) => {}
|
||||
export let onDeleteSuggestion = (s) => {}
|
||||
export let onOpenNodeFolder = (id) => {}
|
||||
export let onOpenInboxArtifact = (item) => {}
|
||||
|
||||
let activeTab = 'feed'
|
||||
|
||||
function pluralize(n, one, few, many) {
|
||||
n = Math.abs(n) % 100
|
||||
if (n >= 5 && n <= 20) return many
|
||||
n %= 10
|
||||
if (n === 1) return one
|
||||
if (n >= 2 && n <= 4) return few
|
||||
return many
|
||||
}
|
||||
|
||||
function extractEventIds(s) {
|
||||
if (s.eventIds && s.eventIds.length) return s.eventIds
|
||||
if (s.events && s.events.length) return s.events.map(ev => ev.id).filter(Boolean)
|
||||
return []
|
||||
}
|
||||
|
||||
function handleAcceptSuggestion(s) {
|
||||
onAcceptSuggestion(s)
|
||||
}
|
||||
|
||||
function handleDeleteSuggestion(s) {
|
||||
onDeleteSuggestion(s)
|
||||
}
|
||||
|
||||
function feedItemLabel(ev) {
|
||||
return eventLabel(ev.eventType)
|
||||
}
|
||||
|
||||
function feedItemIcon(type) {
|
||||
if (type === 'note_created' || type === 'file_added' || type === 'folder_added' || type === 'node_created') return '+'
|
||||
if (type === 'file_deleted' || type === 'folder_deleted') return '×'
|
||||
if (type === 'file_renamed' || type === 'folder_renamed' || type === 'node_updated') return '~'
|
||||
if (type === 'file_copied') return '⧉'
|
||||
if (type === 'file_moved' || type === 'folder_moved') return '→'
|
||||
if (type === 'note_updated') return '✎'
|
||||
if (type === 'action_created') return '▶'
|
||||
if (type === 'action_done') return '✓'
|
||||
if (type === 'worklog_added') return '⏱'
|
||||
return '•'
|
||||
}
|
||||
|
||||
function handleFeedTitleClick(ev) {
|
||||
if (ev.eventType === 'folder_deleted') {
|
||||
onOpenNodeById(ev.nodeId)
|
||||
return
|
||||
}
|
||||
if (['file_added','file_deleted','file_renamed','file_copied','file_moved','folder_added','folder_renamed','folder_moved'].includes(ev.eventType)) {
|
||||
if (ev.targetId) {
|
||||
onOpenActivityTarget(ev)
|
||||
} else {
|
||||
onOpenNodeById(ev.nodeId)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (['note_created','note_updated','note_deleted'].includes(ev.eventType)) {
|
||||
if (ev.targetType === 'note' && ev.targetId) {
|
||||
onOpenActivityTarget(ev)
|
||||
} else {
|
||||
onOpenNodeById(ev.nodeId)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (ev.eventType === 'worklog_added') {
|
||||
onOpenNodeById(ev.nodeId)
|
||||
return
|
||||
}
|
||||
if (['action_created','action_done'].includes(ev.eventType)) {
|
||||
onOpenNodeById(ev.nodeId)
|
||||
return
|
||||
}
|
||||
onOpenActivityEvent(ev)
|
||||
}
|
||||
|
||||
function feedItemSubtitle(ev) {
|
||||
const parts = []
|
||||
if (ev.nodePath) parts.push(ev.nodePath)
|
||||
return parts.length > 0 ? parts.join(' → ') : ''
|
||||
}
|
||||
|
||||
function inProgressSort(items, sortBy, sortDir) {
|
||||
const sorted = [...items]
|
||||
sorted.sort((a, b) => {
|
||||
let cmp = 0
|
||||
if (sortBy === 'name') {
|
||||
cmp = (a.title || '').localeCompare(b.title || '')
|
||||
} else if (sortBy === 'type') {
|
||||
cmp = (eventLabel(a.eventType) || '').localeCompare(eventLabel(b.eventType) || '')
|
||||
} else {
|
||||
cmp = (a.createdAt || '').localeCompare(b.createdAt || '')
|
||||
}
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
let ipSortBy = 'time'
|
||||
let ipSortDir = 'desc'
|
||||
|
||||
let captureSortBy = 'time'
|
||||
let captureSortDir = 'desc'
|
||||
|
||||
function captureSort(items, sortBy, sortDir) {
|
||||
const sorted = [...items]
|
||||
sorted.sort((a, b) => {
|
||||
let cmp = 0
|
||||
if (sortBy === 'name') {
|
||||
cmp = (a.title || '').localeCompare(b.title || '')
|
||||
} else if (sortBy === 'type') {
|
||||
const aKind = a.captureKind || a.sourceKind || ''
|
||||
const bKind = b.captureKind || b.sourceKind || ''
|
||||
cmp = aKind.localeCompare(bKind)
|
||||
} else {
|
||||
cmp = (a.capturedAt || a.createdAt || '').localeCompare(b.capturedAt || b.createdAt || '')
|
||||
}
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
function captureKindIcon(kind) {
|
||||
const map = { text: '📝', url: '🔗', file: '📄', image: '🖼', folder: '📁' }
|
||||
return map[kind] || '📦'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="today-screen">
|
||||
<div class="today-header">
|
||||
<h2>{t('today.title')}</h2>
|
||||
<span class="today-date">{todayDashboard?.date || ''}</span>
|
||||
</div>
|
||||
|
||||
<div class="today-tabs">
|
||||
<button class="today-tab" class:active={activeTab === 'feed'} on:click={() => activeTab = 'feed'}>
|
||||
{t('today.feed')}
|
||||
</button>
|
||||
<button class="today-tab" class:active={activeTab === 'suggestions'} on:click={() => activeTab = 'suggestions'}>
|
||||
{t('today.suggestionsTab')}
|
||||
{#if suggestionCount > 0}<span class="tab-badge">{suggestionCount}</span>{/if}
|
||||
</button>
|
||||
<button class="today-tab" class:active={activeTab === 'inprogress'} on:click={() => activeTab = 'inprogress'}>
|
||||
{t('today.inProgress')}
|
||||
</button>
|
||||
<button class="today-tab" class:active={activeTab === 'captured'} on:click={() => activeTab = 'captured'}>
|
||||
{t('today.captured')}
|
||||
{#if todayCaptures.length > 0}<span class="tab-badge">{todayCaptures.length}</span>{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if activeTab === 'feed'}
|
||||
<div class="today-tab-content">
|
||||
{#if !todayDashboard || !todayDashboard.events || todayDashboard.events.length === 0}
|
||||
<div class="today-empty">
|
||||
<p>{t('today.feedEmpty')}</p>
|
||||
<p class="hint">{t('today.feedHint')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="feed-list">
|
||||
{#each todayDashboard.events as ev}
|
||||
<div class="feed-item" role="button" tabindex="0"
|
||||
on:click={() => handleFeedTitleClick(ev)}
|
||||
on:keydown={(e) => e.key === 'Enter' && handleFeedTitleClick(ev)}>
|
||||
<span class="feed-icon">{feedItemIcon(ev.eventType)}</span>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title-line">
|
||||
<span class="feed-type">{feedItemLabel(ev.eventType)}</span>
|
||||
<span class="feed-colon">:</span>
|
||||
<span class="feed-title link-btn">{ev.title}</span>
|
||||
</div>
|
||||
<div class="feed-meta-line">
|
||||
{#if feedItemSubtitle(ev)}
|
||||
<span class="feed-path">{feedItemSubtitle(ev)}</span>
|
||||
{/if}
|
||||
<span class="feed-event-type">{eventLabel(ev.eventType)}</span>
|
||||
<span class="feed-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="feed-nav-btn" title={t('common.open')} on:click|stopPropagation={() => handleFeedTitleClick(ev)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'suggestions'}
|
||||
<div class="today-tab-content">
|
||||
{#if suggestions.length === 0}
|
||||
<div class="today-empty">
|
||||
<p>{t('suggest.noSuggestions')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="suggestions-list">
|
||||
{#each suggestions as s}
|
||||
<div class="suggestion-card" class:expanded={s._expanded}>
|
||||
<div class="suggestion-main" on:click={() => s._expanded = !s._expanded} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && (s._expanded = !s._expanded)}>
|
||||
<div class="suggestion-info">
|
||||
<button class="suggestion-node link-btn" on:click|stopPropagation={() => onOpenNodeById(s.nodeId)}>{s.nodeTitle}</button>
|
||||
<span class="suggestion-summary">{s.summary}</span>
|
||||
<span class="suggestion-meta">{s.suggestedMin} {t('worklog.min')}{#if s.events && s.events.length > 0} · {s.events.length} {t('suggest.detectedEvents')}{/if}</span>
|
||||
</div>
|
||||
<div class="suggestion-actions">
|
||||
<span class="suggestion-confidence-dot" class:low={s.confidence === 'low'} class:medium={s.confidence === 'medium'} class:high={s.confidence === 'high'} title={t('suggest.confidence.' + s.confidence)} aria-label={t('suggest.confidence.' + s.confidence)}></span>
|
||||
<input type="number" class="suggestion-min-input" value={s.suggestedMin} min="1" max="480"
|
||||
on:change|stopPropagation={(e) => s.suggestedMin = parseInt(e.target.value)} />
|
||||
<span class="suggestion-min-label">{t('suggest.minutes')}</span>
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => onOpenSuggestionWorklog(s)}>{t('suggest.edit')}</button>
|
||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => handleAcceptSuggestion(s)}>{t('suggest.apply')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => handleDeleteSuggestion(s)}>{t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if s._expanded && s.events && s.events.length > 0}
|
||||
<div class="suggestion-detail">
|
||||
<div class="suggestion-detail-title">{t('suggest.detectedEvents')}</div>
|
||||
{#each s.events as ev}
|
||||
<div class="suggestion-detail-event">
|
||||
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
||||
{#if ev.nodePath}<span class="suggestion-event-path">{ev.nodePath}</span>{/if}
|
||||
<span class="suggestion-event-type">{eventLabel(ev.eventType)}</span>
|
||||
<span class="suggestion-event-title">{ev.title}</span>
|
||||
<button class="link-btn" on:click={() => onOpenActivityTarget(ev)}>{t('common.open')}</button>
|
||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||
<button class="link-btn" on:click={() => onOpenNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'inprogress'}
|
||||
<div class="today-tab-content">
|
||||
<div class="tab-controls">
|
||||
<div class="sort-controls">
|
||||
<button class="sort-btn" class:active={ipSortBy === 'time'} on:click={() => { ipSortBy = 'time'; ipSortDir = ipSortBy === 'time' && ipSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByTime')}</button>
|
||||
<button class="sort-btn" class:active={ipSortBy === 'name'} on:click={() => { ipSortBy = 'name'; ipSortDir = ipSortBy === 'name' && ipSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByName')}</button>
|
||||
<button class="sort-btn" class:active={ipSortBy === 'type'} on:click={() => { ipSortBy = 'type'; ipSortDir = ipSortBy === 'type' && ipSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByType')}</button>
|
||||
<span class="sort-dir">{ipSortDir === 'asc' ? '↑' : '↓'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if inProgressItems.length === 0}
|
||||
<div class="today-empty">
|
||||
<p>{t('today.inProgressEmpty')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ip-list">
|
||||
{#each inProgressSort(inProgressItems, ipSortBy, ipSortDir) as item}
|
||||
<div class="ip-item" role="button" tabindex="0"
|
||||
on:click={() => onOpenActivityEvent(item)}
|
||||
on:keydown={(e) => e.key === 'Enter' && onOpenActivityEvent(item)}>
|
||||
<span class="ip-icon">{eventIcon(item.eventType)}</span>
|
||||
<div class="ip-body">
|
||||
<span class="ip-title">{item.title}</span>
|
||||
{#if item.nodePath}
|
||||
<span class="ip-path">{item.nodePath}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="ip-type">{eventLabel(item.eventType)}</span>
|
||||
<span class="ip-time">{formatTime(item.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'captured'}
|
||||
<div class="today-tab-content">
|
||||
<div class="tab-controls">
|
||||
<div class="sort-controls">
|
||||
<button class="sort-btn" class:active={captureSortBy === 'time'} on:click={() => { captureSortBy = 'time'; captureSortDir = captureSortBy === 'time' && captureSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByTime')}</button>
|
||||
<button class="sort-btn" class:active={captureSortBy === 'name'} on:click={() => { captureSortBy = 'name'; captureSortDir = captureSortBy === 'name' && captureSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByName')}</button>
|
||||
<button class="sort-btn" class:active={captureSortBy === 'type'} on:click={() => { captureSortBy = 'type'; captureSortDir = captureSortBy === 'type' && captureSortDir === 'desc' ? 'asc' : 'desc' }}>{t('today.sortByType')}</button>
|
||||
<span class="sort-dir">{captureSortDir === 'asc' ? '↑' : '↓'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if todayCaptures.length === 0}
|
||||
<div class="today-empty">
|
||||
<p>{t('today.capturedEmpty')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="capture-list">
|
||||
{#each captureSort(todayCaptures, captureSortBy, captureSortDir) as item}
|
||||
<div class="capture-item" role="button" tabindex="0"
|
||||
on:click={() => onOpenInboxArtifact(item)}
|
||||
on:keydown={(e) => e.key === 'Enter' && onOpenInboxArtifact(item)}>
|
||||
<span class="capture-icon">{captureKindIcon(item.captureKind || item.sourceKind)}</span>
|
||||
<div class="capture-body">
|
||||
<span class="capture-title">{item.title}</span>
|
||||
<span class="capture-meta">{inboxMetaText(item)}</span>
|
||||
</div>
|
||||
<span class="capture-time">{formatTime(item.capturedAt || item.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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-date { font-size: 13px; color: #666; }
|
||||
|
||||
.today-tabs { display: flex; gap: 0; border-bottom: 1px solid #2a2a3c; margin-bottom: 16px; }
|
||||
.today-tab { padding: 10px 16px; border: none; background: none; color: #888; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; position: relative; }
|
||||
.today-tab:hover { color: #a5b4fc; }
|
||||
.today-tab.active { color: #e4e4ef; border-bottom-color: #818cf8; background: rgba(99,102,241,0.12); font-weight: 600; }
|
||||
.tab-badge { background: #6366f1; color: #fff; font-size: 10px; font-weight: 700; border-radius: 10px; padding: 1px 6px; margin-left: 6px; line-height: 1.4; vertical-align: middle; }
|
||||
|
||||
.today-tab-content { flex: 1; }
|
||||
|
||||
.today-empty { padding: 48px 24px; text-align: center; }
|
||||
.today-empty p { color: #666; font-size: 14px; margin: 0; }
|
||||
.today-empty .hint { font-size: 12px; color: #555; margin-top: 8px; }
|
||||
|
||||
.tab-controls { margin-bottom: 12px; }
|
||||
.sort-controls { display: flex; align-items: center; gap: 6px; }
|
||||
.sort-btn { padding: 4px 10px; border: 1px solid #2a2a3c; background: #13131f; color: #888; border-radius: 4px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
||||
.sort-btn:hover { border-color: #3a3a5c; color: #ccc; }
|
||||
.sort-btn.active { border-color: #6366f1; color: #a5b4fc; background: rgba(99,102,241,0.08); }
|
||||
.sort-dir { font-size: 12px; color: #8888a0; margin-left: 4px; }
|
||||
|
||||
/* Feed */
|
||||
.feed-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.feed-item { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
|
||||
.feed-item:hover { background: #1a1a28; }
|
||||
.feed-icon { width: 20px; text-align: center; color: #6366f1; font-size: 14px; flex-shrink: 0; margin-top: 2px; }
|
||||
.feed-body { flex: 1; min-width: 0; }
|
||||
.feed-title-line { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
|
||||
.feed-type { font-size: 13px; color: #8888a0; }
|
||||
.feed-colon { color: #555; }
|
||||
.feed-title { font-size: 14px; color: #e4e4ef; font-weight: 500; }
|
||||
.feed-title:hover { text-decoration: underline; }
|
||||
.feed-meta-line { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
|
||||
.feed-path { font-size: 11px; color: #8ea0d8; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.feed-event-type { font-size: 10px; color: #555; background: #1e1e2e; padding: 1px 6px; border-radius: 8px; }
|
||||
.feed-time { font-size: 11px; color: #555; margin-left: auto; flex-shrink: 0; }
|
||||
.feed-nav-btn { background: none; border: none; color: #555; cursor: pointer; padding: 4px; border-radius: 4px; flex-shrink: 0; margin-top: 2px; }
|
||||
.feed-nav-btn:hover { color: #a5b4fc; background: #1e1e30; }
|
||||
|
||||
.link-btn { background: none; border: none; padding: 0; color: #a5b4fc; font: inherit; cursor: pointer; text-align: left; }
|
||||
.link-btn:hover { text-decoration: underline; }
|
||||
|
||||
/* Suggestions */
|
||||
.suggestions-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.suggestion-card { display: flex; flex-direction: column; align-items: stretch; padding: 0; background: #1e1e32; border-radius: 6px; margin-bottom: 6px; gap: 0; border: 1px solid transparent; }
|
||||
.suggestion-card:last-child { margin-bottom: 0; }
|
||||
.suggestion-card.expanded { border-color: #3a3a5c; }
|
||||
.suggestion-main { display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; cursor: pointer; gap: 10px; min-height: 38px; }
|
||||
.suggestion-info { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.suggestion-node { color: #a5b4fc; font-weight: 600; font-size: 13px; text-decoration: none; cursor: pointer; }
|
||||
.suggestion-node:hover { text-decoration: underline; }
|
||||
.suggestion-summary { font-size: 14px; color: #e4e4ef; }
|
||||
.suggestion-meta { font-size: 12px; color: #8888a0; margin-top: 2px; }
|
||||
.suggestion-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||||
.suggestion-min-input { width: 54px; padding: 3px 7px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 13px; text-align: center; }
|
||||
.suggestion-min-label { font-size: 12px; color: #8888a0; }
|
||||
.suggestion-confidence-dot { width: 9px; height: 9px; border-radius: 999px; background: #77798a; box-shadow: 0 0 0 3px rgba(119,121,138,0.12); flex: 0 0 auto; }
|
||||
.suggestion-confidence-dot.low { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,0.12); }
|
||||
.suggestion-confidence-dot.medium { background: #fbbf24; box-shadow: 0 0 0 3px rgba(251,191,36,0.12); }
|
||||
.suggestion-confidence-dot.high { background: #34d399; box-shadow: 0 0 0 3px rgba(52,211,153,0.12); }
|
||||
.suggestion-detail { width: 100%; padding: 6px 10px 8px; border-top: 1px solid #2a2a3c; }
|
||||
.suggestion-detail-title { font-size: 10px; font-weight: 600; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.3px; padding: 4px 0; }
|
||||
.suggestion-detail-event { display: flex; align-items: center; gap: 7px; padding: 2px 0; font-size: 12px; color: #b0b0c0; }
|
||||
.suggestion-event-time { color: #a0a0b8; font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 48px; }
|
||||
.suggestion-event-type { color: #8888a0; font-size: 11px; background: #1a1a2e; padding: 1px 6px; border-radius: 3px; }
|
||||
.suggestion-event-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.suggestion-event-path { max-width: 220px; color: #8ea0d8; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: #222233; }
|
||||
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||
.btn-primary:hover { background: #4f46e5; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-danger { color: #ff6b6b; border-color: #4a2222; }
|
||||
.btn-danger:hover { background: #3a2222; }
|
||||
|
||||
/* In Progress */
|
||||
.ip-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.ip-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
|
||||
.ip-item:hover { background: #1a1a28; }
|
||||
.ip-icon { width: 20px; text-align: center; color: #6366f1; font-size: 13px; flex-shrink: 0; }
|
||||
.ip-body { flex: 1; min-width: 0; }
|
||||
.ip-title { font-size: 14px; color: #e4e4ef; display: block; }
|
||||
.ip-path { font-size: 11px; color: #8ea0d8; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
||||
.ip-type { font-size: 11px; color: #666; flex-shrink: 0; }
|
||||
.ip-time { font-size: 11px; color: #555; flex-shrink: 0; margin-left: 8px; }
|
||||
|
||||
/* Captured */
|
||||
.capture-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.capture-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid #2a2a3c; border-radius: 8px; background: #1a1a28; cursor: pointer; }
|
||||
.capture-item:hover { border-color: #3a3a5c; background: #1e1e32; }
|
||||
.capture-icon { font-size: 18px; width: 28px; text-align: center; flex-shrink: 0; }
|
||||
.capture-body { flex: 1; min-width: 0; }
|
||||
.capture-title { font-size: 14px; color: #e4e4ef; font-weight: 500; display: block; }
|
||||
.capture-meta { font-size: 12px; color: #8888a0; display: block; margin-top: 2px; }
|
||||
.capture-time { font-size: 11px; color: #555; flex-shrink: 0; }
|
||||
</style>
|
||||
|
|
@ -25,6 +25,13 @@ export default {
|
|||
'nav.createNode': 'Create element',
|
||||
'nav.moveToRoot': 'Move to root',
|
||||
'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
|
||||
'inbox.sortByDate': 'by date',
|
||||
'inbox.sortByName': 'by name',
|
||||
'inbox.sortByType': 'by type',
|
||||
'inbox.sortAsc': 'ascending',
|
||||
'inbox.sortDesc': 'descending',
|
||||
'inbox.groupByContext': 'Group by capture location',
|
||||
'inbox.noContext': 'No capture location',
|
||||
'inbox.empty': 'No unprocessed items',
|
||||
'inbox.pasteClipboard': 'Paste from clipboard',
|
||||
'inbox.captured': 'Added',
|
||||
|
|
@ -116,6 +123,12 @@ export default {
|
|||
'event.folderRenamed': 'Folder renamed',
|
||||
'event.caseCreated': 'Case created',
|
||||
'event.caseUpdated': 'Case updated',
|
||||
'event.noteDeleted': 'Note deleted',
|
||||
'event.folderMoved': 'Folder moved',
|
||||
'event.nodeDeleted': 'Node deleted',
|
||||
'event.actionCreated': 'Action created',
|
||||
'event.actionDone': 'Action completed',
|
||||
'event.worklogAdded': 'Time entry added',
|
||||
'kind.project': 'Project',
|
||||
'kind.client': 'Client',
|
||||
'kind.document': 'Document',
|
||||
|
|
@ -251,6 +264,19 @@ export default {
|
|||
'today.plural.event_one': 'event',
|
||||
'today.plural.event_few': 'events',
|
||||
'today.plural.event_many': 'events',
|
||||
'today.feed': 'Feed',
|
||||
'today.suggestionsTab': 'Suggestions',
|
||||
'today.inProgress': 'In Progress',
|
||||
'today.captured': 'Captured',
|
||||
'today.feedEmpty': 'No events today',
|
||||
'today.inProgressEmpty': 'Nothing modified today',
|
||||
'today.capturedEmpty': 'Nothing captured today',
|
||||
'today.feedHint': 'Today\'s events: file changes, notes, actions and other events.',
|
||||
'today.sortByTime': 'by time',
|
||||
'today.sortByName': 'by name',
|
||||
'today.sortByType': 'by type',
|
||||
'today.sortAsc': 'ascending',
|
||||
'today.sortDesc': 'descending',
|
||||
'journal.title': 'Work Log',
|
||||
'journal.empty': 'No entries for the selected period',
|
||||
'journal.dateFrom': 'From',
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ export default {
|
|||
'nav.moveToRoot': 'Переместить в корень',
|
||||
|
||||
'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
|
||||
'inbox.sortByDate': 'по дате',
|
||||
'inbox.sortByName': 'по имени',
|
||||
'inbox.sortByType': 'по типу',
|
||||
'inbox.sortAsc': 'по возрастанию',
|
||||
'inbox.sortDesc': 'по убыванию',
|
||||
'inbox.groupByContext': 'Группировать по месту захвата',
|
||||
'inbox.noContext': 'Без места захвата',
|
||||
'inbox.empty': 'Неразобранных элементов нет',
|
||||
'inbox.pasteClipboard': 'Вставить из буфера',
|
||||
'inbox.captured': 'Добавлено',
|
||||
|
|
@ -123,6 +130,12 @@ export default {
|
|||
'event.folderRenamed': 'Папка переименована',
|
||||
'event.caseCreated': 'Дело создано',
|
||||
'event.caseUpdated': 'Дело изменено',
|
||||
'event.noteDeleted': 'Заметка удалена',
|
||||
'event.folderMoved': 'Папка перемещена',
|
||||
'event.nodeDeleted': 'Узел удалён',
|
||||
'event.actionCreated': 'Действие создано',
|
||||
'event.actionDone': 'Действие выполнено',
|
||||
'event.worklogAdded': 'Запись времени добавлена',
|
||||
|
||||
'kind.project': 'Проект',
|
||||
'kind.client': 'Клиент',
|
||||
|
|
@ -266,6 +279,19 @@ export default {
|
|||
'today.plural.event_one': 'событие',
|
||||
'today.plural.event_few': 'события',
|
||||
'today.plural.event_many': 'событий',
|
||||
'today.feed': 'Лента',
|
||||
'today.suggestionsTab': 'Предложения',
|
||||
'today.inProgress': 'В работе',
|
||||
'today.captured': 'Захвачено',
|
||||
'today.feedEmpty': 'Сегодня событий нет',
|
||||
'today.inProgressEmpty': 'Сегодня ничего не изменяли',
|
||||
'today.capturedEmpty': 'Сегодня ничего не захвачено',
|
||||
'today.feedHint': 'События за сегодня: изменения файлов, заметок, действий и другие события.',
|
||||
'today.sortByTime': 'по времени',
|
||||
'today.sortByName': 'по имени',
|
||||
'today.sortByType': 'по типу',
|
||||
'today.sortAsc': 'по возрастанию',
|
||||
'today.sortDesc': 'по убыванию',
|
||||
|
||||
'journal.title': 'Журнал работы',
|
||||
'journal.empty': 'Нет записей за выбранный период',
|
||||
|
|
|
|||
Loading…
Reference in New Issue