oficina / slides Public

fix: use the vendored field and form-foot on the sign-in form

AGENTS.md marks fields and form actions as vendored components to use,
not to reimplement, and the hand-rolled input this page shipped with
carried a placeholder where a label belongs — the wiring those partials
exist to get right (a real <label> bound to the input's id,
aria-describedby listing exactly the ids that render, aria-invalid on
an error). Pinned by a test, so a future hand-rolled input cannot
quietly drop it again.

The shell bar stays on the front door now, naming the app, with the
brand rendered as text rather than a dead link to a route the session
gate bounces back here. Only the narrow-viewport hamburger is hidden,
because on this page every section of the tail it opens is empty.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01563XGCgxcSZToEXwuTBsKE
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 8db6835d7fddc766c1dbb0240e12e7a4d418d99b parent a6cf9ef
2 files changed, +54 −38
  • internal/slides/templates/signin.html +45 −38
  • internal/slidestest/signin_test.go +9 −0
diff --git a/internal/slides/templates/signin.html b/internal/slides/templates/signin.html
index e3264b0..d84859c 100644
--- a/internal/slides/templates/signin.html
+++ b/internal/slides/templates/signin.html
@@ -18,29 +18,30 @@
Bare rst-* attribute grammar throughout, never class="rst-*", and
no JavaScript: a form and a submit button are the whole screen. */}}
{{define "title"}}{{.Title}}{{end}}
+{{/* The app's name, not a link. Everything the shell bar can navigate
+ to on this screen is behind the session gate, so the default
+ brand — an <a href="/"> — would bounce straight back to the page
+ it was clicked from. The account disclosure removes itself
+ (tokens.css hides [rst-shell-account] when its menu is empty, and
+ this page defines no "account" block), which leaves only the
+ narrow-viewport hamburger, hidden below. */}}
+{{define "brand"}}<span rst-shell-brand>Slides</span>{{end}}
{{define "head"}}<style>
- /* No shell bar on the front door. The topbar shell writes
- [rst-shell-bar] on every page, outside any block a page could
- override, so this screen hides it in CSS — the only lever it has.
+ /* The hamburger opens the bar's tail — nav, account, locale — and on
+ this page every one of those is empty. A control that visibly does
+ nothing is worse than no control, so it goes.
- It hides it because there is nothing true to put in it: the
- default brand is a link to "/", which the session gate bounces
- straight back here, and below 800px the hamburger would open an
- empty tail. An empty strip of chrome that looks like it should do
- something is worse than none.
-
- display: none, not visibility or opacity: this must leave the
- accessibility tree too, or a screen-reader user tabs into an
- "Account" disclosure nobody can see. The shell's skip link is a
- sibling of the bar rather than inside it, so this page keeps a
- working skip-to-content.
+ display: none, not visibility or opacity: it must leave the
+ accessibility tree too, or a screen-reader user tabs into a
+ disclosure nobody can see. The bar itself stays: naming the app on
+ its own front door is worth a strip of chrome.
Written as a descendant of [rst-shell-topbar] to match the
framework's own selector, and that is load-bearing rather than
- stylistic: tokens.css lays the bar out with
- `[rst-shell-topbar] [rst-shell-bar]`, specificity (0,2,0), which a
- bare `[rst-shell-bar]` at (0,1,0) would lose to. */
- [rst-shell-topbar] [rst-shell-bar] {
+ stylistic: tokens.css lays the control out with
+ `[rst-shell-topbar] [rst-shell-menu]`, specificity (0,2,0), which a
+ bare `[rst-shell-menu]` at (0,1,0) would lose to. */
+ [rst-shell-topbar] [rst-shell-menu] {
display: none;
}
/* One measure for the whole screen, shared by the callouts and the
@@ -55,21 +56,16 @@
[slides-signin] {
max-inline-size: 32rem;
}
- /* The field and its button share a line, with real space between
- them. flex-basis 12rem with room to shrink, and min-inline-size: 0
- because an <input>'s automatic minimum size is its content width —
- without the explicit 0 it refuses to shrink and the row overflows
- instead of reflowing at the 320px width WCAG 2.2 AA commits to.
- Below that the button wraps under the field rather than either one
- being squeezed unusable. */
- [slides-signin-form] {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--rst-sp-3);
- }
+ /* The field and the button stack, rather than sharing a line. One
+ labelled field over its own submit is the shape that survives a
+ 320px viewport without either control being squeezed, and it keeps
+ the visible label directly above the input it names.
+
+ min-inline-size: 0 on the input because an <input>'s automatic
+ minimum size is its content width: without the explicit 0 it
+ refuses to shrink and the box overflows instead of reflowing. */
[slides-signin-form] [rst-input] {
- flex: 1 1 12rem;
+ inline-size: 100%;
min-inline-size: 0;
}
</style>{{end}}
@@ -91,13 +87,24 @@
{{template "callout" dict "Body" .ErrorMessage "Tone" "negative" "Alert" true}}
{{end}}
<div rst-box>
- <div rst-box-head>
- <p>{{.Body}}</p>
- </div>
- <form slides-signin-form method="post" action="/signin">
+ <p>{{.Body}}</p>
+ {{/* field-text and form-foot rather than a hand-rolled label and
+ button: AGENTS.md marks fields and form actions as vendored
+ components to use, not to reimplement, and what they carry is
+ the wiring that is easy to get subtly wrong — a real <label>
+ bound to the input's id, aria-describedby listing exactly the
+ ids that render, aria-invalid on an error. A placeholder is
+ not a label. */}}
+ <form slides-signin-form rst-form method="post" action="/signin">
<input type="hidden" name="force" value="1">
- <input rst-input type="email" name="address" placeholder="{{.AddressLabel}}" aria-label="{{.AddressLabel}}" autocomplete="email" required>
- <button rst-btn="primary" type="submit">{{.SubmitLabel}}</button>
+ {{template "field-text" dict
+ "Name" "address"
+ "Label" .AddressLabel
+ "Type" "email"
+ "Autocomplete" "email"
+ "Required" true
+ "Autofocus" true}}
+ {{template "form-foot" dict "Submit" .SubmitLabel}}
</form>
</div>
</div>
diff --git a/internal/slidestest/signin_test.go b/internal/slidestest/signin_test.go
index fa3f144..58c79dd 100644
--- a/internal/slidestest/signin_test.go
+++ b/internal/slidestest/signin_test.go
@@ -33,10 +33,19 @@ func TestSigninPageIsPublic(t *testing.T) {
if !strings.Contains(body, `name="address"`) {
t.Errorf("GET /signin: no address field in the form:\n%s", body)
}
+ // A real <label> bound to the input, not a placeholder standing in
+ // for one — the guarantee the vendored field-text partial carries,
+ // pinned here so a future hand-rolled input cannot quietly drop it.
+ if !strings.Contains(body, `for="address"`) {
+ t.Errorf("GET /signin: the address field has no bound <label>:\n%s", body)
+ }
// The catalog resolved, rather than the key leaking onto the page.
if !strings.Contains(body, "Sign in") {
t.Errorf("GET /signin: page does not carry its translated title:\n%s", body)
}
+ if strings.Contains(body, "slides.signin.") {
+ t.Errorf("GET /signin: a catalog key leaked onto the page unresolved:\n%s", body)
+ }
}
// TestSigninMailsAllowedAddressOnly is the allowlist's whole point: an