feat: refine workspace overview
This commit is contained in:
parent
a323710a6f
commit
b056016929
|
|
@ -78,13 +78,13 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
|
|
||||||
const health = page.locator('[data-plugin-manager-summary="health"]');
|
const health = page.locator('[data-plugin-manager-summary="health"]');
|
||||||
await expect(health).toBeVisible();
|
await expect(health).toBeVisible();
|
||||||
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('7');
|
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('9');
|
||||||
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
|
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
|
||||||
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
|
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
|
||||||
|
|
||||||
const risk = page.locator('[data-plugin-manager-summary="risk"]');
|
const risk = page.locator('[data-plugin-manager-summary="risk"]');
|
||||||
await expect(risk).toBeVisible();
|
await expect(risk).toBeVisible();
|
||||||
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('4');
|
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('5');
|
||||||
await expect(risk).toContainText('elevated permissions');
|
await expect(risk).toContainText('elevated permissions');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
|
|
||||||
const tabs = page.getByRole('tab');
|
const tabs = page.getByRole('tab');
|
||||||
await expect(tabs.nth(0)).toHaveText('Overview');
|
await expect(tabs.nth(0)).toHaveText('Overview');
|
||||||
await expect(tabs.nth(1)).toHaveText('Files');
|
await expect(tabs.nth(1)).toHaveText('Notes');
|
||||||
await expect(page.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.getByRole('tab', { name: 'Today' })).toHaveCount(0);
|
await expect(page.getByRole('tab', { name: 'Today' })).toHaveCount(0);
|
||||||
|
|
||||||
|
|
@ -29,18 +29,55 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(overview.locator('[data-overview-section="continue"]')).toContainText('Continue working');
|
await expect(overview.locator('[data-overview-section="continue"]')).toContainText('Continue working');
|
||||||
await expect(overview.locator('[data-overview-section="recent"]')).toContainText('Recent changes');
|
await expect(overview.locator('[data-overview-section="recent"]')).toContainText('Recent changes');
|
||||||
await expect(overview.locator('[data-overview-section="attention"]')).toContainText('Needs attention');
|
await expect(overview.locator('[data-overview-section="attention"]')).toContainText('Needs attention');
|
||||||
await expect(overview.locator('[data-overview-section="quick-actions"]')).toContainText('Quick actions');
|
await expect(overview.locator('[data-overview-section="quick-actions"]')).toHaveCount(0);
|
||||||
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('recent changes');
|
|
||||||
await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('unprocessed captures');
|
const summaryCards = overview.locator('button[data-overview-summary]');
|
||||||
|
await expect(summaryCards).toHaveCount(6);
|
||||||
|
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1 total');
|
||||||
|
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('0 recent changes');
|
||||||
|
await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('0 captures to review');
|
||||||
|
await expect(overview.locator('[data-overview-summary="activity"]')).toContainText('0 recorded events');
|
||||||
|
await expect(overview.locator('[data-overview-summary="journal"]')).toContainText('0 journal entries');
|
||||||
await expect(overview).toContainText('No clear resume point yet');
|
await expect(overview).toContainText('No clear resume point yet');
|
||||||
await expect(overview).toContainText('No meaningful changes for this filter yet');
|
await expect(overview).toContainText('No meaningful changes for this filter yet');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Overview quick action opens Browser Inbox workspace tool', async ({ page }) => {
|
test('Overview summary cards navigate to their corresponding workspace tools', async ({ page }) => {
|
||||||
await page.locator('[data-overview-action="browser-inbox"]').click();
|
const overview = page.locator('[data-overview-root]');
|
||||||
|
await overview.locator('[data-overview-summary="notes"]').click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Notes' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(page.locator('.notes-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
await overview.locator('[data-overview-summary="files"]').click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
await overview.locator('[data-overview-summary="captures"]').click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
await overview.locator('[data-overview-summary="activity"]').click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Activity' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
await overview.locator('[data-overview-summary="journal"]').click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(page.locator('.journal-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
await overview.locator('[data-overview-summary="attention"]').click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Overview keeps the Notes total factual when the Files plugin is unavailable', async ({ page }) => {
|
||||||
|
await page.evaluate(() => window.__wailsMock.setPluginStatus('verstak.files', 'disabled', false));
|
||||||
|
await page.locator('[data-overview-action="refresh"]').click();
|
||||||
|
|
||||||
|
await expect(page.locator('[data-overview-summary="notes"]')).toContainText('1 total');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Overview prioritizes resume work and filters meaningful recent changes', async ({ page }) => {
|
test('Overview prioritizes resume work and filters meaningful recent changes', async ({ page }) => {
|
||||||
|
|
@ -135,19 +172,25 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await page.locator('[data-overview-action="refresh"]').click();
|
await page.locator('[data-overview-action="refresh"]').click();
|
||||||
|
|
||||||
const overview = page.locator('[data-overview-root]');
|
const overview = page.locator('[data-overview-root]');
|
||||||
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1');
|
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1 total');
|
||||||
await expect(overview.locator('[data-overview-summary="files"]')).toContainText('1');
|
await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1 recent change');
|
||||||
|
await expect(overview.locator('[data-overview-summary="files"]')).toContainText('1 recent change');
|
||||||
await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('2');
|
await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('2');
|
||||||
|
await expect(overview.locator('[data-overview-summary="activity"]')).toContainText('5 recorded events');
|
||||||
await expect(overview.locator('[data-overview-summary="journal"]')).toContainText('1');
|
await expect(overview.locator('[data-overview-summary="journal"]')).toContainText('1');
|
||||||
await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
|
await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
|
||||||
|
|
||||||
const resume = overview.locator('[data-overview-section="continue"]');
|
const resume = overview.locator('[data-overview-section="continue"]');
|
||||||
await expect(resume).toContainText('Opened file "draft.md"');
|
const candidates = resume.locator('[data-overview-continue-item]');
|
||||||
await expect(resume).not.toContainText('Research Report');
|
await expect(candidates).toHaveCount(4);
|
||||||
await resume.locator('[data-overview-action="continue-primary"]').click();
|
await expect(candidates.nth(0)).toContainText('Quote to process');
|
||||||
|
await expect(candidates.nth(1)).toContainText('Research Report');
|
||||||
|
await expect(candidates.nth(2)).toContainText('Edited note "Overview"');
|
||||||
|
await expect(candidates.nth(3)).toContainText('Changed file "draft.md"');
|
||||||
|
await candidates.nth(0).click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
const recent = overview.locator('[data-overview-section="recent"]');
|
const recent = overview.locator('[data-overview-section="recent"]');
|
||||||
|
|
@ -158,6 +201,8 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(recent).not.toContainText('Selected file');
|
await expect(recent).not.toContainText('Selected file');
|
||||||
await expect(recent).not.toContainText('Workspace selected');
|
await expect(recent).not.toContainText('Workspace selected');
|
||||||
await expect(recent).not.toContainText('file.opened');
|
await expect(recent).not.toContainText('file.opened');
|
||||||
|
await expect(recent.locator('[data-overview-recent-item]')).toHaveCount(5);
|
||||||
|
await expect(recent.locator('[data-overview-recent-item] button')).toHaveCount(0);
|
||||||
|
|
||||||
await overview.locator('[data-overview-filter="notes"]').click();
|
await overview.locator('[data-overview-filter="notes"]').click();
|
||||||
await expect(recent).toContainText('Edited note "Overview"');
|
await expect(recent).toContainText('Edited note "Overview"');
|
||||||
|
|
|
||||||
|
|
@ -33,27 +33,29 @@
|
||||||
let journalEntries = [];
|
let journalEntries = [];
|
||||||
let worklogSuggestions = [];
|
let worklogSuggestions = [];
|
||||||
let keyResources = [];
|
let keyResources = [];
|
||||||
|
let totalNotes = 0;
|
||||||
let loadedWorkspaceRoot = '';
|
let loadedWorkspaceRoot = '';
|
||||||
let toolProbe = 0;
|
let toolProbe = 0;
|
||||||
|
|
||||||
$: hasNotes = hasTool('notes');
|
$: hasNotes = hasTool('notes');
|
||||||
$: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox');
|
|
||||||
$: hasActivity = hasTool('activity');
|
|
||||||
$: hasFiles = hasTool('files');
|
|
||||||
$: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
|
$: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
|
||||||
$: filteredRecentChanges = activeFilter === 'all'
|
$: filteredRecentChanges = activeFilter === 'all'
|
||||||
? recentChanges
|
? recentChanges
|
||||||
: recentChanges.filter(item => item.category === activeFilter);
|
: recentChanges.filter(item => item.category === activeFilter);
|
||||||
$: needsAttention = buildNeedsAttention(captures, worklogSuggestions);
|
$: noteRecentChanges = countCategory(recentChanges, 'notes');
|
||||||
$: continueItems = buildContinueItems(activityEvents, captures);
|
$: fileRecentChanges = countCategory(recentChanges, 'files');
|
||||||
$: primaryContinue = continueItems[0] || null;
|
$: unprocessedCaptures = captures.filter(item => item?.processed !== true);
|
||||||
|
$: needsAttention = buildNeedsAttention(unprocessedCaptures, worklogSuggestions);
|
||||||
|
$: continueItems = buildContinueItems(activityEvents, unprocessedCaptures, journalEntries);
|
||||||
|
$: attentionActionKind = needsAttention[0]?.actionKind || 'browser-inbox';
|
||||||
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries);
|
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries);
|
||||||
$: summaryItems = [
|
$: summaryItems = [
|
||||||
{ key: 'notes', label: 'Notes', count: countCategory(recentChanges, 'notes'), detail: countLabel(countCategory(recentChanges, 'notes'), 'recent change') },
|
{ key: 'notes', label: 'Notes', count: totalNotes, detail: totalAndRecentLabel(totalNotes, noteRecentChanges), actionKind: 'notes', actionLabel: 'Open Notes' },
|
||||||
{ key: 'files', label: 'Files', count: countCategory(recentChanges, 'files'), detail: countLabel(countCategory(recentChanges, 'files'), 'recent change') },
|
{ key: 'files', label: 'Files', count: fileRecentChanges, detail: countLabel(fileRecentChanges, 'recent change'), actionKind: 'files', actionLabel: 'Open Files' },
|
||||||
{ key: 'captures', label: 'Captures', count: captures.length, detail: countLabel(captures.length, 'unprocessed capture') },
|
{ key: 'captures', label: 'Captures', count: unprocessedCaptures.length, detail: captureReviewLabel(unprocessedCaptures.length), actionKind: 'browser-inbox', actionLabel: 'Review Inbox' },
|
||||||
{ key: 'journal', label: 'Journal / Activity', count: countCategory(recentChanges, 'journal'), detail: countLabel(countCategory(recentChanges, 'journal'), 'entry or event') },
|
{ key: 'activity', label: 'Activity', count: activityEvents.length, detail: countLabel(activityEvents.length, 'recorded event'), actionKind: 'activity', actionLabel: 'View Activity' },
|
||||||
{ key: 'attention', label: 'Needs attention', count: needsAttention.length, detail: countLabel(needsAttention.length, 'pending item') },
|
{ key: 'journal', label: 'Journal', count: journalEntries.length, detail: journalEntryLabel(journalEntries.length), actionKind: 'journal', actionLabel: 'Open Journal' },
|
||||||
|
{ key: 'attention', label: 'Needs attention', count: needsAttention.length, detail: countLabel(needsAttention.length, 'pending item'), actionKind: attentionActionKind, actionLabel: 'Review pending items' },
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -199,7 +201,7 @@
|
||||||
if (type.startsWith('note.')) return 'notes';
|
if (type.startsWith('note.')) return 'notes';
|
||||||
if (type.startsWith('file.')) return 'files';
|
if (type.startsWith('file.')) return 'files';
|
||||||
if (type.startsWith('browser.capture')) return 'captures';
|
if (type.startsWith('browser.capture')) return 'captures';
|
||||||
if (type.startsWith('journal.') || type.startsWith('worklog.') || type.startsWith('action.')) return 'journal';
|
if (type.startsWith('journal.') || type.startsWith('worklog.')) return 'journal';
|
||||||
return 'activity';
|
return 'activity';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +230,7 @@
|
||||||
if (category === 'notes') return { kind: 'notes', label: 'Open Notes' };
|
if (category === 'notes') return { kind: 'notes', label: 'Open Notes' };
|
||||||
if (category === 'files') return { kind: 'files', label: 'Open Files' };
|
if (category === 'files') return { kind: 'files', label: 'Open Files' };
|
||||||
if (category === 'captures') return { kind: 'browser-inbox', label: 'Review Inbox' };
|
if (category === 'captures') return { kind: 'browser-inbox', label: 'Review Inbox' };
|
||||||
if (category === 'journal') return { kind: 'activity', label: 'View Activity' };
|
if (category === 'journal') return { kind: 'journal', label: 'Open Journal' };
|
||||||
return { kind: 'activity', label: 'View Activity' };
|
return { kind: 'activity', label: 'View Activity' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,8 +268,8 @@
|
||||||
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + item.minutes + ' min' : ''}`,
|
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + item.minutes + ' min' : ''}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
actionKind: 'activity',
|
actionKind: 'journal',
|
||||||
actionLabel: 'View Activity',
|
actionLabel: 'Open Journal',
|
||||||
}));
|
}));
|
||||||
return sortByTime([...activityItems, ...captureItems, ...journalItems]).slice(0, 12);
|
return sortByTime([...activityItems, ...captureItems, ...journalItems]).slice(0, 12);
|
||||||
}
|
}
|
||||||
|
|
@ -284,25 +286,25 @@
|
||||||
type === 'browser.capture.converted';
|
type === 'browser.capture.converted';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildContinueItems(events, captureRows) {
|
function continueItemFromActivity(item) {
|
||||||
const fromEvents = sortByTime(events)
|
const category = activityCategory(item);
|
||||||
.filter(isResumeEvent)
|
const action = actionForCategory(category);
|
||||||
.map(item => {
|
return {
|
||||||
const category = activityCategory(item);
|
id: item.activityId || `${item.type}:${timeValue(item)}`,
|
||||||
const action = actionForCategory(category);
|
category,
|
||||||
return {
|
title: activityTitle(item),
|
||||||
id: item.activityId || `${item.type}:${timeValue(item)}`,
|
meta: itemTimeLabel(item),
|
||||||
title: activityTitle(item),
|
time: timeValue(item),
|
||||||
meta: itemTimeLabel(item),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
time: timeValue(item),
|
actionKind: action.kind,
|
||||||
absolute: absoluteTime(timeValue(item)),
|
actionLabel: action.label,
|
||||||
actionKind: action.kind,
|
};
|
||||||
actionLabel: action.label,
|
}
|
||||||
};
|
|
||||||
});
|
function buildContinueItems(events, captureRows, journalRows) {
|
||||||
if (fromEvents.length) return fromEvents.slice(0, 4);
|
const captureCandidates = sortByTime(captureRows).map(item => ({
|
||||||
return sortByTime(captureRows).slice(0, 3).map(item => ({
|
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
|
category: 'captures',
|
||||||
title: `Review capture ${quoted(captureTitle(item))}`,
|
title: `Review capture ${quoted(captureTitle(item))}`,
|
||||||
meta: `${itemTimeLabel(item)} · ${item.domain || item.kind || 'Browser capture'}`,
|
meta: `${itemTimeLabel(item)} · ${item.domain || item.kind || 'Browser capture'}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
|
|
@ -310,6 +312,23 @@
|
||||||
actionKind: 'browser-inbox',
|
actionKind: 'browser-inbox',
|
||||||
actionLabel: 'Review Inbox',
|
actionLabel: 'Review Inbox',
|
||||||
}));
|
}));
|
||||||
|
const noteCandidates = sortByTime(events)
|
||||||
|
.filter(item => isResumeEvent(item) && activityCategory(item) === 'notes')
|
||||||
|
.map(continueItemFromActivity);
|
||||||
|
const fileCandidates = sortByTime(events)
|
||||||
|
.filter(item => ['file.changed', 'file.created'].includes(String(item?.type || '').toLowerCase()))
|
||||||
|
.map(continueItemFromActivity);
|
||||||
|
const journalCandidates = sortByTime(journalRows).map(item => ({
|
||||||
|
id: item.entryId || `journal:${timeValue(item)}`,
|
||||||
|
category: 'journal',
|
||||||
|
title: `Continue journal entry ${quoted(journalTitle(item))}`,
|
||||||
|
meta: itemTimeLabel(item),
|
||||||
|
time: timeValue(item),
|
||||||
|
absolute: absoluteTime(timeValue(item)),
|
||||||
|
actionKind: 'journal',
|
||||||
|
actionLabel: 'Open Journal',
|
||||||
|
}));
|
||||||
|
return [...captureCandidates, ...noteCandidates, ...fileCandidates, ...journalCandidates].slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNeedsAttention(captureRows, suggestions) {
|
function buildNeedsAttention(captureRows, suggestions) {
|
||||||
|
|
@ -322,10 +341,10 @@
|
||||||
}));
|
}));
|
||||||
const suggestionItems = sortByTime(suggestions).slice(0, 4).map(item => ({
|
const suggestionItems = sortByTime(suggestions).slice(0, 4).map(item => ({
|
||||||
id: item.suggestionId || item.entryId || `suggestion:${timeValue(item)}`,
|
id: item.suggestionId || item.entryId || `suggestion:${timeValue(item)}`,
|
||||||
title: item.title || item.summary || 'Pending worklog suggestion',
|
title: item.title || item.summary || 'Possible journal entry',
|
||||||
meta: `${item.minutes ? item.minutes + ' min · ' : ''}${item.date || itemTimeLabel(item)}`,
|
meta: `${item.minutes ? item.minutes + ' min · ' : ''}${item.date || itemTimeLabel(item)}`,
|
||||||
actionKind: 'activity',
|
actionKind: 'journal',
|
||||||
actionLabel: 'View Activity',
|
actionLabel: 'Review candidate',
|
||||||
}));
|
}));
|
||||||
return [...captureItems, ...suggestionItems].slice(0, 6);
|
return [...captureItems, ...suggestionItems].slice(0, 6);
|
||||||
}
|
}
|
||||||
|
|
@ -338,36 +357,51 @@
|
||||||
return `${count} ${singular}${count === 1 ? '' : 's'}`;
|
return `${count} ${singular}${count === 1 ? '' : 's'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function totalAndRecentLabel(total, recent) {
|
||||||
|
return `${total} total · ${countLabel(recent, 'recent change')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureReviewLabel(count) {
|
||||||
|
return `${count} capture${count === 1 ? '' : 's'} to review`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function journalEntryLabel(count) {
|
||||||
|
return `${count} journal entr${count === 1 ? 'y' : 'ies'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function lastActiveDate(items, captureRows, journalRows) {
|
function lastActiveDate(items, captureRows, journalRows) {
|
||||||
const source = sortByTime([...items, ...captureRows, ...journalRows])[0];
|
const source = sortByTime([...items, ...captureRows, ...journalRows])[0];
|
||||||
return timeValue(source);
|
return timeValue(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listFiles(relativeDir) {
|
async function listFiles(pluginId, relativeDir) {
|
||||||
if (!App.ListVaultFiles) return [];
|
if (!App.ListVaultFiles) return [];
|
||||||
try {
|
try {
|
||||||
return decodeTuple(await App.ListVaultFiles('verstak.files', relativeDir), []);
|
return decodeTuple(await App.ListVaultFiles(pluginId, relativeDir), []);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadKeyResources() {
|
async function loadWorkspaceResources() {
|
||||||
const workspace = String(workspaceRootPath || '').trim();
|
const workspace = String(workspaceRootPath || '').trim();
|
||||||
if (!workspace) return [];
|
if (!workspace) return { keyResources: [], totalNotes: 0 };
|
||||||
const [rootEntries, notesEntries] = await Promise.all([
|
const [rootEntries, notesEntries] = await Promise.all([
|
||||||
listFiles(workspace),
|
listFiles('verstak.files', workspace),
|
||||||
listFiles(`${workspace}/Notes`),
|
listFiles('verstak.notes', `${workspace}/Notes`),
|
||||||
]);
|
]);
|
||||||
|
const noteFiles = notesEntries.filter(item => item?.type === 'file');
|
||||||
const overview = [...notesEntries, ...rootEntries].find(item => /(^|\/)overview\.md$/i.test(String(item.relativePath || item.name || '')));
|
const overview = [...notesEntries, ...rootEntries].find(item => /(^|\/)overview\.md$/i.test(String(item.relativePath || item.name || '')));
|
||||||
if (!overview) return [];
|
return {
|
||||||
return [{
|
totalNotes: noteFiles.length,
|
||||||
id: overview.relativePath || overview.name,
|
keyResources: overview ? [{
|
||||||
title: overview.name || fileName(overview.relativePath) || 'Overview.md',
|
id: overview.relativePath || overview.name,
|
||||||
meta: overview.relativePath || 'Workspace overview note',
|
title: overview.name || fileName(overview.relativePath) || 'Overview.md',
|
||||||
actionKind: hasNotes ? 'notes' : 'files',
|
meta: overview.relativePath || 'Workspace overview note',
|
||||||
actionLabel: hasNotes ? 'Open Notes' : 'Open Files',
|
actionKind: hasNotes ? 'notes' : 'files',
|
||||||
}];
|
actionLabel: hasNotes ? 'Open Notes' : 'Open Files',
|
||||||
|
}] : [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOverview() {
|
async function loadOverview() {
|
||||||
|
|
@ -378,7 +412,7 @@
|
||||||
readPluginSettings('verstak.browser-inbox'),
|
readPluginSettings('verstak.browser-inbox'),
|
||||||
readPluginSettings('verstak.activity'),
|
readPluginSettings('verstak.activity'),
|
||||||
readPluginSettings('verstak.journal'),
|
readPluginSettings('verstak.journal'),
|
||||||
loadKeyResources(),
|
loadWorkspaceResources(),
|
||||||
]);
|
]);
|
||||||
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
||||||
|
|
||||||
|
|
@ -400,7 +434,8 @@
|
||||||
workspaceKey('suggestions:workspace:'),
|
workspaceKey('suggestions:workspace:'),
|
||||||
'suggestions',
|
'suggestions',
|
||||||
]);
|
]);
|
||||||
keyResources = resources;
|
keyResources = resources.keyResources;
|
||||||
|
totalNotes = resources.totalNotes;
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,11 +463,20 @@
|
||||||
|
|
||||||
<div class="today-summary overview-summary" aria-label="Workspace overview summary">
|
<div class="today-summary overview-summary" aria-label="Workspace overview summary">
|
||||||
{#each summaryItems as item}
|
{#each summaryItems as item}
|
||||||
<div class="today-summary-item overview-summary-item" data-overview-summary={item.key}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="today-summary-item overview-summary-item"
|
||||||
|
class:summary-attention={item.key === 'attention'}
|
||||||
|
data-overview-summary={item.key}
|
||||||
|
data-overview-action={item.actionKind}
|
||||||
|
aria-label={`${item.label}: ${item.actionLabel}`}
|
||||||
|
on:click={() => openTool(item.actionKind)}
|
||||||
|
>
|
||||||
<strong>{loading ? '...' : item.count}</strong>
|
<strong>{loading ? '...' : item.count}</strong>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
<small>{loading ? 'Loading...' : item.detail}</small>
|
<small>{loading ? 'Loading...' : item.detail}</small>
|
||||||
</div>
|
<em>{item.actionLabel}</em>
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -441,21 +485,33 @@
|
||||||
<section class="today-resume overview-continue" data-overview-section="continue">
|
<section class="today-resume overview-continue" data-overview-section="continue">
|
||||||
<div class="today-resume-copy overview-continue-copy">
|
<div class="today-resume-copy overview-continue-copy">
|
||||||
<span>Continue working</span>
|
<span>Continue working</span>
|
||||||
{#if loading}
|
<h3>Pick up the next useful item in this workspace.</h3>
|
||||||
<strong>Loading workspace signals...</strong>
|
|
||||||
<p>Recent files, notes, captures, and journal entries will appear here.</p>
|
|
||||||
{:else if primaryContinue}
|
|
||||||
<strong title={primaryContinue.title}>{primaryContinue.title}</strong>
|
|
||||||
<p title={primaryContinue.absolute}>{primaryContinue.meta}</p>
|
|
||||||
{:else}
|
|
||||||
<strong>No clear resume point yet</strong>
|
|
||||||
<p>Open files or notes to create a useful return point for this workspace.</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if primaryContinue}
|
{#if loading}
|
||||||
<button type="button" data-overview-action="continue-primary" on:click={() => openTool(primaryContinue.actionKind)}>{primaryContinue.actionLabel}</button>
|
<p class="today-empty compact">Loading workspace signals...</p>
|
||||||
|
{:else if continueItems.length}
|
||||||
|
<div class="overview-continue-list">
|
||||||
|
{#each continueItems as item}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="overview-continue-item"
|
||||||
|
data-overview-continue-item={item.category}
|
||||||
|
data-overview-action={item.actionKind}
|
||||||
|
on:click={() => openTool(item.actionKind)}
|
||||||
|
>
|
||||||
|
<span class="overview-continue-item-copy">
|
||||||
|
<strong title={item.title}>{item.title}</strong>
|
||||||
|
<span title={item.absolute}>{item.meta}</span>
|
||||||
|
</span>
|
||||||
|
<span class="overview-continue-item-action">{item.actionLabel}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button type="button" data-overview-action="continue-primary" on:click={() => openTool('files')}>Open Files</button>
|
<div class="overview-continue-empty">
|
||||||
|
<strong>No clear resume point yet</strong>
|
||||||
|
<p>Recent notes, files, captures, and journal entries will appear here.</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -484,13 +540,19 @@
|
||||||
{:else if filteredRecentChanges.length}
|
{:else if filteredRecentChanges.length}
|
||||||
<div class="today-list overview-list">
|
<div class="today-list overview-list">
|
||||||
{#each filteredRecentChanges as item}
|
{#each filteredRecentChanges as item}
|
||||||
<div class="today-row overview-change-row" data-overview-recent-item={item.category}>
|
<button
|
||||||
<div>
|
type="button"
|
||||||
|
class="today-row overview-change-row"
|
||||||
|
data-overview-recent-item={item.category}
|
||||||
|
data-overview-action={item.actionKind}
|
||||||
|
on:click={() => openTool(item.actionKind)}
|
||||||
|
>
|
||||||
|
<span class="overview-change-copy">
|
||||||
<strong title={item.title}>{item.title}</strong>
|
<strong title={item.title}>{item.title}</strong>
|
||||||
<span title={item.absolute}>{item.meta}</span>
|
<span title={item.absolute}>{item.meta}</span>
|
||||||
</div>
|
</span>
|
||||||
<button type="button" on:click={() => openTool(item.actionKind)}>{item.actionLabel}</button>
|
<span class="overview-row-action">{item.actionLabel}</span>
|
||||||
</div>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -505,7 +567,7 @@
|
||||||
<div class="today-panel-head overview-panel-head">
|
<div class="today-panel-head overview-panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>Needs attention</h3>
|
<h3>Needs attention</h3>
|
||||||
<p>Pending captures and worklog suggestions.</p>
|
<p>Pending captures and possible journal entries.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
|
@ -526,20 +588,6 @@
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="today-panel overview-panel secondary" data-overview-section="quick-actions">
|
|
||||||
<div class="today-panel-head overview-panel-head">
|
|
||||||
<h3>Quick actions</h3>
|
|
||||||
</div>
|
|
||||||
<div class="today-actions overview-actions">
|
|
||||||
{#if hasNotes}
|
|
||||||
<button type="button" data-overview-action="notes" on:click={() => openTool('notes')}>Open Notes</button>
|
|
||||||
{/if}
|
|
||||||
<button type="button" data-overview-action="files" on:click={() => openTool('files')}>Open Files</button>
|
|
||||||
<button type="button" data-overview-action="activity" on:click={() => openTool('activity')}>Review Activity</button>
|
|
||||||
<button type="button" data-overview-action="browser-inbox" on:click={() => openTool('browser-inbox')}>Open Inbox</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if keyResources.length}
|
{#if keyResources.length}
|
||||||
<section class="today-panel overview-panel secondary" data-overview-section="key-resources">
|
<section class="today-panel overview-panel secondary" data-overview-section="key-resources">
|
||||||
<div class="today-panel-head overview-panel-head">
|
<div class="today-panel-head overview-panel-head">
|
||||||
|
|
@ -594,7 +642,7 @@
|
||||||
|
|
||||||
.today-summary {
|
.today-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem 0.75rem 0;
|
padding: 0.75rem 0.75rem 0;
|
||||||
}
|
}
|
||||||
|
|
@ -607,6 +655,17 @@
|
||||||
border: 1px solid var(--vt-color-border);
|
border: 1px solid var(--vt-color-border);
|
||||||
border-radius: var(--vt-radius-lg);
|
border-radius: var(--vt-radius-lg);
|
||||||
background: var(--vt-color-surface);
|
background: var(--vt-color-surface);
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-summary-item:hover,
|
||||||
|
.today-summary-item:focus-visible {
|
||||||
|
border-color: var(--vt-color-accent);
|
||||||
|
background: var(--vt-color-surface-hover);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-summary-item strong {
|
.today-summary-item strong {
|
||||||
|
|
@ -616,7 +675,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-summary-item span,
|
.today-summary-item span,
|
||||||
.today-summary-item small {
|
.today-summary-item small,
|
||||||
|
.today-summary-item em {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
@ -630,6 +690,21 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.today-summary-item em {
|
||||||
|
color: var(--vt-color-accent);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-summary-item.summary-attention {
|
||||||
|
border-color: rgba(255, 200, 87, 0.5);
|
||||||
|
background: var(--vt-color-warning-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-summary-item.summary-attention em {
|
||||||
|
color: var(--vt-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
.overview-layout {
|
.overview-layout {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -647,14 +722,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-resume {
|
.today-resume {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
gap: 0.75rem;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
border: 1px solid rgba(78, 204, 163, 0.24);
|
border: 1px solid rgba(78, 204, 163, 0.24);
|
||||||
border-radius: var(--vt-radius-lg);
|
border-radius: var(--vt-radius-lg);
|
||||||
background: linear-gradient(135deg, rgba(78, 204, 163, 0.11), rgba(27, 36, 64, 0.6));
|
background: var(--vt-color-accent-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-resume-copy {
|
.today-resume-copy {
|
||||||
|
|
@ -671,23 +744,11 @@
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-resume-copy strong {
|
.today-resume-copy h3 {
|
||||||
min-width: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--vt-color-text-primary);
|
color: var(--vt-color-text-primary);
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
.today-resume-copy p {
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--vt-color-text-secondary);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-panel {
|
.today-panel {
|
||||||
|
|
@ -707,6 +768,11 @@
|
||||||
background: var(--vt-color-surface-muted);
|
background: var(--vt-color-surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-overview-section='attention'] {
|
||||||
|
border-color: rgba(255, 200, 87, 0.45);
|
||||||
|
background: var(--vt-color-warning-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.today-panel-head {
|
.today-panel-head {
|
||||||
min-height: 2.8rem;
|
min-height: 2.8rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -741,9 +807,8 @@
|
||||||
|
|
||||||
.overview-filters button,
|
.overview-filters button,
|
||||||
.today-panel-head button,
|
.today-panel-head button,
|
||||||
.today-actions button,
|
|
||||||
.today-header button,
|
.today-header button,
|
||||||
.today-resume button,
|
.overview-continue-item,
|
||||||
.overview-list button {
|
.overview-list button {
|
||||||
min-height: 1.85rem;
|
min-height: 1.85rem;
|
||||||
padding: 0.3rem 0.65rem;
|
padding: 0.3rem 0.65rem;
|
||||||
|
|
@ -765,9 +830,8 @@
|
||||||
.overview-filters button.is-active,
|
.overview-filters button.is-active,
|
||||||
.overview-filters button:hover,
|
.overview-filters button:hover,
|
||||||
.today-panel-head button:hover,
|
.today-panel-head button:hover,
|
||||||
.today-actions button:hover,
|
|
||||||
.today-header button:hover,
|
.today-header button:hover,
|
||||||
.today-resume button:hover,
|
.overview-continue-item:hover,
|
||||||
.overview-list button:hover {
|
.overview-list button:hover {
|
||||||
border-color: var(--vt-color-accent);
|
border-color: var(--vt-color-accent);
|
||||||
color: var(--vt-color-text-primary);
|
color: var(--vt-color-text-primary);
|
||||||
|
|
@ -813,12 +877,51 @@
|
||||||
border: 1px solid var(--vt-color-border);
|
border: 1px solid var(--vt-color-border);
|
||||||
border-radius: var(--vt-radius-md);
|
border-radius: var(--vt-radius-md);
|
||||||
background: var(--vt-color-surface-muted);
|
background: var(--vt-color-surface-muted);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-change-row {
|
.overview-change-row {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--vt-color-border);
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-change-row:hover,
|
||||||
|
.overview-change-row:focus-visible {
|
||||||
|
background: var(--vt-color-surface-hover);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-change-copy,
|
||||||
|
.overview-continue-item-copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-row-action,
|
||||||
|
.overview-continue-item-action {
|
||||||
|
color: var(--vt-color-text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-recent .overview-list {
|
||||||
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-recent .overview-change-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-attention-row,
|
.overview-attention-row,
|
||||||
|
|
@ -844,16 +947,59 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-actions {
|
.overview-continue-list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem;
|
}
|
||||||
|
|
||||||
|
.overview-continue-item {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-continue-item strong,
|
||||||
|
.overview-continue-item-copy span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-continue-item strong {
|
||||||
|
color: var(--vt-color-text-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-continue-item-copy span {
|
||||||
|
color: var(--vt-color-text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-continue-empty {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.22rem;
|
||||||
|
color: var(--vt-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-continue-empty p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vt-color-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1120px) {
|
||||||
|
.today-summary {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.overview-layout,
|
.overview-layout {
|
||||||
.today-summary {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -867,12 +1013,11 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-resume {
|
.overview-continue-list {
|
||||||
align-items: stretch;
|
grid-template-columns: 1fr;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-resume button {
|
.overview-continue-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -881,4 +1026,15 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.today-summary {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-change-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,29 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/files',
|
rootPath: '/tmp/verstak-test/plugins/files',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.notes': {
|
||||||
|
status: 'loaded',
|
||||||
|
enabled: true,
|
||||||
|
manifest: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: 'verstak.notes',
|
||||||
|
name: 'Notes',
|
||||||
|
version: '0.1.0',
|
||||||
|
apiVersion: '0.1.0',
|
||||||
|
description: 'Workspace-scoped notes manager.',
|
||||||
|
source: 'official',
|
||||||
|
icon: 'edit',
|
||||||
|
provides: ['verstak/notes/v1'],
|
||||||
|
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||||
|
permissions: ['files.read', 'files.write', 'files.delete', 'events.subscribe', 'workbench.open', 'ui.register'],
|
||||||
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
|
contributes: {
|
||||||
|
workspaceItems: [{ id: 'verstak.notes.workspace', title: 'Notes', icon: 'edit', component: 'NotesView' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootPath: '/tmp/verstak-test/plugins/notes',
|
||||||
|
error: ''
|
||||||
|
},
|
||||||
'verstak.sync': {
|
'verstak.sync': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -182,6 +205,31 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/activity',
|
rootPath: '/tmp/verstak-test/plugins/activity',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.journal': {
|
||||||
|
status: 'loaded',
|
||||||
|
enabled: true,
|
||||||
|
manifest: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: 'verstak.journal',
|
||||||
|
name: 'Journal',
|
||||||
|
version: '0.1.0',
|
||||||
|
apiVersion: '0.1.0',
|
||||||
|
description: 'Workspace-scoped worklog journal.',
|
||||||
|
source: 'official',
|
||||||
|
icon: 'book-open',
|
||||||
|
provides: ['worklog', 'journal', 'report.worklog'],
|
||||||
|
optionalRequires: ['activity.reconstruction'],
|
||||||
|
permissions: ['storage.namespace', 'ui.register'],
|
||||||
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
|
contributes: {
|
||||||
|
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
||||||
|
sidebarItems: [{ id: 'verstak.journal.sidebar', title: 'Journal', icon: 'book-open', view: 'verstak.journal.view', position: 30 }],
|
||||||
|
workspaceItems: [{ id: 'verstak.journal.workspace', title: 'Journal', icon: 'book-open', component: 'JournalView' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootPath: '/tmp/verstak-test/plugins/journal',
|
||||||
|
error: ''
|
||||||
|
},
|
||||||
'verstak.browser-inbox': {
|
'verstak.browser-inbox': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -234,7 +282,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.sync', 'verstak.activity', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
||||||
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||||
var workbenchPreferences = {};
|
var workbenchPreferences = {};
|
||||||
var openedResources = [];
|
var openedResources = [];
|
||||||
|
|
@ -1381,6 +1429,11 @@
|
||||||
}.toString() + ')();';
|
}.toString() + ')();';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function simplePluginBundle(pluginId, componentName, rootClass, title) {
|
||||||
|
var markup = '<div class="' + rootClass + '"><h2>' + title + '</h2></div>';
|
||||||
|
return '(function(){var Component={mount:function(containerEl){containerEl.innerHTML=' + JSON.stringify(markup) + ';},unmount:function(containerEl){containerEl.innerHTML="";}};window.VerstakPluginRegister(' + JSON.stringify(pluginId) + ',{components:{' + componentName + ':Component}});})();';
|
||||||
|
}
|
||||||
|
|
||||||
function activityBundle() {
|
function activityBundle() {
|
||||||
return '(' + function () {
|
return '(' + function () {
|
||||||
var PLUGIN_ID = 'verstak.activity';
|
var PLUGIN_ID = 'verstak.activity';
|
||||||
|
|
@ -2148,9 +2201,15 @@
|
||||||
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(filesPluginBundle());
|
return Promise.resolve(filesPluginBundle());
|
||||||
}
|
}
|
||||||
|
if (pluginId === 'verstak.notes' && assetPath === 'frontend/dist/index.js') {
|
||||||
|
return Promise.resolve(simplePluginBundle('verstak.notes', 'NotesView', 'notes-root', 'Notes'));
|
||||||
|
}
|
||||||
if (pluginId === 'verstak.activity' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.activity' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(activityBundle());
|
return Promise.resolve(activityBundle());
|
||||||
}
|
}
|
||||||
|
if (pluginId === 'verstak.journal' && assetPath === 'frontend/dist/index.js') {
|
||||||
|
return Promise.resolve(simplePluginBundle('verstak.journal', 'JournalView', 'journal-root', 'Journal'));
|
||||||
|
}
|
||||||
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(browserInboxBundle());
|
return Promise.resolve(browserInboxBundle());
|
||||||
}
|
}
|
||||||
|
|
@ -2632,6 +2691,29 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/files',
|
rootPath: '/tmp/verstak-test/plugins/files',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.notes': {
|
||||||
|
status: 'loaded',
|
||||||
|
enabled: true,
|
||||||
|
manifest: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: 'verstak.notes',
|
||||||
|
name: 'Notes',
|
||||||
|
version: '0.1.0',
|
||||||
|
apiVersion: '0.1.0',
|
||||||
|
description: 'Workspace-scoped notes manager.',
|
||||||
|
source: 'official',
|
||||||
|
icon: 'edit',
|
||||||
|
provides: ['verstak/notes/v1'],
|
||||||
|
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||||
|
permissions: ['files.read', 'files.write', 'files.delete', 'events.subscribe', 'workbench.open', 'ui.register'],
|
||||||
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
|
contributes: {
|
||||||
|
workspaceItems: [{ id: 'verstak.notes.workspace', title: 'Notes', icon: 'edit', component: 'NotesView' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootPath: '/tmp/verstak-test/plugins/notes',
|
||||||
|
error: ''
|
||||||
|
},
|
||||||
'verstak.sync': {
|
'verstak.sync': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -2680,6 +2762,31 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/activity',
|
rootPath: '/tmp/verstak-test/plugins/activity',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.journal': {
|
||||||
|
status: 'loaded',
|
||||||
|
enabled: true,
|
||||||
|
manifest: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: 'verstak.journal',
|
||||||
|
name: 'Journal',
|
||||||
|
version: '0.1.0',
|
||||||
|
apiVersion: '0.1.0',
|
||||||
|
description: 'Workspace-scoped worklog journal.',
|
||||||
|
source: 'official',
|
||||||
|
icon: 'book-open',
|
||||||
|
provides: ['worklog', 'journal', 'report.worklog'],
|
||||||
|
optionalRequires: ['activity.reconstruction'],
|
||||||
|
permissions: ['storage.namespace', 'ui.register'],
|
||||||
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
|
contributes: {
|
||||||
|
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
||||||
|
sidebarItems: [{ id: 'verstak.journal.sidebar', title: 'Journal', icon: 'book-open', view: 'verstak.journal.view', position: 30 }],
|
||||||
|
workspaceItems: [{ id: 'verstak.journal.workspace', title: 'Journal', icon: 'book-open', component: 'JournalView' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootPath: '/tmp/verstak-test/plugins/journal',
|
||||||
|
error: ''
|
||||||
|
},
|
||||||
'verstak.browser-inbox': {
|
'verstak.browser-inbox': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -2731,7 +2838,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||||
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.sync', 'verstak.activity', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
||||||
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||||
workbenchPreferences = {};
|
workbenchPreferences = {};
|
||||||
openedResources = [];
|
openedResources = [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue