diff --git a/.gitignore b/.gitignore index aa849760..048fd577 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ builds/**/meta .vscode/settings.json .playwright-mcp .playwright-cli +test-results/ +.claude/worktrees/ __screenshots__ diff --git a/tko.io/astro.config.mjs b/tko.io/astro.config.mjs index 4d1b8d30..26d303f6 100644 --- a/tko.io/astro.config.mjs +++ b/tko.io/astro.config.mjs @@ -29,6 +29,7 @@ export default defineConfig({ sidebar: [ { label: 'Introduction', slug: 'index' }, { label: 'Knockout 3 to 4 Guide', slug: '3to4' }, + { label: 'Examples', slug: 'examples' }, { label: 'Bindings', autogenerate: { directory: 'bindings' } }, { label: 'Observables', autogenerate: { directory: 'observables' } }, { label: 'Computed', autogenerate: { directory: 'computed' } }, diff --git a/tko.io/public/agent-guide.md b/tko.io/public/agent-guide.md index 3eee6cf9..9a512036 100644 --- a/tko.io/public/agent-guide.md +++ b/tko.io/public/agent-guide.md @@ -1,5 +1,7 @@ # TKO Agent Guide +For preferred state/binding/DOM architecture in examples and prototypes, read `/agents/contract.md`. + ## Setup ```html @@ -31,6 +33,8 @@ ko.when(() => viewModel.isReady() && viewModel.hasData()).then(() => console.log Activate with `ko.applyBindings(viewModel, element)`. +Looking up that mount element with `document.getElementById(...)`, `querySelector(...)`, or another host reference is normal. The important rule is that once bindings are active, observable state should remain the source of truth instead of ad-hoc DOM mutation. + Syntax: `data-bind="bindingName: expression"` on HTML elements. text, html, visible, hidden, css:{class:bool}, style:{prop:val}, attr:{name:val}, diff --git a/tko.io/public/agents/contract.md b/tko.io/public/agents/contract.md new file mode 100644 index 00000000..9e88e8a9 --- /dev/null +++ b/tko.io/public/agents/contract.md @@ -0,0 +1,93 @@ +# TKO Agent Contract + +Use this file when deciding how state, bindings, and DOM work should be divided in TKO examples and prototypes. + +## Core Rule + +- Keep app state in observables, observableArrays, and computeds. +- Let standard bindings and custom bindings carry that state to the DOM. +- Use DOM lookup only to find the mount root for `ko.applyBindings(viewModel, element)`. + +## Replace X With Y + +- This section is about replacing ad-hoc DOM/event/state handling with bindings, not about binding-syntax style. +- If you are about to do `element.textContent = value`, use the `text` binding. +- If you are about to do `element.innerHTML = markup`, first ask whether the content should be plain text instead; prefer the `text` binding by default. Use `html` only when rendering trusted HTML is truly the point. +- If you are about to manually create, replace, or reconcile a repeated set of child nodes, use `foreach`. +- If you are about to toggle classes with `classList`, use `css`. +- If you are about to set attributes manually, use `attr`. +- If you are about to set inline styles from state, use `style`. +- If you are about to wire ordinary UI events with imperative listeners, use `click`, `event:{...}`, `value`, `textInput`, `checked`, or related built-in bindings. +- If you are about to call `focus()` or manage focus from state, use `hasFocus` when it fits. +- If you are about to mirror user input into plain mutable objects, store that input in observables instead. +- If you are about to keep counters, highlights, or explanatory UI state outside observables, move them into observables/computeds so the example demonstrates TKO rather than bypassing it. + +## When Custom Bindings Are The Right Tool + +Use a custom `bindingHandler` when the work is inherently DOM-specific and does not belong in the state layer. + +Typical good fits: +- canvas drawing +- WebGL rendering +- SVG-specific effects +- animation +- resize / measurement +- focus orchestration when `hasFocus` is not enough +- third-party widget integration + +In those cases: +- let observables remain the source of truth +- let the custom binding read observables and update the DOM +- avoid making the custom binding the authoritative owner of app state + +## Security Preference + +- Prefer `text` over `html`. +- Treat `html` as an exception for trusted markup, not the default way to render content. +- If the content originates from users, external services, or mixed trust levels, do not pass it through `html` unless it has been explicitly sanitized for that purpose. + +## Mounting Is Allowed + +These are normal: + +```js +const root = document.getElementById('app') +ko.applyBindings(viewModel, root) +``` + +```js +const root = container.querySelector('[data-app-root]') +ko.applyBindings(viewModel, root) +``` + +The contract is not “never touch `document`”. +The contract is “do not let ad-hoc DOM mutation become your reactive state system”. + +## Binding Syntax Preference + +- `ko-*` and `data-bind` are both valid binding surfaces. +- The choice between them is primarily stylistic / authoring-oriented unless you specifically need classic provider-driven strings or are teaching the classic syntax directly. + +## Render Loops + +Render loops are acceptable when they belong to rendering: +- `requestAnimationFrame` +- canvas redraws +- WebGL frame submission +- resize observers + +Prefer this split: +- state object: observables + domain actions +- renderer / custom binding: DOM, canvas, WebGL, RAF + +## Tests + +Direct DOM reads are fine in tests and verification code. +Example: + +```js +ko.applyBindings(vm, document.getElementById('app')) +console.assert(document.querySelector('#app span').textContent === 'Hello') +``` + +That is verification code, not the app’s state/update architecture. diff --git a/tko.io/public/agents/guide.md b/tko.io/public/agents/guide.md index 693f5b05..d5f363d9 100644 --- a/tko.io/public/agents/guide.md +++ b/tko.io/public/agents/guide.md @@ -2,11 +2,13 @@ Test-backed behavior summaries live under `/agents/verified-behaviors/`. Treat those files as the contract layer when prose docs and implementation need reconciliation. +For preferred state/binding/DOM architecture in examples and prototypes, read `/agents/contract.md`. + ## Setup ```html - + ``` ## Observables @@ -49,6 +51,18 @@ Binding notes: ``` +## Example Discipline + +When the goal is to demonstrate TKO itself, keep the state flow inside observables, computeds, and bindings. + +- If you want a replacement-oriented checklist for DOM/state decisions, use `/agents/contract.md`. +- It is normal to look up a mount element with `document.getElementById(...)`, `querySelector(...)`, or another host-framework reference so you can call `ko.applyBindings(viewModel, element)`. +- Prefer `text`, `css`, `attr`, `event`, `foreach`, and `pureComputed` over manual DOM writes. +- Avoid driving visible state with `textContent`, `innerHTML`, `classList`, or ad-hoc `addEventListener` when bindings can express the same behavior. +- Use custom `bindingHandlers` only for DOM-specific effects that do not belong in the state layer, such as animation, focus, canvas, SVG, or third-party widget integration. +- If an example contrasts reactive models, the counters and highlighted state should also be observable-driven so the example demonstrates the pattern instead of bypassing it. +- The line to avoid is using the DOM itself as the mutable source of truth after bindings are active. + ## Classic data-bind parsing and CSP Classic `data-bind` parsing is provider-driven. Use `DataBindProvider` when you need binding strings, and combine it with other providers through `MultiProvider` as needed. @@ -211,6 +225,8 @@ tko.applyBindings({ removeTodo: t => todos.remove(t) }, root) Observable writes update DOM synchronously — assert immediately after setting: +Direct DOM reads are appropriate here because this is verification code, not the UI update path itself. + ```js const vm = { msg: ko.observable('Hello') } ko.applyBindings(vm, document.getElementById('app')) diff --git a/tko.io/public/examples/form-engine.html b/tko.io/public/examples/form-engine.html new file mode 100644 index 00000000..72db5264 --- /dev/null +++ b/tko.io/public/examples/form-engine.html @@ -0,0 +1,1195 @@ + + + + + + Form Engine + + + + +
+
+
+

Form Engine

+
+ A product-style form that keeps validation, dirty state, derived summaries, and async save flow in observables. + The visible UI is reactive, but the state remains simple. +
+
+ +
+
+
Dirty fields
+
+
changes since the last save
+
+
+
Validation errors
+
+
blocking save right now
+
+
+
Estimated monthly
+
+
derived from plan, seats, and add-ons
+
+
+
Completion
+
+
required fields in good shape
+
+
+
Save state
+
+
+
+
+ +
+
+
+
+
+

Account

+
Primary identity and contact details.
+
+
+
+

Organization

+
+
+ + +
Shown on invoices and the internal summary cards.
+
+
+
+ + +
The person responsible for the draft.
+
+
+
+
+ +
+

Contact

+
+
+ + +
Used for save confirmations and review notes.
+
+
+
+ + +
Separate from the primary contact if needed.
+
+
+
+
+
+
+ +
+
+

Project

+
Timeline, scope, and delivery pressure.
+
+
+
+

Timeline

+
+
+ + +
Used as the main label in the summary.
+
+
+
+
+ + +
+
+
+ + +
Raises the derived monthly estimate when urgent.
+
+
+
+
+
+ +
+

Scope

+
+
+ + +
Feeds the derived pricing summary.
+
+
+ +
+ + +
Affects the per-seat estimate.
+
+
+ +
+ + + +
+
+
+
+
+ +
+
+

Notes

+
Freeform context for reviewers and implementers.
+
+
+
+ + +
Optional, but useful when the form becomes a real intake flow.
+
+
+
+
+
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+
+ + +
+
+
+ + + + + diff --git a/tko.io/public/examples/honeycomb.html b/tko.io/public/examples/honeycomb.html new file mode 100644 index 00000000..3b22a442 --- /dev/null +++ b/tko.io/public/examples/honeycomb.html @@ -0,0 +1,619 @@ + + + + + + Honeycomb Locality + + + + +
+
+
+
+

Honeycomb

+
Counting Naive Virtual DOM Comparisons vs Observable Updates
+
+ +
+
Visible Hexes
+
+
cells currently on screen
+
+ +
+
DOM Comparisons · O(n)
+
+
hover: + all visible, click: + all visible × touched
+
+ +
+
Observable Notifications · O(1)
+
+
hover: + 6 neighbors, click: + touched cells
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + + + + diff --git a/tko.io/public/examples/odoom.html b/tko.io/public/examples/odoom.html new file mode 100644 index 00000000..1f844539 --- /dev/null +++ b/tko.io/public/examples/odoom.html @@ -0,0 +1,999 @@ + + + + + + odoom + + + + +
+
+ +
+
+ +
+
+
+

odoom

+
A tiny WebGL corridor crawler with an observable HUD.
+
+ +
+
+
Health
+
+
+
+
Ammo
+
+
+
+
Cells
+
+
+
+
Heading
+
+
+
+
FPS
+
+
+
+ +
+
+ +
+
+

Controls

+
+
+ + +
+
+ + +
+
+ + +
+
W/S move. A/D turn. Shift sprint. Space fires.
+
+
+ +
+

Minimap

+ +
Green cells are pickups. White shows the current facing direction.
+
+
+
+
+ + + + + diff --git a/tko.io/public/examples/signal-graph.html b/tko.io/public/examples/signal-graph.html new file mode 100644 index 00000000..e99282de --- /dev/null +++ b/tko.io/public/examples/signal-graph.html @@ -0,0 +1,1041 @@ + + + + + + Signal Graph + + + + + +
+
+
+

Signal Graph

+
+ A computed dependency map: mutate one input and only the downstream branch lights up. + The graph shows actual observable changes and computed recomputes, not generic UI noise. +
+
+ + + +
+
+ +
+
+
Focused input
+
+
the input observable you just changed
+
+ +
+
Downstream nodes
+
+
only nodes reachable from the focused input
+
+ +
+
Propagation hits
+
+
writes plus computed recomputes observed so far
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + diff --git a/tko.io/public/examples/spreadsheet.html b/tko.io/public/examples/spreadsheet.html new file mode 100644 index 00000000..d7e61798 --- /dev/null +++ b/tko.io/public/examples/spreadsheet.html @@ -0,0 +1,1290 @@ + + + + + + Spreadsheet + + + + +
+
+
+
+
+

Spreadsheet

+
+ One-shot by AI: cells are observables, formula cells are computeds, and one edit ripples through + the sheet. +
+
+
TKO dependency engine
+
+ +
+
+
Selected
+
+
+
+
Type
+
+
+
+
Value
+
+
+
+
Direct inputs
+
+
+
+ +
+
+
+
Formula Bar
+
+
+
+
+ +
+
Literal cells are observables. Formula cells are computeds that read other cells through the same reactive graph.
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ +
+ +
+ +
+
+
+ + + +
+
+
+
+
+ + +
+
+ + + + + diff --git a/tko.io/public/llms.txt b/tko.io/public/llms.txt index 596f51af..389c9e7e 100644 --- a/tko.io/public/llms.txt +++ b/tko.io/public/llms.txt @@ -16,6 +16,8 @@ - Verified Behaviors Index: /agents/verified-behaviors/index.md Unit-test-backed behavior contract. Prefer this when behavior questions matter. +- Agent Contract: /agents/contract.md + Preferred state/binding/DOM split for TKO examples and prototypes. - Agent Guide: /agents/guide.md API reference, gotchas, and examples. - Agent Testing: /agents/testing.md @@ -24,13 +26,18 @@ Minimal in-browser TSX + esbuild scaffold for rapid prototype work. - Glossary: /agents/glossary.md Domain-specific terms, concepts, and package reference. +- Examples: /examples/ + Interactive self-contained HTML examples that show update locality and TKO's observable model. ## Use This First - Behavior question or edge case: /agents/verified-behaviors/index.md +- State vs DOM architecture choice: /agents/contract.md - API usage or authoring pattern: /agents/guide.md - Verification or test flow: /agents/testing.md - Rapid prototype in-browser: /agents/sample-tsx.html +- Conceptual interactive examples: /examples/ + The examples are self-contained HTML files. ## Build Choice @@ -42,6 +49,7 @@ - Derived `ko-*` values must stay observable or computed. `ko-text={price() > 50 ? 'expensive' : 'cheap'}` freezes; use a computed. - `ko.applyBindings(...)` returns a `Promise`. - TKO connects observable state to the DOM; `bindingHandlers` are the DOM/state bridge. +- Looking up a mount element with `document.getElementById(...)` (or similar) in order to call `ko.applyBindings(viewModel, element)` is normal and expected. What to avoid is driving reactive UI state through ad-hoc DOM mutation once bindings are active. ## Package Routing @@ -54,8 +62,10 @@ - Verified Behaviors: /agents/verified-behaviors/index.md (package-scoped index) - Agent Guide: /agents/guide.md (API reference, gotchas, examples) +- Agent Contract: /agents/contract.md (preferred state/binding/DOM split for examples and prototypes) - Agent Testing: /agents/testing.md (how to run and verify TKO code) - Agent TSX Scaffold: /agents/sample-tsx.html (minimal browser TSX + esbuild scaffold for rapid prototype work) +- Examples: /examples/ (interactive self-contained HTML examples showing update locality and reactive behavior) - Playground: /playground - GitHub: https://github.com/knockout/tko @@ -66,6 +76,7 @@ - Bindings are the DOM integration layer: they read state, update DOM, and write user-driven changes back to state. - `bindingHandlers` are the bridge between the DOM and the observable state layer. - `ko.applyBindings(viewModel, element)` activates that bridge on an existing DOM subtree. +- It is acceptable to locate that mount subtree with `document.getElementById(...)`, `querySelector(...)`, or a framework-provided element reference before calling `ko.applyBindings(...)`. - In TSX, `tko.jsx.render()` creates DOM nodes and `ko.applyBindings({}, root)` then activates the `ko-*` bindings on that rendered DOM. ## Two Binding Syntaxes @@ -76,6 +87,7 @@ TSX: `ko-text={msg}` — compile-time JSX expressions, needs esbuild + `tko.jsx. Inside `ko-foreach` children, binding-context vars use strings: `ko-text="$data"` (not `{$data}`) See /agents/guide.md for usage patterns. +See /agents/contract.md for preferred state vs DOM architecture. Use /agents/verified-behaviors/index.md for test-backed behavior contracts. ## Docs diff --git a/tko.io/src/content/docs/examples/index.md b/tko.io/src/content/docs/examples/index.md new file mode 100644 index 00000000..09d982a0 --- /dev/null +++ b/tko.io/src/content/docs/examples/index.md @@ -0,0 +1,28 @@ +--- +title: Examples +description: Interactive examples that show where TKO differs from virtual DOM frameworks. +sidebar: + label: Overview + order: 0 +--- + +These examples are meant to make TKO's model visible. + +- They focus on observables, bindings, and update locality. +- They are intentionally interactive. +- They are self-contained HTML files you can inspect, copy, or adapt directly. +- They are designed to show what work is happening, not just what UI appears. + +## Available examples + +- [Honeycomb](/examples/honeycomb.html?view=example): Count naive virtual DOM comparisons against observable updates in a dense interactive field. +- [odoom](/examples/odoom.html?view=example): A tiny WebGL corridor crawler where the HUD, controls, and simulation state stay observable-driven. +- [Spreadsheet](/examples/spreadsheet.html?view=example): Cells as observables, formulas as computeds, and dependency propagation you can edit live. +- [Signal Graph](/examples/signal-graph.html?view=example): Mutate one input and watch only the downstream computed branch recompute. +- [Form Engine](/examples/form-engine.html?view=example): A realistic product form showing dirty state, validation, summaries, and save flow as reactive state. + +## Why this section exists + +TKO often feels different from virtual DOM frameworks because it connects observable state directly to DOM updates. + +That difference is easier to understand when you can interact with it and watch the cost accumulate. diff --git a/tko.io/src/styles/tko.css b/tko.io/src/styles/tko.css index 0973624c..10faa4c0 100644 --- a/tko.io/src/styles/tko.css +++ b/tko.io/src/styles/tko.css @@ -106,6 +106,8 @@ h1, .content-panel h1 { font-size: clamp(1.85rem, 4vw, 2.75rem); font-weight: 700; + margin: 0; + line-height: 1; } .content-panel h2 { @@ -190,6 +192,9 @@ header { .content-panel .sl-markdown-content > .sl-heading-wrapper:first-child { margin-top: 0; + display: flex; + align-items: center; + min-height: 100%; } .content-panel a {