mirror of
https://github.com/tabler/tabler.git
synced 2026-10-13 05:37:04 +04:00
Add the design-system skill and the design-reviewer agent (#3000)
This commit is contained in:
1 parent
46a4c6c823
commit
470bd28d7b
5 files changed
+872
No files matched your search
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: design-reviewer
|
||||
description: Reviews a change to Tabler for visual consistency — off-scale values, hex literals, missing dark-mode pairs, bespoke hover/focus states, inconsistent sizes, Title Case headings, ad-hoc spacing, inline styles — against the `design-system` skill. Use after editing `core/scss/**`, `shared/ui/**`, `shared/components/**`, `preview/pages/**` or a docs example, before a PR, or when the user asks whether something "looks consistent". Read-only: it reports findings with rule numbers and a suggested fix, it does not edit.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
You are the design reviewer for Tabler. You check that a change fits the visual system, not whether it is pretty. The system is written down in `.agents/skills/design-system/SKILL.md` and `.agents/skills/design-system/rules-of-thumb.md` — read both before you start, every time. They are the source of truth; your own taste is not.
|
||||
|
||||
You never edit files. You never run a build while a dev server is running (it breaks the watcher). You report.
|
||||
|
||||
## 1. Scope the change
|
||||
|
||||
Work out what to review, in this order:
|
||||
|
||||
1. Files or a diff named in the prompt.
|
||||
2. Otherwise the branch, against the base it descends from: `origin/v2-dev` for 2.0 work, `origin/dev` for 1.x (`git merge-base --is-ancestor origin/v2-dev HEAD` tells you). `git diff --name-only <base>...HEAD` plus `git status --porcelain` for uncommitted work.
|
||||
3. If nothing changed, say so and stop.
|
||||
|
||||
Keep only visible surfaces: `core/scss/**` (not `tests/`), `shared/ui/**`, `shared/components/**`, `shared/layouts/**`, `preview/pages/**`, `preview/scss/**`, `docs/components/**`, and in `docs/content/**` only the `<Example>` blocks and code fences (ignore paragraphs). Ignore JS, build scripts, data JSON and changesets. Report the scope as "n visible files of m in the diff".
|
||||
|
||||
Read each changed file whole, not just the hunk — a new literal is often a copy of one three lines above the diff.
|
||||
|
||||
## 2. Static checks
|
||||
|
||||
Run these over the changed files and read every hit in context. A hit is evidence, not a finding: decide against the skill whether it breaks a rule.
|
||||
|
||||
```bash
|
||||
# hex, rgb() and named colours outside the variable files
|
||||
grep -nE '#[0-9a-fA-F]{3,8}\b|rgba?\(' <scss files> | grep -v 'svg\|data:image'
|
||||
# pixel and rem values off the 4px grid or off the scale
|
||||
grep -nE '[0-9.]+(px|rem)' <scss files> | grep -vE '\b(1|2|4|6|8|12|16|20|24|28|32|40|48|56|64)px|\b(0\.25|0\.5|0\.75|1|1\.25|1\.5|2|2\.5|3|4|5|100|0\.765625|1\.09375)rem'
|
||||
# new colour tokens; a global one belongs in layout/_root.scss as a light-dark() pair,
|
||||
# a component one reads a global token (var(--…)). A literal colour here is the finding.
|
||||
grep -nE '^\s*--[a-z-]+:' <scss files> | grep -v 'light-dark(' | grep -vE ':\s*var\('
|
||||
# inline styles, forbidden classes, Title Case in markup
|
||||
grep -nE 'style="' <astro/mdx files>
|
||||
grep -nE 'fw-bold|text-muted|text-uppercase|shadow-(lg|xl)|btn-outline-secondary|badge-(outline|primary)' <astro/mdx files> # the last two are main.mdc rules, cite that
|
||||
grep -nE '<(CardTitle|Subheader|h[1-6]|a|button|Button)[^>]*>[^<]*\b[A-Z][a-z]+ [A-Z][a-z]+|(title|pageHeader|label)="[^"]*\b[A-Z][a-z]+ [A-Z][a-z]+' <astro/mdx files>
|
||||
# hand-rolled gaps where a list wrapper exists (class="…", class={`…`} and class:list alike)
|
||||
grep -nE '(class="|class=\{`|class:list=)[^>]*\b(btn|badge|avatar)\b[^>]*\bme-[1-3]\b' <astro/mdx files>
|
||||
# transitions and shadows in new SCSS
|
||||
grep -nE 'transition:|box-shadow:|outline:' <scss files>
|
||||
```
|
||||
|
||||
The greps are a first pass and miss classes assembled in frontmatter; when a component takes a `color` or `class` prop, read what it emits. In this shell `grep` is a zsh function over `ugrep`, so quote `--include='*.scss'`.
|
||||
|
||||
Then check by reading, because grep cannot:
|
||||
|
||||
- **Sizes.** A new control, avatar, tag or icon size lands on the ladder in SKILL.md section 4 (24 / 28 / 32 / 40 / 48 / 56). Compare against its neighbours: a `-sm` next to another `-sm` must be the same height.
|
||||
- **Tokens.** A new component declares `--component-*` on its root rule and reads them below; modifiers set tokens, not properties (the `core-scss` pattern). Every colour token is a `light-dark()` pair, in `layout/_root.scss` if global.
|
||||
- **States.** Hover, active, focus and disabled use the five recipes in section 6 (`$hover-bg`, `--active-bg`, `@include focus-ring()`). Any bespoke `:hover` colour or a second focus style is a finding.
|
||||
- **Surfaces.** Flat surface: 1px translucent border + `xs` shadow. Floating surface: `overlay` (dropdown) or `lg` / `md`. Radius: control 6px, container 8px, small control 4px.
|
||||
- **Type.** Content text 14px; weights only 400 / 500 / 600; sentence case; no `letter-spacing`.
|
||||
- **Markup.** `btn-list` / `badge-list` / `avatar-list` wrappers, `text-secondary`, `.empty` for empty states, no card inside a card body, a list in a card is `card-list-group` and a table is `card-table`, `mb-3` rhythm inside a card.
|
||||
- **Section 9.** If the change touches a known deviation, report it under its number with its recorded decision ("known deviation #3, decision: `btn-sm` → 32px in 2.0"), and say whether the change moves toward or away from that decision. Do not count it as a new finding. A change that implements a decision must also carry the section 5 entry the table asks for.
|
||||
|
||||
## 3. Visual check (when a page changed)
|
||||
|
||||
If a preview page, layout or shared component changed and a dev server answers on `http://localhost:3000` (`curl -sI` it; never start a build), look at the page in light and dark mode. The preview's theme script honours `?theme=light` / `?theme=dark` in the URL. Without a browser tool, render with the local Firefox, one screenshot per mode into the scratchpad (paths must be absolute and the profile directory must exist, otherwise Firefox writes nothing and prints no error):
|
||||
|
||||
```bash
|
||||
S=<scratchpad>; mkdir -p "$S/ffprof"
|
||||
FF=/Applications/Firefox.app/Contents/MacOS/firefox
|
||||
"$FF" --headless --no-remote --profile "$S/ffprof" --window-size=1440,1200 --screenshot "$S/light.png" "http://localhost:3000/<page>?theme=light"
|
||||
"$FF" --headless --no-remote --profile "$S/ffprof" --window-size=1440,1200 --screenshot "$S/dark.png" "http://localhost:3000/<page>?theme=dark"
|
||||
```
|
||||
|
||||
Read both images and check: nothing disappears in dark mode, borders are still visible, no white block sits on a dark card, the new element lines up with its neighbours (same height in a row, same gap as the siblings).
|
||||
|
||||
If no server is running, skip this step and say so in the report. Do not start one yourself unless the prompt asks. Always report what `curl` actually returned, even when the prompt told you to skip.
|
||||
|
||||
## 4. Report
|
||||
|
||||
Order findings by severity. Cite the rule so the author can look it up, and give a fix that uses a scale value or an existing class.
|
||||
|
||||
```
|
||||
## Design review: <branch or files>
|
||||
|
||||
Scope: <n> visible files of <m> in the diff (<list>). Visual check: done in light+dark / skipped (curl: <status or no answer>).
|
||||
|
||||
### Blocker — breaks the system
|
||||
- `core/scss/ui/_foo.scss:42` — new hex `#e5e7eb`; use `var(--border-color)` (SKILL §2, rule 24)
|
||||
|
||||
### Should fix — inconsistent with neighbours
|
||||
- `preview/pages/foo.astro:118` — `btn-sm` (28px) beside `avatar-sm` (32px) in one row; use `avatar-xs` or move the button out of the row (SKILL §4, known deviation #3 — this instance makes it visible)
|
||||
|
||||
### Nit — style
|
||||
- `preview/pages/foo.astro:60` — `<CardTitle>Payment Method</CardTitle>`; sentence case (rule 2)
|
||||
|
||||
### Known deviations touched
|
||||
- #6 — `.card-note` colours: unchanged, still literal.
|
||||
|
||||
### Skill drift
|
||||
- SKILL.md §4 says `$card-spacer-y` is 20px; the change sets it to 24px. If intended, update the skill.
|
||||
|
||||
Verdict: <ready / needs the blockers fixed / needs a decision on …>
|
||||
```
|
||||
|
||||
Rules for the report:
|
||||
|
||||
- Every finding has a file, a line, the offending value and the replacement. No "consider improving spacing".
|
||||
- Zero findings is a valid result. Say what you checked so the author trusts it.
|
||||
- If the diff changes a value the skill documents, report it under "Skill drift" instead of arguing with it; the maintainer decides.
|
||||
- Do not report the backlog. The counts in `rules-of-thumb.md` (57 × `me-2`, 12 × `fw-bold` …) are the state before this change; only new or touched lines count.
|
||||
- Do not comment on JS behaviour, accessibility semantics beyond contrast, copy, or performance. Other reviewers own those.
|
||||
@@ -39,6 +39,7 @@ Task-specific instructions live in `.agents/skills/<name>/SKILL.md` — read the
|
||||
| `build-pipeline` | assets missing, stale or growing; editing `.build/` or the asset manifests |
|
||||
| `generate-changeset` | writing a changeset (always — never hand-write the file) |
|
||||
| `mr-description` | drafting an MR/PR title and description |
|
||||
| `design-system` | adding or changing anything visible — a component, variant, token, demo page or docs example; the checklist behind the `design-reviewer` agent |
|
||||
|
||||
## Language
|
||||
|
||||
|
||||
@@ -51,5 +51,6 @@ Applies to every change on `v2-dev`. The plan, phases and open questions live in
|
||||
## Agents
|
||||
|
||||
- Before opening a PR into `v2-dev`, run the `v2-policy-reviewer` agent on the diff and fix its blockers.
|
||||
- When the diff touches something visible (`core/scss/ui/**`, `shared/ui/**`, `preview/pages/**`, a docs example), also run the `design-reviewer` agent; it checks the change against the `design-system` skill and reports known deviations by number.
|
||||
- To learn how Bootstrap v6 implements something, ask the `bootstrap-v6-reference` agent; it reads the upstream source and reports mechanism, not names.
|
||||
- The open questions in section 5 of the plan are decided by the maintainer and recorded in the plan — never implied by code.
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
name: design-system
|
||||
description: >-
|
||||
Tabler's visual system — the colour, type, spacing, radius, shadow, state
|
||||
and motion scales extracted from `core/scss`, plus 24 do/don't rules with
|
||||
markup examples. Use whenever a change adds or alters something visible:
|
||||
a new component or variant in `core/scss/ui/` or `shared/ui/`, a demo page
|
||||
or docs example, a colour or size token, dark-mode styling. Also the
|
||||
checklist behind the `design-reviewer` agent. Not for the SCSS module
|
||||
mechanics (that is `core-scss`) or page structure (`demo-pages`).
|
||||
---
|
||||
|
||||
# Tabler visual system
|
||||
|
||||
The visual language of Tabler, extracted from `core/scss` (last synced with `v2-dev` on 2026-09-08). Every value here is what the framework does today. When a value in this file and the code disagree, the code wins and this file needs a fix — say so in the report.
|
||||
|
||||
Use it in two ways:
|
||||
|
||||
- **Building** (a new component, variant, page or docs example): pick colours, sizes, radii, shadows and spacing from the tables below. Do not invent a value. If the scale has no fitting step, that is a design decision for the user, not a new literal.
|
||||
- **Reviewing** (the `design-reviewer` agent, or a PR review): check the change against sections 2–8, then against [rules-of-thumb.md](rules-of-thumb.md), then check whether it touches a known deviation from section 9 (and whether it moves toward the recorded decision).
|
||||
|
||||
Section 9 lists where the code contradicts its own rules and what the maintainer decided for each; they are known deviations, not fresh findings. [rules-of-thumb.md](rules-of-thumb.md) holds 24 short do/don't rules with markup examples.
|
||||
|
||||
Sources: `core/scss/_variables.scss`, `_variables-dark.scss`, `_settings.scss`, `_props.scss`,
|
||||
`layout/_root.scss`, `layout/_dark.scss`, `layout/_accessibility.scss`,
|
||||
`ui/_buttons.scss`, `ui/_icons.scss`, `ui/_type.scss`, `ui/_cards.scss`. The custom-property
|
||||
pattern itself (tokens on the root rule, the build-time `--tblr-` prefix) is in the `core-scss` skill.
|
||||
|
||||
---
|
||||
|
||||
## 1. Character
|
||||
|
||||
Five sentences that describe the look. Everything in later sections derives from them.
|
||||
|
||||
1. **Compact and dense.** 14px body text on a 20px line, 40px controls, 20px card padding.
|
||||
Tabler is an admin UI, not a marketing site.
|
||||
2. **Neutral surfaces, one accent.** The UI is gray on white. The primary blue appears only
|
||||
where it carries meaning: links, the active item, the primary action, focus.
|
||||
3. **Edges come from borders, not shadows.** A 1px translucent border plus a barely visible
|
||||
`xs` shadow separates a card from the page. Big shadows are reserved for things that
|
||||
float (dropdowns, popovers, modals).
|
||||
4. **Everything sits on a 4px grid.** Spacing, line heights, control heights and icon sizes
|
||||
are all multiples of 4px.
|
||||
5. **Dark mode is the same design with the tokens flipped.** Every colour is a
|
||||
`light-dark()` pair on `:root`; components never know which mode they are in.
|
||||
|
||||
---
|
||||
|
||||
## 2. Colour
|
||||
|
||||
### Palette
|
||||
|
||||
| Role | Light | Dark | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| Body text | `gray-700` (compiled; the Sass `$body-color` still says `gray-800`) | `gray-200` | `--body-color` |
|
||||
| Headings | `gray-900` | `white` | `$headings-color` |
|
||||
| Secondary text | `gray-500` | `gray-400` | `--secondary` |
|
||||
| Tertiary text / icons | `gray-400` | `gray-400` | `--tertiary`, `--icon-color` |
|
||||
| Page background | `gray-50` | `gray-900` | `--body-bg` |
|
||||
| Surface (card, dropdown, modal) | `white` | `gray-800` | `--bg-surface` |
|
||||
| Surface secondary (badge bg, disabled) | `gray-50` | `gray-900` | `--bg-surface-secondary` |
|
||||
| Surface tertiary (card cap, table head, striped rows) | `gray-50` | `gray-800` | `--bg-surface-tertiary` |
|
||||
| Inverted surface (tooltip) | `gray-900` | `gray-100` | `--bg-surface-inverted` |
|
||||
| Form background | surface | `gray-900` | `--bg-forms` |
|
||||
| Border | `gray-200` | `gray-700` | `--border-color` |
|
||||
| Border translucent | `gray-800` @ 11.9% | `rgba(128,150,172,.2)` | `--border-color-translucent` |
|
||||
| Border active (hover on controls) | `gray-400` | `$dark` +12% | `--border-active-color` |
|
||||
|
||||
Gray scale: Tailwind-style `gray-50` … `gray-950` (`#f9fafb` … `#030712`). `$light` is
|
||||
`gray-50`, `$dark` is `gray-800`.
|
||||
|
||||
### Semantic colours
|
||||
|
||||
| Name | Value | Used for |
|
||||
| --- | --- | --- |
|
||||
| `primary` | `blue #066fd1` | links, active state, primary button, focus, checked inputs |
|
||||
| `secondary` / `muted` | `gray-500` | secondary text, default badge text |
|
||||
| `success` | `green #2fb344` | valid state, positive trend |
|
||||
| `info` | `azure #4299e1` | informational alerts |
|
||||
| `warning` | `yellow #f59f00` | warnings |
|
||||
| `danger` | `red #d63939` | errors, invalid state, destructive actions |
|
||||
|
||||
Twelve extra hues (`blue azure indigo purple pink red orange yellow lime green teal cyan`),
|
||||
each with a 100–900 tint/shade ladder (20% steps). Social brand colours are a separate map
|
||||
and are never used for UI chrome.
|
||||
|
||||
### Derived per-colour tokens (generated in `_props.scss`)
|
||||
|
||||
For every theme colour `X`: `--X`, `--X-fg` (auto light/dark foreground), `--X-darken`
|
||||
(hover: 80% colour + 20% transparent), `--X-lt` (10% tint on transparent, used for soft
|
||||
badges/alerts), `--X-200` (20% tint). `--X-rgb` still exists but is 1.x only: phase 2 of the
|
||||
migration plan removes it together with every `rgba(var(--X-rgb))` site, so build with
|
||||
`color-mix()` or `--X-lt` instead.
|
||||
|
||||
### Rules
|
||||
|
||||
- Components read `--body-color`, `--secondary`, `--bg-surface-*`, `--border-*`. They never
|
||||
read a gray directly, except in the token definitions themselves.
|
||||
- No new hex literal outside `_variables.scss` / `_variables-dark.scss`. The `--card-gradient-*` palettes are the one exception (decision #6).
|
||||
- Colour carries meaning. A coloured badge, dot or text says success/warning/danger; colour is
|
||||
not used to decorate. Categorical colour (mail labels, calendar categories, tags) is the one
|
||||
exception: every item in the set gets a colour, as a dot or a soft `-lt` badge, never solid,
|
||||
and the state colours are then not reused for categories.
|
||||
- Links: `--link-color` is primary in light mode, primary tinted 40% white in dark mode (keeps
|
||||
4.5:1). Hover shades 20% in light, tints 20% in dark. No underline at rest, underline on hover.
|
||||
- Text selection: primary at 10% (light) / 40% (dark).
|
||||
- Backdrops: `gray-800` at 68% opacity with a 4px blur in light mode, black in dark.
|
||||
|
||||
---
|
||||
|
||||
## 3. Typography
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| Font family | system stack (`system-ui, -apple-system, Segoe UI, Roboto, …`); no web font |
|
||||
| Monospace | `ui-monospace, SFMono-Regular, Menlo, …` |
|
||||
| Body size / line | `0.875rem` (14px) / `1.25rem` (20px) |
|
||||
| Small / large body | `0.765625rem` (12.25px; `0.75rem` in 2.0, decision #4) / `1.09375rem` (17.5px) |
|
||||
| Weights | 400 body · 500 medium · 600 semibold · 300 display |
|
||||
| Letter spacing | 0 |
|
||||
| Root font size | not set; `rem` follows the user's browser setting (WCAG 1.4.4) |
|
||||
|
||||
### Heading scale (size / line, both on the 4px grid)
|
||||
|
||||
| | Size | Line | Weight |
|
||||
| --- | --- | --- | --- |
|
||||
| h1 | 24px | 32px | 600 |
|
||||
| h2 | 20px | 28px | 600 |
|
||||
| h3 | 16px | 24px | 600 |
|
||||
| h4 | 14px | 20px | 600 |
|
||||
| h5 | 12px | 16px | 600 |
|
||||
| h6 | 10px | 16px | 600 |
|
||||
| display-1 … display-6 | 80 · 72 · 64 · 56 · 48 · 32px | | 300 |
|
||||
|
||||
Heading margin-bottom is `--spacer` (8px). Headings and `strong` share the semibold weight.
|
||||
|
||||
### Where each weight is used
|
||||
|
||||
- **400** body text, lead (lead is body-size in the secondary colour, not larger).
|
||||
- **500** buttons, form labels, badges, `kbd`, card titles.
|
||||
- **600** headings, `strong`, navbar brand, alert links.
|
||||
|
||||
### Component text sizes
|
||||
|
||||
| Element | Size | Weight | Colour |
|
||||
| --- | --- | --- | --- |
|
||||
| Card title | h3 (16px) | 500 | heading colour |
|
||||
| Card subtitle | h4 (14px) when inline | 400 | secondary |
|
||||
| Page title | h2 (20px / 28px) | headings weight | |
|
||||
| Form label | h4 (14px), margin-bottom 8px | 500 | body |
|
||||
| Form hint / feedback | `0.875em`, margin-top 4px | 400 | secondary |
|
||||
| Tooltip / popover body | small (12.25px) | | |
|
||||
| Badge | `0.857em` of parent (sm `0.714em`, lg `1em`) | 500 | |
|
||||
| Sidebar section title | 12px | | |
|
||||
| Code | `0.857em`, `gray-600` on `gray-100` (dark: `gray-400` on `gray-900`) | | |
|
||||
|
||||
---
|
||||
|
||||
## 4. Spacing and sizing
|
||||
|
||||
### Spacing scale
|
||||
|
||||
| Step | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| px | 4 | 8 | 16 | 24 | 32 | 40 | 48 | 64 | 80 | 96 | 112 | 128 |
|
||||
|
||||
Steps 1–6 are the utility classes (`m-*`, `p-*`, `gap-*`); 7–12 exist as `$spacers-extra`.
|
||||
Note the scale skips 12px: the only 12px values in the framework are component paddings
|
||||
(`0.75rem`), not utilities.
|
||||
|
||||
Two "default spacer" tokens exist and disagree: Sass `$spacer` is `1rem` (16px), CSS
|
||||
`--spacer` is `var(--spacer-2)` (8px). Decision #2: `--spacer` becomes 16px in 2.0; write `--spacer-2` when you mean 8px.
|
||||
|
||||
### Control heights (button = input = select)
|
||||
|
||||
Height is line-height + 2 × padding-y + 2px border.
|
||||
|
||||
| Size | Padding y / x | Line | Font | Icon | Height | Radius |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| sm | 5px / 8px | 16px | h5 12px | 16px | **28px** (32px in 2.0, decision #3) | 4px |
|
||||
| default | 9px / 16px | 20px | 14px | 20px | **40px** | 6px |
|
||||
| lg | 11px / 24px | 24px | h3 16px | 24px | **48px** | 8px |
|
||||
| xl | 11px / 28px | 32px | h1 24px | 32px | **56px** | |
|
||||
|
||||
### Other element sizes
|
||||
|
||||
| Element | Sizes |
|
||||
| --- | --- |
|
||||
| Avatar | xxs 16 · xs 20 · sm 32 · **md 40** · lg 48 · xl 80 · 2xl 112 (px); pill radius |
|
||||
| Icon | default 20px · `icon-sm`/inline 16px · `icon-md` 40px · `icon-lg` 56px |
|
||||
| Tag, status | height 24px |
|
||||
| Checkbox / radio | 20px box, checkbox radius 6px |
|
||||
| Status dot | 8px; steps dot 8px; counter dot 24px |
|
||||
| Badge empty dot | 8px |
|
||||
| Loader | 40px |
|
||||
| Progress | 8px (sm 4 · lg 12 · xl 16) |
|
||||
| Close button touch target | at least 24 × 24px |
|
||||
|
||||
### Component padding
|
||||
|
||||
| Component | Padding |
|
||||
| --- | --- |
|
||||
| Card body, card header, list-group item | 20px (`1.25rem`); `.card-sm` 16px, `.card-lg` 32px |
|
||||
| List or table inside a card | `card-list-group` / `card-table` (own borders and padding; never a bare `list-group-flush` in `card-body p-0`) |
|
||||
| Card actions gutter | 8px |
|
||||
| Modal | 24px; footer 12px vertical; header height 56px |
|
||||
| Alert | 12px / 16px, gap 16px |
|
||||
| Accordion | 16px / 20px |
|
||||
| Dropdown item | 8px / 12px; menu min-width 176px; menu padding-y 4px |
|
||||
| Nav link | 8px / 12px |
|
||||
| Table cell | 12px / 12px; header row 8px vertical; `table-sm` 4px |
|
||||
| Toast | 8px / 12px, max 350px |
|
||||
| Tooltip | 4px / 8px, max 200px |
|
||||
| Popover | header 8px / 16px, max 276px |
|
||||
| Pagination link | min-width 32px, gap 4px |
|
||||
| Empty state | 16px (md 48px), icon 48px |
|
||||
|
||||
### Layout chrome
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| Breakpoints | Bootstrap: sm 576 · md 768 · lg 992 · xl 1200 · xxl 1400 |
|
||||
| Containers | 540 · 720 · 960 · 1140 · 1320; `slim` 256 · `tight` 512 · `narrow` 990 |
|
||||
| Page padding | 16px, 8px below `lg`; vertical 24px |
|
||||
| Grid gutter | 16px; card grid gap = page padding |
|
||||
| Navbar height | 56px |
|
||||
| Sidebar | 256px, folded 64px, inset 8px, nav padding 12px, icon 24×20 |
|
||||
| Modal widths | sm 380 · md 540 · lg 720 · xl 1140 (px today, `rem` in 2.0, decision #8) |
|
||||
| Datagrid item | 240px |
|
||||
|
||||
---
|
||||
|
||||
## 5. Shape
|
||||
|
||||
### Radius scale
|
||||
|
||||
| Token | Value | Applied to |
|
||||
| --- | --- | --- |
|
||||
| `xs` | 2px | `avatar-xs`, card status corners |
|
||||
| `sm` | 4px | `btn-sm`, `input-sm`, `pagination-sm` |
|
||||
| `md` (default) | 6px | buttons, inputs, badges, dropdown, tooltip, tags, pills nav, accordion, toast, kbd, checkbox |
|
||||
| `lg` | 8px | cards, modals, popovers, `btn-lg`, `input-lg` |
|
||||
| `xl` / `xxl` | 16px / 32px | utilities only |
|
||||
| `pill` | 100rem | avatars, pill buttons |
|
||||
|
||||
Rule of thumb: **controls 6px, containers 8px, small controls 4px.** A global
|
||||
`--border-radius-scale` multiplier lets users flatten or round the whole UI at once.
|
||||
|
||||
### Borders
|
||||
|
||||
- Width 1px; `wide` 2px (nav-bordered active, steps, modal status, progress ring).
|
||||
- Colour: `--border-color-translucent` for cards, tables, dropdowns, alerts (blends on any
|
||||
background); `--border-color` for navbars, modals headers, list groups, tabs, popovers.
|
||||
- Hover on a control raises the border to `--border-active-color`.
|
||||
|
||||
### Shadows
|
||||
|
||||
| Token | Value | Applied to |
|
||||
| --- | --- | --- |
|
||||
| `xs` | `0 1px 2px rgba(18,18,23,.05)` | **cards, inputs, buttons** (`--shadow-card`, `--shadow-input`) |
|
||||
| `sm` | two layers, ≤ 3px | modal on `xs` screens |
|
||||
| `md` (default `--shadow`) | two layers, ≤ 6px | modals, toasts |
|
||||
| `lg` | two layers, ≤ 15px | popovers, card hover |
|
||||
| `xl` / `2xl` | up to 50px | utilities |
|
||||
| `overlay` | four layers + 1px ring | **dropdowns** |
|
||||
| `border` | 1px ring only | utility |
|
||||
|
||||
Shadow colour is always `rgb(18,18,23)` at 3–25%. Flat surfaces get `xs`; floating
|
||||
surfaces get `overlay` (dropdown) or `lg`/`md` (popover, modal).
|
||||
|
||||
---
|
||||
|
||||
## 6. Interaction states
|
||||
|
||||
Every interactive component expresses the same five states with the same tokens:
|
||||
|
||||
| State | Recipe |
|
||||
| --- | --- |
|
||||
| Hover (list, dropdown, table row) | `$hover-bg` (Sass only, no CSS token yet): secondary at 8%; tables still use their own 7.5% recipe until decision #10 lands |
|
||||
| Hover (button, input) | background unchanged, border to `--border-active-color`; coloured buttons use `--X-darken` |
|
||||
| Active / selected | `--active-bg`: primary at 4% (dark: `$dark` +2%), text `--primary`, border `--primary` |
|
||||
| Focus | ring `0 0 0 2px primary` plus `0 0 0 4px primary @ 25%` — `@include focus-ring()` from `mixins/_mixins.scss`; forced-colors: 2px `Highlight` outline |
|
||||
| Disabled | button opacity 0.4; form-check opacity 0.5; text `--disabled-color` (body @ 40%); bg `--bg-surface-secondary` |
|
||||
|
||||
- The `.btn` default is a white surface with a gray border and `xs` shadow; `btn-primary` is
|
||||
the only filled button on a normal screen. `btn-ghost` is transparent with a gray hover.
|
||||
- Coloured buttons: filled `btn-X`, `btn-outline-X` (transparent, coloured border, fills on
|
||||
hover), `btn-ghost-X` (no border, tints on hover).
|
||||
- Nav variants: `pills` (active = `--active-bg` + primary text), `bordered` (2px primary
|
||||
underline), `tabs` (surface bg + border), `underline` (2px, emphasis colour).
|
||||
- Pagination active is the one place where a filled primary background is used for a
|
||||
selected item.
|
||||
|
||||
---
|
||||
|
||||
## 7. Motion
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| Base | `all 0.2s ease-in-out` |
|
||||
| Fade | `opacity 0.15s linear` |
|
||||
| Collapse | `height 0.35s ease` |
|
||||
| Modal | `transform 0.3s ease-out`, slides down 16px; static backdrop scales 1.02 |
|
||||
| Progress bar | `width 0.6s ease` |
|
||||
| Reveal on hover (`$transition-time`) | `0.3s` — list-group item actions, card options; an opacity fade, not a colour change |
|
||||
| Icon animations | pulse 2s, tada 3s, rotate 3s |
|
||||
|
||||
`prefers-reduced-motion`: decorative loops (blink, pulse, tada, rotate, animated gradient,
|
||||
waves) are removed; loaders and indeterminate progress keep animating but at 3s so state is
|
||||
still visible.
|
||||
|
||||
---
|
||||
|
||||
## 8. Icons
|
||||
|
||||
- Tabler Icons, outline. Stroke follows size: `1.5` up to 24px (`icon-sm`, default, inline), `1` from 40px (`icon-md`, `icon-lg`). Intended, decision #9.
|
||||
- Default colour `gray-400` (`--icon-color`); inside a button the icon inherits text colour.
|
||||
- Inline icon: 16px, `vertical-align: -0.2rem`.
|
||||
- In a button the icon is `--btn-icon-size` with a gap of half the horizontal padding and a
|
||||
negative outer margin of a quarter.
|
||||
- Nav-link icons are the link colour at 50%, 80% on hover.
|
||||
- Filled variant only via `.icon-filled`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Known deviations and their decisions
|
||||
|
||||
Places where the code contradicts its own rules, found on 2026-09-08 and decided by the maintainer the same day. Until a row's fix has landed, a review that touches it reports "known deviation #n (decision: …)", not a new finding, and says whether the change moves toward or away from the decision. When a fix lands, delete the row and update the value in the section it belongs to. Every "fix in 2.0" that changes the value behind an existing class is a value remap under the 2.0 policy: it needs an entry in `BOOTSTRAP-V6-MIGRATION.md` section 5 and an upgrade-guide line. Breakpoints, the spacing scale and the avatar/steps/progress sizes above are the 1.x values; migration questions 4 and 10 and phase 9 may change them, and then this file follows the code.
|
||||
|
||||
| # | Finding | Where | Decision | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `$min-contrast-ratio: 2` picks the auto foreground (`--X-fg`); white on `yellow`, `lime`, `azure` fails WCAG 4.5:1. | `_settings.scss` | **Keep for now.** Decided together with the high-contrast theme, PR #2895. | open, waits for #2895 |
|
||||
| 2 | Sass `$spacer` = 16px but CSS `--spacer` = 8px. Headings use the 8px one, `card-img-overlay` and popover header use the 16px one. | `_variables.scss:341`, `layout/_root.scss` | **Fix in 2.0.** `--spacer` becomes 16px, equal to `$spacer`; headings get an explicit `--spacer-2`. Remap of the shipped `--tblr-spacer`: section 5 entry + upgrade guide. | planned 2.0 |
|
||||
| 3 | Small-element heights do not share a ladder: `btn-sm` 28 · `avatar-sm` 32 · tag/status 24. | buttons, inputs, avatars | **Fix in 2.0.** `btn-sm` (and with it `form-control-sm`, `form-select-sm`) goes to 32px: `$input-btn-padding-y-sm` 5px → 7px. The ladder becomes 24 / 32 / 40 / 48 / 56. Section 5 entry + upgrade guide. | planned 2.0 |
|
||||
| 4 | `$font-size-sm` is 12.25px, off the pixel grid; `h5` is 12px. | `_variables.scss:403` | **Fix in 2.0.** `$font-size-sm: 0.75rem`. Tooltips and popovers drop a quarter pixel. Short section 5 entry. | planned 2.0 |
|
||||
| 5 | Two focus indicators: the ring everywhere, but `outline: 2px solid var(--primary)` in `_buttons.scss:294` and `_calendars.scss:73`. | buttons, calendars | **Fix.** Both go through `@include focus-ring()`; added to the scope of issue #2972. | tracked, #2972 |
|
||||
| 6 | Hex literals outside the variable files: `--card-gradient-*`, `.card-cover` `#666`, `.card-note` `#fff7dd/#fff1c9`, calendar `#66758c` and `#fefeff`. | `ui/_cards.scss`, `ui/_calendars.scss` | **Fix, with one exception.** `.card-note`, `.card-cover` and the calendar get `light-dark()` tokens. The gradients stay literal: they are artwork, nobody rethemes them. | planned, no remap |
|
||||
| 7 | `$dropdown-header-color: $gray-600` and the `$dropdown-dark-*` family are not `light-dark()` pairs. | `_variables.scss` | **Fix in 2.0.** Header colour → `--secondary`. The `$dropdown-dark-*` Sass variables go; `.dropdown-menu-dark` keeps emitting through the dropdown tokens (v5 class, section 5 question 8). | planned 2.0 |
|
||||
| 8 | Tooltip 200px, popover 276px, modals 380/540/720/1140px and toast 350px are the only pixel widths. | `_variables.scss` | **Fix in 2.0.** Move to `rem` (12.5 · 17.25 · 23.75/33.75/45/71.25 · 21.875rem) so overlays scale with the user's font size. Section 5 entry. | planned 2.0 |
|
||||
| 9 | `icon-md` 40px and `icon-lg` 56px drop the stroke to 1; `icon-sm` keeps 1.5. | `ui/_icons.scss` | **Intended.** Rule: stroke 1.5 up to 24px, stroke 1 from 40px. Recorded in section 8. | closed, exception |
|
||||
| 10 | Table hover uses `--emphasis-color` at 7.5% while every other hover uses `$hover-bg` (`--secondary` at 8%). | `_variables.scss` table block | **Fix in 2.0.** `$table-hover-bg: $hover-bg`. Short section 5 entry. | planned 2.0 |
|
||||
---
|
||||
|
||||
## 10. What a review checks (summary)
|
||||
|
||||
1. Colours, radii, shadows, spacing and font sizes come from the tables above. No new literal.
|
||||
2. A component declares its tokens on its root rule — on `v2-dev` through its `$<component>-tokens`
|
||||
map and the `tokens()` mixin — and reads them below; modifiers change tokens, not properties
|
||||
(the mechanics are in `core-scss`).
|
||||
3. Every new colour token is a `light-dark()` pair on `:root` in `layout/_root.scss`.
|
||||
4. Controls are 40px tall by default, 6px radius, `xs` shadow, gray border; only the primary
|
||||
action is filled.
|
||||
5. Hover, active, focus and disabled use the five recipes in section 6, nothing bespoke.
|
||||
6. Flat surfaces: 1px translucent border + `xs` shadow. Floating surfaces: `overlay` / `lg`.
|
||||
7. Everything on the 4px grid; heights from the control ladder; text from the heading scale.
|
||||
8. Colour means state. Decorative colour lives only in explicitly decorative components
|
||||
(card gradients, illustrations).
|
||||
9. Icons: outline, 1.5 stroke, 20px, inherit colour in buttons, `gray-400` elsewhere.
|
||||
10. Motion: 0.15–0.35s, and every decorative animation has a reduced-motion fallback.
|
||||
|
||||
The long form of these, with examples, is [rules-of-thumb.md](rules-of-thumb.md).
|
||||
@@ -0,0 +1,404 @@
|
||||
# Rules of thumb
|
||||
|
||||
Part of the `design-system` skill. Short rules in the style of the Kumo design skill: an
|
||||
imperative title, one sentence on why, a recommended example and one to avoid. Rules 1–9 are
|
||||
Kumo rules that hold for Tabler as they are, 10–14 are Kumo rules adapted to Tabler's
|
||||
choices, 15–24 are Tabler's own. Counts are the state of `preview/pages` and `shared/` on
|
||||
`v2-dev`, 2026-09-08; they show that a rule is not yet met everywhere, so a review reports new
|
||||
violations, not the backlog.
|
||||
|
||||
A review cites a rule by number: "rule 16 — buttons go in `btn-list`".
|
||||
|
||||
### 1. Use 14px for content text
|
||||
|
||||
Body copy, buttons, table cells and form values are 14px. 16px and above belong to headings.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<h3 class="card-title">API tokens</h3>
|
||||
<p>Production token expires in 30 days.</p>
|
||||
|
||||
<!-- Avoid -->
|
||||
<h3 class="card-title">API tokens</h3>
|
||||
<p class="fs-3">Production token expires in 30 days.</p>
|
||||
```
|
||||
|
||||
### 2. Always sentence case headings
|
||||
|
||||
Only the first word and product names take a capital. Title Case reads as marketing, not UI.
|
||||
This covers all UI text: headings, card and page titles, subheaders, buttons, menu items and
|
||||
table headers.
|
||||
Today: "Top Pages", "Payment Method", "Edit Profile" and about ten more card titles in preview.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<h3 class="card-title">Recent requests</h3>
|
||||
<h3 class="card-title">Cloudflare Workers usage</h3>
|
||||
|
||||
<!-- Avoid -->
|
||||
<h3 class="card-title">Recent Requests</h3>
|
||||
<h3 class="card-title text-uppercase">Recent requests</h3>
|
||||
```
|
||||
|
||||
### 3. Never use `fw-bold`
|
||||
|
||||
Headings are semibold (600) and emphasised inline text is medium (500). 700 is not on the
|
||||
weight scale. Today: 12 × `fw-bold` against 25 × `fw-medium` and 8 × `fw-semibold`.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="fw-medium">Account settings</div>
|
||||
<p>This action <span class="fw-medium">is required</span>.</p>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="fw-bold">Account settings</div>
|
||||
<p>This action <strong class="fw-bold">is required</strong>.</p>
|
||||
```
|
||||
|
||||
### 4. Never change letter spacing
|
||||
|
||||
`letter-spacing` is 0 everywhere. The only tracked text is `.subheader`, which the framework
|
||||
styles itself.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="subheader">Worker metrics</div>
|
||||
<h2>Worker metrics</h2>
|
||||
|
||||
<!-- Avoid -->
|
||||
<h2 style="letter-spacing: 0.05em">Worker metrics</h2>
|
||||
<h2 class="text-uppercase tracking-wide">Worker metrics</h2>
|
||||
```
|
||||
|
||||
### 5. Put related text closer together
|
||||
|
||||
A title and its description sit 8px apart; the group sits 16px from the next one. Equal
|
||||
spacing everywhere hides the grouping.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="mb-3">
|
||||
<h3 class="mb-1">Web analytics</h3>
|
||||
<p class="text-secondary mb-0">Measure site traffic without changing your code.</p>
|
||||
</div>
|
||||
<a href="#" class="btn">Configure</a>
|
||||
|
||||
<!-- Avoid -->
|
||||
<h3 class="mb-3">Web analytics</h3>
|
||||
<p class="text-secondary mb-3">Measure site traffic without changing your code.</p>
|
||||
<a href="#" class="btn">Configure</a>
|
||||
```
|
||||
|
||||
### 6. Reduce the font size of inline monospaced text
|
||||
|
||||
Monospace glyphs look larger than the sans at the same size. `code` is `0.857em` in the
|
||||
framework, so use the element rather than a styled span.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<p>Edit <code>wrangler.toml</code> to continue.</p>
|
||||
|
||||
<!-- Avoid -->
|
||||
<p>Edit <span class="font-monospace">wrangler.toml</span> to continue.</p>
|
||||
```
|
||||
|
||||
### 7. Align icons with the first line of text
|
||||
|
||||
An icon next to multi-line text aligns with the first line, not the middle of the block.
|
||||
Inline icons are 16px (`icon-inline`) so they match the x-height.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="d-flex">
|
||||
<Icon name="alert-triangle" class="me-2 mt-1" />
|
||||
<div>API token permissions cannot be changed after creation. Create a new token instead.</div>
|
||||
</div>
|
||||
<p>Runs on <Icon name="bolt" class="icon-inline" /> Workers.</p>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="d-flex align-items-center">
|
||||
<Icon name="alert-triangle" class="me-2" />
|
||||
<div>API token permissions cannot be changed after creation. Create a new token instead.</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 8. Use concentric border radii
|
||||
|
||||
When one rounded box sits inside another with 8px or less between them, the inner radius is
|
||||
the outer radius minus the gap. The framework does this for card headers and dropdown items;
|
||||
hand-written nesting must too.
|
||||
|
||||
```html
|
||||
<!-- Recommended: card 8px, 4px padding, inner element 4px -->
|
||||
<div class="card p-1">
|
||||
<div class="bg-secondary-lt rounded-1 p-3">…</div>
|
||||
</div>
|
||||
|
||||
<!-- Avoid: inner radius equal to the outer one -->
|
||||
<div class="card p-1">
|
||||
<div class="bg-secondary-lt rounded-2 p-3">…</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 9. Use a border to separate sticky elements
|
||||
|
||||
A sticky table head or toolbar gets a 1px border on the edge that meets the content. A
|
||||
shadow there reads as a floating surface, which it is not. Today: 6 × `sticky-top` in preview.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="card-header sticky-top bg-surface border-bottom">…</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card-header sticky-top bg-surface shadow-sm">…</div>
|
||||
```
|
||||
|
||||
### 10. Hover feedback within 150ms
|
||||
|
||||
Kumo forbids colour transitions on hover. Tabler keeps them, but at `0.15s` only, because
|
||||
that is fast enough to feel instant. Never put `--transition-base` (0.2s, `all`) on a hover.
|
||||
An opacity reveal (actions that appear on hover) is not a colour change and uses
|
||||
`$transition-time` (0.3s).
|
||||
|
||||
```scss
|
||||
// Recommended
|
||||
.my-item:hover { background: $hover-bg; }
|
||||
.my-item { transition: background-color 0.15s ease-in-out; }
|
||||
|
||||
// Avoid
|
||||
.my-item { transition: all 0.2s ease-in-out; }
|
||||
```
|
||||
|
||||
### 11. Flat surfaces get a border plus the `xs` shadow, nothing more
|
||||
|
||||
Kumo says never combine a border with a shadow. Tabler's card is exactly that pair, on
|
||||
purpose: the border draws the edge, the 1px `xs` shadow lifts it off the gray page. Anything
|
||||
stronger belongs to floating surfaces.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="card">…</div>
|
||||
<div class="dropdown-menu">…</div> <!-- overlay shadow, it floats -->
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card shadow-lg">…</div>
|
||||
<div class="card border-0 shadow">…</div>
|
||||
```
|
||||
|
||||
### 12. Never nest a card inside a card body
|
||||
|
||||
Two borders and two paddings in a row look like a mistake. Kumo's rule is "never stack";
|
||||
Tabler has `.card-stacked` for the deliberate stack, so the rule is about nesting. A list in a
|
||||
card is `card-list-group`, a table is `card-table`; both keep the card's edge and padding.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Recent requests</h3></div>
|
||||
<div class="table-responsive"><table class="table card-table">…</table></div>
|
||||
</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card"><div class="card-body"><table class="table">…</table></div></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 13. Modals live in the DOM and open with `data-bs-toggle`
|
||||
|
||||
Kumo's "never conditionally render dialogs" becomes: never inject a modal with a script or
|
||||
toggle it with `display`. The framework owns the fade, the backdrop and the focus trap.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modal-edit">Edit</a>
|
||||
<div class="modal" id="modal-edit" tabindex="-1">…</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<a href="#" class="btn" onclick="document.getElementById('modal-edit').style.display='block'">Edit</a>
|
||||
```
|
||||
|
||||
### 14. Maintain content size during collapse animations
|
||||
|
||||
A collapsing element animates `height` only. If the content inside reflows to the new
|
||||
width mid-animation, text jumps. Fix the inner width, not the animation.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="collapse" id="details">
|
||||
<div class="card-body">…</div>
|
||||
</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="collapse" id="details">
|
||||
<div class="card-body w-auto d-flex flex-wrap">…</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 15. Only one filled button per view
|
||||
|
||||
`btn-primary` marks the single main action. Everything else is the plain `btn`, so the eye
|
||||
finds the primary without reading.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn">Cancel</a>
|
||||
<a href="#" class="btn btn-primary">Save changes</a>
|
||||
</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn btn-secondary">Cancel</a>
|
||||
<a href="#" class="btn btn-primary">Save changes</a>
|
||||
<a href="#" class="btn btn-success">Publish</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 16. Buttons go in `btn-list`, badges in `badge-list`, avatars in `avatar-list`
|
||||
|
||||
The wrappers own the gap and the wrapping. Hand-written `me-2` breaks at the last item and
|
||||
in RTL. Today: 57 × `me-2` against 5 × `btn-list`.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn">Export</a>
|
||||
<a href="#" class="btn btn-primary">Create</a>
|
||||
</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<a href="#" class="btn me-2">Export</a>
|
||||
<a href="#" class="btn btn-primary">Create</a>
|
||||
```
|
||||
|
||||
### 17. Use `text-secondary`, never `text-muted`
|
||||
|
||||
One class for secondary text. `text-muted` is a Bootstrap alias kept for compatibility.
|
||||
Today: 254 × `text-secondary`, 18 × `text-muted`. `<Icon color="muted">` still emits
|
||||
`text-muted` and is documented that way; it is not a finding until the prop is remapped.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="text-secondary">Last deployed 4 minutes ago</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="text-muted">Last deployed 4 minutes ago</div>
|
||||
```
|
||||
|
||||
### 18. No inline `style` for sizes
|
||||
|
||||
Sizes come from utilities or a component token, so they follow the scale and the theme.
|
||||
Today: 75 inline `style` attributes in preview, among them 8 × `height: 8rem` and
|
||||
`width: 350px`. The exception is SVG illustration fills, which are data, not layout.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="img-responsive img-responsive-21x9 card-img-top" style="background-image: url(…)"></div>
|
||||
<div class="chart chart-lg">…</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card-img-top" style="height: 8rem"></div>
|
||||
<div class="chart" style="height: 128px; width: 350px">…</div>
|
||||
```
|
||||
|
||||
### 19. Vertical rhythm is `mb-3` inside a card and `row-cards` between cards
|
||||
|
||||
One spacing step per level. Mixing `mb-3`, `mb-4` and `mt-3` in the same block gives uneven
|
||||
gaps nobody chose. Today: 142 × `mb-3`, 76 × `mb-4`, 31 × `mt-3`, 31 × `mt-4`.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6"><div class="card"><div class="card-body">
|
||||
<div class="mb-3">…</div>
|
||||
<div class="mb-3">…</div>
|
||||
<div>…</div>
|
||||
</div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card mb-4"><div class="card-body">
|
||||
<div class="mb-3">…</div>
|
||||
<div class="mt-4">…</div>
|
||||
<div class="mb-2 mt-3">…</div>
|
||||
</div></div>
|
||||
```
|
||||
|
||||
### 20. Colour means state
|
||||
|
||||
A coloured badge, dot or text says success, warning or danger. Colour used for decoration
|
||||
makes real states invisible. Categorical colour (mail labels, calendar categories, tags) is
|
||||
allowed when every item in the set gets one, as a dot or a soft `-lt` badge, never solid,
|
||||
and the state colours are not reused for categories.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<span class="badge bg-success-lt">Active</span>
|
||||
<span class="badge bg-danger-lt">Failed</span>
|
||||
<span class="badge">Draft</span>
|
||||
|
||||
<!-- Avoid: a state colour on a category, and a solid badge for decoration -->
|
||||
<span class="badge bg-success-lt">Marketing</span>
|
||||
<span class="badge bg-purple">Sales</span>
|
||||
```
|
||||
|
||||
### 21. Icons inherit colour inside controls, `gray-400` elsewhere
|
||||
|
||||
A button's icon is the button's text colour. A stand-alone icon is `--icon-color`.
|
||||
Colouring an icon separately makes it read as a second control.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<a href="#" class="btn btn-primary"><Icon name="plus" /> Create</a>
|
||||
<Icon name="search" class="text-secondary" />
|
||||
|
||||
<!-- Avoid -->
|
||||
<a href="#" class="btn"><Icon name="plus" class="text-primary" /> Create</a>
|
||||
```
|
||||
|
||||
### 22. Uppercase only in `.subheader` and list-group headers
|
||||
|
||||
The framework uppercases `.subheader`, `.list-group-header`, avatar initials, ribbons and the
|
||||
pagination "page" label. Nothing else. Today: 9 × `text-uppercase` in preview to review.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<div class="subheader">Total revenue</div>
|
||||
<div class="list-group-header">Today</div>
|
||||
|
||||
<!-- Avoid -->
|
||||
<h3 class="card-title text-uppercase">Total revenue</h3>
|
||||
<th class="text-uppercase">Status</th>
|
||||
```
|
||||
|
||||
### 23. Empty states use `.empty`
|
||||
|
||||
The component gives the icon, title, hint and action the same spacing on every page. A
|
||||
hand-written "No data" paragraph does not.
|
||||
|
||||
```html
|
||||
<!-- Recommended -->
|
||||
<Empty icon="database-off" title="No results found" description="Try adjusting your search or filter.">
|
||||
<a href="#" class="btn btn-primary">Clear filters</a>
|
||||
</Empty>
|
||||
|
||||
<!-- Avoid -->
|
||||
<div class="card-body text-center text-secondary py-5">No data</div>
|
||||
```
|
||||
|
||||
### 24. Every `light-dark()` pair is checked in both modes before merge
|
||||
|
||||
A new colour token is not done when light mode looks right. Capture the page with
|
||||
`screenshots/` (both modes come out of one run) or toggle `data-bs-theme="dark"` in the
|
||||
preview and compare.
|
||||
|
||||
```scss
|
||||
// Recommended
|
||||
--note-bg: light-dark(var(--yellow-lt), color-mix(in oklab, var(--yellow) 15%, transparent));
|
||||
|
||||
// Avoid
|
||||
--note-bg: #fff7dd;
|
||||
```
|
||||
Reference in new issue
Block a user