@3sln/trove 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The left rail — the app's only global chrome now that the title bar is gone.
|
|
2
|
+
// Brand mark at the top (→ home), the primary views below, then a bottom cluster
|
|
3
|
+
// of notifications, settings, and the signed-in principal. Search is reached from
|
|
4
|
+
// the Home view itself and via the double-shift modal, so there is no omni bar.
|
|
5
|
+
|
|
6
|
+
import { dd } from '../../runtime.js';
|
|
7
|
+
import { icon } from '../icon.js';
|
|
8
|
+
import { notificationBell, principalChip } from './social.js';
|
|
9
|
+
|
|
10
|
+
const { div, button, span, img } = dd;
|
|
11
|
+
|
|
12
|
+
const ITEMS = [
|
|
13
|
+
{ id: 'home', icon: 'search', title: 'Search & files', command: 'workbench.view.home' },
|
|
14
|
+
{ id: 'plugins', icon: 'plug', title: 'Plugins', command: 'workbench.view.plugins' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export default function activityBar(state, ui) {
|
|
18
|
+
const active = state.wb.activity;
|
|
19
|
+
const pluginCount = state.plugins?.filter((p) => p.status === 'active').length || 0;
|
|
20
|
+
return div({ className: 'activitybar' },
|
|
21
|
+
button({ className: 'brand-mark', title: 'Trove — home' }, img({ src: '/icon.svg', alt: 'Trove' }))
|
|
22
|
+
.on({ click: () => ui.exec('workbench.view.home') }),
|
|
23
|
+
...ITEMS.map((it) =>
|
|
24
|
+
button({ className: `item ${active === it.id ? 'active' : ''}`, title: it.title },
|
|
25
|
+
icon(it.icon, { size: 22 }),
|
|
26
|
+
it.id === 'plugins' && pluginCount ? span({ className: 'badge' }, String(pluginCount)) : null,
|
|
27
|
+
).on({ click: () => ui.exec(it.command) }),
|
|
28
|
+
),
|
|
29
|
+
div({ className: 'spacer' }),
|
|
30
|
+
notificationBell(state, ui),
|
|
31
|
+
button({ className: `item ${active === 'settings' ? 'active' : ''}`, title: 'Settings' }, icon('gear', { size: 21 }))
|
|
32
|
+
.on({ click: () => ui.exec('workbench.openSettings') }),
|
|
33
|
+
principalChip(state),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The Activity panel — what is running, and what is stuck.
|
|
2
|
+
//
|
|
3
|
+
// Two sections, because they are two different kinds of fact and the user acts on them
|
|
4
|
+
// differently. Running work is watched and possibly cancelled; a standing problem is
|
|
5
|
+
// retried or dismissed. Merging them into one "notifications" list would lose that:
|
|
6
|
+
// a task that finished is over, while an issue that is listed is still true right now.
|
|
7
|
+
//
|
|
8
|
+
// A task that doesn't know its total renders as an indeterminate bar rather than a
|
|
9
|
+
// guess. Nothing here fabricates a percentage.
|
|
10
|
+
|
|
11
|
+
import { dd } from '../../runtime.js';
|
|
12
|
+
import { icon } from '../icon.js';
|
|
13
|
+
import { bytes } from '../format.js';
|
|
14
|
+
|
|
15
|
+
const { div, button, span, h3, p } = dd;
|
|
16
|
+
|
|
17
|
+
const STATUS_ICON = { running: 'refresh', done: 'check', failed: 'close', cancelled: 'close' };
|
|
18
|
+
|
|
19
|
+
function amount(task) {
|
|
20
|
+
if (task.total == null) return null;
|
|
21
|
+
return task.unit === 'bytes'
|
|
22
|
+
? `${bytes(task.done || 0)} of ${bytes(task.total)}`
|
|
23
|
+
: `${task.done || 0} of ${task.total}${task.unit ? ` ${task.unit}` : ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function taskRow(task, ui) {
|
|
27
|
+
const running = task.status === 'running';
|
|
28
|
+
const determinate = running && task.total != null && task.total > 0;
|
|
29
|
+
const pct = determinate ? Math.min(100, Math.round(((task.done || 0) / task.total) * 100)) : 0;
|
|
30
|
+
return div({ className: `act-task act-${task.status}`, 'data-task-id': task.id },
|
|
31
|
+
div({ className: 'act-row' },
|
|
32
|
+
running
|
|
33
|
+
? div({ className: 'spinner', $styling: { width: '12px', height: '12px' } })
|
|
34
|
+
: icon(STATUS_ICON[task.status] || 'info', { size: 13 }),
|
|
35
|
+
div({ className: 'act-body' },
|
|
36
|
+
div({ className: 'act-title' }, task.title),
|
|
37
|
+
task.detail ? div({ className: 'act-detail' }, task.detail) : null,
|
|
38
|
+
task.error ? div({ className: 'act-error' }, task.error) : null,
|
|
39
|
+
),
|
|
40
|
+
running && task.cancellable
|
|
41
|
+
? button({ className: 'act-action', title: 'Cancel' }, icon('close', { size: 12 })).on({ click: () => ui.app.activity.cancel(task.id) })
|
|
42
|
+
: !running
|
|
43
|
+
? button({ className: 'act-action', title: 'Dismiss' }, icon('close', { size: 12 })).on({ click: () => ui.app.activity.dismiss(task.id) })
|
|
44
|
+
: null,
|
|
45
|
+
),
|
|
46
|
+
running
|
|
47
|
+
// An indeterminate bar animates without claiming a position. The alternative —
|
|
48
|
+
// picking a number — would be a lie dressed as information.
|
|
49
|
+
? div({ className: `act-bar ${determinate ? '' : 'indeterminate'}` },
|
|
50
|
+
div({ className: 'act-fill', $styling: determinate ? { width: `${pct}%` } : {} }))
|
|
51
|
+
: null,
|
|
52
|
+
running && amount(task) ? div({ className: 'act-amount' }, amount(task)) : null,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function issueRow(issue, ui) {
|
|
57
|
+
const since = new Date(issue.firstAt || issue.lastAt).toLocaleString();
|
|
58
|
+
return div({ className: `act-issue act-sev-${issue.severity || 'error'}`, 'data-issue-id': issue.id },
|
|
59
|
+
div({ className: 'act-row' },
|
|
60
|
+
icon(issue.severity === 'warning' ? 'info' : 'close', { size: 13 }),
|
|
61
|
+
div({ className: 'act-body' },
|
|
62
|
+
div({ className: 'act-title' }, issue.title),
|
|
63
|
+
issue.detail ? div({ className: 'act-detail' }, issue.detail) : null,
|
|
64
|
+
div({ className: 'act-meta' }, issue.count > 1 ? `${issue.count} times, since ${since}` : `since ${since}`),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
div({ className: 'act-actions' },
|
|
68
|
+
// Retry only when the server said pressing it will actually do something.
|
|
69
|
+
issue.retryable
|
|
70
|
+
? button({ className: 'btn small act-retry' }, icon('refresh', { size: 12 }), span('Retry'))
|
|
71
|
+
.on({ click: () => ui.app.activity.retryIssue(issue.id) })
|
|
72
|
+
: null,
|
|
73
|
+
button({ className: 'btn small ghost act-dismiss' }, 'Dismiss')
|
|
74
|
+
.on({ click: () => ui.app.activity.dismissIssue(issue.id) }),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function activityPanel(state, ui) {
|
|
80
|
+
const act = state.act || { tasks: [], issues: [] };
|
|
81
|
+
if (!act.open) return null;
|
|
82
|
+
const running = act.tasks.filter((t) => t.status === 'running');
|
|
83
|
+
const recent = act.tasks.filter((t) => t.status !== 'running');
|
|
84
|
+
|
|
85
|
+
return div({ className: 'activity-panel' },
|
|
86
|
+
div({ className: 'act-head' },
|
|
87
|
+
h3('Activity'),
|
|
88
|
+
button({ className: 'act-action', title: 'Close' }, icon('close', { size: 13 }))
|
|
89
|
+
.on({ click: () => ui.app.activity.togglePanel(false) }),
|
|
90
|
+
),
|
|
91
|
+
|
|
92
|
+
// "Nothing is wrong" and "we couldn't ask" look identical unless one says so. Both
|
|
93
|
+
// loads are reported, because either can fail alone.
|
|
94
|
+
act.tasksError || act.issuesError
|
|
95
|
+
? div({ className: 'act-offline' }, icon('info', { size: 12 }),
|
|
96
|
+
span(`Couldn't reach the server — this list may be out of date (${act.issuesError || act.tasksError})`))
|
|
97
|
+
: null,
|
|
98
|
+
|
|
99
|
+
div({ className: 'act-section' },
|
|
100
|
+
div({ className: 'act-section-title' }, 'Running'),
|
|
101
|
+
running.length
|
|
102
|
+
? div(...running.map((t) => taskRow(t, ui)))
|
|
103
|
+
: p({ className: 'act-empty' }, 'Nothing running.'),
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
act.issues.length
|
|
107
|
+
? div({ className: 'act-section' },
|
|
108
|
+
div({ className: 'act-section-title' }, `Needs attention (${act.issues.length})`),
|
|
109
|
+
div(...act.issues.map((i) => issueRow(i, ui))),
|
|
110
|
+
)
|
|
111
|
+
: div({ className: 'act-section' },
|
|
112
|
+
div({ className: 'act-section-title' }, 'Needs attention'),
|
|
113
|
+
// An unreadable list is not an empty one, and must never render as "all clear".
|
|
114
|
+
p({ className: 'act-empty' },
|
|
115
|
+
act.issuesError ? 'Could not load the list of problems.' : act.issuesLoading ? 'Checking…' : 'No standing problems.'),
|
|
116
|
+
),
|
|
117
|
+
|
|
118
|
+
recent.length
|
|
119
|
+
? div({ className: 'act-section' },
|
|
120
|
+
div({ className: 'act-section-title' }, 'Recently finished'),
|
|
121
|
+
div(...recent.map((t) => taskRow(t, ui))),
|
|
122
|
+
)
|
|
123
|
+
: null,
|
|
124
|
+
|
|
125
|
+
div({ className: 'act-foot act-actions' },
|
|
126
|
+
button({ className: 'btn small ghost act-rebuild' }, icon('refresh', { size: 12 }), span('Rebuild search index'))
|
|
127
|
+
.on({ click: () => ui.exec('workbench.rebuildIndex') }),
|
|
128
|
+
button({ className: 'btn small ghost act-scan' }, icon('refresh', { size: 12 }), span('Scan for outside changes'))
|
|
129
|
+
.on({ click: () => ui.exec('workbench.scanCollection') }),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Command palette + quick-open. Mode 'commands' fuzzy-filters registered
|
|
2
|
+
// commands (respecting when-clauses); mode 'files' searches the drive by name
|
|
3
|
+
// and opens the chosen file. Arrow keys move, Enter runs, Esc closes.
|
|
4
|
+
|
|
5
|
+
import { dd } from '../../runtime.js';
|
|
6
|
+
import { icon, iconForNode } from '../icon.js';
|
|
7
|
+
import { prettyKey } from '../../platform/keybindings.js';
|
|
8
|
+
import { OpenFileAction, QuickOpenAction } from '../../bl/actions.js';
|
|
9
|
+
|
|
10
|
+
const { div, input, span } = dd;
|
|
11
|
+
|
|
12
|
+
export default function commandPalette(state, ui) {
|
|
13
|
+
const pal = state.overlay.palette;
|
|
14
|
+
if (!pal) return null;
|
|
15
|
+
const wb = ui.platform.workbench;
|
|
16
|
+
// With no query there is nothing to show — and showing the PREVIOUS session's hits
|
|
17
|
+
// would be worse than nothing, since they don't match what the field now says.
|
|
18
|
+
const items = pal.mode === 'files'
|
|
19
|
+
? (pal.query.trim() ? state.se.paletteFiles || [] : [])
|
|
20
|
+
: filterCommands(state, ui, pal.query);
|
|
21
|
+
const index = Math.min(pal.index, Math.max(0, items.length - 1));
|
|
22
|
+
|
|
23
|
+
const run = (item) => {
|
|
24
|
+
if (!item) return;
|
|
25
|
+
wb.closePalette();
|
|
26
|
+
if (pal.mode === 'files') ui.go(new OpenFileAction(item.node));
|
|
27
|
+
else ui.exec(item.id);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return div({},
|
|
31
|
+
div({ className: 'scrim' }).on({ click: () => wb.closePalette() }),
|
|
32
|
+
div({ className: 'palette' },
|
|
33
|
+
div({ className: 'search' },
|
|
34
|
+
icon(pal.mode === 'files' ? 'search' : 'command', { size: 18 }),
|
|
35
|
+
input({
|
|
36
|
+
type: 'text', value: pal.query, autofocus: true,
|
|
37
|
+
placeholder: pal.mode === 'files' ? 'Search files by name…' : 'Type a command…',
|
|
38
|
+
}).on({
|
|
39
|
+
// NOTE: the mode is read from live state inside onQuery, NOT captured here.
|
|
40
|
+
// A listener belongs to the render that created it, and rendering is async —
|
|
41
|
+
// a keystroke landing between `openPalette('files')` and the re-render would
|
|
42
|
+
// otherwise be handled as a COMMANDS keystroke and silently do nothing.
|
|
43
|
+
input: (e) => onQuery(ui, e.target.value),
|
|
44
|
+
keydown: (e) => onKey(e, ui, items, index, run),
|
|
45
|
+
$attach: (el) => {
|
|
46
|
+
queueMicrotask(() => el.focus());
|
|
47
|
+
// Same reason in reverse: dodo only patches `value` when the vnode prop
|
|
48
|
+
// changed, so a reopened palette whose query is '' both times would keep
|
|
49
|
+
// whatever text the previous session left in the live DOM node.
|
|
50
|
+
if (el.value !== pal.query) el.value = pal.query;
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
div({ className: 'results' },
|
|
55
|
+
items.length
|
|
56
|
+
? items.map((item, i) => {
|
|
57
|
+
const hover = () => wb.setPaletteIndex(i);
|
|
58
|
+
return pal.mode === 'files' ? fileOpt(item, i === index, run, hover) : cmdOpt(item, i === index, ui, run, hover);
|
|
59
|
+
})
|
|
60
|
+
: emptyResults(pal, state),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Nothing to show is three different situations, and saying "No files found" for all
|
|
67
|
+
// three tells the user something false in two of them.
|
|
68
|
+
function emptyResults(pal, state) {
|
|
69
|
+
if (pal.mode !== 'files') return div({ className: 'none' }, 'No matching commands');
|
|
70
|
+
if (state.se.paletteError) return div({ className: 'none error' }, icon('warn', { size: 14 }), ` ${state.se.paletteError}`);
|
|
71
|
+
if (state.se.paletteLoading) return div({ className: 'none' }, div({ className: 'spinner' }), ' Searching…');
|
|
72
|
+
return div({ className: 'none' }, pal.query.trim() ? 'No files found' : 'Type to search files by name');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cmdOpt(cmd, active, ui, run, hover) {
|
|
76
|
+
const key = ui.platform.keybindings.labelFor(cmd.id);
|
|
77
|
+
const available = ui.platform.commands.isAvailable(cmd);
|
|
78
|
+
return div({ className: `opt ${active ? 'active' : ''} ${available ? '' : 'unavailable'}` },
|
|
79
|
+
span({ className: 'ico' }, icon(cmd.icon || 'command', { size: 16 })),
|
|
80
|
+
cmd.category ? span({ className: 'cat' }, cmd.category + ' ›') : null,
|
|
81
|
+
span({ className: 'title' }, cmd.title),
|
|
82
|
+
!available ? span({ className: 'offline-tag' }, 'offline') : null,
|
|
83
|
+
key ? span({ className: 'kbd' }, dd.h('kbd', prettyKey(keyRaw(ui, cmd.id)))) : null,
|
|
84
|
+
).on({ click: () => run(cmd), mouseenter: hover });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function keyRaw(ui, id) {
|
|
88
|
+
const b = ui.platform.keybindings.resolved().find((x) => x.command === id);
|
|
89
|
+
return b ? b.key : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fileOpt(item, active, run, hover) {
|
|
93
|
+
return div({ className: `opt ${active ? 'active' : ''}` },
|
|
94
|
+
span({ className: 'ico' }, icon(iconForNode(item.node), { size: 16 })),
|
|
95
|
+
span({ className: 'title' }, item.node.name),
|
|
96
|
+
span({ className: 'sub' }, item.node.contentType || ''),
|
|
97
|
+
).on({ click: () => run(item), mouseenter: hover });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function filterCommands(state, ui, query) {
|
|
101
|
+
const all = ui.platform.commands.paletteCommands();
|
|
102
|
+
const q = query.trim().toLowerCase();
|
|
103
|
+
if (!q) return all.slice(0, 60);
|
|
104
|
+
const scored = [];
|
|
105
|
+
for (const c of all) {
|
|
106
|
+
const hay = `${c.category || ''} ${c.title}`.toLowerCase();
|
|
107
|
+
const s = fuzzyScore(hay, q);
|
|
108
|
+
if (s > 0) scored.push([s, c]);
|
|
109
|
+
}
|
|
110
|
+
return scored.sort((a, b) => b[0] - a[0]).slice(0, 60).map((x) => x[1]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fuzzyScore(hay, q) {
|
|
114
|
+
let score = 0;
|
|
115
|
+
let hi = 0;
|
|
116
|
+
for (const ch of q) {
|
|
117
|
+
const idx = hay.indexOf(ch, hi);
|
|
118
|
+
if (idx < 0) return 0;
|
|
119
|
+
score += idx === hi ? 3 : 1;
|
|
120
|
+
hi = idx + 1;
|
|
121
|
+
}
|
|
122
|
+
if (hay.includes(q)) score += 10;
|
|
123
|
+
return score;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onKey(e, ui, items, index, run) {
|
|
127
|
+
const wb = ui.platform.workbench;
|
|
128
|
+
if (e.key === 'ArrowDown') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
wb.movePalette(1, items.length);
|
|
131
|
+
} else if (e.key === 'ArrowUp') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
wb.movePalette(-1, items.length);
|
|
134
|
+
} else if (e.key === 'Enter') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
run(items[index]);
|
|
137
|
+
} else if (e.key === 'Escape') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
wb.closePalette();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let fileTimer = null;
|
|
144
|
+
function onQuery(ui, value) {
|
|
145
|
+
const wb = ui.platform.workbench;
|
|
146
|
+
wb.setPaletteQuery(value);
|
|
147
|
+
// The palette's CURRENT mode, not whichever one was on screen when this listener
|
|
148
|
+
// was created — see the note at the input.
|
|
149
|
+
clearTimeout(fileTimer);
|
|
150
|
+
if (wb.overlay.state.palette?.mode !== 'files') return;
|
|
151
|
+
// Route the file search through an action; results land in the reactive search
|
|
152
|
+
// service (state.se.paletteFiles), so the palette re-renders without ad-hoc state.
|
|
153
|
+
fileTimer = setTimeout(() => ui.go(new QuickOpenAction(value)), 200);
|
|
154
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The viewer — the top of the panel stack (a file's opener), full-width, with a
|
|
2
|
+
// nav bar: a Back button, a trail of the stacked panels (Search › A › B),
|
|
3
|
+
// and details/close actions. The opener host returns a *stable* keyed alias per
|
|
4
|
+
// panel so dodo reuses its DOM across unrelated workbench re-renders — critical for
|
|
5
|
+
// any media opener, whose <audio>/<video> must keep playing while state churns; the
|
|
6
|
+
// DOM persists as long as the panel stays in the stack.
|
|
7
|
+
|
|
8
|
+
import { dd } from '../../runtime.js';
|
|
9
|
+
import { icon, iconForNode } from '../icon.js';
|
|
10
|
+
import { renderOpener } from './openers/index.js';
|
|
11
|
+
import { availableOpeners } from '../../bl/openers.js';
|
|
12
|
+
|
|
13
|
+
const { div, span, button } = dd;
|
|
14
|
+
|
|
15
|
+
const openerFns = new Map(); // `${panelId}:${openerId}` -> stable render fn
|
|
16
|
+
|
|
17
|
+
export default function editorArea(state, ui) {
|
|
18
|
+
const wb = state.wb;
|
|
19
|
+
const files = state.nav.stack.filter((p) => p.kind === 'file');
|
|
20
|
+
pruneOpeners(files);
|
|
21
|
+
const active = files[files.length - 1];
|
|
22
|
+
if (!active) return div({ className: 'editor' });
|
|
23
|
+
return div({ className: 'editor' },
|
|
24
|
+
navBar(files, active, ui),
|
|
25
|
+
div({ className: 'stage' }, openerHost(active, ui)),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function navBar(files, active, ui) {
|
|
30
|
+
const w = ui.platform.workbench;
|
|
31
|
+
return div({ className: 'viewer-nav' },
|
|
32
|
+
button({ className: 'vn-back', title: 'Back (Esc)' }, icon('chevron-left', { size: 16 }))
|
|
33
|
+
.on({ click: () => w.back() }),
|
|
34
|
+
div({ className: 'vn-trail' },
|
|
35
|
+
button({ className: 'vn-crumb' }, icon('search', { size: 13 }), span('Search'))
|
|
36
|
+
.on({ click: () => w.showHome() }),
|
|
37
|
+
...files.map((p) =>
|
|
38
|
+
button({ className: `vn-crumb ${p.id === active.id ? 'active' : ''}`, title: p.node.name },
|
|
39
|
+
icon(iconForNode(p.node), { size: 13 }), span({ className: 'label' }, p.node.name))
|
|
40
|
+
.on({ click: () => w.openFile(p.node, p.openerId) })),
|
|
41
|
+
),
|
|
42
|
+
div({ className: 'vn-actions' },
|
|
43
|
+
openerSwitch(active, ui),
|
|
44
|
+
button({ className: 'iconbtn', title: 'Details & comments' }, icon('info', { size: 15 }))
|
|
45
|
+
.on({ click: () => w.toggleInfoPanel() }),
|
|
46
|
+
button({ className: 'iconbtn', title: 'Close (Esc)' }, icon('close', { size: 15 }))
|
|
47
|
+
.on({ click: () => w.showHome() }),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// "Open with…" — shown only when more than one opener can handle this file. Opens
|
|
53
|
+
// the chooser (pre-selected to the current opener) so the user can switch viewers,
|
|
54
|
+
// and optionally make the choice their default for this file type.
|
|
55
|
+
function openerSwitch(active, ui) {
|
|
56
|
+
const openers = availableOpeners(ui.platform, active.node);
|
|
57
|
+
if (openers.length <= 1) return null;
|
|
58
|
+
return button({ className: 'iconbtn', title: 'Open with…' }, icon('dots', { size: 15 }))
|
|
59
|
+
.on({ click: () => ui.platform.workbench.showDialog({ kind: 'opener-chooser', node: active.node, openers, current: active.openerId }) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openerHost(panel, ui) {
|
|
63
|
+
const key = `${panel.id}:${panel.openerId}`;
|
|
64
|
+
let fn = openerFns.get(key);
|
|
65
|
+
if (!fn) {
|
|
66
|
+
fn = () => renderOpener(panel.node, panel.openerId, ui);
|
|
67
|
+
openerFns.set(key, fn);
|
|
68
|
+
}
|
|
69
|
+
return dd.alias(fn)().key(panel.id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pruneOpeners(files) {
|
|
73
|
+
const live = new Set(files.map((p) => `${p.id}:${p.openerId}`));
|
|
74
|
+
for (const k of openerFns.keys()) if (!live.has(k)) openerFns.delete(k);
|
|
75
|
+
}
|