@jarenjs/app 0.72.0 → 0.72.3
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 +9 -0
- package/dist/types/docstore.d.ts +3 -2
- package/dist/types/splitter.d.ts +3 -1
- package/package.json +4 -4
- package/src/docstore.js +9 -4
- package/src/splitter.js +41 -10
package/README.md
CHANGED
|
@@ -168,12 +168,21 @@ Jaren owns state and orchestration; the widget owns its DOM. Its `props` come fr
|
|
|
168
168
|
Two IDE-shaped primitives ship ready to bind, so a two-pane surface (the studio, the play playground) doesn't re-implement them:
|
|
169
169
|
|
|
170
170
|
- **`createSplitterWidget({ action, grid, rail, cssVar, min, max, step })`** — a drag handle over a pane boundary. It drives a CSS ratio variable *live* during a drag (no per-move dispatch — that would flood the transaction log and undo) and commits the ratio through `action` on pointer-up only, plus keyboard resize as an ARIA separator. Register it like any widget; parameterize the grid/rail selectors, the CSS variable and the commit action so each surface binds its own.
|
|
171
|
+
|
|
172
|
+
Widget props accept `axis: 'x' | 'y'` (default `'x'`) for side-by-side or
|
|
173
|
+
stacked panes. The widget also writes `${cssVar}-first` and
|
|
174
|
+
`${cssVar}-rest` as fractional grid tracks. Pointer cancellation restores
|
|
175
|
+
the starting ratio without committing; vertical resizing uses Up/Down.
|
|
171
176
|
- **`createDocStore({ storage, key })`** — a keyed `save`/`load`/`remove`/`names`/`all` CRUD over an injected `storage` (`localStorage` in the browser, an in-memory object in tests), so the package never touches `localStorage` itself. Paired with **`encodeShare(snapshot)`** / **`decodeShare(token)`**, a Unicode-safe base64url share-link codec (a corrupt token decodes to `null`, never a throw), it is the new/save/load/delete/share pattern behind the studio and play surfaces.
|
|
172
177
|
|
|
173
178
|
Collection keys and document names such as `__proto__` are ordinary own
|
|
174
179
|
members of the document store and survive persistence. Loading a name
|
|
175
180
|
that has not been saved returns `undefined`, including prototype-member names.
|
|
176
181
|
|
|
182
|
+
Stores created with the same storage adapter share one in-memory root, read
|
|
183
|
+
once. Saves and removals preserve other collections, including when the
|
|
184
|
+
adapter cannot persist writes. A new adapter starts a new read of storage.
|
|
185
|
+
|
|
177
186
|
## Invariants the model can't cheat
|
|
178
187
|
|
|
179
188
|
```javascript
|
package/dist/types/docstore.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* the host so the package stays free of `localStorage`.
|
|
6
6
|
*
|
|
7
7
|
* `createDocStore` is a keyed CRUD over an injected `storage` ({ read, write }):
|
|
8
|
-
* the store is one JSON object `{ [key]: { name → value } }`, read once
|
|
9
|
-
*
|
|
8
|
+
* the store is one JSON object `{ [key]: { name → value } }`, read once per
|
|
9
|
+
* storage adapter, shared by its collections and written back on every
|
|
10
|
+
* mutation. `encodeShare` / `decodeShare`
|
|
10
11
|
* turn a small snapshot into a Unicode-safe base64url token and back —
|
|
11
12
|
* forgivingly: a corrupt token decodes to `null`, never a throw.
|
|
12
13
|
*/
|
package/dist/types/splitter.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* DOM call is guarded so it mounts inertly over a headless stub (there the
|
|
7
7
|
* live drag is browser-verified). The host binds it as a widget and
|
|
8
8
|
* parameterizes the grid/rail selectors, the CSS variable and the commit
|
|
9
|
-
* action
|
|
9
|
+
* action. Widget props `{ ratio, axis: 'x' | 'y' }` select the live direction;
|
|
10
|
+
* the axis defaults to x. The ratio's `-first`/`-rest` CSS variables carry
|
|
11
|
+
* fractional tracks for stacked grids after their fixed toolbar and gaps.
|
|
10
12
|
*/
|
|
11
13
|
/**
|
|
12
14
|
* @param {Object} opts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/app",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.72.
|
|
4
|
+
"version": "0.72.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"prepack": "npm run build:types"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@jarenjs/core": "^0.72.
|
|
54
|
-
"@jarenjs/json": "^0.72.
|
|
55
|
-
"@jarenjs/view": "^0.72.
|
|
53
|
+
"@jarenjs/core": "^0.72.3",
|
|
54
|
+
"@jarenjs/json": "^0.72.3",
|
|
55
|
+
"@jarenjs/view": "^0.72.3"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/docstore.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* the host so the package stays free of `localStorage`.
|
|
7
7
|
*
|
|
8
8
|
* `createDocStore` is a keyed CRUD over an injected `storage` ({ read, write }):
|
|
9
|
-
* the store is one JSON object `{ [key]: { name → value } }`, read once
|
|
10
|
-
*
|
|
9
|
+
* the store is one JSON object `{ [key]: { name → value } }`, read once per
|
|
10
|
+
* storage adapter, shared by its collections and written back on every
|
|
11
|
+
* mutation. `encodeShare` / `decodeShare`
|
|
11
12
|
* turn a small snapshot into a Unicode-safe base64url token and back —
|
|
12
13
|
* forgivingly: a corrupt token decodes to `null`, never a throw.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { setObjectMember } from '@jarenjs/core/object';
|
|
17
|
+
import { createWeakCache } from '@jarenjs/core/cache';
|
|
18
|
+
|
|
19
|
+
/** Collection handles share their adapter's root, including unsaved fallback data. */
|
|
20
|
+
const storesByStorage = createWeakCache();
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* @param {Object} opts
|
|
@@ -29,8 +34,8 @@ import { setObjectMember } from '@jarenjs/core/object';
|
|
|
29
34
|
* }}
|
|
30
35
|
*/
|
|
31
36
|
export function createDocStore({ storage, key = 'experiments' }) {
|
|
32
|
-
//
|
|
33
|
-
const store = storage.read() ?? {};
|
|
37
|
+
// Read once per adapter; every collection writes the same current root.
|
|
38
|
+
const store = storesByStorage.getOrCreate(storage, () => storage.read() ?? {});
|
|
34
39
|
if (!Object.hasOwn(store, key) || store[key] == null) setObjectMember(store, key, {});
|
|
35
40
|
return {
|
|
36
41
|
save(name, value) { setObjectMember(store[key], name, value); storage.write(store); },
|
package/src/splitter.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* DOM call is guarded so it mounts inertly over a headless stub (there the
|
|
8
8
|
* live drag is browser-verified). The host binds it as a widget and
|
|
9
9
|
* parameterizes the grid/rail selectors, the CSS variable and the commit
|
|
10
|
-
* action
|
|
10
|
+
* action. Widget props `{ ratio, axis: 'x' | 'y' }` select the live direction;
|
|
11
|
+
* the axis defaults to x. The ratio's `-first`/`-rest` CSS variables carry
|
|
12
|
+
* fractional tracks for stacked grids after their fixed toolbar and gaps.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -44,22 +46,30 @@ export function createSplitterWidget(opts) {
|
|
|
44
46
|
const gridOf = () => (typeof host.closest === 'function' ? host.closest(gridSel) : null);
|
|
45
47
|
const applyRatio = (r) => {
|
|
46
48
|
gridOf()?.style?.setProperty?.(cssVar, String(r));
|
|
49
|
+
gridOf()?.style?.setProperty?.(cssVar + '-first', `${r}fr`);
|
|
50
|
+
gridOf()?.style?.setProperty?.(cssVar + '-rest', `${1 - r}fr`);
|
|
47
51
|
host.setAttribute?.('aria-valuenow', String(Math.round(r * 100)));
|
|
52
|
+
host.setAttribute?.('aria-orientation', handle.axis === 'y' ? 'horizontal' : 'vertical');
|
|
48
53
|
};
|
|
49
|
-
//
|
|
50
|
-
|
|
54
|
+
// A rail spans the content rows, so its top excludes the toolbar
|
|
55
|
+
// when the same content is stacked vertically.
|
|
56
|
+
const ratioAt = (clientX, clientY) => {
|
|
51
57
|
const g = gridOf();
|
|
52
58
|
if (g === null) return handle.ratio;
|
|
53
59
|
const rail = railSel !== null && typeof g.querySelector === 'function' ? g.querySelector(railSel) : null;
|
|
54
60
|
const box = g.getBoundingClientRect();
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const railBox = rail?.getBoundingClientRect();
|
|
62
|
+
const vertical = handle.axis === 'y';
|
|
63
|
+
const start = vertical ? (railBox?.top ?? box.top) : (railBox?.right ?? box.left);
|
|
64
|
+
const end = vertical ? (railBox?.bottom ?? box.bottom) : box.right;
|
|
65
|
+
const point = vertical ? clientY : clientX;
|
|
66
|
+
if (!(end > start) || !Number.isFinite(point)) return handle.ratio;
|
|
67
|
+
return clamp((point - start) / (end - start));
|
|
58
68
|
};
|
|
59
69
|
const onMove = (e) => {
|
|
60
70
|
if (!handle.dragging) return;
|
|
61
71
|
e.preventDefault?.();
|
|
62
|
-
handle.ratio = ratioAt(e.clientX);
|
|
72
|
+
handle.ratio = ratioAt(e.clientX, e.clientY);
|
|
63
73
|
applyRatio(handle.ratio); // live only — the commit is on pointer-up
|
|
64
74
|
};
|
|
65
75
|
const onUp = () => {
|
|
@@ -67,20 +77,32 @@ export function createSplitterWidget(opts) {
|
|
|
67
77
|
handle.dragging = false;
|
|
68
78
|
doc?.removeEventListener?.('pointermove', onMove);
|
|
69
79
|
doc?.removeEventListener?.('pointerup', onUp);
|
|
80
|
+
doc?.removeEventListener?.('pointercancel', onCancel);
|
|
70
81
|
emit({ action, with: handle.ratio });
|
|
71
82
|
};
|
|
83
|
+
const onCancel = () => {
|
|
84
|
+
if (!handle.dragging) return;
|
|
85
|
+
handle.dragging = false;
|
|
86
|
+
handle.ratio = handle.startRatio;
|
|
87
|
+
applyRatio(handle.ratio);
|
|
88
|
+
doc?.removeEventListener?.('pointermove', onMove);
|
|
89
|
+
doc?.removeEventListener?.('pointerup', onUp);
|
|
90
|
+
doc?.removeEventListener?.('pointercancel', onCancel);
|
|
91
|
+
};
|
|
72
92
|
const onDown = (e) => {
|
|
73
93
|
if (e.button !== undefined && e.button !== 0) return;
|
|
74
94
|
handle.dragging = true;
|
|
95
|
+
handle.startRatio = handle.ratio;
|
|
75
96
|
e.preventDefault?.();
|
|
76
97
|
doc?.addEventListener?.('pointermove', onMove);
|
|
77
98
|
doc?.addEventListener?.('pointerup', onUp);
|
|
99
|
+
doc?.addEventListener?.('pointercancel', onCancel);
|
|
78
100
|
};
|
|
79
101
|
const onKey = (e) => {
|
|
80
102
|
const s = e.shiftKey ? fineStep : step;
|
|
81
103
|
let next = null;
|
|
82
|
-
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = clamp(handle.ratio - s);
|
|
83
|
-
else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = clamp(handle.ratio + s);
|
|
104
|
+
if (e.key === 'ArrowLeft' || e.key === (handle.axis === 'y' ? 'ArrowUp' : 'ArrowDown')) next = clamp(handle.ratio - s);
|
|
105
|
+
else if (e.key === 'ArrowRight' || e.key === (handle.axis === 'y' ? 'ArrowDown' : 'ArrowUp')) next = clamp(handle.ratio + s);
|
|
84
106
|
else if (e.key === 'Home') next = min;
|
|
85
107
|
else if (e.key === 'End') next = max;
|
|
86
108
|
if (next === null) return;
|
|
@@ -90,7 +112,9 @@ export function createSplitterWidget(opts) {
|
|
|
90
112
|
emit({ action, with: next });
|
|
91
113
|
};
|
|
92
114
|
|
|
93
|
-
const handle = { host, doc, ratio: clamp(Number(props?.ratio ?? 0.5)),
|
|
115
|
+
const handle = { host, doc, ratio: clamp(Number(props?.ratio ?? 0.5)),
|
|
116
|
+
axis: props?.axis === 'y' ? 'y' : 'x', startRatio: 0.5,
|
|
117
|
+
dragging: false, applyRatio, onMove, onUp, onCancel };
|
|
94
118
|
applyRatio(handle.ratio);
|
|
95
119
|
host.addEventListener?.('pointerdown', onDown);
|
|
96
120
|
host.addEventListener?.('keydown', onKey);
|
|
@@ -98,6 +122,12 @@ export function createSplitterWidget(opts) {
|
|
|
98
122
|
return handle;
|
|
99
123
|
},
|
|
100
124
|
update(handle, props) {
|
|
125
|
+
const axis = props?.axis === 'y' ? 'y' : 'x';
|
|
126
|
+
if (axis !== handle.axis) {
|
|
127
|
+
handle.onCancel();
|
|
128
|
+
handle.axis = axis;
|
|
129
|
+
handle.applyRatio(handle.ratio);
|
|
130
|
+
}
|
|
101
131
|
const r = clamp(Number(props?.ratio ?? 0.5));
|
|
102
132
|
if (r !== handle.ratio && !handle.dragging) {
|
|
103
133
|
handle.ratio = r;
|
|
@@ -108,6 +138,7 @@ export function createSplitterWidget(opts) {
|
|
|
108
138
|
for (const [type, fn] of handle.hostListeners) handle.host.removeEventListener?.(type, fn);
|
|
109
139
|
handle.doc?.removeEventListener?.('pointermove', handle.onMove);
|
|
110
140
|
handle.doc?.removeEventListener?.('pointerup', handle.onUp);
|
|
141
|
+
handle.doc?.removeEventListener?.('pointercancel', handle.onCancel);
|
|
111
142
|
},
|
|
112
143
|
};
|
|
113
144
|
}
|