Skip to content
Merged
72 changes: 72 additions & 0 deletions plans/legacy-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Plan: Write fresh docs for critical gaps

**Risk class:** LOW — new documentation pages only, no code changes
**Owner:** brianmhunt

## Context

TKO's Starlight docs cover bindings, observables, computed, components, and binding context thoroughly — but lack foundational pages that new users need: installation, API reference, browser support, utility functions, and data serialization. Legacy Knockout docs covered these topics but are outdated. We're writing fresh content informed by the legacy structure.

## New pages (5 files)

### 1. `tko.io/src/content/docs/getting-started/index.md` — Installation & Setup
- Sidebar: `label: Overview, order: 0`
- CDN (ES module + classic script) for `build.reference`
- Package manager install (npm/bun/pnpm/yarn tabs)
- `build.knockout` as migration option with link to `/3to4/`
- First binding example (standalone HTML)
- TypeScript setup notes
- Landing page quick start stays as-is (quick taste vs full guide)

### 2. `tko.io/src/content/docs/getting-started/browser-support.md` — Browser Support
- Modern browser engine coverage (Chromium, WebKit, Gecko)
- ES module support requirements
- Classic script fallback for older environments
- How TKO is tested (Vitest + Playwright, 3 engines in CI)

### 3. `tko.io/src/content/docs/observables/utilities.md` — Utility Functions
- `ko.toJS` / `ko.toJSON` serialization
- `ko.unwrap` / `ko.isObservable` / `ko.isWritableObservable` / `ko.isComputed`
- `.fn` extensibility (`ko.observable.fn`, `ko.observableArray.fn`, etc.)
- Type hierarchy: subscribable → observable → observableArray/computed
- Working examples

### 4. `tko.io/src/content/docs/observables/json-data.md` — Loading & Saving Data
- Serializing view models with `ko.toJS` / `ko.toJSON`
- Loading data into observables (manual assignment patterns)
- Fetch API examples (replacing legacy jQuery patterns)
- Debugging: rendering JSON in the UI

### 5. `tko.io/src/content/docs/api.md` — API Reference
- Root-level page (add to sidebar config)
- Index/lookup table format: function name, one-liner, link to detailed page
- Organized by category: Observables, Computed, Components, Bindings, Utilities
- No full signatures — just names + links for quick navigation

## Sidebar config update

Edit `tko.io/astro.config.mjs` to add:
```js
{ label: 'Getting Started', autogenerate: { directory: 'getting-started' } },
// ... existing sections ...
{ label: 'API Reference', slug: 'api' },
```

Place "Getting Started" right after Introduction. Place "API Reference" before "Knockout 3 → 4 Guide".

## Conventions to follow
- Frontmatter: `title` required, `description` + `sidebar` on index pages
- Filenames: kebab-case `.md`
- Code examples: use `<Tabs>` from `@astrojs/starlight/components` for multi-variant snippets (`.mdx` extension when using imports)
- Link to existing docs pages with relative paths
- Keep pages concise — reference detailed pages rather than duplicating

## Files to modify
- `tko.io/astro.config.mjs` — sidebar config
- 5 new files as listed above

## Verification
1. Run `bun run dev` in `tko.io/` and check all new pages render
2. Verify sidebar navigation order is correct
3. Check all internal links resolve
4. Review on mobile viewport
17 changes: 10 additions & 7 deletions tko.io/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ export default defineConfig({
},
sidebar: [
{ label: 'Introduction', slug: 'index' },
{ label: 'Getting Started', autogenerate: { directory: 'getting-started' } },
{ label: 'Examples', slug: 'examples' },
{ label: 'Bindings', autogenerate: { directory: 'bindings' } },
{ label: 'Observables', autogenerate: { directory: 'observables' } },
{ label: 'Computed', autogenerate: { directory: 'computed' } },
{ label: 'Components', autogenerate: { directory: 'components' } },
{ label: 'Binding Context', autogenerate: { directory: 'binding-context' } },
{ label: 'Advanced', autogenerate: { directory: 'advanced' } },
{ label: 'Knockout 3 → 4 Guide', slug: '3to4' }
{ label: 'Bindings', collapsed: true, autogenerate: { directory: 'bindings' } },
{ label: 'Observables', collapsed: true, autogenerate: { directory: 'observables' } },
{ label: 'Computed', collapsed: true, autogenerate: { directory: 'computed' } },
{ label: 'Components', collapsed: true, autogenerate: { directory: 'components' } },
{ label: 'Binding Context', collapsed: true, autogenerate: { directory: 'binding-context' } },
{ label: 'Advanced', collapsed: true, autogenerate: { directory: 'advanced' } },
{ label: 'API Reference', slug: 'api' },
{ label: 'Knockout 3 → 4 Guide', slug: '3to4' },
{ label: 'History', slug: 'history' }
]
})
]
Expand Down
123 changes: 123 additions & 0 deletions tko.io/src/content/docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: API Reference
description: Quick-lookup index of TKO's public API.
Comment on lines +1 to +3
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update agent docs when adding new API/doc pages

Per /workspace/tko/AGENTS.md, documentation updates involving new APIs/patterns must update both Starlight docs and the agent guide (tko.io/public/agents/guide.md). This commit adds major new pages (Getting Started, API Reference, utilities/data patterns, history) but does not update the agent-facing guide, which leaves agent documentation stale and out of sync with the new canonical docs.

Useful? React with 👍 / 👎.

---

# API Reference

Quick-lookup table for TKO's public API. Each entry links to its detailed documentation.

## Observables

| Function | Description |
|----------|-------------|
| `ko.observable(value?)` | Create a reactive value. [Docs](/observables/) |
| `ko.observableArray(array?)` | Observable wrapper around an array with mutation methods. [Docs](/observables/observablearrays/) |
| `ko.isObservable(value)` | Check if a value is an observable. [Docs](/observables/utilities/) |
| `ko.isObservableArray(value)` | Check if a value is an observable array. [Docs](/observables/utilities/) |
| `ko.isWritableObservable(value)` | Check if a value is a writable observable. [Docs](/observables/utilities/) |
| `ko.isSubscribable(value)` | Check if a value is any subscribable type. [Docs](/observables/utilities/) |
| `ko.peek(value)` | Read an observable's value without creating a dependency. |
| `ko.unwrap(value)` | Read an observable's value, or return a plain value as-is. [Docs](/observables/utilities/) |
| `ko.toJS(object)` | Clone an object tree, replacing observables with their values. [Docs](/observables/utilities/) |
| `ko.toJSON(object, replacer?, space?)` | `ko.toJS` + `JSON.stringify`. [Docs](/observables/utilities/) |

## Computed

| Function | Description |
|----------|-------------|
| `ko.computed(evaluatorOrOptions, owner?, options?)` | Create a value that depends on other observables. Accepts a function or `{ read, write }` options object. [Docs](/computed/computedobservables/) |
| `ko.pureComputed(evaluator, owner?)` | Computed that sleeps when it has no subscribers. [Docs](/computed/computed-pure/) |
| `ko.isComputed(value)` | Check if a value is a computed observable. [Docs](/observables/utilities/) |
| `ko.isPureComputed(value)` | Check if a value is a pure computed. |
| `ko.ignoreDependencies(callback, owner?, args?)` | Run a function without tracking dependencies. |
| `ko.when(predicate, callback?, context?)` | Resolve when the predicate becomes truthy. Returns a promise, or calls callback if provided. [Docs](/observables/) |

## Subscribable instance methods

Every observable, computed, and observable array inherits these from `subscribable.fn`:

| Method | Description |
|--------|-------------|
| `.subscribe(callback, target?, event?)` | Register a callback for changes. [Docs](/observables/) |
| `.when(testOrValue, returnValue?)` | Promise that resolves when the test passes. [Docs](/observables/) |
| `.yet(testOrValue, returnValue?)` | Promise that resolves when the test *fails* (negated `.when`). [Docs](/observables/) |
| `.next()` | Promise that resolves on the next value change. [Docs](/observables/) |
| `.once(callback)` | Call the callback on the next change, then auto-dispose. [Docs](/observables/) |
| `.peek()` | Read the current value without creating a dependency. |
| `.dispose()` | Tear down all subscriptions. |

## Extenders

| Function | Description |
|----------|-------------|
| `observable.extend(extenders)` | Apply extenders to an observable or computed. [Docs](/observables/extenders/) |
| `rateLimit` | Throttle change notifications. [Docs](/observables/ratelimit-observable/) |
| `notify: 'always'` | Force notification even when value hasn't changed. [Docs](/observables/) |

## Bindings

| Function | Description |
|----------|-------------|
| `ko.applyBindings(viewModel, rootNode?)` | Activate bindings on a DOM subtree. [Docs](/observables/#activating-knockout) |
| `ko.applyBindingsToNode(node, bindings, viewModelOrContext?)` | Apply bindings to a single node programmatically. |
| `ko.applyBindingsToDescendants(viewModelOrContext, rootNode)` | Apply bindings to descendants only (used in custom bindings). [Docs](/binding-context/custom-bindings-controlling-descendant-bindings/) |
| `ko.contextFor(node)` | Get the binding context for a DOM node. |
| `ko.dataFor(node)` | Get the view model bound to a DOM node. |
| `ko.cleanNode(node)` | Remove all TKO data and bindings from a node. |
| `ko.bindingHandlers` | Registry of built-in and custom binding handlers. [Docs](/binding-context/custom-bindings/) |
| `ko.bindingEvent` | Binding lifecycle event constants (e.g., `childrenComplete`, `descendantsComplete`). |
| `ko.BindingHandler` | Base class for class-based custom binding handlers. [Docs](/binding-context/custom-bindings/) |
| `ko.AsyncBindingHandler` | Async variant of `BindingHandler` for bindings that load resources. [Docs](/binding-context/custom-bindings/) |

### Built-in bindings

**Text & HTML:** [`text`](/bindings/text-binding/), [`html`](/bindings/html-binding/), [`textInput`](/bindings/textinput-binding/), [`value`](/bindings/value-binding/)

**Appearance:** [`visible`](/bindings/visible-binding/), `hidden`, [`css`](/bindings/css-binding/) (alias: `class`), [`style`](/bindings/style-binding/), [`attr`](/bindings/attr-binding/)

**Control flow:** [`if`](/bindings/if-binding/), [`ifnot`](/bindings/ifnot-binding/) (alias: `unless`), `else`, `elseif`, [`foreach`](/bindings/foreach-binding/) (alias: `each`), [`with`](/bindings/with-binding/), [`template`](/bindings/template-binding/)

**Context:** `let`, `using`

**Form:** [`click`](/bindings/click-binding/), [`event`](/bindings/event-binding/) (alias: `on`), [`submit`](/bindings/submit-binding/), [`enable`](/bindings/enable-binding/), [`disable`](/bindings/disable-binding/), [`checked`](/bindings/checked-binding/), `checkedValue`, [`options`](/bindings/options-binding/), [`selectedOptions`](/bindings/selectedoptions-binding/), [`hasfocus`](/bindings/hasfocus-binding/), [`uniqueName`](/bindings/uniquename-binding/)

**Components:** `component`, `slot`

**Lifecycle:** `descendantsComplete`

## Components

| Function | Description |
|----------|-------------|
| `ko.components.register(name, config)` | Register a component. [Docs](/components/component-registration/) |
| `ko.components.get(name)` | Retrieve a registered component definition. [Docs](/components/component-loaders/) |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document ko.components.get with its required callback

This API row omits the callback parameter, but ko.components.get is callback-based (ko.components.get(name, callback)), and the implementation invokes callback(...) unconditionally (packages/utils.component/src/registry.ts). Readers following this signature will call it with only name and hit runtime errors or unresolved behavior, so the reference should show the actual callable form.

Useful? React with 👍 / 👎.

| `ko.components.isRegistered(name)` | Check if a component name is registered. |
| `ko.components.unregister(name)` | Remove a component registration. |
| `ko.components.clearCachedDefinition(name)` | Clear a cached component definition. |
| `ko.Component` | Base class for class-based components (`ComponentABC`). [Docs](/components/) |

## JSX (build.reference only)

| Function | Description |
|----------|-------------|
| `ko.jsx.createElement(tag, props, ...children)` | Create a JSX element. Used as the JSX factory. |
| `ko.jsx.Fragment` | Fragment component for grouping elements without a wrapper node. |
| `ko.jsx.render(jsx)` | Render JSX to DOM nodes. Returns `{ node, dispose }`. |

## DOM disposal

| Function | Description |
|----------|-------------|
| `ko.domNodeDisposal.addDisposeCallback(node, callback)` | Run a callback when a DOM node is removed by TKO. |
| `ko.domNodeDisposal.removeDisposeCallback(node, callback)` | Remove a previously registered disposal callback. |

## Extensibility

| Function | Description |
|----------|-------------|
| `ko.subscribable.fn` | Prototype for all subscribables. [Docs](/observables/utilities/) |
| `ko.observable.fn` | Prototype for all observables — add methods here. [Docs](/observables/utilities/) |
| `ko.observableArray.fn` | Prototype for observable arrays (`remove`, `replace`, etc.). [Docs](/observables/utilities/) |
| `ko.computed.fn` | Prototype for all computeds. [Docs](/observables/utilities/) |
Comment on lines +1 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sync tko.io/public/agents/ docs with these new API/behavior pages.

This PR adds substantial API and behavioral documentation, but the provided changes do not include corresponding agent-guide updates. That creates human/agent documentation drift.

If helpful, I can draft the matching updates for tko.io/public/agents/guide.md, tko.io/public/agents/contract.md, and tko.io/public/agents/testing.md in the same style.
As per coding guidelines tko.io/**/*.{md,mdx}: “Update both Starlight docs (for humans) and agent guides in tko.io/public/agents/ (for AI agents) when documentation changes for new APIs, bindings, or behavioral changes”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tko.io/src/content/docs/api.md` around lines 1 - 74, The API docs add/changed
many public symbols (e.g., ko.observable, ko.observableArray, ko.unwrap,
ko.toJS, ko.computed, ko.pureComputed, ko.when, observable.extend and extenders
like rateLimit/notify:'always', ko.applyBindings, ko.applyBindingsToNode,
ko.bindingHandlers, ko.components.register/get/isRegistered/unregister and
binding names like text/html/value/foreach/if/visible/css/style/attr) but the
agent-facing guidance wasn't updated; update the agent guide files (guide.md,
contract.md, testing.md in the repo's agent docs) to mirror these new/changed
APIs and behaviors: enumerate each new API entry and its expected behavior and
surface-level usage examples for agents, update component/ binding lists and
extenders and their semantics, and add testable contracts and example test
prompts for agents to validate behavior (e.g., verifying
ko.observable/ko.computed reactivity, ko.applyBindings usage, component
registration/loading, and extenders like rateLimit/notify).

| `ko.tasks` | Microtask scheduler for batching async work. |
46 changes: 46 additions & 0 deletions tko.io/src/content/docs/getting-started/browser-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Browser Support
---

TKO targets modern browsers — any browser that supports ES2020 and `<script type="module">`.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opening line states ES module support requires “ES2020”, but the page also recommends the IIFE build for non-module environments. It would be clearer to distinguish the requirements: the ESM/CDN import path may require ES2020+ features, while the dist/browser*.js IIFE build is transpiled to an older target (built with esbuild --target=es6).

Suggested change
TKO targets modern browsers — any browser that supports ES2020 and `<script type="module">`.
TKO's recommended ES module build targets modern browsers that support ES2020 and `<script type="module">`. For older non-module environments, the `dist/browser*.js` IIFE build is also available and is transpiled to ES6.

Copilot uses AI. Check for mistakes.

## Supported engines

| Engine | Browsers | Tested in CI |
|--------|----------|:---:|
| Chromium | Chrome, Edge, Opera, Brave, Arc | Yes |
| WebKit | Safari (macOS, iOS) | Yes |
| Gecko | Firefox | Yes |

These three engines cover effectively all modern browsers.

## What we test

Every pull request runs the full test suite (2700+ tests) across all three engines using [Vitest](https://vitest.dev) browser mode with [Playwright](https://playwright.dev). The three engines run as parallel CI jobs.

CI tests against the **latest stable** version of each engine. We do not currently test against specific older browser versions, so minimum version support is not precisely known. If you discover a compatibility issue with a particular browser version, please [open an issue](https://github.com/knockout/tko/issues).

The original Knockout supported browsers back to IE6. TKO's modernized codebase uses ES2020+ features (optional chaining, nullish coalescing, etc.), so IE is no longer supported. Exact minimum versions for Chrome, Safari, and Firefox have not been established.

## Loading TKO

The recommended way to load TKO is as an ES module:

```html
<script type="module">
import ko from 'https://esm.run/@tko/build.reference'
</script>
```

An IIFE build is also available for classic `<script>` tag loading:

```html
<script src="https://cdn.jsdelivr.net/npm/@tko/build.reference/dist/browser.min.js"></script>
<script>
const ko = globalThis.tko
</script>
```

## Server-side / Node.js

TKO's observable and computed primitives have no DOM dependencies and can run in Node.js, Bun, or Deno. Binding and template features require a DOM environment.
111 changes: 111 additions & 0 deletions tko.io/src/content/docs/getting-started/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: Deploy in Seconds
description: Deploy a TKO app to static hosting — no build step required.
---

A TKO app can be as simple as a single HTML file. No bundler, no server runtime, no build step — just upload to any static hosting provider and you're live.

## The simplest deploy

Save this as `index.html`:

```html
<!doctype html>
<html>
<body>
<div id="app">
<input data-bind="textInput: name" />
<p>Hello, <strong data-bind="text: name"></strong>.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/@tko/build.reference/dist/browser.min.js"></script>
<script>
const ko = globalThis.tko
ko.applyBindings({ name: ko.observable('TKO') }, document.getElementById('app'))
</script>
</body>
</html>
```

Or as an ES module (no IIFE needed):

```html
<script type="module">
import ko from 'https://esm.run/@tko/build.reference'
ko.applyBindings({ name: ko.observable('TKO') }, document.getElementById('app'))
</script>
```

Upload that single file to any of the platforms below. That's it — a live, reactive web UI.

## GitHub Pages

Free, deploys from a git push.

1. Create a repo and add your `index.html`
2. Go to **Settings → Pages → Source** and select your branch
3. Your site is live at `https://username.github.io/repo/`

Or use the `gh` CLI:

```sh
gh repo create my-app --public --clone
# add index.html
git add index.html && git commit -m "init" && git push
gh browse --settings # enable Pages under Settings → Pages
```

## Cloudflare Pages

Free tier, global CDN, automatic HTTPS.

1. Push your files to a GitHub or GitLab repo
2. Connect the repo at [dash.cloudflare.com](https://dash.cloudflare.com) → **Pages → Create**
3. No build command needed — just set the output directory to `/` (or wherever your HTML lives)

Or deploy directly from the CLI:

```sh
npx wrangler pages deploy . --project-name my-app
```

## Google Cloud Storage

Good for projects already on GCP.

```sh
gcloud storage buckets create gs://my-app.example.com
gcloud storage buckets update gs://my-app.example.com --web-main-page-suffix=index.html
gcloud storage cp index.html gs://my-app.example.com/
```

Add a load balancer or use [Firebase Hosting](https://firebase.google.com/docs/hosting) for automatic HTTPS and CDN.

## Firebase Hosting

Free tier, automatic HTTPS, global CDN.

```sh
npm install -g firebase-tools
firebase init hosting # select your project, set public dir to "."
firebase deploy
```

## Netlify

Free tier, drag-and-drop or git-based deploys.

1. Go to [app.netlify.com/drop](https://app.netlify.com/drop)
2. Drag your project folder onto the page
3. Live in seconds

Or via CLI:

```sh
npx netlify-cli deploy --dir . --prod
```

## Why this works

TKO loads from a CDN (`esm.run` or `jsdelivr`). Your app is just HTML + the browser's ES module loader. No server-side rendering, no Node.js, no build artifacts. The entire deploy is one file.

As your app grows you can add a bundler, but you don't *have* to. Many production TKO apps are a handful of HTML files and a CSS stylesheet.
Loading
Loading