@3sln/trove 0.0.7 → 0.0.9
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/package.json +1 -1
- package/packages/core/src/collections/index.js +146 -1
- package/packages/core/src/encryption/envelope.js +483 -0
- package/packages/core/src/encryption/exposure.js +101 -0
- package/packages/core/src/encryption/keys.js +88 -0
- package/packages/core/src/encryption/policy.js +112 -0
- package/packages/core/src/encryption/rotation.js +432 -0
- package/packages/core/src/index.js +17 -1
- package/packages/core/src/links.js +85 -0
- package/packages/core/src/metadata/memory.js +3 -1
- package/packages/core/src/metadata/sqlite.js +27 -7
- package/packages/core/src/scan.js +35 -1
- package/packages/core/src/storage/cost.js +228 -0
- package/packages/core/src/uploads.js +302 -19
- package/packages/core/src/vfs.js +161 -10
- package/packages/server/src/adapters/staticAssets.js +14 -5
- package/packages/server/src/engine/index.js +1 -1
- package/packages/server/src/engine/providers/core.js +21 -2
- package/packages/server/src/index.js +45 -2
- package/packages/server/src/routes.js +62 -1
- package/packages/web/dist/assets/main-jg5vmp8f.js +356 -0
- package/packages/web/dist/assets/main-jg5vmp8f.js.map +118 -0
- package/packages/web/dist/assets/styles-e5gk19rn.css +1 -0
- package/packages/web/dist/index.html +9 -3
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/src/bl/actions.js +1471 -138
- package/packages/web/src/bl/commands.js +103 -284
- package/packages/web/src/bl/context.js +69 -0
- package/packages/web/src/bl/index.js +136 -40
- package/packages/web/src/bl/intern.js +129 -0
- package/packages/web/src/bl/launcher.js +251 -0
- package/packages/web/src/bl/links.js +1 -1
- package/packages/web/src/bl/match.js +71 -0
- package/packages/web/src/bl/mentions.js +39 -0
- package/packages/web/src/bl/offline.js +0 -4
- package/packages/web/src/bl/openers.js +65 -33
- package/packages/web/src/bl/pluginInstall.js +42 -21
- package/packages/web/src/bl/queries.js +669 -0
- package/packages/web/src/bl/services.js +141 -145
- package/packages/web/src/bl/social.js +3 -1
- package/packages/web/src/bl/state.js +144 -0
- package/packages/web/src/bl/status.js +96 -0
- package/packages/web/src/bl/trust.js +109 -0
- package/packages/web/src/bl/viewState.js +60 -0
- package/packages/web/src/bl/views.js +64 -0
- package/packages/web/src/bl/watchQuery.js +73 -0
- package/packages/web/src/platform/api.js +63 -11
- package/packages/web/src/platform/commands.js +49 -9
- package/packages/web/src/platform/context.js +146 -51
- package/packages/web/src/platform/index.js +27 -20
- package/packages/web/src/platform/navigation.js +34 -4
- package/packages/web/src/platform/pickers.js +46 -0
- package/packages/web/src/platform/pluginHost.js +43 -10
- package/packages/web/src/platform/pluginRpc.js +6 -2
- package/packages/web/src/platform/spatialNav.js +0 -2
- package/packages/web/src/platform/viewport.js +1 -6
- package/packages/web/src/platform/voiceSearch.js +1 -3
- package/packages/web/src/platform/whenclause.js +22 -3
- package/packages/web/src/styles.css +77 -4
- package/packages/web/src/ui/activate.js +22 -0
- package/packages/web/src/ui/components/activityBar.js +7 -3
- package/packages/web/src/ui/components/activityPanel.js +10 -11
- package/packages/web/src/ui/components/adminView.js +191 -0
- package/packages/web/src/ui/components/collectionGate.js +4 -3
- package/packages/web/src/ui/components/commandPalette.js +24 -55
- package/packages/web/src/ui/components/editorArea.js +16 -13
- package/packages/web/src/ui/components/launcher.js +39 -225
- package/packages/web/src/ui/components/openers/index.js +7 -4
- package/packages/web/src/ui/components/openers/markdown.js +5 -3
- package/packages/web/src/ui/components/overlays.js +169 -43
- package/packages/web/src/ui/components/phoneChrome.js +35 -33
- package/packages/web/src/ui/components/pluginReview.js +62 -23
- package/packages/web/src/ui/components/pluginsView.js +30 -28
- package/packages/web/src/ui/components/settingsView.js +151 -58
- package/packages/web/src/ui/components/social.js +42 -62
- package/packages/web/src/ui/components/statusBar.js +30 -74
- package/packages/web/src/ui/components/views/grid.js +3 -2
- package/packages/web/src/ui/components/views/index.js +7 -62
- package/packages/web/src/ui/components/views/list.js +3 -2
- package/packages/web/src/ui/components/views/parts.js +15 -4
- package/packages/web/src/ui/compositions/workbench.js +166 -58
- package/packages/web/src/ui/region.js +53 -0
- package/packages/web/src/workbench.js +17 -15
- package/packages/web/dist/assets/main-f0f2tfhp.js +0 -356
- package/packages/web/dist/assets/main-f0f2tfhp.js.map +0 -104
- package/packages/web/dist/assets/styles-d3cyysgp.css +0 -1
- package/packages/web/src/platform/overlay.js +0 -81
- package/packages/web/src/platform/workbench.js +0 -156
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
// Root composition. Assembles the `ui` helper (stable for the app's life),
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Root composition. Assembles the `ui` helper (stable for the app's life), zips every
|
|
2
|
+
// query the shell reads into one snapshot, and renders the workbench from it.
|
|
3
|
+
//
|
|
4
|
+
// Components are pure: they receive (state, ui) and dispatch. This module knows about
|
|
5
|
+
// every QUERY at once — which is a different thing from what it used to know, when it
|
|
6
|
+
// held the bag of services every component reached through.
|
|
6
7
|
|
|
7
|
-
import { dd,
|
|
8
|
-
import {
|
|
8
|
+
import { dd, derive, constant, watch } from '../../runtime.js';
|
|
9
|
+
import { region } from '../region.js';
|
|
10
|
+
import { watchQuery } from '../../bl/watchQuery.js';
|
|
11
|
+
import * as q from '../../bl/queries.js';
|
|
12
|
+
import { CloseSearchModalAction, ExecCommandAction, NavigateAction } from '../../bl/actions.js';
|
|
9
13
|
import activityBar from '../components/activityBar.js';
|
|
10
14
|
import statusBar from '../components/statusBar.js';
|
|
11
15
|
import launcher from '../components/launcher.js';
|
|
12
16
|
import settingsView from '../components/settingsView.js';
|
|
13
17
|
import collectionGate from '../components/collectionGate.js';
|
|
14
18
|
import pluginsView from '../components/pluginsView.js';
|
|
19
|
+
import adminView from '../components/adminView.js';
|
|
15
20
|
import editorArea from '../components/editorArea.js';
|
|
16
21
|
import commandPalette from '../components/commandPalette.js';
|
|
17
22
|
import { dialog, contextMenu, toasts, transferTray, pluginPanel } from '../components/overlays.js';
|
|
@@ -21,89 +26,192 @@ import { phoneTopBar, phoneBottomBar, phoneSheet } from '../components/phoneChro
|
|
|
21
26
|
|
|
22
27
|
const { alias, div } = dd;
|
|
23
28
|
|
|
24
|
-
export default function workbench({ engine,
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
29
|
+
export default function workbench({ engine, platform }) {
|
|
30
|
+
// What a component is handed, and it is nearly nothing now.
|
|
31
|
+
//
|
|
32
|
+
// `engine` is the thing components talk to: `dispatch` for an action, `query` for a
|
|
33
|
+
// question. It was removed once for being unreferenced, which was true and backwards —
|
|
34
|
+
// nothing referenced it precisely BECAUSE `go` and `exec` stood in for it, and `exec`
|
|
35
|
+
// called the command service directly, so every menu item, keybinding and palette entry
|
|
36
|
+
// a person triggered was invisible to the engine.
|
|
37
|
+
//
|
|
38
|
+
// Both are gone. `go(action)` was `engine.dispatch(action)` with a shorter name, and a
|
|
39
|
+
// shorter name for the one door is how the door stops being obvious. Call sites say
|
|
40
|
+
// `ui.engine.dispatch(new SomeAction(...))` now: longer, and it names what happens.
|
|
41
|
+
//
|
|
42
|
+
// `platform` is still passed, for four uses that are genuinely imperative: two plugin
|
|
43
|
+
// iframe mounts, the media URL machinery, and the opener lookup in openers/index.js —
|
|
44
|
+
// which stays because an opener may resolve to a plugin iframe rather than to a render
|
|
45
|
+
// function, and mounting one is an effect, not a view.
|
|
46
|
+
//
|
|
47
|
+
// Note what is NOT a reason: carrying a function. A query hands over the view renderers
|
|
48
|
+
// (see the `views` query) because a renderer is pure — it builds a vnode from what it is
|
|
49
|
+
// given. The rule is about SIDE EFFECTS escaping through a callable, not about callables.
|
|
50
|
+
//
|
|
51
|
+
// See docs/tickets/009. `app` used to be here too — the whole bag of services — and is
|
|
52
|
+
// gone: nothing in `ui/` reads a service directly any more.
|
|
28
53
|
const ui = {
|
|
29
|
-
engine,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
rerender: () => bump.update((n) => n + 1),
|
|
33
|
-
uninstallPlugin: (id) => plugins?.uninstall(id),
|
|
54
|
+
engine, platform,
|
|
55
|
+
// Rendering a cell is a UI concern, not a platform subsystem to reach through.
|
|
56
|
+
watch,
|
|
34
57
|
};
|
|
35
58
|
|
|
36
|
-
const { watch } = platform.reactive;
|
|
37
59
|
// One derived snapshot of every slice the shell reads. `derive` invalidates when any
|
|
38
60
|
// of them does and recomputes once on the next read, so fifteen changes in a frame
|
|
39
61
|
// still cost one render.
|
|
40
62
|
const combined = derive(
|
|
63
|
+
// EVERY input is a query. Half of these used to read the service cell directly, which
|
|
64
|
+
// looked equivalent and was not: a query may PROJECT, and the projection is where
|
|
65
|
+
// `offline.pinnedIds` and `voice.canListen` come from. Reading the raw cell got the
|
|
66
|
+
// service's state without them, so a pinned file's menu still offered to pin it and the
|
|
67
|
+
// microphone button never appeared — silently, because an undefined field just reads as
|
|
68
|
+
// false. Going through the engine uniformly is what makes "the same slice" mean the
|
|
69
|
+
// same thing everywhere.
|
|
41
70
|
[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
watchQuery(engine, q.workbench),
|
|
72
|
+
watchQuery(engine, q.overlay),
|
|
73
|
+
watchQuery(engine, q.navigation),
|
|
74
|
+
watchQuery(engine, q.explorer),
|
|
75
|
+
watchQuery(engine, q.search),
|
|
76
|
+
watchQuery(engine, q.context),
|
|
77
|
+
watchQuery(engine, q.settings),
|
|
78
|
+
watchQuery(engine, q.offline),
|
|
79
|
+
watchQuery(engine, q.viewport),
|
|
80
|
+
watchQuery(engine, q.voice),
|
|
81
|
+
// What the UI is in the middle of doing — see bl/viewState.js.
|
|
82
|
+
watchQuery(engine, q.viewState),
|
|
83
|
+
// What the server can do. Its resource hands out a cell that starts null and fills
|
|
84
|
+
// in, so this is a normal value while the request is in flight rather than turning
|
|
85
|
+
// the whole snapshot PENDING and blanking the shell.
|
|
86
|
+
watchQuery(engine, q.capabilities),
|
|
87
|
+
// Command id -> its shortcut label, for anything that shows one next to an action.
|
|
88
|
+
watchQuery(engine, q.commandKeys),
|
|
89
|
+
// What the launcher shows, twice: the home screen and the double-shift overlay are
|
|
90
|
+
// both mounted when the overlay is up, and they differ in what picking a row does —
|
|
91
|
+
// the overlay resets the viewer stack and then dismisses itself. That is the only
|
|
92
|
+
// input that is about which INSTANCE is rendering, so it is the query's one
|
|
93
|
+
// parameter, and interning means at most two live realizations.
|
|
94
|
+
watchQuery(engine, q.launcherContent(false)),
|
|
95
|
+
watchQuery(engine, q.launcherContent(true)),
|
|
96
|
+
// How results can be drawn, and every opener that could run. Both used to be worked
|
|
97
|
+
// out mid-render from `platform` — three registry reads that nothing invalidated on,
|
|
98
|
+
// so the launcher and the viewer nav only kept up because this snapshot is coarse
|
|
99
|
+
// enough to redraw them anyway. As a query the dependency is declared, which is what
|
|
100
|
+
// makes it safe to give either of them a region later.
|
|
101
|
+
watchQuery(engine, q.views),
|
|
102
|
+
watchQuery(engine, q.openers),
|
|
60
103
|
],
|
|
61
|
-
// `_bump`
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
(wb, overlay, nav, ex, se,
|
|
66
|
-
|
|
104
|
+
// No `_bump` any more. It existed to defeat `watch`'s shallow-equality check for a
|
|
105
|
+
// forced re-render that left no trace in the snapshot — a counter smuggled into the
|
|
106
|
+
// state to get past an optimisation designed to skip pointless renders. With the state
|
|
107
|
+
// that needed it in a cell, every render has a reason again.
|
|
108
|
+
(wb, overlay, nav, ex, se, ctx, settings, off, vp, voice, view, caps, commandKeys,
|
|
109
|
+
content, modalContent, views, openers) =>
|
|
110
|
+
({ wb, overlay, nav, ex, se, ctx, settings, off, vp, voice, view, caps, commandKeys,
|
|
111
|
+
content, modalContent, views, openers }),
|
|
67
112
|
);
|
|
68
113
|
|
|
69
|
-
|
|
114
|
+
// The chrome, subscribed to what it reads instead of to everything.
|
|
115
|
+
//
|
|
116
|
+
// These four render the same facts in different shapes — `statusFacts` is shared between
|
|
117
|
+
// them — so they share a query set. Splitting them out is what takes `tr` off the main
|
|
118
|
+
// snapshot: an upload progress tick used to invalidate the one watch that builds the whole
|
|
119
|
+
// shell, so moving one number in the transfer tray reconstructed every row of the file
|
|
120
|
+
// list beneath it. Now it reaches the chrome and the tray, and nothing else.
|
|
121
|
+
const chrome = {
|
|
122
|
+
ex: q.explorer, tr: q.transfers, act: q.activity, off: q.offline,
|
|
123
|
+
so: q.social, wb: q.workbench, statusItems: q.statusItems, caps: q.capabilities,
|
|
124
|
+
// The numbers and the one-line verdict both shells report. Derived by the query rather
|
|
125
|
+
// than by the status bar and imported from there by the phone chrome, which is what it
|
|
126
|
+
// was — a business-layer derivation living in a component. See bl/status.js.
|
|
127
|
+
facts: q.statusFacts,
|
|
128
|
+
};
|
|
129
|
+
// A query boots asynchronously — it awaits a container lease first — so a region is
|
|
130
|
+
// PENDING for the first frame or two and `watch` renders its placeholder. For a bar with
|
|
131
|
+
// a fixed height that has to be an empty bar rather than nothing, or the layout jumps
|
|
132
|
+
// once on load and settles.
|
|
133
|
+
const regions = {
|
|
134
|
+
statusBar: region(engine, chrome, (s) => statusBar(s, ui), { placeholder: () => div({ className: 'statusbar' }) }),
|
|
135
|
+
phoneTopBar: region(engine, chrome, (s) => phoneTopBar(s, ui), { placeholder: () => div({ className: 'phonebar top' }) }),
|
|
136
|
+
phoneBottomBar: region(engine, chrome, (s) => phoneBottomBar(s, ui), { placeholder: () => div({ className: 'phonebar bottom' }) }),
|
|
137
|
+
// Both render nothing when there is nothing to show, so an absent first frame is what
|
|
138
|
+
// they would have drawn anyway.
|
|
139
|
+
phoneSheet: region(engine, chrome, (s) => phoneSheet(s, ui)),
|
|
140
|
+
transferTray: region(engine, { tr: q.transfers }, (s) => transferTray(s, ui)),
|
|
141
|
+
// Both read one slice each, and both change often enough to matter: a toast arriving
|
|
142
|
+
// or auto-dismissing used to rebuild the entire shell — including every row of the
|
|
143
|
+
// file list — to add a line in the corner.
|
|
144
|
+
toasts: region(engine, { notif: q.notifications }, (s) => toasts(s, ui)),
|
|
145
|
+
activityPanel: region(engine, { act: q.activity }, (s) => activityPanel(s, ui)),
|
|
146
|
+
// Screens that are usually NOT on screen. Their state still invalidated the shell from
|
|
147
|
+
// wherever they were: minting an API key or a plugin reporting in rebuilt the file
|
|
148
|
+
// list. Each reads a couple of slices, so each is a cheap region.
|
|
149
|
+
settingsView: region(engine, {
|
|
150
|
+
settings: q.settings, settingsGroups: q.settingsGroups, keys: q.apiKeys,
|
|
151
|
+
caps: q.capabilities, ex: q.explorer, assoc: q.openerAssociations,
|
|
152
|
+
}, (s) => settingsView(s, ui)),
|
|
153
|
+
pluginsView: region(engine, { plugins: q.plugins, settings: q.settings },
|
|
154
|
+
(s) => pluginsView(s, ui)),
|
|
155
|
+
// Gathers what already exists rather than fetching anything new — see adminView.js.
|
|
156
|
+
adminView: region(engine, {
|
|
157
|
+
caps: q.capabilities, ex: q.explorer, plugins: q.plugins, act: q.activity, so: q.social,
|
|
158
|
+
}, (s) => adminView(s, ui)),
|
|
159
|
+
infoPanel: region(engine, { so: q.social, off: q.offline, nav: q.navigation },
|
|
160
|
+
(s) => infoPanel(s, ui)),
|
|
161
|
+
// The rail. It renders the notification bell and the identity chip, both of which read
|
|
162
|
+
// the social slice — which is how `so` was still reaching the shell after infoPanel
|
|
163
|
+
// moved out. Missed by looking at infoPanel alone; the bell is imported INTO the bar
|
|
164
|
+
// from social.js, so a grep for the component name does not find it.
|
|
165
|
+
activityBar: region(engine, { wb: q.workbench, plugins: q.plugins, so: q.social },
|
|
166
|
+
(s) => activityBar(s, ui)),
|
|
167
|
+
// The palette is the first thing to read a composed view rather than a service's state:
|
|
168
|
+
// `commands` arrives with keybinding labels resolved and availability decided, so the
|
|
169
|
+
// component stopped asking the keybinding and command services anything mid-render.
|
|
170
|
+
// `matches` is the list already ranked against what has been typed — the palette does
|
|
171
|
+
// no scoring of its own any more. See bl/match.js for why there was more than one.
|
|
172
|
+
commandPalette: region(engine, { overlay: q.overlay, se: q.search, matches: q.paletteMatches },
|
|
173
|
+
(s) => commandPalette(s, ui)),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return alias(() => watch(combined, (state) => view(state, ui, regions)));
|
|
70
177
|
}
|
|
71
178
|
|
|
72
|
-
function view(state, ui) {
|
|
179
|
+
function view(state, ui, regions) {
|
|
73
180
|
const mode = state.vp?.mode || 'desktop';
|
|
74
181
|
const phone = mode === 'phone';
|
|
75
182
|
return div({ className: `shell ${mode} ${state.settings['workbench.density'] === 'compact' ? 'compact' : ''}` },
|
|
76
183
|
// On a phone the rail becomes a bottom bar and the status bar folds behind one icon
|
|
77
184
|
// in a top bar — see phoneChrome. Everything between them is identical, which is the
|
|
78
185
|
// point: only the chrome changes shape, not the app.
|
|
79
|
-
phone ? phoneTopBar(
|
|
186
|
+
phone ? regions.phoneTopBar() : null,
|
|
80
187
|
div({ className: 'body' },
|
|
81
|
-
phone ? null : activityBar(
|
|
82
|
-
mainArea(state, ui),
|
|
188
|
+
phone ? null : regions.activityBar(),
|
|
189
|
+
mainArea(state, ui, regions),
|
|
83
190
|
),
|
|
84
|
-
phone ? phoneBottomBar(
|
|
85
|
-
phone ? phoneSheet(
|
|
191
|
+
phone ? regions.phoneBottomBar() : regions.statusBar(),
|
|
192
|
+
phone ? regions.phoneSheet() : null,
|
|
86
193
|
// Overlays.
|
|
87
194
|
searchModal(state, ui),
|
|
88
|
-
commandPalette(
|
|
195
|
+
regions.commandPalette(),
|
|
89
196
|
dialog(state, ui),
|
|
90
197
|
contextMenu(state, ui),
|
|
91
198
|
pluginPanel(state, ui),
|
|
92
|
-
toasts(
|
|
93
|
-
transferTray(
|
|
94
|
-
activityPanel(
|
|
199
|
+
regions.toasts(),
|
|
200
|
+
regions.transferTray(),
|
|
201
|
+
regions.activityPanel(),
|
|
95
202
|
);
|
|
96
203
|
}
|
|
97
204
|
|
|
98
|
-
function mainArea(state, ui) {
|
|
205
|
+
function mainArea(state, ui, regions) {
|
|
99
206
|
// Before a collection is known there is nothing to draw: every scoped request names one
|
|
100
207
|
// in its path, so a file list, a search box and an Upload button would all be lying.
|
|
101
208
|
// Settings stays reachable — an admin with no collections still needs to get at it.
|
|
102
209
|
if (state.ex?.gate && state.wb.activity !== 'settings') return collectionGate(state, ui);
|
|
103
210
|
|
|
104
211
|
switch (state.wb.activity) {
|
|
105
|
-
case 'settings': return settingsView(
|
|
106
|
-
case 'plugins': return pluginsView(
|
|
212
|
+
case 'settings': return regions.settingsView();
|
|
213
|
+
case 'plugins': return regions.pluginsView();
|
|
214
|
+
case 'admin': return regions.adminView();
|
|
107
215
|
default: {
|
|
108
216
|
// Home: the top of the panel stack — the launcher (base search) or, once a
|
|
109
217
|
// file is open, its opener full-width (optionally split with the info panel).
|
|
@@ -113,8 +221,8 @@ function mainArea(state, ui) {
|
|
|
113
221
|
// details panel takes the whole panel instead — it has its own close button, so
|
|
114
222
|
// there is still a way back to the file.
|
|
115
223
|
return state.vp?.mode === 'phone'
|
|
116
|
-
? infoPanel(
|
|
117
|
-
: div({ className: 'editor-split' }, editorArea(state, ui), infoPanel(
|
|
224
|
+
? regions.infoPanel()
|
|
225
|
+
: div({ className: 'editor-split' }, editorArea(state, ui), regions.infoPanel());
|
|
118
226
|
}
|
|
119
227
|
}
|
|
120
228
|
}
|
|
@@ -124,7 +232,7 @@ function mainArea(state, ui) {
|
|
|
124
232
|
function searchModal(state, ui) {
|
|
125
233
|
if (!state.wb.searchModal) return div();
|
|
126
234
|
return div({ className: 'search-modal' },
|
|
127
|
-
div({ className: 'scrim' }).on({ click: () => ui.
|
|
235
|
+
div({ className: 'scrim' }).on({ click: () => ui.engine.dispatch(new CloseSearchModalAction()) }),
|
|
128
236
|
div({ className: 'search-modal-panel' }, launcher(state, ui, { modal: true })),
|
|
129
237
|
);
|
|
130
238
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A region: one part of the shell, subscribed to only the queries it reads.
|
|
2
|
+
//
|
|
3
|
+
// The workbench is a single `watch` over a `derive` of eighteen cells, so any change to any
|
|
4
|
+
// of them rebuilds the whole VDOM — activity bar, status bar, every overlay, the file list.
|
|
5
|
+
// dodo reconciles, so the DOM update stays small; the REBUILD does not. An upload progress
|
|
6
|
+
// tick reconstructs all 500 rows of a file list to move one number in the transfer tray.
|
|
7
|
+
//
|
|
8
|
+
// A region fixes that by narrowing the subscription instead of the render: it watches the
|
|
9
|
+
// queries it actually reads, and a change to anything else never reaches it. That also makes
|
|
10
|
+
// the dependency legible — the region's queries are its argument list, rather than something
|
|
11
|
+
// you infer by reading which snapshot keys the body touches.
|
|
12
|
+
//
|
|
13
|
+
// The teardown matters as much as the render. A region that leaves the tree drops its
|
|
14
|
+
// watchers; dodo disconnects the cells; ngin kills the query realizations, releasing their
|
|
15
|
+
// leases and whatever they held open. That is what makes a query the right home for
|
|
16
|
+
// something like a minted media URL — alive while a region is looking at it, gone when it
|
|
17
|
+
// stops — with no component writing cleanup.
|
|
18
|
+
//
|
|
19
|
+
// See docs/tickets/009.
|
|
20
|
+
|
|
21
|
+
import { dd, watch, derive } from '../runtime.js';
|
|
22
|
+
import { watchQuery } from '../bl/watchQuery.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} engine
|
|
26
|
+
* @param {Record<string, object>} queries name → SHARED query instance (see bl/queries.js)
|
|
27
|
+
* @param {(state: Record<string, any>) => any} build the region's view; close over `ui`
|
|
28
|
+
* @param {{placeholder?: () => any, error?: (err: Error) => any}} [opts]
|
|
29
|
+
* @returns {() => any} a component, built ONCE — see below for why that matters
|
|
30
|
+
*/
|
|
31
|
+
export function region(engine, queries, build, opts) {
|
|
32
|
+
const names = Object.keys(queries);
|
|
33
|
+
// Resolved at composition, not per render. `watchQuery` caches per (engine, query) so
|
|
34
|
+
// these would be the same cells either way, but doing it here says the subscription
|
|
35
|
+
// belongs to the region rather than being re-established on every pass.
|
|
36
|
+
const cells = names.map((name) => watchQuery(engine, queries[name]));
|
|
37
|
+
// `derive` coalesces: several of these changing in one frame still costs one render.
|
|
38
|
+
const combined = derive(cells, (...values) =>
|
|
39
|
+
Object.fromEntries(names.map((name, i) => [name, values[i]])));
|
|
40
|
+
|
|
41
|
+
// ONE alias function, and one options object, for the life of the app.
|
|
42
|
+
//
|
|
43
|
+
// dodo identifies an alias by the FUNCTION: "swapping the alias function ... is a
|
|
44
|
+
// different component, and reusing the state would skip the old one's detach and the new
|
|
45
|
+
// one's attach". So `alias(...)` built inside the render would detach and re-attach the
|
|
46
|
+
// watch — dropping the subscription, killing the query realization and booting a fresh
|
|
47
|
+
// one — on every pass of the parent. That is the precise opposite of what a region is
|
|
48
|
+
// for, and it would have looked like it worked.
|
|
49
|
+
//
|
|
50
|
+
// `build` and `opts` are hoisted for the same reason one level down: `watch` compares its
|
|
51
|
+
// arguments, and a builder closure rebuilt per render is a new argument every time.
|
|
52
|
+
return dd.alias(() => watch(combined, build, opts));
|
|
53
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { dd, effect } from './runtime.js';
|
|
30
30
|
import { createPlatform } from './platform/index.js';
|
|
31
31
|
import { createApp } from './bl/index.js';
|
|
32
|
-
import { NavigateAction, UploadFilesAction, OpenInitialCollectionAction } from './bl/actions.js';
|
|
32
|
+
import { NavigateAction, UploadFilesAction, OpenInitialCollectionAction, OpenSearchModalAction, OpenInPanelAction } from './bl/actions.js';
|
|
33
33
|
import { parsePackage } from './platform/pluginPackage.js';
|
|
34
34
|
import workbenchComposition from './ui/compositions/workbench.js';
|
|
35
35
|
import { registerBuiltinOpeners } from './ui/components/openers/index.js';
|
|
@@ -75,10 +75,8 @@ export function createWorkbench({
|
|
|
75
75
|
// restore any that were installed on this device, then run.
|
|
76
76
|
platform.plugins.restore();
|
|
77
77
|
|
|
78
|
-
const plugins = { uninstall: (id) => platform.plugins.uninstall(id) };
|
|
79
|
-
|
|
80
78
|
// --- mount ----------------------------------------------------------------
|
|
81
|
-
const App = workbenchComposition({ engine,
|
|
79
|
+
const App = workbenchComposition({ engine, platform });
|
|
82
80
|
dd.reconcile(root, [App()]);
|
|
83
81
|
|
|
84
82
|
// --- theme ----------------------------------------------------------------
|
|
@@ -103,8 +101,10 @@ export function createWorkbench({
|
|
|
103
101
|
platform.keybindings.install(window);
|
|
104
102
|
|
|
105
103
|
// --- viewer stack ↔ browser history ---------------------------------------
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
// Browser back/forward. The panel stack is the only thing that mirrors itself into
|
|
105
|
+
// history, so this reaches it directly rather than through a shell object.
|
|
106
|
+
window.addEventListener('popstate', (e) => app.navigation.onPopState(e));
|
|
107
|
+
installDoubleShift(engine);
|
|
108
108
|
|
|
109
109
|
// --- service worker (offline shell + pinned files + push) -----------------
|
|
110
110
|
if (serviceWorker && 'serviceWorker' in navigator) {
|
|
@@ -116,12 +116,11 @@ export function createWorkbench({
|
|
|
116
116
|
// --- initial load ---------------------------------------------------------
|
|
117
117
|
(async () => {
|
|
118
118
|
try {
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
platform.workbench.touch(); // the status bar reads capabilities
|
|
119
|
+
// What the server can do is an engine query now — see bl/queries.js. It used to be
|
|
120
|
+
// assigned onto `platform` here and followed by `workbench.touch()`, a poke at an
|
|
121
|
+
// unrelated store purely to make the shell redraw, because writing a plain field
|
|
122
|
+
// invalidates nothing. The query has somewhere for the value to arrive, so the fetch
|
|
123
|
+
// belongs to it and this no longer has to announce it.
|
|
125
124
|
} catch (err) {
|
|
126
125
|
platform.notifications.error(`Cannot reach the Trove server: ${err.message}`);
|
|
127
126
|
}
|
|
@@ -165,12 +164,12 @@ function register(platform, type, list) {
|
|
|
165
164
|
|
|
166
165
|
// Double-shift → modal search overlay (Raycast-style). Two Shift presses within 400ms,
|
|
167
166
|
// with no other key between, open it.
|
|
168
|
-
function installDoubleShift(
|
|
167
|
+
function installDoubleShift(engine) {
|
|
169
168
|
let lastShift = 0;
|
|
170
169
|
window.addEventListener('keydown', (e) => {
|
|
171
170
|
if (e.key === 'Shift' && !e.repeat) {
|
|
172
171
|
const now = Date.now();
|
|
173
|
-
if (now - lastShift < 400) {
|
|
172
|
+
if (now - lastShift < 400) { engine.dispatch(new OpenSearchModalAction()); lastShift = 0; }
|
|
174
173
|
else lastShift = now;
|
|
175
174
|
} else if (e.key !== 'Shift') {
|
|
176
175
|
lastShift = 0;
|
|
@@ -200,6 +199,9 @@ function installDragAndDrop(engine, app) {
|
|
|
200
199
|
e.preventDefault();
|
|
201
200
|
dragDepth = 0;
|
|
202
201
|
document.body.classList.remove('dragging-file');
|
|
203
|
-
|
|
202
|
+
// No collection named: UploadFilesAction resolves the open one itself and refuses
|
|
203
|
+
// visibly when there isn't one. Reading it here only to hand it straight back was the
|
|
204
|
+
// drop target knowing where files go, which is the action's business.
|
|
205
|
+
engine.dispatch(new UploadFilesAction(e.dataTransfer.files));
|
|
204
206
|
});
|
|
205
207
|
}
|