toiljs 0.0.96 → 0.0.97

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/docs/README.md CHANGED
@@ -71,10 +71,11 @@ Run `toiljs dev`, open the browser, and both are live with hot reload. That is t
71
71
 
72
72
  **Build the frontend**
73
73
  - [Frontend overview](./frontend/README.md), [Routing](./frontend/routing.md),
74
+ [Navigation](./frontend/navigation.md), [Components](./frontend/components.md),
74
75
  [Rendering and SSR](./frontend/rendering.md), [Styling](./frontend/styling.md),
75
76
  [Images](./frontend/images.md), [Metadata and SEO](./frontend/metadata.md),
76
77
  [Fetching data](./frontend/data-fetching.md), [Scripts](./frontend/scripts.md),
77
- [Search](./frontend/search.md).
78
+ [Search](./frontend/search.md), [The Toil global (reference)](./frontend/toil-global.md).
78
79
 
79
80
  **Build the backend**
80
81
  - [Backend overview](./backend/README.md), [HTTP routes (`@rest`)](./backend/rest.md),
@@ -84,7 +84,9 @@ The same fast data utilities your backend uses are available in client code too,
84
84
 
85
85
  Read them in roughly this order:
86
86
 
87
- - **[Routing](./routing.md)**: turn files into URLs. Index, nested, and dynamic pages; layouts and templates; active links; and navigating in code.
87
+ - **[Routing](./routing.md)**: turn files into URLs. Index, nested, and dynamic pages; layouts and templates.
88
+ - **[Navigation](./navigation.md)**: move between pages. Links and active state, navigating in code, typed hrefs, prefetching, scroll restoration, and animated transitions.
89
+ - **[Components](./components.md)**: use your own React components, plus the toiljs primitives (`Image`, `Script`, `Form`, `Slot`, `Head`, and the SSR markers).
88
90
  - **[Rendering and SSR](./rendering.md)**: what renders on the server versus in the browser, how hydration works, and the current SSR limitations.
89
91
  - **[Styling](./styling.md)**: plain CSS, preprocessors (Sass / Less / Stylus), and Tailwind.
90
92
  - **[Images](./images.md)**: the `Toil.Image` component, automatic blur placeholders, and how it stops layout shift.
@@ -92,6 +94,7 @@ Read them in roughly this order:
92
94
  - **[Fetching data](./data-fetching.md)**: call your backend with the generated typed clients, submit forms, and read who is logged in.
93
95
  - **[Scripts](./scripts.md)**: load external or inline `<script>` tags with a loading strategy, using `Toil.Script`.
94
96
  - **[Search](./search.md)**: the built-in, statically-baked page search and command palette (`usePageSearch`).
97
+ - **[The Toil global (reference)](./toil-global.md)**: a complete, grouped list of everything on the `Toil` object.
95
98
 
96
99
  ## Related
97
100
 
@@ -0,0 +1,286 @@
1
+ # Components
2
+
3
+ A toiljs frontend is a normal React app, so the components you write are just React components: functions that return JSX, hold state with hooks, and compose however you like. There is no toiljs-specific base class, no decorator, and no registration step. On top of your own components, toiljs ships a small set of ready-made ones on the `Toil` global (an image, a script loader, a form, and a few more) for the jobs a plain React app makes you wire up by hand. This page covers both: how your components fit in, and a reference for every `Toil.*` component.
4
+
5
+ ## Writing your own components
6
+
7
+ Anything you already know about writing React components applies unchanged. You write ordinary functions, use `useState` / `useEffect` / `useMemo` and any hooks you like, and return JSX:
8
+
9
+ ```tsx
10
+ // client/components/Counter.tsx
11
+ import { useState } from 'react';
12
+
13
+ export default function Counter({ start = 0 }: { start?: number }) {
14
+ const [n, setN] = useState(start);
15
+ return <button onClick={() => setN(n + 1)}>Clicked {n} times</button>;
16
+ }
17
+ ```
18
+
19
+ Your reusable components live in `client/components/`, and you import them the normal way from anywhere in `client/`:
20
+
21
+ ```tsx
22
+ // client/routes/index.tsx
23
+ import Counter from '../components/Counter';
24
+
25
+ export default function Home() {
26
+ return (
27
+ <main>
28
+ <h1>Welcome</h1>
29
+ <Counter start={10} />
30
+ </main>
31
+ );
32
+ }
33
+ ```
34
+
35
+ A few things are worth spelling out, because they are the parts that are handled for you rather than by you:
36
+
37
+ - **Route files must `export default` a component.** A file under `client/routes/` becomes a page only if it default-exports a component (see [Routing](./routing.md)). Files under `client/components/` have no such rule; export them however you please (default or named).
38
+ - **The `Toil.*` globals need no import.** `Toil.Link`, `Toil.useParams()`, `Toil.Image`, and everything else on `Toil` are ambient. The compiler generates a `toil-env.d.ts` that types the global, so your editor autocompletes `Toil.` and type-checks it with no `import` line. The same goes for `Server` (the typed backend client) and the `FastMap` / `DataWriter` data utilities. See the [Frontend overview](./README.md) for the whole ambient surface.
39
+ - **The JSX runtime is configured for you.** You do not write `import React from 'react'` at the top of every file. The build sets up the automatic JSX runtime, so JSX just works. Import named hooks and types from `react` when you need them (`import { useState, type ReactNode } from 'react'`), but the bare React import for JSX is unnecessary.
40
+
41
+ In other words, a component in a toiljs app is indistinguishable from a component in any React app. Two things are genuinely special, and both are opt-in.
42
+
43
+ ### Special case 1: components inside an SSR route
44
+
45
+ If a route opts into server rendering with `export const ssr = true`, its component tree is rendered once at build time into a template, then filled per request on the edge (see [Rendering and SSR](./rendering.md)). For that to work, any part of the JSX that changes per request (a value from the URL, a list from a loader, a block of user HTML) has to be wrapped in one of the [SSR marker primitives](#ssr-marker-primitives) below, or isolated in a `Toil.Island`. Anything you leave unwrapped gets frozen into the template at its build-time value.
46
+
47
+ You do not have to get this perfect: if an SSR route (or a layout above it) cannot render on the server, toiljs skips SSR for that route at build, prints a warning telling you what to move, and falls back to plain client rendering. So the route still works, it just loses its server first paint until you address the warning.
48
+
49
+ ### Special case 2: importing images and other assets
50
+
51
+ Importing an image with the `?toil` suffix gives you an object carrying the resolved URL, the intrinsic width and height, and an auto-generated blur placeholder (a `blurDataURL`), ready to hand straight to `Toil.Image`:
52
+
53
+ ```tsx
54
+ import hero from './hero.webp?toil';
55
+ // hero is { src, width, height, blurDataURL }
56
+
57
+ <Toil.Image src={hero} alt="Our office" />;
58
+ ```
59
+
60
+ Passing the whole object lets `Toil.Image` reserve the correct aspect ratio and paint the blur while the real image loads, with no extra props. See [Images](./images.md) for the full treatment.
61
+
62
+ Plain asset imports are typed for you as well. A bare `import logo from './logo.svg'` (or `.png`, `.webp`, and so on) resolves to the hashed URL string, and the vite-imagetools query forms (`?url`, `?as=srcset`, `?as=metadata`) are typed too, so you get autocomplete and type-checking on all of them without declaring any modules yourself.
63
+
64
+ ## The toiljs component primitives
65
+
66
+ These are the components toiljs provides on the `Toil` global. They cover the framework-level jobs a plain React app leaves to you. All are ambient (no import), and all are fully typed.
67
+
68
+ | Component | What it is | Renders |
69
+ | --- | --- | --- |
70
+ | `Toil.Image` | Layout-shift-free `<img>` replacement with lazy-load and blur placeholder. | An `<img>` (optionally wrapped in a sizing box). |
71
+ | `Toil.Script` | One-time external or inline `<script>` loader with a load strategy. | Nothing (`null`). |
72
+ | `Toil.Form` | A `<form>` that runs an action on submit and revalidates loader data. | A `<form>`. |
73
+ | `Toil.Slot` | Renders a named parallel-route slot for the current URL. | The slot's route tree, or a fallback. |
74
+ | `Toil.Head` | Declarative `<head>` contribution (title, meta, link). | Nothing (`null`). |
75
+ | `Toil.Metadata` | Declarative route-style metadata applied from any component. | Nothing (`null`). |
76
+
77
+ The [SSR marker primitives](#ssr-marker-primitives) (`Toil.Hole`, `Toil.Repeat`, `Toil.RawHtml`, `Toil.attr`, `Toil.Island`) are a separate group, covered in their own section below.
78
+
79
+ ### `Toil.Image`
80
+
81
+ A drop-in `<img>` replacement that prevents layout shift and lazy-loads by default. You give it `width` and `height` (or `fill`) so it reserves space before the image arrives, it decodes asynchronously, it lazy-loads unless you mark it `priority`, and it can fade in from a blur placeholder. It accepts either a string URL or a `?toil` import object (which auto-fills the size and blur):
82
+
83
+ ```tsx
84
+ import hero from './hero.webp?toil';
85
+
86
+ export default function Home() {
87
+ return (
88
+ <>
89
+ <Toil.Image src={hero} alt="Our office" priority placeholder="blur" />
90
+ <Toil.Image src="/team.jpg" alt="The team" width={800} height={600} />
91
+ </>
92
+ );
93
+ }
94
+ ```
95
+
96
+ Key props are `src`, `alt` (required, pass `alt=""` for a decorative image), `width` / `height` or `fill`, `priority` (for an above-the-fold hero), and `placeholder` (`'empty'` or `'blur'`). It also accepts any standard `<img>` attribute. This is kept brief on purpose: [Images](./images.md) covers sizing, `fill`, blur placeholders, and how it stops layout shift in full.
97
+
98
+ ### `Toil.Script`
99
+
100
+ Loads an external or inline `<script>` exactly once for the whole life of the app (even across client-side navigations), and lets you choose when it runs with a `strategy` prop. Use it instead of a hand-written `<script>` tag for third-party snippets like analytics or chat widgets, which a plain `<script>` in a single-page app runs unreliably or twice:
101
+
102
+ ```tsx
103
+ // client/layout.tsx
104
+ export default function Layout({ children }: { children?: React.ReactNode }) {
105
+ return (
106
+ <div className="app">
107
+ <Toil.Script src="https://cdn.example-analytics.com/analytics.js" />
108
+ {children}
109
+ </div>
110
+ );
111
+ }
112
+ ```
113
+
114
+ The `strategy` is `afterInteractive` (default), `lazyOnload`, or `beforeInteractive`, and there are `onLoad` / `onReady` / `onError` callbacks. `Toil.Script` renders nothing. See [Scripts](./scripts.md) for the strategies, inline scripts, dedup rules, and the full prop table.
115
+
116
+ ### `Toil.Form`
117
+
118
+ A `<form>` that submits without reloading the page. On submit it runs your `action` (which receives the form's `FormData`), tracks pending and error state, and on success revalidates the current route's loader data so the page reflects the write. It is the convenient front end of the loader/action data loop:
119
+
120
+ ```tsx
121
+ // A guestbook that refreshes its list after a successful sign.
122
+ export default function Guestbook() {
123
+ const entries = Toil.useLoaderData<typeof loader>();
124
+
125
+ const sign = async (data: FormData) => {
126
+ const author = String(data.get('author'));
127
+ const message = String(data.get('message'));
128
+ await Server.REST.guestbook.sign({ body: new NewMessage(author, message) });
129
+ };
130
+
131
+ return (
132
+ <Toil.Form action={sign} resetOnSuccess>
133
+ {({ pending, error }) => (
134
+ <>
135
+ <input name="author" placeholder="Your name" />
136
+ <textarea name="message" />
137
+ <button disabled={pending}>{pending ? 'Signing...' : 'Sign'}</button>
138
+ {error ? <p className="err">Could not sign, try again.</p> : null}
139
+ </>
140
+ )}
141
+ </Toil.Form>
142
+ );
143
+ }
144
+ ```
145
+
146
+ Pass a render function as the child to read the live submit state: it receives `{ pending, error, data }`, which is how you disable the button while pending or show an error. The props:
147
+
148
+ | Prop | Type | Default | What it does |
149
+ | --- | --- | --- | --- |
150
+ | `action` | `(data: FormData) => void \| Promise<void>` | (required) | Runs on submit, receiving the form's `FormData`. May be async. |
151
+ | `revalidate` | `RevalidateTarget` | `true` | Which loader data to refetch after a successful submit. `true` is the current route. |
152
+ | `onSuccess` | `() => void` | (none) | Called after a successful submit. |
153
+ | `onError` | `(error: unknown) => void` | (none) | Called when the action throws. |
154
+ | `resetOnSuccess` | `boolean` | `false` | Reset the form fields after a successful submit. |
155
+ | `className` | `string` | (none) | Class on the `<form>` element. |
156
+ | `children` | `ReactNode` or `(state) => ReactNode` | (none) | Form contents. A function child receives `{ pending, error, data }`. |
157
+
158
+ For writes that are not form submits (a delete button, a like toggle), reach for the underlying `Toil.useAction` hook instead. Both are covered in [Fetching data](./data-fetching.md).
159
+
160
+ ### `Toil.Slot`
161
+
162
+ Renders the named parallel-route slot for the current URL. A folder starting with `@` (like `@modal`) under `client/routes/` is a whole second route tree that matches the URL independently of the main page; placing `<Toil.Slot name="modal" />` is where that tree renders. If no slot route matches the current URL, it renders the `fallback` (nothing by default):
163
+
164
+ ```tsx
165
+ // client/routes/gallery/layout.tsx
166
+ export default function GalleryLayout({ children }: { children?: React.ReactNode }) {
167
+ return (
168
+ <div>
169
+ {children}
170
+ <Toil.Slot name="modal" fallback={null} />
171
+ </div>
172
+ );
173
+ }
174
+ ```
175
+
176
+ | Prop | Type | Default | What it does |
177
+ | --- | --- | --- | --- |
178
+ | `name` | `string` | (required) | The slot name: the `@name` directory under `routes/`, without the `@`. |
179
+ | `fallback` | `ReactNode` | `null` | Rendered when no slot route matches the current URL. |
180
+
181
+ Parallel slots and the intercepting routes that fill them (the "click a photo, open it in a modal" pattern) are explained in [Routing](./routing.md).
182
+
183
+ ### `Toil.Head`
184
+
185
+ A declarative way for any component (a page, a layout, a deep child) to contribute to the document `<head>`: a title, `<meta>` tags, and `<link>` tags. It renders nothing; it just applies its head entries for the lifetime of the component and reverts them on unmount. Entries compose across the tree, with later or deeper ones winning per key:
186
+
187
+ ```tsx
188
+ export default function ArticlePage() {
189
+ const post = Toil.useLoaderData<typeof loader>();
190
+ return (
191
+ <>
192
+ <Toil.Head
193
+ title={post.title}
194
+ meta={[
195
+ { name: 'description', content: post.summary },
196
+ { property: 'og:title', content: post.title },
197
+ ]}
198
+ link={[{ rel: 'canonical', href: `https://example.com/blog/${post.id}` }]}
199
+ />
200
+ <article>{/* ... */}</article>
201
+ </>
202
+ );
203
+ }
204
+ ```
205
+
206
+ `<Toil.Head>` takes `title`, `meta` (an array of `{ name | property, content }` tags), and `link` (an array of `{ rel, href }` tags). The hook form `Toil.useHead(spec)` and the shorthand `Toil.useTitle(title)` do the same job imperatively. See [Metadata and SEO](./metadata.md).
207
+
208
+ ### `Toil.Metadata`
209
+
210
+ The declarative, convenience-shaped cousin of `Toil.Head`. Instead of raw meta and link arrays, you pass a route-style metadata object (the same shape a route file's `export const metadata` uses), and toiljs expands the convenience fields (`description`, `keywords`, `canonical`, `openGraph`, and so on) into the right tags. It renders nothing, and applies for the component's lifetime. Use it to set metadata from a component that is not itself a route file (a rendered article, a widget):
211
+
212
+ ```tsx
213
+ <Toil.Metadata
214
+ title="Our pricing"
215
+ description="Simple, flat pricing for teams of any size."
216
+ openGraph={{ title: 'Pricing', image: 'https://example.com/og/pricing.png' }}
217
+ />
218
+ ```
219
+
220
+ Because a route's own `metadata` export is applied last (highest priority), `Toil.Metadata` fills in for routes that declare none and yields to a route that sets the same key. The full field list lives in [Metadata and SEO](./metadata.md).
221
+
222
+ ## SSR marker primitives
223
+
224
+ These five primitives matter only for routes with `export const ssr = true`. They are how you tell the build which parts of a server-rendered page are dynamic (filled per request) versus static (baked into the template once). On a client-only route they do nothing special, so you never need them unless you opt a route into SSR.
225
+
226
+ **The mental model.** In the browser these markers are transparent: `<Toil.Hole>` renders its children, `<Toil.Repeat>` maps its rows, `<Toil.RawHtml>` renders a raw-HTML wrapper, and `Toil.attr(id, value)` returns the value unchanged. Your client-side app runs exactly as written. But under the build-time SSR extractor, each marker instead emits a sentinel token that marks an insertion point. The extractor strips those tokens and records their positions, producing a static template with numbered holes. Per request, your compiled backend fills only the hole values, and the edge splices them into the template. Because the static scaffold around each hole is React's own rendering output and the hole values are escaped exactly as React escapes them, the browser hydrates the result byte-for-byte, with no mismatch.
227
+
228
+ | Marker | Use it for | Shape |
229
+ | --- | --- | --- |
230
+ | `Toil.Hole` | A single dynamic **text** value. | `<Toil.Hole id="...">{value}</Toil.Hole>` |
231
+ | `Toil.Repeat` | A **list**: one row template repeated over an `each` array. | `<Toil.Repeat id="..." each={rows}>{(item, i) => ...}</Toil.Repeat>` |
232
+ | `Toil.RawHtml` | A block of **trusted, pre-rendered HTML**. | `<Toil.RawHtml id="..." html={s} as="section" />` |
233
+ | `Toil.attr` | A dynamic value in **attribute position** (an `href`, a `class`). | `href={Toil.attr('id', value)}` (a function call) |
234
+ | `Toil.Island` | Content that must render **only in the browser** (escape hatch). | `<Toil.Island>{children}</Toil.Island>` |
235
+
236
+ A few rules that keep the template valid:
237
+
238
+ - **Every marker needs a stable `id`**, a short name unique within the page. The build maps each id to a numbered slot, so keep the ids constant across builds.
239
+ - **`Toil.Repeat` needs at least one row at build time.** It captures that first row as the sub-template for every row, so the build render must see a sample with one or more items. An empty `each` gives it nothing to capture.
240
+ - **`Toil.RawHtml` renders inside a wrapper element** (a `<div>` by default, `as="section"` to change the tag), and you own sanitising that HTML, exactly like React's `dangerouslySetInnerHTML`.
241
+ - **`Toil.attr` is a function, not an element.** An attribute is not a child node, so it cannot be a JSX element. You call `Toil.attr(id, value)` right where the attribute value goes, and it composes with literal text around it (`` className={`btn ${Toil.attr('kind', d.kind)}`} ``).
242
+ - **`Toil.Island` renders nothing on the server and on the first (hydration) render**, then reveals its children after mount. So an island gets no server first paint and no SEO, by design. It is the place for anything that genuinely cannot run on the server (reads `window`, calls `Date.now()`, depends on the live URL).
243
+
244
+ Here is a compact SSR route wiring several of them together. The title, tag list, and author link all come from the route's `loader`:
245
+
246
+ ```tsx
247
+ // client/routes/blog/[id].tsx
248
+ export const ssr = true;
249
+
250
+ export const loader = async ({ params }: Toil.LoaderArgs) => {
251
+ // Illustrative shape: { title, tags, authorUrl }.
252
+ return Server.REST.blog.get({ params: { id: params.id } });
253
+ };
254
+
255
+ export default function BlogPost() {
256
+ const post = Toil.useLoaderData<typeof loader>();
257
+ return (
258
+ <article>
259
+ <h1>
260
+ <Toil.Hole id="title">{post.title}</Toil.Hole>
261
+ </h1>
262
+
263
+ {/* A dynamic attribute: call attr() in attribute position. */}
264
+ <a href={Toil.attr('authorUrl', post.authorUrl)}>By the author</a>
265
+
266
+ {/* A list: one row template, stamped once per item on the server. */}
267
+ <ul>
268
+ <Toil.Repeat id="tags" each={post.tags}>
269
+ {(tag) => <li key={tag}>{tag}</li>}
270
+ </Toil.Repeat>
271
+ </ul>
272
+ </article>
273
+ );
274
+ }
275
+ ```
276
+
277
+ For the whole SSR story (the template extraction flow, keeping a route SSR-safe, and the current SSR limitations), see [Rendering and SSR](./rendering.md).
278
+
279
+ ## Related
280
+
281
+ - [Rendering and SSR](./rendering.md): how SSR routes render, hydrate, and use the marker primitives.
282
+ - [Images](./images.md): the full `Toil.Image` reference, blur placeholders, and layout shift.
283
+ - [Scripts](./scripts.md): `Toil.Script` strategies, inline scripts, and dedup.
284
+ - [Fetching data](./data-fetching.md): `Toil.Form`, `useAction`, loaders, and the typed backend clients.
285
+ - [Metadata and SEO](./metadata.md): `Toil.Head`, `Toil.Metadata`, and per-route metadata.
286
+ - [Routing](./routing.md): pages, layouts, and the parallel slots `Toil.Slot` renders.
@@ -0,0 +1,296 @@
1
+ # Navigation
2
+
3
+ Once your files are turned into URLs (see [Routing](./routing.md)), you need a way to move between them. This page is about that half: the links, hooks, and functions that navigate a user from one page to the next without a full reload, keep the current section highlighted, prefetch what is coming, and restore scroll. Everything here lives on the global `Toil` object, so route files need no imports for the common cases.
4
+
5
+ Routing is "file to URL"; navigation is "get me to that URL". If you are looking for how a file becomes a page, or for dynamic params and layouts, that is the [Routing](./routing.md) page.
6
+
7
+ ## `Toil.Link`
8
+
9
+ `Toil.Link` is the client-side replacement for a plain `<a>`. It navigates in place (no full page reload), and it prefetches the target route's chunk on hover or focus, so the click feels instant:
10
+
11
+ ```tsx
12
+ <Toil.Link href="/about">About</Toil.Link>
13
+ ```
14
+
15
+ `Link` accepts every standard anchor attribute (`className`, `style`, `target`, `rel`, `download`, `referrerPolicy`, `ref`, `data-*`, `aria-*`, event handlers, and so on), plus a few toiljs controls:
16
+
17
+ | Prop | Type | Default | What it does |
18
+ | --- | --- | --- | --- |
19
+ | `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |
20
+ | `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |
21
+ | `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |
22
+ | `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |
23
+
24
+ ### When `Link` does not intercept
25
+
26
+ `Link` is deliberate about when to hand a click back to the browser. It only intercepts a plain, same-origin, left-click. All of these fall through to native browser behavior instead:
27
+
28
+ - **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.
29
+ - **`target` other than `_self`** (for example `target="_blank"`, a new tab).
30
+ - **`download`** links.
31
+ - **In-page `#hash`-only** links.
32
+ - **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so "open in new tab" keeps working).
33
+
34
+ Because of this, you can point a `Link` at an external site or a download and it behaves exactly like an `<a>` would, no special-casing on your part. For genuinely external links a plain `<a>` is still fine; reach for `Link` when the target is one of your own routes.
35
+
36
+ ## `Toil.NavLink` and active state
37
+
38
+ `Toil.NavLink` is a `Link` that knows whether it points at the current page. When active it adds a class (`active` by default) and sets `aria-current="page"`, which is exactly what a navigation bar wants for highlighting the current section. It inherits Link's full anchor API and its prefetching.
39
+
40
+ On top of `LinkProps` it adds:
41
+
42
+ | Prop | Type | Default | What it does |
43
+ | --- | --- | --- | --- |
44
+ | `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |
45
+ | `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |
46
+ | `className` | `string \| (state) => string \| undefined` | (none) | A string, or a function of `{ isActive }`. |
47
+ | `style` | `CSSProperties \| (state) => CSSProperties \| undefined` | (none) | A style object, or a function of `{ isActive }`. |
48
+ | `children` | `ReactNode \| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |
49
+
50
+ A simple nav bar with string classes:
51
+
52
+ ```tsx
53
+ // client/components/Nav.tsx
54
+ export default function Nav() {
55
+ return (
56
+ <nav>
57
+ <Toil.NavLink href="/" end>Home</Toil.NavLink>
58
+ <Toil.NavLink href="/blog">Blog</Toil.NavLink>
59
+ <Toil.NavLink href="/about" activeClassName="is-current">About</Toil.NavLink>
60
+ </nav>
61
+ );
62
+ }
63
+ ```
64
+
65
+ By default a parent link stays active for its sub-paths, so `/blog` is active on `/blog`, `/blog/42`, and `/blog/42/edit`. Pass `end` to require an exact match. This matters most for the home link: `/` would otherwise be active on every page, so `Home` almost always wants `end`.
66
+
67
+ The function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:
68
+
69
+ ```tsx
70
+ <Toil.NavLink href="/blog" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>
71
+ {({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}
72
+ </Toil.NavLink>
73
+ ```
74
+
75
+ ## Navigating in code
76
+
77
+ Not every navigation is a click. After a form submit, a login, or a `fetch`, you often want to move the user yourself. There are three ways, from smallest to fullest.
78
+
79
+ ### `Toil.useNavigate()`
80
+
81
+ The hook returns the bare `navigate(href, options)` function:
82
+
83
+ ```tsx
84
+ export default function Login() {
85
+ const navigate = Toil.useNavigate();
86
+ return (
87
+ <button onClick={() => navigate('/dashboard', { replace: true })}>
88
+ Continue
89
+ </button>
90
+ );
91
+ }
92
+ ```
93
+
94
+ The options are `NavigateOptions`, the same two controls `Link` exposes:
95
+
96
+ | Option | Type | Default | What it does |
97
+ | --- | --- | --- | --- |
98
+ | `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |
99
+ | `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |
100
+
101
+ ### `Toil.navigate` (outside React)
102
+
103
+ The exact same function is available free of any hook as `Toil.navigate(href, options)`. Use it in code that is not a React component: a plain event handler, a utility module, or right after a `fetch` resolves:
104
+
105
+ ```tsx
106
+ async function saveDraft(draft: Draft) {
107
+ await Server.REST.posts.create({ body: draft });
108
+ Toil.navigate('/posts');
109
+ }
110
+ ```
111
+
112
+ `Toil.back()`, `Toil.forward()`, and `Toil.refresh()` are the history counterparts, also callable from anywhere: `back` and `forward` step through history, and `refresh` re-renders the current route (re-running its loader for the current URL).
113
+
114
+ ### `Toil.useRouter()`
115
+
116
+ For a full imperative handle, `useRouter()` returns a `RouterInstance`:
117
+
118
+ ```tsx
119
+ export default function PostActions() {
120
+ const router = Toil.useRouter();
121
+ return (
122
+ <>
123
+ <button onClick={() => router.push('/blog/new')}>New post</button>
124
+ <button onClick={() => router.back()}>Back</button>
125
+ <button onClick={() => router.revalidate()}>Refresh data</button>
126
+ </>
127
+ );
128
+ }
129
+ ```
130
+
131
+ | Method | What it does |
132
+ | --- | --- |
133
+ | `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |
134
+ | `replace(href)` | Navigate to `href`, replacing the current history entry. |
135
+ | `back()` | Go back one history entry. |
136
+ | `forward()` | Go forward one history entry. |
137
+ | `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |
138
+ | `revalidate(href?)` | Invalidate cached loader data and re-render so it refetches. No argument targets the active route; pass an `href` for a specific route. Use after a mutation. |
139
+ | `prefetch(href)` | Warm a route's chunk ahead of navigation. |
140
+
141
+ Reach for `revalidate()` after a write (you changed some data and want the current page's loader to refetch), and `refresh()` when you want a clean re-run of everything. `push`/`replace` are the imperative twins of a `Link` click.
142
+
143
+ ## Typed hrefs and the `href()` escape hatch
144
+
145
+ Every `href` you pass to `Toil.Link`, `NavLink`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a `toil-routes.d.ts` that narrows the `Href` type to the union of your actual paths, so a typo is a compile error before you run the app:
146
+
147
+ ```tsx
148
+ <Toil.Link href="/abuot">About</Toil.Link>
149
+ // ^ Type error: "/abuot" is not assignable to type 'Href'. (No such route.)
150
+ ```
151
+
152
+ Dynamic routes appear in that union as template-literal types, so a file at `client/routes/blog/[id].tsx` contributes `` `/blog/${string}` `` and a single interpolation still checks:
153
+
154
+ ```tsx
155
+ <Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine
156
+ ```
157
+
158
+ When a URL is assembled from several data pieces (or from values TypeScript cannot prove the shape of), it is typed as a plain `string`, and `string` is not assignable to `Href`. That is when you reach for `Toil.href()`, the escape hatch that asserts a runtime string is a valid href:
159
+
160
+ ```tsx
161
+ const path = `/${product.category}/${product.slug}`; // string
162
+ Toil.navigate(Toil.href(path)); // asserted valid
163
+ <Toil.Link href={Toil.href(path)}>Open</Toil.Link>;
164
+ ```
165
+
166
+ `href()` is a pure type assertion (it returns the string unchanged), so use it only when the type-check is genuinely in your way, not to paper over a real typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.
167
+
168
+ ## Reading the current location
169
+
170
+ A handful of hooks let a component read where it is. Each re-reads on every navigation, so a component using one re-renders when the location changes:
171
+
172
+ | Hook | Returns |
173
+ | --- | --- |
174
+ | `Toil.usePathname()` | The current pathname, e.g. `"/blog/42"`. |
175
+ | `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |
176
+ | `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |
177
+ | `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |
178
+ | `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |
179
+
180
+ `useNavigationPending()` is what you wire a top loading bar to. It flips to `true` when a navigation begins and back to `false` once the new route commits:
181
+
182
+ ```tsx
183
+ // client/components/ProgressBar.tsx
184
+ export default function ProgressBar() {
185
+ const pending = Toil.useNavigationPending();
186
+ return <div className="progress" data-active={pending} />;
187
+ }
188
+ ```
189
+
190
+ `useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:
191
+
192
+ ```tsx
193
+ export default function Results() {
194
+ const params = Toil.useSearchParams();
195
+ const q = params.get('q') ?? '';
196
+ return <p>Results for {q}</p>;
197
+ }
198
+ ```
199
+
200
+ ## Prefetching
201
+
202
+ toiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:
203
+
204
+ - **Hover / focus intent.** When you hover or focus a link that points at a known internal route, toiljs warms both its route chunk and its loader data, so the actual click can commit right away.
205
+ - **Viewport intent.** As a link scrolls into view (or within about 200px of it), its route chunk is warmed. Links added later by client navigation are picked up automatically.
206
+
207
+ Prefetching is best-effort and cheap: each route loads at most once, a failed prefetch is forgotten (so the real navigation can retry and surface the error), and new-tab / download / opted-out links are skipped. It is also **skipped entirely when the browser signals data-saver** (or reports a 2g-class connection), so you never spend a metered user's bandwidth on speculation.
208
+
209
+ Opt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:
210
+
211
+ ```tsx
212
+ <Toil.Link href="/huge-report" prefetch={false}>Annual report</Toil.Link>
213
+ ```
214
+
215
+ You can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:
216
+
217
+ ```tsx
218
+ <button
219
+ onPointerEnter={() => Toil.prefetch('/dashboard')}
220
+ onClick={() => Toil.navigate('/dashboard')}>
221
+ Open dashboard
222
+ </button>
223
+ ```
224
+
225
+ ## Scroll restoration
226
+
227
+ toiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):
228
+
229
+ - **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.
230
+ - **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.
231
+ - **A `#hash` target** scrolls that element into view instead of jumping to the top.
232
+
233
+ To keep the current scroll on a push navigation, set `scroll={false}` on the `Link` (or `{ scroll: false }` in `NavigateOptions`). This is handy for tab bars or filters that change the URL but should not yank the viewport:
234
+
235
+ ```tsx
236
+ <Toil.Link href="/settings/billing" scroll={false}>Billing</Toil.Link>
237
+ ```
238
+
239
+ ```tsx
240
+ navigate('/settings/billing', { scroll: false });
241
+ ```
242
+
243
+ ## Animated transitions
244
+
245
+ Two optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:
246
+
247
+ ```ts
248
+ // toil.config.ts
249
+ export default {
250
+ client: {
251
+ viewTransitions: true, // browser View Transitions API (a crossfade between pages)
252
+ transitions: true, // React transition: keep the old page visible while the next loads
253
+ },
254
+ };
255
+ ```
256
+
257
+ `viewTransitions` uses the browser's View Transitions API to crossfade the old and new page (and it respects `prefers-reduced-motion`, animating nothing for users who ask for less motion). `transitions` wraps each navigation in a React transition, keeping the current page on screen while the next route's loader runs, instead of showing its `loading.tsx` right away (smoother, but you trade away the immediate loading state).
258
+
259
+ Config is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:
260
+
261
+ ```tsx
262
+ Toil.setViewTransitions(true);
263
+ Toil.setTransitions(false);
264
+ ```
265
+
266
+ You rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.
267
+
268
+ ## Types
269
+
270
+ Almost everything on this page is a value on the global `Toil` object, so you call it with no import (`Toil.Link`, `Toil.navigate`, `Toil.useRouter`, and so on). A few of the **types**, though, are not namespaced under `Toil`, so if you want to annotate a prop or a variable you import them from `toiljs/client`:
271
+
272
+ ```tsx
273
+ import type {
274
+ LinkProps,
275
+ NavLinkProps,
276
+ NavLinkState,
277
+ NavigateOptions,
278
+ RouterInstance,
279
+ } from 'toiljs/client';
280
+ ```
281
+
282
+ For example, a component that forwards `Link` props, or a helper typed against the router handle:
283
+
284
+ ```tsx
285
+ import type { RouterInstance } from 'toiljs/client';
286
+
287
+ function logoutThenGo(router: RouterInstance) {
288
+ router.replace('/login');
289
+ }
290
+ ```
291
+
292
+ ## Related
293
+
294
+ - [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.
295
+ - [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.
296
+ - [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.
@@ -195,6 +195,8 @@ The `(.)` markers mean: `(.)` same level, `(..)` up one level, `(...)` from the
195
195
 
196
196
  ## Links and navigation
197
197
 
198
+ This is a quick tour. For the full treatment, programmatic navigation, typed hrefs, prefetching, scroll restoration, and animated transitions, see [Navigation](./navigation.md).
199
+
198
200
  ### `Toil.Link`
199
201
 
200
202
  Use `Toil.Link` instead of a plain `<a>` for in-app links. It navigates client-side (no full page reload), and prefetches the target route's code on hover or focus so the click feels instant: