micra.js 2.1.0 → 2.2.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/llms-full.txt ADDED
@@ -0,0 +1,600 @@
1
+ # Micra.js — Full LLM Context (llms-full.txt)
2
+
3
+ This file follows the llmstxt.org "expanded" convention: it inlines code recipes so LLMs that crawl this URL get a complete training surface in one read. The short version is `llms.txt` in the same directory.
4
+
5
+ > Lightweight reactive TypeScript framework for server-rendered apps and small SaaS frontends. ~5 KB gzip. No build step required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install micra.js@^2.2.0
11
+ ```
12
+
13
+ ```ts
14
+ import * as Micra from 'micra.js'
15
+ ```
16
+
17
+ Or CDN (no build step):
18
+
19
+ ```html
20
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
21
+ ```
22
+
23
+ This exposes a global `Micra` object.
24
+
25
+ > **CDN choice matters for AI sandboxes.** Use `cdn.jsdelivr.net` (allowed by
26
+ > Claude artifacts, ChatGPT canvas, and most AI runtime CSPs). Do NOT use
27
+ > `unpkg.com` — it is blocked by those Content Security Policies and the script
28
+ > will fail to load. jsDelivr auto-mirrors every npm package.
29
+
30
+ ## When to use Micra.js
31
+
32
+ - Server-rendered pages (Rails, Laravel, Django, Phoenix, etc.) needing a small amount of reactivity
33
+ - Bundle-size-sensitive (~5 KB vs 45+ KB React)
34
+ - No build step desired — drop a `<script>` tag
35
+ - Existing HTML that just needs reactive enhancement
36
+
37
+ When NOT to use: full SPAs with client-side routing, deep nested reactivity needs, teams already on React/Vue.
38
+
39
+ ## Hard rules for code generation
40
+
41
+ ### Rule 0 — Artifact / canvas type
42
+
43
+ If you operate inside a runtime that lets you choose between a React-style
44
+ component artifact and a plain-HTML artifact (Claude artifacts, ChatGPT canvas,
45
+ v0, Bolt, Replit AI, etc.), you **MUST** create the **HTML** kind when generating
46
+ Micra.js code:
47
+
48
+ - Claude: `application/vnd.ant.html` (NOT `application/vnd.ant.react`)
49
+ - ChatGPT canvas: "HTML" output (NOT "React")
50
+ - v0 / Bolt / Replit AI: "Plain HTML" / "Vanilla HTML+JS" template (NOT Next.js / React)
51
+
52
+ These tools have a built-in heuristic that auto-selects React for "build a UI"
53
+ requests. When the user asked for Micra.js — explicitly, or via a shared recipe,
54
+ or via a project file — that signal **overrides** the React default. Do NOT
55
+ translate Micra patterns into React "because the artifact is React": change the
56
+ artifact type instead.
57
+
58
+ If you have already started writing `useState`, `useEffect`, JSX, or
59
+ `function App()` for a Micra task, stop and re-create the artifact as HTML.
60
+
61
+ ### When generating Micra code, do NOT:
62
+
63
+ 1. Use `document.querySelector` / `getElementById` / `el.innerHTML = ...` for component output.
64
+ 2. Use `addEventListener` inside a render-like method (it leaks past `destroy()`).
65
+ 3. Store derived values (counts, totals, filtered subsets) as `state` fields. They MUST be methods called from directives.
66
+ 4. Call `this.renderList()` / `this.update()` / `this.refresh()` after a state mutation. Micra batches a microtask render on every state write.
67
+ 5. Write `this.state.user.name = 'x'` (shallow proxy — nested mutations are invisible). Replace the top-level key.
68
+ 6. Use `@keydown.enter` (no key modifiers). Branch on `e.key === 'Enter'` inside the method.
69
+ 7. Write `data-model="filters.search"` (creates literal flat key `"filters.search"`, not a nested write).
70
+ 8. Import React, Vue, Alpine, htmx, or any other framework.
71
+
72
+ If you find yourself reaching for any of those — stop and rewrite with the patterns below.
73
+
74
+ ## Component shape
75
+
76
+ ```js
77
+ Micra.define('name', {
78
+ state: { /* flat raw data */ },
79
+
80
+ onCreate() { /* mounted, refs available, safe to fetch */ },
81
+ onDestroy() { /* clear timers, remove manual listeners */ },
82
+
83
+ // Derived values — methods, called from directives via `methodName()`
84
+ filtered() { return this.state.items.filter(...) },
85
+ totalCount() { return this.state.items.length },
86
+
87
+ // Actions — mutate state; Micra re-renders automatically
88
+ add() { this.state.items = [...this.state.items, x] },
89
+ })
90
+ Micra.start()
91
+ ```
92
+
93
+ ```html
94
+ <div data-component="name">…</div>
95
+ ```
96
+
97
+ ## `this` context
98
+
99
+ - `this.state` — reactive Proxy
100
+ - `this.$el` — root HTMLElement
101
+ - `this.refs` — `{ [name]: HTMLElement }` from `data-ref`
102
+ - `this.render()` — force sync re-render (rarely needed)
103
+ - `this.destroy()` — unmount and remove tracked listeners
104
+ - `this.prop(name, default?)` — read `data-*` attribute (auto-cast `"true"`/`"42"`)
105
+ - `this.fetch(url, options?)` — JSON + CSRF helper, throws `FetchError` on non-2xx
106
+ - `this.emit(event, payload?)` — emit on global bus
107
+ - `this.on(event, handler)` — subscribe on global bus, auto-unsubscribed on destroy
108
+
109
+ ## Directives
110
+
111
+ | Directive | Example | Behavior |
112
+ |-----------|---------|----------|
113
+ | `data-text` | `data-text="name"` | sets `textContent` |
114
+ | `data-html` | `data-html="content"` | sets `innerHTML` — ⚠️ XSS-prone, only with sanitized values |
115
+ | `data-if` | `data-if="count > 0"` | mounts/**unmounts** from DOM (true detach + re-insert) |
116
+ | `data-show` | `data-show="loaded"` | toggles `style.display` only — element stays in DOM |
117
+ | `data-bind` | `data-bind="href:url, disabled:loading"` | binds attributes; boolean → add/remove |
118
+ | `data-model` | `data-model="search"` | two-way binding; number/range → number; checkbox → boolean |
119
+ | `data-each` | `data-each="items" data-key="id"` | keyed list rendering on `<template>` |
120
+ | `data-ref` | `data-ref="canvas"` | `this.refs.canvas` |
121
+ | `data-class` | `data-class="active:isActive"` | additive class toggle |
122
+ | `data-on` | `data-on="click:save"` | event binding |
123
+ | `@event` | `@click="save"`, `@submit.prevent="…"` | shorthand event binding |
124
+
125
+ Modifiers: `.prevent`, `.stop`, `.self` (event-only — no key modifiers).
126
+
127
+ ## Expression context (what works inside `data-text="…"`)
128
+
129
+ - State keys: `count`, `user.name`
130
+ - Component methods (bound `this`): `format(price)`, `filtered()`
131
+ - Whitelisted globals: `Math`, `JSON`, `Date`, `String`, `Number`, `Boolean`, `Array`, `Object`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `NaN`, `Infinity`, `undefined`
132
+ - Inside `data-each`: `item`, `index`, `$index`
133
+
134
+ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeout`) resolves to `undefined` by design — security shadowing.
135
+
136
+ ---
137
+
138
+ # Recipes (full inline code)
139
+
140
+ ## Recipe 1 — Counter
141
+
142
+ ```html
143
+ <div data-component="counter">
144
+ <button @click="dec">−</button>
145
+ <strong data-text="count"></strong>
146
+ <button @click="inc">+</button>
147
+ </div>
148
+
149
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
150
+ <script>
151
+ Micra.define('counter', {
152
+ state: { count: 0 },
153
+ inc() { this.state.count++ },
154
+ dec() { this.state.count-- },
155
+ })
156
+ Micra.start()
157
+ </script>
158
+ ```
159
+
160
+ ## Recipe 2 — Todo app with localStorage and filters
161
+
162
+ ```html
163
+ <div data-component="todo-app">
164
+ <h1>Todo <small data-text="counterLabel()"></small></h1>
165
+
166
+ <div class="filters">
167
+ <button data-class="active:filter==='all'" @click="setFilter('all')">All</button>
168
+ <button data-class="active:filter==='active'" @click="setFilter('active')">Active</button>
169
+ <button data-class="active:filter==='done'" @click="setFilter('done')">Done</button>
170
+ </div>
171
+
172
+ <div class="row">
173
+ <input data-model="newTask" placeholder="New task..." @keydown="onKey" maxlength="200">
174
+ <button @click="addTask">Add</button>
175
+ </div>
176
+
177
+ <template data-each="filtered()" data-key="id">
178
+ <div class="item" data-class="done:item.done" data-bind="data-id:item.id">
179
+ <input type="checkbox" data-bind="checked:item.done" @change="toggle">
180
+ <span class="text" data-text="item.text"></span>
181
+ <button @click="remove">✕</button>
182
+ </div>
183
+ </template>
184
+
185
+ <p data-if="filtered().length === 0" data-text="emptyLabel()"></p>
186
+
187
+ <footer data-if="todos.length > 0">
188
+ <span data-text="leftLabel()"></span>
189
+ <button data-if="hasDone()" @click="clearDone">Clear done</button>
190
+ </footer>
191
+ </div>
192
+
193
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
194
+ <script>
195
+ Micra.define('todo-app', {
196
+ state: {
197
+ todos: JSON.parse(localStorage.getItem('todos') || '[]'),
198
+ newTask: '',
199
+ filter: 'all',
200
+ },
201
+ // ── derived values: methods, never state fields ─────────────
202
+ filtered() {
203
+ const { todos, filter } = this.state
204
+ if (filter === 'active') return todos.filter(t => !t.done)
205
+ if (filter === 'done') return todos.filter(t => t.done)
206
+ return todos
207
+ },
208
+ counterLabel() { return this.state.todos.length ? `(${this.state.todos.length})` : '' },
209
+ leftLabel() { const n = this.state.todos.filter(t => !t.done).length
210
+ return n ? `${n} left` : 'All done 🎉' },
211
+ hasDone() { return this.state.todos.some(t => t.done) },
212
+ emptyLabel() { return { all: 'No todos yet', active: 'No active todos', done: 'No done todos' }[this.state.filter] },
213
+ // ── persistence and id ──────────────────────────────────────
214
+ save() { localStorage.setItem('todos', JSON.stringify(this.state.todos)) },
215
+ nextId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 6) },
216
+ itemId(e) { return e.currentTarget.closest('[data-id]').dataset.id },
217
+ // ── actions ─────────────────────────────────────────────────
218
+ addTask() {
219
+ const text = this.state.newTask.trim()
220
+ if (!text) return
221
+ this.state.todos = [{ id: this.nextId(), text, done: false }, ...this.state.todos]
222
+ this.state.newTask = ''
223
+ this.save()
224
+ },
225
+ toggle(e) {
226
+ const id = this.itemId(e)
227
+ this.state.todos = this.state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
228
+ this.save()
229
+ },
230
+ remove(e) {
231
+ const id = this.itemId(e)
232
+ this.state.todos = this.state.todos.filter(t => t.id !== id)
233
+ this.save()
234
+ },
235
+ clearDone() { this.state.todos = this.state.todos.filter(t => !t.done); this.save() },
236
+ setFilter(f) { this.state.filter = f },
237
+ onKey(e) { if (e.key === 'Enter') this.addTask() },
238
+ })
239
+ Micra.start()
240
+ </script>
241
+ ```
242
+
243
+ ## Recipe 3 — Data table with search
244
+
245
+ ```html
246
+ <div data-component="users-table">
247
+ <input data-model="query" placeholder="Search users…">
248
+ <p data-text="summary()"></p>
249
+
250
+ <table>
251
+ <thead><tr><th>Name</th><th>Email</th><th>Role</th></tr></thead>
252
+ <tbody>
253
+ <template data-each="filtered()" data-key="id">
254
+ <tr>
255
+ <td data-text="item.name"></td>
256
+ <td data-text="item.email"></td>
257
+ <td data-text="item.role"></td>
258
+ </tr>
259
+ </template>
260
+ </tbody>
261
+ </table>
262
+ <p data-if="filtered().length === 0">No matches.</p>
263
+ </div>
264
+
265
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
266
+ <script>
267
+ Micra.define('users-table', {
268
+ state: {
269
+ query: '',
270
+ users: [
271
+ { id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'admin' },
272
+ { id: 2, name: 'Linus Torvalds', email: 'linus@example.com', role: 'dev' },
273
+ { id: 3, name: 'Grace Hopper', email: 'grace@example.com', role: 'admin' },
274
+ { id: 4, name: 'Margaret Hamilton',email: 'maggie@example.com', role: 'dev' },
275
+ ],
276
+ },
277
+ filtered() {
278
+ const q = this.state.query.toLowerCase().trim()
279
+ if (!q) return this.state.users
280
+ return this.state.users.filter(u =>
281
+ u.name.toLowerCase().includes(q) ||
282
+ u.email.toLowerCase().includes(q) ||
283
+ u.role.toLowerCase().includes(q),
284
+ )
285
+ },
286
+ summary() {
287
+ return `${this.filtered().length} of ${this.state.users.length} users`
288
+ },
289
+ })
290
+ Micra.start()
291
+ </script>
292
+ ```
293
+
294
+ ## Recipe 4 — Form with validation and async submit
295
+
296
+ ```html
297
+ <form data-component="invite-form" @submit.prevent="submit">
298
+ <input data-model="email" type="email" placeholder="Email">
299
+ <button data-bind="disabled:loading">
300
+ <span data-text="loading ? 'Sending…' : 'Send invite'"></span>
301
+ </button>
302
+ <p data-if="error" data-text="error" style="color:red"></p>
303
+ <p data-if="success">Invitation sent ✓</p>
304
+ </form>
305
+
306
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
307
+ <script>
308
+ Micra.define('invite-form', {
309
+ state: { email: '', loading: false, error: '', success: false },
310
+ isValid() { return this.state.email.includes('@') && this.state.email.includes('.') },
311
+ async submit() {
312
+ if (!this.isValid()) { this.state.error = 'Invalid email'; return }
313
+ this.state.loading = true
314
+ this.state.error = ''
315
+ this.state.success = false
316
+ try {
317
+ await this.fetch('/api/invite', { method: 'POST', body: { email: this.state.email } })
318
+ this.state.success = true
319
+ this.state.email = ''
320
+ } catch (e) {
321
+ this.state.error = e.message
322
+ } finally {
323
+ this.state.loading = false
324
+ }
325
+ },
326
+ })
327
+ Micra.start()
328
+ </script>
329
+ ```
330
+
331
+ ## Recipe 5 — Modal via event bus
332
+
333
+ ```html
334
+ <!-- ANYWHERE on the page: -->
335
+ <button data-component="open-modal-btn" @click="open">Delete account</button>
336
+
337
+ <!-- The modal lives once, listens for events: -->
338
+ <div data-component="confirm-modal">
339
+ <div data-if="show" class="backdrop" @click.self="close" style="position:fixed;inset:0;background:#0007;display:flex;align-items:center;justify-content:center">
340
+ <div class="dialog" style="background:white;padding:24px;border-radius:8px">
341
+ <p data-text="message"></p>
342
+ <button @click="confirm">Yes</button>
343
+ <button @click="close">Cancel</button>
344
+ </div>
345
+ </div>
346
+ </div>
347
+
348
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
349
+ <script>
350
+ Micra.define('open-modal-btn', {
351
+ open() {
352
+ this.emit('modal:confirm', {
353
+ message: 'Are you sure you want to delete your account?',
354
+ onYes: () => this.fetch('/api/account/delete', { method: 'DELETE' }),
355
+ })
356
+ },
357
+ })
358
+
359
+ Micra.define('confirm-modal', {
360
+ state: { show: false, message: '', onYes: null },
361
+ onCreate() {
362
+ this.on('modal:confirm', ({ message, onYes }) => {
363
+ this.state.message = message
364
+ this.state.onYes = onYes
365
+ this.state.show = true
366
+ })
367
+ },
368
+ async confirm() {
369
+ try { await this.state.onYes?.() } finally { this.close() }
370
+ },
371
+ close() {
372
+ this.state.show = false
373
+ this.state.onYes = null
374
+ },
375
+ })
376
+ Micra.start()
377
+ </script>
378
+ ```
379
+
380
+ ## Recipe 6 — Tabs (multiple components on one page)
381
+
382
+ ```html
383
+ <div data-component="tabs">
384
+ <nav>
385
+ <button @click="select" data-bind="data-tab:'overview'"
386
+ data-class="active:tab === 'overview'">Overview</button>
387
+ <button @click="select" data-bind="data-tab:'billing'"
388
+ data-class="active:tab === 'billing'">Billing</button>
389
+ <button @click="select" data-bind="data-tab:'security'"
390
+ data-class="active:tab === 'security'">Security</button>
391
+ </nav>
392
+ <section data-if="tab === 'overview'">Overview content</section>
393
+ <section data-if="tab === 'billing'">Billing content</section>
394
+ <section data-if="tab === 'security'">Security content</section>
395
+ </div>
396
+
397
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
398
+ <script>
399
+ Micra.define('tabs', {
400
+ state: { tab: 'overview' },
401
+ select(e) { this.state.tab = e.currentTarget.dataset.tab },
402
+ })
403
+ Micra.start()
404
+ </script>
405
+ ```
406
+
407
+ ## Recipe 7 — SSR component reading server-rendered props
408
+
409
+ ```html
410
+ <!-- Server emits: -->
411
+ <div data-component="user-card" data-user-id="42" data-plan="pro">
412
+ <h2 data-text="name"></h2>
413
+ <span data-text="plan"></span>
414
+ <button @click="upgrade" data-if="plan !== 'enterprise'">Upgrade</button>
415
+ </div>
416
+
417
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
418
+ <script>
419
+ Micra.define('user-card', {
420
+ state: { name: '', plan: '' },
421
+ async onCreate() {
422
+ const userId = this.prop('userId') // 42 (number)
423
+ this.state.plan = this.prop('plan', 'free')
424
+ const user = await this.fetch(`/api/users/${userId}`)
425
+ this.state.name = user.name
426
+ },
427
+ async upgrade() {
428
+ await this.fetch('/api/upgrade', { method: 'POST', body: { plan: 'enterprise' } })
429
+ this.state.plan = 'enterprise'
430
+ },
431
+ })
432
+ Micra.start()
433
+ </script>
434
+ ```
435
+
436
+ ## Recipe 8 — Search with debounce
437
+
438
+ ```html
439
+ <div data-component="search">
440
+ <input data-model="query" @input="debouncedSearch" placeholder="Search…">
441
+ <p data-if="loading">Searching…</p>
442
+ <template data-each="results" data-key="id">
443
+ <div data-text="item.title"></div>
444
+ </template>
445
+ <p data-if="!loading && results.length === 0 && query">No results.</p>
446
+ </div>
447
+
448
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
449
+ <script>
450
+ Micra.define('search', {
451
+ state: { query: '', results: [], loading: false },
452
+ _timer: null,
453
+
454
+ debouncedSearch() {
455
+ clearTimeout(this._timer)
456
+ this._timer = setTimeout(() => this.search(), 250)
457
+ },
458
+ async search() {
459
+ const q = this.state.query.trim()
460
+ if (!q) { this.state.results = []; return }
461
+ this.state.loading = true
462
+ try {
463
+ this.state.results = await this.fetch('/api/search', { q })
464
+ } finally {
465
+ this.state.loading = false
466
+ }
467
+ },
468
+ onDestroy() { clearTimeout(this._timer) },
469
+ })
470
+ Micra.start()
471
+ </script>
472
+ ```
473
+
474
+ ## Recipe 9 — Chart with `data-ref` (imperative third-party API)
475
+
476
+ ```html
477
+ <div data-component="revenue-chart" data-endpoint="/api/revenue">
478
+ <canvas data-ref="canvas" width="600" height="300"></canvas>
479
+ <p data-if="loading">Loading chart…</p>
480
+ </div>
481
+
482
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
483
+ <script>
484
+ Micra.define('revenue-chart', {
485
+ state: { loading: true },
486
+ _chart: null,
487
+ async onCreate() {
488
+ const endpoint = this.prop('endpoint', '/api/revenue')
489
+ const data = await this.fetch(endpoint)
490
+ this._chart = new Chart(this.refs.canvas, { type: 'line', data })
491
+ this.state.loading = false
492
+ },
493
+ onDestroy() { this._chart?.destroy() },
494
+ })
495
+ Micra.start()
496
+ </script>
497
+ ```
498
+
499
+ ## Recipe 10 — Multiple islands on one page (cross-component coordination)
500
+
501
+ ```html
502
+ <div data-component="search-bar">
503
+ <input data-model="query" @input="search" placeholder="Search products…">
504
+ </div>
505
+
506
+ <div data-component="results-table">
507
+ <p data-if="loading">Loading…</p>
508
+ <template data-each="rows" data-key="id">
509
+ <div class="row" data-text="item.title"></div>
510
+ </template>
511
+ <p data-if="!loading && rows.length === 0">No results.</p>
512
+ </div>
513
+
514
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
515
+ <script>
516
+ Micra.define('search-bar', {
517
+ state: { query: '' },
518
+ search() { Micra.emit('search', { query: this.state.query }) },
519
+ })
520
+
521
+ Micra.define('results-table', {
522
+ state: { rows: [], loading: false },
523
+ onCreate() {
524
+ this.on('search', async ({ query }) => {
525
+ if (!query) { this.state.rows = []; return }
526
+ this.state.loading = true
527
+ try { this.state.rows = await this.fetch('/api/search', { q: query }) }
528
+ finally { this.state.loading = false }
529
+ })
530
+ },
531
+ })
532
+ Micra.start()
533
+ </script>
534
+ ```
535
+
536
+ ---
537
+
538
+ # Anti-pattern reference (what LLMs gravitate to — DO NOT)
539
+
540
+ ```js
541
+ // ❌ Hand-rolled list rendering — defeats data-each
542
+ document.getElementById('list').innerHTML = items.map(...).join('')
543
+
544
+ // ❌ Derived state as field — diverges from source of truth
545
+ state: { todos: [], totalCount: 0, hasDone: false }
546
+ updateComputeds() { this.state.totalCount = this.state.todos.length }
547
+
548
+ // ❌ addEventListener in render — leaks past destroy()
549
+ createItem(item) {
550
+ const el = document.createElement('div')
551
+ el.addEventListener('click', () => this.toggle(item.id))
552
+ }
553
+
554
+ // ❌ Manual re-render after mutation — Micra already does this
555
+ addTask() { this.state.todos = [...]; this.renderList() }
556
+
557
+ // ❌ Nested state mutation — shallow proxy can't see it
558
+ this.state.user.email = 'x'
559
+
560
+ // ❌ Key modifier on @event — not supported
561
+ <input @keydown.enter="add">
562
+
563
+ // ❌ Nested path in data-model — writes literal flat key
564
+ <input data-model="filters.search">
565
+
566
+ // ❌ React/Vue/Alpine imports — not Micra
567
+ import React, { useState } from 'react'
568
+ import { ref, computed } from 'vue'
569
+ import Alpine from 'alpinejs'
570
+
571
+ // ❌ unpkg CDN — blocked by Claude artifacts and most AI sandbox CSPs
572
+ <script src="https://unpkg.com/micra.js@2.2.0/dist/micra.min.js"></script>
573
+ // ✅ Use jsDelivr instead — it auto-mirrors npm and is CSP-allowlisted everywhere
574
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
575
+ ```
576
+
577
+ # Final checklist
578
+
579
+ Before returning generated Micra code, verify ALL of these:
580
+
581
+ - [ ] Every list uses `<template data-each>` with `data-key`. No `getElementById`/`innerHTML` for lists.
582
+ - [ ] Every derived value (count, total, filtered, formatted) is a **method** on the component, called from a directive via `methodName()`. No state field for things that can be computed.
583
+ - [ ] Every event handler is `@event` or `data-on`. No `addEventListener` inside methods.
584
+ - [ ] No `this.renderList()` / `this.update()` / `this.refresh()` calls after mutations.
585
+ - [ ] State writes are top-level: `state.x = …` or `state.user = { …state.user, … }`. Never `state.user.x = …`.
586
+ - [ ] No `@keydown.enter` — branch on `e.key` in the method instead.
587
+ - [ ] `Micra.start()` is at the end of the script.
588
+ - [ ] No React/Vue/Alpine imports.
589
+ - [ ] All timers / external listeners in `onCreate` are cleaned up in `onDestroy`.
590
+ - [ ] CDN script src is `cdn.jsdelivr.net/npm/micra.js@…`, never `unpkg.com` (CSP-blocked in Claude/AI sandboxes).
591
+ - [ ] Artifact / canvas type is **HTML** (`application/vnd.ant.html` in Claude), NOT React/Next/Vue. Micra cannot run in a React artifact.
592
+
593
+ # Links
594
+
595
+ - Full docs: https://denisfl.github.io/micra.js/
596
+ - Source: https://github.com/denisfl/micra.js
597
+ - npm: https://www.npmjs.com/package/micra.js
598
+ - LLM short guide: https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md
599
+ - Recipes directory: https://github.com/denisfl/micra.js/tree/master/docs/recipes
600
+ - Short llms.txt: https://github.com/denisfl/micra.js/blob/master/llms.txt