@mohammadhprp/system-prompt 0.11.2 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/framework/agents/researcher.md +1 -1
- package/framework/mcps/github-mcp/README.md +1 -1
- package/framework/mcps/github-mcp/configs/.env.example +1 -1
- package/framework/plugins/ponytail/README.md +1 -1
- package/framework/plugins/ponytail/capabilities.md +1 -1
- package/framework/references/standards/pull-requests.md +1 -1
- package/framework/skills/README.md +3 -3
- package/framework/skills/diagram-design/SKILL.md +1 -1
- package/framework/skills/effective-html/SKILL.md +63 -0
- package/framework/skills/effective-html/examples.md +19 -0
- package/framework/skills/effective-html/references/charts-and-data.md +32 -0
- package/framework/skills/effective-html/references/creative-direction.md +48 -0
- package/framework/skills/effective-html/references/design-artifact.md +78 -0
- package/framework/skills/effective-html/references/diagrams.md +68 -0
- package/framework/skills/effective-html/references/documents-and-presentations.md +28 -0
- package/framework/skills/effective-html/references/html-diagram.md +43 -0
- package/framework/skills/effective-html/references/html-plan.md +40 -0
- package/framework/skills/effective-html/references/html-prototype.md +97 -0
- package/framework/skills/effective-html/references/html-wireframe.md +81 -0
- package/framework/skills/effective-html/references/html.md +72 -0
- package/framework/skills/effective-html/references/interfaces.md +17 -0
- package/framework/skills/great-interface/SKILL.md +43 -0
- package/framework/skills/great-interface/references/animations.md +205 -0
- package/framework/skills/great-interface/references/better-accessibility.md +106 -0
- package/framework/skills/great-interface/references/better-colors.md +100 -0
- package/framework/skills/great-interface/references/better-interface.md +132 -0
- package/framework/skills/great-interface/references/better-layout.md +76 -0
- package/framework/skills/great-interface/references/better-typography.md +157 -0
- package/framework/skills/great-interface/references/better-ui.md +107 -0
- package/framework/skills/great-interface/references/better-writing.md +109 -0
- package/framework/skills/great-interface/references/choosing-fonts.md +64 -0
- package/framework/skills/great-interface/references/color-formats.md +90 -0
- package/framework/skills/great-interface/references/color-usage.md +118 -0
- package/framework/skills/great-interface/references/contrast.md +79 -0
- package/framework/skills/great-interface/references/css-cheat-sheet.md +65 -0
- package/framework/skills/great-interface/references/details-and-accessibility.md +119 -0
- package/framework/skills/great-interface/references/enter-exit.md +147 -0
- package/framework/skills/great-interface/references/explain-interface.md +126 -0
- package/framework/skills/great-interface/references/find-the-effect.md +94 -0
- package/framework/skills/great-interface/references/focus-and-keyboard.md +131 -0
- package/framework/skills/great-interface/references/forms.md +84 -0
- package/framework/skills/great-interface/references/from-an-image.md +55 -0
- package/framework/skills/great-interface/references/grouping-and-alignment.md +123 -0
- package/framework/skills/great-interface/references/hit-areas.md +94 -0
- package/framework/skills/great-interface/references/icon-transitions.md +102 -0
- package/framework/skills/great-interface/references/icons.md +110 -0
- package/framework/skills/great-interface/references/interface-review.md +148 -0
- package/framework/skills/great-interface/references/motion-and-zoom.md +79 -0
- package/framework/skills/great-interface/references/no-browser.md +73 -0
- package/framework/skills/great-interface/references/palette-generation.md +104 -0
- package/framework/skills/great-interface/references/palette-structure.md +76 -0
- package/framework/skills/great-interface/references/performance.md +88 -0
- package/framework/skills/great-interface/references/picker.md +76 -0
- package/framework/skills/great-interface/references/read-the-system.md +178 -0
- package/framework/skills/great-interface/references/removed-signals.md +38 -0
- package/framework/skills/great-interface/references/review-format.md +46 -0
- package/framework/skills/great-interface/references/scope-resolution.md +88 -0
- package/framework/skills/great-interface/references/screen-readers.md +101 -0
- package/framework/skills/great-interface/references/semantics-and-aria.md +84 -0
- package/framework/skills/great-interface/references/spacing-and-adaptivity.md +159 -0
- package/framework/skills/great-interface/references/spacing-and-sizing.md +121 -0
- package/framework/skills/great-interface/references/surfaces.md +219 -0
- package/framework/skills/great-interface/references/token-naming.md +97 -0
- package/framework/skills/great-interface/references/variable-fonts-and-opentype.md +105 -0
- package/framework/skills/great-interface/references/variant.md +104 -0
- package/framework/skills/great-interface/references/wrapping-and-punctuation.md +55 -0
- package/framework/skills/taste/SKILL.md +47 -0
- package/framework/skills/taste/examples.md +73 -0
- package/framework/skills/taste/references/brandkit.md +793 -0
- package/framework/skills/taste/references/design-taste-frontend-v1.md +221 -0
- package/framework/skills/taste/references/design-taste-frontend.md +1201 -0
- package/framework/skills/taste/references/full-output-enforcement.md +44 -0
- package/framework/skills/taste/references/gpt-taste.md +69 -0
- package/framework/skills/taste/references/high-end-visual-design.md +93 -0
- package/framework/skills/taste/references/image-to-code.md +1223 -0
- package/framework/skills/taste/references/imagegen-frontend-mobile.md +1460 -0
- package/framework/skills/taste/references/imagegen-frontend-web.md +982 -0
- package/framework/skills/taste/references/industrial-brutalist-ui.md +87 -0
- package/framework/skills/taste/references/minimalist-ui.md +80 -0
- package/framework/skills/taste/references/redesign-existing-projects.md +173 -0
- package/framework/skills/taste/references/stitch-design-taste-design.md +121 -0
- package/framework/skills/taste/references/stitch-design-taste.md +179 -0
- package/package.json +1 -1
- package/src/agent-configs.js +6 -2
- package/src/catalog.js +3 -2
- package/src/cli.js +82 -2
- package/src/doctor.js +84 -0
- package/src/installer.js +337 -154
- package/framework/skills/brand-guidelines/LICENSE.txt +0 -202
- package/framework/skills/brand-guidelines/SKILL.md +0 -73
- package/framework/skills/lavish/SKILL.md +0 -67
- package/framework/skills/lavish/examples.md +0 -31
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Reading a site without a browser
|
|
2
|
+
|
|
3
|
+
Fetch the HTML, then the stylesheets it links, then grep. That answers most questions a scriptable browser answers and a few it cannot.
|
|
4
|
+
|
|
5
|
+
Do not use a markdown-converting fetch for this. It strips exactly what you came for. Fetch the raw bytes.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -sL --max-time 25 "$URL" -o page.html
|
|
9
|
+
wc -c page.html
|
|
10
|
+
grep -oE 'href="[^"]*\.css[^"]*"' page.html | sed 's/href="//;s/"$//' | sort -u
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Pull each stylesheet the same way, resolving protocol-relative and root-relative hrefs against the page's origin first.
|
|
14
|
+
|
|
15
|
+
## Utility CSS is self-describing
|
|
16
|
+
|
|
17
|
+
Where the site uses utility classes, the markup already contains the declarations and no stylesheet lookup is needed. Grep the class attribute for the effect:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
grep -oE '(backdrop-)?blur-\[[^]]*\]|(backdrop-)?blur-[a-z0-9]+' page.html | sort | uniq -c | sort -rn
|
|
21
|
+
grep -oE 'class="[^"]*(gradient|blur|mask|mix-blend)[^"]*"' page.html | head -20
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This is where the fetch method beats a browser. A class list carries every responsive and state variant at once, so `blur-[50px] md:h-214 md:-translate-x-1/2` says the element changes shape at the `md` breakpoint. Computed styles read at one width cannot.
|
|
25
|
+
|
|
26
|
+
Semantic CSS gives you a hashed class name instead (`Hero_glow__a1b2c`). Take that name to the stylesheet and grep it there.
|
|
27
|
+
|
|
28
|
+
## Inline styles carry the values utilities cannot express
|
|
29
|
+
|
|
30
|
+
A multi-stop gradient is usually too specific for a utility, so it lands in a `style` attribute:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
grep -oE 'linear-gradient\([^)]*\)' page.html | sort -u | head
|
|
34
|
+
grep -oE 'radial-gradient\([^)]*\)' page.html | sort -u | head
|
|
35
|
+
grep -oE 'style="[^"]*(transform|filter|mask)[^"]*"' page.html | head
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The stylesheet, for tokens and generated utilities
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
grep -oE ':root\{[^}]*\}' style.css | head -1 | tr ';' '\n' # the token layer
|
|
42
|
+
grep -oE '@layer [a-z]+' style.css | sort -u # Tailwind v4 emits theme/base/components/utilities
|
|
43
|
+
grep -oE '@media[^{]*\(m(in|ax)-width:[^)]*\)' style.css | sort -u # real breakpoints
|
|
44
|
+
grep -oE '@font-face\{[^}]*\}' style.css | head # families, weights, formats
|
|
45
|
+
grep -oE '@keyframes [a-zA-Z-]+' style.css | sort -u # named animations
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To understand a custom utility, grep its class name in the stylesheet and read the declaration whole. That is how `gradient-ease-in-out` turns into its mechanism, a generated stop list built with `color-mix()` and relative color syntax rather than twelve hand-written stops.
|
|
49
|
+
|
|
50
|
+
## Stack fingerprints from the HTML alone
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
grep -ocE '__NEXT_DATA__|/_next/static' page.html # Next.js
|
|
54
|
+
grep -oc 'self.__next_f' page.html # App Router with RSC payload
|
|
55
|
+
grep -ocE '__NUXT__|/_nuxt/' page.html # Nuxt
|
|
56
|
+
grep -ocE '__remixContext|___gatsby|astro-island' page.html # Remix, Gatsby, Astro
|
|
57
|
+
grep -oc 'class="[^"]*svelte-' page.html # Svelte
|
|
58
|
+
grep -oc 'data-radix-' page.html # Radix primitives
|
|
59
|
+
grep -oc 'bg-linear-to' page.html # Tailwind v4 (v3 wrote bg-gradient-to)
|
|
60
|
+
grep -oE '<meta name="generator"[^>]*>' page.html
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Report these as fingerprints with the evidence that produced them, never as facts. `/_next/static` in an asset path is strong; a utility-looking class name alone is weak.
|
|
64
|
+
|
|
65
|
+
## What this method cannot tell you
|
|
66
|
+
|
|
67
|
+
Say so rather than guessing past it:
|
|
68
|
+
|
|
69
|
+
- **Which rule won.** Nine rules may match one element; only a browser resolves the cascade.
|
|
70
|
+
- **Anything injected at runtime.** CSS-in-JS, a theme applied by script, styles added on interaction.
|
|
71
|
+
- **Paint order and what is actually visible.** A declaration in the CSS may be overridden or never rendered.
|
|
72
|
+
- **Live animation state.** Whether an effect moves at all.
|
|
73
|
+
- **Computed values.** A `rem` stays a `rem`, and you never learn the resolved pixel size.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Palette generation
|
|
2
|
+
|
|
3
|
+
Producing the values once the structure is decided. For which ramps to build and what each step is for, see [palette-structure.md](palette-structure.md).
|
|
4
|
+
|
|
5
|
+
## Start from the brand color
|
|
6
|
+
|
|
7
|
+
A brand color arrives as one value, usually a hex. Two decisions come before any ramp exists:
|
|
8
|
+
|
|
9
|
+
**Which step does it occupy?** A brand color meant for buttons and links belongs on the solid-fill step: `500` in Tailwind, `9` in Radix. Then `bg-brand-500` renders the actual brand color, not an approximation.
|
|
10
|
+
|
|
11
|
+
**Is it pinned or snapped?** Pin a contractually fixed brand color. It stays exact, the ramp builds outward from it and that one step spaces slightly unevenly. Otherwise snap it onto the ramp so every step spaces evenly. That looks better almost always, and nobody notices without a swatch held to the screen.
|
|
12
|
+
|
|
13
|
+
A brand color that fails contrast behind white text is still the brand color, just not the solid-fill step. Put it where it lands and use a darker step for interactive fills. Never quietly darken the brand.
|
|
14
|
+
|
|
15
|
+
## What a correct ramp looks like
|
|
16
|
+
|
|
17
|
+
Properties of the finished ramp, checkable against any output in any notation:
|
|
18
|
+
|
|
19
|
+
- **Steps are evenly spaced in perceived lightness.** Not in the number your format calls "lightness". HSL's is not perceptual, and evenly spaced HSL values bunch at one end.
|
|
20
|
+
- **Hue is constant end to end.** Every step is recognisably the same color. A wandering hue reads as two colors blended and will not sit correctly against a neutral built on a different hue.
|
|
21
|
+
- **Vividness peaks in the middle and falls off at both ends.** The lightest and darkest steps are nearly neutral; the middle carries the color. Holding full vividness into the extremes gives a `50` that glows and a `950` like ink spilled on the brand.
|
|
22
|
+
- **Steps are denser at the light end.** Light backgrounds need finer distinctions than dark ones. Keep `50` to `200` close together and `800` to `950` further apart. Even spacing across the whole range makes the pale end unusable, because `50` and `100` stop reading as two surfaces.
|
|
23
|
+
- **No two adjacent steps are indistinguishable.** If `200` and `300` look identical on a calibrated screen, the ramp has more steps than decisions. Drop one.
|
|
24
|
+
- **Both ends stop short of pure black and white.** A ramp that reaches them loses its identity exactly where the page background lives.
|
|
25
|
+
|
|
26
|
+
## Use a color library
|
|
27
|
+
|
|
28
|
+
Never compute these by hand or by eye. `culori`, `colorjs.io` and `chroma.js` all convert between notations, measure perceived lightness and interpolate perceptually. Read the brand color in whatever format it arrives, do the math in a perceptual space and emit the project's notation:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { formatHex, interpolate, samples } from 'culori'
|
|
32
|
+
|
|
33
|
+
// Perceptual interpolation, hex in and hex out.
|
|
34
|
+
const ramp = interpolate(['#eff6ff', '#3b82f6', '#172554'], 'lab')
|
|
35
|
+
const steps = samples(11).map((t) => formatHex(ramp(t)))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The output format is the project's choice. For a ramp the interpolation space is not, because the steps have to land evenly in perceived lightness and sRGB interpolation produces muddy mid-steps. Decorative gradients are the opposite case, where the space is a deliberate look ([color-usage.md](color-usage.md)).
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
:root {
|
|
42
|
+
--brand-50: #eff6ff;
|
|
43
|
+
--brand-100: #dbeafe;
|
|
44
|
+
--brand-200: #bfdbfe;
|
|
45
|
+
--brand-300: #93c5fd;
|
|
46
|
+
--brand-400: #60a5fa;
|
|
47
|
+
--brand-500: #3b82f6;
|
|
48
|
+
--brand-600: #2563eb;
|
|
49
|
+
--brand-700: #1d4ed8;
|
|
50
|
+
--brand-800: #1e40af;
|
|
51
|
+
--brand-900: #1e3a8a;
|
|
52
|
+
--brand-950: #172554;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Several hues at once
|
|
57
|
+
|
|
58
|
+
With an accent plus status ramps, the ramps must agree step for step. `danger-500` and `brand-500` should read as equally bright and vivid, or a red button looks heavier than a blue one at the same step.
|
|
59
|
+
|
|
60
|
+
- **Match perceived lightness exactly.** Same step, same brightness, across every hue.
|
|
61
|
+
- **Match vividness relatively, not absolutely.** Hues do not share a maximum vividness. A saturated yellow and a saturated blue are not equally far from gray, and no format makes them so. Set each ramp to the same *proportion* of what its own hue reaches. Copying a saturation number across hues leaves one washed out.
|
|
62
|
+
|
|
63
|
+
Yellows and cyans are the usual casualties, peaking much lower than reds and blues. Copy the numbers across and the warning color looks weak beside the danger one.
|
|
64
|
+
|
|
65
|
+
## Dark mode
|
|
66
|
+
|
|
67
|
+
A dark palette is not the light one reversed. Reversal is the starting point, not the output.
|
|
68
|
+
|
|
69
|
+
Swap the semantic roles first, then tune the values:
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
:root {
|
|
73
|
+
--color-bg: var(--brand-50);
|
|
74
|
+
--color-text: var(--brand-950);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dark {
|
|
78
|
+
--color-bg: var(--brand-950);
|
|
79
|
+
--color-text: var(--brand-50);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Three things almost always need hand-tuning after the swap:
|
|
84
|
+
|
|
85
|
+
- **Vividness comes down.** A color that reads as confident on white reads as neon on near-black. Dark appearances need the accent a step or two less vivid.
|
|
86
|
+
- **The dark end needs more separation.** Steps distinguishable as pale backgrounds collapse into each other as dark surfaces.
|
|
87
|
+
- **Contrast does not survive the mirror.** A pair passing in light mode can fail reversed, because contrast is not symmetric. Recheck every foreground against its real background in both appearances ([contrast.md](contrast.md)).
|
|
88
|
+
|
|
89
|
+
### Choosing the switching mechanism
|
|
90
|
+
|
|
91
|
+
Pick one and use it throughout:
|
|
92
|
+
|
|
93
|
+
- **`prefers-color-scheme` alone** is correct with no theme toggle. Nothing to persist, nothing to hydrate.
|
|
94
|
+
- **A `.dark` class** is required as soon as users can override the system setting. The media query then sets only the initial value.
|
|
95
|
+
- **`light-dark()`** collapses both values into one declaration, the least code when the project also sets `color-scheme`. It reads that property rather than a class, so a class-based toggle must set `color-scheme` too.
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
:root {
|
|
99
|
+
color-scheme: light dark;
|
|
100
|
+
--color-bg: light-dark(#ffffff, #172554);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Mixing mechanisms is the common failure. A media query setting some tokens and a class setting others gives a half-themed interface the moment a user overrides their system preference.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Palette structure
|
|
2
|
+
|
|
3
|
+
What a color system is made of, before any values exist. For computing the values see [palette-generation.md](palette-generation.md); for what to call them see [token-naming.md](token-naming.md).
|
|
4
|
+
|
|
5
|
+
## What a system needs
|
|
6
|
+
|
|
7
|
+
Most products need three kinds of ramp and nothing else:
|
|
8
|
+
|
|
9
|
+
| Ramp | How many | Notes |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Neutral | 1 | Carries 80–90% of the interface: backgrounds, borders, body text |
|
|
12
|
+
| Accent | 1 | The brand hue. Interactive and selected states |
|
|
13
|
+
| Status | 0–4 | `danger`, `warning`, `success`, `info`. Add one only when the product shows that state |
|
|
14
|
+
|
|
15
|
+
A second accent hue must also never sit adjacent to the first. Where it would, the accent ramp's own steps provide the range.
|
|
16
|
+
|
|
17
|
+
## Every step has a job
|
|
18
|
+
|
|
19
|
+
Each step maps to a role. Generate the steps the roles below call for and skip the rest.
|
|
20
|
+
|
|
21
|
+
| Role | Tailwind | Radix |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Page background | `50` | `1` |
|
|
24
|
+
| Subtle background | `50` | `2` |
|
|
25
|
+
| Component background | `100` | `3` |
|
|
26
|
+
| Component hover | `200` | `4` |
|
|
27
|
+
| Component active / selected | `200` | `5` |
|
|
28
|
+
| Subtle border | `200` | `6` |
|
|
29
|
+
| Border, separator | `300` | `7` |
|
|
30
|
+
| Strong border, focus ring | `400` | `8` |
|
|
31
|
+
| Solid fill | `500` | `9` |
|
|
32
|
+
| Solid fill hover | `600` | `10` |
|
|
33
|
+
| Low-contrast text | `700` | `11` |
|
|
34
|
+
| High-contrast text | `900` | `12` |
|
|
35
|
+
|
|
36
|
+
The two conventions differ in kind, not only in numbering:
|
|
37
|
+
|
|
38
|
+
- **Radix defines its 12 steps by role.** Step 9 is "the solid fill" in every ramp and appearance. The dark scale is a separate ramp reusing the same numbers, so `--accent-9` is the fill in both and component CSS never changes.
|
|
39
|
+
- **Tailwind defines its 11 steps by lightness.** `50` is light, `950` is dark. The mapping above therefore holds in light mode and inverts in dark, with the page background at `950` and high-contrast text at `50`. Components either swap step numbers per appearance or read a semantic token that swaps once.
|
|
40
|
+
|
|
41
|
+
Match whichever the project uses. For a new system prefer Radix's model, because a role-defined step survives a theme change that a lightness-defined step does not. On Tailwind, keep `50`–`950` and put the role mapping in the semantic tier.
|
|
42
|
+
|
|
43
|
+
Tailwind's 11 steps cover 12 roles, so some do double duty. Where the table repeats a step, the two roles are adjacent in practice and the collision is real. A design needing a subtle border and a component hover to be distinguishable needs a 12-step ramp.
|
|
44
|
+
|
|
45
|
+
## Neutrals
|
|
46
|
+
|
|
47
|
+
A pure gray ramp is a perfectly good default. It sits under any accent hue without competing and never needs revisiting when the brand color changes.
|
|
48
|
+
|
|
49
|
+
Tinting the neutral toward the accent hue is a stylistic option, not a correction. A trace of the accent, a few percent of its vividness, puts the greys in the same family as the accent rather than merely coexisting with it. Enough to measure, not enough to name. Neither choice reads as a mistake.
|
|
50
|
+
|
|
51
|
+
Warm neutrals, hue toward orange, read approachable and editorial; cool ones, toward blue, read technical and precise. Whichever you pick, including none, hold it across the whole ramp. A warm gray border on a cool gray background is visible even when neither color is nameable alone.
|
|
52
|
+
|
|
53
|
+
Neutrals carry the most roles, so they need the most steps. Never generate fewer neutral steps than accent steps.
|
|
54
|
+
|
|
55
|
+
## Status colors
|
|
56
|
+
|
|
57
|
+
Convention constrains status hues before taste does. Red reads as danger, amber as warning, green as success. See the cultural exceptions in [color-usage.md](color-usage.md).
|
|
58
|
+
|
|
59
|
+
Two rules govern them:
|
|
60
|
+
|
|
61
|
+
- **Keep every status hue distinct from the accent.** If the brand is red, the danger ramp cannot also be red. Move danger toward a deeper crimson and check the two side by side, or the destructive and primary actions are the same button.
|
|
62
|
+
- **Status ramps need fewer steps than the accent.** Most render four roles: a background, a border, a solid fill and text. Generate the full ramp only where the product styles status components across the whole range.
|
|
63
|
+
|
|
64
|
+
Status color is never the only signal of a state change; pair it with an icon or text. `better-accessibility` owns that requirement.
|
|
65
|
+
|
|
66
|
+
## Auditing an existing palette
|
|
67
|
+
|
|
68
|
+
Before restructuring a system, inventory it. Most codebases hold several times more colors than the design has decisions.
|
|
69
|
+
|
|
70
|
+
1. **Collect every literal.** Grep for hex, `rgb(`, `hsl(`, `oklch(` and the project's utility-class prefixes. Include SVG `fill`/`stroke`, chart configs and email templates. Colors hide outside stylesheets.
|
|
71
|
+
2. **Sort by perceived lightness within each hue family.** Duplicates surface immediately as near-identical neighbors.
|
|
72
|
+
3. **Collapse near-duplicates.** Two colors closer than about one ramp step are one color that drifted. Keep the one used most and retire the others. Never average them.
|
|
73
|
+
4. **Assign each survivor a role** from the table above. A color matching no role is a missing token or a mistake. Decide which, and say so in the finding.
|
|
74
|
+
5. **Count what is left.** More than one ramp per role above means the palette outgrew its structure, not that the product needs more color.
|
|
75
|
+
|
|
76
|
+
Report the inventory before changing anything. Consolidating a palette changes rendered output on screens nobody asked you to touch, so it stays a proposal until the user accepts it.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
Transition specificity and GPU compositing hints.
|
|
4
|
+
|
|
5
|
+
## Transition only what changes
|
|
6
|
+
|
|
7
|
+
Never use `transition: all` or Tailwind's `transition-all`. Always name the exact properties that change. Tailwind's bare `transition` maps to a curated list of colors, opacity, shadow and transforms rather than `all`, and naming what changes is still better.
|
|
8
|
+
|
|
9
|
+
### Why
|
|
10
|
+
|
|
11
|
+
- `transition: all` forces the browser to watch every property for changes
|
|
12
|
+
- Causes unexpected transitions on properties you didn't intend to animate (colors, padding, shadows)
|
|
13
|
+
- Prevents browser optimizations
|
|
14
|
+
|
|
15
|
+
### CSS example
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
/* Good: only transition what changes */
|
|
19
|
+
.button {
|
|
20
|
+
transition-property: scale, background-color;
|
|
21
|
+
transition-duration: 150ms;
|
|
22
|
+
transition-timing-function: ease-out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Bad: transition everything */
|
|
26
|
+
.button {
|
|
27
|
+
transition: all 150ms ease-out;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Tailwind
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Good: explicit properties
|
|
35
|
+
<button className="transition-[scale,background-color] duration-150 ease-out">
|
|
36
|
+
|
|
37
|
+
// Bad: transition all
|
|
38
|
+
<button className="transition-all duration-150 ease-out">
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Tailwind `transition-transform` note
|
|
42
|
+
|
|
43
|
+
`transition-transform` in Tailwind maps to `transition-property: transform, translate, scale, rotate`, covering every transform-related property rather than only `transform`. Use it when animating transforms alone. For several non-transform properties, use the bracket syntax `transition-[scale,opacity,filter]`.
|
|
44
|
+
|
|
45
|
+
## Use `will-change` sparingly
|
|
46
|
+
|
|
47
|
+
`will-change` hints the browser to pre-promote an element to its own GPU compositing layer. Without it the browser promotes only when the animation starts, and that one-time promotion can cause a micro-stutter on the first frame.
|
|
48
|
+
|
|
49
|
+
It helps most for `scale`, `rotation` and movement through `transform`. For other properties it does little, because the browser cannot composite them on the GPU anyway.
|
|
50
|
+
|
|
51
|
+
### Rules
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
/* Good: specific property that benefits from GPU compositing */
|
|
55
|
+
.animated-card {
|
|
56
|
+
will-change: transform;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Good: multiple compositor-friendly properties */
|
|
60
|
+
.animated-card {
|
|
61
|
+
will-change: transform, opacity;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Bad: never use will-change: all */
|
|
65
|
+
.animated-card {
|
|
66
|
+
will-change: all;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Bad: properties that can't be GPU-composited anyway */
|
|
70
|
+
.animated-card {
|
|
71
|
+
will-change: background-color, padding;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Useful properties
|
|
76
|
+
|
|
77
|
+
| Property | GPU-compositable | Worth using `will-change` |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `transform` | Yes | Yes |
|
|
80
|
+
| `opacity` | Yes | Yes |
|
|
81
|
+
| `filter` (blur, brightness) | Yes | Yes |
|
|
82
|
+
| `clip-path` | Newer Chromium only | Rarely; not reliable cross-browser |
|
|
83
|
+
| `top`, `left`, `width`, `height` | No | No |
|
|
84
|
+
| `background`, `border`, `color` | No | No |
|
|
85
|
+
|
|
86
|
+
### When to skip
|
|
87
|
+
|
|
88
|
+
Modern browsers optimize well on their own. Add `will-change` only when you see first-frame stutter, which Safari benefits from most. Never add it preemptively to every animated element, since each extra compositing layer costs memory.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# The picker
|
|
2
|
+
|
|
3
|
+
The control that switches variants. Its appearance is not a design decision, because it sits on top of the thing being judged. Build the spec below and leave it alone.
|
|
4
|
+
|
|
5
|
+
## Deliberately outside the design system
|
|
6
|
+
|
|
7
|
+
Never style the picker with the project's tokens, fonts, or colors. One that looks native to the product becomes part of what you are looking at, and then you are judging the harness.
|
|
8
|
+
|
|
9
|
+
One dark neutral surface, the system font stack and no project variables. Dark reads as chrome over both light and dark pages, which is why it does not follow the theme.
|
|
10
|
+
|
|
11
|
+
## Behavior
|
|
12
|
+
|
|
13
|
+
- It sets a `variant` search param and reads the active variant back from it, so the URL is the source of truth and every variant a link.
|
|
14
|
+
- Left and right arrows step through the set. Number keys jump to one directly.
|
|
15
|
+
- The active item carries `aria-current="true"`, and the container carries a label.
|
|
16
|
+
- Switching is instant. Flipping is the run's highest-frequency action, so `better-ui`'s motion restraint gives it no transition.
|
|
17
|
+
- It survives a resize, so you can hold a variant and drag the window rather than reloading per width.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
One button per variant, in the order they were named. Add a replay button only where a variant has an entrance worth re-triggering.
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<nav class="variant-picker" aria-label="Variants">
|
|
25
|
+
<button type="button" data-variant="quiet" aria-current="true">Quiet</button>
|
|
26
|
+
<button type="button" data-variant="editorial">Editorial</button>
|
|
27
|
+
<button type="button" data-variant="dense">Dense</button>
|
|
28
|
+
</nav>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Placement and styling
|
|
32
|
+
|
|
33
|
+
Fixed, bottom centre, above everything the page can stack. Keep it clear of the piece under judgement. Where the variants live at the bottom of the viewport, move the picker to top centre and say so.
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
.variant-picker {
|
|
37
|
+
position: fixed;
|
|
38
|
+
bottom: 24px;
|
|
39
|
+
left: 50%;
|
|
40
|
+
translate: -50% 0;
|
|
41
|
+
z-index: 2147483647;
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: 2px;
|
|
44
|
+
padding: 4px;
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
background: rgb(20 20 20 / 0.9);
|
|
47
|
+
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 8px 24px rgb(0 0 0 / 0.25);
|
|
48
|
+
font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
49
|
+
user-select: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.variant-picker button {
|
|
53
|
+
padding: 7px 14px;
|
|
54
|
+
border: 0;
|
|
55
|
+
border-radius: 999px;
|
|
56
|
+
background: none;
|
|
57
|
+
color: rgb(255 255 255 / 0.6);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.variant-picker button:hover {
|
|
62
|
+
color: rgb(255 255 255 / 0.85);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.variant-picker button[aria-current="true"] {
|
|
66
|
+
background: rgb(255 255 255 / 0.14);
|
|
67
|
+
color: rgb(255 255 255);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.variant-picker button:focus-visible {
|
|
71
|
+
outline: 2px solid rgb(255 255 255 / 0.7);
|
|
72
|
+
outline-offset: 2px;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
In a framework, keep the class names and the structure and change only the rendering syntax.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Reading the whole system
|
|
2
|
+
|
|
3
|
+
Use this where no specific effect was named and the question is how the interface is built in general. For one named thing, use [find-the-effect.md](find-the-effect.md).
|
|
4
|
+
|
|
5
|
+
Every snippet runs in the page context, through whatever browser tooling is available: `evaluate_script` in the Chrome DevTools MCP, the console, a Playwright `page.evaluate`. Each returns data rather than printing, so the result comes back whole.
|
|
6
|
+
|
|
7
|
+
Run them in this order. Tokens first, because a page that hands you its custom properties has already told you most of the answer.
|
|
8
|
+
|
|
9
|
+
## The gotcha that costs you the run
|
|
10
|
+
|
|
11
|
+
`sheet.cssRules` throws on a cross-origin stylesheet. Every snippet that walks stylesheets wraps the access and reports what it could not read. An explanation that silently skipped the main stylesheet describes a page nobody is looking at.
|
|
12
|
+
|
|
13
|
+
## The stack first
|
|
14
|
+
|
|
15
|
+
"How was this site built" wants the frontend named before a type scale. Run this, then report each hit with its evidence, never as a bare claim:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const html = document.documentElement;
|
|
19
|
+
const res = performance.getEntriesByType('resource').map(r => r.name);
|
|
20
|
+
const any = re => res.some(n => re.test(n));
|
|
21
|
+
const attr = sel => !!document.querySelector(sel);
|
|
22
|
+
({
|
|
23
|
+
framework: {
|
|
24
|
+
next: !!window.__NEXT_DATA__ || any(/\/_next\/static/),
|
|
25
|
+
nextAppRouter: typeof self.__next_f !== 'undefined',
|
|
26
|
+
nuxt: !!window.__NUXT__ || any(/\/_nuxt\//),
|
|
27
|
+
remix: !!window.__remixContext,
|
|
28
|
+
gatsby: !!window.___gatsby,
|
|
29
|
+
astro: attr('astro-island, [data-astro-cid]'),
|
|
30
|
+
svelte: attr('[class*="svelte-"]') || any(/\/_app\/immutable\//),
|
|
31
|
+
angular: attr('[ng-version]'),
|
|
32
|
+
reactFiber: Object.keys(document.body.firstElementChild ?? {}).some(k => k.startsWith('__react')),
|
|
33
|
+
},
|
|
34
|
+
styling: {
|
|
35
|
+
tailwind: getComputedStyle(html).getPropertyValue('--tw-ring-offset-width') !== ''
|
|
36
|
+
|| !!document.querySelector('[class*="bg-linear-to"], [class*="bg-gradient-to"]'),
|
|
37
|
+
tailwindV4: !!document.querySelector('[class*="bg-linear-to"]'),
|
|
38
|
+
cssModules: attr('[class*="_"][class*="__"]'),
|
|
39
|
+
styledComponents: attr('[class^="sc-"]') || attr('style[data-styled]'),
|
|
40
|
+
emotion: attr('[class^="css-"]'),
|
|
41
|
+
},
|
|
42
|
+
components: {
|
|
43
|
+
radix: attr('[data-radix-popper-content-wrapper], [data-radix-scroll-area-viewport]')
|
|
44
|
+
|| !!document.querySelector('[data-slot], [data-state][data-side]'),
|
|
45
|
+
baseUi: attr('[data-base-ui-portal], [class*="base-ui"]'),
|
|
46
|
+
headlessUi: attr('[data-headlessui-state]'),
|
|
47
|
+
mui: attr('[class*="Mui"]'),
|
|
48
|
+
arkOrChakra: attr('[data-scope][data-part]'),
|
|
49
|
+
},
|
|
50
|
+
motion: { animationsRunning: document.getAnimations().length, gsap: !!window.gsap },
|
|
51
|
+
images: { nextImage: any(/\/_next\/image\?/), modernFormats: [...document.images].some(i => /\.(avif|webp)/.test(i.currentSrc)), srcset: [...document.images].filter(i => i.srcset).length },
|
|
52
|
+
fonts: { count: document.fonts.size, variable: [...document.fonts].some(f => String(f.weight).includes(' ')), selfHosted: !any(/fonts\.g(oogleapis|static)\.com/) },
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Two rules. A fingerprint is not a fact, so give the evidence: `/_next/static` in an asset path is strong, a utility-looking class name alone is weak. And a `false` is not an absence, only a fingerprint that did not fire.
|
|
57
|
+
|
|
58
|
+
## Tokens
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const tokens = {}; const unreadable = [];
|
|
62
|
+
for (const sheet of document.styleSheets) {
|
|
63
|
+
let rules; try { rules = sheet.cssRules } catch { unreadable.push(sheet.href); continue }
|
|
64
|
+
for (const r of rules ?? []) {
|
|
65
|
+
if (r.selectorText === ':root' || r.selectorText === 'html') {
|
|
66
|
+
for (const prop of r.style) {
|
|
67
|
+
if (prop.startsWith('--')) tokens[prop] = r.style.getPropertyValue(prop).trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
({ tokens, unreadable, count: Object.keys(tokens).length });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Group the result by prefix. The prefixes are the system's own layer names, and a two-tier structure, `--blue-500` feeding `--color-text-primary`, is the seam `better-colors` calls the semantic tier.
|
|
76
|
+
|
|
77
|
+
## The type scale
|
|
78
|
+
|
|
79
|
+
Leaf text nodes only, so a wrapper's inherited size is not counted as its own step.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const seen = new Map();
|
|
83
|
+
for (const el of document.querySelectorAll('*')) {
|
|
84
|
+
if (el.children.length || !el.textContent?.trim()) continue;
|
|
85
|
+
const s = getComputedStyle(el);
|
|
86
|
+
const key = `${parseFloat(s.fontSize)}px w${s.fontWeight} lh ${s.lineHeight} ls ${s.letterSpacing}`;
|
|
87
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
[...seen].sort((a, b) => b[1] - a[1]);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Sorted by usage, so the body size is first and the one-offs last. Derive the ratio between adjacent sizes. A consistent ratio means a scale; scattered values mean hard-coded sizes.
|
|
93
|
+
|
|
94
|
+
## The spacing rhythm
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const vals = new Map();
|
|
98
|
+
for (const el of document.querySelectorAll('*')) {
|
|
99
|
+
const s = getComputedStyle(el);
|
|
100
|
+
for (const p of ['paddingTop', 'paddingLeft', 'marginTop', 'marginLeft', 'gap', 'rowGap']) {
|
|
101
|
+
const v = parseFloat(s[p]);
|
|
102
|
+
if (v > 0) vals.set(v, (vals.get(v) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
[...vals].sort((a, b) => a[0] - b[0]);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Look for the base unit that divides most values, then check `better-layout`'s grouping rule. Is the gap between groups at least 2× the gap within one?
|
|
109
|
+
|
|
110
|
+
## Radii, shadows, borders
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const grab = (prop, skip) => {
|
|
114
|
+
const m = new Map();
|
|
115
|
+
for (const el of document.querySelectorAll('*')) {
|
|
116
|
+
const v = getComputedStyle(el)[prop];
|
|
117
|
+
if (v && v !== skip) m.set(v, (m.get(v) ?? 0) + 1);
|
|
118
|
+
}
|
|
119
|
+
return [...m].sort((a, b) => b[1] - a[1]);
|
|
120
|
+
};
|
|
121
|
+
({ radius: grab('borderRadius', '0px'), shadow: grab('boxShadow', 'none') });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Count the distinct shadows. One or two recipes is a system; nine is a page where everyone invented their own elevation.
|
|
125
|
+
|
|
126
|
+
## Motion
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const t = new Map();
|
|
130
|
+
for (const el of document.querySelectorAll('*')) {
|
|
131
|
+
const s = getComputedStyle(el);
|
|
132
|
+
if (s.transitionDuration === '0s') continue;
|
|
133
|
+
const key = `${s.transitionProperty} ${s.transitionDuration} ${s.transitionTimingFunction}`;
|
|
134
|
+
t.set(key, (t.get(key) ?? 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
[...t].sort((a, b) => b[1] - a[1]);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`transition: all` shows up here as `all`. Custom curves arrive as `cubic-bezier(...)`, and the built-in keywords tell you nobody tuned them.
|
|
140
|
+
|
|
141
|
+
## Breakpoints
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const bp = new Set(); const unreadable = [];
|
|
145
|
+
for (const sheet of document.styleSheets) {
|
|
146
|
+
let rules; try { rules = sheet.cssRules } catch { unreadable.push(sheet.href); continue }
|
|
147
|
+
const walk = list => { for (const r of list ?? []) {
|
|
148
|
+
if (r.media) { for (const m of r.media) { const hit = m.match(/(min|max)-width:\s*([\d.]+)(px|r?em)/); if (hit) bp.add(hit[0]) } }
|
|
149
|
+
if (r.cssRules) walk(r.cssRules);
|
|
150
|
+
}};
|
|
151
|
+
walk(rules);
|
|
152
|
+
}
|
|
153
|
+
({ breakpoints: [...bp].sort(), unreadable });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Compare against the framework defaults. Breakpoints at exactly `640/768/1024/1280` are Tailwind's out of the box, which tells you they were never chosen.
|
|
157
|
+
|
|
158
|
+
## Fonts and theming
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
({
|
|
162
|
+
loaded: [...document.fonts].map(f => `${f.family} ${f.weight} ${f.style} ${f.status}`),
|
|
163
|
+
bodyStack: getComputedStyle(document.body).fontFamily,
|
|
164
|
+
variable: [...document.fonts].some(f => String(f.weight).includes(' ')),
|
|
165
|
+
themeClass: document.documentElement.className || '(none)',
|
|
166
|
+
colorScheme: getComputedStyle(document.documentElement).colorScheme,
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`variable: true` means one file covers a weight range. A class on `<html>` beside a `prefers-color-scheme` query means a toggle that can override the system, the mechanism `better-colors` describes.
|
|
171
|
+
|
|
172
|
+
## Reading a second state
|
|
173
|
+
|
|
174
|
+
Everything above reads one state at one width. Before writing the explanation, at minimum:
|
|
175
|
+
|
|
176
|
+
- Resize to 375px and re-run the spacing and breakpoint snippets. The values that change are what is fluid.
|
|
177
|
+
- Toggle the theme and re-run the token snippet. The tokens that change are the themed layer, the ones that do not are the primitives.
|
|
178
|
+
- Tab to the first interactive control and read its `:focus-visible` styles, since a focus ring is one of the most common absences.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Removed signals
|
|
2
|
+
|
|
3
|
+
What to look for on the `-` side of a hunk and which skill owns the judgement. A row here is a lead, never a finding. Route the removal to its owner and report it only once that skill confirms the interface got worse.
|
|
4
|
+
|
|
5
|
+
| Removed from the `-` side | Owner | What to check |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-live`, `role=` | `better-accessibility` | The control or region lost its accessible name, description, or announcement |
|
|
8
|
+
| `alt=`, `<label`, `for=`, `scope=` | `better-accessibility` | Image, field, or table cell lost its programmatic association |
|
|
9
|
+
| `<button>`, `<a>`, `<nav>`, `<main>`, `<ul>` replaced by `div` or `span` | `better-accessibility` | Keyboard and assistive-technology behavior was traded for styling |
|
|
10
|
+
| `:focus-visible`, `:focus`, `outline`, `tabindex` | `better-accessibility` | Keyboard users lost the focus indicator or the element left the tab order |
|
|
11
|
+
| `prefers-reduced-motion`, `prefers-contrast` | `better-accessibility` | Motion or contrast now ignores the user's system preference |
|
|
12
|
+
| Logical properties swapped for `left` / `right` | `better-layout` | Direction-aware layout was dropped |
|
|
13
|
+
| `lang=`, `dir=` | `better-typography` | Language metadata or text direction was dropped |
|
|
14
|
+
| `text-wrap`, `line-clamp`, `overflow-wrap`, `tabular-nums`, `font-feature-settings` | `better-typography` | Text rendering, wrapping, or numeral alignment silently changed |
|
|
15
|
+
| A color token swapped for a literal, or a token swapped for a lighter one | `better-colors` | The rendered contrast pair may now fail; measure it |
|
|
16
|
+
| A user-facing string deleted or shortened | `better-writing` | A label, error, or empty state lost the information it carried |
|
|
17
|
+
|
|
18
|
+
## Equivalent replacements
|
|
19
|
+
|
|
20
|
+
These clear the signal. Check for them before routing anything, or the report fills with refactors reported as regressions:
|
|
21
|
+
|
|
22
|
+
- `aria-label` giving way to `aria-labelledby` pointing at visible text.
|
|
23
|
+
- An explicit `role` dropped because the element became the native equivalent, `role="button"` going as a `div` becomes a `<button>`.
|
|
24
|
+
- `outline` replaced by a `box-shadow` focus ring that still meets the focus-indicator rule.
|
|
25
|
+
- `tabindex="0"` dropped from an element that is now natively focusable.
|
|
26
|
+
- A color literal replaced by a token that measures the same rendered pair.
|
|
27
|
+
- A physical property replaced by its logical counterpart, which is the fix rather than the regression.
|
|
28
|
+
- A string moved into the translation catalogue rather than deleted.
|
|
29
|
+
|
|
30
|
+
## Searching the removed side
|
|
31
|
+
|
|
32
|
+
Restrict the search to deleted lines so additions do not mask a removal:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git diff -U0 "$BASE"...HEAD -- '*.tsx' '*.css' | grep -E '^-[^-]' | grep -E 'aria-|role=|alt=|focus|tabindex|prefers-'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Read the surrounding hunk before deciding. A removed attribute is meaningless without the element it came from, and `-U0` deliberately hides that context.
|