@mohammadhprp/system-prompt 0.12.0 → 0.12.2
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/README.md +0 -1
- package/framework/commands/mr.md +8 -9
- package/framework/mcps/README.md +0 -3
- package/framework/skills/README.md +5 -4
- 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/glab/SKILL.md +222 -0
- package/framework/skills/glab/references/commands-detailed.md +616 -0
- package/framework/skills/glab/references/quick-reference.md +145 -0
- package/framework/skills/glab/references/troubleshooting.md +669 -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/improve/SKILL.md +137 -0
- package/framework/skills/improve/examples.md +19 -0
- package/framework/skills/improve/references/audit-playbook.md +130 -0
- package/framework/skills/improve/references/closing-the-loop.md +96 -0
- package/framework/skills/improve/references/plan-template.md +197 -0
- package/framework/skills/jira-cli/SKILL.md +260 -0
- package/framework/skills/jira-cli/references/commands-detailed.md +268 -0
- package/framework/skills/jira-cli/references/quick-reference.md +111 -0
- package/framework/skills/jira-cli/references/troubleshooting.md +114 -0
- package/framework/styles/README.md +9 -4
- package/framework/styles/factory/DESIGN.md +360 -0
- package/framework/styles/factory/README.md +32 -0
- package/framework/styles/factory/assets/preview.jpg +0 -0
- package/framework/styles/huly/DESIGN.md +449 -0
- package/framework/styles/huly/README.md +32 -0
- package/framework/styles/huly/assets/preview.jpg +0 -0
- package/framework/styles/notion/DESIGN.md +423 -0
- package/framework/styles/notion/README.md +32 -0
- package/framework/styles/notion/assets/preview.jpg +0 -0
- package/package.json +1 -1
- package/src/catalog.js +8 -8
- package/framework/agents/backend-architect.md +0 -146
- package/framework/mcps/github-mcp/README.md +0 -51
- package/framework/mcps/github-mcp/capabilities.md +0 -83
- package/framework/mcps/github-mcp/configs/.env.example +0 -1
- package/framework/mcps/github-mcp/configs/opencode.json +0 -13
- package/framework/mcps/github-mcp/install.md +0 -60
- package/framework/mcps/github-mcp/troubleshooting.md +0 -79
- package/framework/mcps/gitlab-mcp/README.md +0 -53
- package/framework/mcps/gitlab-mcp/capabilities.md +0 -216
- package/framework/mcps/gitlab-mcp/configs/.env.example +0 -2
- package/framework/mcps/gitlab-mcp/configs/opencode.json +0 -13
- package/framework/mcps/gitlab-mcp/install.md +0 -99
- package/framework/mcps/gitlab-mcp/troubleshooting.md +0 -116
- package/framework/mcps/jira-mcp/README.md +0 -52
- package/framework/mcps/jira-mcp/capabilities.md +0 -79
- package/framework/mcps/jira-mcp/configs/.env.example +0 -2
- package/framework/mcps/jira-mcp/configs/opencode.json +0 -13
- package/framework/mcps/jira-mcp/install.md +0 -94
- package/framework/mcps/jira-mcp/troubleshooting.md +0 -113
- package/framework/skills/brand-guidelines/LICENSE.txt +0 -202
- package/framework/skills/brand-guidelines/SKILL.md +0 -73
- package/framework/skills/gitlab-mcp/SKILL.md +0 -83
- package/framework/skills/gitlab-mcp/examples.md +0 -31
- package/framework/skills/gitlab-mcp/references/code-review.md +0 -110
- package/framework/skills/gitlab-mcp/references/issues.md +0 -141
- package/framework/skills/gitlab-mcp/references/merge-requests.md +0 -120
- package/framework/skills/gitlab-mcp/references/pipelines.md +0 -67
- package/framework/skills/gitlab-mcp/references/search.md +0 -17
- package/framework/skills/gitlab-mcp/references/webhooks.md +0 -32
- package/framework/skills/gitlab-mcp/references/work-items.md +0 -50
- package/framework/skills/jira-mcp/SKILL.md +0 -57
- package/framework/skills/jira-mcp/examples.md +0 -31
- package/framework/skills/jira-mcp/references/comments.md +0 -27
- package/framework/skills/jira-mcp/references/issues.md +0 -97
- package/framework/skills/jira-mcp/references/projects.md +0 -39
- package/framework/skills/lavish/SKILL.md +0 -67
- package/framework/skills/lavish/examples.md +0 -31
|
@@ -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.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Review output format
|
|
2
|
+
|
|
3
|
+
This is the format for a review `better-interface` orchestrates. A domain skill reporting on its own carries its own smaller format, in its `## Reporting` section.
|
|
4
|
+
|
|
5
|
+
## Scope and coverage
|
|
6
|
+
|
|
7
|
+
State the exact scope, stack and styling conventions, the project convention documents found in recon and any review boundary. Then show coverage:
|
|
8
|
+
|
|
9
|
+
| Domain | Evidence inspected | Result |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Accessibility | Files, components, states, or checks | Findings count or `Clear` |
|
|
12
|
+
|
|
13
|
+
Include every domain listed under `better-interface`'s **Use domain skills as the sources of truth**. `Clear` means inspected with no actionable finding; `Not reviewed` must explain why.
|
|
14
|
+
|
|
15
|
+
## Findings
|
|
16
|
+
|
|
17
|
+
One table, ordered by severity, then by reach:
|
|
18
|
+
|
|
19
|
+
| Severity | Domain | Location | Before | After | Why |
|
|
20
|
+
| --- | --- | --- | --- | --- | --- |
|
|
21
|
+
| HIGH | Accessibility | `src/Dialog.tsx:42` | `<button><XIcon /></button>` | Add `aria-label="Close"` and hide the icon from the accessibility tree | The icon-only control has no accessible name |
|
|
22
|
+
|
|
23
|
+
- **Severity** comes from `better-interface`'s **Rank by user impact**.
|
|
24
|
+
- **Location** cites `path/to/file:line`. Cite the exact screen and component when the artifact has no source files.
|
|
25
|
+
- **Before / After** show the current implementation and an actionable replacement. Never split them into separate "Before:" and "After:" lines.
|
|
26
|
+
- **Why** names the violated principle and its user impact.
|
|
27
|
+
- **Domain** is the owning skill without the `better-` prefix.
|
|
28
|
+
|
|
29
|
+
Each row is one root cause. Consolidate a repeated systemic issue into one row and list every affected location. Respect the finding cap. With no findings, omit the table and state "No actionable interface findings."
|
|
30
|
+
|
|
31
|
+
## Verification
|
|
32
|
+
|
|
33
|
+
List each check or interaction, the exact command or steps and the observed result. Separate checks that passed from checks marked **Not verified**.
|
|
34
|
+
|
|
35
|
+
## Verdict
|
|
36
|
+
|
|
37
|
+
End with one of two:
|
|
38
|
+
|
|
39
|
+
- `Block`: one or more `HIGH` findings remain. Do not ship until they are fixed.
|
|
40
|
+
- `Approve`: no `HIGH` findings remain. Any `MEDIUM` and `LOW` findings stay in the table as work to do.
|
|
41
|
+
|
|
42
|
+
`Approve` claims the coverage you reported, so never issue it for a domain you did not inspect.
|
|
43
|
+
|
|
44
|
+
## Change-scoped reviews
|
|
45
|
+
|
|
46
|
+
When `interface-review` resolved the scope from version control, it supplies the scope block, a status on every finding and the change-scoped format, which its `## Review output format` holds. Severity, ranking, the cap and the verdict are the ones above, and all four cover `Introduced` and `Regression` only.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Scope resolution
|
|
2
|
+
|
|
3
|
+
Turning a review target into a file list. The commands themselves are ordinary git. What follows is the part that is not obvious, plus the traps that fail quietly and leave the scope block claiming a count it never delivered.
|
|
4
|
+
|
|
5
|
+
## Default branch
|
|
6
|
+
|
|
7
|
+
Try `refs/remotes/origin/HEAD`, then `gh repo view --json defaultBranchRef`, then `init.defaultBranch`. If the ref is missing, ask the remote with `git remote set-head origin --auto` rather than guessing. It needs the network and writes a ref under `.git`, leaving the working tree untouched, so it is permitted; note it in Verification. With no remote at all, fall back to a local `main` or `master` and state which base you assumed.
|
|
8
|
+
|
|
9
|
+
## Targets
|
|
10
|
+
|
|
11
|
+
Accepted targets are `working`, `staged`, `branch`, `pr <n>`, a bare `<ref>` and an explicit `<a>..<b>` or `<a>...<b>` range. Anything else in the invocation is a `<ref>`.
|
|
12
|
+
|
|
13
|
+
Diff a branch against the **merge base**, three dots. Two dots reports every upstream commit that landed on the base branch as part of the change.
|
|
14
|
+
|
|
15
|
+
But use the dots the user wrote when they wrote a range. `<a>..<b>` compares the endpoints; `<a>...<b>` compares `merge-base(<a>, <b>)` with `<b>`. Rewriting `release..feature` to three dots drops everything between `release` and the merge base, which is often exactly what was asked for. State the resolved range in the scope block.
|
|
16
|
+
|
|
17
|
+
`git diff HEAD` reports tracked changes only. Any target including uncommitted work must pair it with `git ls-files --others --exclude-standard`. Otherwise a newly added component is silently dropped from a scope the report claims to cover in full. For `branch` with uncommitted work, report the two counts separately.
|
|
18
|
+
|
|
19
|
+
## Pull requests
|
|
20
|
+
|
|
21
|
+
Fetch the head into a remote-tracking ref, `git fetch origin "pull/<n>/head:refs/remotes/pr/<n>"`, and review it in place. This works for forks, which `origin/<branch>` does not.
|
|
22
|
+
|
|
23
|
+
Read files at that ref with `git show refs/remotes/pr/<n>:path/to/file`. Never open the working-tree copy; on a fork PR it is a different file.
|
|
24
|
+
|
|
25
|
+
`gh pr diff <n>` is a fine shortcut for the patch text. It gives no way to read unchanged context or expand to consumers, so fetch the ref as well.
|
|
26
|
+
|
|
27
|
+
**Citations.** `better-interface` requires `path/to/file:line`, and line numbers from a fetched ref need not match the working tree. Cite against the head ref, and declare that ref and its SHA in the scope block so the numbers resolve.
|
|
28
|
+
|
|
29
|
+
**Intent.** The `title` and `body` from `gh pr view` are the stated intent for **Hold the change to its stated intent**. Add the commit subjects when the body is empty.
|
|
30
|
+
|
|
31
|
+
## Awkward repository states
|
|
32
|
+
|
|
33
|
+
Three worth handling. Everything else fails loudly at `merge-base`: no remote, unrelated histories, a repo with no commits, a moved submodule pointer. Say the base is unresolvable and stop. Never review a range you cannot name.
|
|
34
|
+
|
|
35
|
+
**Detached HEAD.** Use the merge base against the default branch and name the SHA, not a branch, in the scope block.
|
|
36
|
+
|
|
37
|
+
**Shallow clone**, the CI default, where `merge-base` returns nothing. Fetch `--deepen=50`, retry, then `--deepen=200`, then report the scope as unresolvable. Deepening writes to `.git` and not to the working tree, so it is permitted; note it in Verification.
|
|
38
|
+
|
|
39
|
+
**Mid-rebase or mid-merge**, the one that does not fail loudly. `git diff` succeeds and returns something that is not the change, so the review looks fine and is wrong. Detect it with `git rev-parse --git-path` against `rebase-merge`, `rebase-apply`, `MERGE_HEAD` and `CHERRY_PICK_HEAD`. Do not test `.git/` paths directly, because they are not directories inside a linked worktree. Stop and say the tree is mid-operation.
|
|
40
|
+
|
|
41
|
+
## Nothing to review
|
|
42
|
+
|
|
43
|
+
The tree is clean and `HEAD` is not ahead of the merge base. Gather the facts before asking, so the offer is accurate rather than a guess: the current branch, whether the tree is clean, the count ahead of the base, the last commit's SHA and subject and any open pull request from `gh pr status`.
|
|
44
|
+
|
|
45
|
+
`gh pr status` succeeds when no pull request is open. It omits `currentBranch`, so an empty result is an answer, not an error. It fails outright without `gh`, without authentication and on a repository with no GitHub remote. Treat any failure as "no pull request found", say so and offer the remaining routes rather than stopping.
|
|
46
|
+
|
|
47
|
+
Report those facts, then offer the three routes in **With no change, ask rather than invent one**. State the last commit's SHA and subject inside the offer. The user recognises "a1b2c3d Merge pull request #482" as not what they wanted and cannot recognise "the last commit".
|
|
48
|
+
|
|
49
|
+
A whole-repository audit is a different review, not this one with a wider net. Hand the repository to `better-interface` directly, without a scope block, statuses, or a pre-existing section.
|
|
50
|
+
|
|
51
|
+
## Renames
|
|
52
|
+
|
|
53
|
+
Rename detection is on by default for `--name-status`, reporting `R100 old/path new/path`. Raise the window with `--find-renames=40% --find-copies-harder` when a file was moved and edited in the same change.
|
|
54
|
+
|
|
55
|
+
Review a rename as a move, not a delete plus an add. Everything that survived the move is unchanged code, and only the genuine edits are in scope.
|
|
56
|
+
|
|
57
|
+
## Excluded paths
|
|
58
|
+
|
|
59
|
+
Exclude these and name what you excluded in the scope block. They are machine-authored and carry no interface rules.
|
|
60
|
+
|
|
61
|
+
| Category | Patterns |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| Lockfiles | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `composer.lock`, `Gemfile.lock`, `poetry.lock`, `uv.lock` |
|
|
64
|
+
| Snapshots and fixtures | `__snapshots__/`, `*.snap`, `*.approved.*`, `test-results/`, `playwright-report/` |
|
|
65
|
+
| Generated output | `dist/`, `build/`, `out/`, `.next/`, `.turbo/`, `.svelte-kit/`, `coverage/`, `storybook-static/`, `*.min.js`, `*.min.css`, `*.map` |
|
|
66
|
+
| Generated sources | `*.gen.ts`, `*.generated.*`, `*.d.ts` emitted by a build, GraphQL and Prisma client output |
|
|
67
|
+
| Vendored code | `vendor/`, `third_party/`, `node_modules/` |
|
|
68
|
+
| Binaries and media | `*.png`, `*.jpg`, `*.webp`, `*.avif`, `*.woff2`, `*.mp4`, `*.pdf` |
|
|
69
|
+
|
|
70
|
+
Two exceptions stay in scope. A **font file** added or swapped is a `better-typography` change. An **image** added to a component is a `better-ui` and `better-accessibility` change, through its `alt` text and its outline. Review the code that references them, not the bytes.
|
|
71
|
+
|
|
72
|
+
Apply the exclusions as pathspecs so the file count in the scope block is the reviewed count. Two traps under-exclude silently. `*.lock` catches `yarn.lock` and `Cargo.lock` but not `package-lock.json` or `pnpm-lock.yaml`, so cover every suffix in the table. And `**` needs `glob` magic: without it `*` never crosses `/`, so `**/dist/**` excludes `packages/a/dist/` but misses a root-level `dist/`. Run the diff with and without the pathspecs and confirm the count dropped by exactly the files you named.
|
|
73
|
+
|
|
74
|
+
## Expanding to consumers
|
|
75
|
+
|
|
76
|
+
**A diff is not a surface** expands one hop, two for tokens and primitives. Use the project's own resolver where one exists, otherwise import paths.
|
|
77
|
+
|
|
78
|
+
`git grep` searches the working tree by default. Pass the reviewed ref after the pattern, or on a pull request you search a different revision and miss importers the change itself added. Results come back as `<rev>:path/to/file`; read them with `git show`, never the working-tree copy. Pass `-e` when the pattern starts with a dash, such as a `--color-*` token, or git parses it as an option.
|
|
79
|
+
|
|
80
|
+
For a changed token or theme value, search the token name rather than the file, since consumers reference the name and never import it.
|
|
81
|
+
|
|
82
|
+
Order the consumers by a rule you can evaluate, so the cutoff is reproducible instead of a guess:
|
|
83
|
+
|
|
84
|
+
1. **Route and layout entry points first**, whatever the framework treats as a rendered surface: `app/**/page.*`, `app/**/layout.*`, `pages/**`, `routes/**`, `src/views/**`, `*.astro` pages. Everything else only appears inside one.
|
|
85
|
+
2. **Then by importer count**, since a component pulled in by twenty files carries more of the change than one pulled in by two.
|
|
86
|
+
3. **Break ties by proximity**, same package or feature directory first.
|
|
87
|
+
|
|
88
|
+
Review the first five, state how many you did not expand and say plainly if the ordering was arbitrary past a point.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Screen readers
|
|
2
|
+
|
|
3
|
+
Visually hidden content, live regions, toasts, alt text and SVG.
|
|
4
|
+
|
|
5
|
+
## Visually hidden content
|
|
6
|
+
|
|
7
|
+
The canonical `.sr-only` pattern hides content visually while keeping it in the accessibility tree:
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
.sr-only {
|
|
11
|
+
position: absolute;
|
|
12
|
+
width: 1px;
|
|
13
|
+
height: 1px;
|
|
14
|
+
padding: 0;
|
|
15
|
+
margin: -1px;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
clip: rect(0 0 0 0);
|
|
18
|
+
clip-path: inset(50%);
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
border: 0;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `1px` boxes, not `0`, because some screen readers skip zero-sized elements. `white-space: nowrap` stops words being read as one run-together string. Never `display: none` or `visibility: hidden`, which remove the content from assistive tech entirely.
|
|
25
|
+
|
|
26
|
+
Tailwind ships this as `sr-only`. Skip links add a focus variant that un-hides it, `focus:not-sr-only` or an override on `:focus`.
|
|
27
|
+
|
|
28
|
+
Use it for context sighted users get visually: `<span class="sr-only">Opens in new tab</span>`, table captions, or an icon-only control's label where `aria-label` isn't an option.
|
|
29
|
+
|
|
30
|
+
## Choosing how to announce a change
|
|
31
|
+
|
|
32
|
+
Work down this list and stop at the first match:
|
|
33
|
+
|
|
34
|
+
1. **Focus moves there anyway**, as with an opened modal or the first invalid field. Nothing extra needed; the focus move is the announcement.
|
|
35
|
+
2. **Tied to a specific control**, such as a field error or character count: `aria-describedby` on the control, announced with the field.
|
|
36
|
+
3. **Non-urgent, not tied to a control**, such as a toast, "Saved", a result count, or a loading state: a polite live region, `role="status"`.
|
|
37
|
+
4. **Urgent and not tied to a control**, such as a form-level failure or session expiry: `role="alert"`.
|
|
38
|
+
|
|
39
|
+
## Live regions
|
|
40
|
+
|
|
41
|
+
Live regions announce content that changes without a page load: toasts, validation, search-result counts, loading states.
|
|
42
|
+
|
|
43
|
+
| Mechanism | Politeness | Use for |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `role="status"` (= `aria-live="polite"` + `aria-atomic="true"`) | Waits for a pause | Toasts, "Saved", result counts, loading updates |
|
|
46
|
+
| `role="alert"` (= `aria-live="assertive"` + `aria-atomic="true"`) | Interrupts immediately | Errors and urgent problems only |
|
|
47
|
+
|
|
48
|
+
Rules for reliable announcements:
|
|
49
|
+
|
|
50
|
+
- For repeated polite updates, keep a stable empty region in the DOM before changing its text. Inserting a new polite region with its content is announced inconsistently.
|
|
51
|
+
- Dynamically inserted `role="alert"` content is usually announced, but behavior varies. Use it only for urgent errors not tied to a control, and test the target browser and screen-reader combinations.
|
|
52
|
+
- Default to polite. Overusing `assertive` is the most common live-region mistake, because it interrupts whatever the user was reading.
|
|
53
|
+
- Keep messages short and self-contained. `aria-atomic="true"` re-reads the whole region on change.
|
|
54
|
+
- Never move focus to a toast. Announce it and leave focus where the user is working. Give toasts a generous timeout or a dismiss button, and never put the only path to an action inside an auto-dismissing one.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// Region rendered from the start, message injected later
|
|
58
|
+
<div role="status" className="sr-only">
|
|
59
|
+
{statusMessage}
|
|
60
|
+
</div>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For loading states: set `aria-busy="true"` on the updating region, announce "Loading…" politely, then announce the outcome ("Loaded, 12 results").
|
|
64
|
+
|
|
65
|
+
## aria-hidden
|
|
66
|
+
|
|
67
|
+
`aria-hidden="true"` removes an element and its whole subtree from assistive tech. Use it for decorative icons and content duplicated for visual effect. Never put it on or above a focusable element, which creates stops you can Tab to that do not exist for a screen reader. Hiding something interactive means removing it from the tab order too.
|
|
68
|
+
|
|
69
|
+
## Alt text
|
|
70
|
+
|
|
71
|
+
Choose by purpose, not by what the image looks like:
|
|
72
|
+
|
|
73
|
+
| Purpose | Alt | Example |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| Decorative, or redundant with adjacent text | `alt=""` (empty, but present) | Logo next to the company name in text |
|
|
76
|
+
| Informative | Describe the meaning it adds | `alt="Ticket QR code"` |
|
|
77
|
+
| Functional (image is the link/button) | Describe the action or destination | Search icon → `alt="Search"`, not `alt="magnifying glass"` |
|
|
78
|
+
| Image of text | The exact text (better: use real text) | `alt="50% off everything"` |
|
|
79
|
+
| Complex (chart, diagram) | Short summary in `alt`, full data as a table or text nearby | `alt="Revenue by quarter, described below"` |
|
|
80
|
+
|
|
81
|
+
A missing `alt` is worse than an empty one, because screen readers fall back to reading the file name.
|
|
82
|
+
|
|
83
|
+
## SVG
|
|
84
|
+
|
|
85
|
+
- Decorative SVG: `aria-hidden="true"` and `focusable="false"`, the latter for legacy Edge and IE tabbing. No title needed.
|
|
86
|
+
- Meaningful inline SVG: `role="img"` plus `aria-label="…"`, or a `<title>` as the first child referenced by `aria-labelledby`.
|
|
87
|
+
- Simple cases: `<img src="icon.svg" alt="…">` is the most reliable delivery.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Decorative icon inside a labeled button
|
|
91
|
+
<button aria-label="Close">
|
|
92
|
+
<svg aria-hidden="true" focusable="false">…</svg>
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
// Standalone meaningful icon
|
|
96
|
+
<svg role="img" aria-label="Verified account">…</svg>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Video and audio
|
|
100
|
+
|
|
101
|
+
Prerecorded video needs captions; provide transcripts for audio. Never autoplay with sound, and always render controls.
|