slexkit 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to SlexKit.
4
4
 
5
+ ## v0.3.2 - Host CSS isolation and repeated layout hardening
6
+
7
+ ### Changed
8
+ - `$for` rendering now uses comment anchors and direct child insertion instead of a wrapper element that depended on `display: contents`.
9
+ - Site-only mobile navigation CSS moved out of the runtime base stylesheet and into the documentation site shell.
10
+ - Component accessors now share one reactive effect across subscribers instead of creating duplicate subscriber fan-out work.
11
+
12
+ ### Fixed
13
+ - Obsidian and other Markdown hosts no longer need to rewrite `$for` wrapper CSS to avoid `display: contents`, preserving grid and row layouts for repeated items.
14
+ - Published runtime base CSS no longer leaks `#mobileNav` or `body[data-mobile-nav-open]` selectors into host pages.
15
+ - Custom renderers that return no element no longer leave invalid `$for` slots behind during diffing or cleanup.
16
+
5
17
  ## v0.3.1 - Host stability and control rendering hardening
6
18
 
7
19
  ### Added
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="README.zh-CN.md">简体中文</a>
16
16
  </p>
17
17
  <p>
18
- <img alt="version" src="https://img.shields.io/badge/version-0.3.1-18181b">
18
+ <img alt="version" src="https://img.shields.io/badge/version-0.3.2-18181b">
19
19
  <img alt="script" src="https://img.shields.io/badge/Slex-v0.1-18181b">
20
20
  <img alt="TypeScript" src="https://img.shields.io/badge/runtime-TypeScript-3178c6">
21
21
  <img alt="Svelte 5" src="https://img.shields.io/badge/components-Svelte_5-ff3e00">
package/README.zh-CN.md CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="README.md">English</a>
16
16
  </p>
17
17
  <p>
18
- <img alt="version" src="https://img.shields.io/badge/version-0.3.1-18181b">
18
+ <img alt="version" src="https://img.shields.io/badge/version-0.3.2-18181b">
19
19
  <img alt="script" src="https://img.shields.io/badge/Slex-v0.1-18181b">
20
20
  <img alt="TypeScript" src="https://img.shields.io/badge/runtime-TypeScript-3178c6">
21
21
  <img alt="Svelte 5" src="https://img.shields.io/badge/components-Svelte_5-ff3e00">
@@ -1,6 +1,6 @@
1
1
  # SlexKit Full LLM Documentation
2
2
 
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
 
5
5
  This file concatenates SlexKit's canonical English Markdown docs. `slex` fences are preserved exactly.
6
6
 
@@ -27,7 +27,7 @@ Source: README.md
27
27
  <a href="README.zh-CN.md">简体中文</a>
28
28
  </p>
29
29
  <p>
30
- <img alt="version" src="https://img.shields.io/badge/version-0.3.1-18181b">
30
+ <img alt="version" src="https://img.shields.io/badge/version-0.3.2-18181b">
31
31
  <img alt="script" src="https://img.shields.io/badge/Slex-v0.1-18181b">
32
32
  <img alt="TypeScript" src="https://img.shields.io/badge/runtime-TypeScript-3178c6">
33
33
  <img alt="Svelte 5" src="https://img.shields.io/badge/components-Svelte_5-ff3e00">
@@ -7574,6 +7574,18 @@ slexkitRenderMode: component
7574
7574
 
7575
7575
  All notable changes to SlexKit.
7576
7576
 
7577
+ ## v0.3.2 - Host CSS isolation and repeated layout hardening
7578
+
7579
+ ### Changed
7580
+ - `$for` rendering now uses comment anchors and direct child insertion instead of a wrapper element that depended on `display: contents`.
7581
+ - Site-only mobile navigation CSS moved out of the runtime base stylesheet and into the documentation site shell.
7582
+ - Component accessors now share one reactive effect across subscribers instead of creating duplicate subscriber fan-out work.
7583
+
7584
+ ### Fixed
7585
+ - Obsidian and other Markdown hosts no longer need to rewrite `$for` wrapper CSS to avoid `display: contents`, preserving grid and row layouts for repeated items.
7586
+ - Published runtime base CSS no longer leaks `#mobileNav` or `body[data-mobile-nav-open]` selectors into host pages.
7587
+ - Custom renderers that return no element no longer leave invalid `$for` slots behind during diffing or cleanup.
7588
+
7577
7589
  ## v0.3.1 - Host stability and control rendering hardening
7578
7590
 
7579
7591
  ### Added
package/dist/ai/llms.txt CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  SlexKit raw docs are Markdown (`.md`) with explicit `slex` fences. The interactive layer is the fenced `slex` source inside each Markdown page.
14
14
 
15
- Version: 0.3.1
15
+ Version: 0.3.2
16
16
 
17
17
  ## Table of Contents
18
18
 
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "slexkit-ai-docs",
3
3
  "packageName": "slexkit",
4
- "version": "0.3.1",
5
- "generatedAt": "2026-06-20T08:01:21.373Z",
4
+ "version": "0.3.2",
5
+ "generatedAt": "2026-06-20T09:40:31.853Z",
6
6
  "docs": {
7
7
  "llms.txt": {
8
8
  "path": "/llms.txt",
9
9
  "title": "llms.txt",
10
10
  "summary": "- [Full documentation](/llms-full.txt): all canonical English docs pages in one text file.",
11
- "hash": "1445b5b6"
11
+ "hash": "b492e34b"
12
12
  },
13
13
  "llms-full.txt": {
14
14
  "path": "/llms-full.txt",
15
15
  "title": "llms-full.txt",
16
- "summary": "Version: 0.3.1",
17
- "hash": "000eb11f"
16
+ "summary": "Version: 0.3.2",
17
+ "hash": "34989439"
18
18
  },
19
19
  "llms-components.txt": {
20
20
  "path": "/llms-components.txt",
@@ -56,8 +56,8 @@
56
56
  "href": "/",
57
57
  "rawHref": "/README.md",
58
58
  "sourcePath": "README.md",
59
- "body": "<div align=\"center\">\n <p>\n <img src=\"site/assets/logo.svg\" alt=\"SlexKit\" width=\"84\" height=\"84\" />\n </p>\n <h1>SlexKit</h1>\n <p><strong>Streaming Live EXpressions Kit</strong></p>\n <p>\n \"Docs as tools, tools as docs.\" Give Markdown interactive power and make every AI output come alive.\n </p>\n <p>\n <a href=\"site/content/guides/intro/en-US.md\">Documentation</a> ·\n <a href=\"site/content/components/accordion/en-US.md\">Components</a> ·\n <a href=\"site/content/reference/spec/en-US.md\">Specification</a> ·\n <a href=\"site/content/guides/ai-agents/en-US.md\">AI / Agents</a> ·\n <a href=\"README.zh-CN.md\">简体中文</a>\n </p>\n <p>\n <img alt=\"version\" src=\"https://img.shields.io/badge/version-0.3.1-18181b\">\n <img alt=\"script\" src=\"https://img.shields.io/badge/Slex-v0.1-18181b\">\n <img alt=\"TypeScript\" src=\"https://img.shields.io/badge/runtime-TypeScript-3178c6\">\n <img alt=\"Svelte 5\" src=\"https://img.shields.io/badge/components-Svelte_5-ff3e00\">\n <img alt=\"license\" src=\"https://img.shields.io/badge/license-MIT-16a34a\">\n </p>\n</div>\n\n## Live interface blocks inside Markdown\n\n**SlexKit** turns explicit `slex` Markdown fences into live, stateful UI blocks. A Slex source is just a JavaScript object literal: `g` holds state and logic, `layout` describes the component tree, and the browser runtime renders the result in place.\n\nIt is built for chat messages, documents, agent panels, tool results, and AI-authored dashboards. It is not a full application framework.\n\n## Installation\n\n> Just want to use SlexKit in Obsidian? Open **Settings -> Community plugins**, search for **SlexKit**, then install and enable it. The npm package below is for developers integrating SlexKit into web apps, Markdown renderers, Streamdown, or custom hosts.\n\n```sh\nnpm install slexkit\n```\n\n```ts\nimport { mount } from \"slexkit\";\nimport \"slexkit/style.css\";\n```\n\n## Usage\n\n```html\n<div id=\"app\"></div>\n\n<script type=\"module\">\n import { mount } from \"slexkit\";\n import \"slexkit/style.css\";\n\n mount(\n {\n slex: \"0.1\",\n namespace: \"hello\",\n g: { name: \"World\", count: 0 },\n layout: {\n \"card:greeting\": {\n title: \"Greeting\",\n \"text:message\": {\n \"$text\": \"'Hello, ' + g.name + '! Count: ' + g.count\"\n },\n \"button:add\": {\n label: \"+1\",\n onclick: \"g.count++\"\n }\n }\n }\n },\n document.getElementById(\"app\")\n );\n</script>\n```\n\n## Markdown Native\n\nSlexKit-capable hosts process explicit `slex` fences only. Plain `js`, `json`, and unlabeled code blocks stay inert.\n\n````md\n```slex\n{\n slex: \"0.1\",\n namespace: \"status\",\n g: { done: 3, total: 4 },\n layout: {\n \"badge:state\": { label: \"Ready\", tone: \"success\" },\n \"text:summary\": { \"$text\": \"g.done + '/' + g.total + ' complete'\" }\n }\n}\n```\n\n**Status:** Ready. 3/4 complete.\n````\n\nMarkdown platforms without SlexKit support show the fallback text. Hosts with SlexKit render the interactive UI.\n\n## What You Get\n\n- **Zero-build Slex source**: object literals with no imports, scaffolding, or component bundling in the generated output.\n- **Reactive `g` / `layout` model**: centralized state and logic with declarative component trees.\n- **Expression pipes**: `$` read expressions for dynamic props and `on*` write expressions for events.\n- **Directives**: `$if` and `$for` for conditional rendering and keyed list reconciliation.\n- **Official Svelte components**: 40+ layout, input, content, display, disclosure, feedback, and tooling components.\n- **Extensible registry**: custom component types, Svelte renderers, and component state modes.\n- **Trusted and secure runtimes**: host-realm rendering for trusted content, sandbox iframe rendering for untrusted source.\n- **ToolHost**: confirm, choose, and fill-form templates for structured AI tool-call UX.\n- **AI-friendly docs surface**: `llms.txt`, skills, and the `@slexkit/mcp` read-only MCP server.\n\n## Packages\n\n| Package | Install | Contents |\n| --- | --- | --- |\n| `slexkit` | `npm install slexkit` | Runtime, Svelte components, ToolHost, styles |\n| `@slexkit/runtime` | `npm install slexkit @slexkit/runtime` | Component-free runtime wrapper |\n| `@slexkit/components-svelte` | `npm install slexkit @slexkit/runtime @slexkit/components-svelte` | Svelte component registration |\n| `@slexkit/theme-shadcn` | `npm install @slexkit/theme-shadcn` | CSS theme tokens |\n| `@slexkit/streamdown` | `npm install slexkit @slexkit/theme-shadcn @slexkit/streamdown streamdown react react-dom` | React / Streamdown Markdown renderer |\n| `@slexkit/mcp` | `npx -y @slexkit/mcp` | Read-only MCP server for docs, examples, and source validation |\n\nSee [Package Boundaries](site/content/reference/packages/en-US.md) for details.\n\n## Integrations\n\n| Host | Path |\n| --- | --- |\n| Browser DOM | `mount()`, `ingest()`, `boot()`, `disposeNamespace()` |\n| Markdown renderers | `createSlexKitMarkdownRuntimeHost()` |\n| React / Streamdown | `@slexkit/streamdown` |\n| Obsidian | Install **SlexKit** from Community Plugins; release repo: <https://github.com/slexkit/obsidian-slexkit> |\n| AI agents | `@slexkit/mcp`, `llms.txt`, SlexKit skill docs |\n| Custom components | `register()`, `registerSvelteComponent()`, `registerSubset()` |\n\n## Security Runtime\n\nTrusted mode runs inside the host realm and is intended for application-authored or reviewed source. Secure mode isolates untrusted Slex source in a sandbox iframe with an opaque origin, nonce-based CSP, locked-down globals, host-mediated network access, and a heartbeat watchdog.\n\nRead the [Security Runtime](site/content/reference/security/en-US.md) docs before rendering unreviewed user or model output.\n\n## Documentation\n\n| Document | Topic |\n| --- | --- |\n| [Getting Started](site/content/guides/quick-start/en-US.md) | Install and render a first Markdown-friendly Slex source |\n| [Integration](site/content/guides/integration/en-US.md) | Streamdown, Obsidian, and custom host paths |\n| [Runtime model](site/content/reference/runtime/en-US.md) | Mounting, updates, namespace store, lifecycle |\n| [Slex usage reference](site/content/reference/usage/en-US.md) | Source structure, directives, expressions, events, custom components |\n| [Security runtime](site/content/reference/security/en-US.md) | Threat model, sandbox iframe, policy, postMessage bridge |\n| [Slex Specification](site/content/reference/spec/en-US.md) | Protocol v0.1, types, merge rules, lifecycle hooks |\n| [ToolHost](site/content/reference/toolhost/en-US.md) | Tool-call rendering and custom templates |\n| [Icon system](site/content/reference/icons/en-US.md) | Phosphor icons, custom registration, Iconify fallback |\n| [AI / Agents](site/content/guides/ai-agents/en-US.md) | `llms.txt`, MCP server, skills, and authoring rules |\n| [Changelog](CHANGELOG.md) | Release notes and notable changes |\n\n## Version Information\n\n```ts\nimport { SLEXKIT_VERSION, SLEX_PROTOCOL_VERSION, getSlexKitInfo } from \"slexkit\";\n```\n\nThe npm package version, component implementation version, and Slex protocol version are exposed separately. The current public protocol is `v0.1`.\n\n## License\n\nMIT",
60
- "hash": "117cabab"
59
+ "body": "<div align=\"center\">\n <p>\n <img src=\"site/assets/logo.svg\" alt=\"SlexKit\" width=\"84\" height=\"84\" />\n </p>\n <h1>SlexKit</h1>\n <p><strong>Streaming Live EXpressions Kit</strong></p>\n <p>\n \"Docs as tools, tools as docs.\" Give Markdown interactive power and make every AI output come alive.\n </p>\n <p>\n <a href=\"site/content/guides/intro/en-US.md\">Documentation</a> ·\n <a href=\"site/content/components/accordion/en-US.md\">Components</a> ·\n <a href=\"site/content/reference/spec/en-US.md\">Specification</a> ·\n <a href=\"site/content/guides/ai-agents/en-US.md\">AI / Agents</a> ·\n <a href=\"README.zh-CN.md\">简体中文</a>\n </p>\n <p>\n <img alt=\"version\" src=\"https://img.shields.io/badge/version-0.3.2-18181b\">\n <img alt=\"script\" src=\"https://img.shields.io/badge/Slex-v0.1-18181b\">\n <img alt=\"TypeScript\" src=\"https://img.shields.io/badge/runtime-TypeScript-3178c6\">\n <img alt=\"Svelte 5\" src=\"https://img.shields.io/badge/components-Svelte_5-ff3e00\">\n <img alt=\"license\" src=\"https://img.shields.io/badge/license-MIT-16a34a\">\n </p>\n</div>\n\n## Live interface blocks inside Markdown\n\n**SlexKit** turns explicit `slex` Markdown fences into live, stateful UI blocks. A Slex source is just a JavaScript object literal: `g` holds state and logic, `layout` describes the component tree, and the browser runtime renders the result in place.\n\nIt is built for chat messages, documents, agent panels, tool results, and AI-authored dashboards. It is not a full application framework.\n\n## Installation\n\n> Just want to use SlexKit in Obsidian? Open **Settings -> Community plugins**, search for **SlexKit**, then install and enable it. The npm package below is for developers integrating SlexKit into web apps, Markdown renderers, Streamdown, or custom hosts.\n\n```sh\nnpm install slexkit\n```\n\n```ts\nimport { mount } from \"slexkit\";\nimport \"slexkit/style.css\";\n```\n\n## Usage\n\n```html\n<div id=\"app\"></div>\n\n<script type=\"module\">\n import { mount } from \"slexkit\";\n import \"slexkit/style.css\";\n\n mount(\n {\n slex: \"0.1\",\n namespace: \"hello\",\n g: { name: \"World\", count: 0 },\n layout: {\n \"card:greeting\": {\n title: \"Greeting\",\n \"text:message\": {\n \"$text\": \"'Hello, ' + g.name + '! Count: ' + g.count\"\n },\n \"button:add\": {\n label: \"+1\",\n onclick: \"g.count++\"\n }\n }\n }\n },\n document.getElementById(\"app\")\n );\n</script>\n```\n\n## Markdown Native\n\nSlexKit-capable hosts process explicit `slex` fences only. Plain `js`, `json`, and unlabeled code blocks stay inert.\n\n````md\n```slex\n{\n slex: \"0.1\",\n namespace: \"status\",\n g: { done: 3, total: 4 },\n layout: {\n \"badge:state\": { label: \"Ready\", tone: \"success\" },\n \"text:summary\": { \"$text\": \"g.done + '/' + g.total + ' complete'\" }\n }\n}\n```\n\n**Status:** Ready. 3/4 complete.\n````\n\nMarkdown platforms without SlexKit support show the fallback text. Hosts with SlexKit render the interactive UI.\n\n## What You Get\n\n- **Zero-build Slex source**: object literals with no imports, scaffolding, or component bundling in the generated output.\n- **Reactive `g` / `layout` model**: centralized state and logic with declarative component trees.\n- **Expression pipes**: `$` read expressions for dynamic props and `on*` write expressions for events.\n- **Directives**: `$if` and `$for` for conditional rendering and keyed list reconciliation.\n- **Official Svelte components**: 40+ layout, input, content, display, disclosure, feedback, and tooling components.\n- **Extensible registry**: custom component types, Svelte renderers, and component state modes.\n- **Trusted and secure runtimes**: host-realm rendering for trusted content, sandbox iframe rendering for untrusted source.\n- **ToolHost**: confirm, choose, and fill-form templates for structured AI tool-call UX.\n- **AI-friendly docs surface**: `llms.txt`, skills, and the `@slexkit/mcp` read-only MCP server.\n\n## Packages\n\n| Package | Install | Contents |\n| --- | --- | --- |\n| `slexkit` | `npm install slexkit` | Runtime, Svelte components, ToolHost, styles |\n| `@slexkit/runtime` | `npm install slexkit @slexkit/runtime` | Component-free runtime wrapper |\n| `@slexkit/components-svelte` | `npm install slexkit @slexkit/runtime @slexkit/components-svelte` | Svelte component registration |\n| `@slexkit/theme-shadcn` | `npm install @slexkit/theme-shadcn` | CSS theme tokens |\n| `@slexkit/streamdown` | `npm install slexkit @slexkit/theme-shadcn @slexkit/streamdown streamdown react react-dom` | React / Streamdown Markdown renderer |\n| `@slexkit/mcp` | `npx -y @slexkit/mcp` | Read-only MCP server for docs, examples, and source validation |\n\nSee [Package Boundaries](site/content/reference/packages/en-US.md) for details.\n\n## Integrations\n\n| Host | Path |\n| --- | --- |\n| Browser DOM | `mount()`, `ingest()`, `boot()`, `disposeNamespace()` |\n| Markdown renderers | `createSlexKitMarkdownRuntimeHost()` |\n| React / Streamdown | `@slexkit/streamdown` |\n| Obsidian | Install **SlexKit** from Community Plugins; release repo: <https://github.com/slexkit/obsidian-slexkit> |\n| AI agents | `@slexkit/mcp`, `llms.txt`, SlexKit skill docs |\n| Custom components | `register()`, `registerSvelteComponent()`, `registerSubset()` |\n\n## Security Runtime\n\nTrusted mode runs inside the host realm and is intended for application-authored or reviewed source. Secure mode isolates untrusted Slex source in a sandbox iframe with an opaque origin, nonce-based CSP, locked-down globals, host-mediated network access, and a heartbeat watchdog.\n\nRead the [Security Runtime](site/content/reference/security/en-US.md) docs before rendering unreviewed user or model output.\n\n## Documentation\n\n| Document | Topic |\n| --- | --- |\n| [Getting Started](site/content/guides/quick-start/en-US.md) | Install and render a first Markdown-friendly Slex source |\n| [Integration](site/content/guides/integration/en-US.md) | Streamdown, Obsidian, and custom host paths |\n| [Runtime model](site/content/reference/runtime/en-US.md) | Mounting, updates, namespace store, lifecycle |\n| [Slex usage reference](site/content/reference/usage/en-US.md) | Source structure, directives, expressions, events, custom components |\n| [Security runtime](site/content/reference/security/en-US.md) | Threat model, sandbox iframe, policy, postMessage bridge |\n| [Slex Specification](site/content/reference/spec/en-US.md) | Protocol v0.1, types, merge rules, lifecycle hooks |\n| [ToolHost](site/content/reference/toolhost/en-US.md) | Tool-call rendering and custom templates |\n| [Icon system](site/content/reference/icons/en-US.md) | Phosphor icons, custom registration, Iconify fallback |\n| [AI / Agents](site/content/guides/ai-agents/en-US.md) | `llms.txt`, MCP server, skills, and authoring rules |\n| [Changelog](CHANGELOG.md) | Release notes and notable changes |\n\n## Version Information\n\n```ts\nimport { SLEXKIT_VERSION, SLEX_PROTOCOL_VERSION, getSlexKitInfo } from \"slexkit\";\n```\n\nThe npm package version, component implementation version, and Slex protocol version are exposed separately. The current public protocol is `v0.1`.\n\n## License\n\nMIT",
60
+ "hash": "4c55b5dc"
61
61
  },
62
62
  {
63
63
  "id": "guides/intro",
@@ -738,8 +738,8 @@
738
738
  "href": "/docs/releases/changelog",
739
739
  "rawHref": "/docs/releases/changelog.md",
740
740
  "sourcePath": "site/content/releases/changelog/en-US.md",
741
- "body": "---\ntitle: Changelog\ncategory: Releases\nstatus: ready\norder: 10\nsummary: \"Release notes and notable changes for SlexKit.\"\nslexkitRenderMode: component\n---\n\n# Changelog\n\nAll notable changes to SlexKit.\n\n## v0.3.1 - Host stability and control rendering hardening\n\n### Added\n- Runtime style safety tests that block broad `:has()` selectors, `clip-path`, and slider track regressions in shipped CSS.\n- Regression coverage for disabled Switch, Checkbox, and Radio state attributes.\n\n### Changed\n- CI now installs dependencies with `bun install --frozen-lockfile` and runs lint before tests.\n- Disabled Switch, Checkbox, and Radio styling now uses explicit `data-disabled` attributes instead of broad relational selectors.\n- Select and sr-only helper styles avoid `clip-path` for better host and Obsidian CSS compatibility.\n\n### Fixed\n- Slider thumb rendering artifacts caused by painting the range track on the native input box.\n- Input focus visibility after removing custom engineering steppers.\n- Home RC example input labels now use native Input component labels instead of separate text labels.\n- Stat cards no longer clip updated text during cross-document state examples.\n- Markdown calculator examples no longer render duplicate section labels.\n\n## v0.3.0 - Examples overhaul with component audit and i18n\n\n### Added\n- Example gallery: 17 high-quality examples organized by usage scenario (Getting Started, Calculators, Data Browsing, Dashboards, Config Wizards, Decision Support, Platform Features)\n- English translations for all 17 example pages\n- `toolhost-demo`: real `renderToolCall` API with chat-style conversation UI\n- Example rendering infrastructure: `site/routes/examples.js`, `site/pages/examples.slex.js`, `site/data/examples.js`\n- Formula component (`src/components/svelte/content/Formula.svelte`) with KaTeX rendering\n- `src/engine/capabilities.ts`: structured capability docs for AI agents\n- `src/engine/validation.ts`: SPEC contract validation\n- `src/engine/stdlib.ts`: standard library with `math.clamp`, `math.safeDivide`, and other utilities\n- `src/engine/sandbox-runner.ts`: sandbox runner for secure runtime\n- Component state eval context shadowing test suite (`component-state-shadowing.test.ts`)\n- Collapsible and Callout double-rendering regression tests\n- Slider component name shadowing regression test\n\n### Changed\n- Examples reduced from 64 to 17 high-quality examples, organized by user story\n- Example source locale: `zh-CN` (with `en-US` translations)\n- `renderChildren` (`helpers.ts`) now clears existing content when children are present\n- Switch component now accepts `checked`/`value` props for initialization consistency with Checkbox\n- Site UI: DocsShell, DocRail, router, shell improvements\n- Components: Input, Select, Tabs, Table, PlaygroundMarkdown refinements\n- CSS: theme-shadcn, text-input, docs-shell styling updates\n\n### Fixed\n- Eval context shadowing: component names `g` and `api` no longer overwrite reserved context keys\n- `renderChildren` double rendering in Collapsible and Callout\n- Voltage divider summary typo (\"输入输入电压\")\n- Salary calculator fallback numbers to match actual calculator output\n- Tabs-and-branching: title and length conversion mismatch\n- 4 pre-existing test failures (ai-docs, page-structure, theme, markdown-content)\n\n### Removed\n- 47 low-quality/duplicate examples (reduced from 64 to 17)\n- Dead \"Fallback\" copywriting from all example files\n- Unused `DialogShell.svelte` component\n\n## v0.2.0 - First public release\n\n### Added\n- `@slexkit/mcp`: AI Agent Model Context Protocol server with `slexkitDocs`, `slexkitExamples`, `slexkitValidate` tools\n- Protocol marker: `\"slex\": \"0.1\"` required on all Slex expressions and ToolHost templates\n- SPEC contract validation: component specs are now validated against the runtime contract\n- Version sync automation (`scripts/sync-version.ts`) and changelog sync (`scripts/sync-changelog.ts`)\n- AI documentation generation pipeline with structured LLM-friendly output\n- Static site export with SEO metadata engine (`site/data/seo.js`, `site/scripts/export-static.ts`)\n- Chinese documentation for all reference and guide pages\n- Enhanced component state management with lifecycle hooks (`onMount`, `onUnmount`, `onUpdate`)\n\n### Changed\n- Switch component migrated from `checked` to `enabled` state mode\n- Documentation: restructured site content, synced en-US with zh-CN, added reference section\n- Theme: refined select styling, dropdown shadows, footer and info tone polish\n- AI docs generation enhanced with Chinese/English locale awareness\n\n### Fixed\n- Component spec alignment with documentation across all 28 components\n- Site routing and code block highlighting\n- Introduction and quick-start guide wording for clarity\n- Broken links and factual errors in component and reference documentation\n\n## v0.1.9\n\n### Added\n- Icon manager with Phosphor icon system (`registerIcon`, `registerIcons`, `getIcon`, `loadIcon`)\n- Expanded icon support across labeled components (badge, button, callout, etc.)\n- Icon docs page with registration API reference\n\n### Fixed\n- Refined component interactions in static site export\n- Tabs indicator animation restored\n- Callout and toast icon placement in titles\n- Numeric value display formatting\n\n### Changed\n- Site docs shell refactored for static export\n- Site navigation and theme controls alignment\n- Slex naming standardized across codebase\n\n## v0.1.8\n\n### Added\n- CSP-hardened secure runtime sandbox with heartbeat watchdog\n- `mountSecureArtifact()` for isolated iframe rendering\n- `createSlexKitMarkdownRuntimeHost()` for Markdown-hosted SlexKit blocks\n- Streamdown React renderer (`@slexkit/streamdown`)\n- Obsidian plugin adapter (`@slexkit/obsidian`)\n- Shadcn-compatible CSS theme (`@slexkit/theme-shadcn`)\n- Package boundary wrappers (`@slexkit/runtime`, `@slexkit/components-svelte`)\n- ToolHost with built-in templates: `confirm-action`, `choose-options`, `fill-form`\n\n### Changed\n- Component registration model: side-effect import registers all components\n- Styles reorganized into per-component CSS files\n- Build system: Bun.build with Svelte plugin, split ESM entries\n\n## v0.1.7\n\n### Added\n- `$for` list rendering with keyed reconciliation (delete / add-update-reorder / trim phases)\n- `$if` conditional rendering with enter/leave animation support\n- `$key` strategy: `$value`, property-based, or fallback to index\n- Component instance state modes: `value`, `checked`, `enabled`, `readable`, `none`\n- Lifecycle hooks: `g.onMount_<name>()`, `g.onUnmount_<name>()`, `g.onUpdate_<name>()`\n- Engineering number input with SI prefix parsing\n- Rich error diagnostics with line/column/excerpt display\n\n### Changed\n- Expression evaluation: `new Function()` compilation with reactive dependency tracking\n- Layout tree renderer now supports three rendering paths (normal, `$if`, `$for`)\n- `g` deep-merge preserves keys not present in the new state\n\n## v0.1.6 and earlier\n\n### Added\n- Reactive `g`/`layout` split with expression pipes (`$` read-pipes, `on*` write-pipes)\n- Custom fine-grained reactivity system (~280 lines, no external dependency)\n- Component registry with extensible renderer interface\n- Svelte 5 component adapter (creates stores from props, flushSync DOM)\n- `mount()`, `ingest()`, `boot()` entry points\n- 28 built-in Svelte components across 8 categories\n- `parseSlexSource()` DSL parser with `diagnoseSlexKitSource()` error reporting\n- Documentation site with interactive playground",
742
- "hash": "bc737cc1"
741
+ "body": "---\ntitle: Changelog\ncategory: Releases\nstatus: ready\norder: 10\nsummary: \"Release notes and notable changes for SlexKit.\"\nslexkitRenderMode: component\n---\n\n# Changelog\n\nAll notable changes to SlexKit.\n\n## v0.3.2 - Host CSS isolation and repeated layout hardening\n\n### Changed\n- `$for` rendering now uses comment anchors and direct child insertion instead of a wrapper element that depended on `display: contents`.\n- Site-only mobile navigation CSS moved out of the runtime base stylesheet and into the documentation site shell.\n- Component accessors now share one reactive effect across subscribers instead of creating duplicate subscriber fan-out work.\n\n### Fixed\n- Obsidian and other Markdown hosts no longer need to rewrite `$for` wrapper CSS to avoid `display: contents`, preserving grid and row layouts for repeated items.\n- Published runtime base CSS no longer leaks `#mobileNav` or `body[data-mobile-nav-open]` selectors into host pages.\n- Custom renderers that return no element no longer leave invalid `$for` slots behind during diffing or cleanup.\n\n## v0.3.1 - Host stability and control rendering hardening\n\n### Added\n- Runtime style safety tests that block broad `:has()` selectors, `clip-path`, and slider track regressions in shipped CSS.\n- Regression coverage for disabled Switch, Checkbox, and Radio state attributes.\n\n### Changed\n- CI now installs dependencies with `bun install --frozen-lockfile` and runs lint before tests.\n- Disabled Switch, Checkbox, and Radio styling now uses explicit `data-disabled` attributes instead of broad relational selectors.\n- Select and sr-only helper styles avoid `clip-path` for better host and Obsidian CSS compatibility.\n\n### Fixed\n- Slider thumb rendering artifacts caused by painting the range track on the native input box.\n- Input focus visibility after removing custom engineering steppers.\n- Home RC example input labels now use native Input component labels instead of separate text labels.\n- Stat cards no longer clip updated text during cross-document state examples.\n- Markdown calculator examples no longer render duplicate section labels.\n\n## v0.3.0 - Examples overhaul with component audit and i18n\n\n### Added\n- Example gallery: 17 high-quality examples organized by usage scenario (Getting Started, Calculators, Data Browsing, Dashboards, Config Wizards, Decision Support, Platform Features)\n- English translations for all 17 example pages\n- `toolhost-demo`: real `renderToolCall` API with chat-style conversation UI\n- Example rendering infrastructure: `site/routes/examples.js`, `site/pages/examples.slex.js`, `site/data/examples.js`\n- Formula component (`src/components/svelte/content/Formula.svelte`) with KaTeX rendering\n- `src/engine/capabilities.ts`: structured capability docs for AI agents\n- `src/engine/validation.ts`: SPEC contract validation\n- `src/engine/stdlib.ts`: standard library with `math.clamp`, `math.safeDivide`, and other utilities\n- `src/engine/sandbox-runner.ts`: sandbox runner for secure runtime\n- Component state eval context shadowing test suite (`component-state-shadowing.test.ts`)\n- Collapsible and Callout double-rendering regression tests\n- Slider component name shadowing regression test\n\n### Changed\n- Examples reduced from 64 to 17 high-quality examples, organized by user story\n- Example source locale: `zh-CN` (with `en-US` translations)\n- `renderChildren` (`helpers.ts`) now clears existing content when children are present\n- Switch component now accepts `checked`/`value` props for initialization consistency with Checkbox\n- Site UI: DocsShell, DocRail, router, shell improvements\n- Components: Input, Select, Tabs, Table, PlaygroundMarkdown refinements\n- CSS: theme-shadcn, text-input, docs-shell styling updates\n\n### Fixed\n- Eval context shadowing: component names `g` and `api` no longer overwrite reserved context keys\n- `renderChildren` double rendering in Collapsible and Callout\n- Voltage divider summary typo (\"输入输入电压\")\n- Salary calculator fallback numbers to match actual calculator output\n- Tabs-and-branching: title and length conversion mismatch\n- 4 pre-existing test failures (ai-docs, page-structure, theme, markdown-content)\n\n### Removed\n- 47 low-quality/duplicate examples (reduced from 64 to 17)\n- Dead \"Fallback\" copywriting from all example files\n- Unused `DialogShell.svelte` component\n\n## v0.2.0 - First public release\n\n### Added\n- `@slexkit/mcp`: AI Agent Model Context Protocol server with `slexkitDocs`, `slexkitExamples`, `slexkitValidate` tools\n- Protocol marker: `\"slex\": \"0.1\"` required on all Slex expressions and ToolHost templates\n- SPEC contract validation: component specs are now validated against the runtime contract\n- Version sync automation (`scripts/sync-version.ts`) and changelog sync (`scripts/sync-changelog.ts`)\n- AI documentation generation pipeline with structured LLM-friendly output\n- Static site export with SEO metadata engine (`site/data/seo.js`, `site/scripts/export-static.ts`)\n- Chinese documentation for all reference and guide pages\n- Enhanced component state management with lifecycle hooks (`onMount`, `onUnmount`, `onUpdate`)\n\n### Changed\n- Switch component migrated from `checked` to `enabled` state mode\n- Documentation: restructured site content, synced en-US with zh-CN, added reference section\n- Theme: refined select styling, dropdown shadows, footer and info tone polish\n- AI docs generation enhanced with Chinese/English locale awareness\n\n### Fixed\n- Component spec alignment with documentation across all 28 components\n- Site routing and code block highlighting\n- Introduction and quick-start guide wording for clarity\n- Broken links and factual errors in component and reference documentation\n\n## v0.1.9\n\n### Added\n- Icon manager with Phosphor icon system (`registerIcon`, `registerIcons`, `getIcon`, `loadIcon`)\n- Expanded icon support across labeled components (badge, button, callout, etc.)\n- Icon docs page with registration API reference\n\n### Fixed\n- Refined component interactions in static site export\n- Tabs indicator animation restored\n- Callout and toast icon placement in titles\n- Numeric value display formatting\n\n### Changed\n- Site docs shell refactored for static export\n- Site navigation and theme controls alignment\n- Slex naming standardized across codebase\n\n## v0.1.8\n\n### Added\n- CSP-hardened secure runtime sandbox with heartbeat watchdog\n- `mountSecureArtifact()` for isolated iframe rendering\n- `createSlexKitMarkdownRuntimeHost()` for Markdown-hosted SlexKit blocks\n- Streamdown React renderer (`@slexkit/streamdown`)\n- Obsidian plugin adapter (`@slexkit/obsidian`)\n- Shadcn-compatible CSS theme (`@slexkit/theme-shadcn`)\n- Package boundary wrappers (`@slexkit/runtime`, `@slexkit/components-svelte`)\n- ToolHost with built-in templates: `confirm-action`, `choose-options`, `fill-form`\n\n### Changed\n- Component registration model: side-effect import registers all components\n- Styles reorganized into per-component CSS files\n- Build system: Bun.build with Svelte plugin, split ESM entries\n\n## v0.1.7\n\n### Added\n- `$for` list rendering with keyed reconciliation (delete / add-update-reorder / trim phases)\n- `$if` conditional rendering with enter/leave animation support\n- `$key` strategy: `$value`, property-based, or fallback to index\n- Component instance state modes: `value`, `checked`, `enabled`, `readable`, `none`\n- Lifecycle hooks: `g.onMount_<name>()`, `g.onUnmount_<name>()`, `g.onUpdate_<name>()`\n- Engineering number input with SI prefix parsing\n- Rich error diagnostics with line/column/excerpt display\n\n### Changed\n- Expression evaluation: `new Function()` compilation with reactive dependency tracking\n- Layout tree renderer now supports three rendering paths (normal, `$if`, `$for`)\n- `g` deep-merge preserves keys not present in the new state\n\n## v0.1.6 and earlier\n\n### Added\n- Reactive `g`/`layout` split with expression pipes (`$` read-pipes, `on*` write-pipes)\n- Custom fine-grained reactivity system (~280 lines, no external dependency)\n- Component registry with extensible renderer interface\n- Svelte 5 component adapter (creates stores from props, flushSync DOM)\n- `mount()`, `ingest()`, `boot()` entry points\n- 28 built-in Svelte components across 8 categories\n- `parseSlexSource()` DSL parser with `diagnoseSlexKitSource()` error reporting\n- Documentation site with interactive playground",
742
+ "hash": "57cc5d83"
743
743
  }
744
744
  ],
745
745
  "expressionContext": [
@@ -3481,7 +3481,7 @@
3481
3481
  }
3482
3482
  ],
3483
3483
  "sourceHashes": {
3484
- "README.md": "117cabab",
3484
+ "README.md": "4c55b5dc",
3485
3485
  "site/content/guides/intro/en-US.md": "bff82366",
3486
3486
  "site/content/guides/quick-start/en-US.md": "ba82b030",
3487
3487
  "site/content/guides/integration/en-US.md": "1138bddd",
@@ -3514,7 +3514,7 @@
3514
3514
  "site/content/reference/toolhost/en-US.md": "f62ba8aa",
3515
3515
  "site/content/reference/icons/en-US.md": "3d32cbbf",
3516
3516
  "site/content/reference/rationale/en-US.md": "dcf53947",
3517
- "site/content/releases/changelog/en-US.md": "bc737cc1",
3517
+ "site/content/releases/changelog/en-US.md": "57cc5d83",
3518
3518
  "site/content/components/accordion/en-US.md": "9e90867f",
3519
3519
  "site/content/components/badge/en-US.md": "fb8275e7",
3520
3520
  "site/content/components/button/en-US.md": "30463322",
package/dist/base.css CHANGED
@@ -133,52 +133,6 @@ color-scheme: dark;
133
133
  box-sizing: border-box;
134
134
  }
135
135
 
136
- body[data-mobile-nav-open] {
137
- overflow: hidden;
138
- overscroll-behavior: contain;
139
- }
140
-
141
- #mobileNav {
142
- --mobile-nav-backdrop-opacity: 0;
143
- --mobile-nav-panel-translate: -100%;
144
- pointer-events: none;
145
- }
146
-
147
- #mobileNav[data-open="true"] {
148
- --mobile-nav-backdrop-opacity: 1;
149
- --mobile-nav-panel-translate: 0px;
150
- pointer-events: auto;
151
- }
152
-
153
- #mobileNav [data-mobile-nav-backdrop] {
154
- opacity: var(--mobile-nav-backdrop-opacity);
155
- touch-action: pan-y;
156
- transition: opacity 180ms ease;
157
- }
158
-
159
- #mobileNav [data-mobile-nav-panel] {
160
- transform: translateX(var(--mobile-nav-panel-translate));
161
- touch-action: pan-y;
162
- transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
163
- will-change: transform;
164
- }
165
-
166
- #mobileNav[data-dragging="true"] [data-mobile-nav-backdrop],
167
- #mobileNav[data-dragging="true"] [data-mobile-nav-panel] {
168
- transition: none;
169
- }
170
-
171
- @media (prefers-reduced-motion: reduce) {
172
- #mobileNav [data-mobile-nav-backdrop],
173
- #mobileNav [data-mobile-nav-panel] {
174
- transition: none;
175
- }
176
- }
177
-
178
- .slexkit-for-wrapper {
179
- display: contents;
180
- }
181
-
182
136
  .slexkit-source-toolbar {
183
137
  display: flex;
184
138
  align-items: center;
package/dist/runtime.cjs CHANGED
@@ -468,19 +468,31 @@ function configureComponentScope(options) {
468
468
  function createComponentAccessor(read) {
469
469
  const subscribers = new Set;
470
470
  let current = read();
471
- const accessor = () => current;
472
- accessor.subscribe = (run) => {
473
- subscribers.add(run);
474
- run(current);
475
- const stop = createEffect(() => {
471
+ let stopEffect;
472
+ const start = () => {
473
+ if (stopEffect)
474
+ return;
475
+ stopEffect = createEffect(() => {
476
476
  current = read();
477
- for (const subscriber of subscribers)
477
+ for (const subscriber of Array.from(subscribers))
478
478
  subscriber(current);
479
479
  flushDom?.();
480
480
  });
481
+ };
482
+ const accessor = () => current;
483
+ accessor.subscribe = (run) => {
484
+ const wasIdle = subscribers.size === 0;
485
+ subscribers.add(run);
486
+ if (wasIdle)
487
+ start();
488
+ else
489
+ run(current);
481
490
  return () => {
482
491
  subscribers.delete(run);
483
- stop();
492
+ if (subscribers.size === 0) {
493
+ stopEffect?.();
494
+ stopEffect = undefined;
495
+ }
484
496
  };
485
497
  };
486
498
  return accessor;
@@ -1331,9 +1343,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1331
1343
  const renderer = getRenderer(type);
1332
1344
  if (!renderer)
1333
1345
  return;
1334
- const forWrapper = (container.ownerDocument || document).createElement("div");
1335
- forWrapper.className = "slexkit-for-wrapper";
1336
- container.appendChild(forWrapper);
1346
+ const doc = container.ownerDocument || document;
1347
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1348
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1349
+ container.append(startAnchor, endAnchor);
1337
1350
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1338
1351
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1339
1352
  const $keyProp = props.$key;
@@ -1346,8 +1359,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1346
1359
  disposedSlots.add(slot);
1347
1360
  leavingSlots.delete(slot);
1348
1361
  callHook(g, name, "onUnmount");
1349
- disposeComponent(slot.el);
1350
- slot.el.remove();
1362
+ if (slot.el) {
1363
+ disposeComponent(slot.el);
1364
+ slot.el.remove();
1365
+ }
1351
1366
  if (slot.dispose)
1352
1367
  slot.dispose();
1353
1368
  };
@@ -1372,12 +1387,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1372
1387
  }
1373
1388
  }
1374
1389
  for (const slot of deletedSlots) {
1375
- container.appendChild(slot.el);
1376
1390
  leavingSlots.add(slot);
1391
+ if (!slot.el) {
1392
+ disposeSlot(slot);
1393
+ continue;
1394
+ }
1377
1395
  applyLeaveAnimation(slot.el, slot.props, () => {
1378
1396
  disposeSlot(slot);
1379
1397
  });
1380
1398
  }
1399
+ let cursor = startAnchor;
1381
1400
  arr.forEach((item, index) => {
1382
1401
  item = asReactiveValue(item, g);
1383
1402
  const keyVal = newKeys[index];
@@ -1405,20 +1424,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1405
1424
  const indexSignal = createSignal(index);
1406
1425
  const revisionSignal = createSignal(0);
1407
1426
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1408
- if (slot.el) {
1409
- applyEnterAnimation(slot.el, slot.props);
1410
- callHook(g, name, "onMount");
1427
+ if (!slot.el) {
1428
+ disposeSlot(slot);
1429
+ return;
1411
1430
  }
1431
+ applyEnterAnimation(slot.el, slot.props);
1432
+ callHook(g, name, "onMount");
1412
1433
  slotMap.set(keyVal, slot);
1413
1434
  }
1414
- const refChild = forWrapper.children[index];
1415
- if (slot.el && refChild !== slot.el) {
1416
- forWrapper.insertBefore(slot.el, refChild ?? null);
1435
+ const nextChild = cursor.nextSibling;
1436
+ if (slot.el && nextChild !== slot.el) {
1437
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1438
+ }
1439
+ if (slot.el) {
1440
+ cursor = slot.el;
1417
1441
  }
1418
1442
  });
1419
- while (forWrapper.children.length > arr.length) {
1420
- forWrapper.lastChild.remove();
1421
- }
1422
1443
  });
1423
1444
  onCleanup(() => {
1424
1445
  for (const slot of Array.from(slotMap.values()))
@@ -1426,7 +1447,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1426
1447
  slotMap.clear();
1427
1448
  for (const slot of Array.from(leavingSlots))
1428
1449
  disposeSlot(slot);
1429
- forWrapper.remove();
1450
+ startAnchor.remove();
1451
+ endAnchor.remove();
1430
1452
  });
1431
1453
  }
1432
1454
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1803,7 +1825,7 @@ ${parseSource}
1803
1825
  var parseSlexKitDsl = parseSlexSource;
1804
1826
 
1805
1827
  // src/version.ts
1806
- var SLEXKIT_VERSION = "0.3.1";
1828
+ var SLEXKIT_VERSION = "0.3.2";
1807
1829
  var SLEX_PROTOCOL_VERSION = "0.1";
1808
1830
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1809
1831
  function getSlexKitInfo() {
package/dist/runtime.js CHANGED
@@ -396,19 +396,31 @@ function configureComponentScope(options) {
396
396
  function createComponentAccessor(read) {
397
397
  const subscribers = new Set;
398
398
  let current = read();
399
- const accessor = () => current;
400
- accessor.subscribe = (run) => {
401
- subscribers.add(run);
402
- run(current);
403
- const stop = createEffect(() => {
399
+ let stopEffect;
400
+ const start = () => {
401
+ if (stopEffect)
402
+ return;
403
+ stopEffect = createEffect(() => {
404
404
  current = read();
405
- for (const subscriber of subscribers)
405
+ for (const subscriber of Array.from(subscribers))
406
406
  subscriber(current);
407
407
  flushDom?.();
408
408
  });
409
+ };
410
+ const accessor = () => current;
411
+ accessor.subscribe = (run) => {
412
+ const wasIdle = subscribers.size === 0;
413
+ subscribers.add(run);
414
+ if (wasIdle)
415
+ start();
416
+ else
417
+ run(current);
409
418
  return () => {
410
419
  subscribers.delete(run);
411
- stop();
420
+ if (subscribers.size === 0) {
421
+ stopEffect?.();
422
+ stopEffect = undefined;
423
+ }
412
424
  };
413
425
  };
414
426
  return accessor;
@@ -1259,9 +1271,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1259
1271
  const renderer = getRenderer(type);
1260
1272
  if (!renderer)
1261
1273
  return;
1262
- const forWrapper = (container.ownerDocument || document).createElement("div");
1263
- forWrapper.className = "slexkit-for-wrapper";
1264
- container.appendChild(forWrapper);
1274
+ const doc = container.ownerDocument || document;
1275
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1276
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1277
+ container.append(startAnchor, endAnchor);
1265
1278
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1266
1279
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1267
1280
  const $keyProp = props.$key;
@@ -1274,8 +1287,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1274
1287
  disposedSlots.add(slot);
1275
1288
  leavingSlots.delete(slot);
1276
1289
  callHook(g, name, "onUnmount");
1277
- disposeComponent(slot.el);
1278
- slot.el.remove();
1290
+ if (slot.el) {
1291
+ disposeComponent(slot.el);
1292
+ slot.el.remove();
1293
+ }
1279
1294
  if (slot.dispose)
1280
1295
  slot.dispose();
1281
1296
  };
@@ -1300,12 +1315,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1300
1315
  }
1301
1316
  }
1302
1317
  for (const slot of deletedSlots) {
1303
- container.appendChild(slot.el);
1304
1318
  leavingSlots.add(slot);
1319
+ if (!slot.el) {
1320
+ disposeSlot(slot);
1321
+ continue;
1322
+ }
1305
1323
  applyLeaveAnimation(slot.el, slot.props, () => {
1306
1324
  disposeSlot(slot);
1307
1325
  });
1308
1326
  }
1327
+ let cursor = startAnchor;
1309
1328
  arr.forEach((item, index) => {
1310
1329
  item = asReactiveValue(item, g);
1311
1330
  const keyVal = newKeys[index];
@@ -1333,20 +1352,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1333
1352
  const indexSignal = createSignal(index);
1334
1353
  const revisionSignal = createSignal(0);
1335
1354
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1336
- if (slot.el) {
1337
- applyEnterAnimation(slot.el, slot.props);
1338
- callHook(g, name, "onMount");
1355
+ if (!slot.el) {
1356
+ disposeSlot(slot);
1357
+ return;
1339
1358
  }
1359
+ applyEnterAnimation(slot.el, slot.props);
1360
+ callHook(g, name, "onMount");
1340
1361
  slotMap.set(keyVal, slot);
1341
1362
  }
1342
- const refChild = forWrapper.children[index];
1343
- if (slot.el && refChild !== slot.el) {
1344
- forWrapper.insertBefore(slot.el, refChild ?? null);
1363
+ const nextChild = cursor.nextSibling;
1364
+ if (slot.el && nextChild !== slot.el) {
1365
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1366
+ }
1367
+ if (slot.el) {
1368
+ cursor = slot.el;
1345
1369
  }
1346
1370
  });
1347
- while (forWrapper.children.length > arr.length) {
1348
- forWrapper.lastChild.remove();
1349
- }
1350
1371
  });
1351
1372
  onCleanup(() => {
1352
1373
  for (const slot of Array.from(slotMap.values()))
@@ -1354,7 +1375,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1354
1375
  slotMap.clear();
1355
1376
  for (const slot of Array.from(leavingSlots))
1356
1377
  disposeSlot(slot);
1357
- forWrapper.remove();
1378
+ startAnchor.remove();
1379
+ endAnchor.remove();
1358
1380
  });
1359
1381
  }
1360
1382
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1731,7 +1753,7 @@ ${parseSource}
1731
1753
  var parseSlexKitDsl = parseSlexSource;
1732
1754
 
1733
1755
  // src/version.ts
1734
- var SLEXKIT_VERSION = "0.3.1";
1756
+ var SLEXKIT_VERSION = "0.3.2";
1735
1757
  var SLEX_PROTOCOL_VERSION = "0.1";
1736
1758
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1737
1759
  function getSlexKitInfo() {
package/dist/slexkit.cjs CHANGED
@@ -490,19 +490,31 @@ function configureComponentScope(options) {
490
490
  function createComponentAccessor(read) {
491
491
  const subscribers = new Set;
492
492
  let current = read();
493
- const accessor = () => current;
494
- accessor.subscribe = (run) => {
495
- subscribers.add(run);
496
- run(current);
497
- const stop = createEffect(() => {
493
+ let stopEffect;
494
+ const start = () => {
495
+ if (stopEffect)
496
+ return;
497
+ stopEffect = createEffect(() => {
498
498
  current = read();
499
- for (const subscriber of subscribers)
499
+ for (const subscriber of Array.from(subscribers))
500
500
  subscriber(current);
501
501
  flushDom?.();
502
502
  });
503
+ };
504
+ const accessor = () => current;
505
+ accessor.subscribe = (run) => {
506
+ const wasIdle = subscribers.size === 0;
507
+ subscribers.add(run);
508
+ if (wasIdle)
509
+ start();
510
+ else
511
+ run(current);
503
512
  return () => {
504
513
  subscribers.delete(run);
505
- stop();
514
+ if (subscribers.size === 0) {
515
+ stopEffect?.();
516
+ stopEffect = undefined;
517
+ }
506
518
  };
507
519
  };
508
520
  return accessor;
@@ -1353,9 +1365,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1353
1365
  const renderer = getRenderer(type);
1354
1366
  if (!renderer)
1355
1367
  return;
1356
- const forWrapper = (container.ownerDocument || document).createElement("div");
1357
- forWrapper.className = "slexkit-for-wrapper";
1358
- container.appendChild(forWrapper);
1368
+ const doc = container.ownerDocument || document;
1369
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1370
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1371
+ container.append(startAnchor, endAnchor);
1359
1372
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1360
1373
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1361
1374
  const $keyProp = props.$key;
@@ -1368,8 +1381,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1368
1381
  disposedSlots.add(slot);
1369
1382
  leavingSlots.delete(slot);
1370
1383
  callHook(g, name, "onUnmount");
1371
- disposeComponent(slot.el);
1372
- slot.el.remove();
1384
+ if (slot.el) {
1385
+ disposeComponent(slot.el);
1386
+ slot.el.remove();
1387
+ }
1373
1388
  if (slot.dispose)
1374
1389
  slot.dispose();
1375
1390
  };
@@ -1394,12 +1409,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1394
1409
  }
1395
1410
  }
1396
1411
  for (const slot of deletedSlots) {
1397
- container.appendChild(slot.el);
1398
1412
  leavingSlots.add(slot);
1413
+ if (!slot.el) {
1414
+ disposeSlot(slot);
1415
+ continue;
1416
+ }
1399
1417
  applyLeaveAnimation(slot.el, slot.props, () => {
1400
1418
  disposeSlot(slot);
1401
1419
  });
1402
1420
  }
1421
+ let cursor = startAnchor;
1403
1422
  arr.forEach((item, index) => {
1404
1423
  item = asReactiveValue(item, g);
1405
1424
  const keyVal = newKeys[index];
@@ -1427,20 +1446,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1427
1446
  const indexSignal = createSignal(index);
1428
1447
  const revisionSignal = createSignal(0);
1429
1448
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1430
- if (slot.el) {
1431
- applyEnterAnimation(slot.el, slot.props);
1432
- callHook(g, name, "onMount");
1449
+ if (!slot.el) {
1450
+ disposeSlot(slot);
1451
+ return;
1433
1452
  }
1453
+ applyEnterAnimation(slot.el, slot.props);
1454
+ callHook(g, name, "onMount");
1434
1455
  slotMap.set(keyVal, slot);
1435
1456
  }
1436
- const refChild = forWrapper.children[index];
1437
- if (slot.el && refChild !== slot.el) {
1438
- forWrapper.insertBefore(slot.el, refChild ?? null);
1457
+ const nextChild = cursor.nextSibling;
1458
+ if (slot.el && nextChild !== slot.el) {
1459
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1460
+ }
1461
+ if (slot.el) {
1462
+ cursor = slot.el;
1439
1463
  }
1440
1464
  });
1441
- while (forWrapper.children.length > arr.length) {
1442
- forWrapper.lastChild.remove();
1443
- }
1444
1465
  });
1445
1466
  onCleanup(() => {
1446
1467
  for (const slot of Array.from(slotMap.values()))
@@ -1448,7 +1469,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1448
1469
  slotMap.clear();
1449
1470
  for (const slot of Array.from(leavingSlots))
1450
1471
  disposeSlot(slot);
1451
- forWrapper.remove();
1472
+ startAnchor.remove();
1473
+ endAnchor.remove();
1452
1474
  });
1453
1475
  }
1454
1476
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1825,7 +1847,7 @@ ${parseSource}
1825
1847
  var parseSlexKitDsl = parseSlexSource;
1826
1848
 
1827
1849
  // src/version.ts
1828
- var SLEXKIT_VERSION = "0.3.1";
1850
+ var SLEXKIT_VERSION = "0.3.2";
1829
1851
  var SLEX_PROTOCOL_VERSION = "0.1";
1830
1852
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1831
1853
  function getSlexKitInfo() {
package/dist/slexkit.css CHANGED
@@ -904,52 +904,6 @@ color-scheme: dark;
904
904
  box-sizing: border-box;
905
905
  }
906
906
 
907
- body[data-mobile-nav-open] {
908
- overflow: hidden;
909
- overscroll-behavior: contain;
910
- }
911
-
912
- #mobileNav {
913
- --mobile-nav-backdrop-opacity: 0;
914
- --mobile-nav-panel-translate: -100%;
915
- pointer-events: none;
916
- }
917
-
918
- #mobileNav[data-open="true"] {
919
- --mobile-nav-backdrop-opacity: 1;
920
- --mobile-nav-panel-translate: 0px;
921
- pointer-events: auto;
922
- }
923
-
924
- #mobileNav [data-mobile-nav-backdrop] {
925
- opacity: var(--mobile-nav-backdrop-opacity);
926
- touch-action: pan-y;
927
- transition: opacity 180ms ease;
928
- }
929
-
930
- #mobileNav [data-mobile-nav-panel] {
931
- transform: translateX(var(--mobile-nav-panel-translate));
932
- touch-action: pan-y;
933
- transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
934
- will-change: transform;
935
- }
936
-
937
- #mobileNav[data-dragging="true"] [data-mobile-nav-backdrop],
938
- #mobileNav[data-dragging="true"] [data-mobile-nav-panel] {
939
- transition: none;
940
- }
941
-
942
- @media (prefers-reduced-motion: reduce) {
943
- #mobileNav [data-mobile-nav-backdrop],
944
- #mobileNav [data-mobile-nav-panel] {
945
- transition: none;
946
- }
947
- }
948
-
949
- .slexkit-for-wrapper {
950
- display: contents;
951
- }
952
-
953
907
  .slexkit-source-toolbar {
954
908
  display: flex;
955
909
  align-items: center;
package/dist/slexkit.js CHANGED
@@ -396,19 +396,31 @@ function configureComponentScope(options) {
396
396
  function createComponentAccessor(read) {
397
397
  const subscribers = new Set;
398
398
  let current = read();
399
- const accessor = () => current;
400
- accessor.subscribe = (run) => {
401
- subscribers.add(run);
402
- run(current);
403
- const stop = createEffect(() => {
399
+ let stopEffect;
400
+ const start = () => {
401
+ if (stopEffect)
402
+ return;
403
+ stopEffect = createEffect(() => {
404
404
  current = read();
405
- for (const subscriber of subscribers)
405
+ for (const subscriber of Array.from(subscribers))
406
406
  subscriber(current);
407
407
  flushDom?.();
408
408
  });
409
+ };
410
+ const accessor = () => current;
411
+ accessor.subscribe = (run) => {
412
+ const wasIdle = subscribers.size === 0;
413
+ subscribers.add(run);
414
+ if (wasIdle)
415
+ start();
416
+ else
417
+ run(current);
409
418
  return () => {
410
419
  subscribers.delete(run);
411
- stop();
420
+ if (subscribers.size === 0) {
421
+ stopEffect?.();
422
+ stopEffect = undefined;
423
+ }
412
424
  };
413
425
  };
414
426
  return accessor;
@@ -1259,9 +1271,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1259
1271
  const renderer = getRenderer(type);
1260
1272
  if (!renderer)
1261
1273
  return;
1262
- const forWrapper = (container.ownerDocument || document).createElement("div");
1263
- forWrapper.className = "slexkit-for-wrapper";
1264
- container.appendChild(forWrapper);
1274
+ const doc = container.ownerDocument || document;
1275
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1276
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1277
+ container.append(startAnchor, endAnchor);
1265
1278
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1266
1279
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1267
1280
  const $keyProp = props.$key;
@@ -1274,8 +1287,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1274
1287
  disposedSlots.add(slot);
1275
1288
  leavingSlots.delete(slot);
1276
1289
  callHook(g, name, "onUnmount");
1277
- disposeComponent(slot.el);
1278
- slot.el.remove();
1290
+ if (slot.el) {
1291
+ disposeComponent(slot.el);
1292
+ slot.el.remove();
1293
+ }
1279
1294
  if (slot.dispose)
1280
1295
  slot.dispose();
1281
1296
  };
@@ -1300,12 +1315,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1300
1315
  }
1301
1316
  }
1302
1317
  for (const slot of deletedSlots) {
1303
- container.appendChild(slot.el);
1304
1318
  leavingSlots.add(slot);
1319
+ if (!slot.el) {
1320
+ disposeSlot(slot);
1321
+ continue;
1322
+ }
1305
1323
  applyLeaveAnimation(slot.el, slot.props, () => {
1306
1324
  disposeSlot(slot);
1307
1325
  });
1308
1326
  }
1327
+ let cursor = startAnchor;
1309
1328
  arr.forEach((item, index) => {
1310
1329
  item = asReactiveValue(item, g);
1311
1330
  const keyVal = newKeys[index];
@@ -1333,20 +1352,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1333
1352
  const indexSignal = createSignal(index);
1334
1353
  const revisionSignal = createSignal(0);
1335
1354
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1336
- if (slot.el) {
1337
- applyEnterAnimation(slot.el, slot.props);
1338
- callHook(g, name, "onMount");
1355
+ if (!slot.el) {
1356
+ disposeSlot(slot);
1357
+ return;
1339
1358
  }
1359
+ applyEnterAnimation(slot.el, slot.props);
1360
+ callHook(g, name, "onMount");
1340
1361
  slotMap.set(keyVal, slot);
1341
1362
  }
1342
- const refChild = forWrapper.children[index];
1343
- if (slot.el && refChild !== slot.el) {
1344
- forWrapper.insertBefore(slot.el, refChild ?? null);
1363
+ const nextChild = cursor.nextSibling;
1364
+ if (slot.el && nextChild !== slot.el) {
1365
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1366
+ }
1367
+ if (slot.el) {
1368
+ cursor = slot.el;
1345
1369
  }
1346
1370
  });
1347
- while (forWrapper.children.length > arr.length) {
1348
- forWrapper.lastChild.remove();
1349
- }
1350
1371
  });
1351
1372
  onCleanup(() => {
1352
1373
  for (const slot of Array.from(slotMap.values()))
@@ -1354,7 +1375,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1354
1375
  slotMap.clear();
1355
1376
  for (const slot of Array.from(leavingSlots))
1356
1377
  disposeSlot(slot);
1357
- forWrapper.remove();
1378
+ startAnchor.remove();
1379
+ endAnchor.remove();
1358
1380
  });
1359
1381
  }
1360
1382
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1731,7 +1753,7 @@ ${parseSource}
1731
1753
  var parseSlexKitDsl = parseSlexSource;
1732
1754
 
1733
1755
  // src/version.ts
1734
- var SLEXKIT_VERSION = "0.3.1";
1756
+ var SLEXKIT_VERSION = "0.3.2";
1735
1757
  var SLEX_PROTOCOL_VERSION = "0.1";
1736
1758
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1737
1759
  function getSlexKitInfo() {
@@ -23,7 +23,7 @@ export type ForContext = {
23
23
  };
24
24
  export type ForSlot = {
25
25
  key: unknown;
26
- el: HTMLElement;
26
+ el: HTMLElement | null;
27
27
  forCtx: ForContext;
28
28
  index: number;
29
29
  item: unknown;
@@ -1,6 +1,6 @@
1
- export declare const SLEXKIT_VERSION = "0.3.1";
1
+ export declare const SLEXKIT_VERSION = "0.3.2";
2
2
  export declare const SLEX_PROTOCOL_VERSION = "0.1";
3
- export declare const SLEXKIT_COMPONENTS_VERSION = "0.3.1";
3
+ export declare const SLEXKIT_COMPONENTS_VERSION = "0.3.2";
4
4
  export declare function getSlexKitInfo(): {
5
5
  version: string;
6
6
  protocolVersion: string;
@@ -493,19 +493,31 @@
493
493
  function createComponentAccessor(read) {
494
494
  const subscribers = new Set;
495
495
  let current = read();
496
- const accessor = () => current;
497
- accessor.subscribe = (run) => {
498
- subscribers.add(run);
499
- run(current);
500
- const stop = createEffect(() => {
496
+ let stopEffect;
497
+ const start = () => {
498
+ if (stopEffect)
499
+ return;
500
+ stopEffect = createEffect(() => {
501
501
  current = read();
502
- for (const subscriber of subscribers)
502
+ for (const subscriber of Array.from(subscribers))
503
503
  subscriber(current);
504
504
  flushDom?.();
505
505
  });
506
+ };
507
+ const accessor = () => current;
508
+ accessor.subscribe = (run) => {
509
+ const wasIdle = subscribers.size === 0;
510
+ subscribers.add(run);
511
+ if (wasIdle)
512
+ start();
513
+ else
514
+ run(current);
506
515
  return () => {
507
516
  subscribers.delete(run);
508
- stop();
517
+ if (subscribers.size === 0) {
518
+ stopEffect?.();
519
+ stopEffect = undefined;
520
+ }
509
521
  };
510
522
  };
511
523
  return accessor;
@@ -1356,9 +1368,10 @@
1356
1368
  const renderer = getRenderer(type);
1357
1369
  if (!renderer)
1358
1370
  return;
1359
- const forWrapper = (container.ownerDocument || document).createElement("div");
1360
- forWrapper.className = "slexkit-for-wrapper";
1361
- container.appendChild(forWrapper);
1371
+ const doc = container.ownerDocument || document;
1372
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1373
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1374
+ container.append(startAnchor, endAnchor);
1362
1375
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1363
1376
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1364
1377
  const $keyProp = props.$key;
@@ -1371,8 +1384,10 @@
1371
1384
  disposedSlots.add(slot);
1372
1385
  leavingSlots.delete(slot);
1373
1386
  callHook(g, name, "onUnmount");
1374
- disposeComponent(slot.el);
1375
- slot.el.remove();
1387
+ if (slot.el) {
1388
+ disposeComponent(slot.el);
1389
+ slot.el.remove();
1390
+ }
1376
1391
  if (slot.dispose)
1377
1392
  slot.dispose();
1378
1393
  };
@@ -1397,12 +1412,16 @@
1397
1412
  }
1398
1413
  }
1399
1414
  for (const slot of deletedSlots) {
1400
- container.appendChild(slot.el);
1401
1415
  leavingSlots.add(slot);
1416
+ if (!slot.el) {
1417
+ disposeSlot(slot);
1418
+ continue;
1419
+ }
1402
1420
  applyLeaveAnimation(slot.el, slot.props, () => {
1403
1421
  disposeSlot(slot);
1404
1422
  });
1405
1423
  }
1424
+ let cursor = startAnchor;
1406
1425
  arr.forEach((item, index) => {
1407
1426
  item = asReactiveValue(item, g);
1408
1427
  const keyVal = newKeys[index];
@@ -1430,20 +1449,22 @@
1430
1449
  const indexSignal = createSignal(index);
1431
1450
  const revisionSignal = createSignal(0);
1432
1451
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1433
- if (slot.el) {
1434
- applyEnterAnimation(slot.el, slot.props);
1435
- callHook(g, name, "onMount");
1452
+ if (!slot.el) {
1453
+ disposeSlot(slot);
1454
+ return;
1436
1455
  }
1456
+ applyEnterAnimation(slot.el, slot.props);
1457
+ callHook(g, name, "onMount");
1437
1458
  slotMap.set(keyVal, slot);
1438
1459
  }
1439
- const refChild = forWrapper.children[index];
1440
- if (slot.el && refChild !== slot.el) {
1441
- forWrapper.insertBefore(slot.el, refChild ?? null);
1460
+ const nextChild = cursor.nextSibling;
1461
+ if (slot.el && nextChild !== slot.el) {
1462
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1463
+ }
1464
+ if (slot.el) {
1465
+ cursor = slot.el;
1442
1466
  }
1443
1467
  });
1444
- while (forWrapper.children.length > arr.length) {
1445
- forWrapper.lastChild.remove();
1446
- }
1447
1468
  });
1448
1469
  onCleanup(() => {
1449
1470
  for (const slot of Array.from(slotMap.values()))
@@ -1451,7 +1472,8 @@
1451
1472
  slotMap.clear();
1452
1473
  for (const slot of Array.from(leavingSlots))
1453
1474
  disposeSlot(slot);
1454
- forWrapper.remove();
1475
+ startAnchor.remove();
1476
+ endAnchor.remove();
1455
1477
  });
1456
1478
  }
1457
1479
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -499,19 +499,31 @@
499
499
  function createComponentAccessor(read) {
500
500
  const subscribers = new Set;
501
501
  let current = read();
502
- const accessor = () => current;
503
- accessor.subscribe = (run) => {
504
- subscribers.add(run);
505
- run(current);
506
- const stop = createEffect(() => {
502
+ let stopEffect;
503
+ const start = () => {
504
+ if (stopEffect)
505
+ return;
506
+ stopEffect = createEffect(() => {
507
507
  current = read();
508
- for (const subscriber of subscribers)
508
+ for (const subscriber of Array.from(subscribers))
509
509
  subscriber(current);
510
510
  flushDom?.();
511
511
  });
512
+ };
513
+ const accessor = () => current;
514
+ accessor.subscribe = (run) => {
515
+ const wasIdle = subscribers.size === 0;
516
+ subscribers.add(run);
517
+ if (wasIdle)
518
+ start();
519
+ else
520
+ run(current);
512
521
  return () => {
513
522
  subscribers.delete(run);
514
- stop();
523
+ if (subscribers.size === 0) {
524
+ stopEffect?.();
525
+ stopEffect = undefined;
526
+ }
515
527
  };
516
528
  };
517
529
  return accessor;
@@ -1362,9 +1374,10 @@
1362
1374
  const renderer = getRenderer(type);
1363
1375
  if (!renderer)
1364
1376
  return;
1365
- const forWrapper = (container.ownerDocument || document).createElement("div");
1366
- forWrapper.className = "slexkit-for-wrapper";
1367
- container.appendChild(forWrapper);
1377
+ const doc = container.ownerDocument || document;
1378
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1379
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1380
+ container.append(startAnchor, endAnchor);
1368
1381
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1369
1382
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1370
1383
  const $keyProp = props.$key;
@@ -1377,8 +1390,10 @@
1377
1390
  disposedSlots.add(slot);
1378
1391
  leavingSlots.delete(slot);
1379
1392
  callHook(g, name, "onUnmount");
1380
- disposeComponent(slot.el);
1381
- slot.el.remove();
1393
+ if (slot.el) {
1394
+ disposeComponent(slot.el);
1395
+ slot.el.remove();
1396
+ }
1382
1397
  if (slot.dispose)
1383
1398
  slot.dispose();
1384
1399
  };
@@ -1403,12 +1418,16 @@
1403
1418
  }
1404
1419
  }
1405
1420
  for (const slot of deletedSlots) {
1406
- container.appendChild(slot.el);
1407
1421
  leavingSlots.add(slot);
1422
+ if (!slot.el) {
1423
+ disposeSlot(slot);
1424
+ continue;
1425
+ }
1408
1426
  applyLeaveAnimation(slot.el, slot.props, () => {
1409
1427
  disposeSlot(slot);
1410
1428
  });
1411
1429
  }
1430
+ let cursor = startAnchor;
1412
1431
  arr.forEach((item, index) => {
1413
1432
  item = asReactiveValue(item, g);
1414
1433
  const keyVal = newKeys[index];
@@ -1436,20 +1455,22 @@
1436
1455
  const indexSignal = createSignal(index);
1437
1456
  const revisionSignal = createSignal(0);
1438
1457
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1439
- if (slot.el) {
1440
- applyEnterAnimation(slot.el, slot.props);
1441
- callHook(g, name, "onMount");
1458
+ if (!slot.el) {
1459
+ disposeSlot(slot);
1460
+ return;
1442
1461
  }
1462
+ applyEnterAnimation(slot.el, slot.props);
1463
+ callHook(g, name, "onMount");
1443
1464
  slotMap.set(keyVal, slot);
1444
1465
  }
1445
- const refChild = forWrapper.children[index];
1446
- if (slot.el && refChild !== slot.el) {
1447
- forWrapper.insertBefore(slot.el, refChild ?? null);
1466
+ const nextChild = cursor.nextSibling;
1467
+ if (slot.el && nextChild !== slot.el) {
1468
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1469
+ }
1470
+ if (slot.el) {
1471
+ cursor = slot.el;
1448
1472
  }
1449
1473
  });
1450
- while (forWrapper.children.length > arr.length) {
1451
- forWrapper.lastChild.remove();
1452
- }
1453
1474
  });
1454
1475
  onCleanup(() => {
1455
1476
  for (const slot of Array.from(slotMap.values()))
@@ -1457,7 +1478,8 @@
1457
1478
  slotMap.clear();
1458
1479
  for (const slot of Array.from(leavingSlots))
1459
1480
  disposeSlot(slot);
1460
- forWrapper.remove();
1481
+ startAnchor.remove();
1482
+ endAnchor.remove();
1461
1483
  });
1462
1484
  }
1463
1485
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1834,7 +1856,7 @@ ${parseSource}
1834
1856
  var parseSlexKitDsl = parseSlexSource;
1835
1857
 
1836
1858
  // src/version.ts
1837
- var SLEXKIT_VERSION = "0.3.1";
1859
+ var SLEXKIT_VERSION = "0.3.2";
1838
1860
  var SLEX_PROTOCOL_VERSION = "0.1";
1839
1861
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1840
1862
  function getSlexKitInfo() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slexkit",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Zero-build, Markdown-friendly reactive UI runtime for AI output.",
5
5
  "author": "SlexKit contributors",
6
6
  "type": "module",
@@ -32,52 +32,6 @@
32
32
  box-sizing: border-box;
33
33
  }
34
34
 
35
- body[data-mobile-nav-open] {
36
- overflow: hidden;
37
- overscroll-behavior: contain;
38
- }
39
-
40
- #mobileNav {
41
- --mobile-nav-backdrop-opacity: 0;
42
- --mobile-nav-panel-translate: -100%;
43
- pointer-events: none;
44
- }
45
-
46
- #mobileNav[data-open="true"] {
47
- --mobile-nav-backdrop-opacity: 1;
48
- --mobile-nav-panel-translate: 0px;
49
- pointer-events: auto;
50
- }
51
-
52
- #mobileNav [data-mobile-nav-backdrop] {
53
- opacity: var(--mobile-nav-backdrop-opacity);
54
- touch-action: pan-y;
55
- transition: opacity 180ms ease;
56
- }
57
-
58
- #mobileNav [data-mobile-nav-panel] {
59
- transform: translateX(var(--mobile-nav-panel-translate));
60
- touch-action: pan-y;
61
- transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
62
- will-change: transform;
63
- }
64
-
65
- #mobileNav[data-dragging="true"] [data-mobile-nav-backdrop],
66
- #mobileNav[data-dragging="true"] [data-mobile-nav-panel] {
67
- transition: none;
68
- }
69
-
70
- @media (prefers-reduced-motion: reduce) {
71
- #mobileNav [data-mobile-nav-backdrop],
72
- #mobileNav [data-mobile-nav-panel] {
73
- transition: none;
74
- }
75
- }
76
-
77
- .slexkit-for-wrapper {
78
- display: contents;
79
- }
80
-
81
35
  .slexkit-source-toolbar {
82
36
  display: flex;
83
37
  align-items: center;