gesso-framework 0.2.1 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,201 @@
1
1
  # gesso-framework
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **The browser shells post OS file drops.** The `fileDrop` message, its shape and
8
+ the session that turns it into an ordinary drag have existed since drop targets
9
+ did, and nothing posted one: a zone accepting `gesso/files` could be written and
10
+ never reached.
11
+
12
+ `attachFileDrop` is the shell half, shared by `createApp`'s worker shell and
13
+ `createSyncApp`'s single-thread one because it is the same four DOM events
14
+ either way. It answers drags that carry files and no others, prevents the
15
+ default on every `dragover` (which is what tells a browser the drop is wanted)
16
+ and on `drop` (or the tab is replaced by the file), and reads the bytes before
17
+ posting the drop. The worker shell transfers the buffers rather than copying
18
+ them; a drop whose files cannot be read is reported as a leave, so no zone stays
19
+ lit waiting. `GessoRuntime.applyFileDrop` hands the message to the tree's drag
20
+ session.
21
+
22
+ The core half was wrong in a way no spec could have caught: `applyFileDrop` began
23
+ a drag with the files of `enter` and, on `drop`, only moved it, so a zone
24
+ received the payload the drag started with. A browser lets a page see the _types_
25
+ of dragged files and nothing else until they are let go, so that payload has
26
+ empty names and no bytes — every file dropped from a desktop would have arrived
27
+ unopenable. The spec that covered it sent the same files on both phases, which is
28
+ the one shape a browser never produces. It now sends what one does.
29
+
30
+ - **`interceptKey`, for a shortcut the app takes from the browser.**
31
+ `interceptFind` cancelled one browser default so an app's own find bar could
32
+ have Ctrl+F. An app with a Save of its own needs the same for Ctrl+S, or
33
+ Chrome's "Save page as" opens over it, and one with an Open for Ctrl+O.
34
+
35
+ The shell has to decide before the worker has heard of the key, so the decision
36
+ stays the application's and is made on the main thread: a predicate over the
37
+ `KeyboardEvent`, which can say "Ctrl or Cmd" in one line where a list could not.
38
+ The key is still forwarded; only the browser's default is cancelled. The
39
+ single-thread shell needs nothing, since its platform adapter already cancels
40
+ whatever the app's own listener claimed.
41
+
42
+ - **Files through the shell — open, save, reopen, recent.** A picker, a download
43
+ and a `FileSystemFileHandle` are all the window's, so an application in a render
44
+ worker could read a dropped file and do nothing else with files at all.
45
+ `ShellService` now asks the shell, in the shape a popup and a storage request
46
+ already have: a request with an id, and one `ShellFileResult` back on every path.
47
+
48
+ ```
49
+ openFiles showOpenFilePicker, or a file input where there is none
50
+ saveFile to a handle (Save), a picker (Save As), or a download
51
+ reopenFile a remembered file, asking permission again if it lapsed
52
+ recentFiles the remembered files, most recently used first
53
+ forgetFile stop remembering one
54
+ ```
55
+
56
+ A handle crosses as a number. The `FileSystemFileHandle` is not plain data, so
57
+ the shell keeps it — in IndexedDB, which can hold one — and a number handed out
58
+ yesterday still names yesterday's file, which is the whole of how "recent files"
59
+ survives a reload. The same file picked twice is one entry (`isSameEntry`, not
60
+ identity), and past fifty the least recently used is let go. What does not
61
+ survive a reload is the permission, and asking needs a gesture, so reopening
62
+ belongs in a click handler as much as a picker does.
63
+
64
+ `cancelled` is its own outcome because closing a picker is a decision, not a
65
+ failure; `denied` is the browser refusing; `unsupported` is a shell with no way
66
+ to do it. Without the File System Access API files come back with `handle` null
67
+ and a save is a download, and the answer says so rather than leaving the
68
+ application to feature-test.
69
+
70
+ `saveFile` takes bytes as well as text, for a file that is not text — a zip, an
71
+ image. Bytes cross the barrier on the way out as they already do on the way in,
72
+ in `ShellFile.bytes`, and are written to the handle, the picked file or the
73
+ download in place of the text when given.
74
+
75
+ ### Patch Changes
76
+
77
+ - **A key typed before the editing proxy has focus keeps its text.** The proxy
78
+ takes DOM focus when the worker reports a focused editable, a round trip; a key
79
+ typed in that gap reaches the canvas, and the text it would have made never
80
+ exists.
81
+
82
+ The `keyDown` message now says whether it came through the proxy (`textFollows`),
83
+ and a key that did not is inserted by the runtime when an editable has the focus.
84
+
85
+ Typing a word quickly into a spreadsheet cell the first letter opens kept only
86
+ the first letter.
87
+
88
+ - **A shell request made while the root is built is held, not dropped.** The root
89
+ is built in the runtime's constructor, so a component that reads a stored
90
+ preference as it mounts asked before `onShellRequest` could have been called,
91
+ and the request went nowhere — for a storage or file request, a promise that
92
+ never settled.
93
+
94
+ The runtime now holds what is asked before a listener attaches, up to 256, and
95
+ hands it over when one does.
96
+
97
+ Found by a spreadsheet's status-bar figures that never came back after a reload.
98
+
99
+ - Updated dependencies
100
+ - Updated dependencies
101
+ - Updated dependencies
102
+ - Updated dependencies
103
+ - gesso-core@0.4.0
104
+
105
+ ## 0.3.0
106
+
107
+ ### Minor Changes
108
+
109
+ - 025321a: **Breaking: the single-thread configuration is `createSyncApp`.** `createApp` now
110
+ builds the worker configuration and nothing else.
111
+
112
+ ```diff
113
+ -createApp(AppRoot).useChannel(Catalog, { source }).mountSync('#app');
114
+ +createSyncApp(AppRoot).useChannel(Catalog, { source }).mountSync('#app');
115
+ ```
116
+
117
+ Nothing else changes: same builder, same methods, same `mountSync`. The worker
118
+ form, `createApp({ renderWorker })` and the form `gesso-vite-plugin` writes, is
119
+ untouched, so an application that mounts into a render worker needs no edit.
120
+ `createApp` given a component throws and names `createSyncApp`, because the fix
121
+ is one identifier and a message that does not say which one turns a rename into
122
+ an afternoon.
123
+
124
+ The reason is what it costs on the wire. `createApp` took either the options or
125
+ a root component, and the overload that took a component reached
126
+ `GessoAppBuilder` → `GessoApp` → `GessoRuntime`: the layout engine, both
127
+ renderers and the hit-tester, statically, in every shell. A bundler cannot see
128
+ which half of one function a given call reaches, so every worker application
129
+ shipped the whole engine to the thread whose entire job is to create a canvas
130
+ and forward input.
131
+
132
+ Measured on the smallest honest shell, built from source:
133
+
134
+ | | raw | gzipped |
135
+ | ------ | -------- | -------- |
136
+ | before | 662.9 kB | 169.3 kB |
137
+ | after | 46.2 kB | 12.3 kB |
138
+
139
+ And on a real application, gessosheet's shell: 524.9 kB to 152.9 kB raw, 152.7 kB
140
+ to 45.0 kB gzipped. `check-bundle-size.ts` holds the line in CI with a budget and
141
+ with a look for Canvas2D calls in the shell's bytes, because the number alone
142
+ would pass a build that kept the rasterizer and got lucky.
143
+
144
+ Two names cost one line in the configuration that was always the exception, and
145
+ take 120 kB off the main thread of every other kind.
146
+
147
+ - 025321a: **`fanOut`, for when every node wants its own slice.** One source, N things on
148
+ screen, each reading one part of it: a grid, a timeline, a log viewer all arrive
149
+ at this shape, and the natural spelling does not scale. A pipe per slice runs N
150
+ pipelines on every emission whatever changed, and RxJS removes an observer from a
151
+ Subject by scanning its list, so tearing down a window of N is quadratic.
152
+
153
+ `fanOut(source, read, { initial })` holds one subscription for the whole registry
154
+ and hands out a stable cell per key. Its `changed` hint is where a frame is won: a
155
+ source that knows which keys a patch touched reads only those. Ten thousand live
156
+ keys, measured against a pipe per key: mount 29.8 ms to 10.7 ms, teardown 9.7 ms
157
+ to 3.7 ms, and one key changing 2.2 ms to 0.1 ms.
158
+
159
+ Reach for it when N is large _and each emission touches few of them_. A source
160
+ that republishes its whole window on every scroll gets the cheaper mount and
161
+ teardown and nothing from `changed`, because every key really did change.
162
+
163
+ It compares by reference where `select` and `derive` compare by content, which is
164
+ the opposite default for the opposite reason: those run once per emission and this
165
+ runs once per live key per emission. And a registry that has grown past a couple
166
+ of thousand keys having released none of them says so once, because `release` is
167
+ the caller's and forgetting it is the one thing here that goes wrong silently.
168
+
169
+ **`tabStop`, which is `tabindex="-1"`.** Focusable, reachable by a press and by
170
+ `focus()`, skipped by the Tab cycle. `focusable` only ever answered "may this node
171
+ hold focus", which is the wrong question for a container: `UiFocusManager.settleScope`
172
+ blurred when a scope held nothing focusable, so a `Dialog` whose content is a
173
+ sentence handed the keyboard to nothing and could not be dismissed with Escape.
174
+ `settleScope` now falls back to the scope root before blurring, and `Dialog` sets
175
+ `focusable: true, tabStop: false` on its body. Both halves are needed and neither
176
+ is enough alone.
177
+
178
+ **`borders()`, a border per edge, as paint.** `borderWidth` is one number and
179
+ `borderColor` one colour, so a node could not have a heavy bottom edge and a
180
+ hairline top. A border here is paint-only and a decoration is already a coloured
181
+ rectangle in the node's own paint pass, so four edges are four draw instances and
182
+ no extra nodes. `DecorationBox` gains `right` and `bottom` to put them: any two of
183
+ near edge, size and far edge fix an axis, which is CSS's rule for an absolutely
184
+ positioned box, and the only one that can express a side edge spanning between two
185
+ horizontal ones.
186
+
187
+ **`menuBarStep`, a menu bar's keyboard, as a peer of `Menu`.** `Menu` traps focus,
188
+ which is right for a popup opened by a button and wrong for a bar: with focus in
189
+ the panel, ArrowLeft cannot reach the bar to move to the menu next door, and that
190
+ is most of what makes a bar a bar. A pure function, generic in the command type,
191
+ that returns null for a key it does not claim, so a bar can still be tabbed out of.
192
+
193
+ ### Patch Changes
194
+
195
+ - Updated dependencies [025321a]
196
+ - Updated dependencies [025321a]
197
+ - gesso-core@0.3.0
198
+
3
199
  ## 0.2.1
4
200
 
5
201
  ### Patch Changes
package/README.md CHANGED
@@ -70,7 +70,7 @@ The shell spawns both workers, joins them with one `MessageChannel`, hands each
70
70
 
71
71
  A component body runs **once**, and an `Observable` binds straight into the retained graph. So there is no re-render pass, no virtual DOM diff, no `useEffect`, and no base class to extend for your state. Component identity _is_ node identity, so there is no second reconciler.
72
72
 
73
- Single-thread mode exists for tests, headless rendering and environments without `OffscreenCanvas`: `createApp(NotesApp).useChannel(Notes, { source }).mountSync('#app')`. Channels resolve in-process there, so the same contract runs with no ports.
73
+ Single-thread mode exists for tests, headless rendering and environments without `OffscreenCanvas`: `createSyncApp(NotesApp).useChannel(Notes, { source }).mountSync('#app')`. Channels resolve in-process there, so the same contract runs with no ports.
74
74
 
75
75
  ## Entry points
76
76