-
-
Notifications
You must be signed in to change notification settings - Fork 53
Redesign the server-rendered front-end #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
156c392
Document the front-end design system
dahlia 5ca754f
Introduce the UnoCSS toolchain
dahlia ca90f6c
Migrate the layout shell to UnoCSS and remove Pico CSS
dahlia 119060e
Fix UnoCSS color generation and stylesheet caching
dahlia 1970f3e
Redesign the login, setup, and OTP pages
dahlia 21c5c76
Redesign the public home page
dahlia 7357d0b
Redesign the profile page
dahlia 7df79bb
Redesign the post component
dahlia 22d1912
Redesign the account management screens
dahlia 783baae
Redesign the custom emojis pages
dahlia 8fc2b4d
Redesign the federation page
dahlia 6c68bbf
Redesign the thumbnail cleanup page
dahlia 9ba0615
Redesign the public hashtag page
dahlia 39d1233
Redesign the OAuth authorization screens
dahlia 8579507
Redesign the dashboard auth page
dahlia d2bb828
Redesign the migrate account page
dahlia 9ea04fc
Extract reusable form primitives
dahlia 8dd6f16
Adopt form primitives in the auth forms
dahlia dd2efff
Adopt form primitives in AccountForm
dahlia b21ac2e
Replace theme color picker with a swatch grid
dahlia 42f7b10
Re-enable the variant group transformer
dahlia 88f487d
Document the brand color alpha behaviour
dahlia 19ef762
Render link preview cards in posts
dahlia 523b07c
Tighten the single-post permalink page
dahlia a130f8c
Fix theme color picker click feedback
dahlia 00d7697
Force solid 1px borders on form inputs
dahlia 2bb5362
Show a pointer cursor on buttons
dahlia 55170fd
Rename the Defaults section to Preferences
dahlia ea4e7f1
Style the emoji image upload as a dropzone
dahlia 8740296
Unify the account form shape with the other forms
dahlia 3c4c0af
Match input typography across forms
dahlia 229e57f
Stop emitting variant group shorthand into HTML
dahlia 7e2ebe9
Balance the section heading spacing in FieldSection
dahlia ee1cf10
Show @ and @host chips around the username field
dahlia 31d9c9e
Lean on the brand color in profile bios, stats, and tags
dahlia ae88c91
Tint text selection with the active brand color
dahlia 1f8c65a
Fix divide-y borders in dark mode
dahlia 133cebb
Dim primary buttons in dark mode
dahlia 5a5fc5b
Note the front-end redesign in CHANGES.md
dahlia 69a1c49
Switch DESIGN.md code tokens from italics to backticks
dahlia 1e2eaed
Fix tests broken by the front-end redesign
dahlia d490c96
Scope the screenshot ignore rule to the repository root
dahlia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,356 @@ | ||
| Hollo design system | ||
| =================== | ||
|
|
||
| This document defines the visual design language and front-end conventions | ||
| of Hollo's web pages. Hollo is primarily a headless ActivityPub server, but | ||
| it ships a small surface of server-rendered HTML pages — the admin | ||
| dashboard, account profiles, individual posts, the OAuth consent screen, | ||
| and a handful of utility screens. This document specifies how those pages | ||
| should look, feel, and be implemented. | ||
|
|
||
| The design language values *simplicity* and *modernness* over decoration. | ||
| Surfaces are achromatic by default; color only enters the page through | ||
| the account owner's chosen *theme color*, which tints the profile and | ||
| post pages of that account. The visual center of every page is the | ||
| content the user came for, not the chrome around it. | ||
|
|
||
|
|
||
| Brand identity | ||
| -------------- | ||
|
|
||
| The name *Hollo* is always written with a capital “H” and lowercase | ||
| “ollo”. The mark is a circular badge with the wordmark “Hollo” inside, | ||
| distributed as two SVGs at the project root: | ||
|
|
||
| - *logo-black.svg* — for use on light backgrounds. | ||
| - *logo-white.svg* — for use on dark backgrounds. | ||
|
|
||
| Both files are served from */public/* and embedded inline only when an | ||
| icon-sized rendering is needed. Don't recolor the mark; switch between | ||
| black and white based on the surrounding surface. | ||
|
|
||
| The voice of the UI is short and matter-of-fact. Prefer plain English | ||
| sentences over imperative shouts; trust the reader. | ||
|
|
||
|
|
||
| Design principles | ||
| ----------------- | ||
|
|
||
| - *Simplicity*: prefer fewer controls, fewer borders, fewer surfaces. | ||
| Visual hierarchy comes from typography and spacing, not from boxes. | ||
| - *Modernness*: use modern CSS features (logical properties, OKLCH | ||
| color, container queries when needed) but never at the cost of | ||
| progressive degradation. | ||
| - *Content first*: text columns stay at a comfortable measure. Media | ||
| expands only when it earns the room. | ||
| - *Lightweight SSR*: pages are server-rendered with Hono JSX and ship | ||
| zero client-side JavaScript by default. Never reach for a runtime | ||
| framework to solve a styling problem that CSS can answer. | ||
| - *Accessibility*: every interactive element is keyboard-reachable, has | ||
| a visible focus state, and meets WCAG AA contrast in both light and | ||
| dark color schemes. | ||
|
|
||
|
|
||
| Color system | ||
| ------------ | ||
|
|
||
| ### Neutral palette | ||
|
|
||
| The default surface is achromatic. Hollo uses UnoCSS's *Wind4* neutral | ||
| scale (*neutral-50* through *neutral-950*) for backgrounds, borders, | ||
| surfaces, and text. No saturation enters a page until a theme color is | ||
| applied. | ||
|
|
||
| | Role | Light scheme | Dark scheme | | ||
| | --------------- | ------------- | ------------- | | ||
| | Page background | *neutral-50* | *neutral-950* | | ||
| | Surface | *white* | *neutral-900* | | ||
| | Subtle border | *neutral-200* | *neutral-800* | | ||
| | Body text | *neutral-900* | *neutral-100* | | ||
| | Muted text | *neutral-500* | *neutral-400* | | ||
|
|
||
| ### Account theme colors | ||
|
|
||
| Each account owner picks a theme color from a fixed set of twenty named | ||
| hues, defined as the *theme\_color* PostgreSQL enum in *src/schema.ts*: | ||
|
|
||
| ~~~~ | ||
| amber azure blue cyan fuchsia green grey indigo | ||
| jade lime orange pink pumpkin purple red sand | ||
| slate violet yellow zinc | ||
| ~~~~ | ||
|
|
||
| The palette comes from Pico CSS's named color palette. Each hue is | ||
| expressed at nine tonal stops (*50, 100, 200, 300, 400, 500, 600, 700, | ||
| 800, 900*), stored as RGB triples in *src/theme/colors.ts*. | ||
|
|
||
| ### CSS variable injection | ||
|
|
||
| The theme color is applied through CSS custom properties on the | ||
| `<html>` element. *Layout.tsx* reads the account's *themeColor* and | ||
| emits inline declarations: | ||
|
|
||
| ~~~~ html | ||
| <html style="--theme-50:247 248 250; --theme-100:...; ... --theme-900:..."> | ||
| ~~~~ | ||
|
|
||
| The UnoCSS configuration exposes these variables as a generic *brand* | ||
| color token: | ||
|
|
||
| ~~~~ ts | ||
| theme: { | ||
| colors: { | ||
| brand: { | ||
| 50: "rgb(var(--theme-50))", | ||
| // ... 100 through 900 | ||
| DEFAULT: "rgb(var(--theme-500))", | ||
| }, | ||
| }, | ||
| } | ||
| ~~~~ | ||
|
|
||
| This means components write `bg-brand`, `text-brand-700`, | ||
| `border-brand-200`, and so on, without ever knowing which of the twenty | ||
| hues is currently active. No safelist is needed because no class name | ||
| varies with the theme color. | ||
|
|
||
| ### Alpha modifiers | ||
|
|
||
| Wind4 wraps every brand-colored utility in | ||
| `color-mix(in srgb, ... var(--un-bg-opacity), transparent)`, so a | ||
| `--un-bg-opacity` (and the matching `--un-text-opacity`, `--un-border-opacity`, | ||
| `--un-ring-opacity`, `--un-divide-opacity`, `--un-placeholder-opacity`) custom | ||
| property must be defined before any brand utility resolves. *uno.config.ts* | ||
| sets all of these to `100%` on *:root* via a preflight, which makes plain | ||
| `bg-brand-500` behave like fully opaque rgb. | ||
|
|
||
| Slash modifiers work as expected on top of this default: `bg-brand-500/50`, | ||
| `text-brand-700/80`, `ring-brand-200/40`, and so on resolve to a 50%/80%/40% | ||
| mix against transparent. | ||
|
|
||
| ### Dark mode | ||
|
|
||
| Dark mode follows the operating system via `prefers-color-scheme: dark`. | ||
| There is no manual toggle in the first pass; that may be added later | ||
| without changing the underlying tokens. All component recipes specify | ||
| both light and dark variants up front. | ||
|
|
||
|
|
||
| Typography | ||
| ---------- | ||
|
|
||
| ### Type families | ||
|
|
||
| | Role | Family | Source | | ||
| | -------- | ---------------------------------------------- | ------------------------- | | ||
| | Sans | *Inter* | bunny.net (Google mirror) | | ||
| | Sans CJK | *Noto Sans KR*, *Noto Sans JP*, *Noto Sans SC* | bunny.net | | ||
| | Mono | *JetBrains Mono* | bunny.net | | ||
|
|
||
| Fonts are loaded through UnoCSS's *presetWebFonts* with the *bunny* | ||
| provider, which is a privacy-respecting mirror of Google Fonts. The CSS | ||
| font stack lists Inter first, then the three Noto Sans CJK families, and | ||
| falls back to the system stack so initial paint never blocks on a | ||
| network request. | ||
|
|
||
| ### Type scale | ||
|
|
||
| Use the Wind4 default scale unchanged (*text-xs* through *text-5xl*). | ||
| Body copy is *text-base* with *leading-relaxed*. Headings step down by | ||
| one level per nesting depth. | ||
|
|
||
| ### Long-form content | ||
|
|
||
| Rendered Markdown — post bodies, account bio fields, reply chains — is | ||
| wrapped in the *prose* class from *presetTypography*, with | ||
| *prose-neutral* and *dark:prose-invert* variants. Inline code uses the | ||
| mono family; block code is rendered through Shiki and keeps its own | ||
| colors. | ||
|
|
||
|
|
||
| Spacing and layout | ||
| ------------------ | ||
|
|
||
| The spacing scale is Wind4's default 4 px grid. Use multiples of *2* | ||
| (*0.5rem*), *3*, *4*, *6*, *8*, *12*, and *16* for almost all gaps. | ||
|
|
||
| Page widths: | ||
|
|
||
| - Reading column (post body, profile bio, settings forms): *max-w-2xl* | ||
| (~42 rem). | ||
| - Dashboard column (timelines, account list): *max-w-3xl* (~48 rem). | ||
| - Wide chrome (top nav, footer): full width with internal *max-w-5xl*. | ||
|
|
||
| Breakpoints follow the Wind4 defaults (*sm* 640 px, *md* 768 px, *lg* | ||
| 1024 px, *xl* 1280 px). Mobile is the design start point; widen by | ||
| adding `md:` and `lg:` variants. | ||
|
|
||
|
|
||
| Iconography | ||
| ----------- | ||
|
|
||
| Hollo uses a single icon collection: *Lucide*, surfaced through | ||
| UnoCSS's *presetIcons* with the *@iconify-json/lucide* package. Icons | ||
| are written as CSS classes: | ||
|
|
||
| ~~~~ tsx | ||
| <span class="i-lucide-bell text-lg" aria-hidden="true" /> | ||
| ~~~~ | ||
|
|
||
| Sizing follows the surrounding text size by default (*1em*). Icons | ||
| inherit *currentColor*, so they tint with the parent's text color | ||
| (including the theme color where applicable). Decorative icons get | ||
| `aria-hidden="true"`; meaningful icons have a paired text label or | ||
| *aria-label*. | ||
|
|
||
|
|
||
| Components | ||
| ---------- | ||
|
|
||
| ### Button | ||
|
|
||
| Three visual ranks exist: | ||
|
|
||
| - *primary*: solid theme-colored background, white text. At most | ||
| one per pane. | ||
| - *secondary*: neutral surface, neutral border, theme-colored text on | ||
| hover. | ||
| - *ghost*: no background, text-only; used in dense toolbars and inline | ||
| actions. | ||
|
|
||
| A *danger* variant in red is available for destructive submits. Sizes | ||
| are *sm*, *md* (default), and *lg*. All buttons share the same focus | ||
| ring and disabled state. | ||
|
|
||
| ### Form field | ||
|
|
||
| Each field is a labelled stack: *label → control → optional hint or | ||
| error*. Labels are above the control, never floating. Required fields | ||
| get a small “required” badge to the right of the label rather than an | ||
| asterisk. Errors are red and live below the control. | ||
|
|
||
| ### Top nav | ||
|
|
||
| The dashboard's top nav is a single row: logo on the left, primary | ||
| navigation in the center, and the account chip + sign-out button on the | ||
| right. On small screens the center links collapse into a sheet. | ||
|
|
||
| ### Card and article | ||
|
|
||
| Posts and notifications are rendered as *articles* — semantic HTML, no | ||
| visible card border. A subtle bottom divider separates each entry in a | ||
| list. Avatar, display name, handle, timestamp, and content stack | ||
| vertically on mobile and arrange into a media object on *md*. | ||
|
|
||
| ### Avatar | ||
|
|
||
| Always circular. Sizes: *sm* 1.5 rem, *md* 2.5 rem, *lg* 4 rem, | ||
| *xl* 6 rem. Profile headers use *xl*; comment threads use *sm* or *md*. | ||
|
|
||
| ### Footer | ||
|
|
||
| A single line of muted text at the bottom of every page: software name, | ||
| version, and a link to the source. No social icons. | ||
|
|
||
| ### Empty state | ||
|
|
||
| Centred icon (Lucide), one-line headline, optional subline, optional | ||
| primary call-to-action. No illustrations. | ||
|
|
||
|
|
||
| Motion | ||
| ------ | ||
|
|
||
| Motion is reserved for state feedback, not decoration. Allowed | ||
| transitions: | ||
|
|
||
| - *colors*: 150 ms, on hover and focus changes. | ||
| - *opacity*: 150 ms, on disclosure toggles. | ||
| - *transform: scale*: 100 ms, on press of a button. | ||
|
|
||
| Disable all transitions when *prefers-reduced-motion: reduce* is set. | ||
| Page transitions and route animations don't exist; SSR renders the new | ||
| page directly. | ||
|
|
||
|
|
||
| Accessibility | ||
| ------------- | ||
|
|
||
| - Maintain WCAG AA contrast in both light and dark schemes. Theme | ||
| colors pass contrast against neutral surfaces at the *600* and | ||
| *700* stops; lighter stops are background-only. | ||
| - Every focusable element has a visible *focus-visible* ring (a 2 px | ||
| ring in *brand-500* or *neutral-500*, offset by 2 px). | ||
| - Use semantic HTML: *button* for buttons, *a* for navigation, | ||
| *form*/*fieldset*/*label* for forms, *article* for individual posts, | ||
| *nav* for navigation regions. | ||
| - The *html* element's *lang* attribute reflects the page locale where | ||
| a content language is known. | ||
| - Decorative imagery uses *alt=“”* or *aria-hidden*. Meaningful | ||
| imagery has descriptive alternative text. | ||
|
|
||
|
|
||
| Implementation guide | ||
| -------------------- | ||
|
|
||
| ### Stylesheet pipeline | ||
|
|
||
| UnoCSS scans every *.tsx* and *.ts* file under *src/* via *@unocss/cli* | ||
| and writes a single static stylesheet to *src/public/uno.css*. In | ||
| development, *concurrently* runs the UnoCSS watcher alongside *tsx | ||
| watch*. In production, *pnpm build* runs *unocss* once before *tsdown*. | ||
| The stylesheet is served as a static asset, not bundled with the | ||
| application code, and the Layout component links it like any other | ||
| *.css* file. | ||
|
|
||
| ### Layout shell | ||
|
|
||
| *src/components/Layout.tsx* is the only place that emits *html*, *head*, | ||
| and *body*. Pages return a Layout-wrapped tree from their handler. | ||
| Layout is responsible for: | ||
|
|
||
| 1. Linking */public/uno.css*. | ||
| 2. Computing the inline CSS variable string from the requested | ||
| *themeColor* and putting it on *html*. | ||
| 3. Setting *lang*, page metadata (title, OG tags, canonical), and | ||
| favicons. | ||
|
|
||
| ### Theme tokens | ||
|
|
||
| *src/theme/colors.ts* exports a frozen object mapping each | ||
| *ThemeColor* enum value to its 50–900 RGB triples. When changing the | ||
| palette, edit only this file; nothing else needs to know about the | ||
| twenty hues by name. | ||
|
|
||
| ### Forms | ||
|
|
||
| Forms use the small helper components in *src/components/forms.tsx*: | ||
|
|
||
| - *Field* — wraps a control with a label, optional hint, and error | ||
| - *TextField*, *TextareaField*, *SelectField* — labelled controls | ||
| - *CheckboxField* — checkbox with adjacent label and hint | ||
| - *FieldSection* — card-shaped *fieldset* with a legend | ||
| - *SubmitButton* — primary/secondary/danger submit variants | ||
|
|
||
| These compose plain HTML controls with the agreed UnoCSS classes; they | ||
| never wrap a third-party input library. Reach for them first; only | ||
| hand-roll a new control when the form needs geometry the helpers can't | ||
| express (the OTP field, for example, intentionally diverges). | ||
|
|
||
| ### Prose content | ||
|
|
||
| Apply the *prose prose-neutral dark:prose-invert* class set to the | ||
| container that holds rendered Markdown. Don't apply *prose* to | ||
| arbitrary blocks of UI; it is intended for long-form text only. | ||
|
|
||
| ### Variant groups | ||
|
|
||
| The UnoCSS *variantGroup* transformer is enabled. Group related | ||
| variants: | ||
|
|
||
| ~~~~ tsx | ||
| <button class="rounded-md px-4 py-2 (bg-brand text-white hover:bg-brand-600 focus-visible:ring-2 focus-visible:ring-brand-500)"> | ||
| ~~~~ | ||
|
|
||
| Use this judiciously; long flat class lists are still fine when they | ||
| are easier to read. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.