@jarenjs/app 0.72.0 → 0.72.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 CHANGED
@@ -168,6 +168,11 @@ 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
@@ -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, so studio, play and any future two-pane surface share one splitter.
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.0",
4
+ "version": "0.72.2",
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.0",
54
- "@jarenjs/json": "^0.72.0",
55
- "@jarenjs/view": "^0.72.0"
53
+ "@jarenjs/core": "^0.72.2",
54
+ "@jarenjs/json": "^0.72.2",
55
+ "@jarenjs/view": "^0.72.2"
56
56
  }
57
57
  }
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, so studio, play and any future two-pane surface share one splitter.
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
- // pointer x the left content pane's share of the (post-rail) span
50
- const ratioAt = (clientX) => {
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 left = rail !== null ? rail.getBoundingClientRect().right : box.left;
56
- if (!(box.right > left)) return handle.ratio;
57
- return clamp((clientX - left) / (box.right - left));
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)), dragging: false, applyRatio, onMove, onUp };
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
  }