@jarenjs/app 0.56.0 → 0.67.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/README.md CHANGED
@@ -170,6 +170,10 @@ Two IDE-shaped primitives ship ready to bind, so a two-pane surface (the studio,
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
171
  - **`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
172
 
173
+ Collection keys and document names such as `__proto__` are ordinary own
174
+ members of the document store and survive persistence. Loading a name
175
+ that has not been saved returns `undefined`, including prototype-member names.
176
+
173
177
  ## Invariants the model can't cheat
174
178
 
175
179
  ```javascript
@@ -214,7 +218,12 @@ const app = createApp({
214
218
  });
215
219
  ```
216
220
 
217
- Schema in, live form out: text/email/number/date/color inputs, textareas, checkboxes, selects with precomputed options, nested object fieldsets, arrays with add/remove buttons, inline errors, and `x-form` visibility/enablement/computed reacting per keystroke. The `viewModel` option is the general **derivation boundary**: it maps state to the view stylesheet's input before every render, so JS-computed derivations enter the render path without ever entering the state. A DOM control's value is a string, and two controls carry something else: a select over a non-string enum, and the `json` editor over a structured value. Both round-trip through JSON text and decode it in `formEventFields()`, the format's one sanctioned place for host JavaScript at the DOM boundary (APP-FORMAT §5.4) — **register it or those two controls write nothing**. Remaining 0.1 limits (documented in `src/forms.js`): a cleared number input writes `null`, and arrays need to exist in the data (give them `default: []` in the schema).
221
+ Schema in, live form out: text/email/number/date/color inputs, textareas, checkboxes, selects with precomputed options, nested object fieldsets, arrays with add/remove buttons, inline errors, and `x-form` visibility/enablement/computed reacting per keystroke. The `viewModel` option is the general **derivation boundary**: it maps state to the view stylesheet's input before every render, so JS-computed derivations enter the render path without ever entering the state. A DOM control's value is a string, and two controls carry something else: a select over a non-string enum, and the `json` editor over a structured value. Both round-trip through JSON text and decode it in `formEventFields()`, the format's one sanctioned place for host JavaScript at the DOM boundary (APP-FORMAT §5.4) — **register it or those two controls write nothing**. Remaining limits (documented in `src/forms.js`): a cleared number input writes `null`, and intermediate object/array containers need to exist in the data. `createInitialData(model)` supplies them for a new form; loaded documents must supply them too.
222
+
223
+ Nested object and array fields honor `x-form.enabled` through their native
224
+ `fieldset` disablement, including child controls and array buttons. A `readOnly`
225
+ field uses native `readonly` for text inputs and textareas, and `disabled` for
226
+ checkboxes, selects, collection fieldsets, and add/remove buttons.
218
227
 
219
228
  ## Headless and server-side
220
229
 
@@ -233,6 +242,20 @@ Also exported: `compileActions`, `compileSubs`, `createFormView`, `createFormAct
233
242
 
234
243
  Options: `node`, `document`, `effects`, `subs`, `eventFields` (named `$event` field extractors), `widgets` (registered widget definitions, forwarded to the renderer), `compileTypeTest`, `validateState`, `viewModel`, `onError` (default rethrows), `schedule` (render batching; default microtask — pass `(f) => f()` for synchronous tests). Compile failures throw `AppCompileError` (`JA0xxx`, with a `docPath` into the app document); runtime failures route `AppRuntimeError` (`JA2xxx`) through `onError`. The full code table is in [APP-FORMAT.md](docs/APP-FORMAT.md) §10.
235
244
 
245
+ ## Exports
246
+
247
+ Every subpath a consumer can import, derived from the manifest by
248
+ `npm run docs:derive` (`npm run docs:check` fails when the two drift):
249
+
250
+ <!--fact:exports.app-->
251
+ | Import | Kind | Declarations |
252
+ |---|---|---|
253
+ | `@jarenjs/app` | JavaScript | declared |
254
+ | `@jarenjs/app/schemas/jaren-app.draft-07.schema.json` | schema | — |
255
+ | `@jarenjs/app/schemas/jaren-app.schema.json` | schema | — |
256
+ | `@jarenjs/app/package.json` | metadata | — |
257
+ <!--/fact-->
258
+
236
259
  ## Development
237
260
 
238
261
  Unit tests live in `test/app/` at the repository root (`npm run test:app`). See [ROADMAP](../../docs/ROADMAP.md) for what's next: dirty-path-pruned re-rendering and time-travel tooling over the action log.
@@ -22,9 +22,11 @@
22
22
  * JavaScript at the DOM boundary (APP-FORMAT §5.4). A host that renders
23
23
  * these controls MUST register them.
24
24
  *
25
- * Remaining limitation, documented rather than hidden: a cleared number
26
- * input writes `null` (which surfaces as a validation error, not a
27
- * dispatch error).
25
+ * Remaining limitations: a cleared number input writes `null` (which
26
+ * surfaces as a validation error, not a dispatch error), and standard
27
+ * actions require intermediate object/array containers to exist in the
28
+ * data. `createInitialData` supplies those containers for a new form;
29
+ * loaded documents must supply them too.
28
30
  */
29
31
  /**
30
32
  * The event-field extractors the standard form controls need, for
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/app",
3
3
  "private": false,
4
- "version": "0.56.0",
4
+ "version": "0.67.0",
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.56.0",
54
- "@jarenjs/json": "^0.56.0",
55
- "@jarenjs/view": "^0.56.0"
53
+ "@jarenjs/core": "^0.67.0",
54
+ "@jarenjs/json": "^0.67.0",
55
+ "@jarenjs/view": "^0.67.0"
56
56
  }
57
57
  }
package/src/docstore.js CHANGED
@@ -12,6 +12,8 @@
12
12
  * forgivingly: a corrupt token decodes to `null`, never a throw.
13
13
  */
14
14
 
15
+ import { setObjectMember } from '@jarenjs/core/object';
16
+
15
17
  /**
16
18
  * @param {Object} opts
17
19
  * @param {{ read: () => any, write: (store: any) => void }} opts.storage
@@ -29,10 +31,10 @@
29
31
  export function createDocStore({ storage, key = 'experiments' }) {
30
32
  // read once; keep the SAME object reference for every write-back
31
33
  const store = storage.read() ?? {};
32
- if (store[key] === undefined || store[key] === null) store[key] = {};
34
+ if (!Object.hasOwn(store, key) || store[key] == null) setObjectMember(store, key, {});
33
35
  return {
34
- save(name, value) { store[key][name] = value; storage.write(store); },
35
- load(name) { return store[key][name]; },
36
+ save(name, value) { setObjectMember(store[key], name, value); storage.write(store); },
37
+ load(name) { return Object.hasOwn(store[key], name) ? store[key][name] : undefined; },
36
38
  remove(name) { delete store[key][name]; storage.write(store); },
37
39
  names() { return Object.keys(store[key]).sort(); },
38
40
  all() { return store[key]; },
package/src/forms.js CHANGED
@@ -23,9 +23,11 @@
23
23
  * JavaScript at the DOM boundary (APP-FORMAT §5.4). A host that renders
24
24
  * these controls MUST register them.
25
25
  *
26
- * Remaining limitation, documented rather than hidden: a cleared number
27
- * input writes `null` (which surfaces as a validation error, not a
28
- * dispatch error).
26
+ * Remaining limitations: a cleared number input writes `null` (which
27
+ * surfaces as a validation error, not a dispatch error), and standard
28
+ * actions require intermediate object/array containers to exist in the
29
+ * data. `createInitialData` supplies those containers for a new form;
30
+ * loaded documents must supply them too.
29
31
  */
30
32
 
31
33
  /** The default action names shared by both factories. */
@@ -111,6 +113,21 @@ export function createFormView(options = {}) {
111
113
  const ctl = (control) => `${root}..[?@.control == '${control}']`;
112
114
 
113
115
  const disabled = { $not: '$.enabled' };
116
+ // Selects, checkboxes and collection controls have no readonly mode.
117
+ // Disabling their fieldset also prevents edits through child controls.
118
+ const writeDisabled = { $or: [disabled, '$.readOnly'] };
119
+
120
+ /** One removal control for scalar and collection array elements. */
121
+ const remove = { $if: ['$.removable',
122
+ ['button', {
123
+ type: 'button',
124
+ class: `${cls}-remove`,
125
+ // the glyph is decoration; the accessible name is the label
126
+ 'aria-label': labels.removeItem,
127
+ title: labels.removeItem,
128
+ disabled: writeDisabled,
129
+ on: { click: { action: act.remove, with: { pointer: '$.pointer' } } },
130
+ }, options.removeLabel ?? '×']] };
114
131
 
115
132
  /** The shared field chrome around one control vnode. */
116
133
  const field = (control) => ['div', { class: `${cls}-field`, 'data-pointer': '$.pointer' },
@@ -121,15 +138,7 @@ export function createFormView(options = {}) {
121
138
  ],
122
139
  { $if: ['$.description', ['p', { class: `${cls}-description` }, '$.description']] },
123
140
  [{ $apply: '$.errors[*]' }],
124
- { $if: ['$.removable',
125
- ['button', {
126
- type: 'button',
127
- class: `${cls}-remove`,
128
- // the glyph is decoration; the accessible name is the label
129
- 'aria-label': labels.removeItem,
130
- title: labels.removeItem,
131
- on: { click: { action: act.remove, with: { pointer: '$.pointer' } } },
132
- }, options.removeLabel ?? '×']] },
141
+ remove,
133
142
  ];
134
143
 
135
144
  // every write binding carries the element flag: the standard actions
@@ -179,16 +188,17 @@ export function createFormView(options = {}) {
179
188
  // nested objects: a fieldset group
180
189
  {
181
190
  match: ctl('object'),
182
- body: ['fieldset', { class: `${cls}-group`, 'data-pointer': '$.pointer' },
191
+ body: ['fieldset', { class: `${cls}-group`, 'data-pointer': '$.pointer', disabled: writeDisabled },
183
192
  { $if: ['$.label', ['legend', {}, '$.label']] },
184
193
  [{ $apply: '$.children[*]' }],
185
194
  [{ $apply: '$.errors[*]' }],
195
+ remove,
186
196
  ],
187
197
  },
188
198
  // arrays: expanded items plus the add-item button
189
199
  {
190
200
  match: ctl('array'),
191
- body: ['fieldset', { class: `${cls}-array`, 'data-pointer': '$.pointer' },
201
+ body: ['fieldset', { class: `${cls}-array`, 'data-pointer': '$.pointer', disabled: writeDisabled },
192
202
  { $if: ['$.label', ['legend', {}, '$.label']] },
193
203
  [{ $apply: '$.items[*]' }],
194
204
  { $if: [{ $exists: '$.addValue' },
@@ -197,9 +207,11 @@ export function createFormView(options = {}) {
197
207
  class: `${cls}-add`,
198
208
  'aria-label': labels.addItem,
199
209
  title: labels.addItem,
210
+ disabled: writeDisabled,
200
211
  on: { click: { action: act.add, with: { pointer: '$.pointer', value: '$.addValue' } } },
201
212
  }, options.addLabel ?? '+']] },
202
213
  [{ $apply: '$.errors[*]' }],
214
+ remove,
203
215
  ],
204
216
  },
205
217
  // one error line per message
@@ -216,7 +228,7 @@ export function createFormView(options = {}) {
216
228
  {
217
229
  match: ctl('select'),
218
230
  body: field(['select', {
219
- disabled,
231
+ disabled: writeDisabled,
220
232
  on: {
221
233
  change: { action: act.json, with: writeWith, event: [JSON_FIELD] },
222
234
  },
@@ -228,7 +240,7 @@ export function createFormView(options = {}) {
228
240
  body: field(['input', {
229
241
  type: 'checkbox',
230
242
  checked: '$.value',
231
- disabled,
243
+ disabled: writeDisabled,
232
244
  on: { change: { action: act.check, with: writeWith } },
233
245
  }]),
234
246
  },