From e0da55b99021598d4e142f1ce97b7a32b2a4f82e Mon Sep 17 00:00:00 2001 From: Karn Date: Wed, 12 Aug 2026 01:56:00 +0530 Subject: [PATCH 1/3] Put the setup walkthrough on the site, and stop apologising for the relay The page argued with drawings. Every window on it is one: the fleet, the switcher, the relay transcript. Drawings argue well and prove nothing, and the question live at the moment somebody is about to pipe a script into a shell is not "how does it work", it is "does this actually work". So the recording goes in the Install section, between the claim and the command, and on /docs/setup it goes first, ahead of a single instruction, because somebody who opened that page wants to know what they are in for before they start. It opens on a click, not on arrival. Two reasons, both load bearing. The routes here are prerendered to static HTML, and the Mux engine registers a custom element that has nothing to render on a server, so gating it on a click puts it out of that reach by construction: no mounted flag, no ClientOnly, the state starts false on both sides and the first render matches. And the engine is 217 kB gzipped against a page whose whole bundle is 101 kB, on a video nine minutes long that nobody arrived to watch. What is served instead is a real button around a real frame, which is also what a reader with no JavaScript keeps. Eager cost is 2.2 kB gzipped; the engine is fetched when a mouse reaches the card. The player is @karnstack/kino. Its accent is handed over as var(--primary) rather than as a colour, since kino assigns the prop straight to --kino-accent, so the value resolves inside whichever theme the page is wearing and the header's toggle moves the scrubber with everything else. The closed card is drawn to kino's own measurements, the same 76px control and the same easing, so opening the video swaps a control for its twin in the same place. The Status copy was also a version behind. It said the relay had not been through its manual end-to-end gate, which the recording it now sits next to is: a real Cloudflare account, a phone paired from another network, a second machine joined. v0.5.1 is released and installable. What stays is that it is 0.x, said as a fact rather than as an apology. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 18 ++- site/package.json | 1 + site/pnpm-lock.yaml | 75 +++++++++++ site/src/components/walkthrough-player.tsx | 43 +++++++ site/src/components/walkthrough.tsx | 142 +++++++++++++++++++++ site/src/lib/site.ts | 33 +++++ site/src/routes/docs.setup.tsx | 14 ++ site/src/routes/index.tsx | 19 ++- 8 files changed, 337 insertions(+), 8 deletions(-) create mode 100644 site/src/components/walkthrough-player.tsx create mode 100644 site/src/components/walkthrough.tsx diff --git a/README.md b/README.md index cef5181..78fbae4 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,18 @@ browser loads its JavaScript from that origin, is in the ## Status -Pre-1.0, and honest about it. The local terminal, the login service, the -fleet-wide sessions list and the end-to-end pairing all work. The Cloudflare -relay is built and deployable but has not been through its manual end-to-end -gate against a real account ([docs/RELAY.md](docs/RELAY.md)), so treat it as -ready to try rather than ready to rely on. Known rough edges live in -[docs/FOLLOW-UPS.md](docs/FOLLOW-UPS.md). +Released and in daily use. v0.5.1 is the current release, `brew install +karnstack/tap/flue` gets it, and the whole of it works: the local terminal, the +login service, the fleet-wide sessions list, pairing, and the Cloudflare relay. +The relay has been through its manual end-to-end gate +([docs/RELAY.md](docs/RELAY.md)) against a real account, with a phone on a +different network paired to it and a second machine joined to the same relay. +There is a recording of that run at +[flue.sh/docs/setup](https://flue.sh/docs/setup). + +It is 0.x, which means what it usually means: commands, flags and the config +file can still change between releases, and an upgrade may ask something of +you. Known rough edges live in [docs/FOLLOW-UPS.md](docs/FOLLOW-UPS.md). flue is open source and always free. diff --git a/site/package.json b/site/package.json index fbfdd84..1b58c09 100644 --- a/site/package.json +++ b/site/package.json @@ -13,6 +13,7 @@ "deploy": "wrangler deploy" }, "dependencies": { + "@karnstack/kino": "^0.12.0", "@tanstack/react-router": "1.170.17", "@tanstack/react-start": "1.168.27", "class-variance-authority": "^0.7.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index d512239..59c6a56 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@karnstack/kino': + specifier: ^0.12.0 + version: 0.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-router': specifier: 1.170.17 version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -539,6 +542,22 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@karnstack/kino@0.12.0': + resolution: {integrity: sha512-kwrpXfSNPY+lSVms4uI62TQkGRPdPgLs0AhTNfU9FPqtZI7jm2A96B60W+LAYqhWfuxPtt6ZNf3UijIvWyivWg==} + engines: {node: '>=24'} + peerDependencies: + react: '>=19' + react-dom: '>=19' + + '@mux/mux-data-google-ima@0.3.17': + resolution: {integrity: sha512-4wpH6dYybyZhqLn9qGn/+67Z8MZnQRAdqTFEEZw2bx61M9q01uPYYHxd8qwOnYtUGEeafsdTwVHVxKHGD3oc1A==} + + '@mux/mux-video@0.31.2': + resolution: {integrity: sha512-waahxlrat1PpRzcY2pwMQJ5Gyhos+D6iRyKnYzF0wBf4SESfBtunSBj3EuKuljypm8bamh9gWnlA0LSzm5Pz1w==} + + '@mux/playback-core@0.35.2': + resolution: {integrity: sha512-AsMV0LMwJm9T1ig6BBYSddeMlkzz+2RGcd6dkOgYk+L5Hx477ovbTxJZJ/vTW+rZjNWHcG3BvgNbSLgiubyZhw==} + '@napi-rs/wasm-runtime@1.2.2': resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -1599,6 +1618,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/google_interactive_media_ads_types@3.697.1': + resolution: {integrity: sha512-gRjJoPKQRjjTuySHqZe8Pnu6a5B0bKbTUD2ioDLPSWlEuBaAc2p1mzUzy42SCeWkyYffo2Mnv+blUGuvVvx3dQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1650,6 +1672,9 @@ packages: caniuse-lite@1.0.30001809: resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + castable-video@1.1.16: + resolution: {integrity: sha512-wBhe2dZu2afhewL3EaGgVYTyDsa9HvNhY98clMZkNzDrLelOValSrTaoMos9YX7PPBCrgpd1j6YmNyyI2Vbq3w==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -1674,6 +1699,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + custom-media-element@1.4.6: + resolution: {integrity: sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1754,6 +1782,9 @@ packages: crossws: optional: true + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} + isbot@5.2.1: resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} engines: {node: '>=18'} @@ -1942,6 +1973,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + media-tracks@0.3.5: + resolution: {integrity: sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA==} + miniflare@4.20260701.0: resolution: {integrity: sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==} engines: {node: '>=22.0.0'} @@ -1950,6 +1984,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mux-embed@5.18.1: + resolution: {integrity: sha512-ePsHjiEKY+FgrSBiMmaF+LOtTQSSBWv/1zqpREQFN96JE93xlsArT/MEi30yKOE06MgjOlL70YI750molu3y7g==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2655,6 +2692,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@karnstack/kino@0.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@mux/mux-video': 0.31.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@mux/mux-data-google-ima@0.3.17': + dependencies: + mux-embed: 5.18.1 + + '@mux/mux-video@0.31.2': + dependencies: + '@mux/mux-data-google-ima': 0.3.17 + '@mux/playback-core': 0.35.2 + '@types/google_interactive_media_ads_types': 3.697.1 + castable-video: 1.1.16 + custom-media-element: 1.4.6 + media-tracks: 0.3.5 + + '@mux/playback-core@0.35.2': + dependencies: + hls.js: 1.6.17 + mux-embed: 5.18.1 + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -3784,6 +3845,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/google_interactive_media_ads_types@3.697.1': {} + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -3828,6 +3891,10 @@ snapshots: caniuse-lite@1.0.30001809: {} + castable-video@1.1.16: + dependencies: + custom-media-element: 1.4.6 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -3846,6 +3913,8 @@ snapshots: csstype@3.2.3: {} + custom-media-element@1.4.6: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -3918,6 +3987,8 @@ snapshots: rou3: 0.8.1 srvx: 0.11.22 + hls.js@1.6.17: {} + isbot@5.2.1: {} jiti@2.7.0: {} @@ -4044,6 +4115,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + media-tracks@0.3.5: {} + miniflare@4.20260701.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -4058,6 +4131,8 @@ snapshots: ms@2.1.3: {} + mux-embed@5.18.1: {} + nanoid@3.3.18: {} node-releases@2.0.53: {} diff --git a/site/src/components/walkthrough-player.tsx b/site/src/components/walkthrough-player.tsx new file mode 100644 index 0000000..f42832f --- /dev/null +++ b/site/src/components/walkthrough-player.tsx @@ -0,0 +1,43 @@ +import { MuxPlayer } from '@karnstack/kino/mux' +import '@karnstack/kino/styles.css' + +import { WALKTHROUGH_BLUR, WALKTHROUGH_PLAYBACK_ID, WALKTHROUGH_POSTER } from '@/lib/site' + +/** + * The player, alone in its own chunk. + * + * Nothing names this file with a static import. Walkthrough reaches it through + * import() and nothing else does, which is what keeps the adaptive streaming + * engine and kino's stylesheet out of the bytes every visitor downloads. It is + * also what keeps them out of the prerender: every route here is rendered to + * static HTML at build time, and the engine registers a custom element, which + * has no meaning on a server. + * + * The accent is handed over as `var(--primary)` rather than as a colour. kino + * assigns the prop straight to --kino-accent on its own root, so the value is + * resolved where it is used, inside whichever theme the page is wearing, and + * the header's toggle moves the scrubber along with everything else without a + * re-render. A literal would have pinned it to one theme and been wrong in the + * other. + * + * `tokens` is absent on purpose: playback is public, so there is nothing to + * sign and no key to hold. + */ +export default function WalkthroughPlayer() { + return ( + + ) +} diff --git a/site/src/components/walkthrough.tsx b/site/src/components/walkthrough.tsx new file mode 100644 index 0000000..341f8b7 --- /dev/null +++ b/site/src/components/walkthrough.tsx @@ -0,0 +1,142 @@ +import { Clapperboard, Play } from 'lucide-react' +import { lazy, Suspense, useState } from 'react' + +import { WALKTHROUGH_BLUR, WALKTHROUGH_POSTER, WALKTHROUGH_RUNTIME } from '@/lib/site' +import { cn } from '@/lib/utils' + +const Player = lazy(() => import('@/components/walkthrough-player')) + +/** + * Fetch the player on intent rather than on arrival. + * + * A mouse reaching the card, or a keyboard reaching the button, is the last + * warning before a click, and it is usually enough time to have the chunk + * already there when the click lands. Calling import() twice costs nothing: + * the second call gets the promise the first one made. + */ +function warm() { + void import('@/components/walkthrough-player') +} + +/** + * The dark panel, cut the way every other window on this site is cut. + * MockTerminal, FleetWindow and the Shell blocks in the docs all wear it, and + * the recording is the only one of them that is not a drawing. + */ +const PANEL = + '@container relative aspect-video overflow-hidden rounded-xl bg-zinc-950 ring-1 ring-zinc-950/10 dark:ring-white/10' + +const CAPTION = `${WALKTHROUGH_RUNTIME}. Install, one relay in your own Cloudflare account, a phone paired from a QR code, then a second machine.` + +/** + * The setup walkthrough, shut until somebody asks for it. + * + * It opens on a click rather than on arrival for two reasons, and both of them + * are load bearing. + * + * The pages here are prerendered to static HTML at build time, so every route + * is rendered on a server first, and the streaming engine behind this video is + * a custom element that has nothing to render there. Gating it on a click puts + * it out of that reach by construction. There is no mounted flag and no + * ClientOnly wrapper, because the state that gates it starts false on both + * sides, so the first render matches and hydration has nothing to reconcile. + * + * And it is nearly nine minutes long. On a page nobody arrived at to watch a + * video, the honest default is a frame from it with the runtime written + * underneath, and the engine costs nothing until somebody wants it. What is + * served is a real button around a real image, which is also what a reader + * with no JavaScript keeps. + */ +export function Walkthrough({ + align = 'left', + className, +}: { + /** Follows the section it lands in, the same way InstallBlock does. */ + align?: 'left' | 'center' + className?: string +}) { + const [playing, setPlaying] = useState(false) + + return ( +
+ {playing ? ( +
+ }> + + +
+ ) : ( + + )} +
+
+
+ ) +} + +/** + * What the card holds before anything is asked of it: the 24px copy first, so + * there is something to look at in the first paint, and the full frame over it + * once that decodes. It is the same pair kino stacks when the player opens, + * from the same two URLs, so the handover has nothing to redraw. + */ +function Still() { + return ( + <> + + + + ) +} + +/** + * The play control, drawn to kino's own measurements. + * + * Same geometry as the one the player shows while it is idle: max(76px, 9cqw) + * across, a max(32px, 3.8cqw) triangle in it, the same wash of black and the + * same easing. So opening the video swaps a control for its twin in the same + * spot rather than cutting to a different one. The cqw half is why the panel + * opens a query scope of its own, since kino sizes against its own width too. + * + * The lengths are pixels rather than spacing steps because they are quoted + * from a stylesheet this file does not own. A rounder number here would drift + * away from kino the next time either side moved. + */ +function PlayBadge() { + return ( + + + ) +} diff --git a/site/src/lib/site.ts b/site/src/lib/site.ts index 34f0c1f..b416972 100644 --- a/site/src/lib/site.ts +++ b/site/src/lib/site.ts @@ -4,3 +4,36 @@ export const X_URL = 'https://x.com/gyankarn' export const INSTALL_CMD = 'curl -fsSL https://flue.sh/install.sh | sh' export const BREW_CMD = 'brew install karnstack/tap/flue' + +/** + * The setup walkthrough, on Mux with public playback. + * + * There is no signing key here and nothing to mint at runtime. A playback id + * is the whole of what it takes to watch this, and it is not a secret. + */ +export const WALKTHROUGH_PLAYBACK_ID = 'vnrDD86c33LvJ7Ftv6uIWlEDTGH6dxqnE3cH6e02pZ94' + +/** How long it runs. Written in the caption, and read out in the button label. */ +export const WALKTHROUGH_RUNTIME = '8:51' + +/** + * One frame, named once and used twice: the closed card paints it, and the + * player is handed the same URL as its poster when it opens. Naming it twice + * is what makes the swap seamless, because the second request is a cache hit + * rather than a fresh fetch across an empty rectangle. + * + * 5:30 is the second worth choosing. It is the Ubuntu machine taking the + * install line with the sessions page already up behind it, which is the most + * legible frame in the run and one of the few that says what the whole thing + * is about. Several of the later ones show a relay secret, so they are out. + */ +export const WALKTHROUGH_POSTER = `https://image.mux.com/${WALKTHROUGH_PLAYBACK_ID}/thumbnail.webp?time=330&width=1200` + +/** + * That same frame at 24px across, written out here at 263 bytes so the card + * has something to paint before anything is fetched. kino lays it under the + * poster the same way once the player opens, which is why both halves are + * given it. + */ +export const WALKTHROUGH_BLUR = + 'data:image/webp;base64,UklGRqwAAABXRUJQVlA4IKAAAAAwBACdASoYAA4APlEgjUQjoiEYBAA4BQSzAFiNqALX4WRJCDRqDujU6YwA/v8rl6RU7q8kvuFZDpsXENrifU0Ahz/kXdneUSBeBVkGphdpCdwmD5sUyoP+dwh+2GmbntH5//OW/GnmdJ0Flr88cz3OPlY3GzJprjYUdTTcvQwS8I3YClHd8aXjjLMDn/p/IzRA7SwbBX3zW5/NZT35QAAA' diff --git a/site/src/routes/docs.setup.tsx b/site/src/routes/docs.setup.tsx index ecb2f57..4061db9 100644 --- a/site/src/routes/docs.setup.tsx +++ b/site/src/routes/docs.setup.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Code, DocPage, Lead, Link, Note, P, Section, Shell, Step, Steps } from '@/components/doc-page' +import { Walkthrough } from '@/components/walkthrough' import { docTitle, findDoc } from '@/lib/docs' const DOC = findDoc('setup')! @@ -15,6 +16,19 @@ export const Route = createFileRoute('/docs/setup')({ function Setup() { return ( + {/* First, before a single command. Somebody who opened this page has + already decided to install flue, and the thing they want to know + before they start is how long it takes and what it looks like when + it works. The steps below answer neither, and eight minutes of + somebody doing it answers both. */} +
+ + Every step below, run on two machines: a Mac from nothing, then an Ubuntu box joining the + same relay. + + +
+
One relay. Every machine joined to it. Every device paired once. Everything below is a diff --git a/site/src/routes/index.tsx b/site/src/routes/index.tsx index b586e38..c1fda52 100644 --- a/site/src/routes/index.tsx +++ b/site/src/routes/index.tsx @@ -8,6 +8,7 @@ import { SwitcherWindow } from '@/components/mock/switcher' import { MockTerminal, ok, output, prompt } from '@/components/mock/terminal' import { SiteFooter } from '@/components/site-footer' import { SiteHeader } from '@/components/site-header' +import { Walkthrough } from '@/components/walkthrough' import { GithubMark } from '@/components/wordmark' import { BREW_CMD, INSTALL_CMD, REPO_URL } from '@/lib/site' @@ -335,8 +336,7 @@ function Remote() {

Everything crossing it is end-to-end encrypted with Noise IK. Your browser pins the daemon’s key when it pairs, so the Worker forwards ciphertext it holds no key - for. The relay is new: it works, but it has not been through its release gate, so - treat it as ready to try rather than ready to rely on. + for. It is a 0.x release, so commands and config can still change between versions.

@@ -432,6 +442,11 @@ function Install() { .

+ {/* Full width, the way the switcher above sits full width in a + section that is otherwise columns. The caption is centred with + the rest of this section rather than left aligned, since it is + the only line here that would have read off axis. */} +
From 076989cefae631a0d4e7f037d774eb9031e4e0ee Mon Sep 17 00:00:00 2001 From: Karn Date: Wed, 12 Aug 2026 02:08:03 +0530 Subject: [PATCH 2/3] Stop the walkthrough card impersonating kino's own control The closed card had grown a copy of kino's play button, drawn from measurements quoted out of kino's stylesheet: max(76px,9cqw) across, a max(32px,3.8cqw) triangle in it, kino's easing curve, and a container query on the panel so the cqw halves resolved against the same width. The point was that opening the video swapped a control for its twin in the same spot. It is not worth what it costs. Those lengths belong to a package this file does not own, so the twin drifts apart the first time either side moves, and nothing fails when it does. The card also stacked its own blur-up under the poster, which is the job kino's placeholder prop already does once the player mounts. What stays is the part that earns its keep: the engine is 218 kB gzipped against a site whose whole bundle is 101 kB, so it is still fetched on a click rather than on arrival, which also keeps the custom element out of a build that prerenders every route. A poster and a plain play button stand in until then. 142 lines to 84. --- site/src/components/walkthrough.tsx | 138 ++++++++-------------------- 1 file changed, 40 insertions(+), 98 deletions(-) diff --git a/site/src/components/walkthrough.tsx b/site/src/components/walkthrough.tsx index 341f8b7..0b9fef4 100644 --- a/site/src/components/walkthrough.tsx +++ b/site/src/components/walkthrough.tsx @@ -1,51 +1,32 @@ import { Clapperboard, Play } from 'lucide-react' import { lazy, Suspense, useState } from 'react' -import { WALKTHROUGH_BLUR, WALKTHROUGH_POSTER, WALKTHROUGH_RUNTIME } from '@/lib/site' +import { WALKTHROUGH_POSTER, WALKTHROUGH_RUNTIME } from '@/lib/site' import { cn } from '@/lib/utils' const Player = lazy(() => import('@/components/walkthrough-player')) /** - * Fetch the player on intent rather than on arrival. - * - * A mouse reaching the card, or a keyboard reaching the button, is the last - * warning before a click, and it is usually enough time to have the chunk - * already there when the click lands. Calling import() twice costs nothing: - * the second call gets the promise the first one made. - */ -function warm() { - void import('@/components/walkthrough-player') -} - -/** - * The dark panel, cut the way every other window on this site is cut. - * MockTerminal, FleetWindow and the Shell blocks in the docs all wear it, and - * the recording is the only one of them that is not a drawing. + * The panel, cut the way every other window on this site is cut. MockTerminal, + * FleetWindow and the docs Shell blocks all wear it. */ const PANEL = - '@container relative aspect-video overflow-hidden rounded-xl bg-zinc-950 ring-1 ring-zinc-950/10 dark:ring-white/10' - -const CAPTION = `${WALKTHROUGH_RUNTIME}. Install, one relay in your own Cloudflare account, a phone paired from a QR code, then a second machine.` + 'relative aspect-video overflow-hidden rounded-xl bg-zinc-950 ring-1 ring-zinc-950/10 dark:ring-white/10' /** - * The setup walkthrough, shut until somebody asks for it. - * - * It opens on a click rather than on arrival for two reasons, and both of them - * are load bearing. + * The setup walkthrough, mounted on a click. * - * The pages here are prerendered to static HTML at build time, so every route - * is rendered on a server first, and the streaming engine behind this video is - * a custom element that has nothing to render there. Gating it on a click puts - * it out of that reach by construction. There is no mounted flag and no - * ClientOnly wrapper, because the state that gates it starts false on both - * sides, so the first render matches and hydration has nothing to reconcile. + * The click is not decoration. kino pulls in an adaptive streaming engine, and + * that chunk is 218 kB gzipped against a site whose entire bundle is 101 kB, so + * mounting it on arrival would triple what every visitor downloads for a video + * most of them will not play. Deferring it also keeps the custom element out of + * the prerender, which renders every route to static HTML at build time and has + * nothing to do with a custom element. * - * And it is nearly nine minutes long. On a page nobody arrived at to watch a - * video, the honest default is a frame from it with the runtime written - * underneath, and the engine costs nothing until somebody wants it. What is - * served is a real button around a real image, which is also what a reader - * with no JavaScript keeps. + * What stands in for it until then is a poster and a play button. Deliberately + * not a copy of kino's own control: matching that pixel for pixel means copying + * measurements out of a stylesheet this file does not own, and it goes wrong + * the first time either side changes. */ export function Walkthrough({ align = 'left', @@ -59,28 +40,25 @@ export function Walkthrough({ return (
- {playing ? ( -
- }> +
+ {playing ? ( + }> -
- ) : ( - - )} + ) : ( + + )} +
) } -/** - * What the card holds before anything is asked of it: the 24px copy first, so - * there is something to look at in the first paint, and the full frame over it - * once that decodes. It is the same pair kino stacks when the player opens, - * from the same two URLs, so the handover has nothing to redraw. - */ -function Still() { - return ( - <> - - - - ) -} - -/** - * The play control, drawn to kino's own measurements. - * - * Same geometry as the one the player shows while it is idle: max(76px, 9cqw) - * across, a max(32px, 3.8cqw) triangle in it, the same wash of black and the - * same easing. So opening the video swaps a control for its twin in the same - * spot rather than cutting to a different one. The cqw half is why the panel - * opens a query scope of its own, since kino sizes against its own width too. - * - * The lengths are pixels rather than spacing steps because they are quoted - * from a stylesheet this file does not own. A rounder number here would drift - * away from kino the next time either side moved. - */ -function PlayBadge() { +function Poster() { return ( - - + ) } From 2c078115f1c93e895a327e8ba9c2300a582d9e1b Mon Sep 17 00:00:00 2001 From: Karn Date: Wed, 12 Aug 2026 02:32:04 +0530 Subject: [PATCH 3/3] Render the player, rather than a picture of one Opening the walkthrough took two clicks. The first swapped a poster and a hand-drawn play button for kino, the second started it, and the first one bought nothing a viewer wanted. It was there to defer the streaming engine, and it paid for that with the one interaction the section exists for. So the player is imported and rendered like any other component, and the facade is gone. /docs/setup and / carry 327 kB gzipped where they carried 101 kB, and that is the honest price of putting a video on a page: if it is worth being there it is worth loading. The prerender does not mind. The custom element registers on import, every route here is rendered to static HTML at build time, and all six still prerender. Also drops autoPlay, which caused two audio tracks to play a beat apart. kino builds its in a mount effect and sets autoplay on the element there, while its teardown calls remove(), which detaches an element without pausing it. React runs a mount effect twice in development, so the first element was left detached, playing, and unreachable, with no control on the page able to stop it because the provider that owned it was gone. Measured two created against one attached. The leak is still kino's to fix; nothing here starts it now. The viewer presses play, which is what they came to do. --- site/src/components/walkthrough-player.tsx | 43 ----------- site/src/components/walkthrough.tsx | 88 +++++++++------------- 2 files changed, 35 insertions(+), 96 deletions(-) delete mode 100644 site/src/components/walkthrough-player.tsx diff --git a/site/src/components/walkthrough-player.tsx b/site/src/components/walkthrough-player.tsx deleted file mode 100644 index f42832f..0000000 --- a/site/src/components/walkthrough-player.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { MuxPlayer } from '@karnstack/kino/mux' -import '@karnstack/kino/styles.css' - -import { WALKTHROUGH_BLUR, WALKTHROUGH_PLAYBACK_ID, WALKTHROUGH_POSTER } from '@/lib/site' - -/** - * The player, alone in its own chunk. - * - * Nothing names this file with a static import. Walkthrough reaches it through - * import() and nothing else does, which is what keeps the adaptive streaming - * engine and kino's stylesheet out of the bytes every visitor downloads. It is - * also what keeps them out of the prerender: every route here is rendered to - * static HTML at build time, and the engine registers a custom element, which - * has no meaning on a server. - * - * The accent is handed over as `var(--primary)` rather than as a colour. kino - * assigns the prop straight to --kino-accent on its own root, so the value is - * resolved where it is used, inside whichever theme the page is wearing, and - * the header's toggle moves the scrubber along with everything else without a - * re-render. A literal would have pinned it to one theme and been wrong in the - * other. - * - * `tokens` is absent on purpose: playback is public, so there is nothing to - * sign and no key to hold. - */ -export default function WalkthroughPlayer() { - return ( - - ) -} diff --git a/site/src/components/walkthrough.tsx b/site/src/components/walkthrough.tsx index 0b9fef4..a9ea46c 100644 --- a/site/src/components/walkthrough.tsx +++ b/site/src/components/walkthrough.tsx @@ -1,32 +1,34 @@ -import { Clapperboard, Play } from 'lucide-react' -import { lazy, Suspense, useState } from 'react' +import { MuxPlayer } from '@karnstack/kino/mux' +import '@karnstack/kino/styles.css' +import { Clapperboard } from 'lucide-react' -import { WALKTHROUGH_POSTER, WALKTHROUGH_RUNTIME } from '@/lib/site' +import { + WALKTHROUGH_BLUR, + WALKTHROUGH_PLAYBACK_ID, + WALKTHROUGH_POSTER, + WALKTHROUGH_RUNTIME, +} from '@/lib/site' import { cn } from '@/lib/utils' -const Player = lazy(() => import('@/components/walkthrough-player')) - -/** - * The panel, cut the way every other window on this site is cut. MockTerminal, - * FleetWindow and the docs Shell blocks all wear it. - */ -const PANEL = - 'relative aspect-video overflow-hidden rounded-xl bg-zinc-950 ring-1 ring-zinc-950/10 dark:ring-white/10' - /** - * The setup walkthrough, mounted on a click. + * The setup walkthrough. + * + * Nothing here starts playback, and that is worth saying because kino's + * `autoPlay` prop is right there and using it breaks in a way that takes a + * while to see. kino builds its inside a mount effect and sets + * `autoplay` on the element there, while its teardown calls remove(), which + * detaches an element without pausing it. React runs a mount effect twice in + * development, so the first element is left detached and playing and + * unreachable: a second audio track a beat behind the visible one, which no + * control on the page can stop, because the provider that owned it is gone. * - * The click is not decoration. kino pulls in an adaptive streaming engine, and - * that chunk is 218 kB gzipped against a site whose entire bundle is 101 kB, so - * mounting it on arrival would triple what every visitor downloads for a video - * most of them will not play. Deferring it also keeps the custom element out of - * the prerender, which renders every route to static HTML at build time and has - * nothing to do with a custom element. + * The accent is handed over as `var(--primary)` rather than as a colour. kino + * assigns the prop straight to --kino-accent on its own root, so it resolves + * inside whichever theme the page is wearing and the header's toggle moves the + * scrubber with everything else. A literal would have pinned it to one theme. * - * What stands in for it until then is a poster and a play button. Deliberately - * not a copy of kino's own control: matching that pixel for pixel means copying - * measurements out of a stylesheet this file does not own, and it goes wrong - * the first time either side changes. + * `tokens` is absent on purpose: playback is public, so there is nothing to + * sign and no key to hold. */ export function Walkthrough({ align = 'left', @@ -36,28 +38,18 @@ export function Walkthrough({ align?: 'left' | 'center' className?: string }) { - const [playing, setPlaying] = useState(false) - return (
-
- {playing ? ( - }> - - - ) : ( - - )} +
+
) } - -function Poster() { - return ( - - ) -}