@symbiotejs/symbiote 3.8.0 → 3.8.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/llms-full.txt CHANGED
@@ -2,23 +2,25 @@
2
2
 
3
3
  <img src="https://rnd-pro.com/svg/symbiote/index.svg" width="200" alt="Symbiote.js">
4
4
 
5
- A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional. **~7.3kb** brotli / **~8.1kb** gzip.
5
+ A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no black boxes, no excess repaints. No build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional.
6
6
 
7
- Symbiote.js gives you the convenience of a modern framework while staying close to the native platform - HTML, CSS, and DOM APIs. Components are real custom elements that work everywhere: in any framework, in plain HTML, or in a micro-frontend architecture. And with **isomorphic mode**, the same component code works on the server and the client - server-rendered pages hydrate automatically, no diffing, no mismatch errors.
7
+ Here are the three most important differences between Symbiote.js and other frameworks:
8
+ 1. **Natural DOM Extension Philosophy** - designed to extend platform APIs, not to replace them
9
+ 2. **Runtime-Agnostic HTML Templates** - outstanding flexibility for rendering strategies and further customization
10
+ 3. **Powerful App-wide State Management** - combine data contexts without bloated boilerplate or external tools
8
11
 
9
- ## What's new?
12
+ ## What's new in v3.x?
10
13
 
11
- - **Experimental WebMCP support** - expose live Symbiote UI actions as browser-native tools for agents. See the [WebMCP docs](https://github.com/symbiotejs/symbiote.js/blob/webmcp/docs/webmcp.md).
14
+ - **WebMCP support** - expose live Symbiote UI actions as browser-native tools for agents. See the [WebMCP docs](https://github.com/symbiotejs/symbiote.js/blob/webmcp/docs/webmcp.md).
12
15
  - **Server-Side Rendering** - render components to HTML with `SSR.processHtml()` or stream chunks with `SSR.renderToStream()`. Client-side hydration via `ssrMode` attaches bindings to existing DOM without re-rendering.
13
16
  - **Isomorphic components** - `isoMode` flag makes components work in both SSR and client-only scenarios automatically. If server-rendered content exists, it hydrates; otherwise it renders the template from scratch. One component, zero conditional logic.
14
- - **Computed properties** - reactive derived state with microtask batching.
17
+ - **Computed properties refined** - reactive derived state with microtask batching.
15
18
  - **Path-based router** - optional `AppRouter` module with `:param` extraction, route guards, and lazy loading.
16
19
  - **Exit animations** - `animateOut(el)` for CSS-driven exit transitions, integrated into itemize API.
17
20
  - **Dev mode** - `Symbiote.devMode` enables verbose warnings; import `devMessages.js` for full human-readable messages.
18
21
  - **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM.
19
22
  - **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods.
20
23
  - **Lazy mode** - `lazyMode` flag defers component initialization and rendering based on viewport visibility. Can also be enabled via the `lazy` attribute on `itemize` containers to efficiently handle massive data sets.
21
- - And [more](https://github.com/symbiotejs/symbiote.js/blob/webmcp/CHANGELOG.md).
22
24
 
23
25
  ## Quick start
24
26
 
@@ -56,57 +58,6 @@ npm i @symbiotejs/symbiote
56
58
  import Symbiote, { html, css } from '@symbiotejs/symbiote';
57
59
  ```
58
60
 
59
- ## Isomorphic Web Components
60
-
61
- One component. Server-rendered or client-rendered - automatically. Set `isoMode = true` and the component figures it out: if server-rendered content exists, it hydrates; otherwise it renders from template. No conditional logic, no separate server/client versions:
62
- ```js
63
- class MyComponent extends Symbiote {
64
- isoMode = true;
65
- count = 0;
66
- increment() {
67
- this.$.count++;
68
- }
69
- }
70
-
71
- MyComponent.template = html`
72
- <h2 ${{textContent: 'count'}}></h2>
73
- <button ${{onclick: 'increment'}}>Click me!</button>
74
- `;
75
- MyComponent.reg('my-component');
76
- ```
77
-
78
- This exact code runs **everywhere** - SSR on the server, hydration on the client, or pure client rendering. No framework split, no `'use client'` directives, no hydration mismatch errors.
79
-
80
- ### SSR - one class, zero config
81
-
82
- Server rendering doesn't need a virtual DOM, a reconciler, or framework-specific packages:
83
-
84
- ```js
85
- import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
86
-
87
- await SSR.init(); // patches globals with linkedom
88
- await import('./my-app.js'); // components register normally
89
-
90
- let html = await SSR.processHtml('<my-app></my-app>');
91
- SSR.destroy();
92
- ```
93
-
94
- For large pages, stream HTML chunks with `SSR.renderToStream()` for faster TTFB. See [SSR docs](./docs/ssr.md) and [server setup recipes](./docs/ssr-server.md).
95
-
96
- ### How it compares
97
-
98
- | | **Symbiote.js** | **Next.js (React)** | **Lit** (`@lit-labs/ssr`) |
99
- |--|----------------|---------------------|----|
100
- | **Isomorphic code** | Same code, `isoMode` auto-detects | Server Components vs Client Components split | Same code, but load-order constraints |
101
- | **Hydration** | Binding-based - attaches to existing DOM, no diffing | `hydrateRoot()` - must produce identical output or errors | Requires `ssr-client` + hydrate support module |
102
- | **Packages** | 1 module + `linkedom` peer dep | Full framework buy-in | 3 packages: `ssr`, `ssr-client`, `ssr-dom-shim` |
103
- | **Streaming** | `renderToStream()` async generator | `renderToPipeableStream()` | Iterable `RenderResult` |
104
- | **Mismatch handling** | Not needed - bindings attach to whatever DOM exists | Hard errors if server/client output differs | N/A |
105
- | **Template output** | Clean HTML with `bind=` attributes | HTML with framework markers | HTML with `<!--lit-part-->` comment markers |
106
- | **Lock-in** | None - standard Web Components | Full framework commitment | Lit-specific, but Web Components |
107
-
108
- **Key insight:** There are no hydration mismatches because there's no diffing. The server produces HTML with binding attributes. The client reads those attributes and adds reactivity. That's it.
109
-
110
61
  ## Core concepts
111
62
 
112
63
  ### Reactive state
@@ -135,7 +86,7 @@ This makes it easy to control Symbiote-based widgets and microfrontends from any
135
86
 
136
87
  ### Templates
137
88
 
138
- Templates are plain HTML strings - context-free, easy to test, easy to move between files:
89
+ Templates are plain HTML strings - runtime-agnostic, easy to test, easy to move between files:
139
90
 
140
91
  ```js
141
92
  // Separate file: my-component.template.js
@@ -153,34 +104,24 @@ The `html` function supports two interpolation modes:
153
104
 
154
105
  ### Itemize (dynamic reactive lists)
155
106
 
156
- Render lists from data arrays with efficient updates:
107
+ Render lists from data arrays or objects with efficient updates:
157
108
  ```js
158
109
  class TaskList extends Symbiote {
159
110
  tasks = [
160
111
  { name: 'Buy groceries' },
161
112
  { name: 'Write docs' },
162
113
  ];
163
- init$ = {
164
- // Needs to be defined in init$ for pop-up binding to work
165
- onItemClick: () => {
166
- console.log('clicked!');
167
- },
168
- }
169
114
  }
170
115
 
171
116
  TaskList.template = html`
172
- <div itemize="tasks">
117
+ <ul itemize="tasks">
173
118
  <template>
174
- <div ${{onclick: '^onItemClick'}}>{{name}}</div>
119
+ <li>{{name}}</li>
175
120
  </template>
176
- </div>
121
+ </ul>
177
122
  `;
178
123
  ```
179
124
 
180
- Items have their own state scope. Use the **`^` prefix** to reach higher-level component properties and handlers - `'^onItemClick'` binds to the parent's `onItemClick`, not the item's. Properties referenced via `^` must be defined in the parent's `init$`.
181
-
182
- > **Performance Tip:** For massive lists, add the `lazy` attribute to the container (`<div itemize="tasks" lazy>`). It defers component initialization until they enter the viewport and cleans them up when they leave, heavily optimizing memory and rendering performance.
183
-
184
125
  ### Pop-up binding (`^`)
185
126
 
186
127
  The `^` prefix works in any nested component template - it walks up the DOM tree to find the nearest ancestor that has the property registered in its data context (`init$` or `add$()`):
@@ -193,14 +134,11 @@ The `^` prefix works in any nested component template - it walks up the DOM tree
193
134
  <button ${{onclick: '^parentHandler'}}>Click</button>
194
135
  ```
195
136
 
196
- > **Note:** Class property fallbacks are not checked by the `^` walk - the parent must define the property in `init$`.
197
-
198
137
  ### Named data contexts
199
138
 
200
139
  Share state across components without prop drilling:
201
-
202
140
  ```js
203
- import { PubSub } from '@symbiotejs/symbiote';
141
+ import { PubSub, html } from '@symbiotejs/symbiote';
204
142
 
205
143
  PubSub.registerCtx({
206
144
  user: 'Alex',
@@ -209,6 +147,9 @@ PubSub.registerCtx({
209
147
 
210
148
  // Any component can read/write:
211
149
  this.$['APP/user'] = 'New name';
150
+
151
+ // Any template can use property directly:
152
+ let template = html`<h2>{{APP/user}}</h2>`;
212
153
  ```
213
154
 
214
155
  ### Shared context (`*`)
@@ -241,11 +182,10 @@ class StatusBar extends Symbiote {
241
182
 
242
183
  All three components access the same `*files` state - no parent component, no prop drilling, no global store boilerplate. Just set `ctx="gallery"` in HTML and use `*`-prefixed properties. This makes it trivial to build complex component relationships purely in markup, with ready-made components that don't need to know about each other.
243
184
 
244
- The context name can also be inherited via CSS custom property `--ctx`, enabling layout-driven grouping.
245
-
246
- ### Routing (optional module)
185
+ ### Application routing
247
186
 
248
187
  ```js
188
+ // Import optional module:
249
189
  import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
250
190
 
251
191
  AppRouter.initRoutingCtx('R', {
@@ -255,23 +195,7 @@ AppRouter.initRoutingCtx('R', {
255
195
  });
256
196
  ```
257
197
 
258
- ### Exit animations
259
-
260
- CSS-driven transitions with zero JS animation code:
261
-
262
- ```css
263
- task-item {
264
- opacity: 1;
265
- transition: opacity 0.3s;
266
-
267
- @starting-style { opacity: 0; } /* enter */
268
- &[leaving] { opacity: 0; } /* exit */
269
- }
270
- ```
271
-
272
- `animateOut(el)` sets `[leaving]`, waits for `transitionend`, then removes. Itemize uses this automatically.
273
-
274
- ### Styling
198
+ ### CSS Styling
275
199
 
276
200
  Shadow DOM is **optional** in Symbiote - use it when you need isolation, skip it when you don't. This gives full flexibility:
277
201
 
@@ -288,6 +212,8 @@ MyComponent.rootStyles = css`
288
212
  `;
289
213
  ```
290
214
 
215
+ This style will be applied to nearest upper shadow root, if exists and to common document if not.
216
+
291
217
  **Shadow DOM** - opt-in isolation when needed:
292
218
 
293
219
  ```js
@@ -301,9 +227,9 @@ Isolated.shadowStyles = css`
301
227
 
302
228
  All native CSS features work as expected: CSS variables flow through shadow boundaries, `::part()` exposes internals, modern nesting, `@layer`, `@container` - no framework abstractions in the way. Mix light DOM and shadow DOM components freely in the same app.
303
229
 
304
- ### CSS Data Binding
230
+ ### CSS Data
305
231
 
306
- Components can read CSS custom properties as reactive state via `cssInit$`:
232
+ Components can read CSS custom property values to initiate reactive state:
307
233
 
308
234
  ```css
309
235
  my-widget {
@@ -319,8 +245,6 @@ MyWidget.template = html`
319
245
  `;
320
246
  ```
321
247
 
322
- CSS values are parsed automatically - quoted strings become strings, numbers become numbers. Call `this.updateCssData()` to re-read after runtime CSS changes. This enables CSS-driven configuration: theme values, layout parameters, or localized strings - all settable from CSS without touching JS.
323
-
324
248
  ## Best for
325
249
 
326
250
  - **Complex widgets** embedded in any host application
@@ -329,32 +253,15 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
329
253
  - **Reusable component libraries** - works in React, Vue, Angular, or plain HTML
330
254
  - **SSR-powered apps** - lightweight server rendering without framework lock-in
331
255
  - **Framework-agnostic solutions** - one codebase, any context
332
-
333
- ## Bundle size
334
-
335
- | Library | Minified | Gzip | Brotli |
336
- |---------|----------|------|--------|
337
- | **Symbiote.js** (core) | 23.6 kb | 8.1 kb | **7.3 kb** |
338
- | **Symbiote.js** (full, with AppRouter + WebMCP export) | 35.6 kb | 12.1 kb | **11.0 kb** |
339
- | **Symbiote.js** (WebMCP extension) | 31.4 kb | 10.6 kb | **9.6 kb** |
340
- | **Lit** 3.3 | 15.5 kb | 6.0 kb | **~5.1 kb** |
341
- | **React 19 + ReactDOM** | ~186 kb | ~59 kb | **~50 kb** |
342
-
343
- Symbiote and Lit have similar base sizes, but Symbiote's **7.3 kb** core includes more built-in features: global state management, lists (itemize API), exit animations, computed properties etc. Lit needs additional packages for comparable features. React is **~7× larger** before adding a router, state manager, or SSR framework.
344
-
345
- ## Browser support
346
-
347
- All modern browsers: Chrome, Firefox, Safari, Edge, Opera.
256
+ - **Modern AI-first web** - expose the application state to WebMCP tools automatically
348
257
 
349
258
  ## Docs & Examples
350
259
 
351
260
  - [Documentation](https://github.com/symbiotejs/symbiote.js/blob/main/docs/README.md)
352
- - [Lit vs Symbiote.js](https://github.com/symbiotejs/symbiote.js/blob/main/docs/lit-vs-symbiote.md) - Side-by-side comparison
353
261
  - [Live Examples](https://rnd-pro.com/symbiote/3x/examples/) - Interactive Code Playground
354
262
  - [JSDA-Kit](https://github.com/rnd-pro/jsda-kit) - All-in-one companion tool: server, SSG, bundling, import maps, and native Symbiote.js SSR integration
355
263
  - [AI / llms.txt](https://rnd-pro.com/symbiote/llms.txt) — index for AI tools
356
264
  - [Full docs (single file)](https://rnd-pro.com/symbiote/llms-full.txt) — complete merged reference for AI context
357
- - [Changelog](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md)
358
265
 
359
266
  ## Related articles
360
267
 
@@ -411,7 +318,7 @@ my-item {
411
318
 
412
319
  ## Itemize integration
413
320
 
414
- Both itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
321
+ Itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
415
322
  ```css
416
323
  user-card {
417
324
  opacity: 1;
@@ -455,7 +362,7 @@ class MyComponent extends Symbiote {}
455
362
  MyComponent.template = html`<h1>{{@attribute-name}}</h1>`;
456
363
  ```
457
364
 
458
- Then use it as an attribute in markup:
365
+ Then use it as an component's attribute in markup:
459
366
  ```html
460
367
  <my-component attribute-name="attribute value"></my-component>
461
368
  ```
@@ -479,7 +386,7 @@ MyComponent.observedAttributes = [
479
386
 
480
387
  ## `bindAttributes()` static method
481
388
 
482
- Bind attributes to property values directly:
389
+ Reflect attribute values to property values:
483
390
  ```js
484
391
  class MyComponent extends Symbiote {
485
392
 
@@ -760,7 +667,69 @@ Named external contexts (`CTX/prop`) are different: they are registered globally
760
667
 
761
668
  ---
762
669
 
763
- ## 10. Treating `init$` as a plain object
670
+ ## 10. Using `{{prop}}` binding syntax inside tag definitions
671
+
672
+ `{{prop}}` is a **text node** binding — it only works inside element content. Any binding that targets a tag itself — attributes, DOM properties, event handlers, or CSS custom properties — must use `${{}}` or `bind=""` syntax inside the opening tag.
673
+
674
+ ```html
675
+ <!-- WRONG — {{}} syntax does not work inside tag definitions -->
676
+ <div class="{{APP/theme}}">...</div>
677
+ <img src="{{imageUrl}}">
678
+ <button onclick="{{onAction}}">Go</button>
679
+
680
+ <!-- CORRECT — use ${{}} binding object inside the tag -->
681
+ <div ${{'@class': 'APP/theme'}}>...</div>
682
+ <img ${{'@src': 'imageUrl'}}>
683
+ <button ${{onclick: 'onAction'}}>Go</button>
684
+ <div ${{'style.color': 'colorProp'}}>...</div>
685
+
686
+ <!-- CORRECT — or use the bind attribute string syntax -->
687
+ <div bind="@class: APP/theme">...</div>
688
+ <img bind="@src: imageUrl">
689
+ <button bind="onclick: onAction">Go</button>
690
+
691
+ <!-- CORRECT — {{}} works fine in text node content -->
692
+ <h1>Hello, {{userName}}!</h1>
693
+ <p>Theme: {{APP/theme}}</p>
694
+ ```
695
+
696
+ ---
697
+
698
+ ## 11. Expecting two-way data binding
699
+
700
+ Symbiote.js bindings are **one-way by design** — from state to DOM. There is no `v-model`, `[(ngModel)]`, or `bind:value` equivalent. To react to user input, wire the event handler explicitly and write back to state yourself.
701
+
702
+ ```js
703
+ // WRONG — expecting the binding to also update state on user input
704
+ MyComponent.template = html`
705
+ <input ${{value: 'query'}}>
706
+ `;
707
+
708
+ // CORRECT — handle the input event and write back to state
709
+ class MyComponent extends Symbiote {
710
+ init$ = { query: '' }
711
+ }
712
+
713
+ MyComponent.template = html`
714
+ <input
715
+ ${{value: 'query', oninput: 'onInput'}}
716
+ >
717
+ `;
718
+ ```
719
+ ```js
720
+ class MyComponent extends Symbiote {
721
+ init$ = {
722
+ query: '',
723
+ onInput: (e) => { this.$.query = e.target.value; },
724
+ }
725
+ }
726
+ ```
727
+
728
+ This is intentional — explicit event handling keeps data flow predictable and avoids the hidden side effects that two-way binding can introduce.
729
+
730
+ ---
731
+
732
+ ## 12. Treating `init$` as a plain object
764
733
 
765
734
  `init$` is processed once at connection time to populate the component's reactive context. Mutating it after the fact has no effect. Use `this.$` or `add$()` for runtime changes.
766
735
 
@@ -786,39 +755,46 @@ class MyComponent extends Symbiote {
786
755
 
787
756
  # Context
788
757
 
789
- Usage context is one of the central things in Symbiote.js. Every Symbiote component is able to analyze its environment, read external settings from its actual position in DOM, and provide data to related components. Symbiote.js utilizes the DOM structure as the basic entity for data flow management and interconnection.
758
+ Context is the central concept in Symbiote.js. Rather than passing data through prop chains or maintaining a separate global store, Symbiote uses the DOM structure itself as the data flow graph. Every component can read from and write to multiple data sources - its **context** is the union of all the sources currently accessible to it.
790
759
 
791
- Component context is a sum of all accessible data sources. Each source represents its own type of interaction. Let's have a look at them.
760
+ There are seven context types, each addressed by a token prefix in property keys:
761
+
762
+ | Token | Context Type | Scope |
763
+ |-------|-------------|-------|
764
+ | _(none)_ | [Local](#local-context) | Component instance |
765
+ | `^` | [Pop-up](#pop-up-context) | Nearest ancestor that owns the property |
766
+ | `*` | [Shared](#shared-context) | All components sharing a `ctx` attribute |
767
+ | `name/` | [Named](#named-context) | Any component, anywhere |
768
+ | `--` | [CSS Data](#css-data-context) | Inherited from the CSS cascade |
769
+ | `@` | [Attribute](./attributes.md) | HTML attribute on the element |
770
+ | `+` | [Computed](./properties.md#computed-properties) | Derived, auto-recalculated value |
771
+
772
+ ---
792
773
 
793
774
  ## Local context
794
775
 
795
- Local context properties work the same way as component state in most other libraries:
776
+ Local context is the component's own reactive state, scoped to the instance. It works the same way as component state in other frameworks and is invisible to other components.
777
+
778
+ Define properties in `init$`:
796
779
  ```js
797
780
  class MyComponent extends Symbiote {
798
-
799
781
  init$ = {
800
782
  myProperty: 'some value',
801
783
  }
802
-
803
784
  }
804
785
  ```
805
786
 
806
- Use the `$` proxy to read and write values:
787
+ Use the `$` proxy to read and write values at runtime:
807
788
  ```js
808
789
  class MyComponent extends Symbiote {
809
-
810
790
  init$ = {
811
791
  myProperty: 'some value',
812
792
  }
813
793
 
814
794
  renderCallback() {
815
- // Read:
816
795
  console.log(this.$.myProperty); // > 'some value'
817
-
818
- // Write:
819
796
  this.$.myProperty = 'new value';
820
797
  }
821
-
822
798
  }
823
799
 
824
800
  MyComponent.template = html`
@@ -826,97 +802,143 @@ MyComponent.template = html`
826
802
  `;
827
803
  ```
828
804
 
805
+ For simple components without shared or computed props, you can also declare properties as plain class fields - Symbiote picks them up automatically via class-field fallback:
806
+ ```js
807
+ class MyComponent extends Symbiote {
808
+ count = 0;
809
+ label = 'Hello';
810
+ }
811
+ ```
812
+
813
+ > Use `init$` when you need shared (`*`), computed (`+`), or attribute (`@`) props in the same declaration - those tokens are only recognized inside `init$`.
814
+
815
+ See [Properties →](./properties.md) for the full property API: `add$()`, `sub()`, `set$()`, computed props, and more.
816
+
817
+ ---
818
+
829
819
  ## Named context
830
820
 
831
- Named context is an external abstract data source accessed by its name. It can contain any application data or be used for a dedicated purpose.
821
+ Named context is a global, named data store created independently of any component. Any component can read from or write to it using the `CONTEXT_NAME/property` syntax - regardless of DOM position.
822
+
823
+ Use named context when data must be shared across unrelated parts of the application.
832
824
 
833
- Example using named context as a localization tool:
825
+ **Creating a named context:**
826
+ ```js
827
+ import { PubSub } from '@symbiotejs/symbiote';
828
+
829
+ let appCtx = PubSub.registerCtx({
830
+ theme: 'light',
831
+ user: null,
832
+ }, 'APP');
833
+ ```
834
+
835
+ **Accessing it from a component:**
836
+ ```js
837
+ class MyComponent extends Symbiote {
838
+ init$ = {
839
+ 'APP/theme': 'light', // optional local fallback - used if the named context hasn't published yet
840
+ }
841
+
842
+ renderCallback() {
843
+ console.log(this.$['APP/theme']); // read
844
+ this.$['APP/theme'] = 'dark'; // write - updates the named context for all subscribers
845
+ }
846
+ }
847
+
848
+ MyComponent.template = html`
849
+ <div ${{'@class': 'APP/theme'}}>...</div>
850
+ `;
851
+ ```
852
+
853
+ **Example - localization:**
834
854
  ```js
835
855
  import Symbiote, { html, PubSub } from '@symbiotejs/symbiote';
836
856
 
837
- // Create localization map for English:
838
- let EN = {
857
+ let l10nCtx = PubSub.registerCtx({
839
858
  users: 'Users',
840
859
  comments: 'Comments',
841
860
  likes: 'Likes',
842
- };
843
-
844
- // Create localization context:
845
- let l10nCtx = PubSub.registerCtx(EN, 'L10N');
846
-
847
- // Use localized strings in templates:
848
- class MyComponent extends Symbiote { ... }
861
+ }, 'L10N');
849
862
 
850
863
  MyComponent.template = html`
851
864
  <div>{{L10N/users}} - {{numberOfUsers}}</div>
852
865
  <div>{{L10N/comments}} - {{numberOfComments}}</div>
853
866
  <div>{{L10N/likes}} - {{numberOfLikes}}</div>
854
867
  `;
855
- ```
856
-
857
- Use `/` token with the context key to access properties: `L10N/users`.
858
868
 
859
- Switching language:
860
- ```js
861
- let ES = {
869
+ // Switch language at any time - all subscribed components update instantly:
870
+ l10nCtx.multiPub({
862
871
  users: 'Usuarios',
863
872
  comments: 'Comentarios',
864
873
  likes: 'Gustos',
865
- };
866
-
867
- l10nCtx.multiPub(ES);
874
+ });
868
875
  ```
869
876
 
870
- Read and modify named context properties with the `$` proxy:
877
+ You can also read and modify named context directly from any component using the `$` proxy:
871
878
  ```js
872
879
  class MyComponent extends Symbiote {
873
880
  renderCallback() {
874
- // Read:
875
881
  console.log(this.$['L10N/users']);
876
-
877
- // Modify:
878
882
  this.$['L10N/users'] = 'ユーザー';
879
883
  }
880
884
  }
881
885
  ```
882
886
 
883
- More information about `PubSub` in the [PubSub section](./pubsub.md).
887
+ More information about `PubSub` in the [PubSub ](./pubsub.md) section.
888
+
889
+ ---
884
890
 
885
891
  ## Pop-up context
886
892
 
887
- Pop-up binding helps control component interactions based on their DOM position and hierarchy. It works similarly to the CSS cascade model.
893
+ Pop-up context lets a child component reach a property defined by an ancestor, without the ancestor needing to pass it down explicitly. Symbiote walks up the DOM tree until it finds a component that has the requested property in its data context.
894
+
895
+ Use the `^` token to reference a pop-up property - in both text bindings and event handlers:
896
+ ```html
897
+ <!-- Text binding to ancestor property: -->
898
+ <div>{{^parentTitle}}</div>
899
+
900
+ <!-- Handler binding to ancestor method: -->
901
+ <button ${{onclick: '^onButtonClicked'}}>Click me!</button>
902
+ ```
888
903
 
889
- Use the `^` token to reference a higher-level property:
890
904
  ```js
891
- class MyComponent extends Symbiote {}
905
+ class MyButton extends Symbiote {}
892
906
 
893
- MyComponent.template = html`
907
+ MyButton.template = html`
908
+ <span>{{^parentTitle}}</span>
894
909
  <button ${{onclick: '^onButtonClicked'}}>Click me!</button>
895
910
  `;
896
911
  ```
897
- Symbiote walks up the DOM tree until it finds the component with `onButtonClicked` registered in its data context.
912
+
913
+ Symbiote walks up the DOM tree until it finds a component with the requested property registered in its context:
914
+ ```js
915
+ class MyEditor extends Symbiote {
916
+ init$ = {
917
+ onButtonClicked: () => console.log('clicked'),
918
+ editorTitle: 'My Editor',
919
+ }
920
+ }
921
+ ```
898
922
 
899
923
  > [!IMPORTANT]
900
- > The `^` lookup only checks the parent's **data context** (properties registered via `init$` or `add$()`). Class property fallbacks are **not** resolved during this walk. Always define `^`-targeted properties in the parent's `init$`:
924
+ > Pop-up lookup only searches the **data context** (properties registered via `init$` or `add$()`). Plain class properties are not resolved this way. Always declare `^`-targeted properties in the parent's `init$`:
901
925
  > ```js
902
926
  > class ParentComponent extends Symbiote {
903
927
  > init$ = {
904
- > onButtonClicked: () => { console.log('clicked'); },
928
+ > onButtonClicked: () => console.log('clicked'),
905
929
  > parentTitle: 'Hello',
906
930
  > }
907
931
  > }
908
932
  > ```
909
933
 
910
- This is useful for composition, customization, and responsibility splitting:
934
+ Pop-up context is useful for composition - the same child component adapts to whichever ancestor provides the expected behavior:
911
935
  ```js
912
936
  html`
913
937
  <my-text-editor>
914
938
  <complete-toolbar></complete-toolbar>
915
939
  </my-text-editor>
916
940
  `;
917
- ```
918
- Or:
919
- ```js
941
+ // or:
920
942
  html`
921
943
  <my-text-editor>
922
944
  <simplified-toolbar></simplified-toolbar>
@@ -925,19 +947,21 @@ html`
925
947
  `;
926
948
  ```
927
949
 
928
- > Like in CSS, pop-up properties have no collision guard use additional prefixes in uncontrolled environments.
950
+ > Like the CSS cascade, pop-up context has no collision guard. Use additional prefixes (e.g. `myApp_onSave`) in environments you don't fully control.
951
+
952
+ ---
929
953
 
930
954
  ## Shared context
931
955
 
932
- Shared context works similarly to native HTML radio inputs set the `name` attribute and different inputs connect into one workflow.
956
+ Shared context is inspired by native HTML `name` attributes - the same way `<input name="group">` connects radio buttons into one workflow, the `ctx` attribute connects Symbiote components into a shared data context. Components with the same `ctx` name access a common reactive store with no intermediary component required.
933
957
 
934
- In 3.x, an explicit context name is **required** for shared properties. Set it using the `ctx` HTML attribute:
958
+ **Assign a context name via the `ctx` HTML attribute:**
935
959
  ```html
936
960
  <upload-btn ctx="gallery"></upload-btn>
937
961
  <file-list ctx="gallery"></file-list>
938
962
  ```
939
963
 
940
- Or using the `--ctx` CSS custom property:
964
+ **Or via the `--ctx` CSS custom property**, which cascades like any CSS variable:
941
965
  ```css
942
966
  .gallery-section {
943
967
  --ctx: gallery;
@@ -951,49 +975,64 @@ Or using the `--ctx` CSS custom property:
951
975
  ```
952
976
 
953
977
  The CSS approach is useful when:
954
- - You want **layout-driven grouping** components inherit the context from their visual container rather than repeating the attribute on each one
955
- - You need to **reassign context at different DOM levels** just like any CSS custom property, `--ctx` cascades and can be overridden in nested selectors
956
- - You work in a **framework-agnostic setup** CSS can be managed separately from markup, so context assignment doesn't depend on template engine or host framework
978
+ - You want **layout-driven grouping** - components inherit context from their visual container rather than repeating the attribute on each one
979
+ - You need to **override context at different DOM levels** - just like any CSS custom property, `--ctx` cascades and can be reassigned in nested selectors
980
+ - You work in a **framework-agnostic setup** - CSS context assignment is independent of the host template engine
957
981
 
958
- Then use the `*` token to define shared properties:
982
+ **Define shared properties with the `*` token:**
959
983
  ```js
960
984
  class UploadBtn extends Symbiote {
961
985
  init$ = { '*files': [] }
962
986
 
963
- onUpload() {
987
+ onUpload(newFile) {
964
988
  this.$['*files'] = [...this.$['*files'], newFile];
965
989
  }
966
990
  }
967
991
 
968
992
  class FileList extends Symbiote {
969
- init$ = { '*files': [] } // same shared prop first-registered value wins
993
+ init$ = { '*files': [] } // same shared prop - first-registered value wins
970
994
  }
971
995
  ```
972
996
 
973
- Both components access the same `*files` state no parent component, no prop drilling, no global store.
997
+ Both components read and write the same `*files` store. When one updates it, the other reacts automatically - no parent component, no prop drilling, no global store.
974
998
 
975
999
  ### Context name resolution
976
1000
 
977
- The context name is resolved in this order (first match wins):
1001
+ The `ctx` name is resolved in this order (first match wins):
978
1002
 
979
- 1. `ctx="name"` HTML attribute
980
- 2. `--ctx` CSS custom property (inherited from ancestors)
1003
+ 1. `ctx="name"` HTML attribute on the element
1004
+ 2. `--ctx` CSS custom property inherited from ancestors
981
1005
 
982
- > **IMPORTANT**: In 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx` CSS variable. Without one, the shared context is not created, `*` props have no effect, and dev mode will warn about it.
1006
+ > **IMPORTANT**: In Symbiote 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx` variable. Without one, no shared context is created, `*` props have no effect, and dev mode will warn (W6).
1007
+
1008
+ ---
983
1009
 
984
1010
  ## CSS Data context
985
1011
 
986
- Symbiote components can initiate their properties from CSS custom property values:
987
- ```css
988
- :root {
989
- --header: 'CSS Data';
990
- --text: 'Hello!';
1012
+ Symbiote components can initialize their properties from CSS custom property values, enabling CSS-driven configuration: theme tokens, layout parameters, or localized strings - all settable from a stylesheet without touching JavaScript.
1013
+
1014
+ Use `cssInit$` to explicitly declare CSS-initialized properties with fallback values:
1015
+ ```js
1016
+ class MyWidget extends Symbiote {
1017
+ cssInit$ = {
1018
+ '--columns': 1,
1019
+ '--label': '',
1020
+ }
991
1021
  }
1022
+
1023
+ MyWidget.template = html`
1024
+ <span>{{--label}}</span>
1025
+ `;
992
1026
  ```
993
1027
 
994
- > CSS custom property values should be valid JSON values, parseable with `JSON.parse()`. Use numbers for boolean flags (`0`/`1`).
1028
+ ```css
1029
+ my-widget {
1030
+ --columns: 3;
1031
+ --label: 'Click me';
1032
+ }
1033
+ ```
995
1034
 
996
- Use CSS values in templates directly:
1035
+ You can also use `--` bindings directly in templates without `cssInit$`:
997
1036
  ```js
998
1037
  class TestApp extends Symbiote {}
999
1038
 
@@ -1002,10 +1041,79 @@ TestApp.template = html`
1002
1041
  <div>{{--text}}</div>
1003
1042
  `;
1004
1043
  ```
1044
+ ```css
1045
+ :root {
1046
+ --header: 'CSS Data';
1047
+ --text: 'Hello!';
1048
+ }
1049
+ ```
1005
1050
 
1006
- > CSS custom properties are used for value initialization only. After that, they act like normal local context properties.
1051
+ > CSS custom property values must be valid JSON - use quoted strings (`'text'`), numbers, and `0`/`1` for booleans.
1052
+
1053
+ > CSS properties are used for **initialization only**. After the component mounts, they act as normal local context properties and no longer track CSS changes. Call `this.updateCssData()` to re-read them after runtime CSS updates.
1054
+
1055
+ Full details - `updateCssData()`, `dropCssDataCache()`, `ResizeObserver` patterns, and SSR caveats - in the [CSS Data →](./css-data.md) section.
1056
+
1057
+ ---
1058
+
1059
+ ## Choosing the right context type
1060
+
1061
+ | Situation | Use |
1062
+ |-----------|-----|
1063
+ | Component-local reactive state | Local - no token |
1064
+ | Behavior or data passed from an ancestor | Pop-up - `^` |
1065
+ | Sibling components sharing a workflow state | Shared - `*` |
1066
+ | App-wide data, accessed from anywhere in the tree | Named - `/` |
1067
+ | Component configured via CSS / design tokens | CSS Data - `--` |
1068
+ | Reacting to HTML attribute values | Attribute - `@` |
1069
+ | Values derived from other properties | Computed - `+` |
1070
+
1071
+ ---
1072
+
1073
+ ## All context types in one component
1074
+
1075
+ A concise example showing local, attribute, named, shared, and pop-up contexts working together:
1076
+
1077
+ ```js
1078
+ import Symbiote, { html } from '@symbiotejs/symbiote';
1079
+
1080
+ class MyApp extends Symbiote {
1081
+ init$ = {
1082
+ localCtxProp: 'LOCAL',
1083
+ '@attr-test': '', // bound to HTML attribute
1084
+ 'APP/namedProp': 'NAMED', // named context
1085
+ '*sharedProp': 'SHARED', // shared context (requires ctx="..." in HTML)
1086
+ }
1007
1087
 
1008
- More details in the [CSS Data](./css-data.md) section.
1088
+ onUpdate() {
1089
+ let suffix = ' updated';
1090
+ this.$.localCtxProp += suffix;
1091
+ this.$['APP/namedProp'] += suffix;
1092
+ this.$['*sharedProp'] += suffix;
1093
+ }
1094
+ }
1095
+
1096
+ MyApp.template = html`
1097
+ <div>local: {{localCtxProp}}</div>
1098
+ <div>attribute: {{@attr-test}}</div>
1099
+ <div>named: {{APP/namedProp}}</div>
1100
+ <div>shared: {{*sharedProp}}</div>
1101
+ <button ${{onclick: 'onUpdate'}}>Update</button>
1102
+ <inner-el></inner-el> <!-- reads ^localCtxProp via pop-up -->
1103
+ `;
1104
+
1105
+ MyApp.reg('my-app');
1106
+
1107
+ class InnerEl extends Symbiote {}
1108
+ InnerEl.template = html`<h2>pop-up: {{^localCtxProp}}</h2>`;
1109
+ InnerEl.reg('inner-el');
1110
+ ```
1111
+
1112
+ ```html
1113
+ <my-app attr-test="HTML value" ctx="my-ctx"></my-app>
1114
+ ```
1115
+
1116
+ ---
1009
1117
 
1010
1118
  ## Property token summary
1011
1119
 
@@ -1016,8 +1124,8 @@ More details in the [CSS Data](./css-data.md) section.
1016
1124
  | `*` | Shared | `*sharedProp` |
1017
1125
  | `/` | Named | `APP/myProp` |
1018
1126
  | `--` | CSS Data | `--my-css-var` |
1019
- | `@` | Attribute | `@my-attribute` |
1020
- | `+` | Computed | `+computedProp` |
1127
+ | `@` | Attribute - see [Attributes →](./attributes.md) | `@my-attribute` |
1128
+ | `+` | Computed - see [Properties →](./properties.md#computed-properties) | `+computedProp` |
1021
1129
 
1022
1130
  ---
1023
1131
 
@@ -5149,15 +5257,9 @@ Use Symbiote dev/runtime diagnostics for template binding correctness, because t
5149
5257
 
5150
5258
  ---
5151
5259
 
5152
- # WebMCP Experimental
5153
-
5154
- Symbiote.js can expose the current browser UI state as native WebMCP tools. This feature is experimental and intended for testing with browser builds that expose native WebMCP APIs, such as Chrome Canary 150.
5260
+ # WebMCP
5155
5261
 
5156
- Install the experimental npm release with the `webmcp` tag:
5157
-
5158
- ```shell
5159
- npm i @symbiotejs/symbiote@webmcp
5160
- ```
5262
+ Symbiote.js can expose the current browser UI state as native WebMCP tools.
5161
5263
 
5162
5264
  ## Activation
5163
5265
 
@@ -5165,7 +5267,7 @@ WebMCP is optional. Import the extension before WebMCP-enabled components render
5165
5267
 
5166
5268
  ```js
5167
5269
  import Symbiote, { html } from '@symbiotejs/symbiote';
5168
- import { ToolDescriptor } from '@symbiotejs/symbiote/webmcp';
5270
+ import '@symbiotejs/symbiote/webmcp';
5169
5271
  ```
5170
5272
 
5171
5273
  ## Automatic Tools
@@ -5215,6 +5317,8 @@ removeItem_in_task-list_task-item_alpha
5215
5317
  Use `ToolDescriptor` for descriptions, input schemas, execution logic, and dynamic visibility:
5216
5318
 
5217
5319
  ```js
5320
+ import { ToolDescriptor } from '@symbiotejs/symbiote/webmcp';
5321
+
5218
5322
  class WebmcpPanel extends Symbiote {
5219
5323
  componentDescription = async () => {
5220
5324
  return 'Visible order editor with selected item state.';