Skip to content

Commit 69a1c49

Browse files
committed
Switch DESIGN.md code tokens from italics to backticks
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
1 parent 5a5fc5b commit 69a1c49

1 file changed

Lines changed: 86 additions & 88 deletions

File tree

DESIGN.md

Lines changed: 86 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,22 @@ Color system
5757
### Neutral palette
5858

5959
The default surface is achromatic. Hollo uses UnoCSS's *Wind4* neutral
60-
scale (*neutral-50* through *neutral-950*) for backgrounds, borders,
60+
scale (`neutral-50` through `neutral-950`) for backgrounds, borders,
6161
surfaces, and text. No saturation enters a page until a theme color is
6262
applied.
6363

6464
| Role | Light scheme | Dark scheme |
6565
| --------------- | ------------- | ------------- |
66-
| Page background | *neutral-50* | *neutral-950* |
67-
| Surface | *white* | *neutral-900* |
68-
| Subtle border | *neutral-200* | *neutral-800* |
69-
| Body text | *neutral-900* | *neutral-100* |
70-
| Muted text | *neutral-500* | *neutral-400* |
66+
| Page background | `neutral-50` | `neutral-950` |
67+
| Surface | `white` | `neutral-900` |
68+
| Subtle border | `neutral-200` | `neutral-800` |
69+
| Body text | `neutral-900` | `neutral-100` |
70+
| Muted text | `neutral-500` | `neutral-400` |
7171

7272
### Account theme colors
7373

7474
Each account owner picks a theme color from a fixed set of twenty named
75-
hues, defined as the *theme\_color* PostgreSQL enum in *src/schema.ts*:
75+
hues, defined as the `theme_color` PostgreSQL enum in *src/schema.ts*:
7676

7777
~~~~
7878
amber azure blue cyan fuchsia green grey indigo
@@ -81,20 +81,21 @@ slate violet yellow zinc
8181
~~~~
8282

8383
The palette comes from Pico CSS's named color palette. Each hue is
84-
expressed at nine tonal stops (*50, 100, 200, 300, 400, 500, 600, 700,
85-
800, 900*), stored as RGB triples in *src/theme/colors.ts*.
84+
expressed at nine tonal stops (`50`, `100`, `200`, `300`, `400`, `500`,
85+
`600`, `700`, `800`, `900`), stored as RGB triples in
86+
*src/theme/colors.ts*.
8687

8788
### CSS variable injection
8889

8990
The theme color is applied through CSS custom properties on the
90-
`<html>` element. *Layout.tsx* reads the account's *themeColor* and
91+
`<html>` element. *Layout.tsx* reads the account's `themeColor` and
9192
emits inline declarations:
9293

9394
~~~~ html
9495
<html style="--theme-50:247 248 250; --theme-100:...; ... --theme-900:...">
9596
~~~~
9697

97-
The UnoCSS configuration exposes these variables as a generic *brand*
98+
The UnoCSS configuration exposes these variables as a generic `brand`
9899
color token:
99100

100101
~~~~ ts
@@ -118,15 +119,16 @@ varies with the theme color.
118119

119120
Wind4 wraps every brand-colored utility in
120121
`color-mix(in srgb, ... var(--un-bg-opacity), transparent)`, so a
121-
`--un-bg-opacity` (and the matching `--un-text-opacity`, `--un-border-opacity`,
122-
`--un-ring-opacity`, `--un-divide-opacity`, `--un-placeholder-opacity`) custom
123-
property must be defined before any brand utility resolves. *uno.config.ts*
124-
sets all of these to `100%` on *:root* via a preflight, which makes plain
125-
`bg-brand-500` behave like fully opaque rgb.
122+
`--un-bg-opacity` (and the matching `--un-text-opacity`,
123+
`--un-border-opacity`, `--un-ring-opacity`, `--un-divide-opacity`,
124+
`--un-placeholder-opacity`) custom property must be defined before any
125+
brand utility resolves. *uno.config.ts* sets all of these to `100%` on
126+
`:root` via a preflight, which makes plain `bg-brand-500` behave like
127+
fully opaque rgb.
126128

127-
Slash modifiers work as expected on top of this default: `bg-brand-500/50`,
128-
`text-brand-700/80`, `ring-brand-200/40`, and so on resolve to a 50%/80%/40%
129-
mix against transparent.
129+
Slash modifiers work as expected on top of this default:
130+
`bg-brand-500/50`, `text-brand-700/80`, `ring-brand-200/40`, and so on
131+
resolve to a 50%/80%/40% mix against transparent.
130132

131133
### Dark mode
132134

@@ -147,61 +149,61 @@ Typography
147149
| Sans CJK | *Noto Sans KR*, *Noto Sans JP*, *Noto Sans SC* | bunny.net |
148150
| Mono | *JetBrains Mono* | bunny.net |
149151

150-
Fonts are loaded through UnoCSS's *presetWebFonts* with the *bunny*
152+
Fonts are loaded through UnoCSS's `presetWebFonts` with the `bunny`
151153
provider, which is a privacy-respecting mirror of Google Fonts. The CSS
152154
font stack lists Inter first, then the three Noto Sans CJK families, and
153155
falls back to the system stack so initial paint never blocks on a
154156
network request.
155157

156158
### Type scale
157159

158-
Use the Wind4 default scale unchanged (*text-xs* through *text-5xl*).
159-
Body copy is *text-base* with *leading-relaxed*. Headings step down by
160+
Use the Wind4 default scale unchanged (`text-xs` through `text-5xl`).
161+
Body copy is `text-base` with `leading-relaxed`. Headings step down by
160162
one level per nesting depth.
161163

162164
### Long-form content
163165

164166
Rendered Markdown — post bodies, account bio fields, reply chains — is
165-
wrapped in the *prose* class from *presetTypography*, with
166-
*prose-neutral* and *dark:prose-invert* variants. Inline code uses the
167+
wrapped in the `prose` class from `presetTypography`, with
168+
`prose-neutral` and `dark:prose-invert` variants. Inline code uses the
167169
mono family; block code is rendered through Shiki and keeps its own
168170
colors.
169171

170172

171173
Spacing and layout
172174
------------------
173175

174-
The spacing scale is Wind4's default 4 px grid. Use multiples of *2*
175-
(*0.5rem*), *3*, *4*, *6*, *8*, *12*, and *16* for almost all gaps.
176+
The spacing scale is Wind4's default 4 px grid. Use multiples of `2`
177+
(`0.5rem`), `3`, `4`, `6`, `8`, `12`, and `16` for almost all gaps.
176178

177179
Page widths:
178180

179-
- Reading column (post body, profile bio, settings forms): *max-w-2xl*
180-
(~42 rem).
181-
- Dashboard column (timelines, account list): *max-w-3xl* (~48 rem).
182-
- Wide chrome (top nav, footer): full width with internal *max-w-5xl*.
181+
- Reading column (post body, profile bio, settings forms):
182+
`max-w-2xl` (~42 rem).
183+
- Dashboard column (timelines, account list): `max-w-3xl` (~48 rem).
184+
- Wide chrome (top nav, footer): full width with internal `max-w-5xl`.
183185

184-
Breakpoints follow the Wind4 defaults (*sm* 640 px, *md* 768 px, *lg*
185-
1024 px, *xl* 1280 px). Mobile is the design start point; widen by
186+
Breakpoints follow the Wind4 defaults (`sm` 640 px, `md` 768 px, `lg`
187+
1024 px, `xl` 1280 px). Mobile is the design start point; widen by
186188
adding `md:` and `lg:` variants.
187189

188190

189191
Iconography
190192
-----------
191193

192194
Hollo uses a single icon collection: *Lucide*, surfaced through
193-
UnoCSS's *presetIcons* with the *@iconify-json/lucide* package. Icons
195+
UnoCSS's `presetIcons` with the *@iconify-json/lucide* package. Icons
194196
are written as CSS classes:
195197

196198
~~~~ tsx
197199
<span class="i-lucide-bell text-lg" aria-hidden="true" />
198200
~~~~
199201

200-
Sizing follows the surrounding text size by default (*1em*). Icons
201-
inherit *currentColor*, so they tint with the parent's text color
202+
Sizing follows the surrounding text size by default (`1em`). Icons
203+
inherit `currentColor`, so they tint with the parent's text color
202204
(including the theme color where applicable). Decorative icons get
203205
`aria-hidden="true"`; meaningful icons have a paired text label or
204-
*aria-label*.
206+
`aria-label`.
205207

206208

207209
Components
@@ -224,8 +226,8 @@ ring and disabled state.
224226

225227
### Form field
226228

227-
Each field is a labelled stack: *label → control → optional hint or
228-
error*. Labels are above the control, never floating. Required fields
229+
Each field is a labelled stack: label → control → optional hint or
230+
error. Labels are above the control, never floating. Required fields
229231
get a small “required” badge to the right of the label rather than an
230232
asterisk. Errors are red and live below the control.
231233

@@ -237,10 +239,10 @@ right. On small screens the center links collapse into a sheet.
237239

238240
### Card and article
239241

240-
Posts and notifications are rendered as *articles* — semantic HTML, no
241-
visible card border. A subtle bottom divider separates each entry in a
242-
list. Avatar, display name, handle, timestamp, and content stack
243-
vertically on mobile and arrange into a media object on *md*.
242+
Posts and notifications are rendered as `<article>` elements — semantic
243+
HTML, no visible card border. A subtle bottom divider separates each
244+
entry in a list. Avatar, display name, handle, timestamp, and content
245+
stack vertically on mobile and arrange into a media object on `md`.
244246

245247
### Avatar
246248

@@ -254,7 +256,7 @@ version, and a link to the source. No social icons.
254256

255257
### Empty state
256258

257-
Centred icon (Lucide), one-line headline, optional subline, optional
259+
Centered icon (Lucide), one-line headline, optional subline, optional
258260
primary call-to-action. No illustrations.
259261

260262

@@ -264,11 +266,11 @@ Motion
264266
Motion is reserved for state feedback, not decoration. Allowed
265267
transitions:
266268

267-
- *colors*: 150 ms, on hover and focus changes.
268-
- *opacity*: 150 ms, on disclosure toggles.
269-
- *transform: scale*: 100 ms, on press of a button.
269+
- `colors`: 150 ms, on hover and focus changes.
270+
- `opacity`: 150 ms, on disclosure toggles.
271+
- `transform: scale`: 100 ms, on press of a button.
270272

271-
Disable all transitions when *prefers-reduced-motion: reduce* is set.
273+
Disable all transitions when `prefers-reduced-motion: reduce` is set.
272274
Page transitions and route animations don't exist; SSR renders the new
273275
page directly.
274276

@@ -277,16 +279,16 @@ Accessibility
277279
-------------
278280

279281
- Maintain WCAG AA contrast in both light and dark schemes. Theme
280-
colors pass contrast against neutral surfaces at the *600* and
281-
*700* stops; lighter stops are background-only.
282-
- Every focusable element has a visible *focus-visible* ring (a 2 px
283-
ring in *brand-500* or *neutral-500*, offset by 2 px).
284-
- Use semantic HTML: *button* for buttons, *a* for navigation,
285-
*form*/*fieldset*/*label* for forms, *article* for individual posts,
286-
*nav* for navigation regions.
287-
- The *html* element's *lang* attribute reflects the page locale where
288-
a content language is known.
289-
- Decorative imagery uses *alt=“”* or *aria-hidden*. Meaningful
282+
colors pass contrast against neutral surfaces at the `600` and
283+
`700` stops; lighter stops are background-only.
284+
- Every focusable element has a visible `:focus-visible` ring (a 2 px
285+
ring in `brand-500` or `neutral-500`, offset by 2 px).
286+
- Use semantic HTML: `<button>` for buttons, `<a>` for navigation,
287+
`<form>`/`<fieldset>`/`<label>` for forms, `<article>` for individual
288+
posts, `<nav>` for navigation regions.
289+
- The `<html>` element's `lang` attribute reflects the page locale
290+
where a content language is known.
291+
- Decorative imagery uses `alt=""` or `aria-hidden`. Meaningful
290292
imagery has descriptive alternative text.
291293

292294

@@ -297,40 +299,39 @@ Implementation guide
297299

298300
UnoCSS scans every *.tsx* and *.ts* file under *src/* via *@unocss/cli*
299301
and writes a single static stylesheet to *src/public/uno.css*. In
300-
development, *concurrently* runs the UnoCSS watcher alongside *tsx
301-
watch*. In production, *pnpm build* runs *unocss* once before *tsdown*.
302-
The stylesheet is served as a static asset, not bundled with the
303-
application code, and the Layout component links it like any other
304-
*.css* file.
302+
development, *concurrently* runs the UnoCSS watcher alongside `tsx watch`. In
303+
production, `pnpm build` runs `unocss` once before `tsdown`. The stylesheet is
304+
served as a static asset, not bundled with the application code, and the Layout
305+
component links it like any other *.css* file.
305306

306307
### Layout shell
307308

308-
*src/components/Layout.tsx* is the only place that emits *html*, *head*,
309-
and *body*. Pages return a Layout-wrapped tree from their handler.
310-
Layout is responsible for:
309+
*src/components/Layout.tsx* is the only place that emits `<html>`,
310+
`<head>`, and `<body>`. Pages return a Layout-wrapped tree from their
311+
handler. Layout is responsible for:
311312

312313
1. Linking */public/uno.css*.
313314
2. Computing the inline CSS variable string from the requested
314-
*themeColor* and putting it on *html*.
315-
3. Setting *lang*, page metadata (title, OG tags, canonical), and
315+
`themeColor` and putting it on `<html>`.
316+
3. Setting `lang`, page metadata (title, OG tags, canonical), and
316317
favicons.
317318

318319
### Theme tokens
319320

320-
*src/theme/colors.ts* exports a frozen object mapping each
321-
*ThemeColor* enum value to its 50–900 RGB triples. When changing the
322-
palette, edit only this file; nothing else needs to know about the
323-
twenty hues by name.
321+
*src/theme/colors.ts* exports a frozen object mapping each `ThemeColor`
322+
enum value to its 50–900 RGB triples. When changing the palette, edit
323+
only this file; nothing else needs to know about the twenty hues by
324+
name.
324325

325326
### Forms
326327

327328
Forms use the small helper components in *src/components/forms.tsx*:
328329

329-
- *Field* — wraps a control with a label, optional hint, and error
330-
- *TextField*, *TextareaField*, *SelectField* — labelled controls
331-
- *CheckboxField* — checkbox with adjacent label and hint
332-
- *FieldSection*card-shaped *fieldset* with a legend
333-
- *SubmitButton* — primary/secondary/danger submit variants
330+
- `Field` — wraps a control with a label, optional hint, and error
331+
- `TextField`, `TextareaField`, `SelectField` — labelled controls
332+
- `CheckboxField` — checkbox with adjacent label and hint
333+
- `FieldSection`borderless `<fieldset>` with a legend
334+
- `SubmitButton` — primary/secondary/danger submit variants
334335

335336
These compose plain HTML controls with the agreed UnoCSS classes; they
336337
never wrap a third-party input library. Reach for them first; only
@@ -339,18 +340,15 @@ express (the OTP field, for example, intentionally diverges).
339340

340341
### Prose content
341342

342-
Apply the *prose prose-neutral dark:prose-invert* class set to the
343-
container that holds rendered Markdown. Don't apply *prose* to
343+
Apply the `prose prose-neutral dark:prose-invert` class set to the
344+
container that holds rendered Markdown. Don't apply `prose` to
344345
arbitrary blocks of UI; it is intended for long-form text only.
345346

346-
### Variant groups
347+
### Variant group syntax
347348

348-
The UnoCSS *variantGroup* transformer is enabled. Group related
349-
variants:
350-
351-
~~~~ tsx
352-
<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)">
353-
~~~~
354-
355-
Use this judiciously; long flat class lists are still fine when they
356-
are easier to read.
349+
Variant group shorthand (`focus:(border-brand-500 ring-2)`) is **not**
350+
used in this project. `transformerVariantGroup` only expands the
351+
shorthand at class extraction time, but the original string still ships
352+
in the HTML `class` attribute, where the browser splits it on
353+
whitespace and matches `ring-2` (etc.) as a standalone class. Always
354+
write each variant out long-form (`focus:border-brand-500 focus:ring-2 …`).

0 commit comments

Comments
 (0)