chadtmiller.com/knotless
git that lives in your Atmosphere account
Pages go brotli, run their theme script inline, and have an icon
crates/knotless-serve/src/web/pages.rs+6−2
| @@ -35,10 +35,14 @@ static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| { | ||
.expect("a client builds") | ||
}); | ||
| ||
/// A router that hands every request to the app at `web`. | ||
/// A router that hands every request to the app at `web`: what it answers | ||
/// compressed as this server's own answers are, brotli to a browser that | ||
/// takes it, but for what the app compressed itself, as its scripts. | ||
pub fn pages(web: String) -> Router { | ||
let web: &'static str = web.trim_end_matches('/').to_string().leak(); | ||
Router::new().fallback(move |req: Request| page(web, req)) | ||
Router::new() | ||
.fallback(move |req: Request| page(web, req)) | ||
.layer(tower_http::compression::CompressionLayer::new()) | ||
crates/knotless-serve/src/web/pages.rsline 45This uses the default level, not the quality 4 the body describes. Set it with The default is quality 4 for brotli, not 11: tower-http 0.7.1, which this workspace pins, sets it so itself in | ||
} | ||
| ||
async fn page(web: &str, req: Request) -> Response { | ||
crates/knotless-serve/tests/web.rs+19−0
| @@ -2679,6 +2679,10 @@ fn pages_drawn_elsewhere() { | ||
res.headers_mut() | ||
.insert("referrer-policy", policy.parse().unwrap()); | ||
} | ||
if uri.path() == "/packed" { | ||
res.headers_mut() | ||
.insert("content-encoding", "zstd".parse().unwrap()); | ||
} | ||
res | ||
}, | ||
); | ||
| @@ -2729,6 +2733,21 @@ fn pages_drawn_elsewhere() { | ||
let own = client.get(format!("{base}/own")).send().unwrap(); | ||
assert_eq!(own.headers()["referrer-policy"], "no-referrer"); | ||
assert_eq!(own.json::<Value>().unwrap()["proto"], "http"); | ||
// A page goes compressed to a browser that takes brotli; what the app | ||
// compressed itself goes as it is. | ||
let squeezed = |at: &str| { | ||
let res = client | ||
.get(format!("{base}{at}")) | ||
.header("accept-encoding", "br") | ||
.send() | ||
.unwrap(); | ||
res.headers()["content-encoding"] | ||
.to_str() | ||
.unwrap() | ||
.to_string() | ||
}; | ||
assert_eq!(squeezed("/alice.test/tally"), "br"); | ||
assert_eq!(squeezed("/packed"), "zstd"); | ||
// What knotless-serve answers itself is not handed on. | ||
let ours = client | ||
.get(format!("{base}/xrpc/dev.knotless.getFeed")) | ||
docs/design/serve.md+10−5
| @@ -712,7 +712,9 @@ branches. | ||
| ||
The pages are a SvelteKit app in `web/`, run separately. The server hands it | ||
every request it does not answer itself, and the app asks the server for each | ||
page's data, then draws the HTML. It asks `dev.knotless.*` XRPC queries, as | ||
page's data, then draws the HTML. The server compresses what the app answers | ||
as it does its own, brotli to a browser that takes it, but for what the app | ||
sends compressed already, as its scripts. It asks `dev.knotless.*` XRPC queries, as | ||
atproto's appviews answer theirs: each with a lexicon under `lexicons/`, | ||
sharing the views of `dev.knotless.defs` (a repository, a commit, a check, a | ||
diff, a pull or an issue). Pictures a page shows, a check's or an account's own, | ||
| @@ -1397,10 +1399,13 @@ it goes in a cookie as it is. Each theme is a block of colour tokens in | ||
`web/src/tokens.css`. A pair keeps its night half in `theme-night` and | ||
`palette-night`, and the server writes both halves on `<html>`, as | ||
`data-day` and `data-night`, with the palettes of either in a `<style>` | ||
of their own; `/theme.js`, before the page is drawn, takes the half the | ||
system's light or dark says, as `data-theme` and `data-half`, and again | ||
as it changes, so the pages flip with the system without a reload. Its | ||
own file, as the pages allow no script inline. | ||
of their own; a script in the page's head, before the page is drawn, | ||
takes the half the system's light or dark says, as `data-theme` and | ||
`data-half`, and again as it changes, so the pages flip with the system | ||
without a reload. It is in the page rather than a file of its own, so | ||
that nothing is asked for before the page is drawn, and the pages' | ||
policy lets it run by its hash alone, taken from `web/src/app.html` as | ||
the pages are built. | ||
| ||
Anyone can make a theme of their own colours, and publish it for anyone to | ||
choose. A `dev.knotless.theme` record holds one: a name, and a palette of | ||
web/src/lib/organisms/CodeResults.svelte+3−1
| @@ -73,7 +73,9 @@ | ||
{#each f.chunks as c, i (i)} | ||
{@const shown = lines(c)} | ||
<div class="code code-result"> | ||
<pre class="ln" aria-hidden="true">{#each shown as l (l.n)}<a href={href(f, l.n)}>{l.n}</a | ||
<pre class="ln" aria-hidden="true">{#each shown as l (l.n)}<a | ||
href={href(f, l.n)} | ||
tabindex="-1">{l.n}</a | ||
>{'\n'}{/each}</pre> | ||
<pre class="src"><code use:painted={c.highlights} | ||
>{#each shown as l (l.n)}{#each l.pieces as p, j (j)}{#if p.found}<mark>{p.text}</mark | ||
web/src/lib/organisms/CodeView.svelte+2−1
| @@ -45,7 +45,8 @@ | ||
class="ln" | ||
aria-hidden="true" | ||
bind:this={ln} | ||
onclick={mark}>{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}">{i + 1}</a | ||
onclick={mark}>{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}" tabindex="-1" | ||
>{i + 1}</a | ||
>{'\n'}{/each}</pre> | ||
<pre class="src" use:highlight={{ ranges, text }}><code>{text}</code></pre> | ||
</div> | ||
web/src/lib/theme.ts+2−2
| @@ -13,7 +13,7 @@ | ||
// A pair, one theme by day and another at night, keeps its night half in | ||
// `theme-night` and `palette-night` too. The server writes both halves on | ||
// <html>, as `data-day` and `data-night`, with any palette of either in a | ||
// <style> of its own; and /theme.js, before the page is drawn, takes the | ||
// <style> of its own; and app.html's script, before the page is drawn, takes the | ||
// half the system's light or dark says, as `data-theme` and `data-half`, | ||
// and again as it changes. | ||
| ||
| @@ -182,7 +182,7 @@ export function halvesStyle(day: Palette | null, night: Palette | null): string | ||
return rule('day', day) + rule('night', night); | ||
} | ||
| ||
/** Shows the half of a pair the system's light or dark says, as /theme.js does. */ | ||
/** Shows the half of a pair the system's light or dark says, as app.html's script does. */ | ||
function flip() { | ||
const html = document.documentElement; | ||
const half = matchMedia('(prefers-color-scheme: dark)').matches ? 'night' : 'day'; | ||
web/src/routes/[authority]/[repo]/commits/[...rest]/+page.svelte+1−1
| @@ -67,7 +67,7 @@ | ||
tags={data.tags} | ||
/> | ||
{#each days as [day, commits] (commits[0].commit.sha)} | ||
<h3 class="day" class:zoneless={!zone.tz}><Icon name="commit" />Commits on {day}</h3> | ||
<h2 class="day" class:zoneless={!zone.tz}><Icon name="commit" />Commits on {day}</h2> | ||
<div class="box"> | ||
<ul class="list"> | ||
{#each commits as { commit: c, checks } (c.sha)} | ||
web/src/app.html+25−1
| @@ -4,7 +4,31 @@ | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
<meta name="color-scheme" content="light dark" /> | ||
<script src="/theme.js"></script> | ||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> | ||
<!-- | ||
Draws the page in the half of its pair of themes the system's light | ||
or dark says: `data-day` while it is light, `data-night` while it is | ||
dark, as `data-theme`, with `data-half` naming which | ||
(src/lib/theme.ts). Run before the page is drawn, and again as the | ||
system changes; a page with no pair is left as it is. Here rather | ||
than a file of its own, so that a page asks for nothing before it is | ||
drawn: vite.config.ts lets it run by its hash. | ||
--> | ||
<script> | ||
(() => { | ||
const html = document.documentElement; | ||
const dark = matchMedia('(prefers-color-scheme: dark)'); | ||
const flip = () => { | ||
const half = dark.matches ? 'night' : 'day'; | ||
const id = html.dataset[half]; | ||
if (!id) return; | ||
html.dataset.half = half; | ||
html.dataset.theme = id; | ||
}; | ||
flip(); | ||
dark.addEventListener('change', flip); | ||
})(); | ||
</script> | ||
%sveltekit.head% | ||
</head> | ||
<body data-sveltekit-preload-data="hover"> | ||
web/src/hooks.server.ts+1−1
| @@ -25,7 +25,7 @@ function half(theme: string | null, said: string | undefined) { | ||
return theme && (palette || !theme.startsWith('custom-')) ? { id: theme, palette } : null; | ||
} | ||
| ||
/** What `<html>` says of a pair, by day and at night, and the <style> its palettes are drawn from: /theme.js shows the half the system says. */ | ||
/** What `<html>` says of a pair, by day and at night, and the <style> its palettes are drawn from: app.html's script shows the half the system says. */ | ||
function pair(day: ReturnType<typeof half>, night: ReturnType<typeof half>) { | ||
if (!day || !night) return null; | ||
const own = halvesStyle(day.palette, night.palette); | ||
web/static/favicon.svgnew+1−0
| @@ -0,0 +1 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><rect width="1024" height="1024" fill="#171717"/><path d="M204.8 542.7 L211.2 536.4 L217.6 530.1 L224.0 523.8 L230.4 517.6 L236.8 511.5 L243.2 505.6 L249.6 499.7 L256.0 494.1 L262.4 488.6 L268.8 483.3 L275.2 478.3 L281.6 473.5 L288.0 469.0 L294.4 464.7 L300.8 460.7 L307.2 457.1 L313.6 453.8 L320.0 450.8 L326.4 448.1 L332.8 445.8 L339.2 443.9 L345.6 442.3 L352.0 441.1 L358.4 440.3 L364.8 439.9 L371.2 439.8 L377.6 440.1 L384.0 440.7 L390.4 441.7 L396.8 443.1 L403.2 444.8 L409.6 446.9 L416.0 449.2 L422.4 451.9 L428.8 454.9 L435.2 458.1 L441.6 461.7 L448.0 465.4 L454.4 469.4 L460.8 473.6 L467.2 478.0 L473.6 482.5 L480.0 487.2 L486.4 492.0 L492.8 496.9 L499.2 501.9 L505.6 506.9 L512.0 512.0 L518.4 517.1 L524.8 522.1 L531.2 527.1 L537.6 532.0 L544.0 536.8 L550.4 541.5 L556.8 546.0 L563.2 550.4 L569.6 554.6 L576.0 558.6 L582.4 562.3 L588.8 565.9 L595.2 569.1 L601.6 572.1 L608.0 574.8 L614.4 577.1 L620.8 579.2 L627.2 580.9 L633.6 582.3 L640.0 583.3 L646.4 583.9 L652.8 584.2 L659.2 584.1 L665.6 583.7 L672.0 582.9 L678.4 581.7 L684.8 580.1 L691.2 578.2 L697.6 575.9 L704.0 573.2 L710.4 570.2 L716.8 566.9 L723.2 563.3 L729.6 559.3 L736.0 555.0 L742.4 550.5 L748.8 545.7 L755.2 540.7 L761.6 535.4 L768.0 529.9 L774.4 524.3 L780.8 518.4 L787.2 512.5 L793.6 506.4 L800.0 500.2 L806.4 493.9 L812.8 487.6 L819.2 481.3" fill="none" stroke="#fff" stroke-width="76.8" stroke-linecap="round" stroke-linejoin="round"/></svg> | ||