From cb93c7a6205aa408307cf52e57768cff2892cc80 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Wed, 29 Jul 2026 13:14:57 +0100 Subject: [PATCH 1/5] chore: update agent instructions on writing CSS --- CLAUDE.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8a012b3517c..094c2665b26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,12 +97,13 @@ Each React component should have its own named folder: ``` ComponentName/ ├── index.tsx # Component implementation -└── index.scss # Styles (if applicable) +└── index.css # Styles (if applicable) ``` -- **Do:** Create a folder per component with `index.tsx` and `index.scss` -- **Don't:** Place multiple `ComponentName.tsx` files in a single folder with one shared `.scss` file +- **Do:** Create a folder per component with `index.tsx` and `index.css` +- **Don't:** Place multiple `ComponentName.tsx` files in a single folder with one shared `.css` file - Re-export from barrel files (`index.ts`) when grouping related components in a parent directory +- New styles should be written in plain CSS, not SCSS - SCSS is being phased out and is no longer linted (see [Writing CSS](#writing-css)) ### Running Dev Server @@ -326,6 +327,35 @@ const docs = await payload.find({ }) ``` +### Writing CSS + +Stylelint enforces the rules below on `.css` files (SCSS is no longer linted and is being phased out). Run `pnpm run lint:css` to check, or `pnpm run lint` to run all linters. + +**Mobile-first media queries only - never `max-width`:** + +```css +/* BAD - rejected by plugin/no-max-width-media-query */ +@media (max-width: 768px) { + ...; +} + +/* GOOD */ +@media (min-width: 768px) { + ...; +} +``` + +**Only the four canonical breakpoints are allowed in a media query** (`plugin/no-non-standard-breakpoints`): `400px`, `768px`, `1024px`, `1440px`. Don't invent one-off breakpoint values. + +**Never use `!important`** (`plugin/no-important`). Refactor selector specificity instead. +Exceptions to this rule are when it's not possible to do so when dealing with external libraries. + +**No sub-pixel precision** (`plugin/no-subpixel-values`): + +- Pixel values may have at most one decimal place (e.g. `0.5px` is fine, `13.523px` is not). +- Box-model and position properties - `width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, `inset*`, `gap*`, `flex-basis`, `min-`/`max-width`/`height` - must be whole numbers with no decimals at all. +- `font-size`, `letter-spacing`, `line-height`, and custom properties (`--*`) are exempt, since a `var()` can't be statically traced to the property it ends up on. + ### RSC/Client Bundling Rules These rules prevent production bundling issues where client code gets evaluated in server context. From be94ed5671b9731881c76c1c3b550a7913398467 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Wed, 29 Jul 2026 14:38:01 +0100 Subject: [PATCH 2/5] specify using tokens --- CLAUDE.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 094c2665b26..c917abb59eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -350,7 +350,16 @@ Stylelint enforces the rules below on `.css` files (SCSS is no longer linted and **Never use `!important`** (`plugin/no-important`). Refactor selector specificity instead. Exceptions to this rule are when it's not possible to do so when dealing with external libraries. -**No sub-pixel precision** (`plugin/no-subpixel-values`): +**Prioritize design tokens over hardcoded pixel/rem values:** + +- Spacing (`width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, `inset*`, `gap*`, `flex-basis`, etc.) should use a `--spacer-*` token from `packages/ui/src/css/spacing.css` (`--spacer-0` through `--spacer-6`), not a raw pixel or rem value. +- If the value needed isn't an exact token, round to the nearest `--spacer-*` token rather than hand-writing a one-off value. +- If a niche value must be precise (not a rounding-friendly case), use `calc()` with a spacer token instead of a raw pixel/rem value, e.g. `calc(var(--spacer-1) * 2.5)` for `10px`. +- The same principle applies to other token families - colors (`--color-*` in `colors.css`), radius (`--radius-*` in `radius.css`), typography (`--text-*` in `typography.css`) - prefer the token over a hardcoded value. +- `1px` borders/strokes and non-standard one-off values with no sensible token (documented as such) are acceptable exceptions. +- See the `ui4` and `ui4-review` skills for full token tables and rounding rules used when migrating existing CSS. + +**No sub-pixel precision** (`plugin/no-subpixel-values`) - applies to whatever raw value remains after the above (e.g. an exception case): - Pixel values may have at most one decimal place (e.g. `0.5px` is fine, `13.523px` is not). - Box-model and position properties - `width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, `inset*`, `gap*`, `flex-basis`, `min-`/`max-width`/`height` - must be whole numbers with no decimals at all. From 2e9f781ee387bd2b3bf0fecc5f709d3d2f0d045f Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Wed, 29 Jul 2026 16:12:57 +0100 Subject: [PATCH 3/5] add more guidance --- CLAUDE.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c917abb59eb..668c2fcd387 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -350,14 +350,49 @@ Stylelint enforces the rules below on `.css` files (SCSS is no longer linted and **Never use `!important`** (`plugin/no-important`). Refactor selector specificity instead. Exceptions to this rule are when it's not possible to do so when dealing with external libraries. +**Prefer logical properties over physical properties** for RTL support: + +```css +/* BAD - physical properties don't flip for RTL */ +padding-left: var(--spacer-3); +margin-right: var(--spacer-2); +border-left: 1px solid var(--color-border); +left: 0; + +/* GOOD - logical properties adapt automatically */ +padding-inline-start: var(--spacer-3); +margin-inline-end: var(--spacer-2); +border-inline-start: 1px solid var(--color-border); +inset-inline-start: 0; +``` + +Use `padding-inline`/`padding-block`, `margin-inline`/`margin-block`, `inset-inline`/`inset-block`, and `border-inline`/`border-block` (with their `-start`/`-end` variants) instead of the `-left`/`-right`/`-top`/`-bottom` equivalents where a direction is implied. + **Prioritize design tokens over hardcoded pixel/rem values:** -- Spacing (`width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, `inset*`, `gap*`, `flex-basis`, etc.) should use a `--spacer-*` token from `packages/ui/src/css/spacing.css` (`--spacer-0` through `--spacer-6`), not a raw pixel or rem value. +Spacing (`width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, `inset*`, `gap*`, `flex-basis`, etc.) should use a `--spacer-*` token from `packages/ui/src/css/spacing.css`, not a raw pixel or rem value: + +| Token | Pixel | +| -------------- | ----- | +| `--spacer-0` | 0px | +| `--spacer-1` | 4px | +| `--spacer-1-5` | 6px | +| `--spacer-2` | 8px | +| `--spacer-2-5` | 12px | +| `--spacer-3` | 16px | +| `--spacer-4` | 24px | +| `--spacer-5` | 32px | +| `--spacer-6` | 40px | + - If the value needed isn't an exact token, round to the nearest `--spacer-*` token rather than hand-writing a one-off value. - If a niche value must be precise (not a rounding-friendly case), use `calc()` with a spacer token instead of a raw pixel/rem value, e.g. `calc(var(--spacer-1) * 2.5)` for `10px`. -- The same principle applies to other token families - colors (`--color-*` in `colors.css`), radius (`--radius-*` in `radius.css`), typography (`--text-*` in `typography.css`) - prefer the token over a hardcoded value. +- The same principle applies to other token families: + - **Colors:** use semantic `--color-*` tokens from `colors.css` (e.g. `--color-bg`, `--color-text-brand`, `--color-border`). Never reference raw `--ramp-*` palette tokens directly outside of `colors.css` as they aren't theme-aware. + - **Radius:** use `--radius-*` from `radius.css` (`--radius-small` 2px, `--radius-medium` 5px, `--radius-large` 13px, `--radius-full` 9999px) instead of hardcoded values. + - **Stroke width:** use `--stroke-width-small` (1px) / `--stroke-width-medium` (2px) from `theme.css`. + - **Box shadows:** use elevation tokens from `elevations.css` (`--elevation-100-canvas`, `--elevation-300-tooltip`, `--elevation-400-menu-panel`, `--elevation-500-modal-window`) instead of a hardcoded `box-shadow`/`rgba()` value - they also handle light/dark theming. + - **Typography:** use `--text-*` tokens from `typography.css`. - `1px` borders/strokes and non-standard one-off values with no sensible token (documented as such) are acceptable exceptions. -- See the `ui4` and `ui4-review` skills for full token tables and rounding rules used when migrating existing CSS. **No sub-pixel precision** (`plugin/no-subpixel-values`) - applies to whatever raw value remains after the above (e.g. an exception case): From 47897c2bc4a8a1c0102498260238c188f8788143 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Thu, 30 Jul 2026 17:42:03 +0100 Subject: [PATCH 4/5] fix --- CLAUDE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 668c2fcd387..0ebf08de34d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -362,7 +362,7 @@ left: 0; /* GOOD - logical properties adapt automatically */ padding-inline-start: var(--spacer-3); margin-inline-end: var(--spacer-2); -border-inline-start: 1px solid var(--color-border); +border-inline-start: var(--stroke-width-small) solid var(--color-border); inset-inline-start: 0; ``` @@ -389,7 +389,6 @@ Spacing (`width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, - The same principle applies to other token families: - **Colors:** use semantic `--color-*` tokens from `colors.css` (e.g. `--color-bg`, `--color-text-brand`, `--color-border`). Never reference raw `--ramp-*` palette tokens directly outside of `colors.css` as they aren't theme-aware. - **Radius:** use `--radius-*` from `radius.css` (`--radius-small` 2px, `--radius-medium` 5px, `--radius-large` 13px, `--radius-full` 9999px) instead of hardcoded values. - - **Stroke width:** use `--stroke-width-small` (1px) / `--stroke-width-medium` (2px) from `theme.css`. - **Box shadows:** use elevation tokens from `elevations.css` (`--elevation-100-canvas`, `--elevation-300-tooltip`, `--elevation-400-menu-panel`, `--elevation-500-modal-window`) instead of a hardcoded `box-shadow`/`rgba()` value - they also handle light/dark theming. - **Typography:** use `--text-*` tokens from `typography.css`. - `1px` borders/strokes and non-standard one-off values with no sensible token (documented as such) are acceptable exceptions. From 4c4038f6162ca92bc975c9764a0dfb45c0435220 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Thu, 30 Jul 2026 17:45:43 +0100 Subject: [PATCH 5/5] oops --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ebf08de34d..5f16f18698f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -389,9 +389,9 @@ Spacing (`width`/`height`, `margin*`, `padding*`, `top`/`right`/`bottom`/`left`, - The same principle applies to other token families: - **Colors:** use semantic `--color-*` tokens from `colors.css` (e.g. `--color-bg`, `--color-text-brand`, `--color-border`). Never reference raw `--ramp-*` palette tokens directly outside of `colors.css` as they aren't theme-aware. - **Radius:** use `--radius-*` from `radius.css` (`--radius-small` 2px, `--radius-medium` 5px, `--radius-large` 13px, `--radius-full` 9999px) instead of hardcoded values. + - **Stroke width:** use `--stroke-width-small` (1px) / `--stroke-width-medium` (2px) from `theme.css`. - **Box shadows:** use elevation tokens from `elevations.css` (`--elevation-100-canvas`, `--elevation-300-tooltip`, `--elevation-400-menu-panel`, `--elevation-500-modal-window`) instead of a hardcoded `box-shadow`/`rgba()` value - they also handle light/dark theming. - **Typography:** use `--text-*` tokens from `typography.css`. -- `1px` borders/strokes and non-standard one-off values with no sensible token (documented as such) are acceptable exceptions. **No sub-pixel precision** (`plugin/no-subpixel-values`) - applies to whatever raw value remains after the above (e.g. an exception case):