@ruledwdl/dom 0.1.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.
@@ -0,0 +1,936 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RuledWDL DOM Refactor — CDN & Live State Mutations</title>
7
+ <style>
8
+ :root {
9
+ --bg: #090d16;
10
+ --card-bg: #131b2e;
11
+ --border: #22304d;
12
+ --text: #f1f5f9;
13
+ --muted: #94a3b8;
14
+ --accent: #38bdf8;
15
+ --accent-hover: #0ea5e9;
16
+ --accent-dim: rgba(56, 189, 248, 0.12);
17
+ --success: #4ade80;
18
+ --warning: #fbbf24;
19
+ --danger: #f87171;
20
+ }
21
+
22
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
+
24
+ body {
25
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ min-height: 100vh;
29
+ padding: 2rem;
30
+ line-height: 1.5;
31
+ }
32
+
33
+ .container {
34
+ max-width: 1200px;
35
+ margin: 0 auto;
36
+ }
37
+
38
+ header {
39
+ margin-bottom: 2rem;
40
+ }
41
+
42
+ .header-tag {
43
+ display: inline-block;
44
+ font-size: 0.75rem;
45
+ font-weight: 700;
46
+ letter-spacing: 0.08em;
47
+ text-transform: uppercase;
48
+ background: var(--accent-dim);
49
+ color: var(--accent);
50
+ padding: 0.25rem 0.65rem;
51
+ border-radius: 9999px;
52
+ margin-bottom: 0.5rem;
53
+ border: 1px solid rgba(56, 189, 248, 0.3);
54
+ }
55
+
56
+ .runtime-badge {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 0.4rem;
60
+ font-size: 0.75rem;
61
+ font-weight: 600;
62
+ background: #1e293b;
63
+ color: #94a3b8;
64
+ padding: 0.2rem 0.6rem;
65
+ border-radius: 6px;
66
+ border: 1px solid var(--border);
67
+ margin-left: 0.75rem;
68
+ vertical-align: middle;
69
+ }
70
+
71
+ .runtime-badge .dot {
72
+ width: 6px;
73
+ height: 6px;
74
+ border-radius: 50%;
75
+ background: var(--success);
76
+ }
77
+
78
+ h1 {
79
+ font-size: 1.85rem;
80
+ font-weight: 800;
81
+ letter-spacing: -0.02em;
82
+ color: #ffffff;
83
+ margin-bottom: 0.35rem;
84
+ }
85
+
86
+ .subtitle {
87
+ color: var(--muted);
88
+ font-size: 0.95rem;
89
+ }
90
+
91
+ .grid-layout {
92
+ display: grid;
93
+ grid-template-columns: 1fr 380px;
94
+ gap: 1.5rem;
95
+ }
96
+
97
+ @media (max-width: 900px) {
98
+ .grid-layout {
99
+ grid-template-columns: 1fr;
100
+ }
101
+ }
102
+
103
+ .card {
104
+ background: var(--card-bg);
105
+ border: 1px solid var(--border);
106
+ border-radius: 12px;
107
+ padding: 1.5rem;
108
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
109
+ }
110
+
111
+ .card-title {
112
+ font-size: 0.85rem;
113
+ font-weight: 700;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.05em;
116
+ color: var(--muted);
117
+ margin-bottom: 1.25rem;
118
+ display: flex;
119
+ justify-content: space-between;
120
+ align-items: center;
121
+ }
122
+
123
+ /* Live Preview Box */
124
+ #mount-area {
125
+ min-height: 220px;
126
+ background: #080c14;
127
+ border: 1px dashed var(--border);
128
+ border-radius: 8px;
129
+ padding: 1.5rem;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
134
+
135
+ /* Component base styles */
136
+ .hero {
137
+ background: linear-gradient(135deg, #1e293b, #0f172a);
138
+ border: 1px solid #334155;
139
+ border-radius: 12px;
140
+ padding: 1.75rem;
141
+ width: 100%;
142
+ max-width: 500px;
143
+ transition: all 0.25s ease;
144
+ }
145
+
146
+ .hero[data-variant="elevated"] {
147
+ border-color: var(--accent);
148
+ box-shadow: 0 12px 30px -4px rgba(56, 189, 248, 0.25);
149
+ transform: translateY(-2px);
150
+ }
151
+
152
+ .hero[data-variant="outlined"] {
153
+ background: transparent;
154
+ border: 2px dashed var(--accent);
155
+ }
156
+
157
+ .hero .title {
158
+ font-size: 1.5rem;
159
+ font-weight: 700;
160
+ color: #ffffff;
161
+ margin-bottom: 0.4rem;
162
+ }
163
+
164
+ .hero .subtitle {
165
+ color: var(--muted);
166
+ font-size: 0.95rem;
167
+ margin-bottom: 1rem;
168
+ }
169
+
170
+ .hero .lead {
171
+ color: #cbd5e1;
172
+ font-size: 0.9rem;
173
+ margin-bottom: 0.75rem;
174
+ font-style: italic;
175
+ }
176
+
177
+ .hero .badge {
178
+ display: inline-block;
179
+ background: #334155;
180
+ color: var(--accent);
181
+ font-size: 0.75rem;
182
+ font-weight: 600;
183
+ padding: 0.2rem 0.55rem;
184
+ border-radius: 9999px;
185
+ margin-bottom: 0.5rem;
186
+ }
187
+
188
+ .hero .title-banner {
189
+ background: rgba(56, 189, 248, 0.1);
190
+ border-left: 3px solid var(--accent);
191
+ padding: 0.5rem 0.75rem;
192
+ border-radius: 4px;
193
+ margin-bottom: 0.75rem;
194
+ }
195
+
196
+ .hero .cta {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ gap: 0.5rem;
200
+ background: var(--accent);
201
+ color: #090d16;
202
+ border: none;
203
+ padding: 0.55rem 1.15rem;
204
+ border-radius: 6px;
205
+ font-weight: 700;
206
+ font-size: 0.9rem;
207
+ cursor: pointer;
208
+ margin-top: 0.5rem;
209
+ margin-right: 0.5rem;
210
+ transition: background 0.15s ease;
211
+ }
212
+
213
+ .hero .cta:hover {
214
+ background: var(--accent-hover);
215
+ }
216
+
217
+ /* Actions Panel */
218
+ .btn-group {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 0.6rem;
222
+ margin-bottom: 1.5rem;
223
+ }
224
+
225
+ .op-btn {
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: space-between;
229
+ width: 100%;
230
+ background: #1a233a;
231
+ border: 1px solid var(--border);
232
+ color: var(--text);
233
+ padding: 0.65rem 0.9rem;
234
+ border-radius: 8px;
235
+ font-size: 0.85rem;
236
+ font-weight: 600;
237
+ cursor: pointer;
238
+ transition: all 0.15s ease;
239
+ text-align: left;
240
+ }
241
+
242
+ .op-btn:hover {
243
+ background: #233050;
244
+ border-color: var(--accent);
245
+ color: #ffffff;
246
+ }
247
+
248
+ .op-btn .op-code {
249
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
250
+ font-size: 0.75rem;
251
+ color: var(--accent);
252
+ background: rgba(56, 189, 248, 0.08);
253
+ padding: 0.15rem 0.4rem;
254
+ border-radius: 4px;
255
+ }
256
+
257
+ .btn-reset {
258
+ background: rgba(248, 113, 113, 0.1);
259
+ border-color: rgba(248, 113, 113, 0.3);
260
+ color: var(--danger);
261
+ }
262
+
263
+ .btn-reset:hover {
264
+ background: rgba(248, 113, 113, 0.2);
265
+ border-color: var(--danger);
266
+ color: #ffffff;
267
+ }
268
+
269
+ /* AST & Log Panel */
270
+ .code-view {
271
+ background: #080c14;
272
+ border: 1px solid var(--border);
273
+ border-radius: 6px;
274
+ padding: 0.75rem;
275
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
276
+ font-size: 0.8rem;
277
+ color: #93c5fd;
278
+ overflow-x: auto;
279
+ white-space: pre-wrap;
280
+ word-break: break-all;
281
+ margin-bottom: 1.25rem;
282
+ }
283
+
284
+ #event-log {
285
+ max-height: 180px;
286
+ overflow-y: auto;
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 0.4rem;
290
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
291
+ font-size: 0.75rem;
292
+ }
293
+
294
+ .log-entry {
295
+ padding: 0.35rem 0.6rem;
296
+ border-radius: 4px;
297
+ background: #0d1527;
298
+ border-left: 3px solid var(--accent);
299
+ color: #cbd5e1;
300
+ }
301
+
302
+ .log-entry.op { border-left-color: var(--success); }
303
+ .log-entry.warn { border-left-color: var(--warning); color: #fde68a; }
304
+ </style>
305
+ </head>
306
+ <body>
307
+ <div class="container">
308
+ <header>
309
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
310
+ <span class="header-tag">@ruledwdl/dom &bull; CDN Runtime</span>
311
+ <span id="runtime-badge" class="runtime-badge"><span class="dot"></span><span id="runtime-label">Initializing...</span></span>
312
+ </div>
313
+ <h1>Surgical DOM Refactor Engine</h1>
314
+ <p class="subtitle">Live `@ruledwdl/state` event bus synchronized with native browser DOM updates — 0% innerHTML redraws.</p>
315
+ </header>
316
+
317
+ <div class="grid-layout">
318
+ <!-- Live Preview Column -->
319
+ <div style="display: flex; flex-direction: column; gap: 1.5rem;">
320
+ <div class="card">
321
+ <div class="card-title">
322
+ <span>Live DOM Element Tree</span>
323
+ <span style="color: var(--success); font-size: 0.75rem;">● Active Mounted</span>
324
+ </div>
325
+ <div id="mount-area"></div>
326
+ </div>
327
+
328
+ <div class="card">
329
+ <div class="card-title">
330
+ <span>Current WDL Layers & State Snapshot</span>
331
+ </div>
332
+ <div id="layers-display" class="code-view">Loading...</div>
333
+ </div>
334
+ </div>
335
+
336
+ <!-- Controls & Log Column -->
337
+ <div style="display: flex; flex-direction: column; gap: 1.5rem;">
338
+ <div class="card">
339
+ <div class="card-title">
340
+ <span>Surgical DOM Mutations</span>
341
+ </div>
342
+ <div class="btn-group">
343
+ <button id="btn-text" class="op-btn">
344
+ <span>1. Update Title Text</span>
345
+ <span class="op-code">attr.set</span>
346
+ </button>
347
+ <button id="btn-append-cta" class="op-btn">
348
+ <span>2. Append Button CTA</span>
349
+ <span class="op-code">layers.append</span>
350
+ </button>
351
+ <button id="btn-before-badge" class="op-btn">
352
+ <span>3. Insert Badge Before</span>
353
+ <span class="op-code">layers.before</span>
354
+ </button>
355
+ <button id="btn-after-lead" class="op-btn">
356
+ <span>4. Insert Lead After</span>
357
+ <span class="op-code">layers.after</span>
358
+ </button>
359
+ <button id="btn-wrap-title" class="op-btn">
360
+ <span>5. Wrap Title Banner</span>
361
+ <span class="op-code">layers.wrap</span>
362
+ </button>
363
+ <button id="btn-toggle-variant" class="op-btn">
364
+ <span>6. Toggle Variant</span>
365
+ <span class="op-code">variant.set</span>
366
+ </button>
367
+ <button id="btn-add-rule" class="op-btn">
368
+ <span>7. Add Scoped CSS Rule</span>
369
+ <span class="op-code">registry.addRule</span>
370
+ </button>
371
+ <button id="btn-remove-badge" class="op-btn">
372
+ <span>8. Remove Badge</span>
373
+ <span class="op-code">layers.remove</span>
374
+ </button>
375
+ <button id="btn-reset" class="op-btn btn-reset">
376
+ <span>↺ Reset Component Tree</span>
377
+ <span class="op-code">layers.set</span>
378
+ </button>
379
+ </div>
380
+ </div>
381
+
382
+ <div class="card">
383
+ <div class="card-title">
384
+ <span>Surgical Operation Log</span>
385
+ </div>
386
+ <div id="event-log"></div>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ <!-- CDN ESM Integration Scripts -->
393
+ <script type="module">
394
+ // ---------------------------------------------------------------------------
395
+ // Standalone Inline Fallback Definitions
396
+ // (Used when running over file:/// or offline before CDN index replication)
397
+ // ---------------------------------------------------------------------------
398
+ class FallbackComponentState {
399
+ constructor(id, initial = {}) {
400
+ this.id = id;
401
+ this.listeners = new Map();
402
+ this._layers = initial.layers || '';
403
+ this._attr = structuredClone(initial.attr || {});
404
+ this._data = structuredClone(initial.data || {});
405
+ this._registry = structuredClone(initial.registry || null);
406
+ this._variant = initial.variant || null;
407
+
408
+ this.layers = {
409
+ list: () => this._layers,
410
+ tree: () => parseLayersSimple(this._layers),
411
+ set: (expr) => {
412
+ this._layers = expr;
413
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'set', payload: expr });
414
+ },
415
+ append: (targetId, expr) => {
416
+ this._layers = `${this._layers} + ${expr}`;
417
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'append', targetId, payload: expr });
418
+ },
419
+ before: (targetId, expr) => {
420
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'before', targetId, payload: expr });
421
+ },
422
+ after: (targetId, expr) => {
423
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'after', targetId, payload: expr });
424
+ },
425
+ wrap: (targetId, expr) => {
426
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'wrap', targetId, payload: expr });
427
+ },
428
+ remove: (targetId) => {
429
+ this.emit('layers:change', { type: 'layers:change', componentId: this.id, action: 'remove', targetId });
430
+ }
431
+ };
432
+
433
+ this.attr = {
434
+ list: () => this._attr,
435
+ set: (targetId, props) => {
436
+ const key = targetId.startsWith('.') ? targetId : `.${targetId}`;
437
+ this._attr[key] = { ...(this._attr[key] || {}), ...props };
438
+ this.emit('attr:change', { type: 'attr:change', componentId: this.id, action: 'set', targetId, payload: props });
439
+ },
440
+ remove: (targetId, propKey) => {
441
+ const key = targetId.startsWith('.') ? targetId : `.${targetId}`;
442
+ if (propKey && this._attr[key]) delete this._attr[key][propKey];
443
+ else delete this._attr[key];
444
+ this.emit('attr:change', { type: 'attr:change', componentId: this.id, action: 'remove', targetId, payload: propKey });
445
+ }
446
+ };
447
+
448
+ this.variant = {
449
+ get: () => this._variant,
450
+ set: (name, targetId) => {
451
+ this._variant = name || null;
452
+ this.emit('variant:change', { type: 'variant:change', componentId: this.id, action: 'set', targetId, payload: name });
453
+ }
454
+ };
455
+
456
+ this.registry = {
457
+ get: () => this._registry,
458
+ addRule: (rule) => {
459
+ if (!this._registry) this._registry = { $version: '2.1', rules: [] };
460
+ if (!Array.isArray(this._registry.rules)) this._registry.rules = [];
461
+ this._registry.rules.push(rule);
462
+ this.emit('registry:change', { type: 'registry:change', componentId: this.id, action: 'addRule', payload: rule });
463
+ }
464
+ };
465
+ }
466
+
467
+ on(type, handler) {
468
+ if (!this.listeners.has(type)) this.listeners.set(type, []);
469
+ this.listeners.get(type).push(handler);
470
+ return () => this.off(type, handler);
471
+ }
472
+
473
+ off(type, handler) {
474
+ const list = this.listeners.get(type) || [];
475
+ const idx = list.indexOf(handler);
476
+ if (idx !== -1) list.splice(idx, 1);
477
+ }
478
+
479
+ emit(type, event) {
480
+ event.timestamp = Date.now();
481
+ const list = this.listeners.get(type) || [];
482
+ list.forEach((fn) => {
483
+ try { fn(event); } catch (err) { console.error(err); }
484
+ });
485
+ }
486
+
487
+ getSnapshot() {
488
+ return {
489
+ id: this.id,
490
+ layers: this._layers,
491
+ attr: this._attr,
492
+ data: this._data,
493
+ registry: this._registry,
494
+ variant: this._variant
495
+ };
496
+ }
497
+ }
498
+
499
+ class FallbackComponentManager {
500
+ constructor() {
501
+ this.components = new Map();
502
+ }
503
+ create(id, initial) {
504
+ const state = new FallbackComponentState(id, initial);
505
+ this.components.set(id, state);
506
+ return state;
507
+ }
508
+ get(id) { return this.components.get(id); }
509
+ has(id) { return this.components.has(id); }
510
+ }
511
+
512
+ // Helpers
513
+ const ATTR_SKIP = new Set(['text', 'class', 'html']);
514
+ function normalizeId(id) { return id == null ? '' : String(id).replace(/^\./, ''); }
515
+ function parseLayerToken(expr) {
516
+ if (typeof expr !== 'string') {
517
+ if (expr && typeof expr === 'object') {
518
+ return { tag: String(expr.tag || 'div').toLowerCase(), semanticId: normalizeId(expr.semanticId || expr.id || '') };
519
+ }
520
+ throw new Error('[wdl-dom] invalid layer expression');
521
+ }
522
+ const trimmed = expr.trim();
523
+ const m = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)(?:\.([a-zA-Z0-9_-]+))?$/);
524
+ if (!m) return { tag: trimmed.toLowerCase() || 'div', semanticId: '' };
525
+ return { tag: m[1].toLowerCase(), semanticId: normalizeId(m[2] || '') };
526
+ }
527
+
528
+ function parseLayersSimple(str) {
529
+ if (Array.isArray(str)) return structuredClone(str);
530
+ if (typeof str !== 'string' || !str.trim()) return [];
531
+ const root = { tag: '__root__', semanticId: '', children: [] };
532
+ const stack = [root];
533
+ let i = 0;
534
+ const top = () => stack[stack.length - 1];
535
+ while (i < str.length) {
536
+ const ch = str[i];
537
+ if (/\s/.test(ch)) { i++; continue; }
538
+ if (ch === '>') {
539
+ const last = top().children[top().children.length - 1];
540
+ if (last) stack.push(last);
541
+ i++;
542
+ continue;
543
+ }
544
+ if (ch === '+') { i++; continue; }
545
+ if (ch === '<') {
546
+ i++;
547
+ if (stack.length > 1) stack.pop();
548
+ continue;
549
+ }
550
+ let tag = '';
551
+ while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) tag += str[i++];
552
+ let semanticId = '';
553
+ if (str[i] === '.') {
554
+ i++;
555
+ while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) semanticId += str[i++];
556
+ }
557
+ top().children.push({ tag: tag.toLowerCase() || 'div', semanticId, children: [] });
558
+ }
559
+ return root.children;
560
+ }
561
+
562
+ class FallbackWdlDom {
563
+ constructor(options = {}) {
564
+ const { container, component, styleTarget = document.head, debug = false } = options;
565
+ this.container = typeof container === 'string' ? document.querySelector(container) : container;
566
+ this.component = component;
567
+ this.styleTarget = styleTarget;
568
+ this.debug = debug;
569
+ this.liveMap = new Map();
570
+ this._unsubs = [];
571
+ this._styleEl = null;
572
+ this._mount();
573
+ this._bindStateEvents();
574
+ }
575
+
576
+ remount() { this._mount(); }
577
+ getLiveMap() { return new Map(this.liveMap); }
578
+ destroy() {
579
+ this._unsubs.forEach((u) => { try { u(); } catch (_) {} });
580
+ this._unsubs = [];
581
+ this.container.replaceChildren();
582
+ this.liveMap.clear();
583
+ if (this._styleEl?.parentNode) this._styleEl.parentNode.removeChild(this._styleEl);
584
+ this._styleEl = null;
585
+ }
586
+
587
+ _mount() {
588
+ this.container.replaceChildren();
589
+ this.liveMap.clear();
590
+ const tree = typeof this.component.layers?.tree === 'function'
591
+ ? this.component.layers.tree()
592
+ : parseLayersSimple(this.component.layers?.list?.() ?? this.component.getSnapshot?.()?.layers);
593
+ for (const node of tree) {
594
+ this.container.appendChild(this._createElementFromNode(node));
595
+ }
596
+ const attrMap = this.component.attr?.list?.() ?? this.component.getSnapshot?.()?.attr ?? {};
597
+ for (const [sel, props] of Object.entries(attrMap)) {
598
+ this._applyAttrs(normalizeId(sel), props);
599
+ }
600
+ this._applyRootVariant();
601
+ this._syncRegistryStyles();
602
+ }
603
+
604
+ _createElementFromNode(node) {
605
+ const tag = (node.tag || 'div').toLowerCase();
606
+ const id = normalizeId(node.semanticId || node.id || '');
607
+ const el = document.createElement(tag);
608
+ if (id) {
609
+ el.classList.add(id);
610
+ el.setAttribute('wdl-comp', id);
611
+ this.liveMap.set(id, el);
612
+ }
613
+ if (Array.isArray(node.children)) {
614
+ for (const child of node.children) el.appendChild(this._createElementFromNode(child));
615
+ }
616
+ return el;
617
+ }
618
+
619
+ _bindStateEvents() {
620
+ const c = this.component;
621
+ if (typeof c.on !== 'function') return;
622
+ const handler = (e) => this._onStateEvent(e);
623
+ ['layers:change', 'attr:change', 'data:change', 'variant:change', 'registry:change'].forEach(t => {
624
+ const off = c.on(t, handler);
625
+ if (typeof off === 'function') this._unsubs.push(off);
626
+ else this._unsubs.push(() => c.off?.(t, handler));
627
+ });
628
+ }
629
+
630
+ _onStateEvent(event) {
631
+ if (!event?.type) return;
632
+ switch (event.type) {
633
+ case 'layers:change': this._handleLayers(event); break;
634
+ case 'attr:change': this._handleAttr(event); break;
635
+ case 'variant:change': this._handleVariant(event); break;
636
+ case 'registry:change': this._handleRegistry(event); break;
637
+ }
638
+ }
639
+
640
+ _handleLayers(event) {
641
+ const { action, targetId, payload } = event;
642
+ const id = normalizeId(targetId);
643
+ switch (action) {
644
+ case 'set': this._mount(); break;
645
+ case 'append': {
646
+ const parent = this.liveMap.get(id);
647
+ if (!parent) return this._mount();
648
+ const token = parseLayerToken(payload);
649
+ const el = this._createElementFromNode({ tag: token.tag, semanticId: token.semanticId, children: [] });
650
+ parent.appendChild(el);
651
+ break;
652
+ }
653
+ case 'before':
654
+ case 'after': {
655
+ const target = this.liveMap.get(id);
656
+ if (!target?.parentNode) return this._mount();
657
+ const token = parseLayerToken(payload);
658
+ const el = this._createElementFromNode({ tag: token.tag, semanticId: token.semanticId, children: [] });
659
+ if (action === 'before') target.parentNode.insertBefore(el, target);
660
+ else target.parentNode.insertBefore(el, target.nextSibling);
661
+ break;
662
+ }
663
+ case 'wrap': {
664
+ const target = this.liveMap.get(id);
665
+ if (!target?.parentNode) return this._mount();
666
+ const token = parseLayerToken(payload);
667
+ const wrapper = this._createElementFromNode({ tag: token.tag, semanticId: token.semanticId, children: [] });
668
+ target.parentNode.insertBefore(wrapper, target);
669
+ wrapper.appendChild(target);
670
+ break;
671
+ }
672
+ case 'remove': {
673
+ const el = this.liveMap.get(id);
674
+ if (!el) return;
675
+ for (const [sid, node] of [...this.liveMap]) {
676
+ if (sid !== id && el.contains(node)) this.liveMap.delete(sid);
677
+ }
678
+ el.remove();
679
+ this.liveMap.delete(id);
680
+ break;
681
+ }
682
+ default: this._mount();
683
+ }
684
+ }
685
+
686
+ _handleAttr(event) {
687
+ const { action, targetId, payload } = event;
688
+ const id = normalizeId(targetId);
689
+ if (action === 'set' || action === 'update') {
690
+ this._applyAttrs(id, payload || {});
691
+ } else if (action === 'remove') {
692
+ const el = this.liveMap.get(id);
693
+ if (!el) return;
694
+ if (!payload) { el.textContent = ''; el.className = id; }
695
+ else if (payload === 'text') el.textContent = '';
696
+ else if (payload === 'class') el.className = id;
697
+ else el.removeAttribute(payload);
698
+ }
699
+ }
700
+
701
+ _applyAttrs(id, props) {
702
+ if (!props || typeof props !== 'object') return;
703
+ const el = this.liveMap.get(id);
704
+ if (!el) return;
705
+ if (props.text !== undefined) el.textContent = String(props.text);
706
+ if (props.class !== undefined) {
707
+ const extra = String(props.class).trim();
708
+ el.className = extra ? `${id} ${extra}` : id;
709
+ }
710
+ for (const [k, v] of Object.entries(props)) {
711
+ if (ATTR_SKIP.has(k)) continue;
712
+ if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
713
+ else if (v == null) el.removeAttribute(k);
714
+ else el.setAttribute(k, String(v));
715
+ }
716
+ }
717
+
718
+ _handleVariant(event) {
719
+ const id = normalizeId(event.targetId) || this._rootSemanticId();
720
+ const el = this.liveMap.get(id);
721
+ if (el) {
722
+ if (event.payload) el.dataset.variant = String(event.payload);
723
+ else delete el.dataset.variant;
724
+ }
725
+ }
726
+
727
+ _applyRootVariant() {
728
+ const rootId = this._rootSemanticId();
729
+ if (!rootId) return;
730
+ const variant = this.component.variant?.get?.() || this.component.getSnapshot?.()?.variant;
731
+ if (variant && this.liveMap.has(rootId)) {
732
+ this.liveMap.get(rootId).dataset.variant = String(variant);
733
+ }
734
+ }
735
+
736
+ _rootSemanticId() {
737
+ for (const [id, el] of this.liveMap) {
738
+ if (el.parentNode === this.container) return id;
739
+ }
740
+ const first = this.liveMap.keys().next();
741
+ return first.done ? '' : first.value;
742
+ }
743
+
744
+ _handleRegistry() { this._syncRegistryStyles(); }
745
+
746
+ _syncRegistryStyles() {
747
+ if (!this.styleTarget) return;
748
+ const registry = this.component.registry?.get?.() || this.component.getSnapshot?.()?.registry;
749
+ const componentId = this.component.id || 'comp';
750
+ if (!this._styleEl) {
751
+ this._styleEl = document.createElement('style');
752
+ this._styleEl.setAttribute('data-wdl-dom', componentId);
753
+ this.styleTarget.appendChild(this._styleEl);
754
+ }
755
+ if (!registry?.rules) { this._styleEl.textContent = ''; return; }
756
+ const parts = registry.rules.map(rule => {
757
+ const css = typeof rule.css === 'string' ? rule.css : Object.entries(rule.css || {}).map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v};`).join(' ');
758
+ const sel = rule.selector.startsWith('&') ? rule.selector.replace('&', `[wdl-comp="${componentId}"]`) : rule.selector;
759
+ return `${sel} { ${css} }`;
760
+ });
761
+ this._styleEl.textContent = parts.join('\n');
762
+ }
763
+ }
764
+
765
+ // ---------------------------------------------------------------------------
766
+ // Runtime Loader: CDN Primary with Graceful Standalone Fallback
767
+ // ---------------------------------------------------------------------------
768
+ let ComponentManager = FallbackComponentManager;
769
+ let createWdlDom = (opts) => new FallbackWdlDom(opts);
770
+ let runtimeSource = 'Standalone Engine (Local / file:// mode)';
771
+
772
+ // Try CDN dynamically if online and supported
773
+ try {
774
+ if (window.location.protocol.startsWith('http') || navigator.onLine) {
775
+ const statePromise = import('https://cdn.jsdelivr.net/npm/@ruledwdl/state@0.1.3/dist/index.js');
776
+ const domPromise = import('https://cdn.jsdelivr.net/npm/@ruledwdl/dom/dist/wdl-dom.min.js').catch(() => null);
777
+
778
+ const [stateMod, domMod] = await Promise.allSettled([statePromise, domPromise]);
779
+ if (stateMod.status === 'fulfilled' && stateMod.value?.ComponentManager) {
780
+ ComponentManager = stateMod.value.ComponentManager;
781
+ runtimeSource = 'CDN (jsDelivr: @ruledwdl/state)';
782
+ }
783
+ if (domMod.status === 'fulfilled' && domMod.value?.createWdlDom) {
784
+ createWdlDom = domMod.value.createWdlDom;
785
+ runtimeSource = 'CDN (jsDelivr: @ruledwdl/state + @ruledwdl/dom)';
786
+ }
787
+ }
788
+ } catch (err) {
789
+ console.log('Using standalone inline runtime fallback:', err);
790
+ }
791
+
792
+ const badgeEl = document.getElementById('runtime-label');
793
+ if (badgeEl) badgeEl.textContent = runtimeSource;
794
+
795
+ // Logging helper
796
+ const logContainer = document.getElementById('event-log');
797
+ function logEntry(kind, text) {
798
+ const el = document.createElement('div');
799
+ el.className = `log-entry ${kind}`;
800
+ el.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
801
+ logContainer.prepend(el);
802
+ }
803
+
804
+ // 1. Initialize State Manager
805
+ const mgr = new ComponentManager();
806
+ const hero = mgr.create('hero', {
807
+ layers: 'section.hero > h1.title + p.subtitle',
808
+ attr: {
809
+ '.title': { text: 'Reactive Component' },
810
+ '.subtitle': { text: 'Engineered with @ruledwdl/state and @ruledwdl/dom' }
811
+ }
812
+ });
813
+
814
+ // 2. Mount DOM runtime
815
+ const mountTarget = document.getElementById('mount-area');
816
+ const domRuntime = createWdlDom({
817
+ container: mountTarget,
818
+ component: hero,
819
+ debug: true
820
+ });
821
+
822
+ // Display Updater
823
+ function updateLayersDisplay() {
824
+ const display = document.getElementById('layers-display');
825
+ const listExpr = typeof hero.layers?.list === 'function' ? hero.layers.list() : hero.getSnapshot().layers;
826
+ const liveKeys = Array.from(domRuntime.getLiveMap().keys()).map(k => `"${k}"`).join(', ');
827
+ display.textContent = `Layers Expression:\n"${listExpr}"\n\nLive Map Registered Semantic IDs:\n[ ${liveKeys} ]`;
828
+ }
829
+
830
+ updateLayersDisplay();
831
+ logEntry('op', 'Mounted initial component: section.hero > h1.title + p.subtitle');
832
+
833
+ // Subscribe to state changes for UI logs
834
+ hero.on('layers:change', (e) => {
835
+ logEntry('op', `layers:${e.action} (target: #${e.targetId || 'root'})`);
836
+ updateLayersDisplay();
837
+ });
838
+
839
+ hero.on('attr:change', (e) => {
840
+ logEntry('op', `attr:${e.action} (target: #${e.targetId})`);
841
+ updateLayersDisplay();
842
+ });
843
+
844
+ hero.on('variant:change', (e) => {
845
+ logEntry('op', `variant:${e.action} → "${e.payload || 'none'}"`);
846
+ updateLayersDisplay();
847
+ });
848
+
849
+ hero.on('registry:change', (e) => {
850
+ logEntry('op', `registry:${e.action}`);
851
+ updateLayersDisplay();
852
+ });
853
+
854
+ // -------------------------------------------------------------
855
+ // Interactive Mutation Actions
856
+ // -------------------------------------------------------------
857
+ let titleCounter = 1;
858
+ document.getElementById('btn-text').onclick = () => {
859
+ titleCounter++;
860
+ hero.attr.set('title', { text: `Title Updated (${titleCounter})` });
861
+ };
862
+
863
+ document.getElementById('btn-append-cta').onclick = () => {
864
+ if (domRuntime.getLiveMap().has('cta')) {
865
+ logEntry('warn', 'CTA button already appended');
866
+ return;
867
+ }
868
+ hero.layers.append('hero', 'button.cta');
869
+ hero.attr.set('cta', { text: 'Explore RuledWDL →' });
870
+ };
871
+
872
+ document.getElementById('btn-before-badge').onclick = () => {
873
+ if (domRuntime.getLiveMap().has('badge')) {
874
+ logEntry('warn', 'Badge already inserted');
875
+ return;
876
+ }
877
+ hero.layers.before('title', 'span.badge');
878
+ hero.attr.set('badge', { text: '⚡ FAST ESM' });
879
+ };
880
+
881
+ document.getElementById('btn-after-lead').onclick = () => {
882
+ if (domRuntime.getLiveMap().has('lead')) {
883
+ logEntry('warn', 'Lead already inserted');
884
+ return;
885
+ }
886
+ hero.layers.after('title', 'p.lead');
887
+ hero.attr.set('lead', { text: 'Surgical DOM mutations powered by Web Definition Language.' });
888
+ };
889
+
890
+ document.getElementById('btn-wrap-title').onclick = () => {
891
+ if (domRuntime.getLiveMap().has('title-banner')) {
892
+ logEntry('warn', 'Title already wrapped');
893
+ return;
894
+ }
895
+ hero.layers.wrap('title', 'div.title-banner');
896
+ };
897
+
898
+ let variantState = 0;
899
+ const variants = ['elevated', 'outlined', null];
900
+ document.getElementById('btn-toggle-variant').onclick = () => {
901
+ const v = variants[variantState % variants.length];
902
+ variantState++;
903
+ hero.variant.set(v);
904
+ };
905
+
906
+ document.getElementById('btn-add-rule').onclick = () => {
907
+ hero.registry.addRule({
908
+ selector: '& .cta',
909
+ css: {
910
+ background: 'linear-gradient(135deg, #38bdf8, #0ea5e9)',
911
+ boxShadow: '0 4px 15px rgba(56, 189, 248, 0.4)'
912
+ }
913
+ });
914
+ logEntry('op', 'Added scoped CSS rule to component registry');
915
+ };
916
+
917
+ document.getElementById('btn-remove-badge').onclick = () => {
918
+ if (!domRuntime.getLiveMap().has('badge')) {
919
+ logEntry('warn', 'Badge does not exist to remove');
920
+ return;
921
+ }
922
+ hero.layers.remove('badge');
923
+ };
924
+
925
+ document.getElementById('btn-reset').onclick = () => {
926
+ hero.layers.set('section.hero > h1.title + p.subtitle');
927
+ hero.attr.set('title', { text: 'Reactive Component' });
928
+ hero.attr.set('subtitle', { text: 'Engineered with @ruledwdl/state and @ruledwdl/dom' });
929
+ hero.variant.set(null);
930
+ titleCounter = 1;
931
+ variantState = 0;
932
+ logEntry('op', 'Reset complete: layers remounted cleanly');
933
+ };
934
+ </script>
935
+ </body>
936
+ </html>