diff --git a/web/css/06-crate.css b/web/css/06-crate.css index f6ca040..f264207 100644 --- a/web/css/06-crate.css +++ b/web/css/06-crate.css @@ -101,6 +101,20 @@ white-space: nowrap; } +/* Compact variant (login): wordmark + tagline, no count/badge slots. */ +.hero--compact { + height: 104px; +} +.hero__tagline { + position: absolute; + left: 16px; + right: 16px; + top: 42px; + margin: 0; + color: color-mix(in srgb, var(--cream-fixed) 65%, transparent); + font: 400 11px/1.3 var(--mono); +} + /* --- toolbar / tags --------------------------------------------------- */ .body { padding: 16px; diff --git a/web/css/16-login.css b/web/css/16-login.css index 102c926..f9f28e8 100644 --- a/web/css/16-login.css +++ b/web/css/16-login.css @@ -1,15 +1,6 @@ /* --- signed-out / login ----------------------------------------------- */ -.signin { - padding: 24px 16px; -} -.signin h2 { - font: 400 30px/1 var(--display); - margin: 0 0 4px; -} -.signin .tagline { - color: var(--ink-muted); - margin: 0 0 20px; - font-size: 13px; +.signin__body { + padding: 20px 16px 24px; } /* --- login OAuth error panel ------------------------------------------- */ @@ -50,43 +41,66 @@ color: var(--ink); } -/* --- register: PDS provider hand-off ---------------------------------- */ +/* --- login panel + register card rhythm -------------------------------- */ +.signin .card { + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} +.signin .card form { + display: flex; + flex-direction: column; + gap: 10px; +} +.signin .card .field { + margin-bottom: 0; +} .signin .card + .card { margin-top: 16px; } + +/* --- section header: ink block, matches the badge/layer-band idiom ----- */ +.section-header { + margin: 0; + background: var(--ink-fixed); + color: var(--cream-fixed); + border: 2px solid var(--ink); + box-shadow: var(--shadow-sm); + padding: 7px 14px; + font: 700 14px/1.2 var(--mono); +} + +/* --- register: PDS provider hand-off ---------------------------------- */ .providers { display: flex; flex-direction: column; - gap: 8px; - margin-top: 12px; } .provider { display: flex; align-items: baseline; gap: 8px; - border: 2px solid var(--ink); - box-shadow: var(--shadow-sm); - background: var(--surface); - padding: 10px 12px; + padding: 10px 0; text-decoration: none; color: var(--ink); } -/* was `var(--cream)`, an undefined token, so this hover silently no-oped */ +.provider + .provider { + border-top: 1px solid var(--line); +} @media (hover: hover) and (pointer: fine) { .provider:hover { background: var(--bg); } } .provider__name { - font: 700 14px/1 var(--display); + font: 700 13px/1 var(--mono); } .provider__blurb { flex: 1; color: var(--ink-muted); - font-size: 12px; + font-size: 11px; } .provider__arrow { font: 700 13px/1 var(--mono); color: var(--ink-muted); } - diff --git a/web/src/at_record_web/pages/login.gleam b/web/src/at_record_web/pages/login.gleam index de11433..ab5cda8 100644 --- a/web/src/at_record_web/pages/login.gleam +++ b/web/src/at_record_web/pages/login.gleam @@ -24,19 +24,33 @@ pub fn restoring() -> Element(Msg) { pub fn view(model: Model) -> Element(Msg) { html.div([attr.class("signin")], [ - html.h2([], [text("AT·RECORD")]), - html.p([attr.class("tagline")], [text("your crate, stored on your PDS")]), - error_panel(model.login_error), - cov.card([ - html.form([event.on_submit(fn(_) { StartLogin })], [ - handle_autocomplete(model), - login_action(model.login_handle), - ]), - html.p([attr.class("hint")], [ - text("You'll be sent to your PDS to authorize at-record via OAuth."), + hero(), + html.div([attr.class("signin__body")], [ + error_panel(model.login_error), + cov.card([ + html.form([event.on_submit(fn(_) { StartLogin })], [ + handle_autocomplete(model), + login_action(model.login_handle), + ]), + html.p([attr.class("hint")], [ + text("you'll be sent to your PDS to authorize crate via OAuth"), + ]), ]), + register_section(), + ]), + ]) +} + +/// The compact greenhouse hero, same wordmark/art as the authed crate hero +/// minus its authed chrome (no browse/scan/avatar buttons on login). +fn hero() -> Element(Msg) { + html.div([attr.class("hero hero--compact")], [ + html.div([attr.class("hero__bar")], [ + html.span([attr.class("hero__brand")], [text("CRATE|FEED")]), + ]), + html.p([attr.class("hero__tagline")], [ + text("your crate, stored on your PDS"), ]), - register_section(), ]) } @@ -45,20 +59,20 @@ pub fn view(model: Model) -> Element(Msg) { /// registration roadmap item; hosting our own PDS comes later. fn register_section() -> Element(Msg) { cov.card([ - frm.section_label("NEW TO THE ATMOSPHERE?"), - html.p([attr.class("hint")], [ - text("Create an account on a PDS provider, then come back and log in:"), - ]), + html.p([attr.class("section-header")], [text("NEW TO THE ATMOSPHERE?")]), html.div( [attr.class("providers")], [ #("Bluesky", "the original, largest", "https://bsky.app"), - #("Blacksky", "Black-led community infra", "https://blacksky.community"), - #("Eurosky", "European servers", "https://eurosky.tech"), + #("Blacksky", "Black-led community infra", "https://blacksky.app"), + #("Eurosky", "European servers", "https://eurosky.social"), #("Northsky", "Canada, community-run", "https://northsky.social"), ] |> list.map(provider_link), ), + html.p([attr.class("hint")], [ + text("make an account on any PDS, then come back and log in"), + ]), ]) } @@ -149,7 +163,7 @@ fn login_action(handle: String) -> Element(Msg) { // FFI. An anchor would be hijacked by modem (same-origin) and never reach // the server. Submission lives on the form so Enter works too. let disabled = string.trim(handle) == "" - ctl.button("LOG IN WITH ATPROTO", ctl.Primary, [ + ctl.button("LOG IN WITH ATPROTO →", ctl.Primary, [ attr.disabled(disabled), attr.class("btn--block"), attr.type_("submit"), diff --git a/web/test/login_test.gleam b/web/test/login_test.gleam index 0882852..a8b1511 100644 --- a/web/test/login_test.gleam +++ b/web/test/login_test.gleam @@ -14,6 +14,29 @@ pub fn login_submits_through_a_form_test() { assert string.contains(html, "LOG IN WITH ATPROTO") } +pub fn login_shows_the_hero_wordmark_and_tagline_test() { + let html = login.view(base()) |> element.to_string + assert string.contains(html, "class=\"hero hero--compact\"") + assert string.contains(html, "CRATE|FEED") + assert string.contains(html, "your crate, stored on your PDS") +} + +pub fn login_shows_the_atmosphere_section_header_test() { + let html = login.view(base()) |> element.to_string + assert string.contains(html, "class=\"section-header\"") + assert string.contains(html, "NEW TO THE ATMOSPHERE?") +} + +pub fn login_lists_every_pds_provider_test() { + let html = login.view(base()) |> element.to_string + assert string.contains(html, "href=\"https://bsky.app\"") + assert string.contains(html, "href=\"https://blacksky.app\"") + assert string.contains(html, "href=\"https://eurosky.social\"") + assert string.contains(html, "href=\"https://northsky.social\"") + assert string.contains(html, "target=\"_blank\"") + assert string.contains(html, "rel=\"noopener\"") +} + pub fn oauth_error_copy_maps_known_codes_test() { assert string.contains( login.error_copy("access_denied"),