micra.js 2.3.1 → 2.3.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/llms-full.txt CHANGED
@@ -7,7 +7,7 @@ This file follows the llmstxt.org "expanded" convention: it inlines code recipes
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install micra.js@^2.3.1
10
+ npm install micra.js@^2.3.2
11
11
  ```
12
12
 
13
13
  ```ts
@@ -17,7 +17,7 @@ import * as Micra from 'micra.js'
17
17
  Or CDN (no build step):
18
18
 
19
19
  ```html
20
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
21
21
  ```
22
22
 
23
23
  This exposes a global `Micra` object.
@@ -146,7 +146,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
146
146
  <button @click="inc">+</button>
147
147
  </div>
148
148
 
149
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
149
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
150
150
  <script>
151
151
  Micra.define('counter', {
152
152
  state: { count: 0 },
@@ -190,7 +190,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
190
190
  </footer>
191
191
  </div>
192
192
 
193
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
193
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
194
194
  <script>
195
195
  Micra.define('todo-app', {
196
196
  state: {
@@ -262,7 +262,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
262
262
  <p data-if="filtered().length === 0">No matches.</p>
263
263
  </div>
264
264
 
265
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
265
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
266
266
  <script>
267
267
  Micra.define('users-table', {
268
268
  state: {
@@ -303,7 +303,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
303
303
  <p data-if="success">Invitation sent ✓</p>
304
304
  </form>
305
305
 
306
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
306
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
307
307
  <script>
308
308
  Micra.define('invite-form', {
309
309
  state: { email: '', loading: false, error: '', success: false },
@@ -345,7 +345,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
345
345
  </div>
346
346
  </div>
347
347
 
348
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
348
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
349
349
  <script>
350
350
  Micra.define('open-modal-btn', {
351
351
  open() {
@@ -394,7 +394,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
394
394
  <section data-if="tab === 'security'">Security content</section>
395
395
  </div>
396
396
 
397
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
397
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
398
398
  <script>
399
399
  Micra.define('tabs', {
400
400
  state: { tab: 'overview' },
@@ -414,7 +414,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
414
414
  <button @click="upgrade" data-if="plan !== 'enterprise'">Upgrade</button>
415
415
  </div>
416
416
 
417
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
417
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
418
418
  <script>
419
419
  Micra.define('user-card', {
420
420
  state: { name: '', plan: '' },
@@ -445,7 +445,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
445
445
  <p data-if="!loading && results.length === 0 && query">No results.</p>
446
446
  </div>
447
447
 
448
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
448
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
449
449
  <script>
450
450
  Micra.define('search', {
451
451
  state: { query: '', results: [], loading: false },
@@ -479,7 +479,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
479
479
  <p data-if="loading">Loading chart…</p>
480
480
  </div>
481
481
 
482
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
482
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
483
483
  <script>
484
484
  Micra.define('revenue-chart', {
485
485
  state: { loading: true },
@@ -511,7 +511,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
511
511
  <p data-if="!loading && rows.length === 0">No results.</p>
512
512
  </div>
513
513
 
514
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
514
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
515
515
  <script>
516
516
  Micra.define('search-bar', {
517
517
  state: { query: '' },
@@ -542,7 +542,7 @@ the same page. Twelve lines of glue, written once.
542
542
  <main hx-get="/page/home" hx-trigger="load" hx-swap="innerHTML"></main>
543
543
 
544
544
  <script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
545
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
545
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
546
546
  <script>
547
547
  Micra.define('counter', {
548
548
  state: { count: 0 },
@@ -596,7 +596,7 @@ a `turbo:load` mirror so the second navigation doesn't ghost.
596
596
  ```ruby
597
597
  # config/importmap.rb
598
598
  pin "micra",
599
- to: "https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.esm.js",
599
+ to: "https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.esm.js",
600
600
  preload: true
601
601
  ```
602
602
 
@@ -717,9 +717,9 @@ import { ref, computed } from 'vue'
717
717
  import Alpine from 'alpinejs'
718
718
 
719
719
  // ❌ unpkg CDN — blocked by Claude artifacts and most AI sandbox CSPs
720
- <script src="https://unpkg.com/micra.js@2.3.1/dist/micra.min.js"></script>
720
+ <script src="https://unpkg.com/micra.js@2.3.2/dist/micra.min.js"></script>
721
721
  // ✅ Use jsDelivr instead — it auto-mirrors npm and is CSP-allowlisted everywhere
722
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
722
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
723
723
  ```
724
724
 
725
725
  # Final checklist
package/llms.txt CHANGED
@@ -25,7 +25,7 @@ import * as Micra from 'micra.js'
25
25
  Or via CDN (no build step):
26
26
 
27
27
  ```html
28
- <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
28
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
29
29
  ```
30
30
 
31
31
  This exposes a global `Micra` object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micra.js",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Lightweight reactive UI framework for server-rendered pages — reactive state, directives, event bus. < 5 KB gzip.",
5
5
  "type": "module",
6
6
  "main": "./dist/micra.cjs.js",
@@ -30,7 +30,7 @@
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
32
  "test:coverage": "vitest run --coverage",
33
- "docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/",
33
+ "docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/ && cp llms.txt llms-full.txt site/",
34
34
  "docs:build": "npm run build && npm run docs:sync",
35
35
  "docs:dev": "npm run docs:sync && npx serve -p 4321 site"
36
36
  },
package/src/dom/each.ts CHANGED
@@ -106,8 +106,25 @@ function createRowNode<S extends StateRecord>(
106
106
  ): MicraElement {
107
107
  const frag = tmpl.content.cloneNode(true) as DocumentFragment
108
108
  let node: MicraElement
109
- if (frag.childNodes.length === 1) {
110
- node = frag.firstElementChild as MicraElement
109
+ // Single-root detection must ignore whitespace-only text nodes — a
110
+ // pretty-printed `<template>\n <tr>…</tr>\n</template>` is still one root.
111
+ // Wrapping a lone <tr> in <micra-each-item> would put invalid content
112
+ // inside <tbody> and break `tbody > tr` selectors. Only TOP-LEVEL child
113
+ // nodes are scanned (O(1-ish), not O(subtree)); NBSP counts as meaningful
114
+ // (it renders), so it keeps the wrapper. Comments beside the root are
115
+ // dropped — they don't render and aren't worth a wrapper in <tbody>.
116
+ const first = frag.firstElementChild as MicraElement | null
117
+ // meaningful = any char with code > 32 (NBSP included; \t \n \f \r and
118
+ // space excluded) in a top-level text node
119
+ const single =
120
+ !!first &&
121
+ !first.nextElementSibling &&
122
+ !Array.prototype.some.call(
123
+ frag.childNodes,
124
+ (c: Node) => c.nodeType === 3 && /[^\x00- ]/.test(c.textContent!),
125
+ )
126
+ if (single) {
127
+ node = first!
111
128
  } else {
112
129
  node = document.createElement('micra-each-item') as MicraElement
113
130
  node.style.display = 'contents'
package/src/utils/expr.ts CHANGED
@@ -52,10 +52,9 @@ const SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/
52
52
  * Globals reachable from directive expressions. Anything else (window, fetch,
53
53
  * constructor, eval, ...) is shadowed by SAFE_OUTER and resolves to undefined.
54
54
  */
55
- const ALLOWED_GLOBALS = new Set<string>([
56
- 'Math', 'JSON', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
57
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'NaN', 'Infinity', 'undefined',
58
- ])
55
+ const ALLOWED_GLOBALS = new Set<string>(
56
+ 'Math,JSON,Date,String,Number,Boolean,Array,Object,parseInt,parseFloat,isNaN,isFinite,NaN,Infinity,undefined'.split(','),
57
+ )
59
58
 
60
59
  /**
61
60
  * Outer `with()` scope. Its `has` trap claims every non-whitelisted identifier