steid

@jamesgill /

5d3dfa5feat: a global top bar, and pages choose their own width1d
1use topcoat::{
2 Result,
3 context::Cx,
4 router::layout,
5 tailwind,
6 view::{View, component, view},
7};
8
9use crate::application::Identity;
10
11use super::context::identity;
14223e7feat: claim, sign in, and sign out through the browser1mo
12
13/// The HTML shell every page renders inside.
14///
15/// A layout rather than a helper function: `view!` needs the request context in scope,
16/// so a plain `fn shell(..)` cannot build one. Layouts wrap by path prefix, and this
17/// one is at `/`, so it wraps everything.
af126c7feat: tailwind theme and the first components25d
18///
19/// `class="dark"` opts the whole document into the dark palette. Steid is dark-first —
20/// developers live there — and light mode is a later toggle rather than the default.
5d3dfa5feat: a global top bar, and pages choose their own width1d
21///
22/// The shell imposes no width: prose-shaped pages wrap themselves in [`narrow`] and
23/// code-shaped ones in [`wide`], because a repository tree and a settings form do not
24/// want the same column. A page that forgets a container renders full-bleed, which is
25/// immediately visible — the same reasoning as request helpers being functions.
14223e7feat: claim, sign in, and sign out through the browser1mo
26#[layout("/")]
5d3dfa5feat: a global top bar, and pages choose their own width1d
27async fn root_layout(cx: &Cx, slot: Result) -> Result {
14223e7feat: claim, sign in, and sign out through the browser1mo
28 let content = slot?;
5d3dfa5feat: a global top bar, and pages choose their own width1d
29 let viewer = identity(cx).await?;
14223e7feat: claim, sign in, and sign out through the browser1mo
30
31 view! {
32 <!DOCTYPE html>
af126c7feat: tailwind theme and the first components25d
33 <html lang="en" class="dark">
14223e7feat: claim, sign in, and sign out through the browser1mo
34 <head>
35 <meta charset="utf-8" />
36 <meta name="viewport" content="width=device-width, initial-scale=1" />
37 <title>"steid"</title>
af126c7feat: tailwind theme and the first components25d
38 <link rel="stylesheet" href=(tailwind::stylesheet!()) />
14223e7feat: claim, sign in, and sign out through the browser1mo
39 topcoat::dev::script()
40 </head>
41 <body>
5d3dfa5feat: a global top bar, and pages choose their own width1d
42 top_bar(viewer: &viewer)
43 <main class="px-6 py-10">(content)</main>
14223e7feat: claim, sign in, and sign out through the browser1mo
44 </body>
45 </html>
46 }
47}
5d3dfa5feat: a global top bar, and pages choose their own width1d
48
49/// The one-line global bar: where you are signed in, or the way in.
50///
51/// Deliberately not an app shell. Steid's visitors are anonymous readers of someone's
52/// site, so the chrome stays a slim bar and real navigation lives with the thing being
53/// navigated — the profile's tab bar, the repository's — rather than in a global rail.
54///
55/// Everything on the signed-in side is owner furniture; a visitor sees only "Sign in",
56/// and the wordmark, which goes to `/` — the owner's profile, by the front-door rule.
57#[component]
58async fn top_bar(viewer: &Option<Identity>) -> Result {
59 view! {
60 <header class="border-b border-border">
61 <div class="flex h-11 items-center px-6 text-[13px]">
62 <a href="/" class="font-mono font-semibold tracking-tight">"steid"</a>
63 <div class="ml-auto flex items-center gap-5">
64 match viewer {
65 Some(me) => {
66 <a
67 href=(format!("/{}", me.handle))
68 class="font-mono text-muted-foreground hover:text-foreground"
69 >"@" (me.handle.as_str())</a>
70 <a
71 href=(format!("/{}/settings", me.handle))
72 class="text-muted-foreground hover:text-foreground"
73 >"Settings"</a>
74 <form method="post" action="/auth/logout">
75 <button
76 type="submit"
77 class="cursor-pointer text-muted-foreground hover:text-foreground"
78 >"Sign out"</button>
79 </form>
80 }
81 None => {
82 <a
83 href="/auth/login"
84 class="text-muted-foreground hover:text-foreground"
85 >"Sign in"</a>
86 }
87 }
88 </div>
89 </div>
90 </header>
91 }
92}
93
94/// The centered column for prose-shaped pages: profiles, forms, settings, writing.
95#[component]
96pub(super) async fn narrow(#[default] child: View) -> Result {
97 view! { <div class="mx-auto w-full max-w-3xl">(child)</div> }
98}
99
100/// The full working width for code-shaped pages: trees, blobs, logs.
101///
102/// Info density is the point — a file view in a 48rem column wastes the screen the
103/// reader sized their window to.
104#[component]
105pub(super) async fn wide(#[default] child: View) -> Result {
106 view! { <div class="mx-auto w-full max-w-screen-xl">(child)</div> }
107}