project-graph-mcp 1.0.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,834 @@
1
+ # Symbiote.js — AI Context Reference (v3.x)
2
+
3
+ > **Purpose**: Authoritative reference for AI code assistants. All information is derived from source code analysis of [symbiote.js](https://github.com/symbiotejs/symbiote.js).
4
+ > Current version: **3.2.0**. Zero dependencies. ~6 KB gzip.
5
+
6
+ ---
7
+
8
+ ## Installation & Import
9
+
10
+ ```js
11
+ // NPM
12
+ import Symbiote, { html, css, PubSub, DICT } from '@symbiotejs/symbiote';
13
+
14
+ // CDN / HTTPS
15
+ import Symbiote, { html, css } from 'https://esm.run/@symbiotejs/symbiote';
16
+
17
+ // Individual module imports (tree-shaking)
18
+ import Symbiote from '@symbiotejs/symbiote/core/Symbiote.js';
19
+ import { PubSub } from '@symbiotejs/symbiote/core/PubSub.js';
20
+ import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
21
+ import { html } from '@symbiotejs/symbiote/core/html.js';
22
+ import { css } from '@symbiotejs/symbiote/core/css.js';
23
+ ```
24
+
25
+ ### Full export list (index.js)
26
+ `Symbiote` (default), `html`, `css`, `PubSub`, `DICT`
27
+
28
+ > **v3.2 change**: `UID`, `setNestedProp`, `applyStyles`, `applyAttributes`, `create`, `kebabToCamel`, `reassignDictionary` moved to `@symbiotejs/symbiote/utils`.
29
+ > `AppRouter` moved to `@symbiotejs/symbiote/core/AppRouter.js` since v3.0.
30
+
31
+ ### Utils entry point (`@symbiotejs/symbiote/utils`)
32
+ ```js
33
+ import { UID, create, applyStyles, applyAttributes, setNestedProp, kebabToCamel, reassignDictionary } from '@symbiotejs/symbiote/utils';
34
+ ```
35
+ Individual deep imports also work: `@symbiotejs/symbiote/utils/UID.js`, etc.
36
+
37
+ ---
38
+
39
+ ## Component Basics
40
+
41
+ Symbiote extends `HTMLElement`. Every component is a Custom Element.
42
+
43
+ ```js
44
+ class MyComponent extends Symbiote {
45
+ // Initial reactive state (key-value pairs)
46
+ init$ = {
47
+ name: 'World',
48
+ count: 0,
49
+ onBtnClick: () => {
50
+ this.$.count++;
51
+ },
52
+ };
53
+
54
+ // Called once after init$ is processed but BEFORE template is rendered
55
+ initCallback() {}
56
+
57
+ // Called once AFTER template is rendered and attached to DOM
58
+ renderCallback() {
59
+ // Safe to access this.ref, this.$, DOM children here
60
+ }
61
+
62
+ // Called when element is disconnected and readyToDestroy is true
63
+ destroyCallback() {}
64
+ }
65
+
66
+ // Template — assigned via static property SETTER, outside the class body
67
+ MyComponent.template = html`
68
+ <h1>Hello {{name}}!</h1>
69
+ <p>Count: {{count}}</p>
70
+ <button ${{onclick: 'onBtnClick'}}>Increment</button>
71
+ `;
72
+
73
+ // Register Custom Element tag
74
+ MyComponent.reg('my-component');
75
+ ```
76
+
77
+ > **CRITICAL**: `template` is a **static property setter** on the `Symbiote` class, not a regular static class field.
78
+ > You **MUST** assign it **outside** the class body: `MyComponent.template = html\`...\``.
79
+ > Using `static template = html\`...\`` inside the class declaration **will NOT work**.
80
+ > Templates are plain HTML strings, NOT JSX. Use the `html` tagged template literal.
81
+
82
+ ### Usage in HTML
83
+ ```html
84
+ <my-component></my-component>
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Template Binding Syntax
90
+
91
+ Use the `html` tagged template literal for ergonomic binding syntax. It supports **two interpolation modes**:
92
+
93
+ - **`Object`** → converted to `bind="prop:key;"` attribute (reactive binding)
94
+ - **`string` / `number`** → concatenated as-is (native interpolation, useful for SSR page shells)
95
+
96
+ This dual-mode design means `html` works for both component templates and full-page SSR output — no separate "server-only template" function is needed.
97
+
98
+ ### Text node binding
99
+ ```html
100
+ <div>{{propName}}</div>
101
+ ```
102
+ Binds `propName` from component state to the text content of a text node. Works inside any element. Multiple bindings in one text node are supported: `{{first}} - {{second}}`.
103
+
104
+ ### Property binding (element's own properties)
105
+ ```html
106
+ <button ${{onclick: 'handlerName'}}>Click</button>
107
+ <div ${{textContent: 'myProp'}}></div>
108
+ ```
109
+ The `${{key: 'value'}}` interpolation creates a `bind="key:value;"` attribute. Keys are DOM element property names. Values are component state property names (strings).
110
+
111
+ **Class property fallback (v3.1+):** When a binding key is not found in `init$`, Symbiote falls back to own class properties (checked via `Object.hasOwn`). This works for ALL properties, not just `on*` handlers. Functions are auto-bound to the component instance. Inherited `HTMLElement` properties are never picked up.
112
+ ```js
113
+ class MyComp extends Symbiote {
114
+ // Approach 1: state property (arrow function)
115
+ init$ = { onClick: () => console.log('clicked') };
116
+
117
+ // Approach 2: class property (fallback for any binding)
118
+ label = 'Click me';
119
+ onSubmit() { console.log('submitted'); }
120
+ }
121
+ ```
122
+
123
+ ### Nested property binding
124
+ ```html
125
+ <div ${{'style.color': 'colorProp'}}>Text</div>
126
+ ```
127
+ Dot notation navigates nested properties on the element.
128
+
129
+ ### Direct child component state binding
130
+ ```html
131
+ <child-component ${{'$.innerProp': 'parentProp'}}></child-component>
132
+ ```
133
+ The `$.` prefix accesses the child component's `$` state proxy directly.
134
+
135
+ ### Attribute binding (`@` prefix)
136
+ ```html
137
+ <div ${{'@hidden': 'isHidden'}}>Content</div>
138
+ <input ${{'@disabled': 'isDisabled'}}>
139
+ <div ${{'@data-value': 'myValue'}}></div>
140
+ ```
141
+ The `@` prefix means "bind to HTML attribute" (not DOM property). For boolean attributes: `true` → attribute present, `false` → attribute removed. `@` is for binding syntax only, do NOT use it as a regular HTML attribute prefix.
142
+
143
+ ### Type casting (`!` / `!!`)
144
+ ```html
145
+ <div ${{'@hidden': '!showContent'}}>...</div> <!-- inverted boolean -->
146
+ <div ${{'@contenteditable': '!!hasText'}}>...</div> <!-- double inversion = cast to boolean -->
147
+ ```
148
+
149
+ ### Loose-coupling alternative (plain HTML, no JS context needed)
150
+ ```html
151
+ <div bind="textContent: myProp"></div>
152
+ <div bind="onclick: handler; @hidden: !flag"></div>
153
+ ```
154
+ This is the raw form. The `html` helper generates it automatically.
155
+
156
+ ---
157
+
158
+ ## Property Token Prefixes
159
+
160
+ Prefixes control which data context a binding resolves to:
161
+
162
+ | Prefix | Meaning | Example | Description |
163
+ |--------|---------|---------|-------------|
164
+ | _(none)_ | Local state | `{{count}}` | Current component's local context |
165
+ | `^` | Parent inherited | `{{^parentProp}}` | Walk up DOM ancestry to find nearest component that has this prop |
166
+ | `*` | Shared context | `{{*sharedProp}}` | Shared context scoped by `ctx` attribute or CSS `--ctx` |
167
+ | `/` | Named context | `{{APP/myProp}}` | Global named context identified by key before `/` |
168
+ | `--` | CSS Data | `${{textContent: '--my-css-var'}}` | Read value from CSS custom property |
169
+ | `+` | Computed | (in init$) `'+sum': () => ...` | Function recalculated when local dependencies change (auto-tracked) |
170
+
171
+ ### Examples in init$
172
+ ```js
173
+ init$ = {
174
+ localProp: 'hello', // local
175
+ '*sharedProp': 'shared value', // shared context
176
+ 'APP/globalProp': 42, // named context "APP"
177
+ '+computed': () => this.$.a + this.$.b, // local computed (auto-tracked)
178
+ };
179
+ ```
180
+
181
+ ### Computed properties (v3.x)
182
+
183
+ Computed props use the `+` prefix and are auto-tracked: dependencies are recorded when the function executes.
184
+
185
+ **Local computed** — reacts to local state changes automatically:
186
+ ```js
187
+ init$ = {
188
+ a: 1,
189
+ b: 2,
190
+ '+sum': () => this.$.a + this.$.b, // auto-tracks 'a' and 'b'
191
+ };
192
+ ```
193
+
194
+ **Cross-context computed** — reacts to external named context changes via explicit deps:
195
+ ```js
196
+ init$ = {
197
+ local: 0,
198
+ '+total': {
199
+ deps: ['GAME/score'],
200
+ fn: () => this.$['GAME/score'] + this.$.local,
201
+ },
202
+ };
203
+ ```
204
+
205
+ > **NOTE**: Computed values are recalculated asynchronously (via `queueMicrotask`), so subscribers are notified in the next microtask, not inline during `pub()`.
206
+ ```
207
+
208
+ ---
209
+
210
+ ## State Management API
211
+
212
+ ### `$` proxy (read/write state)
213
+ ```js
214
+ this.$.count = 10; // publish
215
+ let val = this.$.count; // read
216
+ this.$['APP/prop'] = 'x'; // named context
217
+ this.$['^parentProp'] = 5; // parent context
218
+ ```
219
+
220
+ ### `set$(kvObj, forcePrimitives?)` — bulk update
221
+ ```js
222
+ this.set$({ name: 'Jane', count: 5 });
223
+ // forcePrimitives=true → triggers callbacks even if value unchanged (for primitives)
224
+ ```
225
+
226
+ ### `sub(prop, handler, init?)` — subscribe to changes
227
+ ```js
228
+ this.sub('count', (val) => {
229
+ console.log('count changed:', val);
230
+ });
231
+ // init defaults to true (handler called immediately with current value)
232
+ ```
233
+
234
+ ### `add(prop, val, rewrite?)` — add property to context
235
+ ### `add$(obj, rewrite?)` — bulk add
236
+
237
+ ### `has(prop)` — check if property exists in context
238
+ ### `notify(prop)` — force notification to all subscribers
239
+
240
+ > **WARNING**: Property keys with nested dots (`prop.sub`) are NOT supported as state keys.
241
+ > Use flat names: `propSub` instead of `prop.sub`.
242
+
243
+ ---
244
+
245
+ ## PubSub (Standalone State Management)
246
+
247
+ ```js
248
+ import { PubSub } from '@symbiotejs/symbiote';
249
+
250
+ // Register a named global context
251
+ const ctx = PubSub.registerCtx({
252
+ userName: 'Anonymous',
253
+ score: 0,
254
+ }, 'GAME'); // 'GAME' is the context key
255
+
256
+ // Read/write from any component
257
+ this.$['GAME/userName'] = 'Player 1';
258
+ console.log(this.$['GAME/score']);
259
+
260
+ // Subscribe from any component
261
+ this.sub('GAME/score', (val) => {
262
+ console.log('Score:', val);
263
+ });
264
+
265
+ // Direct PubSub API
266
+ ctx.pub('score', 100);
267
+ ctx.read('score');
268
+ ctx.sub('score', callback);
269
+ ctx.multiPub({ score: 100, userName: 'Hero' });
270
+ ```
271
+
272
+ ### PubSub static methods
273
+ - `PubSub.registerCtx(schema, uid?)` → `PubSub` instance
274
+ - `PubSub.getCtx(uid, notify?)` → `PubSub` instance or null
275
+ - `PubSub.deleteCtx(uid)`
276
+
277
+ ---
278
+
279
+ ## Shared Context (`*` prefix)
280
+
281
+ Components grouped by the `ctx` HTML attribute (or `--ctx` CSS custom property) share a data context. Properties with `*` prefix are read/written in this shared context — inspired by native HTML `name` attribute grouping (like radio button groups):
282
+
283
+ ```html
284
+ <upload-btn ctx="gallery"></upload-btn>
285
+ <file-list ctx="gallery"></file-list>
286
+ ```
287
+
288
+ ```js
289
+ class UploadBtn extends Symbiote {
290
+ init$ = {
291
+ '*files': [],
292
+ onUpload: () => {
293
+ this.$['*files'] = [...this.$['*files'], newFile];
294
+ },
295
+ }
296
+ }
297
+
298
+ class FileList extends Symbiote {
299
+ init$ = {
300
+ '*files': [], // same shared prop — first-registered value wins
301
+ }
302
+ }
303
+ ```
304
+
305
+ Both components access the same `*files` state — no parent component, no prop drilling, no global store. Just set `ctx="gallery"` in HTML and use `*`-prefixed properties.
306
+
307
+ ### Context name resolution (first match wins)
308
+ 1. `ctx="name"` HTML attribute
309
+ 2. `--ctx` CSS custom property (inherited from ancestors)
310
+ 3. No match → `*` props are **silently skipped** (dev mode warns)
311
+
312
+ > **WARNING**: `*` properties require an explicit `ctx` attribute or `--ctx` CSS variable. Without one, the shared context is not created and `*` props have no effect.
313
+
314
+ ---
315
+
316
+ ## Lifecycle & Instance Properties
317
+
318
+ ### Lifecycle callbacks (override in subclass)
319
+ | Method | When called |
320
+ |--------|------------|
321
+ | `initCallback()` | Once, after state initialized, before render (if `pauseRender=true`) or normally after render |
322
+ | `renderCallback()` | Once, after template is rendered and attached to DOM |
323
+ | `destroyCallback()` | On disconnect, after 100ms delay, only if `readyToDestroy=true` |
324
+
325
+ ### Constructor flags (set in constructor or as class fields)
326
+ | Property | Default | Description |
327
+ |----------|---------|-------------|
328
+ | `pauseRender` | `false` | Skip automatic rendering; call `this.render()` manually later |
329
+ | `renderShadow` | `false` | Render template into Shadow DOM |
330
+ | `readyToDestroy` | `true` | Allow cleanup on disconnect |
331
+ | `processInnerHtml` | `false` | Process existing inner HTML with template processors |
332
+ | `ssrMode` | `false` | **Client-only.** Hydrate server-rendered HTML: skips template injection, attaches bindings to existing DOM. Supports both light DOM and Declarative Shadow DOM. Ignored when `__SYMBIOTE_SSR` is active (server side) |
333
+ | `allowCustomTemplate` | `false` | Allow `use-template="#selector"` attribute |
334
+ | `isVirtual` | `false` | Replace element with its template fragment |
335
+ | `allowTemplateInits` | `true` | Auto-add props found in template but not in init$ |
336
+
337
+ ### Instance properties (available after render)
338
+ - `this.ref` — object map of `ref`-attributed elements
339
+ - `this.initChildren` — array of original child nodes (before template render)
340
+ - `this.$` — state proxy
341
+ - `this.allSubs` — Set of all subscriptions (for cleanup)
342
+ ---
343
+
344
+ ## Exit Animation (`animateOut`)
345
+
346
+ `animateOut(el)` sets `[leaving]` attribute, waits for CSS `transitionend`, then removes the element. If no CSS transition is defined, removes immediately. Available as standalone import or `Symbiote.animateOut`.
347
+
348
+ ```js
349
+ import { animateOut } from '@symbiotejs/symbiote';
350
+ // or: Symbiote.animateOut(el)
351
+ ```
352
+
353
+ ### CSS pattern
354
+
355
+ ```css
356
+ my-item {
357
+ opacity: 1;
358
+ transform: translateY(0);
359
+ transition: opacity 0.3s, transform 0.3s;
360
+
361
+ /* Enter (CSS-native, no JS needed): */
362
+ @starting-style {
363
+ opacity: 0;
364
+ transform: translateY(20px);
365
+ }
366
+
367
+ /* Exit (triggered by animateOut): */
368
+ &[leaving] {
369
+ opacity: 0;
370
+ transform: translateY(-10px);
371
+ }
372
+ }
373
+ ```
374
+
375
+ ### Itemize integration
376
+
377
+ Both itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM.
378
+
379
+ ---
380
+
381
+ ## Styling
382
+
383
+ ### rootStyles (Light DOM, adopted stylesheets)
384
+ ```js
385
+ MyComponent.rootStyles = css`
386
+ my-component {
387
+ display: block;
388
+ color: var(--text-color);
389
+ }
390
+ `;
391
+ ```
392
+ Styles are added to the closest document root via `adoptedStyleSheets`. Use the custom tag name as selector.
393
+
394
+ ### shadowStyles (Shadow DOM, auto-creates shadow root)
395
+ ```js
396
+ MyComponent.shadowStyles = css`
397
+ :host {
398
+ display: block;
399
+ }
400
+ button {
401
+ color: red;
402
+ }
403
+ `;
404
+ ```
405
+ Setting `shadowStyles` automatically creates a Shadow Root and uses `adoptedStyleSheets`.
406
+
407
+ ### addRootStyles / addShadowStyles (append additional sheets)
408
+ ```js
409
+ MyComponent.addRootStyles(anotherSheet);
410
+ MyComponent.addShadowStyles(anotherSheet);
411
+ ```
412
+
413
+ ### `css` tag function
414
+ Returns a `CSSStyleSheet` instance (constructable stylesheet). Supports processors:
415
+ ```js
416
+ css.useProcessor((txt) => txt.replaceAll('$accent', '#ff0'));
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Element References
422
+
423
+ ```js
424
+ MyComponent.template = html`
425
+ <input ${{ref: 'nameInput'}}>
426
+ <button ${{ref: 'submitBtn', onclick: 'onSubmit'}}>Submit</button>
427
+ `;
428
+
429
+ // In renderCallback:
430
+ this.ref.nameInput.focus();
431
+ this.ref.submitBtn.disabled = true;
432
+ ```
433
+
434
+ Alternative HTML syntax: `<div ref="myRef"></div>`
435
+
436
+ ---
437
+
438
+ ## Itemize API (Dynamic Lists)
439
+
440
+ ```js
441
+ class MyList extends Symbiote {
442
+ init$ = {
443
+ items: [
444
+ { name: 'Alice', role: 'Admin' },
445
+ { name: 'Bob', role: 'User' },
446
+ ],
447
+ onItemClick: (e) => {
448
+ console.log('clicked');
449
+ },
450
+ };
451
+ }
452
+
453
+ MyList.template = html`
454
+ <ul ${{itemize: 'items'}}>
455
+ <template>
456
+ <li>
457
+ <span>{{name}}</span> - <span>{{role}}</span>
458
+ <button ${{onclick: '^onItemClick'}}>Click</button>
459
+ </li>
460
+ </template>
461
+ </ul>
462
+ `;
463
+ ```
464
+
465
+ > **CRITICAL**: Inside itemize templates, items are full Symbiote components with their own state scope.
466
+ > - `{{name}}` — item's own property
467
+ > - `${{onclick: 'handler'}}` — binds to the item component's own method/property
468
+ > - `${{onclick: '^handler'}}` — use `^` prefix to reach the **parent** component's property
469
+ > - Failure to use `^` for parent handlers will result in broken event bindings
470
+
471
+ ### Custom item component
472
+ ```html
473
+ <div ${{itemize: 'items', 'item-tag': 'my-item'}}></div>
474
+ ```
475
+ Then define `my-item` as a separate Symbiote component.
476
+
477
+ ### Data formats
478
+ - **Array**: `[{prop: val}, ...]` — items rendered in order
479
+ - **Object**: `{key1: {prop: val}, ...}` — items get `_KEY_` property added
480
+
481
+ ### Updating lists
482
+ Assign new array to trigger re-render:
483
+ ```js
484
+ this.$.items = [...newItems]; // triggers update
485
+ ```
486
+ Existing items are updated in-place via `set$`, new items appended, excess removed.
487
+
488
+ ### Itemize class property fallback (v3.2)
489
+ The `itemize` data source property supports class property fallback, consistent with other template processors:
490
+ ```js
491
+ class MyList extends Symbiote {
492
+ items = [{ name: 'Alice' }, { name: 'Bob' }]; // no init$ needed
493
+ }
494
+ ```
495
+
496
+ ### Keyed itemize processor (optional, v3.0+)
497
+ Drop-in replacement with reference-equality fast paths and key-based reconciliation. Up to **3×** faster for appends, **32×** for no-ops:
498
+ ```js
499
+ import { itemizeProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor-keyed.js';
500
+ import { itemizeProcessor as defaultProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor.js';
501
+
502
+ class BigList extends Symbiote {
503
+ constructor() {
504
+ super();
505
+ this.templateProcessors.delete(defaultProcessor);
506
+ this.templateProcessors = new Set([itemizeProcessor, ...this.templateProcessors]);
507
+ }
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Slots (Light DOM)
514
+
515
+ Slots work without Shadow DOM (processed by `slotProcessor`). Import and add manually since v2.x:
516
+
517
+ ```js
518
+ import { slotProcessor } from '@symbiotejs/symbiote/core/slotProcessor.js';
519
+
520
+ class MyWrapper extends Symbiote {
521
+ constructor() {
522
+ super();
523
+ this.templateProcessors.add(slotProcessor);
524
+ }
525
+ }
526
+
527
+ MyWrapper.template = html`
528
+ <header><slot name="header"></slot></header>
529
+ <main><slot></slot></main>
530
+ `;
531
+ ```
532
+
533
+ Usage:
534
+ ```html
535
+ <my-wrapper>
536
+ <h1 slot="header">Title</h1>
537
+ <p>Default slot content</p>
538
+ </my-wrapper>
539
+ ```
540
+
541
+ ---
542
+
543
+ ## Server-Side Rendering (SSR)
544
+
545
+ Import `node/SSR.js` to render components to HTML strings on the server. Requires `linkedom` (optional peer dependency).
546
+
547
+ ### Basic usage — `processHtml`
548
+
549
+ ```js
550
+ import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
551
+
552
+ await SSR.init(); // patches globals with linkedom env
553
+ await import('./my-component.js'); // component reg() works normally
554
+
555
+ let html = await SSR.processHtml('<div><my-component></my-component></div>');
556
+ // => '<div><my-component><style>...</style><template shadowrootmode="open">...</template>content</my-component></div>'
557
+
558
+ SSR.destroy(); // cleanup globals
559
+ ```
560
+
561
+ `processHtml` takes any HTML string, renders all Symbiote components found within, and returns the processed HTML. If `SSR.init()` was already called, it reuses the existing environment; otherwise it auto-initializes (and auto-destroys after).
562
+
563
+ ### Advanced — `renderToString` / `renderToStream`
564
+
565
+ ```js
566
+ import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
567
+
568
+ await SSR.init();
569
+ await import('./my-component.js');
570
+
571
+ let html = SSR.renderToString('my-component', { title: 'Hello' });
572
+ // => '<my-component title="Hello"><h1>Hello</h1></my-component>'
573
+
574
+ SSR.destroy();
575
+ ```
576
+
577
+ ### API
578
+
579
+ | Method | Description |
580
+ |--------|-------------|
581
+ | `SSR.init()` | `async` — creates linkedom document, polyfills CSSStyleSheet/NodeFilter/MutationObserver/adoptedStyleSheets, patches globals |
582
+ | `SSR.processHtml(html)` | `async` — parses HTML string, renders all custom elements, returns processed HTML. Auto-inits if needed |
583
+ | `SSR.renderToString(tagName, attrs?)` | Creates element, triggers `connectedCallback`, serializes to HTML string |
584
+ | `SSR.renderToStream(tagName, attrs?)` | Async generator — yields HTML chunks. Same output as `renderToString`, but streamed for lower TTFB |
585
+ | `SSR.destroy()` | Removes global patches, cleans up document |
586
+
587
+ ### Styles in SSR output
588
+
589
+ - **rootStyles** → `<style>` tag as first child of the component (light DOM, deduplicated per constructor)
590
+ - **shadowStyles** → `<style>` inside the Declarative Shadow DOM `<template>`
591
+ - Both are supported simultaneously on the same component
592
+
593
+ ### Streaming usage
594
+
595
+ ```js
596
+ import http from 'node:http';
597
+ import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
598
+
599
+ await SSR.init();
600
+ import './my-app.js';
601
+
602
+ http.createServer(async (req, res) => {
603
+ res.writeHead(200, { 'Content-Type': 'text/html' });
604
+ res.write('<!DOCTYPE html><html><body>');
605
+ for await (let chunk of SSR.renderToStream('my-app')) {
606
+ res.write(chunk);
607
+ }
608
+ res.end('</body></html>');
609
+ }).listen(3000);
610
+ ```
611
+
612
+ ### Shadow DOM output
613
+
614
+ Shadow components produce Declarative Shadow DOM markup with styles inlined. Light DOM content is preserved alongside the DSD template:
615
+ ```html
616
+ <my-shadow>
617
+ <style>my-shadow { display: block; }</style>
618
+ <template shadowrootmode="open">
619
+ <style>:host { color: red; }</style>
620
+ <h1>Content</h1>
621
+ <slot></slot>
622
+ </template>
623
+ Light DOM content here
624
+ </my-shadow>
625
+ ```
626
+
627
+ ### SSR context detection
628
+
629
+ `SSR.init()` sets `globalThis.__SYMBIOTE_SSR = true`. This is separate from the instance `ssrMode` flag:
630
+
631
+ | Flag | Scope | Purpose |
632
+ |------|-------|-------|
633
+ | `__SYMBIOTE_SSR` | Server (global) | Preserves binding attributes (`bind`, `ref`, `itemize`) in HTML output. Bypasses `ssrMode` effects |
634
+ | `ssrMode` | Client (instance) | Skips template injection, hydrates existing DOM with bindings |
635
+
636
+ ### Hydration flow
637
+
638
+ 1. **Server**: `SSR.processHtml()` / `SSR.renderToString()` produces HTML with `bind=` / `itemize=` attributes preserved
639
+ 2. **Client**: component with `ssrMode = true` skips template injection, attaches bindings to pre-rendered DOM
640
+ 3. State mutations on client update DOM reactively
641
+
642
+ ---
643
+
644
+ ## Routing (AppRouter)
645
+
646
+ ### Path-based routing (recommended)
647
+ ```js
648
+ import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
649
+
650
+ const routerCtx = AppRouter.initRoutingCtx('R', {
651
+ home: { pattern: '/', title: 'Home', default: true },
652
+ user: { pattern: '/users/:id', title: 'User Profile' },
653
+ settings: { pattern: '/settings', title: 'Settings' },
654
+ notFound: { pattern: '/404', title: 'Not Found', error: true },
655
+ });
656
+
657
+ // Navigate programmatically
658
+ AppRouter.navigate('user', { id: '42' });
659
+ // URL becomes: /users/42
660
+
661
+ // React to route changes in any component
662
+ this.sub('R/route', (route) => console.log('Route:', route));
663
+ this.sub('R/options', (opts) => console.log('Params:', opts)); // { id: '42' }
664
+ ```
665
+
666
+ ### Query-string routing (legacy/alternative)
667
+ ```js
668
+ // Routes WITHOUT `pattern` use query-string mode automatically
669
+ const routerCtx = AppRouter.initRoutingCtx('R', {
670
+ home: { title: 'Home', default: true },
671
+ about: { title: 'About' },
672
+ });
673
+ AppRouter.navigate('about', { section: 'team' });
674
+ // URL becomes: ?about&section=team
675
+ ```
676
+
677
+ ### Route guards
678
+ ```js
679
+ // Register guard — runs before every navigation
680
+ let unsub = AppRouter.beforeRoute((to, from) => {
681
+ if (!isAuth && to.route === 'settings') {
682
+ return 'login'; // redirect
683
+ }
684
+ // return false to cancel, nothing to proceed
685
+ });
686
+
687
+ unsub(); // remove guard
688
+ ```
689
+
690
+ ### Lazy loaded routes
691
+ ```js
692
+ AppRouter.initRoutingCtx('R', {
693
+ settings: {
694
+ pattern: '/settings',
695
+ title: 'Settings',
696
+ load: () => import('./pages/settings.js'), // loaded once, cached
697
+ },
698
+ });
699
+ ```
700
+
701
+ ### AppRouter API
702
+ - `AppRouter.initRoutingCtx(ctxName, routingMap)` → PubSub
703
+ - `AppRouter.navigate(route, options?)` — navigate and dispatch event
704
+ - `AppRouter.reflect(route, options?)` — update URL without triggering event
705
+ - `AppRouter.notify()` — read URL, run guards, lazy load, dispatch event
706
+ - `AppRouter.beforeRoute(fn)` — register guard, returns unsubscribe fn
707
+ - `AppRouter.setRoutingMap(map)` — extend routes
708
+ - `AppRouter.readAddressBar()` → `{ route, options }`
709
+ - `AppRouter.setSeparator(char)` — default `&` (query-string mode)
710
+ - `AppRouter.setDefaultTitle(title)`
711
+ - `AppRouter.removePopstateListener()`
712
+ - Mode auto-detected: routes with `pattern` → path-based, without → query-string
713
+
714
+ ---
715
+
716
+ ## Attribute Binding
717
+
718
+ ```js
719
+ class MyComponent extends Symbiote {
720
+ init$ = {
721
+ '@name': '', // reads from HTML attribute `name` automatically
722
+ };
723
+ }
724
+ MyComponent.bindAttributes({
725
+ 'value': 'inputValue', // maps HTML attr `value` → state prop `inputValue`
726
+ });
727
+ // observedAttributes is auto-populated
728
+ ```
729
+
730
+ ---
731
+
732
+ ## CSS Data Binding
733
+
734
+ Read CSS custom property values into component state:
735
+
736
+ ```js
737
+ class MyComponent extends Symbiote {
738
+ cssInit$ = {
739
+ '--accent-color': '#ff0', // fallback value
740
+ };
741
+ }
742
+ ```
743
+
744
+ Or in template:
745
+ ```html
746
+ <div ${{textContent: '--my-css-prop'}}>...</div>
747
+ ```
748
+
749
+ Update with: `this.updateCssData()` / `this.dropCssDataCache()`.
750
+
751
+ ---
752
+
753
+ ## Component Registration
754
+
755
+ ```js
756
+ // Explicit tag name
757
+ MyComponent.reg('my-component');
758
+
759
+ // Auto-generated tag (sym-1, sym-2, ...)
760
+ MyComponent.reg();
761
+
762
+ // Alias registration (creates a subclass)
763
+ MyComponent.reg('my-alias', true);
764
+
765
+ // Get tag name (auto-registers if needed)
766
+ const tag = MyComponent.is; // 'my-component'
767
+ ```
768
+
769
+ ---
770
+
771
+ ## Utilities
772
+
773
+ ```js
774
+ import { UID } from '@symbiotejs/symbiote';
775
+ UID.generate('XXXXX-XXX'); // e.g. 'aB3kD-z9Q'
776
+
777
+ import { create, applyStyles, applyAttributes } from '@symbiotejs/symbiote';
778
+ let el = create({ tag: 'div', attributes: { id: 'x' }, styles: { color: 'red' }, children: [...] });
779
+
780
+ import { reassignDictionary } from '@symbiotejs/symbiote';
781
+ reassignDictionary({ BIND_ATTR: 'data-bind' }); // customize internal attribute names
782
+ ```
783
+
784
+ ---
785
+
786
+ ## Security (Trusted Types)
787
+
788
+ Template `innerHTML` writes use a Trusted Types policy when the API is available:
789
+
790
+ ```js
791
+ // Symbiote creates a passthrough policy automatically:
792
+ // trustedTypes.createPolicy('symbiote', { createHTML: (s) => s })
793
+ ```
794
+
795
+ This makes Symbiote compatible with strict CSP headers:
796
+ ```
797
+ Content-Security-Policy: require-trusted-types-for 'script'; trusted-types symbiote
798
+ ```
799
+
800
+ No sanitization is performed — templates are developer-authored, not user input. The policy name is `'symbiote'`.
801
+
802
+ ---
803
+
804
+ ## Dev Mode
805
+
806
+ Enable verbose warnings during development:
807
+ ```js
808
+ Symbiote.devMode = true;
809
+ ```
810
+
811
+ **Always-on** (regardless of `devMode`):
812
+ - `[Symbiote]` prefixed warnings for PubSub errors, duplicate tags, type mismatches, router issues
813
+ - `this` in template interpolation error (`html` tag detects `${this.x}` usage)
814
+
815
+ **Dev-only** (`devMode = true`):
816
+ - Unresolved binding keys — warns when a template binding auto-initializes to `null` (likely typo)
817
+ - `*prop` without `ctx` attribute or `--ctx` CSS variable — warns that shared context won't be created
818
+ - `*prop` conflict — warns when a later component tries to set a different initial value for the same shared prop
819
+
820
+ ---
821
+
822
+ ## Common Mistakes to Avoid
823
+
824
+ 1. **DON'T** use `this` in template strings — templates are decoupled from component context
825
+ 2. **DON'T** nest property keys with dots in state: `'obj.prop'` won't work as a state key
826
+ 3. **DON'T** forget `^` prefix when referencing **parent** component properties from itemize items
827
+ 4. **DON'T** use `@` prefix directly in HTML — it's only for binding syntax (`${{'@attr': 'prop'}}`)
828
+ 5. **DON'T** treat `init$` as a regular object — it's processed at connection time
829
+ 6. **DON'T** define `template` inside the class body (`static template = html\`...\`` won't work) — it's a static property **setter**, assign it outside: `MyComponent.template = html\`...\``. Same applies to `rootStyles` and `shadowStyles`.
830
+ 7. **DON'T** expect Shadow DOM by default — use `renderShadow = true` or `shadowStyles` to opt in
831
+ 8. **DON'T** wrap Custom Elements in extra divs — the custom tag IS the wrapper
832
+ 9. **DON'T** use CSS frameworks (Tailwind, etc.) — use native CSS with custom properties
833
+ 10. **DON'T** use `require()` — ESM only (import/export)
834
+ 11. **DON'T** use `*prop` without `ctx` attribute or `--ctx` CSS variable — shared context won't be created