toiljs 0.0.11 → 0.0.14
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/README.md +3 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +58 -30
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +33 -24
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -41
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
- package/examples/basic/client/routes/about.tsx +21 -22
- package/examples/basic/client/routes/blog/[id].tsx +26 -18
- package/examples/basic/client/routes/features/actions.tsx +67 -67
- package/examples/basic/client/routes/features/error/index.tsx +27 -27
- package/examples/basic/client/routes/features/head.tsx +38 -38
- package/examples/basic/client/routes/features/index.tsx +83 -75
- package/examples/basic/client/routes/features/realtime.tsx +34 -32
- package/examples/basic/client/routes/features/script.tsx +31 -31
- package/examples/basic/client/routes/features/seo.tsx +39 -39
- package/examples/basic/client/routes/features/template/index.tsx +20 -20
- package/examples/basic/client/routes/features/template/template.tsx +16 -18
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
- package/examples/basic/client/routes/gallery/index.tsx +42 -42
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -96
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/package.json +2 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +45 -27
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -145
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +1 -1
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +1 -2
- package/test/dom/router-loading.test.tsx +1 -1
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +30 -8
- package/test/update.test.ts +44 -0
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// Mutations: a `loader` reads, an action writes, then revalidation refetches the loader so the UI
|
|
2
|
-
// reflects the new state with no manual refetch. Here a module-level counter stands in for a server.
|
|
3
|
-
let serverCount = 0;
|
|
4
|
-
async function wait(ms: number): Promise<void> {
|
|
5
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const metadata: Toil.Metadata = {
|
|
9
|
-
title: 'Actions and forms',
|
|
10
|
-
description: 'useAction and <Form> mutations with pending state and revalidation.'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const loader = async () => {
|
|
14
|
-
await wait(150);
|
|
15
|
-
return { count: serverCount };
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default function ActionsDemo() {
|
|
19
|
-
const { count } = Toil.useLoaderData(loader);
|
|
20
|
-
|
|
21
|
-
// useAction: run a mutation, track pending/error, then revalidate this route's loader on success.
|
|
22
|
-
const increment = Toil.useAction(
|
|
23
|
-
async (by: number) => {
|
|
24
|
-
await wait(400);
|
|
25
|
-
serverCount += by;
|
|
26
|
-
return serverCount;
|
|
27
|
-
},
|
|
28
|
-
{ revalidate: true }
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<main>
|
|
33
|
-
<h1>Actions and forms</h1>
|
|
34
|
-
<p>
|
|
35
|
-
Server count (from the loader): <strong>{count}</strong>
|
|
36
|
-
</p>
|
|
37
|
-
|
|
38
|
-
<p>
|
|
39
|
-
<button type="button" disabled={increment.pending} onClick={() => void increment.run(1)}>
|
|
40
|
-
{increment.pending ? 'Saving' : 'Increment via useAction'}
|
|
41
|
-
</button>
|
|
42
|
-
{increment.error ? <span style={{ color: 'crimson' }}> failed</span> : null}
|
|
43
|
-
</p>
|
|
44
|
-
|
|
45
|
-
{/* The declarative form: submits to an action, revalidates on success, exposes pending. */}
|
|
46
|
-
<Toil.Form
|
|
47
|
-
action={async (form) => {
|
|
48
|
-
await wait(400);
|
|
49
|
-
serverCount += Number(form.get('by') || 0);
|
|
50
|
-
}}
|
|
51
|
-
revalidate>
|
|
52
|
-
{({ pending }) => (
|
|
53
|
-
<>
|
|
54
|
-
<input name="by" type="number" defaultValue={5} disabled={pending} />
|
|
55
|
-
<button type="submit" disabled={pending}>
|
|
56
|
-
{pending ? 'Saving' : 'Add via <Form>'}
|
|
57
|
-
</button>
|
|
58
|
-
</>
|
|
59
|
-
)}
|
|
60
|
-
</Toil.Form>
|
|
61
|
-
|
|
62
|
-
<p>
|
|
63
|
-
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
64
|
-
</p>
|
|
65
|
-
</main>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
1
|
+
// Mutations: a `loader` reads, an action writes, then revalidation refetches the loader so the UI
|
|
2
|
+
// reflects the new state with no manual refetch. Here a module-level counter stands in for a server.
|
|
3
|
+
let serverCount = 0;
|
|
4
|
+
async function wait(ms: number): Promise<void> {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const metadata: Toil.Metadata = {
|
|
9
|
+
title: 'Actions and forms',
|
|
10
|
+
description: 'useAction and <Form> mutations with pending state and revalidation.'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const loader = async () => {
|
|
14
|
+
await wait(150);
|
|
15
|
+
return { count: serverCount };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function ActionsDemo() {
|
|
19
|
+
const { count } = Toil.useLoaderData(loader);
|
|
20
|
+
|
|
21
|
+
// useAction: run a mutation, track pending/error, then revalidate this route's loader on success.
|
|
22
|
+
const increment = Toil.useAction(
|
|
23
|
+
async (by: number) => {
|
|
24
|
+
await wait(400);
|
|
25
|
+
serverCount += by;
|
|
26
|
+
return serverCount;
|
|
27
|
+
},
|
|
28
|
+
{ revalidate: true }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<main>
|
|
33
|
+
<h1>Actions and forms</h1>
|
|
34
|
+
<p>
|
|
35
|
+
Server count (from the loader): <strong>{count}</strong>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<p>
|
|
39
|
+
<button type="button" disabled={increment.pending} onClick={() => void increment.run(1)}>
|
|
40
|
+
{increment.pending ? 'Saving' : 'Increment via useAction'}
|
|
41
|
+
</button>
|
|
42
|
+
{increment.error ? <span style={{ color: 'crimson' }}> failed</span> : null}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
{/* The declarative form: submits to an action, revalidates on success, exposes pending. */}
|
|
46
|
+
<Toil.Form
|
|
47
|
+
action={async (form) => {
|
|
48
|
+
await wait(400);
|
|
49
|
+
serverCount += Number(form.get('by') || 0);
|
|
50
|
+
}}
|
|
51
|
+
revalidate>
|
|
52
|
+
{({ pending }) => (
|
|
53
|
+
<>
|
|
54
|
+
<input name="by" type="number" defaultValue={5} disabled={pending} />
|
|
55
|
+
<button type="submit" disabled={pending}>
|
|
56
|
+
{pending ? 'Saving' : 'Add via <Form>'}
|
|
57
|
+
</button>
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</Toil.Form>
|
|
61
|
+
|
|
62
|
+
<p>
|
|
63
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
64
|
+
</p>
|
|
65
|
+
</main>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const metadata: Toil.Metadata = { title: 'Error boundary' };
|
|
4
|
-
|
|
5
|
-
// When a route throws during render, the nearest `error.tsx` catches it and renders instead of a
|
|
6
|
-
// blank screen. Click the button to flip into a throwing render and watch error.tsx take over.
|
|
7
|
-
export default function ErrorDemo() {
|
|
8
|
-
const [boom, setBoom] = useState(false);
|
|
9
|
-
if (boom) throw new Error('Kaboom from features/error, caught by error.tsx');
|
|
10
|
-
return (
|
|
11
|
-
<main>
|
|
12
|
-
<h1>Error boundary</h1>
|
|
13
|
-
<p>
|
|
14
|
-
A thrown render is caught by <code>error.tsx</code> in this folder, scoped to this
|
|
15
|
-
|
|
16
|
-
</p>
|
|
17
|
-
<p>
|
|
18
|
-
<button type="button" onClick={() => setBoom(true)}>
|
|
19
|
-
Throw an error
|
|
20
|
-
</button>
|
|
21
|
-
</p>
|
|
22
|
-
<p>
|
|
23
|
-
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
24
|
-
</p>
|
|
25
|
-
</main>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const metadata: Toil.Metadata = { title: 'Error boundary' };
|
|
4
|
+
|
|
5
|
+
// When a route throws during render, the nearest `error.tsx` catches it and renders instead of a
|
|
6
|
+
// blank screen. Click the button to flip into a throwing render and watch error.tsx take over.
|
|
7
|
+
export default function ErrorDemo() {
|
|
8
|
+
const [boom, setBoom] = useState(false);
|
|
9
|
+
if (boom) throw new Error('Kaboom from features/error, caught by error.tsx');
|
|
10
|
+
return (
|
|
11
|
+
<main>
|
|
12
|
+
<h1>Error boundary</h1>
|
|
13
|
+
<p>
|
|
14
|
+
A thrown render is caught by <code>error.tsx</code> in this folder, scoped to this segment so the rest
|
|
15
|
+
of the app keeps working.
|
|
16
|
+
</p>
|
|
17
|
+
<p>
|
|
18
|
+
<button type="button" onClick={() => setBoom(true)}>
|
|
19
|
+
Throw an error
|
|
20
|
+
</button>
|
|
21
|
+
</p>
|
|
22
|
+
<p>
|
|
23
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
24
|
+
</p>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
// The imperative head API, for when the title or tags depend on component state rather than a static
|
|
4
|
-
// export. `useTitle` / `useHead` apply for the component's lifetime and revert on unmount; `<Toil.Head>`
|
|
5
|
-
// is the declarative form. They compose with (and can override) the route `metadata`.
|
|
6
|
-
export default function HeadDemo() {
|
|
7
|
-
const [count, setCount] = useState(0);
|
|
8
|
-
|
|
9
|
-
// Live title: the tab updates every render as `count` changes.
|
|
10
|
-
Toil.useTitle(`Clicked ${count} times`);
|
|
11
|
-
|
|
12
|
-
// Add a meta tag and a canonical link for this page only.
|
|
13
|
-
Toil.useHead({
|
|
14
|
-
meta: [{ name: 'description', content: `Imperative head demo, clicked ${count} times.` }],
|
|
15
|
-
link: [{ rel: 'canonical', href: 'https://toil.example/features/head' }]
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<main>
|
|
20
|
-
<h1>Imperative head</h1>
|
|
21
|
-
<p>
|
|
22
|
-
The tab title is driven by component state via <code>Toil.useTitle</code>. Click the
|
|
23
|
-
|
|
24
|
-
</p>
|
|
25
|
-
<p>
|
|
26
|
-
<button type="button" onClick={() => setCount((c) => c + 1)}>
|
|
27
|
-
Clicked {count} times
|
|
28
|
-
</button>
|
|
29
|
-
</p>
|
|
30
|
-
{/* Declarative equivalent, the same merge rules apply. */}
|
|
31
|
-
<Toil.Head meta={[{ property: 'og:title', content: `Clicked ${count} times` }]} />
|
|
32
|
-
<p>
|
|
33
|
-
<Toil.Link href="/features/seo">Compare with route metadata</Toil.Link>{' '}
|
|
34
|
-
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
35
|
-
</p>
|
|
36
|
-
</main>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// The imperative head API, for when the title or tags depend on component state rather than a static
|
|
4
|
+
// export. `useTitle` / `useHead` apply for the component's lifetime and revert on unmount; `<Toil.Head>`
|
|
5
|
+
// is the declarative form. They compose with (and can override) the route `metadata`.
|
|
6
|
+
export default function HeadDemo() {
|
|
7
|
+
const [count, setCount] = useState(0);
|
|
8
|
+
|
|
9
|
+
// Live title: the tab updates every render as `count` changes.
|
|
10
|
+
Toil.useTitle(`Clicked ${count} times`);
|
|
11
|
+
|
|
12
|
+
// Add a meta tag and a canonical link for this page only.
|
|
13
|
+
Toil.useHead({
|
|
14
|
+
meta: [{ name: 'description', content: `Imperative head demo, clicked ${count} times.` }],
|
|
15
|
+
link: [{ rel: 'canonical', href: 'https://toil.example/features/head' }]
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<main>
|
|
20
|
+
<h1>Imperative head</h1>
|
|
21
|
+
<p>
|
|
22
|
+
The tab title is driven by component state via <code>Toil.useTitle</code>. Click the button and watch it
|
|
23
|
+
change, then leave the page and the title reverts.
|
|
24
|
+
</p>
|
|
25
|
+
<p>
|
|
26
|
+
<button type="button" onClick={() => setCount((c) => c + 1)}>
|
|
27
|
+
Clicked {count} times
|
|
28
|
+
</button>
|
|
29
|
+
</p>
|
|
30
|
+
{/* Declarative equivalent, the same merge rules apply. */}
|
|
31
|
+
<Toil.Head meta={[{ property: 'og:title', content: `Clicked ${count} times` }]} />
|
|
32
|
+
<p>
|
|
33
|
+
<Toil.Link href="/features/seo">Compare with route metadata</Toil.Link>{' '}
|
|
34
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
35
|
+
</p>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -1,75 +1,83 @@
|
|
|
1
|
-
// The feature hub: one place that links to a live demo of every ToilJS capability. Its own tab
|
|
2
|
-
// title comes from the `metadata` export below (rendered as "Features | ToilJS" via the layout
|
|
3
|
-
// template) and is baked into build/client/features/index.html at build time.
|
|
4
|
-
export const metadata: Toil.Metadata = {
|
|
5
|
-
title: 'Features',
|
|
6
|
-
description: 'Live demos of every ToilJS feature: routing, data, head/SEO, components, realtime, and binary IO.',
|
|
7
|
-
openGraph: { title: 'Every ToilJS feature, demoed', type: 'website' }
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const groups: { heading: string; items: { href: Toil.Href; label: string; note: string }[] }[] = [
|
|
11
|
-
{
|
|
12
|
-
heading: 'Routing',
|
|
13
|
-
items: [
|
|
14
|
-
{ href: '/blog/42', label: 'Dynamic route', note: 'blog/[id].tsx, /blog/42' },
|
|
15
|
-
{ href: '/docs/getting/started', label: 'Catch-all', note: 'docs/[...slug].tsx' },
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{ href: '/features/
|
|
42
|
-
{ href: '/
|
|
43
|
-
]
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
// The feature hub: one place that links to a live demo of every ToilJS capability. Its own tab
|
|
2
|
+
// title comes from the `metadata` export below (rendered as "Features | ToilJS" via the layout
|
|
3
|
+
// template) and is baked into build/client/features/index.html at build time.
|
|
4
|
+
export const metadata: Toil.Metadata = {
|
|
5
|
+
title: 'Features',
|
|
6
|
+
description: 'Live demos of every ToilJS feature: routing, data, head/SEO, components, realtime, and binary IO.',
|
|
7
|
+
openGraph: { title: 'Every ToilJS feature, demoed', type: 'website' }
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const groups: { heading: string; items: { href: Toil.Href; label: string; note: string }[] }[] = [
|
|
11
|
+
{
|
|
12
|
+
heading: 'Routing',
|
|
13
|
+
items: [
|
|
14
|
+
{ href: '/blog/42', label: 'Dynamic route', note: 'blog/[id].tsx, /blog/42' },
|
|
15
|
+
{ href: '/docs/getting/started', label: 'Catch-all', note: 'docs/[...slug].tsx' },
|
|
16
|
+
{
|
|
17
|
+
href: '/files',
|
|
18
|
+
label: 'Optional catch-all',
|
|
19
|
+
note: 'files/[[...slug]].tsx, matches /files and /files/a/b'
|
|
20
|
+
},
|
|
21
|
+
{ href: '/privacy', label: 'Route group', note: '(legal)/privacy.tsx, no URL segment' },
|
|
22
|
+
{ href: '/gallery', label: 'Parallel + intercepting', note: '@modal/(.)photo/[id], a real modal route' },
|
|
23
|
+
{ href: '/features/template', label: 'Templates', note: 'template.tsx remounts on every navigation' },
|
|
24
|
+
{ href: '/features/error', label: 'Error boundary', note: 'error.tsx catches a thrown route' }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
heading: 'Data',
|
|
29
|
+
items: [
|
|
30
|
+
{ href: '/loader-demo', label: 'Loader + revalidate', note: 'data before render, cached, refetchable' },
|
|
31
|
+
{
|
|
32
|
+
href: '/features/actions',
|
|
33
|
+
label: 'Actions + Form',
|
|
34
|
+
note: 'useAction / <Form>, pending state, revalidate'
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
heading: 'Head and SEO',
|
|
40
|
+
items: [
|
|
41
|
+
{ href: '/features/seo', label: 'Route metadata', note: 'export const metadata, title override' },
|
|
42
|
+
{ href: '/features/head', label: 'Imperative head', note: 'useTitle / useHead / <Head>' }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
heading: 'Components and runtime',
|
|
47
|
+
items: [
|
|
48
|
+
{ href: '/features/script', label: 'Script', note: 'Toil.Script with a load strategy' },
|
|
49
|
+
{ href: '/features/realtime', label: 'WebSocket channel', note: 'Toil.useChannel against /_toil' },
|
|
50
|
+
{ href: '/io', label: 'Binary IO', note: 'BinaryWriter / BinaryReader / FastSet, no import' }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export default function Features() {
|
|
56
|
+
return (
|
|
57
|
+
<main>
|
|
58
|
+
<h1>Every feature, live</h1>
|
|
59
|
+
<p>
|
|
60
|
+
Each link is a working demo served from <code>client/routes/</code>. Watch the tab title change as you
|
|
61
|
+
navigate, that is the per-route <code>metadata</code> at work.
|
|
62
|
+
</p>
|
|
63
|
+
{groups.map((g) => (
|
|
64
|
+
<section key={g.heading} style={{ marginTop: 24 }}>
|
|
65
|
+
<h2 style={{ fontSize: '1rem', opacity: 0.7 }}>{g.heading}</h2>
|
|
66
|
+
<ul style={{ display: 'grid', gap: 8, listStyle: 'none', padding: 0 }}>
|
|
67
|
+
{g.items.map((it) => (
|
|
68
|
+
<li key={it.href}>
|
|
69
|
+
<Toil.Link href={it.href} style={{ fontWeight: 600 }}>
|
|
70
|
+
{it.label}
|
|
71
|
+
</Toil.Link>
|
|
72
|
+
<span style={{ opacity: 0.6 }}> , {it.note}</span>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
</section>
|
|
77
|
+
))}
|
|
78
|
+
<p style={{ marginTop: 24 }}>
|
|
79
|
+
<Toil.Link href="/">Back home</Toil.Link>
|
|
80
|
+
</p>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
export const metadata: Toil.Metadata = {
|
|
2
|
-
title: 'Realtime',
|
|
3
|
-
description: 'A typed WebSocket channel to the server with connect, reconnect, and message decoding.'
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
// Toil.useChannel opens a WebSocket to the server (default path /_toil), tracks `connected`, collects
|
|
7
|
-
// `messages`, and exposes `send`. It reconnects automatically. With no server socket running the demo
|
|
8
|
-
// simply shows "disconnected", the API is the same once the server handles the channel.
|
|
9
|
-
export default function RealtimeDemo() {
|
|
10
|
-
const chat = Toil.useChannel({ path: '/_toil' });
|
|
11
|
-
return (
|
|
12
|
-
<main>
|
|
13
|
-
<h1>Realtime</h1>
|
|
14
|
-
<p>
|
|
15
|
-
Connection: <strong>{chat.connected ? 'connected' : 'disconnected'}</strong>, messages
|
|
16
|
-
|
|
17
|
-
</p>
|
|
18
|
-
<p>
|
|
19
|
-
<button type="button" onClick={() => chat.send('ping')}>
|
|
20
|
-
Send ping
|
|
21
|
-
</button>
|
|
22
|
-
</p>
|
|
23
|
-
<p style={{ opacity: 0.6 }}>
|
|
24
|
-
<code>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
export const metadata: Toil.Metadata = {
|
|
2
|
+
title: 'Realtime',
|
|
3
|
+
description: 'A typed WebSocket channel to the server with connect, reconnect, and message decoding.'
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// Toil.useChannel opens a WebSocket to the server (default path /_toil), tracks `connected`, collects
|
|
7
|
+
// `messages`, and exposes `send`. It reconnects automatically. With no server socket running the demo
|
|
8
|
+
// simply shows "disconnected", the API is the same once the server handles the channel.
|
|
9
|
+
export default function RealtimeDemo() {
|
|
10
|
+
const chat = Toil.useChannel({ path: '/_toil' });
|
|
11
|
+
return (
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Realtime</h1>
|
|
14
|
+
<p>
|
|
15
|
+
Connection: <strong>{chat.connected ? 'connected' : 'disconnected'}</strong>, messages received:{' '}
|
|
16
|
+
<strong>{chat.messages.length}</strong>.
|
|
17
|
+
</p>
|
|
18
|
+
<p>
|
|
19
|
+
<button type="button" onClick={() => chat.send('ping')}>
|
|
20
|
+
Send ping
|
|
21
|
+
</button>
|
|
22
|
+
</p>
|
|
23
|
+
<p style={{ opacity: 0.6 }}>
|
|
24
|
+
<code>
|
|
25
|
+
const chat = Toil.useChannel({'{'} path: '/_toil' {'}'})
|
|
26
|
+
</code>
|
|
27
|
+
, connect, reconnect, and decoding are handled for you.
|
|
28
|
+
</p>
|
|
29
|
+
<p>
|
|
30
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
31
|
+
</p>
|
|
32
|
+
</main>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const metadata: Toil.Metadata = {
|
|
4
|
-
title: 'Script',
|
|
5
|
-
description: 'Load third-party scripts with a strategy, deduplicated so they never run twice.'
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
// Toil.Script injects an external or inline script once (deduped by src/id), with a load strategy.
|
|
9
|
-
// `onReady` fires when it has loaded. Here we load a tiny public script and report when it is ready.
|
|
10
|
-
export default function ScriptDemo() {
|
|
11
|
-
const [ready, setReady] = useState(false);
|
|
12
|
-
return (
|
|
13
|
-
<main>
|
|
14
|
-
<h1>Script</h1>
|
|
15
|
-
<p>
|
|
16
|
-
<code>Toil.Script</code> loads external scripts with a <code>strategy</code>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</p>
|
|
20
|
-
<Toil.Script
|
|
21
|
-
src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"
|
|
22
|
-
strategy="afterInteractive"
|
|
23
|
-
onReady={() => setReady(true)}
|
|
24
|
-
/>
|
|
25
|
-
<p>Status: {ready ? 'script ready' : 'loading'}</p>
|
|
26
|
-
<p>
|
|
27
|
-
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
28
|
-
</p>
|
|
29
|
-
</main>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const metadata: Toil.Metadata = {
|
|
4
|
+
title: 'Script',
|
|
5
|
+
description: 'Load third-party scripts with a strategy, deduplicated so they never run twice.'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Toil.Script injects an external or inline script once (deduped by src/id), with a load strategy.
|
|
9
|
+
// `onReady` fires when it has loaded. Here we load a tiny public script and report when it is ready.
|
|
10
|
+
export default function ScriptDemo() {
|
|
11
|
+
const [ready, setReady] = useState(false);
|
|
12
|
+
return (
|
|
13
|
+
<main>
|
|
14
|
+
<h1>Script</h1>
|
|
15
|
+
<p>
|
|
16
|
+
<code>Toil.Script</code> loads external scripts with a <code>strategy</code> (
|
|
17
|
+
<code>afterInteractive</code>, <code>lazyOnload</code>, <code>beforeInteractive</code>) and dedupes
|
|
18
|
+
them, so the same script never runs twice across navigations.
|
|
19
|
+
</p>
|
|
20
|
+
<Toil.Script
|
|
21
|
+
src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"
|
|
22
|
+
strategy="afterInteractive"
|
|
23
|
+
onReady={() => setReady(true)}
|
|
24
|
+
/>
|
|
25
|
+
<p>Status: {ready ? 'script ready' : 'loading'}</p>
|
|
26
|
+
<p>
|
|
27
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
28
|
+
</p>
|
|
29
|
+
</main>
|
|
30
|
+
);
|
|
31
|
+
}
|