Compare commits
2 Commits
c8aaf36533
...
ceee03959b
| Author | SHA1 | Date |
|---|---|---|
|
|
ceee03959b | |
|
|
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
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>
|
||||
</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 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}
|
||||
</div>
|
||||
|
||||
{: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