# Extend a TypeSafe reference recipe

Choose the starting point for your change before editing:

| Change | Start here |
| --- | --- |
| Add a documentation page | [Clone the complete shell](#clone-a-documentation-page). The command adds an incoming link from the source recipe; choose any additional navigation or search placement. |
| Extend an existing control | [Markup and interaction contracts](#extend-the-contract-with-the-markup). Name its catalog component ID, source recipe and preserved anatomy before editing. |
| Add a reusable local choice or response component | [The callable component API](#local-finite-outcome-component-api), including inputs, defaults, two instances, updates and cleanup. |
| Handle local loading, retries or overlapping requests | [The component API's request lifecycle](#local-finite-outcome-component-api) and [runtime boundary](#runtime-boundary). |
| Write a Python SDK request or error example | [The public SDK contract record](#public-sdk-names-and-local-fixtures), including actual retry settings and exception names. |
| Check an extension | [Browser verification](#verify-the-chosen-change), including retained shell controls. |

Start with the closest complete page in `site/examples`. Retain its linked CSS, JavaScript and source assets, and change the content or component inside that shell. The clone command below preserves the documentation shell for you.

Place a derived documentation page beside the original in `examples/`. Its relative asset URLs and the shared search destinations assume that depth. If a route needs another location, update every relative resource and navigation destination together. Keep `docs-recipes.js` loaded: a retained search, theme, menu, tab, copy or image control still needs its original behavior.

The downloadable recipe-starters.zip contains the complete static guide, examples, local fonts, source assets and this file. Serve the unpacked folder with a static server. There is no installation or framework build.

Paths beginning with `site/` refer to the research package. In the unpacked starter, use the same path without that prefix; for example, `site/assets/local-component-api.js` becomes `assets/local-component-api.js`.

## Record the six handoff stages

Keep a short worksheet while working. It makes a later reader able to inspect the result rather than infer what happened.

| Stage | Record before advancing |
| --- | --- |
| Find | The inventory route family, source recipe, component-contract ID, SDK record if code is shown, and the existing page or registry that will discover a new route. |
| Choose | The shell, component or helper, and the reason it fits the recorded source. For a new page, name the existing route that links to it. |
| Specify | The input and output shape, defaults, empty behavior, state transitions, source anatomy, imports, and the browser observations that will prove the acceptance criteria. |
| Implement | The changed paths and the component root that owns each new interaction. Use the callable fixture API below for finite local outcomes, or document an equivalent caller-owned contract. |
| Verify | Executable assertions for every task acceptance criterion and the applicable component checks below, with the exact command, raw result and relevant screenshot or trace. |
| Explain | The source paths, consequential choices, known limits, and whether every criterion passed. Do not call a direct-load self-link discovery. |

Keep the task's acceptance criteria and their IDs unchanged. A criterion can require several assertions. Include the chosen component's named keyboard, state, configuration, safety and lifecycle checks in the same plan, and link those assertions to the task they support.

## Clone a documentation page

Use the included clone command to preserve the complete documentation shell:

```sh
python3 downloads/clone-docs-recipe.py --template cookbook --name my-extension --title "My extension"
```

From the research package, the same command is `python3 tools/clone-docs-recipe.py ...`. It creates a new file under `examples/`, updates the title, mobile page label and lead, retains the source page's complete content and dependencies, and adds the new page to its own sidebar. It also updates the source recipe's sidebar with an incoming link to the new page. The command reports that existing route as `discoveryFrom`. Start there and follow the link to verify discovery. Keep both changed HTML files when moving the extension.

Choose any additional docs placement and add the route to the `data` list in `site/assets/docs-recipes.js` when search should find it. A current-page self-link is only a marker after direct navigation. The command refuses to overwrite an existing file. Do not reconstruct the header or sidebar from memory.

## Choose a shell

| Work | Starting page | Shared files |
| --- | --- | --- |
| Developer onboarding and installation | examples/quickstart.html | assets/docs-recipes.css, assets/docs-recipes.js |
| An API member or optional response field | examples/python-api.html | The same docs files and API table classes |
| Typed choices and response interpretation | examples/score.html or examples/cookbook.html | The same docs files and code/table classes |
| Passive video or image content | examples/smart-home.html | The same docs files and native media dialog |
| A public marketing action | examples/home.html | assets/product-recipes.css, assets/product-recipes.js |
| An editorial page | examples/launch.html or examples/lesson.html | The same product files and article classes |
| Public sign-in entry | examples/console.html | The same product files; sign-in remains a local fixture |

Keep the original font faces, source marks, type sizes, content measure, responsive rules and navigation. The documentation shell uses the retrieved DocsInter font and a dark three-column composition. The marketing shell uses Die Grotesk and Lisa Terminal. Read foundations.html for measured roles; do not substitute a new type scale, color palette, radius scale or text approximation of the logo.

Component IDs in components.html map to a source record, specimen and local contract. Use inventory.html to confirm the relevant page family before choosing a component. The guide's own editorial header and example-control demos are labelled guide-only; they are not the TypeSafe documentation shell.

## Extend the contract with the markup

Keep stable IDs unique. If a component references another element through aria-controls, aria-labelledby, aria-describedby or a data target, update both ends when duplicating it. The docs script currently initializes one page. When extracting a reusable component, scope its selectors to its root and keep each instance's state separate.

A new reusable pattern needs an actual callable API or a documented initialization contract. State which inputs it accepts, their defaults, how updates change the output, and how it cleans up listeners or pending work. Demonstrate two independent instances and the no-items collection branch. Scope selectors and listeners to the caller's root. A module that queries one document-wide selector, hard-codes its fixtures, or changes only a border does not establish a reusable rendering API.

Existing documentation tabs use data-doc-tab for the installation methods pip and uv. Adding a language requires a language-to-code map and an updated click handler; changing the button text alone does not add a new variant. Retain roving tabindex, ArrowLeft/ArrowRight, Home/End, aria-selected, aria-controls and the panel's accessible label. Copy the selected panel only. Keep visible feedback and a manual-copy path when clipboard permission is denied.

API examples must retain the public SDK's actual names and payload shape. The Python reference uses AsyncTypeSafeClient, client.models and client.system_one; use the retrieved API and cookbook examples for the complete request. A local UI fixture may have a different data shape, but label it as an original local fixture and do not present it as an executable SDK call.

When a variant extends `docs-code-tabs`, retain its tablist and give each tab an `aria-controls` reference to its sibling `pre[role="tabpanel"]` in the same step. Update that panel's `aria-labelledby` when selection changes. The tablist does not contain the panel. `docs-code-block` is a separate root with a Copy button, `pre` and status paragraph; retain that structure when extending a copyable code example. Start code changes from the documented import and complete call, then add the variant. A visible class name without its import is not a complete SDK example.

Insert user-provided strings with textContent. Build repeated rows with DOM elements or an escaping function. A prompt containing angle brackets must appear as text. Do not interpolate it into innerHTML, a URL, a selector or an event handler.

## Runtime boundary

The guide has no authentication backend, model execution, remote feature flags or analytics transport. Public console entry and synthetic responses are the verified boundary. Do not infer an authenticated role matrix, billing policy or API authorization rule from these examples.

For a new asynchronous local fixture, define idle, loading, success, empty, error and offline states before implementing it. Preserve entered values and the last request context across retry. Use a monotonically increasing request ID or AbortController so an older response cannot replace a newer result. Mark the affected region aria-busy during loading and announce the resulting state. Clear loading on every terminal path.

If the extension needs a local feature flag, accept an explicit boolean configuration with a documented default. Test both values; a badge containing a flag name is not a gate. If it needs analytics for a prototype, accept an injected callback that defaults to a no-op. Use synthetic event data and leave the transport disabled. Both additions are local contracts, not observations about TypeSafe production.

## Verify the chosen change

Run the real modified page in Chromium. Preserve commands, raw results and screenshots for each requirement; a title check or screenshot alone does not verify an interaction. Before implementation, transcribe every acceptance criterion and applicable component-contract check into an assertion plan with a browser action and an observable result. A generic pattern is:

```js
const results = [];
function criterion(id, observed) {
  if (!observed) throw new Error(`Criterion failed: ${id}`);
  results.push({ id, pass: true });
}

// Repeat once for every acceptance criterion, using a fresh browser observation.
await page.getByRole("link", { name: "Existing docs entry" }).click();
criterion("new-page.1", await page.locator("[data-component='docs-shell']").count() === 1);
```

Give each assertion the task criterion ID and retain the `results` JSON. Direct navigation to the new route, checking a static string, or counting planned checks does not establish discovery, interaction, copy, state, or reflow behavior.

Turn each acceptance criterion into an assertion that fails the verification command when the observed result is wrong. Record intermediate loading as well as the final result. Exercise every introduced option, both values of each flag, and retained shell controls. Recording a value in JSON without checking it is an observation, not a passing test.

| Concern | Required observation |
| --- | --- |
| Visual reuse | The before/after page retains the selected family, local fonts, exact marks, shell, typography and responsive behavior. Explain any intended local addition. |
| Input and recovery | Empty and invalid values announce a useful error; correction and retry work without losing input. |
| Keyboard | Tab order, visible focus, Enter/Space, tab-list arrows, dialog Escape and focus return work where applicable. |
| Copy | Test success and denied/unavailable clipboard access, including the fallback. |
| Async state | Observe loading and every terminal state. Force two responses to arrive out of order and check that only the newest commits. |
| Safety | Render angle brackets, long content and mixed-direction text as text; check that no handler executes and no document overflow appears. |
| Configuration | Toggle every introduced local flag and assert both outcomes; inspect callback events without sending telemetry. |
| Reflow | Inspect 390, 768, 1440 and 1920 CSS pixels. Code may scroll internally; the document must not. |
| Accessibility | Run axe, inspect the accessibility tree and test keyboard operation. Record untested browser and assistive-technology boundaries. |

For a handoff assessment, use the six-stage record above. Mark a missing consequential decision or a failed requirement honestly. Keep original attempts and test output when revising the reference.

## Local finite-outcome component API

`site/assets/local-component-api.js` is an original local fixture. It is not a TypeSafe SDK call or a claim about TypeSafe production behavior. It renders a finite native-button choice group and accepts only a caller-provided loader. It sends no request on its own.

```html
<!-- examples/my-extension.html -->
<div id="first"></div>
<div id="second"></div>
<script type="module" src="../assets/my-extension.js"></script>
```

```js
// site/assets/my-extension.js
import { mountFiniteOutcome } from "../assets/local-component-api.js";

const first = mountFiniteOutcome(document.querySelector("#first"), {
  label: "Choose a route",
  items: [{ id: "account", label: "Account help", kind: "response", body: "Ask for the account email." }],
  onSelect: ({ id }) => console.log(id),
});
const second = mountFiniteOutcome(document.querySelector("#second"), {
  items: [],
  enabled: false,
});

first.update({ enabled: false });
first.update({ enabled: true, selectedId: "account" });
await first.request({ question: "restore access" }, async (context, { signal }) => {
  if (signal.aborted) throw new DOMException("Cancelled", "AbortError");
  return { status: "success", items: [{ id: "account", label: context.question, kind: "response", body: "Ask for the account email." }] };
});
await first.retry();
first.destroy();
second.destroy();
```

`mountFiniteOutcome(root, options)` accepts `label` (default `"Finite outcome"`), `items` (default `[]`), `selectedId` (default `null`), `enabled` (default `true`) and `onSelect` (default no-op). Each item needs a unique non-empty `id` and `label`, a caller-supplied `kind` such as `response` or `error`, and a `body`. Missing `kind` becomes `response`; missing `body` becomes an empty string. The root is a labelled region and its choices are a labelled native-button group. The selected item shows its kind and body in a native `pre` element, which keeps long code internally scrollable. Labels, kinds and bodies render through `textContent`. `update(partialOptions)` changes only supplied fields, so omitted fields retain their current values. Passing `selectedId: null` clears selection. A non-function `onSelect` in an update resets it to the no-op.

`request(context, loader)` retains both arguments for `retry()`. The loader receives `(context, { signal, requestId })` and resolves to an item array or `{ status, items?, message? }`, where `status` is `success`, `empty`, `error` or `offline`. An empty success becomes `empty`; a thrown error with `code: "offline"` or `name: "OfflineError"` becomes `offline`; other thrown errors become `error`. Loading is visible and sets `aria-busy="true"`. `enabled` governs user actions, including retry. Direct `request()` calls remain explicit programmatic calls. A selection or an update with `items` or `selectedId` aborts and invalidates an in-flight request because it supersedes that result. A newer request does the same. `destroy()` removes the listener and owned DOM, aborts the active signal and invalidates pending completion.

The fixture has no transport, cache, authentication, analytics or persisted state. Loaders that ignore their abort signal still run in caller code, but their late result cannot update a destroyed component or replace a newer request.

## Public SDK names and local fixtures

For request construction and result access, start with the [Python SDK request and result](../examples/quickstart.html#section-2). Keep its method, arguments and response access together when adapting the example. The retry and exception record below supplements that complete call; it does not define a different request method or success schema.

The included [Python SDK contract record](../data/public-sdk-contracts.json) records retry settings and exception names checked against the public [retry reference](https://docs.typesafe.ai/sdk/python/api/retries) and [exception reference](https://docs.typesafe.ai/sdk/python/api/exceptions). The downloadable starter includes this JSON at `data/public-sdk-contracts.json`. Keep SDK names separate from the local component API above. Use the documented imports; label synthetic error bodies and application recovery decisions as local examples.
