on_the_money 0.5.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.2] — 2026-05-23
8
+
9
+ ### Documentation
10
+
11
+ - **Added the "Discipline" section to the README.** Names the framework's three state-response mechanisms explicitly: (1) CSS attribute selectors for state→visual, (2) imperative API calls colocated with state writes for one-off platform side effects, (3) `MutationObserver` for DRY of imperative responses across many call sites. Lists the right-vs-wrong matrix for `MutationObserver` (imperative-only responses: yes; CSS's job: no). Ships the "deletion test" self-check. Documents the element-as-state-carrier pattern (`<dialog open>`, `<details open>`, etc.) where state lives on the element with native CSS hooks. (#80)
12
+ - **Reframed the existing "Reacting to state changes" pattern** as "Imperative dispatch from many sites (mechanism #3)" — same example, sharper framing as a case of the three-mechanism vocabulary, with an explicit "before reaching for this pattern, check whether CSS can do the job" warning.
13
+ - **ESLint rule messages now reference §The Discipline** so consumers hitting a lint failure can read the architectural why in one place, not just the local rule hint.
14
+
15
+ ### Why
16
+
17
+ The framework rejects subscriber/signal/effect patterns but the discipline was implicit. LLM pretraining is heavily biased toward those patterns; naming the trap explicitly (with the three-mechanism vocabulary as the alternative) gives consumers and LLM authors a concrete framework to reach for instead. The element-as-state-carrier section is the highest-leverage addition — most consumers don't realize `<dialog open>` is already a state attribute with CSS hooks.
18
+
7
19
  ## [0.5.1] — 2026-05-23
8
20
 
9
21
  ### Documentation
package/README.md CHANGED
@@ -260,6 +260,97 @@ Clones the first element of `<template selector>`, runs `_t(el)` for i18n hydrat
260
260
 
261
261
  For `beforebegin` and `afterend`, the first argument is a sibling reference, not a true parent.
262
262
 
263
+ ## The Discipline
264
+
265
+ on_the_money has no reactivity primitives. No signal, effect, autorun, watch, atom, store, derived state, or subscription. There is also no event broadcast on `the()` writes. If you find yourself reaching for any of these — including importing them from another library — you're solving a problem the framework rejects.
266
+
267
+ Instead, the framework has **three state-response mechanisms**, each with one job:
268
+
269
+ ### 1. CSS attribute selectors handle state → visual
270
+
271
+ Default. Anything visual goes through `[data-key="value"]` selectors in your stylesheet.
272
+
273
+ ```css
274
+ body[data-modal="session-expired"] #session-expired-modal { display: block }
275
+ body[data-page="home"] main > section[data-route="home"] { display: block }
276
+ [data-state="loading"] .spinner { display: inline }
277
+ ```
278
+
279
+ ```javascript
280
+ the("modal", "session-expired"); // CSS rule above runs the visual change
281
+ ```
282
+
283
+ No JS step between the `the()` write and the visual update. The CSS rule **is** the correspondence. This is the framework's reason for existing — DOM-as-state-database is valuable specifically because CSS can read that database without JS.
284
+
285
+ ### 2. Imperative API calls colocated with state writes
286
+
287
+ For one-off platform side effects (`dialog.showModal()`, `el.focus()`, `el.scrollIntoView()`, media `.play()`, animation triggers), the imperative call lives in the same `on()` handler that wrote the state.
288
+
289
+ ```javascript
290
+ on("button.open", "click", () => {
291
+ the("modal", "session-expired");
292
+ $("#session-expired-modal").showModal();
293
+ });
294
+ ```
295
+
296
+ Cause (user click) and imperative effect (`showModal()`) are colocated. The `the()` write records the state; the platform call performs the imperative action. No subscriber between them.
297
+
298
+ ### 3. `MutationObserver` for DRY of imperative responses across many call sites
299
+
300
+ When the same state can be written from multiple places (user click, fetch handler, boot replay, postMessage) and they all need the same imperative response, observe the attribute once:
301
+
302
+ ```javascript
303
+ new MutationObserver(() => {
304
+ const want = the("modal") ?? "";
305
+ for (const open of $$("dialog[open]")) {
306
+ if (open.id !== `${want}-modal`) open.close();
307
+ }
308
+ if (want) $(`#${want}-modal`)?.showModal();
309
+ }).observe(document.body, { attributes: true, attributeFilter: ["data-modal"] });
310
+ ```
311
+
312
+ The observer doesn't enable reactivity — it deduplicates the imperative response. Any place in the codebase that writes `data-modal` gets the same dispatch logic without copy-pasting.
313
+
314
+ ### When `MutationObserver` is right vs wrong
315
+
316
+ | Right (the platform call does work CSS can't reach) | Wrong (CSS's job) |
317
+ | --- | --- |
318
+ | `el.focus()`, `el.blur()` — moving keyboard focus | Showing/hiding via `display: none` |
319
+ | `el.scrollIntoView()`, `window.scrollTo(...)` — viewport movement | Changing color, layout, transform |
320
+ | `media.play()`, `media.pause()` — playback state | Adding/removing a class |
321
+ | `el.animate(...)` — Web Animations timing | Conditional opacity / visibility |
322
+ | `el.requestFullscreen()` — browser mode | Animation triggers expressible as CSS transitions |
323
+ | `navigator.clipboard.writeText(...)` — out-of-DOM side effects | Toggling text content (use `[data-text]`) |
324
+
325
+ `dialog.showModal()` is a borderline case: it shows the dialog (visual) AND traps focus, paints the backdrop, registers an Escape handler, and announces modal semantics to assistive tech (behavioral + a11y). If you want only the visual, `dialog[open]` + CSS suffices. If you want the bundled behavior, call `showModal()` — and reach for `MutationObserver` only if `data-modal` can be set from multiple call sites that all need the same dispatch.
326
+
327
+ If your `MutationObserver` callback is setting styles, toggling classes, or changing `textContent`, you've smuggled a JS reactivity layer into a project that explicitly rejected one. The CSS attribute selector was already doing that job — declaratively, with zero JS, with the right perf shape.
328
+
329
+ ### The deletion test
330
+
331
+ The single best self-check: strip every JS function except `on()` handlers, the imperative calls inside them, and any `MutationObserver` that handles a many-sites imperative response. Open the page; mutate `body` `data-*` attributes in DevTools.
332
+
333
+ - Does the page respond correctly? You're idiomatic.
334
+ - Does it not? You're missing either a CSS rule (most likely) or an imperative call at a state-write site (occasionally).
335
+
336
+ ### Element-as-state-carrier
337
+
338
+ Many platform elements already carry state in their own attributes, with native CSS hooks. When the state genuinely lives on a specific element, use that — don't add a `body[data-x]` indirection:
339
+
340
+ | Element | Native state attribute | What you do |
341
+ | --- | --- | --- |
342
+ | `<dialog>` | `[open]` | `el.setAttribute("open", "")` + Pico's `dialog[open]` CSS, or `el.showModal()` if you need the backdrop and focus trap |
343
+ | `<details>` | `[open]` | Same shape; browser handles the disclosure UX |
344
+ | `<input>`, `<textarea>` | `[aria-invalid]`, `[aria-required]`, `[disabled]` | Direct `setAttribute`; CSS targets `:invalid`, `[disabled]` |
345
+ | `<button>` | `[aria-pressed]` (toggle state), `[disabled]` | Native focus/style for free |
346
+ | `<select>` | The current `value` | Read via `el.value`; no body indirection needed |
347
+
348
+ The `body[data-modal]` indirection is correct when the state is a named enum the consumer controls (which modal is open, current page, theme). When the state genuinely lives on an element (is THIS dialog open?), let it live there.
349
+
350
+ ### Banned vocabulary, in spirit
351
+
352
+ If you're searching online for "how to subscribe to OTM state changes" or "OTM reactive system" — you're looking for something the framework doesn't have and won't add. The three mechanisms above are the whole story. Reactivity-by-CSS, imperative-by-handler, DRY-by-observer.
353
+
263
354
  ## Patterns
264
355
 
265
356
  ### Form intake
@@ -348,9 +439,9 @@ route(() => {
348
439
  });
349
440
  ```
350
441
 
351
- ### Reacting to state changes (MutationObserver)
442
+ ### Imperative dispatch from many sites (mechanism #3)
352
443
 
353
- The framework deliberately doesn't broadcast events on every `the()` write. To react to attribute changes, use the platform's primitive directly. This is the canonical pattern for state-driven UI hooks the framework can't express declaratively (modal orchestration, focus management, etc.):
444
+ This is the case-3 pattern from [The Discipline](#the-discipline): the same state can be written from several places (user click, fetch error handler, boot replay, etc.) and all of them need the same imperative response. Observe the attribute once, dispatch from there:
354
445
 
355
446
  ```javascript
356
447
  new MutationObserver(() => {
@@ -364,6 +455,8 @@ new MutationObserver(() => {
364
455
 
365
456
  `document.body` is the allowed identifier under `otm/no-document-query`. Filter by `attributeFilter` to scope the observer narrowly.
366
457
 
458
+ **Before reaching for this pattern, check whether CSS can do the job.** If the response is purely visual (show/hide, color, layout), the CSS attribute selector is the right tool and the observer is overhead. Reserve `MutationObserver` for imperative APIs CSS can't express (`showModal`, `focus`, `scrollIntoView`, media controls, animation triggers).
459
+
367
460
  ### Working around `_t()` with an empty dictionary
368
461
 
369
462
  When `the.boot({ defaultLocale })` skips the dictionary fetch, programmatic `_t(key, options)` calls return the key (no template to interpolate against). For default-locale interpolation, use template literals or a small wrapper:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "on_the_money",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Opinionated, attribute-driven, standards-oriented modern framework. <2KB gzip. Native browser APIs only. See README.md for the authoring context.",
5
5
  "type": "module",
6
6
  "main": "dist/on_the_money.min.js",
@@ -12,7 +12,7 @@ const preferOn = {
12
12
  },
13
13
  messages: {
14
14
  useOn:
15
- "Direct addEventListener is forbidden. Use on() for event delegation. Common LLM mistake: copying jQuery / React tutorial patterns — on() is the single delegated-listener idiom.",
15
+ "Direct addEventListener is forbidden. Use on() for event delegation. Common LLM mistake: copying jQuery / React tutorial patterns — on() is the single delegated-listener idiom. See README §The Discipline.",
16
16
  },
17
17
  schema: [],
18
18
  },
@@ -36,7 +36,7 @@ const preferTheSet = {
36
36
  },
37
37
  messages: {
38
38
  useThe:
39
- "Direct text manipulation is forbidden. Use the() or [data-text] binding instead. Common LLM mistake: doing this server-side via linkedom/jsdom — same fix, emit JSON and let the OTM client render.",
39
+ "Direct text manipulation is forbidden. Use the() or [data-text] binding instead. Common LLM mistake: doing this server-side via linkedom/jsdom — same fix, emit JSON and let the OTM client render. See README §The Discipline.",
40
40
  },
41
41
  schema: [],
42
42
  },
@@ -64,7 +64,7 @@ const flatState = {
64
64
  },
65
65
  messages: {
66
66
  notFlat:
67
- "State must be flat. Nested objects or arrays are forbidden in the(). Common LLM mistake: passing a nested object — use the.flat(obj) to compose first.",
67
+ "State must be flat. Nested objects or arrays are forbidden in the(). Common LLM mistake: passing a nested object — use the.flat(obj) to compose first. See README §The Discipline.",
68
68
  },
69
69
  schema: [],
70
70
  },
@@ -147,7 +147,7 @@ const noStyleMutation = {
147
147
  },
148
148
  messages: {
149
149
  noStyle:
150
- "Direct style manipulation is forbidden. Use the() with attribute selectors instead. Common LLM mistake: copying React/Vue inline-style patterns — CSS rules keyed off [data-state=...] handle state-driven styling cleanly.",
150
+ "Direct style manipulation is forbidden. Use the() with attribute selectors instead. Common LLM mistake: copying React/Vue inline-style patterns — CSS rules keyed off [data-state=...] handle state-driven styling cleanly. See README §The Discipline.",
151
151
  },
152
152
  schema: [],
153
153
  },
@@ -197,7 +197,7 @@ const noServerDom = {
197
197
  },
198
198
  messages: {
199
199
  noServerDom:
200
- "Server-side DOM library '{{name}}' is forbidden. Common LLM mistake: rendering HTML server-side instead of emitting JSON. The OTM client hydrates static templates via $.clone() + the() — keep server output to data.",
200
+ "Server-side DOM library '{{name}}' is forbidden. Common LLM mistake: rendering HTML server-side instead of emitting JSON. The OTM client hydrates static templates via $.clone() + the() — keep server output to data. See README §The Discipline.",
201
201
  },
202
202
  schema: [],
203
203
  },
@@ -237,7 +237,7 @@ const noDocumentQuery = {
237
237
  },
238
238
  messages: {
239
239
  noDocumentQuery:
240
- "Direct document.{{method}} is forbidden. Common LLM mistake: querying through document instead of OTM's selectors. Use $(selector) for one, $$(selector) for many, or $.clone(parent, '#tmpl') for templates.",
240
+ "Direct document.{{method}} is forbidden. Common LLM mistake: querying through document instead of OTM's selectors. Use $(selector) for one, $$(selector) for many, or $.clone(parent, '#tmpl') for templates. See README §The Discipline.",
241
241
  },
242
242
  schema: [],
243
243
  },