diff --git a/plans/legacy-docs.md b/plans/legacy-docs.md new file mode 100644 index 00000000..19256e83 --- /dev/null +++ b/plans/legacy-docs.md @@ -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 `` 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 diff --git a/tko.io/astro.config.mjs b/tko.io/astro.config.mjs index 69bfbbc4..efadbf40 100644 --- a/tko.io/astro.config.mjs +++ b/tko.io/astro.config.mjs @@ -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' } ] }) ] diff --git a/tko.io/src/content/docs/api.md b/tko.io/src/content/docs/api.md new file mode 100644 index 00000000..d1c63cf3 --- /dev/null +++ b/tko.io/src/content/docs/api.md @@ -0,0 +1,123 @@ +--- +title: API Reference +description: Quick-lookup index of TKO's public API. +--- + +# 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/) | +| `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/) | +| `ko.tasks` | Microtask scheduler for batching async work. | diff --git a/tko.io/src/content/docs/getting-started/browser-support.md b/tko.io/src/content/docs/getting-started/browser-support.md new file mode 100644 index 00000000..2b675be4 --- /dev/null +++ b/tko.io/src/content/docs/getting-started/browser-support.md @@ -0,0 +1,46 @@ +--- +title: Browser Support +--- + +TKO targets modern browsers — any browser that supports ES2020 and ` +``` + +An IIFE build is also available for classic ` + +``` + +## 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. diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md new file mode 100644 index 00000000..fe74a156 --- /dev/null +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -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 + + + +
+ +

Hello, .

+
+ + + + +``` + +Or as an ES module (no IIFE needed): + +```html + +``` + +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. diff --git a/tko.io/src/content/docs/getting-started/index.mdx b/tko.io/src/content/docs/getting-started/index.mdx new file mode 100644 index 00000000..81c6ff3a --- /dev/null +++ b/tko.io/src/content/docs/getting-started/index.mdx @@ -0,0 +1,119 @@ +--- +title: Installation +description: How to add TKO to your project — CDN, ES module, or package manager. +sidebar: + label: Overview + order: 0 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +TKO ships two builds. Pick the one that fits your project: + +| Build | When to use | +|-------|-------------| +| `@tko/build.reference` | **New projects.** Modern features: TSX, `ko-*` attributes, native provider, strict equality. | +| `@tko/build.knockout` | **Migrating from Knockout 3.x.** Drop-in compatible. See the [migration guide](/3to4/). | + +The examples below use `build.reference`. Swap in `build.knockout` if you need Knockout compatibility. + +## CDN + +The fastest way to try TKO — no build step required. + + + + ```html + + ``` + + + ```html + + + ``` + + + +## Package manager + + + + ```sh + npm install @tko/build.reference + ``` + + + ```sh + bun add @tko/build.reference + ``` + + + ```sh + pnpm add @tko/build.reference + ``` + + + ```sh + yarn add @tko/build.reference + ``` + + + +Then import in your code: + +```js +import ko from '@tko/build.reference' +``` + +## First binding + +Here's a complete, self-contained example you can save as an HTML file and open in a browser: + +```html + + + +
+ +

Hello, .

+
+ + + + + +``` + +`ko.applyBindings` connects a view model to a DOM subtree. The `textInput` binding keeps the input in sync with the `name` observable, and the `text` binding displays its current value. + +## TypeScript + +TKO is written in TypeScript. Types are included — no separate `@types` package needed. + +```ts +import ko from '@tko/build.reference' + +const name = ko.observable('TKO') // inferred as Observable +``` + +## What to read next + +- [Bindings](/bindings/) — the full list of built-in bindings +- [Observables](/observables/) — how reactive state works +- [Examples](/examples/) — interactive demos diff --git a/tko.io/src/content/docs/history.md b/tko.io/src/content/docs/history.md new file mode 100644 index 00000000..11e4b066 --- /dev/null +++ b/tko.io/src/content/docs/history.md @@ -0,0 +1,75 @@ +--- +title: History +description: How Knockout became TKO — from Microsoft's Big Four framework to a modern observable UI library. +--- + +TKO is the modern successor to Knockout.js, the observable data-binding library created by Steve Sanderson in 2010. + +## Origins (2010) + +Steve Sanderson created Knockout independently, and the project's success led to him joining Microsoft's ASP.NET team. The core idea — observable values that automatically update the DOM when they change — was borrowed from the MVVM pattern that .NET developers already knew from WPF and Silverlight. + +The first release shipped in July 2010. By late that year, Scott Hanselman was interviewing Sanderson about it on Hanselminutes, and Microsoft began featuring it in official ASP.NET documentation. + +## The Big Four era (2011-2014) + +From 2011 to 2014, Knockout was one of the four dominant JavaScript frameworks alongside Backbone.js, AngularJS, and Ember.js. Every framework comparison article and conference talk of that era included all four. + +[ThoughtWorks placed Knockout in their Technology Radar's "Trial" ring](https://www.thoughtworks.com/radar/languages-and-frameworks/angularjs-and-knockout) in October 2012, calling it one of the "front-runners" for building single-page applications. + +Knockout [peaked in Stack Overflow question volume](https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/) around 2013-2014, and had the highest answer rate (69.6%) of the Big Four — a sign of a mature, well-understood library. + +## Microsoft adoption + +Microsoft adopted Knockout extensively: + +- **Azure Portal** — [described at VS Live 2016](https://learn.microsoft.com/en-us/shows/visual-studio-visual-studio-live-redmond-2016/t15) as "the largest single-page application in the world," with 500+ developers building on Knockout and TypeScript +- **ASP.NET SPA template** — Visual Studio 2013 shipped with Knockout as the default JavaScript framework for new SPA projects +- **Hot Towel** — John Papa's widely-adopted SPA template combined Knockout with Durandal, Breeze, and Bootstrap +- **Office Add-ins** — Knockout was a supported framework for building Excel, Outlook, and Word add-ins + +The MVVM pattern felt natural to .NET developers, and the official template support made Knockout the de facto standard for .NET shops doing client-side work from roughly 2012 to 2015. + +## Enterprise adoption + +Beyond Microsoft, Knockout found deep roots in enterprise: + +- **Magento 2 (Adobe Commerce)** — chose Knockout as its frontend binding framework. Every Magento 2 e-commerce site runs Knockout. +- **Oracle JET** — Oracle built their entire enterprise JavaScript UI toolkit on top of Knockout rather than creating their own data-binding layer. +- **Financial services and government** — banks, insurance companies, and government agencies adopted Knockout for compliance-critical applications that are still running today. + +Notable adopters included Stack Overflow, Ancestry.com, BMW, TD Ameritrade, Royal Mail, Transport for London, and Bandcamp. Knockout accumulated over [36 million NuGet downloads](https://www.nuget.org/packages/knockoutjs/). + +## The React shift (2015-2016) + +React changed the framework landscape starting around 2015. Unlike AngularJS — which had Google's continued investment and a full rewrite as Angular 2 — Knockout never received that level of sustained corporate backing for evolution. + +The library remained stable and reliable. Applications built on it continued to work. But new projects increasingly chose React, Vue, or Angular 2+, and Knockout gradually fell out of the top-tier framework conversation. + +By 2019, the last Knockout 3.x release (v3.5.1) shipped. The library wasn't broken — it just wasn't evolving. Steve Sanderson himself noted that maintaining a framework like Knockout requires a team, and without that sustained investment, the project stalled. + +## TKO (2016-present) + +TKO started as a monorepo reorganization of Knockout's codebase, breaking the monolithic library into 27 modular packages. The goals: + +- **Modernize the tooling** — TypeScript source, ES modules, Bun, Vitest, Biome +- **Keep backwards compatibility** — existing Knockout applications can migrate by swapping a script tag +- **Add modern features** — TSX support, `ko-*` attribute bindings, native providers, strict equality +- **Enable AI-assisted development** — comprehensive test coverage, machine-readable documentation, and tooling designed for autonomous maintenance. The "team" that Sanderson said a framework needs may no longer require humans for every role. + +## Why TKO in 2026 + +The frameworks that replaced Knockout optimized for large teams — elaborate build pipelines, complex state management, steep learning curves justified by organizational coordination. That tradeoff made sense when shipping required dozens of developers. + +AI changes the equation. When an AI agent can read, test, modify, and ship code autonomously, the bottleneck shifts from team coordination to iteration speed. The winning framework is the one with the lowest friction: fastest path from idea to working UI, instantly testable, trivially verifiable, easy to modify. + +TKO fits this model: + +- **Zero build step** — a single HTML file is a complete application +- **Instant feedback** — change an observable, the DOM updates. No reconciliation, no diffing +- **Small surface area** — 27 focused packages, each simple enough for an AI to fully understand +- **Fast verification** — 2700+ tests across three browser engines catch regressions instantly + +This is the [dark factory](https://github.com/knockout/tko/blob/main/plans/dark-factory.md) thesis: tooling, tests, and documentation robust enough that AI agents can handle routine maintenance autonomously. The "team" that every framework needs doesn't have to be all-human anymore. + +Over 70,000 websites still run Knockout today. TKO gives them — and a new generation of AI-assisted developers — a path forward. diff --git a/tko.io/src/content/docs/index.mdx b/tko.io/src/content/docs/index.mdx index 70d85db8..b8a3c8b0 100644 --- a/tko.io/src/content/docs/index.mdx +++ b/tko.io/src/content/docs/index.mdx @@ -5,12 +5,14 @@ description: Modern Knockout — reactive data binding and UI templating with ze import { Tabs, TabItem } from '@astrojs/starlight/components'; +TKO v4.0.1 is here → +
-

TKO v4.0.1

-

Modern Knockout, clarified

-

Reactive data binding and UI templating with zero runtime dependencies. Start with the package you need and move from overview to working bindings.

+

TKO

+

Battle-tested since 2010

+

Observable data binding. Zero dependencies, zero ceremony. Ship a dynamic web UI in seconds.

diff --git a/tko.io/src/content/docs/observables/json-data.md b/tko.io/src/content/docs/observables/json-data.md new file mode 100644 index 00000000..19ec9c76 --- /dev/null +++ b/tko.io/src/content/docs/observables/json-data.md @@ -0,0 +1,88 @@ +--- +title: Loading & Saving Data +--- + +# Loading and saving data + +TKO doesn't prescribe how you load or save data — use `fetch`, a library, or whatever your server expects. This page covers the patterns for moving data between your server and your observable view models. + +## Serializing a view model + +Use `ko.toJS` to strip observables from a view model before sending it to a server: + +```js +const vm = { + firstName: ko.observable('Jane'), + lastName: ko.observable('Doe'), + tags: ko.observableArray(['admin', 'active']) +} + +const save = async () => { + const data = ko.toJS(vm) + // data is { firstName: 'Jane', lastName: 'Doe', tags: ['admin', 'active'] } + + await fetch('/api/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) +} +``` + +Or use `ko.toJSON` as a shorthand for `JSON.stringify(ko.toJS(...))`: + +```js +await fetch('/api/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: ko.toJSON(vm) +}) +``` + +## Loading data into observables + +When you receive data from a server, update each observable individually: + +```js +const load = async () => { + const response = await fetch('/api/user') + const data = await response.json() + + vm.firstName(data.firstName) + vm.lastName(data.lastName) + vm.tags(data.tags) +} +``` + +For larger models, a helper keeps things tidy: + +```js +const updateFrom = (viewModel, data) => { + for (const [key, value] of Object.entries(data)) { + if (ko.isWritableObservable(viewModel[key])) { + viewModel[key](value) + } + } +} + +// Usage +const load = async () => { + const data = await fetch('/api/user').then(r => r.json()) + updateFrom(vm, data) +} +``` + +## Debugging: showing JSON in the UI + +Bind `ko.toJSON` directly to see the live state of your view model: + +```html +

+```
+
+This updates in real time as observables change — useful during development.
+
+## See also
+
+- [Utility Functions](./utilities/) — `ko.toJS`, `ko.toJSON`, and type-checking helpers
+- [Observables](/observables/) — how reactive state works
diff --git a/tko.io/src/content/docs/observables/utilities.md b/tko.io/src/content/docs/observables/utilities.md
new file mode 100644
index 00000000..f5adcbfb
--- /dev/null
+++ b/tko.io/src/content/docs/observables/utilities.md
@@ -0,0 +1,96 @@
+---
+title: Utility Functions
+---
+
+# Utility functions
+
+TKO provides helper functions for inspecting and converting observables.
+
+## Type checking
+
+| Function | Returns `true` when |
+|----------|-------------------|
+| `ko.isObservable(value)` | The value is an observable, observable array, or computed |
+| `ko.isObservableArray(value)` | The value is an observable array |
+| `ko.isWritableObservable(value)` | The value is a writable observable (not a read-only computed) |
+| `ko.isComputed(value)` | The value is a computed observable |
+| `ko.isSubscribable(value)` | The value is any subscribable (observable, computed, or raw subscribable) |
+
+```js
+const name = ko.observable('TKO')
+const tags = ko.observableArray(['fast'])
+const upper = ko.computed(() => name().toUpperCase())
+
+ko.isObservable(name)           // true
+ko.isObservable(upper)          // true
+ko.isObservable('plain')        // false
+ko.isObservableArray(tags)      // true
+ko.isObservableArray(name)      // false
+ko.isWritableObservable(name)   // true
+ko.isWritableObservable(upper)  // false
+ko.isComputed(upper)            // true
+```
+
+## Unwrapping
+
+`ko.unwrap(value)` reads the value if it's an observable, or returns it as-is if it isn't. Useful when a function accepts either an observable or a plain value:
+
+```js
+ko.unwrap(ko.observable(42))  // 42
+ko.unwrap(42)                 // 42
+```
+
+This is commonly used inside custom bindings where a parameter could be either type.
+
+## Serialization
+
+`ko.toJS` and `ko.toJSON` convert an object graph containing observables into plain data.
+
+`ko.toJS(viewModel)` — clones the object tree, replacing every observable with its current value:
+
+```js
+const vm = {
+  name: ko.observable('TKO'),
+  tags: ko.observableArray(['fast', 'reactive']),
+  nested: {
+    count: ko.observable(3)
+  }
+}
+
+ko.toJS(vm)
+// { name: 'TKO', tags: ['fast', 'reactive'], nested: { count: 3 } }
+```
+
+`ko.toJSON(viewModel, replacer?, space?)` — same as `ko.toJS` followed by `JSON.stringify`. Accepts the same optional `replacer` and `space` arguments as `JSON.stringify`:
+
+```js
+ko.toJSON(vm, null, 2)
+// Pretty-printed JSON string
+```
+
+See [Loading & Saving Data](./json-data/) for practical patterns.
+
+## Extending types with `.fn`
+
+Every observable inherits from a prototype chain you can extend:
+
+```
+subscribable.fn  →  observable.fn  →  observableArray.fn
+                 →  computed.fn
+```
+
+Adding a method to `ko.observable.fn` makes it available on all observables:
+
+```js
+ko.observable.fn.log = function (label) {  // note: must be function, not arrow, to preserve `this`
+  this.subscribe(v => console.log(label, v))
+  return this
+}
+
+const name = ko.observable('TKO').log('name changed:')
+name('v5')  // console: "name changed: v5"
+```
+
+Adding to `ko.subscribable.fn` affects everything — observables, computed, and observable arrays.
+
+Use `.fn` when the behavior is broadly useful. For one-off logic, prefer a regular function or [extender](/observables/extenders/) instead.
diff --git a/tko.io/src/styles/tko.css b/tko.io/src/styles/tko.css
index 46c612d7..c6d26b47 100644
--- a/tko.io/src/styles/tko.css
+++ b/tko.io/src/styles/tko.css
@@ -134,11 +134,10 @@ header {
 
 .content-panel .sl-container,
 .right-sidebar-panel .sl-container {
-  background: var(--tko-surface);
-  border: 1px solid var(--tko-border);
-  border-radius: 1.25rem;
-  box-shadow: var(--tko-shadow);
-  backdrop-filter: blur(12px);
+  background: transparent;
+  border: none;
+  border-radius: 0;
+  box-shadow: none;
 }
 
 .content-panel .sl-container {
@@ -231,15 +230,21 @@ header {
   box-shadow: 0 10px 24px -22px rgba(17, 24, 39, 0.22);
 }
 
+/* Hide the Starlight page title when the hero is present */
+.content-panel:has(.landing-hero) [data-page-title],
+.content-panel:has(.landing-hero) > .sl-container > :first-child:is(header, .content-title) {
+  display: none;
+}
+
 .landing-hero {
   margin: 0 0 1.4rem;
-  padding: clamp(1.25rem, 3vw, 2.1rem);
+  padding: clamp(1.5rem, 3.5vw, 2.8rem);
   border: 1px solid var(--tko-border);
   border-radius: 1.5rem;
+  box-shadow: var(--tko-shadow);
   background:
     radial-gradient(circle at top right, rgba(15, 98, 254, 0.12), transparent 34rem),
     linear-gradient(180deg, var(--tko-surface-strong), color-mix(in srgb, var(--tko-surface-strong) 78%, var(--sl-color-accent-low)));
-  box-shadow: var(--tko-shadow);
 }
 
 :root[data-theme='dark'] .landing-hero {
@@ -257,20 +262,35 @@ header {
   color: var(--sl-color-text-accent);
 }
 
-.landing-hero .landing-version {
-  margin: 0 0 0.3rem;
-  font-family: var(--tko-font-display);
-  font-size: clamp(2rem, 5vw, 3.2rem);
-  font-weight: 700;
+.landing-version {
+  display: inline-block;
+  margin: 0 0 1rem;
+  padding: 0.25em 0.85em;
+  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
+  font-size: 0.82rem;
+  font-weight: 500;
+  letter-spacing: 0.03em;
+  color: var(--sl-color-gray-3);
+  text-decoration: none;
+  border: 1px solid var(--tko-border);
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.04);
+  transition: border-color 0.15s, color 0.15s;
+}
+
+.landing-version:hover {
   color: var(--sl-color-white);
+  border-color: var(--sl-color-gray-4);
 }
 
-.landing-hero h2:not(.landing-version) {
-  margin: 0;
-  max-width: 11ch;
-  font-size: clamp(1.55rem, 3.7vw, 2.55rem);
-  line-height: 1.02;
-  font-weight: 600;
+.landing-hero h2 {
+  margin: 0 0 0.2rem;
+  font-family: var(--tko-font-display);
+  font-size: clamp(3.5rem, 8vw, 6rem);
+  line-height: 1;
+  font-weight: 900;
+  letter-spacing: -0.03em;
+  color: var(--sl-color-white);
 }
 
 .landing-lede {