From 717df5208ce8ddfbdc652b7f05ce9f93e6834d11 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 21 Sep 2026 10:22:55 +0200 Subject: [PATCH] Implemented phase 0 for the POC prototype in the Website service. --- Services/Website/README.md | 11 +- .../Website/Resources/Static/css/index.css | 23 + .../Resources/Static/css/not-found.css | 48 +- .../Website/Resources/Static/css/shared.css | 1483 +++++++++++++++-- .../Website/Resources/Static/font/README.txt | 7 + .../Resources/Static/font/anton-400.woff2 | Bin 0 -> 12004 bytes .../Static/font/archivo-variable.woff2 | Bin 0 -> 34940 bytes .../Static/font/oswald-variable.woff2 | Bin 0 -> 21412 bytes .../Website/Resources/Static/site.webmanifest | 8 +- .../Library/Catalogs/Localizable.xcstrings | 8 +- .../Library/Internal/Components/Icon.swift | 96 ++ .../Internal/Components/SiteFooter.swift | 154 ++ .../Internal/Components/SiteHeader.swift | 253 +++ .../Internal/Components/SiteLayout.swift | 53 + .../Internal/Enumerations/Format.swift | 34 + .../Internal/Extensions/Page+Defaults.swift | 8 +- .../Internal/Extensions/String+Routes.swift | 39 + .../Library/Internal/Pages/IndexPage.swift | 31 +- .../Library/Internal/Pages/NotFoundPage.swift | 20 +- .../Library/Internal/Types/StaticFile.swift | 12 + .../Library/Internal/Types/Viewer.swift | 50 + Services/Website/Tests/App/AppTests.swift | 2 +- .../Cases/Internal/Components/IconTests.swift | 30 + .../Internal/Components/SiteFooterTests.swift | 40 + .../Internal/Components/SiteHeaderTests.swift | 78 + .../Internal/Enumerations/FormatTests.swift | 29 + .../Cases/Internal/Pages/IndexPageTests.swift | 41 +- .../Internal/Types/StaticFileTests.swift | 3 + .../Cases/Internal/Types/ViewerTests.swift | 24 + .../Controllers/RootControllerTests.swift | 4 +- .../Utils/Extensions/Tag+Constants.swift | 2 + 31 files changed, 2351 insertions(+), 240 deletions(-) create mode 100644 Services/Website/Resources/Static/font/README.txt create mode 100644 Services/Website/Resources/Static/font/anton-400.woff2 create mode 100644 Services/Website/Resources/Static/font/archivo-variable.woff2 create mode 100644 Services/Website/Resources/Static/font/oswald-variable.woff2 create mode 100644 Services/Website/Sources/Library/Internal/Components/Icon.swift create mode 100644 Services/Website/Sources/Library/Internal/Components/SiteFooter.swift create mode 100644 Services/Website/Sources/Library/Internal/Components/SiteHeader.swift create mode 100644 Services/Website/Sources/Library/Internal/Components/SiteLayout.swift create mode 100644 Services/Website/Sources/Library/Internal/Enumerations/Format.swift create mode 100644 Services/Website/Sources/Library/Internal/Extensions/String+Routes.swift create mode 100644 Services/Website/Sources/Library/Internal/Types/Viewer.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Components/IconTests.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Components/SiteFooterTests.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Components/SiteHeaderTests.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Enumerations/FormatTests.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Types/ViewerTests.swift diff --git a/Services/Website/README.md b/Services/Website/README.md index ecb3da1..3f8d700 100644 --- a/Services/Website/README.md +++ b/Services/Website/README.md @@ -51,6 +51,13 @@ HealthController (GET /health → liveness, GET /health The router is created with `.autoGenerateHeadEndpoints`, so each of those `GET` routes gets a `HEAD` sibling for free. +### Design foundation +The screens follow the **CCN marketplace** design canvas: +- **Tokens and primitives** live in `Resources/Static/css/shared.css`, written phone-first with breakpoints at 600px and 1024px; the dark theme only repoints the tokens. +- **No inline styles**: the `Content-Security-Policy` blocks them, so markup carries classes only. +- **Shared frame**: `SiteHeader`, `SiteFooter`, and the `SiteLayout` a page wraps its `content` in, under `Sources/Library/Internal/Components`. +- **Paths**: `String.Route` lists every screen's path; screens not built yet answer with the not-found page. + ### Page metadata Each page conforms to `Infrastructure`'s `Page` protocol and supplies only its `title`, `content`, `stylesheets`, and `scripts`; the protocol assembles the document around them and renders the head in a fixed order: the viewport declaration, the `analytics` origin preconnect hint, the `summary`, the `canonicalURL` link, the `socialCard` tags, the `structuredData` script, the `analytics` tracker script, then the page `metadata` and the stylesheet links. The body is the content followed by the script tags. @@ -68,7 +75,7 @@ What the pages *do* ship is in `Page+Defaults` (`Sources/Library/Internal/Extens `ogLocale` renders Open Graph's `language_TERRITORY` form by looking the page's `lang` up in the `ogLocales` map, which ships one pairing: `en` → `en_US`. A site serving a language in a territory of its own repoints or extends the map (`en_NL`, `nl_NL`, …); a language with no entry stays a bare code, which scrapers also accept. Nothing reads it until a page supplies a `socialCard`. -`preloadedFonts` is empty until the site ships fonts. Listing one emits `` at `/font/.woff2`, deliberately unversioned: a preload URL must match the stylesheet's `@font-face` source exactly, or the browser fetches the font twice. List only the faces the stylesheets actually render — a subset gated by a `unicode-range` no page reaches would add a download that never otherwise happens. +`preloadedFonts` lists the three self-hosted faces under `Resources/Static/font`: Archivo, Oswald, and Anton. Listing one emits `` at `/font/.woff2`, deliberately unversioned: a preload URL must match the stylesheet's `@font-face` source exactly, or the browser fetches the font twice. List only the faces the stylesheets actually render — a subset gated by a `unicode-range` no page reaches would add a download that never otherwise happens. ### Language editions The `WebsiteLibrary` String Catalog is the single source of truth for the languages the site serves: add a localization and it appears, with no code change. `Language` (`Sources/Library/Internal/Types`) reads that list and derives each language's URLs from it. @@ -373,7 +380,7 @@ All icons are renditions of the star mark in `icon.svg`, the canonical source | `icon-192.png`, `icon-512.png` | 192/512 | `site.webmanifest` install icons and splash screens | None (no platform mechanism); full artwork, light rendering, on a transparent background. | | `apple-touch-icon.png` | 180×180 | iOS home-screen bookmarks | None (fetched once, outside any page context); full artwork, deliberately opaque — iOS fills transparent regions with black. | -The icon and manifest links come from the shared `Page+Defaults` extension, so every page carries them, alongside two `theme-color` metas: `#fafafa` unqualified, then `#0c0710` qualified with `(prefers-color-scheme: dark)`. +The icon and manifest links come from the shared `Page+Defaults` extension, so every page carries them, alongside two `theme-color` metas matching the site header, which stays dark in both themes: `#191713` unqualified, then `#0f0e0b` qualified with `(prefers-color-scheme: dark)`. > **Note:** a browser applies the *first* `theme-color` whose media query matches, so the unqualified light value currently wins on both themes. Put the dark, media-qualified meta first in `Page+Defaults` if the dark value should take effect. diff --git a/Services/Website/Resources/Static/css/index.css b/Services/Website/Resources/Static/css/index.css index e69de29..643ea7c 100644 --- a/Services/Website/Resources/Static/css/index.css +++ b/Services/Website/Resources/Static/css/index.css @@ -0,0 +1,23 @@ +/* CCN — landing page. */ + +.hero { + display: flex; + flex-direction: column; + gap: var(--space-3); + max-width: 640px; + padding: var(--space-1) 0 var(--space-2); +} + +.hero__actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + padding-top: var(--space-3); +} + +@media (min-width: 1024px) { + .hero { + gap: var(--space-5); + padding: var(--space-10) 0; + } +} diff --git a/Services/Website/Resources/Static/css/not-found.css b/Services/Website/Resources/Static/css/not-found.css index 5a95a8d..89d1dcd 100644 --- a/Services/Website/Resources/Static/css/not-found.css +++ b/Services/Website/Resources/Static/css/not-found.css @@ -1,43 +1,9 @@ -/* Styles for the not-found (404) error page. */ +/* CCN — not-found page. */ -* { - line-height: 1.2; - margin: 0; -} - -html { - color: #888; - display: table; - font-family: sans-serif; - height: 100%; - text-align: center; - width: 100%; -} - -body { - display: table-cell; - vertical-align: middle; - margin: 2em auto; -} - -h1 { - color: #555; - font-size: 2em; - font-weight: 400; -} - -p { - margin: 0 auto; - width: 280px; -} - -@media only screen and (max-width: 280px) { - body, p { - width: 95%; - } - - h1 { - font-size: 1.5em; - margin: 0 0 0.3em; - } +.not-found { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-4); + padding-top: var(--space-14); } diff --git a/Services/Website/Resources/Static/css/shared.css b/Services/Website/Resources/Static/css/shared.css index ba80dc8..c3a799e 100644 --- a/Services/Website/Resources/Static/css/shared.css +++ b/Services/Website/Resources/Static/css/shared.css @@ -1,70 +1,258 @@ -/*! HTML5 Boilerplate v9.0.1 | MIT License | https://html5boilerplate.com/ */ - -/* main.css 3.0.0 | MIT License | https://github.com/h5bp/main.css#readme */ /* - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. + * CCN — shared stylesheet: design tokens, base styles, primitives, site header and footer. + * + * Tokens mirror the CCN design system; the dark theme only repoints them. + * The CSP blocks inline styles, so every rule lives in a stylesheet. + * Phone-first; layouts switch at 600px and 1024px. */ /* ========================================================================== - Base styles: opinionated defaults + Fonts — self-hosted latin subsets. URLs stay unversioned to match the preload links. ========================================================================== */ +@font-face { + font-family: "Anton"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/font/anton-400.woff2") format("woff2"); +} + +@font-face { + font-family: "Archivo"; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url("/font/archivo-variable.woff2") format("woff2"); +} + +@font-face { + font-family: "Oswald"; + font-style: normal; + font-weight: 500 700; + font-display: swap; + src: url("/font/oswald-variable.woff2") format("woff2"); +} + +/* ========================================================================== + Tokens + ========================================================================== */ + +:root { + color-scheme: light dark; + + /* Colour — light theme */ + --paper: #f5f3ee; + --card: #ffffff; + --tint: #edeae2; + --header: #191713; + --header-well: #24211b; + --ink: #191713; + --ink-soft: #3d3a33; + --muted: #56524a; + --faint: #a39d8f; + --placeholder: #8e8779; + --on-dark: #ffffff; + --on-dark-muted: #cfc9bc; + --on-dark-faint: #8e8779; + --on-dark-fill: #f5f3ee; + --on-dark-fill-text: #191713; + --line: #ddd8cd; + --rule: #c9c3b6; + --line-dark: #3a362e; + --accent: #b3271e; + --accent-hover: #8e1f18; + --gold: #e8b84b; + --good: #1f6f4a; + --good-bg: #e7f0ea; + --good-line: #bfd8c9; + --warn: #8a5a0a; + --warn-bg: #fbefd9; + --warn-line: #ead3a0; + --bad: #8e1f18; + --bad-bg: #f8e3e1; + --bad-line: #e9b8b3; + + /* Placeholder cover grounds */ + --cover-navy: #2b3a55; + --cover-wine: #5b2333; + --cover-forest: #1f3d33; + --cover-rust: #6b3a1f; + --cover-slate: #26363b; + --cover-plum: #43304f; + + /* Type families */ + --font-display: "Oswald", "Arial Narrow", "Helvetica Neue", sans-serif; + --font-body: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif; + --font-lettering: "Anton", "Haettenschweiler", Impact, sans-serif; + + /* Spacing — a 4px grid */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 28px; + --space-8: 32px; + --space-10: 40px; + --space-14: 56px; + + /* Page gutter: 16 / 32 / 56 */ + --gutter: 16px; + --content-max: 1328px; + + /* Radius */ + --radius-xs: 2px; + --radius-sm: 3px; + --radius-sheet: 12px; + --radius-pill: 999px; + + /* Shadow */ + --shadow-cover: 0 24px 48px rgba(25, 23, 19, 0.22); + --shadow-cover-sm: 0 18px 40px rgba(25, 23, 19, 0.28); + --shadow-sheet: 0 8px 24px rgba(25, 23, 19, 0.1); + + /* Control heights */ + --control-xs: 32px; + --control-sm: 36px; + --control-md: 44px; + --control-lg: 48px; + --control-xl: 52px; + --header-height: 56px; + + /* Card grid: 2 / 3 / 5 columns */ + --grid-columns: 2; + --grid-gap: 14px; + --card-cover: 258px; +} + +@media (min-width: 600px) { + :root { + --gutter: 32px; + --grid-columns: 3; + --grid-gap: 24px; + --card-cover: 330px; + } +} + +@media (min-width: 1024px) { + :root { + --gutter: 56px; + --header-height: 64px; + --grid-columns: 5; + --card-cover: 370px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --paper: #1c1a15; + --card: #24211b; + --tint: #2c2922; + --header: #0f0e0b; + --header-well: #1c1a15; + --ink: #f5f3ee; + --ink-soft: #d9d3c6; + --muted: #b5ae9f; + --faint: #7d776a; + --placeholder: #7d776a; + --on-dark: #f5f3ee; + --on-dark-muted: #b5ae9f; + --on-dark-faint: #7d776a; + --line: #3a362e; + --rule: #4a453c; + --accent: #d9453a; + --accent-hover: #b3271e; + --good: #7fc79a; + --good-bg: #1f3328; + --good-line: #2f5a41; + --warn: #e8b84b; + --warn-bg: #3a2e12; + --warn-line: #6a5320; + --bad: #f0a099; + --bad-bg: #3a1a17; + --bad-line: #6a2f2a; + } +} + +/* ========================================================================== + Base + ========================================================================== */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + html { - color: #222; - font-size: 1em; - line-height: 1.4; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; } -/* - * Remove text-shadow in selection highlight: - * https://twitter.com/miketaylr/status/12228805301 - * - * Customize the background color to match your design. - */ - -::-moz-selection { - background: #b3d4fc; - text-shadow: none; +body { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; + margin: 0; + background: var(--paper); + color: var(--ink); + font-family: var(--font-body); + font-size: 15px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; } -::selection { - background: #b3d4fc; - text-shadow: none; +main { + flex: 1 0 auto; } -/* - * A better looking default horizontal rule - */ +h1, +h2, +h3, +h4, +p, +ul, +ol, +dl, +dd, +figure { + margin: 0; +} -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; +ul, +ol { padding: 0; + list-style: none; } -/* - * Remove the gap between audio, canvas, iframes, - * images, videos and the bottom of their containers: - * https://github.com/h5bp/html5-boilerplate/issues/440 - */ - -audio, -canvas, -iframe, img, -svg, -video { +svg { vertical-align: middle; } -/* - * Remove default fieldset styles. - */ +img { + max-width: 100%; + height: auto; +} + +a { + color: var(--accent); +} + +a:hover { + color: var(--accent-hover); +} + +button, +input, +select, +textarea { + font: inherit; + color: inherit; +} fieldset { border: 0; @@ -72,115 +260,1100 @@ fieldset { padding: 0; } -/* - * Allow only vertical resizing of textareas. - */ - textarea { resize: vertical; } -/* ========================================================================== - Author's custom styles - ========================================================================== */ +hr { + height: 0; + margin: 0; + border: 0; + border-top: 1px solid var(--line); +} + +input::placeholder, +textarea::placeholder { + color: var(--placeholder); + opacity: 1; +} + +input[type="checkbox"], +input[type="radio"] { + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--ink); +} + +::selection { + background: var(--gold); + color: #191713; + text-shadow: none; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} /* ========================================================================== - Helper classes + Layout ========================================================================== */ -/* - * Hide visually and from screen readers - */ +/* The centred content column. */ +.page { + width: 100%; + max-width: calc(var(--content-max) + 2 * var(--gutter)); + margin: 0 auto; + padding: var(--space-6) var(--gutter) var(--space-14); +} + +.stack { + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.card-grid { + display: grid; + grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr)); + gap: var(--grid-gap); +} + +.section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-4); +} + +.skip-link { + position: absolute; + top: var(--space-2); + left: var(--space-2); + z-index: 10; + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + background: var(--on-dark-fill); + color: var(--on-dark-fill-text); + font-size: 13px; + font-weight: 600; + transform: translateY(-200%); +} + +.skip-link:focus { + color: var(--on-dark-fill-text); + transform: none; +} + +/* ========================================================================== + Type + ========================================================================== */ + +.type-hero, +.type-h1, +.type-h2, +.type-panel, +.type-price, +.type-stat { + font-family: var(--font-display); + font-weight: 600; + letter-spacing: 0; +} + +.type-hero, +.type-h1 { + font-size: 36px; + line-height: 1.02; + text-wrap: balance; +} + +.type-h2, +.type-panel { + font-size: 24px; + line-height: 1.1; +} + +.type-price { + font-size: 38px; + line-height: 1; +} + +.type-stat { + font-size: 36px; + line-height: 1; +} + +@media (min-width: 1024px) { + .type-hero { + font-size: 64px; + } + + .type-h1 { + font-size: 48px; + } + + .type-h2 { + font-size: 28px; + } + + .type-price { + font-size: 44px; + } +} + +.type-lead { + color: var(--ink-soft); + font-size: 15px; + line-height: 1.5; + text-wrap: pretty; +} + +@media (min-width: 1024px) { + .type-lead { + font-size: 17px; + } +} + +.type-body { + font-size: 15px; + line-height: 1.5; +} + +.type-body-sm { + font-size: 14px; + line-height: 1.5; +} + +.type-caption { + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.type-small { + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.type-label { + font-size: 13px; + font-weight: 600; + line-height: 1.2; +} + +.type-eyebrow { + color: var(--muted); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.12em; + line-height: 1.2; + text-transform: uppercase; +} + +.type-card-title { + font-size: 14px; + font-weight: 600; + line-height: 1.3; +} + +.type-price-card { + font-size: 18px; + font-weight: 600; + line-height: 1.2; +} + +/* A quiet, ink-coloured link. */ +.link-ink { + color: var(--ink); + font-size: 13px; + font-weight: 600; + text-decoration: underline; + text-underline-offset: 3px; +} + +.link-ink:hover { + color: var(--accent); +} + +/* ========================================================================== + Primitives + ========================================================================== */ + +/* Buttons, and links styled as buttons. */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + height: var(--control-md); + padding: 0 var(--space-5); + border: 1px solid var(--rule); + border-radius: var(--radius-sm); + background: var(--card); + color: var(--ink); + font-size: 14px; + font-weight: 600; + line-height: 1; + text-decoration: none; + white-space: nowrap; + cursor: pointer; +} + +.button:hover { + border-color: var(--ink); + color: var(--ink); +} + +.button--primary { + border-color: var(--accent); + background: var(--accent); + color: #ffffff; +} + +.button--primary:hover { + border-color: var(--accent-hover); + background: var(--accent-hover); + color: #ffffff; +} + +.button--dark { + border-color: var(--ink); + background: var(--ink); + color: var(--paper); +} + +.button--dark:hover { + background: var(--ink-soft); + color: var(--paper); +} + +.button--outline { + border-color: var(--ink); +} + +.button--sm { + height: var(--control-sm); + padding: 0 var(--space-4); + font-size: 13px; +} + +.button--lg { + height: var(--control-lg); + padding: 0 var(--space-6); + font-size: 15px; +} + +.button--block { + width: 100%; +} + +.button:disabled, +.button[aria-disabled="true"] { + border-color: var(--line); + background: var(--tint); + color: var(--muted); + cursor: not-allowed; +} + +/* Chips: grade, status and format markers. */ +.chip { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 3px 7px; + border: 1px solid var(--line); + border-radius: var(--radius-xs); + background: var(--tint); + color: var(--ink); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + line-height: 1.2; + white-space: nowrap; +} + +@media (min-width: 600px) { + .chip { + padding: var(--space-1) var(--space-2); + font-size: 11px; + } +} + +.chip--good { + border-color: var(--good-line); + background: var(--good-bg); + color: var(--good); +} + +.chip--warn { + border-color: var(--warn-line); + background: var(--warn-bg); + color: var(--warn); +} + +.chip--bad { + border-color: var(--bad-line); + background: var(--bad-bg); + color: var(--bad); +} + +/* Pills: filter chips and quick replies. */ +.pill { + display: inline-flex; + align-items: center; + height: 40px; + padding: 0 14px; + border: 1px solid var(--rule); + border-radius: var(--radius-pill); + background: var(--card); + color: var(--ink); + font-size: 13px; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + cursor: pointer; +} + +.pill:hover { + border-color: var(--ink); + color: var(--ink); +} + +.pill[aria-current], +.pill[aria-pressed="true"] { + border-color: var(--header); + background: var(--header); + color: var(--on-dark); + font-weight: 600; +} + +@media (min-width: 1024px) { + .pill { + height: var(--control-xs); + } +} + +/* Form fields */ +.field { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.field__label { + font-size: 13px; + font-weight: 600; + line-height: 1.2; +} + +.field__hint { + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.input { + width: 100%; + height: var(--control-md); + padding: 0 var(--space-3); + border: 1px solid var(--rule); + border-radius: var(--radius-sm); + background: var(--card); + color: var(--ink); + font-size: 14px; +} + +textarea.input { + height: auto; + min-height: 96px; + padding: var(--space-3); + line-height: 1.5; +} + +.input:hover { + border-color: var(--ink); +} + +.input[aria-invalid="true"] { + border-color: var(--bad); +} + +/* Surfaces */ +.card { + padding: var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--card); +} + +@media (min-width: 1024px) { + .card { + padding: var(--space-6); + } +} + +/* Notices */ +.notice { + padding: var(--space-3) var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--tint); + color: var(--ink); + font-size: 13px; + line-height: 1.45; +} + +.notice--good { + border-color: var(--good-line); + background: var(--good-bg); + color: var(--good); +} + +.notice--warn { + border-color: var(--warn-line); + background: var(--warn-bg); + color: var(--warn); +} + +.notice--bad { + border-color: var(--bad-line); + background: var(--bad-bg); + color: var(--bad); +} + +/* Two-letter monograms */ +.monogram { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + background: var(--header); + color: var(--on-dark); + font-family: var(--font-lettering); + font-size: 15px; + letter-spacing: 0.04em; + line-height: 1; +} + +.icon { + flex-shrink: 0; +} + +/* ========================================================================== + Site header — one markup, rearranged per breakpoint through grid areas. + ========================================================================== */ + +.site-header { + flex-shrink: 0; + /* On phone, the search and pills below the dark bar sit on paper. */ + background: linear-gradient(var(--header) 0 var(--header-height), var(--paper) var(--header-height)); + color: var(--on-dark); +} + +.site-header :focus-visible { + outline-color: var(--gold); +} + +.site-header__inner { + display: grid; + grid-template-areas: + "brand actions" + "search search" + "nav nav"; + grid-template-columns: auto 1fr; + grid-template-rows: var(--header-height) auto auto; + align-items: center; + width: 100%; + max-width: calc(var(--content-max) + 2 * var(--gutter)); + margin: 0 auto; + padding: 0 var(--gutter); +} + +.site-header__brand { + grid-area: brand; + color: var(--on-dark); + font-family: var(--font-lettering); + font-size: 26px; + letter-spacing: 0.04em; + line-height: 1; + text-decoration: none; +} + +.site-header__brand:hover { + color: var(--on-dark); +} + +/* Search */ +.site-search { + grid-area: search; + display: flex; + align-items: center; + min-width: 0; + padding-top: var(--space-4); +} + +.site-search__input { + flex: 1 1 auto; + min-width: 0; + height: var(--control-lg); + padding: 0 14px; + border: 1px solid var(--rule); + border-right: 0; + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + background: var(--card); + color: var(--ink); + font-size: 15px; + -webkit-appearance: none; + appearance: none; +} + +.site-search__input:focus-visible { + outline-color: var(--accent); + outline-offset: -2px; +} + +.site-search__submit { + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: var(--control-lg); + padding: 0; + border: 0; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + background: var(--header); + color: var(--on-dark); + cursor: pointer; +} + +.site-search__submit:focus-visible { + outline-color: var(--accent); +} + +/* Format navigation */ +.site-nav { + grid-area: nav; + display: flex; + align-items: center; + gap: var(--space-2); + margin: 0 calc(-1 * var(--gutter)); + padding: var(--space-5) var(--gutter) var(--space-1); + overflow-x: auto; + scrollbar-width: none; +} + +.site-nav::-webkit-scrollbar { + display: none; +} + +.site-nav__link { + display: inline-flex; + flex-shrink: 0; + align-items: center; + height: 40px; + padding: 0 14px; + border: 1px solid var(--rule); + border-radius: var(--radius-pill); + background: var(--card); + color: var(--ink); + font-size: 13px; + font-weight: 500; + text-decoration: none; + white-space: nowrap; +} + +.site-nav__link:hover { + border-color: var(--ink); + color: var(--ink); +} + +.site-nav__link[aria-current] { + border-color: var(--header); + background: var(--header); + color: var(--on-dark); + font-weight: 600; +} + +.site-nav__link:focus-visible { + outline-color: var(--accent); +} + +/* Tablet only. */ +.site-nav__link--extra { + display: none; +} + +/* Actions */ +.site-header__actions { + grid-area: actions; + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-1); +} + +.site-header__icon-link { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + color: var(--on-dark); +} + +.site-header__icon-link:hover { + color: var(--gold); +} + +.site-header__badge { + position: absolute; + top: 6px; + right: 6px; + display: flex; + align-items: center; + justify-content: center; + min-width: 16px; + height: 16px; + padding: 0 var(--space-1); + border-radius: 8px; + background: var(--accent); + color: #ffffff; + font-size: 10px; + font-weight: 700; + line-height: 1; +} + +.site-header__text-link { + color: var(--on-dark-muted); + font-size: 13px; + font-weight: 500; + text-decoration: none; +} + +.site-header__text-link:hover { + color: var(--on-dark); +} + +.site-header__text-link--strong { + color: var(--on-dark); + font-weight: 600; +} + +.site-header__sign-in { + display: inline-flex; + align-items: center; + gap: 7px; + height: 40px; + padding: 0 14px; + border-radius: var(--radius-sm); + background: var(--accent); + color: #ffffff; + font-size: 13px; + font-weight: 600; + text-decoration: none; + white-space: nowrap; +} + +.site-header__sign-in:hover { + background: var(--accent-hover); + color: #ffffff; +} + +.site-header__account { + display: inline-flex; + align-items: center; + gap: 10px; + height: 44px; + padding: 0 var(--space-2); + border-radius: var(--radius-sm); + color: var(--on-dark); + font-size: 13px; + font-weight: 600; + text-decoration: none; +} + +.site-header__account:hover { + color: var(--on-dark); +} + +.site-header__avatar { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: var(--radius-sm); + background: var(--on-dark-fill); + color: var(--on-dark-fill-text); + font-family: var(--font-lettering); + font-size: 12px; + letter-spacing: 0.04em; +} + +/* Breakpoint visibility helpers */ +.from-tablet, +.from-desktop { + display: none; +} + +@media (min-width: 600px) { + .site-header { + background: var(--header); + } + + .site-header__inner { + grid-template-areas: + "brand search actions" + "nav nav nav"; + grid-template-columns: auto minmax(0, 1fr) auto; + grid-template-rows: var(--header-height) 44px; + column-gap: var(--space-4); + } + + .site-search { + padding-top: 0; + } + + .site-search__input { + height: 40px; + border-color: var(--line-dark); + background: var(--header-well); + color: var(--on-dark); + font-size: 14px; + } + + .site-search__input:focus-visible, + .site-search__submit:focus-visible, + .site-nav__link:focus-visible { + outline-color: var(--gold); + } + + .site-search__submit { + width: 44px; + height: 40px; + background: var(--accent); + color: #ffffff; + } + + .site-search__submit:hover { + background: var(--accent-hover); + } + + .site-nav { + align-self: stretch; + gap: 22px; + padding: 0 var(--gutter); + border-top: 1px solid var(--line-dark); + } + + .site-nav__link, + .site-nav__link[aria-current] { + height: auto; + padding: 0; + border: 0; + border-radius: 0; + background: none; + } + + .site-nav__link { + color: var(--on-dark-muted); + } + + .site-nav__link:hover, + .site-nav__link[aria-current] { + color: var(--on-dark); + } + + .site-nav__link--extra { + display: inline-flex; + } + + /* Push the extra links to the end of the row. */ + .site-nav__link:not(.site-nav__link--extra) + .site-nav__link--extra { + margin-left: auto; + } + + .site-header__actions { + gap: var(--space-3); + } + + .site-header__sign-in { + height: 38px; + } + + .from-tablet { + display: revert; + } + + .phone-only { + display: none; + } +} + +@media (min-width: 1024px) { + .site-header__inner { + grid-template-areas: "brand nav search actions"; + grid-template-columns: auto auto minmax(0, 440px) 1fr; + grid-template-rows: var(--header-height); + column-gap: var(--space-7); + } + + .site-header__brand { + font-size: 28px; + } + + .site-search__input, + .site-search__submit { + height: 38px; + } + + .site-search__input { + font-size: 13px; + } + + .site-nav { + align-self: center; + margin: 0; + padding: 0; + border-top: 0; + overflow: visible; + } + + .site-nav__link--extra { + display: none; + } + + .site-header__actions { + gap: 18px; + } + + .site-header__icon-link { + width: 40px; + height: 40px; + } + + .site-header__badge { + top: 4px; + right: 4px; + } + + .site-header__sign-in { + gap: var(--space-2); + padding: 0 var(--space-4); + } + + .site-header__account { + height: 38px; + padding: 0 6px 0 var(--space-3); + } + + .from-desktop { + display: revert; + } + + .until-desktop { + display: none; + } +} + +/* ========================================================================== + Site footer + ========================================================================== */ + +.site-footer { + flex-shrink: 0; + background: var(--header); + color: var(--on-dark-muted); + font-size: 13px; +} + +.site-footer :focus-visible { + outline-color: var(--gold); +} + +.site-footer__inner { + display: flex; + flex-direction: column; + gap: var(--space-8); + width: 100%; + max-width: calc(var(--content-max) + 2 * var(--gutter)); + margin: 0 auto; + padding: var(--space-10) var(--gutter) var(--space-7); +} + +.site-footer__columns { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-8) var(--space-6); + align-items: start; +} + +.site-footer__about { + display: flex; + flex-direction: column; + grid-column: 1 / -1; + gap: 10px; + max-width: 420px; +} + +.site-footer__wordmark { + color: var(--on-dark); + font-family: var(--font-lettering); + font-size: 28px; + letter-spacing: 0.04em; + line-height: 1.2; +} + +.site-footer__column { + display: flex; + flex-direction: column; + gap: 10px; +} + +.site-footer__heading { + color: var(--on-dark); + font-family: inherit; + font-size: 13px; + font-weight: 600; + line-height: 1.5; +} + +.site-footer__links { + display: flex; + flex-direction: column; + gap: 10px; +} + +.site-footer a { + color: var(--on-dark-muted); + text-decoration: none; +} + +.site-footer a:hover { + color: var(--on-dark); + text-decoration: underline; + text-underline-offset: 3px; +} + +.site-footer__language { + align-items: flex-start; +} + +.language-toggle { + display: inline-flex; + overflow: hidden; + border: 1px solid var(--line-dark); + border-radius: var(--radius-sm); +} + +.language-toggle__option { + height: var(--control-sm); + padding: 0 14px; + border: 0; + background: transparent; + color: var(--on-dark-muted); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.language-toggle__option[aria-pressed="true"] { + background: var(--on-dark-fill); + color: var(--on-dark-fill-text); +} + +.language-toggle__option:focus-visible { + outline-offset: -2px; +} + +.site-footer__note { + color: var(--on-dark-faint); + font-size: 12px; +} + +.site-footer__legal { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding-top: var(--space-4); + border-top: 1px solid var(--line-dark); + color: var(--on-dark-faint); + font-size: 12px; +} + +.site-footer__legal-links { + display: flex; + flex-shrink: 0; + gap: var(--space-4); +} + +@media (min-width: 600px) { + .site-footer__columns { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@media (min-width: 1024px) { + .site-footer__columns { + grid-template-columns: 300px auto auto auto 1fr; + column-gap: 64px; + } + + .site-footer__about { + grid-column: auto; + } + + .site-footer__language { + justify-self: end; + } + + .site-footer__legal { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--space-8); + } +} + +/* ========================================================================== + Helpers + ========================================================================== */ .hidden, [hidden] { display: none !important; } -/* - * Hide only visually, but have it available for screen readers: - * https://snook.ca/archives/html_and_css/hiding-content-for-accessibility - * - * 1. For long content, line feeds are not interpreted as spaces and small width - * causes content to wrap 1 word per line: - * https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe - */ - +/* Hidden visually, still read out. */ .visually-hidden { - border: 0; - clip: rect(0, 0, 0, 0); + position: absolute; + width: 1px; height: 1px; margin: -1px; - overflow: hidden; padding: 0; - position: absolute; + overflow: hidden; + border: 0; + clip-path: inset(50%); white-space: nowrap; - width: 1px; - /* 1 */ -} - -/* - * Extends the .visually-hidden class to allow the element - * to be focusable when navigated to via the keyboard: - * https://www.drupal.org/node/897638 - */ - -.visually-hidden.focusable:active, -.visually-hidden.focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - white-space: inherit; - width: auto; -} - -/* - * Hide visually and from screen readers, but maintain layout - */ - -.invisible { - visibility: hidden; -} - -/* - * Clearfix: contain floats - * - * The use of `table` rather than `block` is only necessary if using - * `::before` to contain the top-margins of child elements. - */ - -.clearfix::before, -.clearfix::after { - content: ""; - display: table; -} - -.clearfix::after { - clear: both; } /* ========================================================================== - EXAMPLE Media Queries for Responsive Design. - These examples override the primary ('mobile first') styles. - Modify as content requires. - ========================================================================== */ - -@media only screen and (min-width: 35em) { - /* Style adjustments for viewports that meet the condition */ -} - -@media print, - (min-resolution: 1.25dppx), - (min-resolution: 120dpi) { - /* Style adjustments for high resolution devices */ -} - -/* ========================================================================== - Print styles. - Inlined to avoid the additional HTTP request: - https://www.phpied.com/delay-loading-your-print-css/ + Print ========================================================================== */ @media print { @@ -188,59 +1361,29 @@ textarea { *::before, *::after { background: #fff !important; - color: #000 !important; - /* Black prints faster */ box-shadow: none !important; + color: #000 !important; text-shadow: none !important; } + .site-header, + .site-footer, + .skip-link { + display: none !important; + } + a, a:visited { text-decoration: underline; } - a[href]::after { - content: " (" attr(href) ")"; - } - - abbr[title]::after { - content: " (" attr(title) ")"; - } - - /* - * Don't show links that are fragment identifiers, - * or use the `javascript:` pseudo protocol - */ - a[href^="#"]::after, - a[href^="javascript:"]::after { - content: ""; - } - - pre { - white-space: pre-wrap !important; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - tr, img { page-break-inside: avoid; } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, h3 { page-break-after: avoid; } } - diff --git a/Services/Website/Resources/Static/font/README.txt b/Services/Website/Resources/Static/font/README.txt new file mode 100644 index 0000000..45ce2d6 --- /dev/null +++ b/Services/Website/Resources/Static/font/README.txt @@ -0,0 +1,7 @@ +CCN — self-hosted fonts (latin subsets, from Google Fonts; SIL Open Font License 1.1) + + anton-400.woff2 Anton 400 lettering + archivo-variable.woff2 Archivo 400-700 body + oswald-variable.woff2 Oswald 500-700 display + +Served unversioned: the preload links must match the `@font-face` URLs in `css/shared.css`. diff --git a/Services/Website/Resources/Static/font/anton-400.woff2 b/Services/Website/Resources/Static/font/anton-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..35e8f012a8465a5a58da9f3420c179d3f7ff4372 GIT binary patch literal 12004 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 C>1K!k literal 0 HcmV?d00001 diff --git a/Services/Website/Resources/Static/font/archivo-variable.woff2 b/Services/Website/Resources/Static/font/archivo-variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a31bef933041b7c88ab8e8c947eec137289111e9 GIT binary patch literal 34940 zcmV)5K*_&%Pew8T0RR910Em156951J0bU3I0Eh|z0RR9100000000000000000000 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 Q8GKxUfS;l%L<;}_0MM30!2kdN literal 0 HcmV?d00001 diff --git a/Services/Website/Resources/Static/font/oswald-variable.woff2 b/Services/Website/Resources/Static/font/oswald-variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..aa7ae2dc0ed2632b5927458e6408a01d15572fd4 GIT binary patch literal 21412 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 literal 0 HcmV?d00001 diff --git a/Services/Website/Resources/Static/site.webmanifest b/Services/Website/Resources/Static/site.webmanifest index e5e517f..c396092 100644 --- a/Services/Website/Resources/Static/site.webmanifest +++ b/Services/Website/Resources/Static/site.webmanifest @@ -1,6 +1,6 @@ { - "short_name": "", - "name": "", + "short_name": "CCN", + "name": "CCN — Comics marketplace", "icons": [{ "src": "icon-192.png", "type": "image/png", @@ -11,6 +11,6 @@ "sizes": "512x512" }], "start_url": "/?utm_source=homescreen", - "background_color": "#fafafa", - "theme_color": "#fafafa" + "background_color": "#f5f3ee", + "theme_color": "#191713" } diff --git a/Services/Website/Sources/Library/Catalogs/Localizable.xcstrings b/Services/Website/Sources/Library/Catalogs/Localizable.xcstrings index 729eda6..a17c14d 100644 --- a/Services/Website/Sources/Library/Catalogs/Localizable.xcstrings +++ b/Services/Website/Sources/Library/Catalogs/Localizable.xcstrings @@ -1,13 +1,13 @@ { "sourceLanguage" : "en", "strings" : { - "index.greeting" : { + "index.heading" : { "comment" : "The landing page's greeting paragraph.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Hello world! This is HTML5 Boilerplate." + "value" : "Back issues, from shops you can vet first." } } } @@ -18,7 +18,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Index page" + "value" : "CCN — Comics marketplace" } } } @@ -51,7 +51,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Page Not Found" + "value" : "Page Not Found — CCN" } } } diff --git a/Services/Website/Sources/Library/Internal/Components/Icon.swift b/Services/Website/Sources/Library/Internal/Components/Icon.swift new file mode 100644 index 0000000..8882535 --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Components/Icon.swift @@ -0,0 +1,96 @@ +import Elementary + +/// A decorative inline SVG stroke icon, drawn in the current text colour. +struct Icon: HTML { + + // MARK: Properties + + /// The glyph the icon draws. + let glyph: Glyph + + /// The icon's rendered width and height, in pixels. + let size: Int + + // MARK: Initializers + + /// Creates an icon. + /// - Parameters: + /// - glyph: the glyph the icon draws. + /// - size: the icon's rendered width and height, in pixels; 20 by default. + init( + _ glyph: Glyph, + size: Int = 20 + ) { + self.glyph = glyph + self.size = size + } + + // MARK: Computed + + /// Rendered raw: the markup is static, never user input. + var body: some HTML { + HTMLRaw( + """ + + """ + ) + } + +} + +// MARK: - Enumerations + +extension Icon { + /// A glyph an ``Icon`` can draw. + enum Glyph { + /// A bell. + case bell + /// A bookmark. + case bookmark + /// A check mark. + case check + /// Filter lines. + case filter + /// A key. + case key + /// A speech bubble. + case message + /// A magnifying glass. + case search + /// A shopfront. + case shop + /// Sort arrows. + case sort + } +} + +extension Icon.Glyph { + + // MARK: Computed + + /// The shapes the glyph is drawn with. + var shapes: String { + switch self { + case .bell: #""# + case .bookmark: #""# + case .check: #""# + case .filter: #""# + case .key: #""# + case .message: #""# + case .search: #""# + case .shop: #""# + case .sort: #""# + } + } + + /// The glyph's stroke width; heavier for glyphs drawn small. + var strokeWidth: String { + switch self { + case .check: "3.5" + case .filter, .key, .search, .sort: "2.2" + default: "2" + } + } + +} diff --git a/Services/Website/Sources/Library/Internal/Components/SiteFooter.swift b/Services/Website/Sources/Library/Internal/Components/SiteFooter.swift new file mode 100644 index 0000000..1cde858 --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Components/SiteFooter.swift @@ -0,0 +1,154 @@ +import Elementary + +/// The site footer: the about blurb, the link columns, the language toggle, and the legal line. +struct SiteFooter: HTML { + + // MARK: Computed + + var body: some HTML { + footer(.class("site-footer")) { + div(.class("site-footer__inner")) { + div(.class("site-footer__columns")) { + div(.class("site-footer__about")) { + span(.class("site-footer__wordmark")) { + "CCN" + } + p { + "A marketplace for single-issue comics, manga, trade paperbacks and hardcovers, sold by independent shops in the Netherlands and across the EU." + } + } + for column in Self.columns { + nav( + .class("site-footer__column"), + .custom( + name: "aria-label", + value: column.title + ) + ) { + h2(.class("site-footer__heading")) { + column.title + } + ul(.class("site-footer__links")) { + for link in column.links { + li { + a(.href(link.path)) { + link.label + } + } + } + } + } + } + language + } + div(.class("site-footer__legal")) { + span { + "CCN B.V. · Amsterdam · KvK [NUMBER] · BTW-id [NUMBER] · Business sellers show their KvK number on their shop page; consumers have a 14-day right of withdrawal on purchases from business sellers." + } + span(.class("site-footer__legal-links")) { + a(.href(.Route.terms)) { + "Terms" + } + a(.href(.Route.privacy)) { + "Privacy" + } + a(.href(.Route.sellerRules)) { + "Seller rules" + } + } + } + } + } + } + +} + +// MARK: - Helpers + +private extension SiteFooter { + + // MARK: Type aliases + + /// A titled column of links. + typealias Column = (title: String, links: [(label: String, path: String)]) + + // MARK: Constants + + /// The link columns, in reading order. + static let columns: [Column] = [ + ( + title: "Browse", + links: Format.allCases.map { ($0.label, $0.path) } + [ + ("By series", .Route.series) + ] + ), + ( + title: "Sell", + links: [ + ("Open a shop", .Route.openShop), + ("Seller dashboard", .Route.sellerDashboard), + ("Grading guide", .Route.gradingGuide), + ("Trade at events", .Route.events), + ] + ), + ( + title: "Account", + links: [ + ("Sign in or join", .Route.signIn), + ("Want list & alerts", .Route.wants), + ("My collection", .Route.collection), + ("Messages", .Route.messages), + ("Passkeys & settings", .Route.account), + ("Lost your passkey?", .Route.recover), + ] + ), + ] + + // MARK: Computed + + /// The language toggle; display-only while the prototype serves English alone. + var language: some HTML { + div(.class("site-footer__column site-footer__language")) { + h2(.class("site-footer__heading")) { + "Language" + } + div( + .class("language-toggle"), + .custom( + name: "role", + value: "group" + ), + .custom( + name: "aria-label", + value: "Language" + ) + ) { + button( + .class("language-toggle__option"), + .type(.button), + .custom( + name: "aria-pressed", + value: "false" + ), + .disabled + ) { + "NL" + } + button( + .class("language-toggle__option"), + .type(.button), + .custom( + name: "aria-pressed", + value: "true" + ) + ) { + "EN" + } + } + span(.class("site-footer__note")) { + "Prices in € · ships to NL" + } + } + } + +} diff --git a/Services/Website/Sources/Library/Internal/Components/SiteHeader.swift b/Services/Website/Sources/Library/Internal/Components/SiteHeader.swift new file mode 100644 index 0000000..46187c0 --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Components/SiteHeader.swift @@ -0,0 +1,253 @@ +import Elementary + +/// The site header: the wordmark, the format navigation, the search field, and the viewer's actions. +/// +/// One markup serves all three layouts; the shared stylesheet rearranges it through grid areas. +struct SiteHeader: HTML { + + // MARK: Properties + + /// The format marked as current in the navigation, or `nil` for none. + let active: Format? + + /// The signed-in viewer, or `nil` to render the signed-out actions. + let viewer: Viewer? + + // MARK: Initializers + + /// Creates a site header. + /// - Parameters: + /// - active: the format marked as current, or `nil` (the default) for none. + /// - viewer: the signed-in viewer, or `nil` (the default) to render the signed-out actions. + init( + active: Format? = nil, + viewer: Viewer? = nil + ) { + self.active = active + self.viewer = viewer + } + + // MARK: Computed + + var body: some HTML { + header(.class("site-header")) { + div(.class("site-header__inner")) { + a( + .class("site-header__brand"), + .href(.Route.home) + ) { + "CCN" + } + navigation + search + div(.class("site-header__actions")) { + if let viewer { + actions(for: viewer) + } else { + guestActions + } + } + } + } + } + +} + +// MARK: - Helpers + +private extension SiteHeader { + + // MARK: Computed + + /// The signed-out actions: *Open a shop* and the passkey sign-in. + @HTMLBuilder + var guestActions: some HTML { + a( + .class("site-header__icon-link phone-only"), + .href(.Route.openShop), + .custom( + name: "aria-label", + value: "Open a shop" + ) + ) { + Icon(.shop, size: 22) + } + a( + .class("site-header__text-link from-desktop"), + .href(.Route.openShop) + ) { + "Open a shop" + } + a( + .class("site-header__sign-in"), + .href(.Route.signIn) + ) { + Icon(.key, size: 15) + span { + "Sign in" + span(.class("from-desktop")) { + " with passkey" + } + } + } + } + + /// The format links, plus the series and events links shown on tablet only. + var navigation: some HTML { + nav( + .class("site-nav"), + .custom( + name: "aria-label", + value: "Formats" + ) + ) { + for format in Format.allCases { + if format == active { + a( + .class("site-nav__link"), + .href(format.path), + .custom( + name: "aria-current", + value: "page" + ) + ) { + format.label + } + } else { + a( + .class("site-nav__link"), + .href(format.path) + ) { + format.label + } + } + } + a( + .class("site-nav__link site-nav__link--extra"), + .href(.Route.series) + ) { + "Series" + } + a( + .class("site-nav__link site-nav__link--extra"), + .href(.Route.events) + ) { + "Events" + } + } + } + + /// The search field, a plain `GET` form. + var search: some HTML { + form( + .class("site-search"), + .action(.Route.search), + .method(.get), + .custom( + name: "role", + value: "search" + ) + ) { + label( + .class("visually-hidden"), + .for("site-search-query") + ) { + "Search listings" + } + input( + .class("site-search__input"), + .id("site-search-query"), + .type(.search), + .name("q"), + .placeholder("Search title, issue, grade or shop") + ) + button( + .class("site-search__submit"), + .type(.submit), + .custom( + name: "aria-label", + value: "Search" + ) + ) { + Icon(.search, size: 16) + } + } + } + + // MARK: Methods + + /// The signed-in actions: want list, messages, shop, and account. + /// - Parameter viewer: the signed-in viewer. + @HTMLBuilder + func actions( + for viewer: Viewer + ) -> some HTML { + counter( + .bell, + label: "Want list", + count: viewer.newWantMatches, + unit: "new matches", + path: .Route.wants + ) + counter( + .message, + label: "Messages", + count: viewer.unreadMessages, + unit: "unread", + path: .Route.messages + ) + a( + .class("site-header__text-link site-header__text-link--strong from-desktop"), + .href(.Route.sellerDashboard) + ) { + "My shop" + } + a( + .class("site-header__account"), + .href(.Route.account), + .custom( + name: "aria-label", + value: "Account of \(viewer.displayName)" + ) + ) { + span(.class("from-desktop")) { + viewer.displayName + } + span(.class("site-header__avatar")) { + viewer.initials + } + } + } + + /// An icon link with a count badge, omitted at zero. + /// - Parameters: + /// - glyph: the glyph the link shows. + /// - label: the link's accessible name. + /// - count: the number the badge shows. + /// - unit: what the count measures. + /// - path: the path the link leads to. + func counter( + _ glyph: Icon.Glyph, + label: String, + count: Int, + unit: String, + path: String + ) -> some HTML { + a( + .class("site-header__icon-link"), + .href(path), + .custom( + name: "aria-label", + value: count > 0 ? "\(label), \(count) \(unit)" : label + ) + ) { + Icon(glyph) + if count > 0 { + span(.class("site-header__badge")) { + "\(count)" + } + } + } + } + +} diff --git a/Services/Website/Sources/Library/Internal/Components/SiteLayout.swift b/Services/Website/Sources/Library/Internal/Components/SiteLayout.swift new file mode 100644 index 0000000..e2707da --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Components/SiteLayout.swift @@ -0,0 +1,53 @@ +import Elementary + +/// The frame a page wraps its content in: a skip link, the ``SiteHeader``, the content inside `main`, and the ``SiteFooter``. +struct SiteLayout: HTML { + + // MARK: Properties + + /// The format marked as current in the header, or `nil` for none. + let active: Format? + + /// The screen's markup, rendered inside the `main` landmark. + let content: Content + + /// The signed-in viewer, or `nil` to render the signed-out header. + let viewer: Viewer? + + // MARK: Initializers + + /// Creates a site layout around a screen's content. + /// - Parameters: + /// - active: the format marked as current, or `nil` (the default) for none. + /// - viewer: the signed-in viewer, or `nil` (the default) to render the signed-out header. + /// - content: the screen's markup. + init( + active: Format? = nil, + viewer: Viewer? = nil, + @HTMLBuilder content: () -> Content + ) { + self.active = active + self.content = content() + self.viewer = viewer + } + + // MARK: Computed + + var body: some HTML { + a( + .class("skip-link"), + .href("#main") + ) { + "Skip to content" + } + SiteHeader( + active: active, + viewer: viewer + ) + main(.id("main")) { + content + } + SiteFooter() + } + +} diff --git a/Services/Website/Sources/Library/Internal/Enumerations/Format.swift b/Services/Website/Sources/Library/Internal/Enumerations/Format.swift new file mode 100644 index 0000000..b388dc3 --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Enumerations/Format.swift @@ -0,0 +1,34 @@ +/// A format listings are published in, declared in navigation order; the raw value is the search filter token. +enum Format: String, CaseIterable, Sendable { + /// Single-issue comics. + case singleIssues = "single-issues" + /// Manga volumes. + case manga + /// Trade paperbacks. + case tradePaperbacks = "trade-paperbacks" + /// Hardcovers. + case hardcovers +} + +// MARK: - Extensions + +extension Format { + + // MARK: Computed + + /// The format's name as shown in the navigation. + var label: String { + switch self { + case .singleIssues: "Single issues" + case .manga: "Manga" + case .tradePaperbacks: "Trade paperbacks" + case .hardcovers: "Hardcovers" + } + } + + /// The path of the search screen filtered to the format. + var path: String { + "\(String.Route.search)?format=\(rawValue)" + } + +} diff --git a/Services/Website/Sources/Library/Internal/Extensions/Page+Defaults.swift b/Services/Website/Sources/Library/Internal/Extensions/Page+Defaults.swift index 7ce8886..2eabd7e 100644 --- a/Services/Website/Sources/Library/Internal/Extensions/Page+Defaults.swift +++ b/Services/Website/Sources/Library/Internal/Extensions/Page+Defaults.swift @@ -11,9 +11,9 @@ extension Page { /// The fonts preloaded on every page: one per face the stylesheets actually render. /// /// Listed rather than derived from every WOFF2 in ``StaticFile/all``: a subset gated by a `unicode-range` no page reaches would add a - /// download that never otherwise happens. Empty until the site ships fonts of its own. + /// download that never otherwise happens. All three faces render on every page. static var preloadedFonts: [StaticFile] { - [] + [.fontArchivo, .fontOswald, .fontAnton] } /// The territories paired with the site's languages, in Open Graph's `language_TERRITORY` form. @@ -147,11 +147,11 @@ extension Page { ) meta( .name("theme-color"), - .content("#fafafa") + .content("#191713") ) meta( .name("theme-color"), - .content("#0c0710"), + .content("#0f0e0b"), .custom( name: "media", value: "(prefers-color-scheme: dark)" diff --git a/Services/Website/Sources/Library/Internal/Extensions/String+Routes.swift b/Services/Website/Sources/Library/Internal/Extensions/String+Routes.swift new file mode 100644 index 0000000..9d79f3b --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Extensions/String+Routes.swift @@ -0,0 +1,39 @@ +extension String { + /// A namespace for the paths of the site's screens. + /// + /// Screens not built yet are listed too, and answer with the not-found page until they are. + enum Route { + /// The account screen. + static let account = "/account" + /// The collection tracker. + static let collection = "/collection" + /// The events screen. + static let events = "/events" + /// The public grading guide. + static let gradingGuide = "/grading-guide" + /// The browse / home screen. + static let home = "/" + /// The message threads. + static let messages = "/messages" + /// The open-a-shop onboarding. + static let openShop = "/sell/open-a-shop" + /// The privacy statement. + static let privacy = "/privacy" + /// The passkey recovery screen. + static let recover = "/recover" + /// The search results screen. + static let search = "/search" + /// The seller dashboard. + static let sellerDashboard = "/sell" + /// The rules sellers agree to. + static let sellerRules = "/seller-rules" + /// The series index. + static let series = "/series" + /// The passkey sign-in screen. + static let signIn = "/sign-in" + /// The terms of service. + static let terms = "/terms" + /// The want list. + static let wants = "/wants" + } +} diff --git a/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift b/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift index ee61949..5485dcb 100644 --- a/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift +++ b/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift @@ -82,9 +82,36 @@ extension IndexPage: Page { // MARK: Properties + /// The hero alone, until the listings exist. var content: some HTML { - p { - localize("index.greeting", locale: locale) + SiteLayout { + div(.class("page")) { + section(.class("hero")) { + p(.class("type-eyebrow")) { + "Single issues · Manga · Trades · Hardcovers" + } + h1(.class("type-hero")) { + localize("index.heading", locale: locale) + } + p(.class("type-lead")) { + "Search every listing by title, print and grade. Browse any seller's shop without an account — sign in with a passkey only when you're ready to buy, make an offer or message." + } + div(.class("hero__actions")) { + a( + .class("button button--primary button--lg"), + .href(Format.singleIssues.path) + ) { + "Browse single issues" + } + a( + .class("button button--outline button--lg"), + .href(.Route.series) + ) { + "Browse by series" + } + } + } + } } } diff --git a/Services/Website/Sources/Library/Internal/Pages/NotFoundPage.swift b/Services/Website/Sources/Library/Internal/Pages/NotFoundPage.swift index 902f827..cbc4a52 100644 --- a/Services/Website/Sources/Library/Internal/Pages/NotFoundPage.swift +++ b/Services/Website/Sources/Library/Internal/Pages/NotFoundPage.swift @@ -47,11 +47,21 @@ extension NotFoundPage: Page { // MARK: Properties var content: some HTML { - h1 { - localize("notFound.heading", locale: locale) - } - p { - localize("notFound.message", locale: locale) + SiteLayout { + div(.class("page not-found")) { + h1(.class("type-h1")) { + localize("notFound.heading", locale: locale) + } + p(.class("type-lead")) { + localize("notFound.message", locale: locale) + } + a( + .class("button button--dark"), + .href(.Route.home) + ) { + "Back to browsing" + } + } } } diff --git a/Services/Website/Sources/Library/Internal/Types/StaticFile.swift b/Services/Website/Sources/Library/Internal/Types/StaticFile.swift index 7925703..3612c37 100644 --- a/Services/Website/Sources/Library/Internal/Types/StaticFile.swift +++ b/Services/Website/Sources/Library/Internal/Types/StaticFile.swift @@ -51,6 +51,9 @@ extension StaticFile { static let all: [Self] = [ .appleTouchIcon, .favicon, + .fontAnton, + .fontArchivo, + .fontOswald, .icon, .icon192, .icon512, @@ -104,6 +107,15 @@ extension StaticFile { /// The `favicon.ico` icon. static let favicon = Self("favicon", as: .ico) + /// The `font/anton-400.woff2` lettering face. + static let fontAnton = Self("anton-400", as: .woff2) + + /// The `font/archivo-variable.woff2` body face (weights 400–700). + static let fontArchivo = Self("archivo-variable", as: .woff2) + + /// The `font/oswald-variable.woff2` display face (weights 500–700). + static let fontOswald = Self("oswald-variable", as: .woff2) + /// The `icon.svg` icon. static let icon = Self("icon", as: .svg) diff --git a/Services/Website/Sources/Library/Internal/Types/Viewer.swift b/Services/Website/Sources/Library/Internal/Types/Viewer.swift new file mode 100644 index 0000000..04fda7c --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Types/Viewer.swift @@ -0,0 +1,50 @@ +/// The signed-in person a page is rendered for. +struct Viewer: Sendable { + + // MARK: Properties + + /// The viewer's name. + let displayName: String + + /// The number of new listings matching the viewer's want list. + let newWantMatches: Int + + /// The number of messages the viewer has not read yet. + let unreadMessages: Int + + // MARK: Initializers + + /// Creates a viewer. + /// - Parameters: + /// - displayName: the viewer's name. + /// - newWantMatches: the number of new listings matching the viewer's want list; none by default. + /// - unreadMessages: the number of messages the viewer has not read yet; none by default. + init( + displayName: String, + newWantMatches: Int = 0, + unreadMessages: Int = 0 + ) { + self.displayName = displayName + self.newWantMatches = newWantMatches + self.unreadMessages = unreadMessages + } + +} + +// MARK: - Extensions + +extension Viewer { + + // MARK: Computed + + /// The uppercased initials of the first two words of the display name. + var initials: String { + displayName + .split(separator: " ") + .prefix(2) + .compactMap(\.first) + .map { String($0).uppercased() } + .joined() + } + +} diff --git a/Services/Website/Tests/App/AppTests.swift b/Services/Website/Tests/App/AppTests.swift index 61ca5f3..b4ba04f 100644 --- a/Services/Website/Tests/App/AppTests.swift +++ b/Services/Website/Tests/App/AppTests.swift @@ -53,7 +53,7 @@ struct AppTests { #expect(response.status == .ok) #expect(response.headers[.contentType] == "text/html; charset=utf-8") - #expect(body.contains("Hello world!")) + #expect(body.contains("Back issues, from shops you can vet first.")) } } } diff --git a/Services/Website/Tests/Library/Cases/Internal/Components/IconTests.swift b/Services/Website/Tests/Library/Cases/Internal/Components/IconTests.swift new file mode 100644 index 0000000..2982055 --- /dev/null +++ b/Services/Website/Tests/Library/Cases/Internal/Components/IconTests.swift @@ -0,0 +1,30 @@ +import Elementary +import Testing + +@testable import WebsiteLibrary + +@Suite( + "Icon component", + .tags(.component) +) +struct IconTests { + + // MARK: Functional tests + + @Test + func `renders a decorative icon at the given size`() { + let html = Icon(.search, size: 16).render() + + #expect(html.hasPrefix("\(title)")) + } + + for path in [String.Route.series, .Route.openShop, .Route.gradingGuide, .Route.signIn, .Route.recover, .Route.terms] { + #expect(html.contains(#"href="\#(path)""#)) + } + } + + @Test + func `escapes the ampersands in its labels`() { + #expect(SiteFooter().render().contains("Want list & alerts")) + } + + @Test + func `presses the one language the prototype serves`() { + let html = SiteFooter().render() + + #expect(html.components(separatedBy: #"aria-pressed="true""#).count - 1 == 1) + #expect(html.contains(#"aria-pressed="true">EN<"#)) + } + +} diff --git a/Services/Website/Tests/Library/Cases/Internal/Components/SiteHeaderTests.swift b/Services/Website/Tests/Library/Cases/Internal/Components/SiteHeaderTests.swift new file mode 100644 index 0000000..441abf1 --- /dev/null +++ b/Services/Website/Tests/Library/Cases/Internal/Components/SiteHeaderTests.swift @@ -0,0 +1,78 @@ +import Elementary +import Testing + +@testable import WebsiteLibrary + +@Suite( + "SiteHeader component", + .tags(.component) +) +struct SiteHeaderTests { + + // MARK: Functional tests + + @Test + func `renders the signed-out actions without a viewer`() { + let html = SiteHeader().render() + + #expect(html.contains(#"href="/sign-in""#)) + #expect(html.contains("Open a shop")) + #expect(!html.contains(#"href="/account""#)) + #expect(!html.contains(#"href="/messages""#)) + } + + @Test + func `renders the signed-in actions for a viewer`() { + let html = SiteHeader(viewer: .init( + displayName: "Mara Visser", + newWantMatches: 2, + unreadMessages: 3 + )).render() + + #expect(html.contains(#"aria-label="Want list, 2 new matches""#)) + #expect(html.contains(#"aria-label="Messages, 3 unread""#)) + #expect(html.contains(#"href="/sell""#)) + #expect(html.contains(">MV<")) + #expect(!html.contains(#"href="/sign-in""#)) + } + + @Test + func `omits a badge that counts nothing`() { + let html = SiteHeader(viewer: .init(displayName: "Mara")).render() + + #expect(html.contains(#"aria-label="Messages""#)) + #expect(!html.contains("site-header__badge")) + } + + @Test + func `links every format and marks none current by default`() { + let html = SiteHeader().render() + + for format in Format.allCases { + #expect(html.contains(#"href="\#(format.path)""#)) + } + + #expect(!html.contains("aria-current")) + } + + @Test(arguments: Format.allCases) + func `marks the active format alone as current`( + _ format: Format + ) { + let html = SiteHeader(active: format).render() + + #expect(html.components(separatedBy: #"aria-current="page""#).count - 1 == 1) + #expect(html.contains(#"href="\#(format.path)" aria-current="page""#)) + } + + @Test + func `searches through a plain GET form`() { + let html = SiteHeader().render() + + #expect(html.contains(#"action="/search""#)) + #expect(html.contains(#"method="get""#)) + #expect(html.contains(#"name="q""#)) + #expect(html.contains(#"for="site-search-query""#)) + } + +} diff --git a/Services/Website/Tests/Library/Cases/Internal/Enumerations/FormatTests.swift b/Services/Website/Tests/Library/Cases/Internal/Enumerations/FormatTests.swift new file mode 100644 index 0000000..617436e --- /dev/null +++ b/Services/Website/Tests/Library/Cases/Internal/Enumerations/FormatTests.swift @@ -0,0 +1,29 @@ +import Testing + +@testable import WebsiteLibrary + +@Suite( + "Format enumeration", + .tags(.enumeration) +) +struct FormatTests { + + // MARK: Computed tests + + @Test(arguments: zip( + Format.allCases, + [ + "/search?format=single-issues", + "/search?format=manga", + "/search?format=trade-paperbacks", + "/search?format=hardcovers", + ] + )) + func `path`( + of format: Format, + expects path: String + ) { + #expect(format.path == path) + } + +} diff --git a/Services/Website/Tests/Library/Cases/Internal/Pages/IndexPageTests.swift b/Services/Website/Tests/Library/Cases/Internal/Pages/IndexPageTests.swift index 5e49a81..705e5a0 100644 --- a/Services/Website/Tests/Library/Cases/Internal/Pages/IndexPageTests.swift +++ b/Services/Website/Tests/Library/Cases/Internal/Pages/IndexPageTests.swift @@ -27,16 +27,47 @@ struct IndexPageTests { #expect(html.contains("/apple-touch-icon.png")) #expect(html.contains("/site.webmanifest")) #expect(html.contains(#"media="(prefers-color-scheme: dark)""#)) - #expect(html.contains("Hello world!")) + #expect(html.contains("Back issues, from shops you can vet first.")) #expect(html.contains("/js/shared.js")) #expect(html.contains("/js/index.js")) } @Test - func `renders no font preloads until fonts are declared`() { - // The template ships no fonts, so a preload link here would point at a file the service does not serve. - #expect(IndexPage.preloadedFonts.isEmpty) - #expect(!IndexPage(locale: .init(identifier: "en")).render().contains(#"rel="preload""#)) + func `renders the site frame around its content`() { + let html = IndexPage( + locale: .init(identifier: "en") + ).render() + + #expect(html.contains(#"class="skip-link""#)) + #expect(html.contains(#"class="site-header""#)) + #expect(html.contains(#"
"#)) + #expect(html.contains(#"class="site-footer""#)) + } + + @Test + func `renders no inline styles`() { + // The Content-Security-Policy is `default-src 'self'`, which blocks `style` attributes and elements alike. + let html = IndexPage( + locale: .init(identifier: "en") + ).render() + + #expect(!html.contains("style=")) + #expect(!html.contains("