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 +12 -0
- package/README.md +95 -2
- package/package.json +1 -1
- package/src/eslint/plugin.js +6 -6
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
|
-
###
|
|
442
|
+
### Imperative dispatch from many sites (mechanism #3)
|
|
352
443
|
|
|
353
|
-
|
|
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.
|
|
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",
|
package/src/eslint/plugin.js
CHANGED
|
@@ -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
|
},
|