@jsweb/ui 1.3.0 → 1.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/SKILL.md ADDED
@@ -0,0 +1,609 @@
1
+ ---
2
+ name: jsweb-ui
3
+ description: Comprehensive guide and operational instructions for building reactive user interfaces with the @jsweb/ui micro-framework. Use this skill whenever creating, modifying, or refactoring web applications that use @jsweb/ui, or when building fine-grained reactive HTML components with custom directives (ui:*, :*, ui@*, @*), two-way data binding, conditional rendering, keyed list loops, DOM refs, and shared reactive stores without a Virtual DOM.
4
+ ---
5
+
6
+ # @jsweb/ui Skill Guide
7
+
8
+ `@jsweb/ui` is a lightweight, zero-dependency frontend micro-framework written in TypeScript. It delivers Vue 3-inspired fine-grained reactivity (`Proxy` + `ReactiveEffect`) combined with Alpine.js-style declarative HTML attributes, applying updates directly to the real DOM without any Virtual DOM overhead.
9
+
10
+ ---
11
+
12
+ ## 1. Quick Start & Installation
13
+
14
+ ### NPM / Bundler (Vite, Webpack, Rollup)
15
+
16
+ ```bash
17
+ npm install @jsweb/ui
18
+ ```
19
+
20
+ ```typescript
21
+ import { createScope, reactive, watch } from '@jsweb/ui'
22
+
23
+ const state = reactive({
24
+ count: 0,
25
+ increment() {
26
+ this.count++
27
+ },
28
+ })
29
+
30
+ createScope('#app', state)
31
+ ```
32
+
33
+ ### Standalone CDN (Direct HTML / No Build Step)
34
+
35
+ Available via unpkg or jsDelivr. Exposes the global object `window.jsweb.ui`:
36
+
37
+ ```html
38
+ <!doctype html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8" />
42
+ <script src="https://unpkg.com/@jsweb/ui"></script>
43
+ </head>
44
+ <body>
45
+ <div :scope="{ count: 0 }">
46
+ <h1 :text="count"></h1>
47
+ <button @click="count++">+</button>
48
+ </div>
49
+
50
+ <script>
51
+ const { createScope } = window.jsweb.ui
52
+ createScope('body')
53
+ </script>
54
+ </body>
55
+ </html>
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 2. Core Architecture & Mental Model
61
+
62
+ 1. **No Virtual DOM**: Changes to reactive properties mutate the real DOM directly using targeted granular effects.
63
+ 2. **Deep Proxy Reactivity**: State created with `reactive()` wraps objects and arrays into reactive Proxies. Mutating nested properties or calling array methods (`push`, `pop`, `splice`, `shift`, etc.) triggers targeted DOM re-renders.
64
+ 3. **Template Directives Cleanup**: Directive attributes (`:text`, `:bind`, `:if`, `:for`, `:class`, etc.) are evaluated and immediately removed from DOM elements after mounting, keeping the HTML clean.
65
+ 4. **Context Hierarchy**: Scopes inherit variables from outer scopes or parent contexts. Expressions are safely evaluated in the context scope with dynamic evaluation (`with(this)`).
66
+ 5. **Memory Safe & Auto-Cleanup**: When elements are removed (via `:if` or `:for`), all associated event listeners and reactive effects are automatically cleaned up.
67
+
68
+ ---
69
+
70
+ ## 3. JavaScript / TypeScript API Reference
71
+
72
+ ### `reactive(target)`
73
+
74
+ Creates a deep reactive Proxy of the given object or array.
75
+
76
+ - **Object Literal Typing (`ThisType`)**: Wrapped in `ThisType<T & ScopeContext>`, enabling complete IDE autocompletion and strict type checking for `this.property`, `this.method()`, computeds via getters, and framework helpers (`this.$refs`, `this.$emit`).
77
+ - **Class Instances**: Fully supports TypeScript class instances (including those extending `Scope`).
78
+ - **Deep tracking**: Accessing nested objects returns nested reactive proxies automatically.
79
+ - **Array mutations**: Tracks index sets and automatically triggers updates for array length changes.
80
+ - **Computed properties**: Use native JavaScript getters (`get prop() { return ... }`). When accessed inside templates or effects, dependencies are automatically tracked.
81
+ - **Bypassed types**: `Map`, `Set`, `WeakMap`, `WeakSet`, `Date`, `RegExp`, and DOM `Node` instances are preserved as-is without proxy wrapping.
82
+
83
+ ```typescript
84
+ import { reactive } from '@jsweb/ui'
85
+
86
+ const store = reactive({
87
+ items: ['Learn @jsweb/ui', 'Build an app'],
88
+ filter: '',
89
+ // Native getter acts as a computed property:
90
+ get filteredItems() {
91
+ return this.items.filter((item) =>
92
+ item.toLowerCase().includes(this.filter.toLowerCase()),
93
+ )
94
+ },
95
+ addItem(text: string) {
96
+ if (text.trim()) {
97
+ this.items.push(text.trim())
98
+ this.$emit('item-added', text.trim())
99
+ }
100
+ },
101
+ })
102
+ ```
103
+
104
+ ---
105
+
106
+ ### `Scope` Class & `ScopeContext` Interface
107
+
108
+ For Object-Oriented component architecture, extend the base `Scope` class. `$refs` is implemented as a read-only `Map` and `$emit` has a standard implementation as `protected`, allowing subclasses to override it:
109
+
110
+ ```typescript
111
+ export interface ScopeContext {
112
+ /** Read-only Map indexing referenced DOM nodes */
113
+ readonly $refs: Map<string, any>
114
+ /** Dispatches CustomEvents */
115
+ $emit: (event: string, detail?: any) => void
116
+ /** Root DOM element attached to the scope (read-only) */
117
+ readonly $el: HTMLElement
118
+ /** Current loop index */
119
+ $index?: number
120
+ /** Unique key for loop iteration */
121
+ $key?: any
122
+ }
123
+
124
+ export class Scope {
125
+ readonly $el: HTMLElement
126
+ protected readonly $refs: Map<string, any>
127
+ protected $emit(event: string, detail?: any): void
128
+ declare $index?: number
129
+ declare $key?: any
130
+ constructor(init?: Record<string, any>)
131
+ }
132
+ ```
133
+
134
+ #### OOP Example:
135
+
136
+ ```typescript
137
+ import { createScope, reactive, Scope } from '@jsweb/ui'
138
+
139
+ class TodoListScope extends Scope {
140
+ tasks: string[] = []
141
+ newTask = ''
142
+
143
+ get count() {
144
+ return this.tasks.length
145
+ }
146
+
147
+ addTask() {
148
+ if (this.newTask.trim()) {
149
+ this.tasks.push(this.newTask.trim())
150
+ this.newTask = ''
151
+ this.$emit('tasks-updated', this.tasks)
152
+ this.$refs.get('taskInput')?.focus()
153
+ }
154
+ }
155
+
156
+ // Can override $emit if custom logic is needed:
157
+ protected override $emit(event: string, detail?: any) {
158
+ console.log(`[Event: ${event}]`, detail)
159
+ super.$emit(event, detail)
160
+ }
161
+ }
162
+
163
+ const todoScope = reactive(new TodoListScope())
164
+ createScope('#todo-app', todoScope)
165
+ ```
166
+
167
+ ---
168
+
169
+ ### `watch<T>(source, callback, options?)`
170
+
171
+ Watches a reactive property or getter function and fires a callback when its value changes.
172
+
173
+ - `source`: A getter function `() => value` or a reactive object/array directly (auto-traversed).
174
+ - `callback`: `(newValue: T, oldValue: T | undefined) => void`.
175
+ - `options`: `{ immediate?: boolean }`. If `immediate: true`, the callback runs immediately on registration (`oldValue` will be `undefined`).
176
+ - **Returns**: A `stop()` function to cancel the watcher.
177
+
178
+ ```typescript
179
+ import { reactive, watch } from '@jsweb/ui'
180
+
181
+ const state = reactive({ count: 0 })
182
+
183
+ const unwatch = watch(
184
+ () => state.count,
185
+ (newVal, oldVal) => {
186
+ console.log(`Count changed from ${oldVal} to ${newVal}`)
187
+ },
188
+ { immediate: true },
189
+ )
190
+
191
+ // When done:
192
+ // unwatch()
193
+ ```
194
+
195
+ ---
196
+
197
+ ### `createScope<T>(selectorOrElement, context?)`
198
+
199
+ Mounts the reactive engine on a DOM element and parses directives within that tree.
200
+
201
+ - `selectorOrElement`: A CSS selector string (e.g. `'#app'`, `'body'`) or an `HTMLElement`.
202
+ - `context`: An optional initial state object or shared context, typed contextually with `ThisType<T & ScopeContext>`.
203
+ - **Automatic Injections**:
204
+ - `context.$emit`: Dispatches bubbling, composed `CustomEvent`s.
205
+ - `context.$refs`: Native `Map<string, any>` storing DOM element references.
206
+
207
+ ```typescript
208
+ import { createScope, reactive } from '@jsweb/ui'
209
+
210
+ const appState = reactive({
211
+ title: 'My Application',
212
+ user: { name: 'Alice' },
213
+ })
214
+
215
+ createScope('#app', appState)
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 4. Template Directives & Attributes
221
+
222
+ All directives support both the full syntax (`ui:*`, `ui@*`) and the concise shorthand (`:*`, `@*`). The shorthand is recommended for concise templates.
223
+
224
+ | Directive | Shorthand | Purpose | Example |
225
+ | :----------- | :--------- | :----------------------------------------------------- | :----------------------------------------- |
226
+ | `ui:scope` | `:scope` | Defines local state scope for element and descendants | `<div :scope="{ open: false }">` |
227
+ | `ui:text` | `:text` | Sets reactive `textContent` | `<span :text="username"></span>` |
228
+ | `ui:bind` | `:bind` | Two-way data binding for form inputs | `<input type="text" :bind="query" />` |
229
+ | `ui:if` | `:if` | Conditionally mounts/unmounts element in DOM | `<div :if="isLoggedIn">Welcome</div>` |
230
+ | `ui:for` | `:for` | Repeats element over an array (`in` or `of`) | `<li :for="item of items">` |
231
+ | `ui:key` | `:key` | Reconciliation key for list recycling | `<li :for="user of users" :key="user.id">` |
232
+ | `ui:class` | `:class` | Reactive CSS classes (Object, Array, or String) | `<div :class="{ active: isActive }">` |
233
+ | `ui:style` | `:style` | Reactive inline styles (Object key-value) | `<div :style="{ color: themeColor }">` |
234
+ | `ui:ref` | `:ref` | Registers DOM element into `$refs` `Map` | `<input :ref="searchField" />` |
235
+ | `ui:[attr]` | `:[attr]` | Dynamic attribute binding (`:disabled`, `:href`, etc.) | `<button :disabled="isSubmitting">` |
236
+ | `ui@[event]` | `@[event]` | Event listener with modifier support | `<button @click.prevent="submit">` |
237
+
238
+ ---
239
+
240
+ ### Detailed Directive Specifications
241
+
242
+ #### 1. `:scope`
243
+
244
+ Sets up a local reactive scope. If an object is supplied, it is merged into the context hierarchy.
245
+
246
+ ```html
247
+ <div :scope="{ count: 0, title: 'Counter' }">
248
+ <h2 :text="title"></h2>
249
+ <button @click="count++">Increment</button>
250
+ <span :text="count"></span>
251
+ </div>
252
+ ```
253
+
254
+ #### 2. `:text`
255
+
256
+ Updates `textContent`. If the evaluated value is `null` or `undefined`, sets empty text `""`.
257
+
258
+ ```html
259
+ <p>Total: <strong :text="totalPrice"></strong></p>
260
+ ```
261
+
262
+ #### 3. `:bind` (Two-Way Data Binding)
263
+
264
+ Automatically handles different input types:
265
+
266
+ - **Text / Number / Search / Textarea**: Binds `value` property, updates state on the `'input'` event.
267
+ - **Checkbox (`type="checkbox"`)**: Binds boolean `checked` property, updates state on `'change'`.
268
+ - **Radio (`type="radio"`)**: Sets `checked = (el.value === String(stateValue))`, updates state on `'change'`.
269
+ - **Select (`<select>`)**: Binds `value` property, updates state on `'change'`.
270
+
271
+ ```html
272
+ <!-- Text / Textarea -->
273
+ <input type="text" :bind="user.name" />
274
+ <textarea :bind="user.bio"></textarea>
275
+
276
+ <!-- Checkbox (boolean) -->
277
+ <input type="checkbox" :bind="user.agreeToTerms" />
278
+
279
+ <!-- Radios -->
280
+ <input type="radio" name="plan" value="free" :bind="selectedPlan" /> Free
281
+ <input type="radio" name="plan" value="pro" :bind="selectedPlan" /> Pro
282
+
283
+ <!-- Select -->
284
+ <select :bind="selectedCountry">
285
+ <option value="BR">Brazil</option>
286
+ <option value="US">USA</option>
287
+ </select>
288
+ ```
289
+
290
+ #### 4. `:if`
291
+
292
+ Performs true conditional rendering. When false, the element is detached from the DOM and replaced by a lightweight comment node placeholder. When true, it is inserted back.
293
+
294
+ ```html
295
+ <div :if="errorMessage" class="alert alert-danger">
296
+ <span :text="errorMessage"></span>
297
+ </div>
298
+ ```
299
+
300
+ #### 5. `:for` and `:key`
301
+
302
+ Iterates over an array using `item of items` or `item in items`.
303
+
304
+ - **Always provide `:key`** for optimal performance and reliable reconciliation.
305
+ - Exposes `$index` (0-based numeric index) and `$key` in the loop's context.
306
+ - Recycles existing DOM nodes based on key matches, preserving focus, scroll, and component state.
307
+
308
+ ```html
309
+ <ul>
310
+ <li :for="task of tasks" :key="task.id">
311
+ <span :text="$index + 1"></span>.
312
+ <span :text="task.title"></span>
313
+ <button @click="removeTask(task.id)">Delete</button>
314
+ </li>
315
+ </ul>
316
+ ```
317
+
318
+ #### 6. `:class`
319
+
320
+ Dynamically manages CSS classes without removing existing static classes on the element.
321
+
322
+ - **Object Syntax**: Keys are class names, values are boolean conditions.
323
+ ```html
324
+ <div
325
+ class="card"
326
+ :class="{ 'card-active': isActive, 'border-danger': hasError }"
327
+ ></div>
328
+ ```
329
+ - **Array Syntax**: Array of class name strings or ternary expressions.
330
+ ```html
331
+ <div
332
+ :class="['badge', isPrimary ? 'badge-primary' : 'badge-secondary']"
333
+ ></div>
334
+ ```
335
+ - **String Syntax**: Single dynamic class name string.
336
+ ```html
337
+ <div :class="themeClass"></div>
338
+ ```
339
+
340
+ #### 7. `:style`
341
+
342
+ Receives an object of CSS properties. Automatically removes previous styles when keys are removed or cleared.
343
+
344
+ ```html
345
+ <div
346
+ :style="{ backgroundColor: bgColor, transform: `scale(${scale})`, opacity: isVisible ? 1 : 0 }"
347
+ ></div>
348
+ ```
349
+
350
+ #### 8. `:ref` and `$refs`
351
+
352
+ Registers the DOM element into the context's `$refs` `Map`.
353
+
354
+ - **Single Element**: Directly stored as the `HTMLElement`.
355
+ ```html
356
+ <input type="text" :ref="nameInput" />
357
+ <button @click="$refs.get('nameInput').focus()">Focus Input</button>
358
+ ```
359
+ - **Inside Keyed `:for` Loops**: Stored as a nested `Map<Key, HTMLElement>`.
360
+ ```html
361
+ <ul>
362
+ <li :for="item of items" :key="item.id">
363
+ <input :ref="itemField" :value="item.name" />
364
+ <button @click="$refs.get('itemField').get(item.id)?.focus()">
365
+ Focus this row
366
+ </button>
367
+ </li>
368
+ </ul>
369
+ ```
370
+ - **Automatic Lifecycle Cleanup**: When an element leaves the DOM (via `:if` or `:for`), its entry is removed from the `$refs` map to prevent memory leaks.
371
+
372
+ #### 9. Dynamic Attributes (`:[attr]`)
373
+
374
+ Binds any HTML attribute dynamically:
375
+
376
+ - If evaluated value is `false`, `null`, or `undefined`: attribute is removed.
377
+ - If evaluated value is `true`: attribute is added with empty value (`el.setAttribute(attr, '')`).
378
+ - Otherwise: `el.setAttribute(attr, String(value))`.
379
+
380
+ ```html
381
+ <button :disabled="isLoading">Submit</button>
382
+ <a :href="user.profileUrl" :title="user.bio">Profile</a>
383
+ <img :src="imageUrl" :alt="imageAlt" />
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 5. Event Handling & Modifiers
389
+
390
+ ### Event Directives (`@[event]` / `ui@[event]`)
391
+
392
+ Attach DOM event listeners.
393
+
394
+ ```html
395
+ <!-- Method reference: automatically receives $event as first argument -->
396
+ <button @click="handleClick">Click Me</button>
397
+
398
+ <!-- Inline expression: $event is explicitly available -->
399
+ <button @click="handleClick($event, 'customArg')">Click Me</button>
400
+
401
+ <!-- Direct mutation -->
402
+ <button @click="count++">Increment</button>
403
+ ```
404
+
405
+ ### Event Modifiers
406
+
407
+ Modifiers can be chained directly to the event name:
408
+
409
+ - **`.prevent`**: Calls `$event.preventDefault()`.
410
+ - **`.stop`**: Calls `$event.stopPropagation()`.
411
+ - **`.self`**: Only executes the handler if `$event.target === el` (the event originated on the bound element itself).
412
+ - **`.outside`**: Dispatches when a click or event occurs outside the element (ideal for dropdowns, tooltips, and modals). Sets up a document listener with automatic cleanup when the element unmounts.
413
+
414
+ ```html
415
+ <!-- Prevent default form submission -->
416
+ <form @submit.prevent="saveData">
417
+ <input :bind="formData.title" />
418
+ <button type="submit">Save</button>
419
+ </form>
420
+
421
+ <!-- Stop event propagation -->
422
+ <div @click="outerClick">
423
+ <button @click.stop="innerClick">Inner</button>
424
+ </div>
425
+
426
+ <!-- Click outside to close modal or dropdown -->
427
+ <div class="dropdown-menu" :if="isOpen" @click.outside="isOpen = false">
428
+ <p>Dropdown Content</p>
429
+ </div>
430
+ ```
431
+
432
+ ---
433
+
434
+ ## 6. Context Helpers & Variables
435
+
436
+ Available within template expressions and component methods:
437
+
438
+ | Variable | Type | Description |
439
+ | :---------------------- | :----------------- | :-------------------------------------------------------------------------- |
440
+ | `$refs` | `Map<string, any>` | Map of registered element refs. |
441
+ | `$emit(event, detail?)` | `Function` | Dispatches a bubbling composed `CustomEvent` from the scope's root element. |
442
+ | `$el` | `HTMLElement` | The root DOM element attached to the scope (read-only). |
443
+ | `$event` | `Event` | The native DOM event object (available in event expressions). |
444
+ | `$index` | `number` | The current 0-based iteration index in a `:for` loop. |
445
+ | `$key` | `any` | The current key value evaluated for the item in a `:for` loop. |
446
+
447
+ ### Component Communication with `$emit`
448
+
449
+ Child scopes can dispatch events up the DOM tree, and parents can listen with standard `@` event syntax:
450
+
451
+ ```html
452
+ <div id="parent-component" @task-added="handleNewTask">
453
+ <!-- Child Component / Scope -->
454
+ <div :scope="{ newTaskName: '' }">
455
+ <input :bind="newTaskName" placeholder="New task..." />
456
+ <button
457
+ @click="$emit('task-added', { name: newTaskName }); newTaskName = ''"
458
+ >
459
+ Add Task
460
+ </button>
461
+ </div>
462
+ </div>
463
+ ```
464
+
465
+ ---
466
+
467
+ ## 7. Common Patterns & Best Practices
468
+
469
+ ### Pattern 1: Standalone Interactive Widget (HTML Only)
470
+
471
+ No build step needed. Place everything in an HTML file:
472
+
473
+ ```html
474
+ <div
475
+ id="calculator"
476
+ :scope="{
477
+ a: 0,
478
+ b: 0,
479
+ get sum() { return Number(this.a) + Number(this.b) }
480
+ }"
481
+ >
482
+ <input type="number" :bind="a" /> + <input type="number" :bind="b" /> =
483
+ <span :text="sum"></span>
484
+ </div>
485
+
486
+ <script>
487
+ window.jsweb.ui.createScope('#calculator')
488
+ </script>
489
+ ```
490
+
491
+ ### Pattern 2: Global Shared Store (TypeScript / ESM)
492
+
493
+ Export a `reactive` store from a module and import it across multiple views or components:
494
+
495
+ ```typescript
496
+ // store.ts
497
+ import { reactive } from '@jsweb/ui'
498
+
499
+ export const authStore = reactive({
500
+ user: null as { name: string; token: string } | null,
501
+ get isAuthenticated() {
502
+ return this.user !== null
503
+ },
504
+ login(name: string, token: string) {
505
+ this.user = { name, token }
506
+ },
507
+ logout() {
508
+ this.user = null
509
+ },
510
+ })
511
+ ```
512
+
513
+ ```typescript
514
+ // main.ts
515
+ import { createScope } from '@jsweb/ui'
516
+ import { authStore } from './store'
517
+
518
+ createScope('#navbar', { auth: authStore })
519
+ createScope('#main-content', { auth: authStore })
520
+ ```
521
+
522
+ ### Pattern 3: Full CRUD List with Keys & Refs
523
+
524
+ ```html
525
+ <div id="todo-app">
526
+ <h2>Todo List</h2>
527
+
528
+ <form @submit.prevent="addTodo">
529
+ <input
530
+ type="text"
531
+ :bind="newTitle"
532
+ :ref="titleInput"
533
+ placeholder="Add task..."
534
+ />
535
+ <button type="submit" :disabled="!newTitle.trim()">Add</button>
536
+ </form>
537
+
538
+ <ul>
539
+ <li :for="todo of todos" :key="todo.id">
540
+ <input type="checkbox" :bind="todo.completed" />
541
+ <span
542
+ :style="{ textDecoration: todo.completed ? 'line-through' : 'none' }"
543
+ :text="todo.title"
544
+ ></span>
545
+ <button @click="deleteTodo(todo.id)">✕</button>
546
+ </li>
547
+ </ul>
548
+ </div>
549
+
550
+ <script type="module">
551
+ import { createScope, reactive } from '@jsweb/ui'
552
+
553
+ let nextId = 1
554
+ const app = reactive({
555
+ newTitle: '',
556
+ todos: [
557
+ { id: nextId++, title: 'Buy milk', completed: false },
558
+ { id: nextId++, title: 'Write tests', completed: true },
559
+ ],
560
+ addTodo() {
561
+ if (!this.newTitle.trim()) return
562
+ this.todos.push({
563
+ id: nextId++,
564
+ title: this.newTitle.trim(),
565
+ completed: false,
566
+ })
567
+ this.newTitle = ''
568
+ this.$refs.get('titleInput')?.focus()
569
+ },
570
+ deleteTodo(id) {
571
+ const idx = this.todos.findIndex((t) => t.id === id)
572
+ if (idx !== -1) this.todos.splice(idx, 1)
573
+ },
574
+ })
575
+
576
+ createScope('#todo-app', app)
577
+ </script>
578
+ ```
579
+
580
+ ### Pattern 4: Modal / Popover with `@click.outside`
581
+
582
+ ```html
583
+ <div :scope="{ isOpen: false }">
584
+ <button @click="isOpen = true">Open Details</button>
585
+
586
+ <div class="modal-backdrop" :if="isOpen">
587
+ <div class="modal-card" @click.outside="isOpen = false">
588
+ <h3>Modal Title</h3>
589
+ <p>Clicking outside this box automatically closes the modal.</p>
590
+ <button @click="isOpen = false">Close</button>
591
+ </div>
592
+ </div>
593
+ </div>
594
+ ```
595
+
596
+ ---
597
+
598
+ ## 8. Critical Rules for AI Coding Agents
599
+
600
+ When generating, editing, or diagnosing `@jsweb/ui` code, adhere to these rules:
601
+
602
+ 1. **Do NOT inject Virtual DOM or JSX**: `@jsweb/ui` operates directly on standard HTML DOM nodes. Do not import React, JSX runtimes, or attempt to return VNodes.
603
+ 2. **Always supply `:key` in `:for` loops**: When generating lists with `:for`, always include `:key="item.id"` (or another unique identifier) to ensure efficient element recycling and avoid state leakage between list items.
604
+ 3. **Use Native Getters for Computed State**: Do not invent a `computed()` function. `@jsweb/ui` uses standard JavaScript getters on reactive objects (`get myProp() { ... }`).
605
+ 4. **Wrap Root State with `reactive()`**: If mutating properties from JavaScript, ensure the object was wrapped in `reactive()`. Mutating a plain unproxied object will not trigger DOM updates.
606
+ 5. **Remember Directive Attributes Disappear from DOM**: Directives like `:text`, `:bind`, and `:if` are stripped during parsing. Do not write CSS selectors or external DOM queries relying on directive attributes remaining on elements in runtime.
607
+ 6. **Use `$refs.get('key')`**: `$refs` is a JavaScript `Map`. Access references using `$refs.get('name')`, not `$refs.name`. In keyed `:for` loops, access nested elements with `$refs.get('name').get(key)`.
608
+ 7. **Two-Way Binding on Inputs**: Always use `:bind="prop"`. Avoid manually pairing `:value="prop"` and `@input="prop = $event.target.value"` unless custom debouncing or data conversion is needed.
609
+ 8. **Scope Merging**: When calling `createScope(selector, state)`, the properties of `state` become directly accessible in that DOM subtree. If `:scope="{ ... }"` is also defined on the HTML element, the inline scope inherits and shadows properties from the parent state.
package/index.es.js ADDED
@@ -0,0 +1,2 @@
1
+ var e=null,t=/* @__PURE__ */new WeakMap,n=/* @__PURE__ */new WeakMap,o=/* @__PURE__ */new WeakMap,s=class{fn;active=!0;deps=/* @__PURE__ */new Set;constructor(e){this.fn=e}run(){if(!this.active)return this.fn();this.cleanup(),e=Symbol(),o.set(e,this);try{return this.fn()}finally{o.delete(e),e=null}}stop(){this.active&&(this.cleanup(),this.active=!1)}cleanup(){this.deps.forEach(e=>e.delete(this)),this.deps.clear()}effect(){return{run:()=>this.run(),stop:()=>this.stop()}}};function i(e){const t=new s(e);return t.run(),t.effect()}function c(e,n){const o=t.get(e);if(!o)return;const s=o.get(n);s&&new Set(s).forEach(e=>e.run())}var r=class{$refs=/* @__PURE__ */new Map;$emit(e,t){(this.$el||("undefined"!=typeof window?window:null))?.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}constructor(e){e&&"object"==typeof e&&Object.assign(this,e)}};function f(s){if("object"!=typeof s||null===s)return s;if(s instanceof Map||s instanceof Set||s instanceof WeakMap||s instanceof WeakSet||s instanceof Date||s instanceof RegExp||"function"==typeof Node&&s instanceof Node)return s;if(Object.hasOwn(s,"_isReactive"))return s;const i=n.get(s);if(i)return i;const r=new Proxy(s,{get(n,s,i){if("_isReactive"===s)return!0;!function(n,s){if(e){let i=t.get(n);i||(i=/* @__PURE__ */new Map,t.set(n,i));let c=i.get(s);c||(c=/* @__PURE__ */new Set,i.set(s,c));const r=o.get(e);r&&(c.add(r),r.deps.add(c))}}(n,s);const c=Reflect.get(n,s,i);return"object"==typeof c&&null!==c?f(c):c},set(e,t,n,o){const s=Array.isArray(e),i=Reflect.get(e,t,o),r=s&&String(Number(t))===t?Number(t)<e.length:Object.hasOwn(e,t),f=Reflect.set(e,t,n,o);return r?i!==n&&c(e,t):(c(e,t),s&&"length"!==t&&c(e,"length")),f}});return n.set(s,r),r}function u(e,t=/* @__PURE__ */new Set){if("object"!=typeof e||null===e||t.has(e))return e;t.add(e);for(const n in e)u(e[n],t);return e}function a(e,t,n){let o,s=!0;const c=e instanceof Function?e:()=>u(e);return i(()=>{const e=c();s?(s=!1,o=e,n?.immediate&&t(e,void 0)):(t(e,o),o=e)}).stop}function l(e,t={}){try{return new Function(`with(this) { return ${e} }`).call(t)}catch{return}}function d(e,t,n={}){try{const o=t.trim(),s=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(o);new Function("$event",`with(this) { ${s?`${o} instanceof Function ? ${o}.call(this, $event) : ${o}`:o} }`).call(n,e)}catch{console.warn(`[jsweb/ui] Error evaluating event: ${t}`)}}function p(e){const t=e;t._effects&&(t._effects.forEach(e=>e()),t._effects=[]);const n=Array.from(e.childNodes);for(const o of n)p(o)}function h(e,t={}){const n=e._isReactive?e:f(e);return new Proxy(n,{get:(e,n)=>"_isContext"===n||(n in e?Reflect.get(e,n,e):n in t?Reflect.get(t,n,t):Reflect.get(e,n,e)),set:(e,n,o)=>n in e?Reflect.set(e,n,o,e):n in t?Reflect.set(t,n,o,t):Reflect.set(e,n,o,e),has:(e,n)=>n in e||n in t})}function m(e,t){if(e.nodeType!==Node.ELEMENT_NODE)return;const n=e,o=function(e,t){const n=["ui:scope",":scope"],o=w(e,n);if(!o)return t;const s=l(o,t);if(!s)return;y(e,n),s.$el=e,s.$emit||(s.$emit=(t,n)=>{e.dispatchEvent(new CustomEvent(t,{detail:n,bubbles:!0,composed:!0}))});s.$refs||(s.$refs=t.$refs??/* @__PURE__ */new Map);return h(s,t)}(n,t);if(!o)return;const s=["ui:for",":for"],i=w(n,s);if(i)return y(n,s),void function(e,t,n){if(!e.parentNode)return;const o=/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(t);if(!o)return console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,s,i]=o,c=["ui:key",":key"],r=w(e,c);y(e,c);const u=crypto.randomUUID(),a=document.createComment(` ui:for ${u} `);e.replaceWith(a);let d=[];b(a,()=>{const t=l(i,n);if(!Array.isArray(t))return d.forEach(e=>{e.el.remove(),p(e.el)}),void(d=[]);const o=[],c=/* @__PURE__ */new Map;d.forEach(e=>c.set(e.key,e)),t.forEach((t,i)=>{let u=i;if(r){const e=h({[s]:t,$index:i},n);u=l(r,e)}let a=c.get(u);if(a)a.scope[s]=t,a.scope.$index=i,a.scope.$key=u,a.scope.$el=a.el,c.delete(u);else{const o=e.cloneNode(!0),c=f({[s]:t,$index:i,$key:u,$el:o});m(o,h(c,n)),a={key:u,el:o,scope:c}}o.push(a)}),c.forEach(e=>{e.el.remove(),p(e.el)});let u=a.nextSibling;o.forEach(e=>{u===e.el?u=u.nextSibling:a.parentNode?.insertBefore(e.el,u)}),d=o})}(n,i,o);const c=["ui:if",":if"],r=w(n,c);r&&(y(n,c),function(e,t,n){if(!e.parentNode)return;const o=crypto.randomUUID(),s=document.createComment(` ui:if ${o} `);e.before(s),b(s,()=>{l(t,n)?e.parentNode||s.parentNode?.insertBefore(e,s.nextSibling):e.parentNode&&e.remove()})}(n,r,o)),function(e,t){const n=Array.from(e.attributes);for(const o of n){const{name:n,value:s}=o,i=["ui:text",":text"].includes(n),c=["ui:bind",":bind"].includes(n),r=["ui:class",":class"].includes(n),f=["ui:style",":style"].includes(n),u=["ui:ref",":ref"].includes(n),a=n.startsWith("ui:")||n.startsWith(":"),l=n.startsWith("ui@")||n.startsWith("@");i?($(e,s,t),e.removeAttribute(n)):c?(E(e,s,t),e.removeAttribute(n)):r?(S(e,s,t),e.removeAttribute(n)):f?(x(e,s,t),e.removeAttribute(n)):u?(g(e,s,t),e.removeAttribute(n)):a?(A(e,n.split(":").pop(),s,t),e.removeAttribute(n)):l&&(M(e,n,s,t),e.removeAttribute(n))}}(n,o);const u=Array.from(n.childNodes);for(const f of u)m(f,o)}function v(e,t){const n="string"==typeof e?document.querySelector(e):e;if(n){const e=t??{};e.$el=n,e.$emit||(e.$emit=(e,t)=>{n.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}),e.$refs||(e.$refs=/* @__PURE__ */new Map),m(n,e)}else console.warn("[jsweb/ui] Element not found:",e)}function b(e,t){const n=i(t),o=e;o._effects??=[],o._effects.push(n.stop)}function w(e,t){for(const n of t){const t=e.getAttribute(n);if(null!==t)return t}return null}function y(e,t){for(const n of t)e.removeAttribute(n)}function g(e,t,n){const o=t.trim().replace(/^['"]|['"]$/g,"");if(!o)return;const s=n.$refs;if(!s)return;const i=n.$key;if(void 0!==i){let t=s.get(o);t instanceof Map||(t=/* @__PURE__ */new Map,s.set(o,t)),t.set(i,e)}else s.set(o,e);const c=e;c._effects??=[],c._effects.push(()=>{if(void 0!==i){const e=s.get(o);e instanceof Map&&(e.delete(i),0===e.size&&s.delete(o))}else s.get(o)===e&&s.delete(o)})}function $(e,t,n){b(e,()=>{const o=l(t,n);e.textContent=null!=o?String(o):""})}function E(e,t,n){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,s=e instanceof HTMLInputElement&&"radio"===e.type;b(e,()=>{const i=l(t,n);if(o)e.checked=!!i;else if(s)e.checked=e.value===String(i);else{e.value=null==i?"":String(i)}});const i=o||s||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(i,e=>{d(e,`${t} = ${"$event.target."+(o?"checked":"value")}`,n)})}function A(e,t,n,o){b(e,()=>{const s=l(n,o);null==s||!1===s?e.removeAttribute(t):!0===s?e.setAttribute(t,""):e.setAttribute(t,String(s))})}function S(e,t,n){let o=/* @__PURE__ */new Set;b(e,()=>{const s=l(t,n),i=/* @__PURE__ */new Set,c=e=>e&&i.add(e),r=e=>e.split(/\s+/).forEach(c);"string"==typeof s?r(s):Array.isArray(s)?s.flat().forEach(e=>{"string"==typeof e&&r(e)}):"object"==typeof s&&null!==s&&Object.entries(s).forEach(([e,t])=>{t&&r(e)}),o.forEach(t=>{i.has(t)||e.classList.remove(t)}),i.forEach(t=>{o.has(t)||e.classList.add(t)}),o=i})}function x(e,t,n){let o={};b(e,()=>{const s=l(t,n),i="object"==typeof s&&null!==s?s:{};for(const t in o)t in i||(e.style[t]="");for(const t in i)o[t]!==i[t]&&(e.style[t]=i[t]);o={...i}})}function M(e,t,n,o){const[s,...i]=t.split("@").pop().split("."),c=i.includes("outside"),r=c?document:e,f=t=>{if(!e.isConnected)return;const s=t.target instanceof Node;c&&s&&e.contains(t.target)||i.includes("self")&&t.target!==e||(i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),d(t,n,o))};if(r.addEventListener(s,f),c){const t=e;t._effects??=[],t._effects.push(()=>r.removeEventListener(s,f))}}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createScope:v,reactive:f,watch:a,Scope:r}}export{r as Scope,v as createScope,f as reactive,a as watch};
2
+ //# sourceMappingURL=index.es.js.map