@tamsensedev/runner 0.0.0-stage → 0.1.0

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.
Files changed (79) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +45 -2
  3. package/dist/cli.js +2234 -0
  4. package/dist/cli.js.map +1 -0
  5. package/library/.claude-plugin/plugin.json +5 -0
  6. package/library/THIRD_PARTY_NOTICES.md +123 -0
  7. package/library/agents/code-architect.md +67 -0
  8. package/library/agents/code-explorer.md +61 -0
  9. package/library/agents/code-reviewer.md +73 -0
  10. package/library/agents/code-simplifier.md +62 -0
  11. package/library/agents/design-critic.md +52 -0
  12. package/library/agents/second-opinion.md +64 -0
  13. package/library/agents/security-auditor.md +86 -0
  14. package/library/agents/silent-failure-hunter.md +70 -0
  15. package/library/agents/test-coverage-analyzer.md +78 -0
  16. package/library/agents/test-runner.md +48 -0
  17. package/library/agents/ui-tester.md +47 -0
  18. package/library/licenses/Apache-2.0.txt +202 -0
  19. package/library/skills/accessibility/SKILL.md +144 -0
  20. package/library/skills/accessibility/references/wcag-checks.md +168 -0
  21. package/library/skills/addressing-review-feedback/SKILL.md +109 -0
  22. package/library/skills/app-screens/SKILL.md +96 -0
  23. package/library/skills/app-screens/references/shell-kpis-charts.md +108 -0
  24. package/library/skills/app-screens/references/states-density-phone.md +54 -0
  25. package/library/skills/app-screens/references/tables-forms-settings.md +104 -0
  26. package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -0
  27. package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -0
  28. package/library/skills/clean-commits/SKILL.md +85 -0
  29. package/library/skills/design-review/SKILL.md +107 -0
  30. package/library/skills/design-review/references/critique-passes.md +101 -0
  31. package/library/skills/design-review/references/screenshot-procedure.md +113 -0
  32. package/library/skills/differential-security-review/LICENSE +427 -0
  33. package/library/skills/differential-security-review/SKILL.md +105 -0
  34. package/library/skills/differential-security-review/references/adversarial.md +115 -0
  35. package/library/skills/differential-security-review/references/methodology.md +145 -0
  36. package/library/skills/differential-security-review/references/patterns.md +108 -0
  37. package/library/skills/false-positive-check/LICENSE +427 -0
  38. package/library/skills/false-positive-check/SKILL.md +95 -0
  39. package/library/skills/false-positive-check/references/bug-class-verification.md +128 -0
  40. package/library/skills/false-positive-check/references/deep-verification.md +113 -0
  41. package/library/skills/false-positive-check/references/evidence-templates.md +93 -0
  42. package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -0
  43. package/library/skills/false-positive-check/references/gate-reviews.md +36 -0
  44. package/library/skills/false-positive-check/references/standard-verification.md +80 -0
  45. package/library/skills/focused-implementation/SKILL.md +104 -0
  46. package/library/skills/implementation-plan/SKILL.md +66 -0
  47. package/library/skills/interface-design/SKILL.md +144 -0
  48. package/library/skills/interface-design/references/craft-defaults.md +130 -0
  49. package/library/skills/interface-design/references/new-direction.md +62 -0
  50. package/library/skills/interface-design/references/writing.md +52 -0
  51. package/library/skills/interface-polish/SKILL.md +103 -0
  52. package/library/skills/interface-polish/references/detail-rules.md +154 -0
  53. package/library/skills/interface-polish/references/worst-case-data.md +158 -0
  54. package/library/skills/motion-design/SKILL.md +106 -0
  55. package/library/skills/motion-design/references/recipes.md +243 -0
  56. package/library/skills/postgres-performance/SKILL.md +97 -0
  57. package/library/skills/postgres-performance/references/rules.md +265 -0
  58. package/library/skills/project-orientation/SKILL.md +77 -0
  59. package/library/skills/reviewing-changes/SKILL.md +183 -0
  60. package/library/skills/safe-database-changes/SKILL.md +111 -0
  61. package/library/skills/safe-database-changes/references/backfills.md +75 -0
  62. package/library/skills/safe-database-changes/references/migration-tools.md +69 -0
  63. package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -0
  64. package/library/skills/security-review/SKILL.md +132 -0
  65. package/library/skills/security-review/references/checks-by-area.md +107 -0
  66. package/library/skills/security-review/references/do-not-report.md +86 -0
  67. package/library/skills/spec-writing/SKILL.md +144 -0
  68. package/library/skills/systematic-debugging/SKILL.md +130 -0
  69. package/library/skills/systematic-debugging/references/flaky-tests.md +51 -0
  70. package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -0
  71. package/library/skills/task-interview/SKILL.md +84 -0
  72. package/library/skills/test-driven-development/SKILL.md +116 -0
  73. package/library/skills/test-driven-development/references/writing-good-tests.md +85 -0
  74. package/library/skills/ui-testing/SKILL.md +99 -0
  75. package/library/skills/ui-testing/references/headless-check.md +262 -0
  76. package/library/skills/ui-testing/references/playwright-patterns.md +134 -0
  77. package/library/skills/verification-before-completion/SKILL.md +113 -0
  78. package/library/skills/web-interface-guidelines/SKILL.md +202 -0
  79. package/package.json +44 -5
@@ -0,0 +1,154 @@
1
+ # Detail rules with code
2
+
3
+ Read this when applying a specific polish rule and you want the exact values. Examples are in plain CSS with a Tailwind equivalent where it helps; translate them into the project's styling system and tokens.
4
+
5
+ ## Concentric radius
6
+
7
+ ```css
8
+ .card { border-radius: 16px; padding: 8px; } /* 8 + 8 */
9
+ .card-inner { border-radius: 8px; }
10
+ ```
11
+
12
+ Tailwind: `rounded-2xl p-2` (16px radius, 8px padding) on the outer element, `rounded-lg` (8px = 16 - 8) on the inner one.
13
+
14
+ With padding over about 24px, the layers read as separate surfaces; choose each radius from the scale.
15
+
16
+ ## Optical alignment
17
+
18
+ ```css
19
+ .button-with-trailing-icon { padding-inline: 16px 14px; } /* icon side = text side - 2px */
20
+ .play-button svg { translate: 2px 0; } /* triangle's visual centre */
21
+ ```
22
+
23
+ For lopsided icons (stars, carets), adjust the SVG `viewBox` or path so no wrapper offset is needed.
24
+
25
+ ## Shadow as border, and elevation
26
+
27
+ ```css
28
+ :root {
29
+ --shadow-border:
30
+ 0 0 0 1px rgb(0 0 0 / 0.06),
31
+ 0 1px 2px -1px rgb(0 0 0 / 0.06),
32
+ 0 2px 4px 0 rgb(0 0 0 / 0.04);
33
+ --shadow-border-hover:
34
+ 0 0 0 1px rgb(0 0 0 / 0.08),
35
+ 0 1px 2px -1px rgb(0 0 0 / 0.08),
36
+ 0 2px 4px 0 rgb(0 0 0 / 0.06);
37
+ }
38
+ .dark {
39
+ --shadow-border: 0 0 0 1px rgb(255 255 255 / 0.08);
40
+ --shadow-border-hover: 0 0 0 1px rgb(255 255 255 / 0.13);
41
+ }
42
+ .card { box-shadow: var(--shadow-border); transition: box-shadow 150ms ease-out; }
43
+ .card:hover { box-shadow: var(--shadow-border-hover); }
44
+ ```
45
+
46
+ | Prefer a shadow | Keep a border |
47
+ |---|---|
48
+ | Cards and buttons on varied backgrounds | Dividers between list items |
49
+ | Floating layers (menus, popovers, dialogs) | Table cell boundaries |
50
+ | Hover lift on clickable cards | Form input outlines |
51
+
52
+ Use this only if it matches the product's depth strategy; a product built on plain borders keeps borders.
53
+
54
+ ## Image outlines
55
+
56
+ ```css
57
+ img.media { outline: 1px solid rgb(0 0 0 / 0.1); outline-offset: -1px; }
58
+ .dark img.media { outline-color: rgb(255 255 255 / 0.1); }
59
+ ```
60
+
61
+ Tailwind: `outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10`. Pure black or white only, never a tinted grey from the palette.
62
+
63
+ ## Hit areas
64
+
65
+ ```css
66
+ .icon-button { position: relative; width: 20px; height: 20px; }
67
+ .icon-button::after {
68
+ content: "";
69
+ position: absolute;
70
+ inset: 50% auto auto 50%;
71
+ width: 44px; height: 44px;
72
+ translate: -50% -50%;
73
+ }
74
+ ```
75
+
76
+ If the extended area would overlap a neighbour, shrink it to the largest size that does not.
77
+
78
+ ## Type rendering
79
+
80
+ ```css
81
+ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
82
+ h1, h2, h3 { text-wrap: balance; } /* short text only: browsers balance up to about 6 lines */
83
+ p, li, figcaption { text-wrap: pretty; }
84
+ .num { font-variant-numeric: tabular-nums; }
85
+ ```
86
+
87
+ `tabular-nums` belongs on counters, timers, prices that update and table columns, not on phone numbers, version strings or decorative display numbers. Some fonts change the look of "1" with it; check it renders well.
88
+
89
+ Font family is not a polish decision: use the product's type system and never add a paid face for polish.
90
+
91
+ ## Overflow in rows
92
+
93
+ ```css
94
+ .row { display: flex; align-items: center; gap: 12px; }
95
+ .row-avatar { flex-shrink: 0; }
96
+ .row-text { min-width: 0; flex: 1; }
97
+ .row-email { overflow-wrap: anywhere; }
98
+ .row-badge { flex-shrink: 0; white-space: nowrap; }
99
+ .row-menu { flex-shrink: 0; }
100
+ ```
101
+
102
+ Grid equivalent: `grid-template-columns: auto minmax(0, 1fr) auto`. When text can wrap to several lines, top-align the row (`align-items: flex-start`) so the avatar does not drift.
103
+
104
+ ## Icons
105
+
106
+ | Text beside the icon | Stroke on a 24px grid |
107
+ |---|---|
108
+ | Regular (400), 14 to 16px | 1.5px |
109
+ | Medium or semibold (500 to 600) | 2px |
110
+ | Bold (700) or standalone emphasis | 2.5px |
111
+
112
+ ```html
113
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">...</svg>
114
+ ```
115
+
116
+ ```css
117
+ .icon-button { color: var(--text-muted); }
118
+ .icon-button:hover { color: var(--text); }
119
+ .icon-button[aria-pressed="true"] { color: var(--accent); }
120
+ ```
121
+
122
+ Strip hard-coded `fill` and `stroke` colours from imported icons. Inline icons are about 1em to 1.25em. Directional icons (back, forward, chevrons, send) flip in RTL; logos, checkmarks, clocks and media controls do not. Icon-only buttons need an accessible name; decorative icons get `aria-hidden="true"`.
123
+
124
+ ## Press feedback and transitions
125
+
126
+ ```css
127
+ .button {
128
+ transition-property: transform, background-color;
129
+ transition-duration: 150ms;
130
+ transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
131
+ }
132
+ .button:active:not(:disabled) { transform: scale(0.97); }
133
+
134
+ @media (hover: hover) and (pointer: fine) {
135
+ .card-link:hover { background-color: var(--surface-hover); }
136
+ }
137
+ ```
138
+
139
+ Tailwind: `transition-[transform,background-color] duration-150 active:not-disabled:scale-[0.97]`. Never `transition-all`. Use `will-change: transform` only to cure an observed first-frame stutter, never broadly.
140
+
141
+ ## Focus
142
+
143
+ ```css
144
+ :focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
145
+ .input:focus-visible { outline-offset: 0; }
146
+ ```
147
+
148
+ Never `outline: none` without a replacement. Sticky headers and footers must not cover the focused element: `scroll-padding-top` on the scroll container equal to the header height.
149
+
150
+ ## Overlays and clipping
151
+
152
+ A menu inside an `overflow: hidden` or `overflow: auto` ancestor is clipped. Use the project's popover or portal component, `<dialog>`, the `popover` attribute, or `position: fixed` with collision handling. Popovers scale from their trigger (`transform-origin` at the trigger), dialogs from the centre.
153
+
154
+ Sources: adapted from jakubkrehel/make-interfaces-feel-better surfaces, typography, icons, animations and performance references (MIT, changed: values reconciled to scale(0.97), Motion-specific code removed, plain CSS first), emilkowalski/skills break-ui failure signatures (MIT) and pbakaus/impeccable operate reference (Apache-2.0, changed: rewritten).
@@ -0,0 +1,158 @@
1
+ # Stress-testing with worst-case data
2
+
3
+ Read this when checking that a component or screen survives real data, before finishing UI work or as part of a review.
4
+
5
+ The component was almost certainly built against kind data: "Jane Doe, jane@acme.com, 12 members". Production has Aleksandra Wiśniewska-Kowalczyk, `bartholomew.fitzgerald@northwind-industries-holdings.example.com`, an intern called Jo and a workspace with 1,284 members.
6
+
7
+ ## Rules
8
+
9
+ 1. **Plausible or schema-backed, never random.** Each value is something a real user could produce, or the actual limit from the validation schema, database column or API contract. Five thousand "a" characters prove nothing. If no limit exists, that is a finding ("unbounded"); test something long but believable.
10
+ 2. **Change the data, not the component.** The worst case enters through the same boundary as the demo data: the fixture, mock, story args, seed, or an intercepted API response. Never edit markup or CSS to provoke a break.
11
+ 3. **One dataset, many failures.** Spread different worst cases across the first rows (row 1 the long name, row 2 the long email, row 3 the one-letter name), the way real data mixes them.
12
+ 4. **Do not stop at long text.** The breaks that ship are the short name, the missing avatar, "1 members", the empty list, the translated badge.
13
+ 5. **Leave no test toggles in product code.** In a code step, a worst-case fixture may be committed only where the project keeps fixtures (tests, stories, mocks), following its conventions. In a review step, use only interception or existing fixtures and change nothing.
14
+
15
+ ## Phase 1: map what the component renders
16
+
17
+ List every value it shows, with its source, type, limit and whether it is optional:
18
+
19
+ | Field | Source | Type | Limit | Optional |
20
+ |---|---|---|---|---|
21
+ | name | member.name | string | 255 (schema.ts:14) | no |
22
+ | email | member.email | string | none found | no |
23
+ | role | member.title | string | 120 | yes |
24
+ | status | enum | active / invited / expired | n/a | no |
25
+ | count | workspace.memberCount | int | n/a | no |
26
+
27
+ Include the values people forget: counts in headers, relative times, badge and status text, labels that come from data, tooltips, avatar images, and the list length itself. Find limits in validation schemas, migrations, API types and `maxlength` attributes; note where front end and back end disagree.
28
+
29
+ ## Phase 2: build the worst case
30
+
31
+ Pick values from the catalog below. Also cover cases that are not one dataset:
32
+
33
+ - **Empty**: zero items, and no results after filtering.
34
+ - **One**: a single item, and every count at exactly 1.
35
+ - **Huge**: the realistic upper bound (1,000 or more rows if unpaginated).
36
+
37
+ ## Phase 3: look
38
+
39
+ Check the worst case:
40
+
41
+ - at the component's real container width (a sidebar list is not a full-page list), then at 320px and the widest supported layout;
42
+ - with browser zoom at 200%, because text grows and boxes do not;
43
+ - in dark mode and with `dir="rtl"` if the product supports them.
44
+
45
+ Screenshot both demo and worst case at each width (procedure in `design-review`). Say which findings you saw and which you inferred from code.
46
+
47
+ ## Catalog
48
+
49
+ Use `example.com`, `example.org` or `.test` domains so fixtures never point at real inboxes.
50
+
51
+ **People and names**
52
+
53
+ | Value | Breaks |
54
+ |---|---|
55
+ | `Aleksandra Wiśniewska-Kowalczyk` | Long, hyphenated, diacritics; wraps at the hyphen |
56
+ | `Christopher Alexander Montgomery III` | Suffix; naive initials give "CI" |
57
+ | `Jo`, `J` | Near-empty name column; one-letter initials; tiny click target if the name is the link |
58
+ | `Đặng Thị Ngọc Hân` | Stacked diacritics clipped by tight line height with `overflow: hidden` |
59
+ | `王秀英` | No spaces; "first plus last word" initials find one word |
60
+ | `نور الهدى عبد الرحمن` | RTL; punctuation and icons on the wrong side without `dir="auto"` |
61
+ | `Seán O'Brien-Ó Súilleabháin` | Apostrophe and accents: escaping, initials, search |
62
+ | A name starting with an emoji ("Fox" after a fox emoji), or with a skin-tone and joiner sequence before "Priya" | `charAt(0)` returns half a character; slicing splits the sequence |
63
+ | ` Sam Lee ` | Leading, trailing and repeated spaces |
64
+ | missing name | Only an email; the UI needs a fallback |
65
+
66
+ **Emails, URLs, identifiers**
67
+
68
+ | Value | Breaks |
69
+ |---|---|
70
+ | `bartholomew.fitzgerald@northwind-industries-holdings.example.com` | No wrap points; pushes siblings off the row |
71
+ | `a@b.co` | Layout that assumed a long email looks empty |
72
+ | `first.last+billing-notifications@example.com` | Plus addressing rejected or truncated |
73
+ | A long URL with query string | End truncation hides the part that differs |
74
+ | `9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f` | UUID width; candidate for middle truncation |
75
+ | `Q3 Board Deck FINAL (revised) v12 [approved].pdf` | End truncation hides version and extension |
76
+
77
+ **Labels and text from data**
78
+
79
+ | Value | Breaks |
80
+ |---|---|
81
+ | `Senior Product Design Engineer, Platform Infrastructure` | Wraps to three lines in a secondary slot |
82
+ | `Invitation expired 12 days ago` as a badge | Badge wraps or squeezes the name |
83
+ | `Benachrichtigungseinstellungen` | German compound, no break points |
84
+ | French or German UI strings | About 30% longer than English |
85
+ | Twelve tags on one item | Tag rows become a wall; needs "+8" |
86
+ | Empty string or whitespace title | Collapsed heading, zero-height row |
87
+ | `<script>alert(1)</script>`, `&amp;`, `**bold**` | Must render as literal text |
88
+ | A 2,000-character description | Clamping, "show more", textarea growth |
89
+
90
+ **Numbers and money**
91
+
92
+ | Value | Breaks |
93
+ |---|---|
94
+ | 0 | "0 members", empty bars, division by zero in percentages |
95
+ | 1 | "1 members", "1 days ago" |
96
+ | 1284, 1000000 | Missing separators; badge width; consider compact form where precision does not matter |
97
+ | 12345678.9 as currency | Overflowing totals columns |
98
+ | -42.5 | Sign, colour logic, accounting parentheses |
99
+ | 0.1 + 0.2 | `0.30000000000000004` rendered raw |
100
+ | 142%, -3% | Progress bars past their bounds |
101
+ | null, undefined, NaN | Rendered literally |
102
+ | A live value 99 then 100 | Width jump without tabular figures |
103
+
104
+ **Collections**: 0 items; 1 item (grids that look broken with one card); exactly the page size and one more; 1,000+ unpaginated; one item ten times larger than the others; items with identical names.
105
+
106
+ **Time**: now ("0 seconds ago"); 12 days, 11 months, 3 years ago (switch to absolute dates); a future date; `1970-01-01`; a time near midnight in another time zone; a duration of 1,284 hours.
107
+
108
+ **Images**: avatar URL that fails (fallback to initials); no avatar; a 4000 x 200 panorama and a 200 x 4000 strip (need `object-fit: cover`); a dark transparent logo on a dark theme; a slow image (needs fixed dimensions).
109
+
110
+ **States**: loading; API error (no raw `TypeError` text on screen); partial data; every status value at once; no permission; the current user in the list.
111
+
112
+ ## Failure signatures
113
+
114
+ | What you see | Cause | Fix |
115
+ |---|---|---|
116
+ | Avatar or icon squashed into an oval | Flex child shrinking | `flex-shrink: 0` on fixed-size items |
117
+ | Text overflows instead of wrapping or truncating | `min-width: auto` on a flex or grid child | `min-width: 0`, or `minmax(0, 1fr)` |
118
+ | Email or URL runs past the edge | No break opportunities | `overflow-wrap: anywhere` on that element |
119
+ | Trailing menu pushed off-screen | Middle content took the space | `min-width: 0` on the middle, `flex-shrink: 0` on the action |
120
+ | Badge wraps onto two lines | Badge allowed to shrink | `white-space: nowrap; flex-shrink: 0`, decide what yields |
121
+ | Avatar adrift beside a three-line name | `align-items: center` on rows of varying height | Top-align once text can wrap |
122
+ | Last row cut mid-glyph | Fixed height without scroll affordance | Visible scroll or a fade, intended `overflow` |
123
+ | Heading breaks mid-word | `word-break: break-all` | `overflow-wrap: anywhere` |
124
+ | Wrong initials | `split(' ')[0][0]` | Grapheme-aware initials (`Intl.Segmenter`), first and last word, icon fallback |
125
+ | Orphaned dash where an optional field was | Placeholder for a missing value | Omit the line, or reserve its height on purpose |
126
+ | "1 members" | Hard-coded plural | `Intl.PluralRules` or the project's i18n |
127
+ | Numbers jitter as they update | Proportional figures | `tabular-nums` |
128
+ | `1284`, `NaN`, `undefined` | Raw number rendered | `Intl.NumberFormat`, guard null |
129
+ | Translated button label overflows | Fixed width | Width from content with a `min-width` |
130
+ | Tall scripts clipped | Tight line height with hidden overflow | Looser line height, no clipping on text |
131
+ | Broken-image icon | No error fallback | Fall back to initials or a neutral icon |
132
+ | Truncated text with no way to read it | Ellipsis only | Title or tooltip, and the full value in a detail view |
133
+ | 1,000 rows stutter | Every row rendered | Virtualize or paginate |
134
+ | Raw `<b>`, `&amp;` or `**text**` shown | Wrong escaping layer | Escape once at render; never inject user HTML |
135
+
136
+ ## Truncate, wrap or clamp
137
+
138
+ Decide per field:
139
+
140
+ - **Wrap** what identifies something (names, titles in a detail view). Two lines is fine; four means the column is too narrow.
141
+ - **Truncate at the end** for secondary metadata where the start carries the meaning (role, preview text), always with a way to see the full value.
142
+ - **Truncate in the middle** where items differ at the end (file names, paths, hashes, emails sharing a long domain).
143
+ - **Clamp** (`line-clamp: 2`) multi-line previews in cards so heights stay predictable.
144
+ - **Never truncate** numbers, amounts, dates, or anything people compare.
145
+
146
+ ## Output
147
+
148
+ | # | Severity | Field | Worst-case value | What happens | Fix (`file:line`) |
149
+ |---|---|---|---|---|---|
150
+ | 1 | Broken | email | `bartholomew.fitzgerald@...example.com` | Menu unreachable at 400px | `min-width: 0` on text column, `overflow-wrap: anywhere` on email |
151
+ | 2 | Ugly | count | 1 | "1 members" | Plural rules |
152
+ | 3 | Fragile | name | n/a | No max length in schema or form | Matching limits in both |
153
+
154
+ Severity: **Broken** (unreadable, unreachable, wrong data), **Ugly** (readable but visibly wrong), **Fragile** (fine now, one realistic step from breaking). Also list what held up, so the test is visibly real.
155
+
156
+ In a code step, fix the breaks; where there are several right answers (truncate or wrap, what an empty role shows), choose by the rules above, apply it, and list the choice in the report. In a review step, report only.
157
+
158
+ Sources: adapted from emilkowalski/skills break-ui SKILL.md and CATALOG.md (MIT, changed: no interactive toggle shipped, no "report and wait for the user" step; code steps fix, review steps report) and pbakaus/impeccable harden reference (Apache-2.0, changed: rewritten).
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: motion-design
3
+ description: Use when adding, changing or reviewing UI animation or transitions: press and hover feedback, menus, dialogs, drawers, toasts, list changes, view transitions, marketing-page motion. Covers whether to animate at all, durations, easing, performance, interruption and reduced motion, CSS first, then the project's motion library.
4
+ ---
5
+
6
+ # Motion design
7
+
8
+ Motion should be felt, not watched. Good motion explains a change (where the menu came from, that the click registered, what was removed) and then gets out of the way. Generated UI tends to do the opposite: a fade-up on every block, hover lifts on every card, bounce easing, 400ms dropdowns.
9
+
10
+ The project's motion tokens, existing transitions and installed animation library win over every value here. If the codebase already animates dialogs at 200ms with its own curve, new dialogs do the same.
11
+
12
+ ## Decide before you animate
13
+
14
+ ### 1. Should this animate at all?
15
+
16
+ | How often people see it | Decision |
17
+ |---|---|
18
+ | 100+ times a day (keyboard shortcuts, command palette, typing, list navigation by keys) | No animation |
19
+ | Tens of times a day (hover, tab switches, row selection) | Instant, or a 100 to 150ms colour or opacity change |
20
+ | Occasionally (menus, dialogs, drawers, toasts, expanding sections) | Standard animation |
21
+ | Rarely (first run, a completed setup, an empty state the first time) | May carry a little more character |
22
+
23
+ Never animate keyboard-initiated actions. Product screens load into the task: no orchestrated page-load sequences.
24
+
25
+ ### 2. What is it for?
26
+
27
+ Valid purposes: feedback (the press registered), state (open, selected, saved), spatial continuity (the toast leaves the way it came, the drawer slides from its edge), preventing a jarring jump (content appearing or disappearing), and on marketing pages, explaining how something works. "It looks nice" is not a purpose for something seen often.
28
+
29
+ ### 3. Easing
30
+
31
+ - Entering or exiting: **ease-out**. Default curve `cubic-bezier(0.23, 1, 0.32, 1)`.
32
+ - Moving or morphing on screen: **ease-in-out**, `cubic-bezier(0.77, 0, 0.175, 1)`.
33
+ - Hover and colour changes: `ease`.
34
+ - Constant motion (progress, marquee): `linear`.
35
+ - Drawers and sheets: `cubic-bezier(0.32, 0.72, 0, 1)` reads like native mobile sheets.
36
+ - Never `ease-in` for UI: it delays the first frame, exactly when the person is watching. No bounce or elastic curves by reflex.
37
+
38
+ ### 4. Duration
39
+
40
+ | Element | Duration |
41
+ |---|---|
42
+ | Press feedback | 100 to 160ms |
43
+ | Hover, colour | 100 to 150ms |
44
+ | Tooltip, small popover | 125 to 200ms |
45
+ | Dropdown, select, menu | 150 to 250ms |
46
+ | Dialog, drawer, sheet | 200 to 300ms (large drawers up to 500ms) |
47
+ | Layout or view transition | 250 to 400ms |
48
+ | Authored moment on a marketing page | 500 to 800ms |
49
+
50
+ UI motion stays under 300ms. Exits are faster and quieter than entrances (about 75% of the enter time, a small offset rather than the full distance).
51
+
52
+ ## Rules
53
+
54
+ 1. **Animate `transform` and `opacity`.** They skip layout and paint. Animate `width`, `height`, `top`, `left` or margins only with a technique built for it (FLIP, grid-template-rows `0fr` to `1fr`, View Transitions). `filter`, `clip-path` and shadows are fine on small, bounded elements.
55
+ 2. **Never `transition: all`.** Name the properties.
56
+ 3. **Interruptible by default.** Use CSS transitions (or springs) for anything the person can trigger again mid-way; they retarget. Keyframes restart from zero; keep them for one-shot sequences.
57
+ 4. **Nothing appears from nothing.** Enter from `scale(0.95)` to `scale(0.97)` with opacity 0, never `scale(0)`.
58
+ 5. **Origin-aware popovers.** Menus, popovers and tooltips scale from their trigger (`transform-origin` at the trigger side). Dialogs stay centred.
59
+ 6. **Stagger only real lists**, 30 to 80ms between items, total under about 300ms, and never block interaction while it plays.
60
+ 7. **Press feedback**: `scale(0.97)` on `:active` for buttons (not below 0.95).
61
+ 8. **Tooltips**: a short delay before the first one; once one is open, neighbours open instantly with no animation.
62
+ 9. **Hover motion only for fine pointers**: wrap it in `@media (hover: hover) and (pointer: fine)`.
63
+ 10. **Content is visible by default.** If a script fails or an observer never fires, nothing stays hidden. Scroll reveals start from an already-visible state or have a no-script fallback.
64
+ 11. **Reduced motion means fewer and gentler, not none.** Under `prefers-reduced-motion: reduce`, remove movement, scaling and parallax; keep short opacity and colour changes that confirm state. Stop decorative loops. Autoplaying motion over 5 seconds needs a pause control.
65
+ 12. **Motion is never the only signal.** Every animated state change also has a static cue (colour, icon, text).
66
+ 13. **`will-change` only to cure an observed first-frame stutter**, on the element and only during the animation.
67
+ 14. **No new dependency for an effect the stack can already express.** CSS transitions, `@starting-style`, the Web Animations API and View Transitions cover most UI needs.
68
+
69
+ ## Implement with what the project has
70
+
71
+ In order: CSS transitions and keyframes; the Web Animations API for programmatic control; the project's existing library (Vue `<Transition>`, Svelte transitions, Angular animations, Motion for React, GSAP); View Transitions for continuity between states or routes. Recipes for each are in [references/recipes.md](references/recipes.md).
72
+
73
+ ## Marketing pages
74
+
75
+ A page read once may carry one authored moment: a hero sequence, one interactive demo, a single scroll-linked reveal whose scroll relationship means something. One strong idea beats an identical fade-up on every section. Keep it under about 800ms, make it skippable by scrolling, give it a reduced-motion version, and keep the page complete without it.
76
+
77
+ ## Check
78
+
79
+ - Play it at slow speed (DevTools Animations panel at 10%, or multiply durations by 5 temporarily). Look for two states overlapping in a crossfade, wrong origins, properties out of sync, a jump at the end.
80
+ - Trigger it twice quickly and mid-way: it reverses smoothly, nothing sticks.
81
+ - Toggle reduced motion (DevTools rendering panel, or Playwright `reducedMotion: 'reduce'`): state changes still read.
82
+ - Throttle the CPU 4x: no dropped frames on the common path.
83
+ - A screenshot taken mid-load shows no blank bands where a reveal did not fire.
84
+
85
+ ## Common mistakes
86
+
87
+ | Mistake | Fix |
88
+ |---|---|
89
+ | `transition: all 300ms` | `transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out)` |
90
+ | `scale(0)` entrance | `scale(0.95)` with opacity 0 |
91
+ | `ease-in` on a dropdown | ease-out, custom curve |
92
+ | Popover scaling from its centre | `transform-origin` at the trigger |
93
+ | Animation on a keyboard shortcut | Remove it |
94
+ | 400ms dropdown | 150 to 250ms |
95
+ | Keyframes on a toggle that can be re-triggered | A transition |
96
+ | Hover scale that fires on tap | Gate with the hover and pointer media query |
97
+ | Every section fades up on scroll | Remove; one authored moment at most, on marketing pages |
98
+ | Exit as slow as the enter | Shorter, smaller exit |
99
+
100
+ ## Output in reviews
101
+
102
+ A table with **Before**, **After** and **Why**, one row per issue, citing `file:line`.
103
+
104
+ Related: `interface-polish`, `interface-design`, `accessibility`.
105
+
106
+ Sources: adapted from emilkowalski/skills emil-design-eng (MIT, changed: framework-neutral, React and Motion specifics moved to examples, "initial response" step removed), jakubkrehel/make-interfaces-feel-better animations reference (MIT) and pbakaus/impeccable animate and operate references (Apache-2.0, changed: rewritten, ask steps removed).
@@ -0,0 +1,243 @@
1
+ # Motion recipes
2
+
3
+ Read this when implementing a specific transition. Use the project's existing motion tokens and library if it has them; these recipes are CSS first, then examples in common libraries. Do not add a library to use a recipe.
4
+
5
+ ## Tokens
6
+
7
+ ```css
8
+ :root {
9
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
10
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
11
+ --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
12
+ --duration-press: 150ms;
13
+ --duration-popover: 180ms;
14
+ --duration-dialog: 240ms;
15
+ }
16
+ ```
17
+
18
+ ## Reduced motion pattern
19
+
20
+ Define movement only for people who have not asked to reduce it; keep the opacity change for everyone.
21
+
22
+ ```css
23
+ .toast { transition: opacity 200ms ease; }
24
+
25
+ @media (prefers-reduced-motion: no-preference) {
26
+ .toast {
27
+ transition:
28
+ opacity 200ms var(--ease-out),
29
+ transform 200ms var(--ease-out);
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Press feedback
35
+
36
+ ```css
37
+ .button {
38
+ transition: transform var(--duration-press) var(--ease-out);
39
+ }
40
+ .button:active:not(:disabled) { transform: scale(0.97); }
41
+ ```
42
+
43
+ ## Popover or menu (native popover, no JavaScript)
44
+
45
+ ```css
46
+ .menu[popover] {
47
+ transform-origin: top left;
48
+ opacity: 0;
49
+ transform: scale(0.97);
50
+ transition:
51
+ opacity var(--duration-popover) var(--ease-out),
52
+ transform var(--duration-popover) var(--ease-out),
53
+ overlay var(--duration-popover) allow-discrete,
54
+ display var(--duration-popover) allow-discrete;
55
+ }
56
+ .menu[popover]:popover-open { opacity: 1; transform: scale(1); }
57
+
58
+ @starting-style {
59
+ .menu[popover]:popover-open { opacity: 0; transform: scale(0.97); }
60
+ }
61
+ ```
62
+
63
+ Browsers without `@starting-style` simply show the menu without animation. Set `transform-origin` to the side of the trigger. With a headless library, use the origin variable it exposes (for example `var(--radix-popover-content-transform-origin)` or `var(--transform-origin)`).
64
+
65
+ ## Dialog
66
+
67
+ Same pattern on `dialog[open]`, centred origin, `--duration-dialog`, plus a backdrop fade:
68
+
69
+ ```css
70
+ dialog::backdrop { background: rgb(0 0 0 / 0.4); transition: opacity var(--duration-dialog) ease; }
71
+ ```
72
+
73
+ Exit about 75% of the enter duration.
74
+
75
+ ## Drawer or bottom sheet
76
+
77
+ ```css
78
+ .sheet {
79
+ transform: translateY(100%);
80
+ transition: transform 300ms var(--ease-drawer);
81
+ }
82
+ .sheet[data-open="true"] { transform: translateY(0); }
83
+ ```
84
+
85
+ Percent translations move by the element's own size, so they work at any height. Add `overscroll-behavior: contain` on the sheet's scroll area.
86
+
87
+ ## Expand and collapse without animating height
88
+
89
+ ```css
90
+ .collapsible {
91
+ display: grid;
92
+ grid-template-rows: 0fr;
93
+ transition: grid-template-rows 250ms var(--ease-out);
94
+ }
95
+ .collapsible[data-open="true"] { grid-template-rows: 1fr; }
96
+ .collapsible > .inner { overflow: hidden; }
97
+ ```
98
+
99
+ ## Toast
100
+
101
+ Enter from the edge it lives on, leave the same way, a little faster:
102
+
103
+ ```css
104
+ .toast { transform: translateY(0); opacity: 1; }
105
+ .toast[data-state="entering"], .toast[data-state="leaving"] { transform: translateY(100%); opacity: 0; }
106
+ .toast[data-state="leaving"] { transition-duration: 150ms; }
107
+ ```
108
+
109
+ Pause auto-dismiss timers while the toast is hovered or the tab is hidden.
110
+
111
+ ## Icon swap (copy to copied, play to pause)
112
+
113
+ Keep both icons in the DOM, one positioned over the other, and cross-fade:
114
+
115
+ ```css
116
+ .icon-swap { position: relative; display: inline-grid; }
117
+ .icon-swap > * {
118
+ grid-area: 1 / 1;
119
+ transition:
120
+ opacity 250ms cubic-bezier(0.2, 0, 0, 1),
121
+ transform 250ms cubic-bezier(0.2, 0, 0, 1),
122
+ filter 250ms cubic-bezier(0.2, 0, 0, 1);
123
+ }
124
+ .icon-swap > [data-hidden="true"] { opacity: 0; transform: scale(0.25); filter: blur(4px); }
125
+ ```
126
+
127
+ ## Skeleton
128
+
129
+ A flat muted block at the final size. If the project pulses skeletons, use a slow opacity pulse (1.5 to 2s) and stop it under reduced motion. No shimmer gradient sweeping across the screen.
130
+
131
+ ## Web Animations API
132
+
133
+ ```js
134
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
135
+ element.animate(
136
+ reduce
137
+ ? [{ opacity: 0 }, { opacity: 1 }]
138
+ : [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }],
139
+ { duration: 200, easing: 'cubic-bezier(0.23, 1, 0.32, 1)', fill: 'backwards' },
140
+ )
141
+ ```
142
+
143
+ ## View Transitions
144
+
145
+ ```js
146
+ function update(change) {
147
+ if (!document.startViewTransition) return change()
148
+ document.startViewTransition(change)
149
+ }
150
+ ```
151
+
152
+ ```css
153
+ ::view-transition-old(root),
154
+ ::view-transition-new(root) { animation-duration: 200ms; }
155
+
156
+ @media (prefers-reduced-motion: reduce) {
157
+ ::view-transition-group(*),
158
+ ::view-transition-old(*),
159
+ ::view-transition-new(*) { animation: none; }
160
+ }
161
+ ```
162
+
163
+ Use them when continuity between two states is the point (a card expanding into its detail), not on every route change. Many routers (Astro, Nuxt, SvelteKit, Next.js) have their own switch for this; use it.
164
+
165
+ ## Vue
166
+
167
+ ```vue
168
+ <template>
169
+ <Transition name="pop">
170
+ <div v-if="open" class="menu">...</div>
171
+ </Transition>
172
+ </template>
173
+
174
+ <style>
175
+ .pop-enter-active { transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out); }
176
+ .pop-leave-active { transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out); }
177
+ .pop-enter-from,
178
+ .pop-leave-to { opacity: 0; transform: scale(0.97); }
179
+ </style>
180
+ ```
181
+
182
+ For lists, `<TransitionGroup>` with a `.list-move { transition: transform 200ms var(--ease-out); }` class animates reordering.
183
+
184
+ ## Svelte
185
+
186
+ ```svelte
187
+ <script>
188
+ import { fly } from 'svelte/transition'
189
+ import { cubicOut } from 'svelte/easing'
190
+ </script>
191
+
192
+ {#if open}
193
+ <div class="menu" transition:fly={{ y: -4, duration: 180, easing: cubicOut }}>...</div>
194
+ {/if}
195
+ ```
196
+
197
+ ## Motion for React
198
+
199
+ Only if `motion` or `framer-motion` is already installed; import from whichever the project uses.
200
+
201
+ ```jsx
202
+ import { AnimatePresence, motion } from 'motion/react'
203
+
204
+ <AnimatePresence initial={false}>
205
+ {open && (
206
+ <motion.div
207
+ key="panel"
208
+ initial={{ opacity: 0, transform: 'scale(0.97)' }}
209
+ animate={{ opacity: 1, transform: 'scale(1)' }}
210
+ exit={{ opacity: 0, transform: 'scale(0.97)', transition: { duration: 0.12 } }}
211
+ transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}
212
+ />
213
+ )}
214
+ </AnimatePresence>
215
+ ```
216
+
217
+ `initial={false}` stops elements already present on first render from animating in. Full `transform` strings stay on the compositor when the main thread is busy; the `x` and `y` shorthands do not. For gestures and drags, springs (`{ type: 'spring', duration: 0.3, bounce: 0 }`) keep their velocity when interrupted; keep bounce at 0 in product UI. `useReducedMotion()` returns the user's preference.
218
+
219
+ ## GSAP
220
+
221
+ Only if GSAP is already installed.
222
+
223
+ ```js
224
+ import { gsap } from 'gsap'
225
+
226
+ const mm = gsap.matchMedia()
227
+ mm.add('(prefers-reduced-motion: no-preference)', () => {
228
+ gsap.from('.step', { opacity: 0, y: 12, duration: 0.4, ease: 'power3.out', stagger: 0.06 })
229
+ })
230
+ ```
231
+
232
+ `gsap.matchMedia()` reverts the animation when the media query stops matching. Keep scroll-triggered work for marketing pages, and give it a version that leaves content visible.
233
+
234
+ ## Drag and gestures
235
+
236
+ - Capture the pointer once dragging starts (`setPointerCapture`) so the drag continues outside the element.
237
+ - Ignore extra touch points after the drag begins.
238
+ - Dismiss on velocity as well as distance (a quick flick is enough).
239
+ - Past a boundary, apply friction instead of a hard stop.
240
+ - On `pointercancel`, `lostpointercapture` and window `blur`, clear the dragging state so the next drag works.
241
+ - Offer a click or keyboard alternative for every gesture.
242
+
243
+ Sources: adapted from emilkowalski/skills emil-design-eng (MIT, changed: examples made framework-neutral, Vue, Svelte and GSAP examples added), jakubkrehel/make-interfaces-feel-better animations reference (MIT, changed: icon swap duration aligned to 250ms, CSS grid variant) and pbakaus/impeccable animate and harden references (Apache-2.0, changed: rewritten).