gesso-framework 0.2.0 → 0.3.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,105 @@
1
1
  # gesso-framework
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 025321a: **Breaking: the single-thread configuration is `createSyncApp`.** `createApp` now
8
+ builds the worker configuration and nothing else.
9
+
10
+ ```diff
11
+ -createApp(AppRoot).useChannel(Catalog, { source }).mountSync('#app');
12
+ +createSyncApp(AppRoot).useChannel(Catalog, { source }).mountSync('#app');
13
+ ```
14
+
15
+ Nothing else changes: same builder, same methods, same `mountSync`. The worker
16
+ form, `createApp({ renderWorker })` and the form `gesso-vite-plugin` writes, is
17
+ untouched, so an application that mounts into a render worker needs no edit.
18
+ `createApp` given a component throws and names `createSyncApp`, because the fix
19
+ is one identifier and a message that does not say which one turns a rename into
20
+ an afternoon.
21
+
22
+ The reason is what it costs on the wire. `createApp` took either the options or
23
+ a root component, and the overload that took a component reached
24
+ `GessoAppBuilder` → `GessoApp` → `GessoRuntime`: the layout engine, both
25
+ renderers and the hit-tester, statically, in every shell. A bundler cannot see
26
+ which half of one function a given call reaches, so every worker application
27
+ shipped the whole engine to the thread whose entire job is to create a canvas
28
+ and forward input.
29
+
30
+ Measured on the smallest honest shell, built from source:
31
+
32
+ | | raw | gzipped |
33
+ | ------ | -------- | -------- |
34
+ | before | 662.9 kB | 169.3 kB |
35
+ | after | 46.2 kB | 12.3 kB |
36
+
37
+ And on a real application, gessosheet's shell: 524.9 kB to 152.9 kB raw, 152.7 kB
38
+ to 45.0 kB gzipped. `check-bundle-size.ts` holds the line in CI with a budget and
39
+ with a look for Canvas2D calls in the shell's bytes, because the number alone
40
+ would pass a build that kept the rasterizer and got lucky.
41
+
42
+ Two names cost one line in the configuration that was always the exception, and
43
+ take 120 kB off the main thread of every other kind.
44
+
45
+ - 025321a: **`fanOut`, for when every node wants its own slice.** One source, N things on
46
+ screen, each reading one part of it: a grid, a timeline, a log viewer all arrive
47
+ at this shape, and the natural spelling does not scale. A pipe per slice runs N
48
+ pipelines on every emission whatever changed, and RxJS removes an observer from a
49
+ Subject by scanning its list, so tearing down a window of N is quadratic.
50
+
51
+ `fanOut(source, read, { initial })` holds one subscription for the whole registry
52
+ and hands out a stable cell per key. Its `changed` hint is where a frame is won: a
53
+ source that knows which keys a patch touched reads only those. Ten thousand live
54
+ keys, measured against a pipe per key: mount 29.8 ms to 10.7 ms, teardown 9.7 ms
55
+ to 3.7 ms, and one key changing 2.2 ms to 0.1 ms.
56
+
57
+ Reach for it when N is large _and each emission touches few of them_. A source
58
+ that republishes its whole window on every scroll gets the cheaper mount and
59
+ teardown and nothing from `changed`, because every key really did change.
60
+
61
+ It compares by reference where `select` and `derive` compare by content, which is
62
+ the opposite default for the opposite reason: those run once per emission and this
63
+ runs once per live key per emission. And a registry that has grown past a couple
64
+ of thousand keys having released none of them says so once, because `release` is
65
+ the caller's and forgetting it is the one thing here that goes wrong silently.
66
+
67
+ **`tabStop`, which is `tabindex="-1"`.** Focusable, reachable by a press and by
68
+ `focus()`, skipped by the Tab cycle. `focusable` only ever answered "may this node
69
+ hold focus", which is the wrong question for a container: `UiFocusManager.settleScope`
70
+ blurred when a scope held nothing focusable, so a `Dialog` whose content is a
71
+ sentence handed the keyboard to nothing and could not be dismissed with Escape.
72
+ `settleScope` now falls back to the scope root before blurring, and `Dialog` sets
73
+ `focusable: true, tabStop: false` on its body. Both halves are needed and neither
74
+ is enough alone.
75
+
76
+ **`borders()`, a border per edge, as paint.** `borderWidth` is one number and
77
+ `borderColor` one colour, so a node could not have a heavy bottom edge and a
78
+ hairline top. A border here is paint-only and a decoration is already a coloured
79
+ rectangle in the node's own paint pass, so four edges are four draw instances and
80
+ no extra nodes. `DecorationBox` gains `right` and `bottom` to put them: any two of
81
+ near edge, size and far edge fix an axis, which is CSS's rule for an absolutely
82
+ positioned box, and the only one that can express a side edge spanning between two
83
+ horizontal ones.
84
+
85
+ **`menuBarStep`, a menu bar's keyboard, as a peer of `Menu`.** `Menu` traps focus,
86
+ which is right for a popup opened by a button and wrong for a bar: with focus in
87
+ the panel, ArrowLeft cannot reach the bar to move to the menu next door, and that
88
+ is most of what makes a bar a bar. A pure function, generic in the command type,
89
+ that returns null for a key it does not claim, so a bar can still be tabbed out of.
90
+
91
+ ### Patch Changes
92
+
93
+ - Updated dependencies [025321a]
94
+ - Updated dependencies [025321a]
95
+ - gesso-core@0.3.0
96
+
97
+ ## 0.2.1
98
+
99
+ ### Patch Changes
100
+
101
+ - gesso-core@0.2.1
102
+
3
103
  ## 0.2.0
4
104
 
5
105
  ### 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