A page says nothing until it has something to say

The Suspense fallback on a question is the whole served page until
the question resolves - no header, no wordmark - so the word
"loading…" was the first and only thing a visitor or a crawler read
about the site. True, and not about them. It holds its space and
stays quiet now; `aria-busy` says what is happening to a reader that
would otherwise find an empty document. Same for the two resource
slots inside a page, which keep a line's worth of room.

Also gone: the `gitea_org_repos` resource kind, which no site ever
named. `gitea_starred` and `gitea_releases` stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-26 13:25:11 +02:00
co-authored by Claude Opus 5
parent f18d1a5181
commit 9891c462c6
6 changed files with 22 additions and 14 deletions
+2
View File
@@ -12,6 +12,8 @@ content repos pin the tag they run (see the end of this file).
- A state can grant a group: `grants: <group>` beside its `event`, so an approval that means membership does not also need somebody to remember to invite them afterwards. The lint requires the forms that fill such a bucket to collect a required name and email, because by the time a desk presses approve there is nobody left to ask. - A state can grant a group: `grants: <group>` beside its `event`, so an approval that means membership does not also need somebody to remember to invite them afterwards. The lint requires the forms that fill such a bucket to collect a required name and email, because by the time a desk presses approve there is nobody left to ask.
- A state can be moved by a clock: `after: { days, to }`, swept hourly, measured from the record's last move (`src/deadlines.rs`). A deadline may only do what a person could have done - `after.to` is held to the same declared edge as a desk button - and the move fires the same mail and publishes the same event, stamped `deadline`. - A state can be moved by a clock: `after: { days, to }`, swept hourly, measured from the record's last move (`src/deadlines.rs`). A deadline may only do what a person could have done - `after.to` is held to the same declared edge as a desk button - and the move fires the same mail and publishes the same event, stamped `deadline`.
- Documentation moved out of the README into `docs/`: architecture, content reference, state machines, access, operations. - Documentation moved out of the README into `docs/`: architecture, content reference, state machines, access, operations.
- A page says nothing until it has something to say. The Suspense fallback on a question was the whole served page - no header, no wordmark - so the first and only thing a visitor or a crawler read was the word "loading…", which is true and is not about them. It now holds the space and stays quiet, with `aria-busy` for a reader that would otherwise find an empty document.
- Gone: the `gitea_org_repos` resource kind, which no site ever named. `gitea_starred` and `gitea_releases` stay.
## 0.4.0 (2026-09-24) ## 0.4.0 (2026-09-24)
+1 -1
View File
@@ -210,7 +210,7 @@ reads is never a client-supplied parameter.
- { from: complete, to: on_hearing, label: Send it out } - { from: complete, to: on_hearing, label: Send it out }
``` ```
`source.kind` is one of `kv`, `gitea_starred`, `gitea_org_repos`, `source.kind` is one of `kv`, `gitea_starred`,
`gitea_releases`, `url`. `gitea_releases`, `url`.
A spec with neither `public: true` nor `requires_group` is A spec with neither `public: true` nor `requires_group` is
+11 -7
View File
@@ -244,12 +244,16 @@ fn QuestionPage() -> impl IntoView {
let site = expect_context::<Resource<Result<SiteConfig, ServerFnError>>>(); let site = expect_context::<Resource<Result<SiteConfig, ServerFnError>>>();
view! { view! {
// Nothing is written here on purpose. This fallback is the
// whole served page until the question resolves - no header,
// no wordmark - so any word in it is the first and only thing
// a visitor, or a crawler, reads about the site. It used to
// say "loading…", which is true and is not about them. The
// space is held so the page does not jump when the words
// arrive, and `aria-busy` says what is happening to anyone
// whose reader would otherwise find an empty document.
<Suspense fallback=|| { <Suspense fallback=|| {
view! { view! { <main class="loading" aria-busy="true"></main> }
<main class="loading">
<p>"loading…"</p>
</main>
}
}> }>
{move || { {move || {
// Both read unconditionally (not nested inside the match // Both read unconditionally (not nested inside the match
@@ -1620,7 +1624,7 @@ fn ResourceFeature(
view! { view! {
<div class="resource"> <div class="resource">
<Suspense fallback=|| view! { <p class="resource-loading">"loading…"</p> }> <Suspense fallback=|| view! { <p class="resource-loading" aria-busy="true"></p> }>
{move || { {move || {
let feature_name = feature_name.clone(); let feature_name = feature_name.clone();
let transitions = transitions.clone(); let transitions = transitions.clone();
@@ -1877,7 +1881,7 @@ fn SelectField(
view! { view! {
<div class="select-field"> <div class="select-field">
<Suspense fallback=|| view! { <p class="resource-loading">"loading…"</p> }> <Suspense fallback=|| view! { <p class="resource-loading" aria-busy="true"></p> }>
{move || { {move || {
let id_field = id_field.clone(); let id_field = id_field.clone();
data.get() data.get()
-2
View File
@@ -446,7 +446,6 @@ pub struct ResourceSpec {
pub enum ResourceSource { pub enum ResourceSource {
Kv { bucket: String }, Kv { bucket: String },
GiteaStarred { username: String }, GiteaStarred { username: String },
GiteaOrgRepos { org: String },
/// A repo's releases - fetched with `GITEA_API_TOKEN`, so it works /// A repo's releases - fetched with `GITEA_API_TOKEN`, so it works
/// for private repos too (shape the link with jq: a private /// for private repos too (shape the link with jq: a private
/// release's html_url 404s for anonymous visitors, so map it to /// release's html_url 404s for anonymous visitors, so map it to
@@ -466,7 +465,6 @@ impl ResourceSpec {
match &self.source { match &self.source {
ResourceSource::Kv { bucket } => Some(bucket), ResourceSource::Kv { bucket } => Some(bucket),
ResourceSource::GiteaStarred { .. } ResourceSource::GiteaStarred { .. }
| ResourceSource::GiteaOrgRepos { .. }
| ResourceSource::GiteaReleases { .. } | ResourceSource::GiteaReleases { .. }
| ResourceSource::Url { .. } => None, | ResourceSource::Url { .. } => None,
} }
+1 -4
View File
@@ -13,7 +13,7 @@ use leptos::prelude::*;
/// `public: true` nor `requires_group` set is unreachable, not "open" /// `public: true` nor `requires_group` set is unreachable, not "open"
/// by omission. `params` are the requesting form's current field /// by omission. `params` are the requesting form's current field
/// values (if any) - forwarded as query parameters to a live external /// values (if any) - forwarded as query parameters to a live external
/// fetch (`GiteaStarred`/`GiteaOrgRepos`/`Url`), the mechanism behind /// fetch (`GiteaStarred`/`GiteaReleases`/`Url`), the mechanism behind
/// "a resource parameterized by other form fields"; a `Kv` resource /// "a resource parameterized by other form fields"; a `Kv` resource
/// ignores them entirely, same as today. /// ignores them entirely, same as today.
#[server(endpoint = "get_resource")] #[server(endpoint = "get_resource")]
@@ -295,9 +295,6 @@ async fn fetch_resource_value(
ResourceSource::GiteaStarred { username } => { ResourceSource::GiteaStarred { username } => {
fetch_gitea_json(state, &format!("/api/v1/users/{username}/starred"), params).await? fetch_gitea_json(state, &format!("/api/v1/users/{username}/starred"), params).await?
} }
ResourceSource::GiteaOrgRepos { org } => {
fetch_gitea_json(state, &format!("/api/v1/orgs/{org}/repos"), params).await?
}
ResourceSource::GiteaReleases { owner, repo } => { ResourceSource::GiteaReleases { owner, repo } => {
fetch_gitea_json(state, &format!("/api/v1/repos/{owner}/{repo}/releases"), params) fetch_gitea_json(state, &format!("/api/v1/repos/{owner}/{repo}/releases"), params)
.await? .await?
+7
View File
@@ -1227,3 +1227,10 @@ textarea:focus {
font-size: 0.9em; font-size: 0.9em;
color: var(--ink-dim); color: var(--ink-dim);
} }
/* A loading slot holds its line's worth of space and says nothing.
The words that belong here have not arrived yet, and "loading…" is
not one of them. */
.resource-loading {
min-height: 1.4em;
}