Skip to content

Redesign the server-rendered front-end#458

Merged
dahlia merged 42 commits intofedify-dev:mainfrom
dahlia:redesign
Apr 29, 2026
Merged

Redesign the server-rendered front-end#458
dahlia merged 42 commits intofedify-dev:mainfrom
dahlia:redesign

Conversation

@dahlia
Copy link
Copy Markdown
Member

@dahlia dahlia commented Apr 29, 2026

This pull request replaces Pico CSS with a new in-house design system, documented in DESIGN.md and styled through UnoCSS, and rebuilds every server-rendered page on top of it. The result is a single coherent visual language for the public profile pages, single-post permalinks, hashtag streams, and the entire administration dashboard, with each account owner's chosen theme color tinting their own profile and post pages while the dashboard chrome stays achromatic.

Highlights

Design system

  • DESIGN.md documents the visual language: simplicity-first layout, an achromatic neutral palette, twenty named theme colors per account owner, typography, spacing, iconography, components, motion, and accessibility expectations. AGENTS.md points contributors at it.
  • Pico CSS and its twenty-two generated theme stylesheets are removed. UnoCSS (Wind4 + Icons + Typography + Web Fonts presets) emits a single src/public/uno.css.

Brand color system

  • Each account owner's chosen theme color is injected on <html> as --theme-50 through --theme-950 CSS variables, sourced from a static RGB table in src/theme/colors.ts.
  • Components use generic bg-brand, text-brand-700, etc., so they automatically pick up whichever of the twenty hues the page belongs to. No safelist is needed.
  • Profile bios, post bodies, follower/following counts, featured-tag chips, and the text selection color (::selection) all adopt the active brand color.

Pages

Every public and dashboard page is rebuilt: login, setup, OTP, the public home, account profiles, single-post permalinks (with a featured larger body and an “N replies” header), the hashtag stream, the account list and editor, custom emojis (with a drag-and-drop-style image picker), federation, thumbnail cleanup, the OAuth consent screen, and the dashboard 2FA panel.

Post component

Posts render as a media object with a circular avatar, prose markdown body, attached media as a two-column grid, polls as brand-tinted progress bars, quoted posts as an inset card, and Open Graph link previews as a thumbnail/host/title/description card.

Forms

A small set of primitives in src/components/forms.tsx (Field, TextField, TextareaField, SelectField, CheckboxField, FieldSection, SubmitButton) backs the auth, account, emoji, federation, thumbnail cleanup, and migrate forms. The account form's theme color picker is a twenty-swatch grid; the username field is bracketed by @ and @host chips so the resulting fediverse handle is obvious.

Typography and icons

Web Fonts come from bunny.net (a privacy-respecting Google Fonts mirror): Inter for Latin, Noto Sans KR/JP/SC for CJK, and JetBrains Mono for code. Lucide icons replace ad-hoc iconography via UnoCSS's presetIcons.

Dark mode

prefers-color-scheme: dark is honored automatically. Primary buttons step from brand-600 to brand-700 so they don't dominate dark surfaces; divide-y borders are pinned to neutral-200/neutral-800 via a preflight (because UnoCSS sorts dark:divide-* before divide-* alphabetically and would otherwise let the light variant win); text selection adopts the active brand color in both schemes.

Build pipeline

pnpm dev now runs unocss --watch alongside tsx watch via concurrently; pnpm build runs unocss once before tsdown. src/public/uno.css is git-ignored. The Hollo logos move from a jsDelivr URL to src/public/logo-black.svg and src/public/logo-white.svg. src/components/Layout.tsx appends a ?v=<mtime> cache buster so CDN caches always pick up the latest stylesheet.

Screenshots

Public home

Public home page at dorikom.hollo.social showing the black Hollo wordmark logo centered above the host name as a large heading, the line “This Hollo instance hosts the following account,” and a single rounded white card for the Hollo Dev account with a circular avatar, the @hollodev@dorikom.hollo.social handle, and a one-line bio.  Below the card sits a muted “Administration dashboard” link with a settings gear icon.

Dashboard — account list

Hollo dashboard Accounts page.  A thin top bar carries the Hollo wordmark with a “DASHBOARD” eyebrow on the left, the navigation links Accounts, Custom emojis, Federation, Thumbnail cleanup, and Auth in the middle, and a Logout button on the right.  Below it the page heading “Accounts” sits over an explanatory paragraph and a brand-blue “+ New account” button.  A single account card for Hollo Dev shows the circular avatar, display name, handle, bio, “Created 2025-11-12,” and a footer of brand-blue Edit, neutral Migrate, and red-outlined Delete buttons.

Dashboard — account editor

The “Edit @hollodev” page inside a single rounded card divided into four sections by small all-caps section labels: IDENTITY (with a username field bracketed by an “@” chip on the left and an “@dorikom.hollo.social” chip on the right, plus display name and bio inputs), PRIVACY (three checkboxes: Protect this account is checked, Discoverable and Expand content warnings by default are unchecked), PREFERENCES (Default language set to English (English), Default visibility set to Followers only, and a Theme color grid of twenty colored swatches with the purple swatch selected via a dark ring and a check mark), and UPDATES (an unchecked “Receive Hollo news” checkbox).  A brand-purple “Save changes” button sits at the bottom-right of the card.

Public profile

Hollo Dev's public profile page.  A wide cover photo shows a Taiwan shopfront with red couplets and a tiled facade.  A circular avatar overlaps the bottom-left of the cover and bleeds into a rounded white card below.  Inside the card the display name “Hollo Dev” appears in semibold, the @hollodev@dorikom.hollo.social handle below it in muted text, then a stats line “4 following · 5 followers” where the numbers are highlighted in jade brand color, and finally the bio “This account is for developing and testing Hollo. Please ignore me.”  Below the profile card a PINNED post (with a small jade pin icon) leads into the timeline, with one post showing a Japanese-language entry titled 
“ハングル専用下の漢字教育について” rendered as an Open Graph link preview from writings.hongminhee.org.

dahlia added 30 commits April 29, 2026 11:53
DESIGN.md introduces the design language for Hollo's
server-rendered web pages: simplicity and modernness on an
achromatic neutral base, tinted by each account's chosen theme
color through CSS custom properties on the html element.  It
covers brand identity, design principles, the color system
(neutral palette, twenty theme hues, and CSS variable injection),
typography (Inter + Noto Sans CJK + JetBrains Mono via
presetWebFonts), spacing, iconography (Lucide via presetIcons),
component recipes, motion, and accessibility expectations.

AGENTS.md is updated to point future contributors at DESIGN.md
as the source of truth for front-end decisions.

Assisted-by: Claude Code:claude-opus-4-7
Adds UnoCSS with the Wind4, Icons (Lucide), Typography, and Web
Fonts (bunny.net) presets, configured via uno.config.ts.  The
brand color tokens are exposed as CSS variables (--theme-50
through --theme-950) so components can write bg-brand,
text-brand-700, etc. without ever knowing which of the twenty
account theme hues is currently active.

The 50-950 RGB triples for each of the twenty theme_color enum
values are stored in src/theme/colors.ts, along with a helper
that emits the CSS variable string for a given theme color.

The pnpm dev script now runs unocss in watch mode alongside tsx
watch via concurrently; pnpm build runs unocss once before
tsdown.  The generated src/public/uno.css is git-ignored.

logo-black.svg and logo-white.svg are copied into src/public/
for self-hosting; the project root copies remain so the README
files can keep referencing them.

Assisted-by: Claude Code:claude-opus-4-7
Layout.tsx now links a single /public/uno.css stylesheet and
injects the per-account theme color into the html element as
inline CSS custom properties (--theme-50 through --theme-950),
which the brand color tokens in uno.config.ts dereference.  The
body element gets a neutral surface (bg-neutral-50 in light,
bg-neutral-950 in dark) and the new sans-serif font stack.

DashboardLayout.tsx is rebuilt around the new UnoCSS classes:
the header is a thin bordered bar with the Hollo wordmark, an
inline navigation row that highlights the active menu, and a
self-hosted logo (/public/logo-*.svg) instead of the previous
jsdelivr CDN.  The footer is centered muted text.

The twenty-two pico.*.min.css files and the legacy hollo.css
are deleted.  The only remaining bit of hollo.css -- the
Shiki dark-mode override -- is moved into uno.config.ts as a
preflight so it stays in the generated stylesheet.

Generated screenshots and the .playwright-mcp directory are
git-ignored.

Assisted-by: Claude Code:claude-opus-4-7
Three fixes that surfaced after the layout migration when the
new utilities started being used in earnest:

1. The variant group transformer (transformerVariantGroup) was
   not expanding "dark:(bg-x text-y)" syntax in CLI mode, so
   classes inside the parens never reached the generated CSS.
   The transformer is removed from uno.config.ts and every call
   site is rewritten with the long form (dark:bg-x dark:text-y).

2. presetWind4 emits bg-* rules as
   "color-mix(in srgb, rgb(...) var(--un-bg-opacity), transparent)".
   Without --un-bg-opacity defined the whole expression is invalid
   and falls back to transparent, which made every theme-colored
   surface disappear.  A preflight now sets the default opacity
   custom properties (--un-bg-opacity, --un-text-opacity, etc.)
   to 100% on :root.

3. Cloudflare in front of dorikom.hollo.social caches uno.css for
   four hours, so newly-built stylesheets were not visible to the
   browser.  Layout.tsx now appends a ?v= query string sourced
   from the file mtime of src/public/uno.css, evaluated on each
   render so dev rebuilds always bust the CDN cache.

Assisted-by: Claude Code:claude-opus-4-7
Introduces an AuthCard component that wraps all unauthenticated
auth screens: a centered, max-w-md card with the Hollo logo
above, a bordered surface in light, and a flat dark-mode panel.

LoginForm, SetupForm, and OtpForm are rebuilt with stacked
labelled inputs, focus rings tinted in the active brand color,
inline error and hint messages, and a primary submit button
that uses the theme color (defaults to azure for the
unauthenticated dashboard chrome).  The OTP code field uses the
JetBrains Mono stack with wide letter-spacing for readability.

login.tsx and setup/index.tsx are updated to render the new
LoginPage, OtpPage, and SetupPage inside AuthCard.  The setup
page's reverse-proxy warning is restyled as a neutral red
notice.

Assisted-by: Claude Code:claude-opus-4-7
The instance landing page (/) now leads with the Hollo logo and
the host name, and lists each account owner as a card with a
circular avatar, display name, handle, and prose-styled bio.
The administration dashboard link drops below the cards as a
muted secondary action with a Lucide settings icon.

The active theme color picks up the first account owner's
selection so the public face of a one-account instance picks up
that owner's brand tint.

Assisted-by: Claude Code:claude-opus-4-7
The profile header is now a card with a tall hero (the cover
image, or a brand-tinted gradient when no cover is set), an
overlapping circular avatar bordered in the page surface color,
and a stacked block of display name, handle, follower stats,
prose-styled bio, and a two-column field grid.

The surrounding ProfilePage is wired into a max-w-2xl reading
column, the featured tags become muted chips that pick up the
brand color on hover, the posts list uses divide-y separators,
and the prev/next pagination is a flex row with Lucide arrow
icons.  The "#tag" page heading uses the brand color so the
tag-filtered view picks up the account's tint.

Assisted-by: Claude Code:claude-opus-4-7
The post block is now a media object: a square avatar on the
left, then display name, handle, optional reply target, body,
attachments, optional poll, and a footer with timestamp and
counts.  The footer uses Lucide icons for shared/likes/shares
and the post body is wrapped in prose for proper Markdown
rendering.

Quoted posts inset themselves into a softer secondary surface
so they read as a quoted block rather than a separate post.
Pinned posts get a small brand-colored "Pinned" tag at the top.
Polls switch from a table to a stack of bars whose width grows
with the vote percentage.  Media attachments lay out in a
two-column grid on wider viewports.

The article wrapper drops Pico's inline borders; the divide-y
that the profile page now renders between consecutive posts
provides separation instead.

Assisted-by: Claude Code:claude-opus-4-7
The account list page has a heading row with a "New account"
call-to-action and renders each account owner as a card with a
circular avatar, display name, handle, prose-styled bio, a
created/fetched timestamp, and a footer of edit, migrate, and
delete actions.

AccountForm splits its fields into four card-shaped sections
(Identity, Privacy, Defaults, Updates) using legend headings,
the same labelled-input pattern as the auth forms, a small
checkbox helper, and a primary submit button.  The theme color
selector drops the pico-color-* classes and now renders as a
plain styled select with capitalized hue names.

The edit account page gets a breadcrumb-style "Accounts" link
and a brand-tinted handle in the title.  The migrate page is
left for a follow-up.

Assisted-by: Claude Code:claude-opus-4-7
The emoji list, new emoji, and import pages all switch to the
dashboard's standard layout: a heading with breadcrumb-style
back link where appropriate, primary and secondary action
buttons in the header, and content rendered inside bordered
cards.

The emoji table is now a clean grid with a sticky header row,
checkboxes that submit a brand-tinted "Delete selected" button,
and an empty state when no emojis exist yet.  The new and
import forms use the labelled-input pattern, with file inputs
styled to match the brand button.

Assisted-by: Claude Code:claude-opus-4-7
The federation control panel is split into three card sections
(Force refresh, Task queue, Shut down).  The refresh form uses
the inline label pattern from the auth forms and shows
contextual hint or error text below the field.  The task queue
becomes a clean two-column table with monospace types and
right-aligned, tabular-nums message counts; an explicit empty
state replaces the empty pico table when the queue is empty.

Assisted-by: Claude Code:claude-opus-4-7
The page now lays out four card sections (statistics, preview,
progress, cleanup) using the dashboard's standard surface
treatment.  The progress block replaces the native <progress>
element with a brand-tinted bar, and successful and failed
counts get green and red emphasis.  Date inputs and buttons
adopt the form patterns used elsewhere in the dashboard.

Assisted-by: Claude Code:claude-opus-4-7
The hashtag page now uses the same max-w-2xl reading column as
the profile page, with a Hashtag eyebrow above the title, a
brand-tinted hash sign in the title, and a count of matching
posts in the subtitle.  Posts are separated by the same
divide-y treatment used on profile pages.

Assisted-by: Claude Code:claude-opus-4-7
The OAuth consent screen is now an AuthCard with a clear list
of requested scopes shown as monospace chips, account picker
radio cards that highlight on selection, and side-by-side Deny
and Allow buttons.  The authorization-code page reuses AuthCard
to display the code as a copyable monospace block.

Assisted-by: Claude Code:claude-opus-4-7
The 2FA settings card now shows a status pill (Enabled or
Disabled) in the header, the setup flow lays the QR code and
explanatory text side by side on wider viewports, the verify
input uses the JetBrains Mono large-tracked field, and the
disable action becomes a clearly red destructive button.

Assisted-by: Claude Code:claude-opus-4-7
The migrate page now follows the same card-section layout as
the rest of the dashboard.  Aliases are listed as monospace
chips, the existing account handle is shown as a brand-tinted
inline code, the "irreversible" warning is highlighted in red,
the export table gets right-aligned tabular numbers and a
Lucide download icon for each CSV link, and the import progress
block uses the same brand-tinted progress bar as the thumbnail
cleanup page.  The import form upgrades to a labelled grid with
a brand-styled file input.

Assisted-by: Claude Code:claude-opus-4-7
src/components/forms.tsx introduces a small set of components
that the auth forms, account form, and emoji forms can share:

  - Field         label + hint/error wrapper around an arbitrary control
  - TextField     labelled <input> (text/email/password/number/url)
  - TextareaField labelled <textarea> with an optional value
  - SelectField   labelled <select>
  - CheckboxField checkbox laid out beside a label and hint
  - FieldSection  card-shaped <fieldset> with a legend and description
  - SubmitButton  primary/secondary/danger button variants

The helpers fix the styling vocabulary in one place so individual
forms can stop carrying long inline class lists.

Assisted-by: Claude Code:claude-opus-4-7
LoginForm, SetupForm, and OtpForm switch to TextField,
SubmitButton, and the new shared field styles, dropping their
local copies of the input/label/error class strings.

Visual output is unchanged; the change is in the source size
and how easy each form is to read.  OtpForm keeps its custom
mono large-tracked input since it intentionally diverges from
the standard TextField geometry.

Assisted-by: Claude Code:claude-opus-4-7
AccountForm switches to FieldSection, TextField, TextareaField,
SelectField, CheckboxField, and SubmitButton.  All four sections
(Identity, Privacy, Defaults, Updates) collapse from local
markup into helper calls, and the local CheckboxField
implementation is removed in favor of the shared one.

The theme color picker is factored into a small ThemeColorField
component so a follow-up commit can swap it for a swatch grid
without touching the rest of the form.

Assisted-by: Claude Code:claude-opus-4-7
The theme color field now renders the twenty hues as a grid of
colored buttons instead of a flat <select>.  Each swatch is a
radio input with the brand's 500-tone background, the active
choice gets a neutral ring and a check icon, and a hint line
shows the picked color's name in plain English.

The grid is wider than the other Default fields, so the section
also restructures the row: language and visibility share a
two-column row, and the swatch grid spans the full width below
them.

Assisted-by: Claude Code:claude-opus-4-7
Earlier the transformer was dropped because the CLI seemed not
to expand "dark:(bg-x text-y)" shorthand.  A direct test on a
disposable file confirmed the CLI does call configured
transformers when it extracts utilities, so the transformer is
re-added to uno.config.ts and forms.tsx switches the focus,
disabled, and read-only blocks of its base input class to the
shorthand form.

Nested groups are not expanded, so dark:read-only:* keeps two
flat utility names rather than nesting under the dark:() group.

DESIGN.md's Forms section now documents the actual helpers
shipped in src/components/forms.tsx instead of the planned set.

Assisted-by: Claude Code:claude-opus-4-7
A short experiment confirmed that bg-brand-500/50, text-brand-700/80,
ring-brand-200/40, and friends all resolve correctly under the
current setup: Wind4 emits a color-mix expression that uses the
slash percentage as the mix amount, and falls back to the
:root --un-*-opacity preflight default of 100% when no slash is
given.

DESIGN.md now spells out the mechanism (the color-mix wrapper,
the opacity custom properties, and the slash modifier) under
the Color system section.  The brand color sample in that
section also drops the stale <alpha-value> placeholder so it
matches the live config.

Assisted-by: Claude Code:claude-opus-4-7
Posts that have a stored preview_card now show it as a media-
object card below the body and any poll: a square thumbnail on
the left (when og:image is set), the host name as a small all-
caps eyebrow, the page title in semibold, and a two-line
description.  The whole card is a single anchor that opens the
target in a new tab with rel="noopener nofollow".

The card is suppressed when the post already has attached media,
matching Mastodon's own UI.  Quoted posts keep showing their
content but never their preview card, so nested quote blocks
don't pile up two link cards on top of each other.

The PreviewCard data and fetcher already lived in
src/previewcard.ts; this commit only adds the visual layer.

Assisted-by: Claude Code:claude-opus-4-7
The /@handle/:postId page used to drop a bare PostView and its
replies straight into the body, which in the new layout left
the post pinned to the top-left of the viewport.  PostPage now
wraps its content in the same max-w-2xl reading column the
profile page uses, with horizontal padding and breathing room
above and below.

PostView gains a "featured" prop that the permalink page sets
to true.  In featured mode the avatar steps up from size-11 to
size-12, the display name renders at text-lg, and the body
prose grows from prose-sm to prose-base — small bumps that make
a single, focused post read like the centerpiece it is on its
own page without breaking the divide-y rhythm of timelines or
the inset look of quote blocks.

The replies section gets a small "N replies" eyebrow above its
divide-y stack so it reads as a separate region from the main
post.

Assisted-by: Claude Code:claude-opus-4-7
The swatch ring and check-mark used to be rendered conditionally
on the server-side isSelected value, so clicking a different
swatch in the browser changed the radio's :checked state without
visibly updating the UI.  The fix moves both pieces to CSS:

  - the label uses has-[:checked]:ring-neutral-900 to draw the
    selected ring,
  - the check-mark span is always present and toggled with
    peer-checked:opacity-100 against a default opacity-0.

The hint that previously echoed "Currently picked: X" is dropped
because it would otherwise lag a click.  The Field's id now
points at the swatch matching the active color so a hint click
focuses something useful.

Assisted-by: Claude Code:claude-opus-4-7
UnoCSS Wind4's reset zeroes border widths on every element but
doesn't override the user-agent input/textarea/select borders,
which leaves Chromium picking its own 2px inset border for form
controls in some configurations and ignoring the .border-color
utility.

uno.config.ts now sets border-style: solid and border-width: 1px
on input, textarea, and select via a preflight, and forms.tsx's
fieldBase drops the redundant border/border-solid utilities since
the preflight covers them.

Assisted-by: Claude Code:claude-opus-4-7
Browsers default to the arrow cursor over <button> elements,
which makes them feel like static labels rather than something
you can click.  A small preflight rule sets cursor: pointer on
every enabled <button> and any element with role="button", and
keeps cursor: not-allowed on disabled ones.

Assisted-by: Claude Code:claude-opus-4-7
The third section of the account form holds the default
language, default visibility, and theme color.  "Defaults" only
fits the first two; the theme color is a per-account preference
rather than a default applied to anything.  Renaming the legend
to "Preferences" lets the three fields sit together without
feeling out of place.

Assisted-by: Claude Code:claude-opus-4-7
The previous file picker was a thin bordered shell with the
default Chrome "Choose file / No file chosen" combo on the
left, which read as half-styled next to the rest of the form.
The image field now renders a centered drag-and-drop-style
labelled dropzone: dashed neutral border, lucide image-up
glyph, a "Click to choose an image" call-to-action, and a
PNG/JPEG/GIF/WebP hint underneath.  The native <input type=file>
sits inside the label as sr-only.

The forms preflight stops applying the 1px solid border to
input[type=file], input[type=checkbox], and input[type=radio]
since those types intentionally diverge from text-style
controls.

Assisted-by: Claude Code:claude-opus-4-7
The account form used to render each FieldSection as its own
small card.  That made the page read as four separate forms,
visually different from the single-card forms used elsewhere
(login, setup, emoji new, federation refresh, migrate, etc.).

FieldSection is now a borderless fieldset with a small all-caps
legend and a thin top divider before sibling sections, and the
account form wraps all of its sections in a single rounded
neutral card.  The submit button sits below a final divider
inside the same card so the form reads as one cohesive sheet.

Assisted-by: Claude Code:claude-opus-4-7
dahlia added 9 commits April 29, 2026 11:53
The forms helper inputs rendered at the browser default 16px,
while every hand-written form on the dashboard (emoji new,
federation refresh, thumbnail cleanup, migrate, etc.) used
text-sm.  Putting the two side by side made the account form
feel oversized and out of family.

fieldBase now sets text-sm so TextField, TextareaField, and
SelectField match the rest of the dashboard at 14px.  The OTP
input keeps its custom large mono geometry.

Assisted-by: Claude Code:claude-opus-4-7
The fieldBase string for the form helpers used variant group
shorthand like "focus:(border-brand-500 outline-none ring-2
ring-brand-100)".  transformerVariantGroup expanded that for
class extraction at build time, but the original shorthand
shipped to the browser inside className verbatim.  The browser
splits className on whitespace, so "ring-2" landed on every
input as a standalone class and pinned a 2 px black ring around
the field forever — which made AccountForm inputs look like
they had a heavier, darker border than the rest of the dashboard.

The fix is straightforward: write each variant out long-form
("focus:border-brand-500 focus:outline-none focus:ring-2 ...")
so the className the browser sees matches the CSS rules that
were actually generated.  transformerVariantGroup is removed
from uno.config.ts since the shorthand is no longer worth the
foot-gun.

Assisted-by: Claude Code:claude-opus-4-7
The padding-top and divider used to live on the <fieldset>
itself.  fieldset/legend has native rendering quirks that
swallow padding-top, so the legend ended up flush against the
divider while children sat 36 px below it.  Side by side that
read as the section heading clinging to the previous section.

The wrapper is now a plain <div> that carries the divider and
the 24 px padding-top, with the bare <fieldset> nested inside.
The padding now actually lands above the legend, and the gap
between the legend and its first child grows from mt-3 to mt-4
(12 → 16 px) so the heading no longer sits closer to the
divider than to the controls it introduces.

Assisted-by: Claude Code:claude-opus-4-7
The username input on the new and edit account forms now sits
between two muted neutral chips: a leading "@" and a trailing
"@hostname".  This makes it obvious that the value being typed
is the local part of a fediverse handle that resolves to
@user@host on the network.  The chips share the same wrapper as
the input so the focus ring lights up the whole field as a unit
via focus-within.

AccountForm and NewAccountPage gain a host prop that the route
handlers populate from new URL(c.req.url).host.

The forms preflight selector is also relaxed: it now uses
:where() so the input/textarea/select rule contributes no extra
specificity, which lets utility classes like border-0 (used by
the inner username input inside its wrapper) actually win.

Assisted-by: Claude Code:claude-opus-4-7
The account theme color now actually shows up in three high-
visibility places on the profile page:

  - The following and followers counts render the numbers
    themselves in brand-700 (brand-400 in dark mode), with the
    "following" / "followers" labels staying muted.

  - Markdown links inside profile bios use prose-a:text-brand-700
    so every external link in the bio picks up the brand tint
    instead of the default neutral.

  - Featured tags are now solid brand-50 chips with a brand-200
    border and brand-700 text, instead of muted neutral chips
    that only turned brand on hover.

Post bodies (Post.tsx) get the same prose-a override, so links
inside post text on profiles, hashtag pages, and permalinks pick
up the page's theme color too.

Assisted-by: Claude Code:claude-opus-4-7
Selecting text now lights up in the page's theme color instead
of the browser's default blue.  A small ::selection block in
the preflight uses brand-200 over brand-900 in light mode and
brand-800 over brand-100 in dark mode, all sourced from the
same --theme-* CSS variables that Layout already injects on
<html>.  Profile and post pages adopt the account owner's
chosen color; the dashboard chrome falls back to azure as
elsewhere.

Assisted-by: Claude Code:claude-opus-4-7
UnoCSS sorts utility rules alphabetically, which lands
.dark:divide-* before .divide-* in the generated stylesheet
because "dark" precedes "divide".  Same specificity, source
order decides — so .divide-neutral-200 wins in both schemes
and the inter-post divider on profile, hashtag, and reply
pages stayed almost white in dark mode.

A small preflight pins the divider color directly:
neutral-200 by default, neutral-800 under
prefers-color-scheme: dark, with specificity 0,2,0 so the
existing utility classes can no longer override it.

Other dark-mode prone classes (border-, bg-, text-, etc.) all
sort before "dark" alphabetically and don't hit this issue, so
this targeted fix is enough.

Assisted-by: Claude Code:claude-opus-4-7
Brand-600 sat on top of neutral-950 surfaces a step too bright,
so primary buttons read as more attention-grabbing than the
content around them.  Each primary button now drops one notch
in dark mode: bg-brand-600 stays in light, while dark mode uses
brand-700 with a brand-800 hover.  The same shift is applied to
the file-selector button on migrate's CSV import.

Covered:
  - SubmitButton primary variant in src/components/forms.tsx
  - Dashboard call-to-action buttons (New account, Edit, Add
    emoji, Refresh, Preview, Clean, Import, Save changes)
  - Auth setup, OAuth Allow, account list Edit
  - Migrate import file picker

Progress bars (the brand-600 fill on cleanup/migrate progress)
are left as-is so they still read as a visible bar in dark mode.

Assisted-by: Claude Code:claude-opus-4-7
Adds a Version 0.9.0 entry that summarizes the move from Pico
CSS to the UnoCSS-based design system: the new tokens, fonts,
and icons; the page-by-page rebuild; the form primitives, theme
color picker, and username chip layout; the dark mode and
selection color treatment; and the static asset cleanup.

Assisted-by: Claude Code:claude-opus-4-7
@dahlia dahlia self-assigned this Apr 29, 2026
@dahlia dahlia added the enhancement New feature or request label Apr 29, 2026
@dahlia dahlia added this to the Hollo 0.9 milestone Apr 29, 2026
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented Apr 29, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a5fc5bf01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .gitignore Outdated
dahlia added 3 commits April 29, 2026 12:08
CSS class names, HTML elements, attributes, properties, custom
property names, function names, types, and props are now wrapped
in backticks instead of italics, since italics in this project
are reserved for package names, file paths, font families, and
brand/concept names per AGENTS.md's markdown style guide.

Italics are kept on package names like @iconify-json/lucide and
@unocss/cli, font families like Inter or Noto Sans KR, file
paths like src/components/Layout.tsx, and brand/concept names
like Wind4 or Lucide.

The "Variant groups" section is also rewritten to document the
project's actual policy: variant group shorthand is not used
because it leaks into HTML class attributes and triggers false
positive class matches.

Assisted-by: Claude Code:claude-opus-4-7
Five tests started failing once the redesign rewrote the
markup of pages they assert against:

  - src/oauth.test.ts looks up the listed scopes via
    "#scopes > li > code".  The new authorization page used
    the same <ul>/<li>/<code> structure but had dropped the
    id="scopes" attribute on the <ul>.  Re-added.

  - src/pages/federation.test.tsx expects the canonical handle
    conflict copy to read "Account refresh was blocked by a
    canonical handle conflict."  The redesign had shortened it
    to "Refresh blocked by a canonical handle conflict.";
    restore the longer wording — it's also clearer to the
    reader.

  - src/pages/thumbnail_cleanup.test.ts asserted
    "Number of Items: 4,980" on the preview output.  The
    preview now renders the count inside a <dl><dt>Items</dt>
    <dd>4,980</dd> grid, so the test is updated to assert the
    new "Items" label and "4,980" value separately, and its
    expect.assertions count moves from 5 to 6.

  - src/pages/profile/index.test.tsx asserted
    "Posts tagged #TestTag" as a single string.  The redesign
    splits the heading into "Posts tagged " followed by a
    brand-tinted <span>#TestTag</span>, so the assertion is
    split in two and the assertions count moves from 5 to 6.

Assisted-by: Claude Code:claude-opus-4-7
The previous "*.png" entry in .gitignore matched every PNG file
anywhere in the tree, including the existing screenshots under
docs/ and the default avatar at assets/default-screenshot.png,
and would have silently dropped any future PNG asset added by a
contributor.  Replace it with "/*.png", which only ignores the
ad-hoc Playwright screenshots that land in the repository root
during local front-end development.

Addresses fedify-dev#458 (comment)

Assisted-by: Claude Code:claude-opus-4-7
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented Apr 29, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dahlia dahlia merged commit b3106e6 into fedify-dev:main Apr 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant