super-productivity/docs/styling-guide.md
Johannes Millan a9932f9f71
fix(theme): polish, atomic activation, and validator hardening (#9157)
* docs(agents): require reviews to ask if a feature earns its place

* build: add output folder to gitignore because of playwright writing there

* style(themes): polish built-in visual states

* fix(theme): make stylesheet activation atomic

* fix(theme): harden custom theme contracts

* fix(theme): reject unterminated url() and exempt commented keywords

Two validator correctness fixes found in review of the theme-polish branch:

- Reject any url()/src() that runs to end-of-input without a closing ')'.
  The CSS tokenizer still emits a fetchable url-token at EOF (Syntax
  §4.3.6), so a theme file ending mid-url (e.g. `background:url(http://x`)
  beaconed on every load — the argument regex requires the ')' and missed
  it. Proven to fetch in Chromium.

- Move the @import / image() / image-set() keyword-presence bans onto the
  comment-stripped view. Scanning the unstripped source rejected benign
  themes that merely mention these tokens in a comment; because stored
  themes are re-validated on every load, such a theme silently reverted to
  Default after updating. url()/src() stay on the unstripped view so a
  disguised token can never hide a later live fetch.

Docs and specs updated (+5 cases: EOF-fetch payloads, line reporting,
comment exemption, and real-rule-next-to-comment still rejects).

* fix(theme): restore plainspace board-panel header underline

The token-model pass removed the data:-URL `--underline-hand` mask and
migrated its consumers to `clip-path: var(--underline-hand-shape)`, but the
board-panel `header::after` consumer was missed. Its `mask: var(--underline-
hand)` then resolved to an undefined variable, collapsing the hand-drawn
ribbon to a solid 10px bar. Convert it to the shared clip-path polygon like
the other two consumers; the percentage-based polygon scales to the box.
2026-07-18 18:49:00 +02:00

229 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Styling Guide
## Rules
- **All visual styling must use CSS variables** from `src/styles/_css-variables.scss` — never hardcode colors, spacing, shadows, transitions, or z-index.
- **Positioning/layout is fine as plain CSS** — flexbox, grid, display, position, dimensions.
- **Check `src/app/ui/` first** before creating new styled elements — 40+ reusable components exist.
- **Component SCSS should be minimal** — shared styles belong in `src/styles/components/` or as a mixin.
- **Material overlay components** (menus, dialogs, tooltips) render outside component scope — style them in `src/styles/components/` and add a comment in the component pointing there.
## Anti-Patterns
| Avoid | Do Instead |
| ------------------------------------ | ------------------------------------------------------------------------- |
| Hardcoded colors (`#fff`, `red`) | CSS variables (`--text-color`, `--card-bg`, `--color-danger`) |
| Hardcoded spacing (`16px`, `1rem`) | Spacing variables (`--s2`, `--s`, `--s-half`) |
| Hardcoded shadows | Elevation variables (`--whiteframe-shadow-*dp`, `--md-sys-level*`) |
| Hardcoded transitions/durations | Transition variables (`--transition-standard`, `--transition-duration-*`) |
| Custom z-index values | Z-index variables (`--z-main-header`, `--z-backdrop`, etc.) |
| New styled elements without checking | Check `src/app/ui/` for existing reusable components first |
## Dialog action buttons
One treatment per role so dialogs feel consistent. Classify by **function, not label** ("Close" that dismisses a read-only view is secondary; "Close" that commits is primary).
| Role | Treatment | Notes |
| -------------------------------- | --------------------------------- | ---------------------------------------------------------------- |
| Primary / confirming | `mat-flat-button color="primary"` | Save, OK, Submit, Create, Schedule — the main affirmative action |
| Cancel / dismiss / secondary | `mat-button` (no `color`) | De-emphasized text button |
| Destructive | `color="warn"` (keep the variant) | Delete, Remove, Unschedule — never folded into primary |
| Genuine alternative (not cancel) | `mat-stroked-button` | e.g. "Skip instance", "Configure" next to a primary action |
Rules:
- **Icons:** drop generic `check`/`close` icons on OK/Cancel/Save/Submit — they add nothing. Keep icons that carry meaning (`alarm`/`today`/`event_busy` in scheduling, `wb_sunny`, `save`, `cloud_upload`, `delete_forever`).
- **No `color` on cancel/close** — leftover `color="primary"` on a Cancel just tints it; remove it.
- **No dead classes** — the legacy Bootstrap `btn btn-primary` classes are gone; don't reintroduce them. `submit-button` is only styled inside `dialog-create-tag`.
- **Symmetric choice dialogs** (e.g. sync "use remote" vs "use local") may use two matched `mat-stroked-button`s — there is no single primary.
## Key Files
| File | Purpose |
| -------------------------------- | ------------------------------------------------------------- |
| `src/styles/_css-variables.scss` | All CSS custom properties (design tokens) |
| `src/styles/themes.scss` | Material theme setup + utility classes |
| `src/styles/page.scss` | Global page/body styles |
| `src/styles/util.scss` | Utility classes |
| `src/styles/components/` | Global component styles (Material overrides, shared patterns) |
| `src/styles/mixins/` | Reusable SCSS mixins |
| `src/app/ui/` | 40+ reusable Angular UI components |
## Spacing Variables (8px Grid)
| Variable | Value | Variable | Value |
| ------------- | ----- | -------- | ----- |
| `--s-quarter` | 2px | `--s4` | 32px |
| `--s-half` | 4px | `--s5` | 40px |
| `--s` | 8px | `--s6` | 48px |
| `--s2` | 16px | `--s7` | 56px |
| `--s3` | 24px | `--s8` | 64px |
```scss
// ✅ Good
padding: var(--s2) var(--s3);
gap: var(--s-half);
// ❌ Bad
padding: 16px 24px;
gap: 4px;
```
## Color Variables
| Use case | Variables |
| ------------------ | -------------------------------------------------------------------------------------------------- |
| Text | `--text-color`, `--text-color-muted`, `--text-color-most-intense` |
| Backgrounds | `--bg`, `--card-bg`, `--task-c-bg`, `--sub-task-c-bg` |
| Semantic | `--color-success` (#4caf50), `--color-warning` (#ff9800), `--color-danger` (#f44336) |
| Material palette | `--palette-primary-500`, `--palette-accent-500`, `--palette-warn-500` (100900) |
| Overlays | `--c-dark-10` through `--c-dark-90`, `--c-light-05` through `--c-light-90` |
| Alpha coefficients | `--border-alpha` (0.12), `--overlay-alpha` (0.1), `--muted-alpha` (0.6), `--separator-alpha` (0.3) |
### Theme-Specific Values
Light theme sets: `--bg: #f8f8f7`, `--card-bg: #ffffff`, `--text-color: rgb(44, 44, 44)`
Dark theme sets: `--bg: #131314`, `--card-bg: var(--dark3)`, `--text-color: rgb(230, 230, 230)`
Dark elevation colors: `--dark0` (rgb(0,0,0)) through `--dark24` (rgb(56,56,56))
### Theme-Specific Overrides in Components
```scss
@include darkTheme() {
/* dark-only styles */
}
@include lightTheme() {
/* light-only styles */
}
```
Mixins are in `src/styles/mixins/_theming.scss`.
## Shadows & Elevation
- `--whiteframe-shadow-1dp` through `--whiteframe-shadow-24dp` — classic Material shadows
- `--md-sys-level1` through `--md-sys-level5` — Material Design 3 style
## Transitions & Animations
| Type | Variables |
| ---------- | ------------------------------------------------------------------------------------------------------------------ |
| Shorthands | `--transition-fast`, `--transition-standard`, `--transition-enter`, `--transition-leave` |
| Durations | `--transition-duration-xs` (90ms), `-s` (150ms), `-m` (225ms), `-l` (375ms) |
| Additional | `--transition-duration-enter` (225ms), `--transition-duration-leave` (195ms), `--page-transition-duration` (225ms) |
| Timing | `--ani-standard-timing`, `--ani-enter-timing`, `--ani-leave-timing`, `--ani-sharp-timing` |
When migrating hardcoded durations, pick the nearest bucket — up to ~15% drift is acceptable for UI transitions.
## Focus Ring
Keyboard-accessibility tokens for custom interactive elements. Material components keep their own focus treatment; use these for non-Material buttons, cards, and custom controls.
| Variable | Value |
| --------------------- | ------------------- |
| `--focus-ring-width` | 2px |
| `--focus-ring-offset` | 2px |
| `--focus-ring` | `var(--brand)` |
| `--focus-ring-color` | `var(--focus-ring)` |
Themes should override the public `--focus-ring` primitive on `body` / `body.isDarkTheme`. `--focus-ring-color` is the compatibility alias consumed by existing components.
Quickest adoption — add the `.focus-ring` utility class from `util.scss`, which applies an `outline` on `:focus-visible` only (so it doesn't fire on mouse clicks).
```scss
// opt-in per-element
.my-button:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
```
## Z-Index Layers
| Variable | Value | Purpose |
| ------------------------ | ----- | ------------------------ |
| `--z-check-done` | 11 | Task done checkbox |
| `--z-main-header` | 12 | Main header |
| `--z-task-title-focus` | 32 | Focused task title |
| `--z-mobile-bottom-nav` | 50 | Mobile bottom navigation |
| `--z-side-nav` | 60 | Side navigation |
| `--z-backdrop` | 222 | Backdrop overlay |
| `--z-add-task-bar` | 999 | Add task bar |
| `--z-search-bar` | 999 | Search bar |
| `--z-onboarding-presets` | 999 | Onboarding preset screen |
| `--z-tour` | 1001 | Tour overlay |
## Layout Variables
| Variable | Value | Notes |
| ----------------------- | ----- | ------------------ |
| `--component-max-width` | 800px | 9001000px on iPad |
| `--side-nav-width` | 200px | |
| `--side-nav-width-l` | 400px | |
| `--bar-height-large` | 56px | |
| `--bar-height` | 48px | |
| `--bar-height-small` | 40px | |
## Responsive Breakpoints
Available as CSS vars (`--layout-xxxs` through `--layout-xl`) and as SCSS variables and mixins in `src/styles/mixins/_media-queries.scss`:
| Breakpoint | Value |
| ---------- | ------ |
| `xxxs` | 398px |
| `xxs` | 440px |
| `xs` | 600px |
| `sm` | 960px |
| `md` | 1280px |
| `lg` | 1920px |
| `xl` | 2000px |
## Utility Classes
Defined in `src/styles/util.scss` and `src/styles/themes.scss`:
- Layout: `.center-wrapper`, `.mw` (max-width container)
- Responsive: `.hide-xs`, `.hide-xxs`, `.hide-gt-sm`
- Input: `.show-only-on-touch-primary`, `.show-only-on-mouse-primary`
- Theme: `.show-dark-only`, `.show-light-only`
- Color: `.bg-primary`, `.bgc-accent`, `.color-primary`, `.bg-success`, `.bg-warning`, `.bg-danger`
- Effects: `.milk-glass` (backdrop blur)
## Authoring Themes
Theme files live in `src/assets/themes/*.css` and are loaded at runtime.
### Side nav: never apply CB-creating properties to the `magic-side-nav` host
The mobile drawer (`.nav-sidenav`) and its overlay (`.nav-backdrop-mobile`) are `position: fixed` children of the `magic-side-nav` host. On mobile the host shrinks to `width: 0` (`hostWidthSignal` in `magic-side-nav.component.ts`).
Any property that establishes a [new containing block for fixed-position descendants](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block) — `backdrop-filter`, `filter`, `transform`, `perspective`, `contain: paint|layout|strict`, or a matching `will-change` — re-anchors the drawer and backdrop to the 0-wide host. The drawer never appears when the user taps the menu.
Apply glass/blur/tint to the inner `.nav-sidenav` instead:
```css
/* ❌ Bad — collapses the mobile drawer */
body.isDarkTheme magic-side-nav {
background: var(--my-pane);
backdrop-filter: blur(32px);
}
/* ✅ Good — host stays visually inert */
body.isDarkTheme magic-side-nav .nav-sidenav {
background: var(--my-pane);
backdrop-filter: blur(32px);
}
```
Properties that do NOT create a containing block — `background`, `border`, `box-shadow`, `margin` — are safe on the host. See `velvet.css` and `liquid-glass.css` for full reference patterns.
## Global Component Styles
Located in `src/styles/components/`, these are needed for elements that render outside component scope:
- `_overwrite-material.scss` — Material component customizations
- `_customizer-menu.scss`, `backdrop.scss`, `bottom-panel.scss`
- `markdown.scss`, `mentions.scss`, `table.scss`
- `fab-wrapper.scss`, `wrap-buttons.scss`, `multi-btn-wrapper.scss`
- `planner-shared.scss`, `formly-rows.scss`, `scrollbars.scss`