|
| 1 | +--- |
| 2 | +paths: |
| 3 | + - "packages/components/src/**/*.module.css" |
| 4 | +--- |
| 5 | + |
| 6 | +# Component CSS |
| 7 | + |
| 8 | +[ADR 0004](../../docs/adr/0004-design-tokens-are-the-only-source-of-visual-values.md) and |
| 9 | +[ADR 0005](../../docs/adr/0005-styling-uses-style-props-and-native-css.md) own the reasoning. |
| 10 | + |
| 11 | +## Hard Rules |
| 12 | + |
| 13 | +| Rule | Violation | |
| 14 | +| ---- | --------- | |
| 15 | +| Resolve every colour to a **semantic** token | A hex, `rgb()`, `hsl()`, or named colour | |
| 16 | +| Resolve a local declaration to a token, not a raw length | `--hop-MenuItem-sm-padding-block: 0.625rem` instead of a `--hop-space-*` token | |
| 17 | +| Prefer a semantic token over a core one — component CSS references no core colour today | Reaching past `--hop-neutral-text` to a core palette entry like `--hop-coastal-25` | |
| 18 | +| Read `--hop-comp-<family>-*` only from a module in that family | `list-box/src/ListBoxItem.module.css` reading `--hop-comp-select-*` | |
| 19 | +| Redefine a token-package property only inside `packages/tokens` | Overriding `--hop-comp-button-*` from another component's module | |
| 20 | +| Locate a token file by grepping its `comp-` key, not by component name | Expecting `checkbox.tokens.json`; the file is `mark.checkbox.tokens.json` | |
| 21 | +| Declare a local on the module root when the component has no token file | Adding a 22nd token file for a one-off value | |
| 22 | + |
| 23 | +Stylelint owns the mechanical layer — `px` is outside `unit-allowed-list`, `selector-class-pattern` |
| 24 | +enforces `hop-ComponentName__element-name--modifier-name`, and `custom-property-pattern` enforces |
| 25 | +`hop-ComponentName-*`. Because `px` already fails at lint, the live failure mode is a `rem` literal |
| 26 | +that lint permits; converting `8px` to `0.5rem` is not tokenizing it. |
| 27 | + |
| 28 | +## Sanctioned raw values |
| 29 | + |
| 30 | +| Case | Why | |
| 31 | +| ---- | --- | |
| 32 | +| Hairlines as `0.0625rem` — 21 in component CSS | There is no border-width token family | |
| 33 | +| `--hop-easing-*` referenced directly | Motion has core tokens but no semantic tier | |
| 34 | +| Focus rings | There is no global `--hop-focus-ring` token. Set `outline: none` in the base rule and restore it under `[data-focus-visible]`, through a local `--hop-<Component>-focus-ring-color` pointing at `--hop-primary-border-focus` or the family's `--hop-comp-*-border-color-focus` | |
| 35 | +| Breakpoint values | Deliberately not tokens — custom properties cannot be used in media query conditions. The scale lives in `packages/styled-system/src/responsive/Breakpoints.ts` | |
| 36 | + |
| 37 | +## Token families are shared |
| 38 | + |
| 39 | +Only 21 token files exist per brand against 90 CSS modules, and five families have no single owning |
| 40 | +component — `field` (read by 12 modules), `mark`, `control`, `select`, `tabs`. So the read boundary is |
| 41 | +the *family*, not the file. Reading across families is the violation. The three modules in `calendar/` |
| 42 | +and `date-picker/` that override `--hop-comp-button-*` are known debt, not a pattern to copy. |
| 43 | + |
| 44 | +## The three layers |
| 45 | + |
| 46 | +```css |
| 47 | +.hop-Button { |
| 48 | + --hop-Button-text-font: var(--hop-comp-button-text-font); |
| 49 | + --hop-Button-column-gap: var(--hop-space-inline-xs); |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +| Layer | Defined in | Read from | |
| 54 | +| ----- | ---------- | --------- | |
| 55 | +| `--hop-comp-<family>-*` | `packages/tokens/src/tokens/components/<brand>/*.tokens.json` | Modules in that family | |
| 56 | +| `--hop-<category>-*` | `packages/tokens/src/tokens/core/`, `.../semantic/<brand>/<light\|dark>/` | Any module, semantic tier first | |
| 57 | +| `--hop-<PascalName>-*` | The module's own root selector | That module — unless deliberately published as a theming hook, as `--hop-RichIcon-*` is for `packages/icons` | |
| 58 | + |
| 59 | +Sibling modules inherit wholesale with `composes: hop-Input from "../../inputs/src/Input.module.css"`, |
| 60 | +which pulls in the other module's class *and* its locals. Check what a `composes:` target declares |
| 61 | +before adding a local that may already exist there. |
0 commit comments