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:
@@ -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>
|
||||
Reference in New Issue
Block a user