micra.js 2.1.0 → 2.2.1
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 +244 -0
- package/README.md +39 -14
- package/dist/core/mount.d.ts +8 -2
- package/dist/core/reactive.d.ts +1 -1
- package/dist/core/registry.d.ts +13 -4
- package/dist/dom/directives.d.ts +11 -15
- package/dist/dom/each.d.ts +10 -7
- package/dist/dom/events.d.ts +15 -5
- package/dist/dom/refs.d.ts +5 -5
- package/dist/dom/scan.d.ts +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +302 -177
- package/dist/micra.cjs.js.map +4 -4
- package/dist/micra.esm.js +302 -177
- package/dist/micra.esm.js.map +4 -4
- package/dist/micra.js +302 -177
- package/dist/micra.js.map +4 -4
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +67 -22
- package/llms-full.txt +600 -0
- package/llms.txt +148 -0
- package/package.json +11 -3
- package/src/core/mount.ts +136 -99
- package/src/core/reactive.ts +2 -1
- package/src/core/registry.ts +19 -9
- package/src/dom/directives.ts +39 -122
- package/src/dom/each.ts +133 -37
- package/src/dom/events.ts +23 -31
- package/src/dom/refs.ts +7 -7
- package/src/dom/scan.ts +189 -0
- package/src/index.ts +2 -0
- package/src/types.ts +80 -22
- package/src/utils/expr.ts +34 -21
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.1
|
|
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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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
|