@ericrisco/rsc 1.1.4 → 1.1.6

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 (66) hide show
  1. package/README.md +81 -11
  2. package/manifest.json +56 -231
  3. package/package.json +1 -1
  4. package/scripts/doctor.js +68 -1
  5. package/scripts/install-apply.js +91 -16
  6. package/scripts/lib/divergence.js +33 -0
  7. package/scripts/lib/domains.js +3 -3
  8. package/scripts/lib/manifest-file.js +53 -0
  9. package/scripts/lib/repair.js +156 -0
  10. package/scripts/lib/ui.js +5 -0
  11. package/scripts/rsc.js +84 -11
  12. package/skills/design/SKILL.md +3 -3
  13. package/skills/design/references/design-systems.md +1 -1
  14. package/skills/design/references/inspiration-sources.md +2 -2
  15. package/skills/design/references/motion-and-interaction.md +2 -2
  16. package/skills/design-dna/evals/cases.yaml +1 -1
  17. package/skills/design-loop/SKILL.md +3 -3
  18. package/skills/design-loop/evals/cases.yaml +3 -3
  19. package/skills/expo/SKILL.md +1 -1
  20. package/skills/motion-craft/SKILL.md +125 -0
  21. package/skills/motion-craft/evals/cases.yaml +38 -0
  22. package/skills/motion-craft/scripts/verify.sh +75 -0
  23. package/skills/react/SKILL.md +2 -2
  24. package/skills/react-native/SKILL.md +1 -1
  25. package/skills/suggest/SKILL.md +25 -1
  26. package/skills/swift-ios/SKILL.md +2 -2
  27. package/skills/ui-engineering/SKILL.md +114 -0
  28. package/skills/ui-engineering/evals/cases.yaml +38 -0
  29. package/skills/variant-explorer/SKILL.md +93 -0
  30. package/skills/variant-explorer/evals/cases.yaml +38 -0
  31. package/targets/_md-block.js +17 -4
  32. package/targets/claude.js +17 -7
  33. package/targets/index.js +66 -19
  34. package/skills/animate/SKILL.md +0 -851
  35. package/skills/animate/evals/cases.yaml +0 -36
  36. package/skills/animate/references/recipes.md +0 -324
  37. package/skills/animate-expo/SKILL.md +0 -281
  38. package/skills/animate-expo/evals/cases.yaml +0 -36
  39. package/skills/animate-expo/references/recipes.md +0 -385
  40. package/skills/animation-vocabulary/SKILL.md +0 -189
  41. package/skills/animation-vocabulary/evals/cases.yaml +0 -31
  42. package/skills/apple-design/SKILL.md +0 -301
  43. package/skills/apple-design/evals/cases.yaml +0 -33
  44. package/skills/ask-sonner/SKILL.md +0 -104
  45. package/skills/ask-sonner/evals/cases.yaml +0 -33
  46. package/skills/ask-sonner/references/api.md +0 -64
  47. package/skills/design-eng/SKILL.md +0 -88
  48. package/skills/design-eng/evals/cases.yaml +0 -36
  49. package/skills/find-animation-opportunities/SKILL.md +0 -150
  50. package/skills/find-animation-opportunities/evals/cases.yaml +0 -31
  51. package/skills/improve-animations/SKILL.md +0 -126
  52. package/skills/improve-animations/evals/cases.yaml +0 -31
  53. package/skills/improve-animations/references/audit.md +0 -115
  54. package/skills/improve-animations/references/plan-template.md +0 -73
  55. package/skills/pick-ui-library/SKILL.md +0 -101
  56. package/skills/pick-ui-library/evals/cases.yaml +0 -33
  57. package/skills/pick-ui-library/references/component-galleries.md +0 -59
  58. package/skills/prototype/SKILL.md +0 -113
  59. package/skills/prototype/evals/cases.yaml +0 -31
  60. package/skills/prototype/references/picker.md +0 -197
  61. package/skills/review-animations/SKILL.md +0 -188
  62. package/skills/review-animations/evals/cases.yaml +0 -34
  63. package/skills/review-animations/references/standards.md +0 -187
  64. package/skills/review-animations/scripts/verify.sh +0 -164
  65. package/skills/write-swift/SKILL.md +0 -413
  66. package/skills/write-swift/evals/cases.yaml +0 -36
@@ -59,7 +59,7 @@ these are for when a piece is "the footer" or "the empty state".
59
59
  | [Footer.design](https://www.footer.design) | Footers only. Narrow on purpose, and the narrowness is why it is good. |
60
60
  | [Collect UI](https://collectui.com) | Daily-UI-style collections by component and pattern. |
61
61
  | [UI Garage](https://uigarage.net) | Patterns tagged by element and by industry. |
62
- | [UI Movement](https://uimovement.com) | Motion and interaction specifically — pair with the frequency gate in `../../animate/SKILL.md` before copying anything. |
62
+ | [UI Movement](https://uimovement.com) | Motion and interaction specifically — pair with the frequency gate in `../../motion-craft/SKILL.md` before copying anything. |
63
63
  | [Dark.design](https://dark.design) · [Dark Mode Design](https://darkmodedesign.com) | Dark interfaces done properly, which is a different problem from inverting a light one. |
64
64
  | [Scrnshts](https://scrnshts.club) | App Store screenshot design — a real discipline of its own. |
65
65
 
@@ -91,6 +91,6 @@ component", these outrank any gallery.
91
91
  - **[Bestfolios](https://www.bestfolios.com) returned 502 on 2026-08-24** and is excluded until it
92
92
  comes back. Do not cite it.
93
93
  - **Component ideas**, as opposed to whole pages, live in
94
- `../../pick-ui-library/references/component-galleries.md` — Aceternity, 21st.dev, React Bits.
94
+ Component galleries — Aceternity, 21st.dev, React Bits.
95
95
  - **Identity outranks all of this.** Check `02-DOCS/wiki/brand/` for what the project refuses before
96
96
  bringing an idea back. A gallery cannot know what your brand vetoed.
@@ -30,8 +30,8 @@ Need one that is not here? Take it from [easing.dev](https://easing.dev/), do no
30
30
 
31
31
  The choice, in decision order: entering or exiting → `ease-out`; moving on screen → `ease-in-out`;
32
32
  hover or color → `ease`; constant motion (marquee, progress) → `linear`; unsure → `ease-out`.
33
- `../../animate/SKILL.md` owns the full build sequence and the per-element duration table, and
34
- `../../review-animations/scripts/verify.sh` flags an `ease-in` that slips through.
33
+ `../../motion-craft/SKILL.md` owns the duration ranges, the easing families and the never-animate list, and
34
+ its diagnosis table maps the complaint — sluggish, cheap, janky — back to the cause.
35
35
 
36
36
  - **Enter vs exit asymmetry:** entrances are slower and softer (`--dur-normal`, `--ease-out`); exits are quicker and quieter (~150ms) so dismissals feel responsive.
37
37
  - **Press feedback:** `scale(0.97)` on `:active` gives a tactile button without distracting movement.
@@ -30,7 +30,7 @@ should_not_trigger:
30
30
  route_to: "nextjs"
31
31
  why: "The build. The identity stays in the record; the framework work is the sibling's."
32
32
  - prompt: "Give me three different directions for this poster and I'll choose."
33
- route_to: "prototype"
33
+ route_to: "variant-explorer"
34
34
  why: "Exploration among variants. One skill per style: merging two candidate identities into one spec produces the average of two good designs, which is a bad design."
35
35
 
36
36
  capability:
@@ -40,7 +40,7 @@ and proposing from memory is how this promise used to resolve to the AI-template
40
40
 
41
41
  **Where to go looking.** For a whole page to beat, and for the flows and single patterns behind it:
42
42
  `../design/references/inspiration-sources.md`. For a component idea rather than a whole reference:
43
- `../pick-ui-library/references/component-galleries.md`. Browse for the technique, never to clone a
43
+ a gallery of real products. Browse for the technique, never to clone a
44
44
  competitor's exact grid — and come back with **a specific page you can open**, because a gallery
45
45
  is not a bar.
46
46
 
@@ -152,8 +152,8 @@ how the look drifts back to generic by the fifth piece.
152
152
  this loop invokes; it stays reachable directly when there is no bar to beat and no loop to run.
153
153
  - **Codifying the winner into a permanent identity** → `../design-dna/SKILL.md`.
154
154
  - **The words** → `../marketing/SKILL.md`. **The build** → `../nextjs/SKILL.md`.
155
- - **Motion craft, and routing among the frontend craft siblings** → `../design-eng/SKILL.md`.
156
- - **"Show me a few versions and I'll pick"** → `../prototype/SKILL.md`. That is selection among
155
+ - **Motion craft** → `../motion-craft/SKILL.md`. **How the interface should be built** → `../ui-engineering/SKILL.md`.
156
+ - **"Show me a few versions and I'll pick"** → `../variant-explorer/SKILL.md`. That is selection among
157
157
  variants; this is convergence against a bar.
158
158
  - **SDD gate.** This skill runs a method; it does not outrank the chain. If the request is a new
159
159
  feature or a non-trivial behaviour change in a real app, `../specify/SKILL.md` runs first, then
@@ -24,11 +24,11 @@ should_not_trigger:
24
24
  route_to: "design"
25
25
  why: "Visual system work with no reference to beat and nothing to judge. `design` stays directly reachable exactly for this."
26
26
  - prompt: "Show me three versions of this button and I'll pick the one I like."
27
- route_to: "prototype"
27
+ route_to: "variant-explorer"
28
28
  why: "Selection among variants by human preference, not convergence against a measured bar. The user is the judge and there is no reference."
29
29
  - prompt: "Review the motion in this diff and tell me what's off."
30
- route_to: "review-animations"
31
- why: "One diff, motion-specific, no reference and no build-then-judge rounds. Reviewing is not looping."
30
+ route_to: "motion-craft"
31
+ why: "One diff, motion-specific, no reference and no build-then-judge rounds. The motion craft skill owns the complaint-to-cause mapping; reviewing is not looping."
32
32
  - prompt: "Write the hero headline and value prop for our developer-tool site."
33
33
  route_to: "marketing"
34
34
  why: "The words, not the bar. `voice` constrains copy in this pipe; it does not write it."
@@ -11,7 +11,7 @@ origin: risco
11
11
  **Hand-off — the app vs its motion.** This skill owns the Expo platform: router, config plugins, EAS,
12
12
  native modules, store builds. **Animation and gestures** — Reanimated worklets, the UI runtime,
13
13
  sheets, screen transitions, press feedback, haptics, motion that stutters on a real device — are
14
- `../animate-expo/SKILL.md`'s.
14
+ `../motion-craft/SKILL.md`'s.
15
15
 
16
16
 
17
17
  ## What this skill owns
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: motion-craft
3
+ description: "Use when an interface moves and the movement has to feel right — durations and easing, what must never animate, fixing motion that reads sluggish, cheap or janky, and reduced-motion. Web and native. NOT the visual system (that is `design`), NOT the graded critique (that is `design-loop`)."
4
+ tags: [motion, animation, interface, craft, frontend]
5
+ recommends: [design]
6
+ profiles: [full]
7
+ origin: risco
8
+ ---
9
+
10
+ # Motion craft — movement that reads as intent
11
+
12
+ Motion is not decoration laid over an interface. It is the interface **explaining what just
13
+ happened**: what appeared, what it came from, what is still loading, what the user just did. When
14
+ motion feels wrong the complaint is almost never "the curve is off" — it is "this feels cheap", or
15
+ "slow", or "I don't know what happened". Those are the symptoms this skill exists to convert into
16
+ decisions.
17
+
18
+ ## The one rule
19
+
20
+ **Every animation answers a question the user is already asking.** If you cannot name the question,
21
+ delete the animation — it is costing time and attention and returning nothing.
22
+
23
+ | The user is asking | The motion that answers it |
24
+ | --- | --- |
25
+ | "Where did this come from?" | It enters from its origin, not from nowhere |
26
+ | "Is it working?" | Immediate feedback, under 100ms, even before the result |
27
+ | "What did I just do?" | The changed thing moves; nothing else does |
28
+ | "Where did it go?" | It exits toward where it can be found again |
29
+ | nothing | **no animation** |
30
+
31
+ That table is the whole method. Everything below is how to honour it without shipping something
32
+ that janks.
33
+
34
+ ## Duration: the only numbers worth memorising
35
+
36
+ Movement is read as *slow* long before it is read as *long*. The usable range is narrower than most
37
+ people expect, and the mistake is almost always **too slow**, not too fast.
38
+
39
+ | What moves | Range | Why |
40
+ | --- | --- | --- |
41
+ | State on an element already on screen (hover, toggle, press) | 100–150ms | It must feel like a response, not an event |
42
+ | Something entering or leaving in place (menu, tooltip, toast) | 150–250ms | Long enough to be seen, short enough not to be waited for |
43
+ | A region rearranging (list reflow, panel) | 250–350ms | Bigger travel needs more time to stay legible |
44
+ | A full view transition | 300–400ms | The ceiling. Past 400ms it is a wait, not a transition |
45
+
46
+ Two rules that come from those numbers, not from taste:
47
+
48
+ - **Exits are faster than entrances**, usually by about a third. The user has already decided; making
49
+ them watch the decision leave is the single most common way an interface feels sluggish.
50
+ - **Distance and duration scale together, sub-linearly.** Doubling the travel does not double the
51
+ time. If a value grows past 400ms because the element crosses the screen, the layout is the
52
+ problem, not the timing.
53
+
54
+ ## Easing: three curves cover almost everything
55
+
56
+ - **Entering, or responding to input** — start fast, settle slow (`ease-out` family). The user's
57
+ attention is already there; arriving quickly and settling reads as responsive.
58
+ - **Leaving** — start slow, accelerate away (`ease-in` family). It matches how the eye lets go.
59
+ - **Moving between two on-screen positions** — accelerate and decelerate (`ease-in-out` family).
60
+
61
+ **Linear is for one thing only: continuous, non-positional change** — a progress bar filling, a
62
+ spinner rotating, a colour crossfade. Linear on anything that moves in space reads as mechanical,
63
+ because nothing physical starts at full speed.
64
+
65
+ Springs are worth reaching for when the motion should feel *physical* — dragging, dismissing,
66
+ anything the user's finger or pointer is still in contact with. They are the wrong tool for
67
+ appearing and disappearing, where a bounce reads as indecision.
68
+
69
+ ## What must never animate
70
+
71
+ This is where most jank comes from, and it is not a matter of degree.
72
+
73
+ - **Never animate layout.** Width, height, top, left, margin, padding — each frame forces the engine
74
+ to recompute geometry. Animate transform and opacity, which do not.
75
+ - **Never `transition: all`.** It is the shortest path to animating layout by accident. List the
76
+ properties.
77
+ - **Never animate what the user is reading.** Text that moves while being read is illegible.
78
+ - **Never block input on an animation.** The interface accepts the next action immediately, even
79
+ mid-motion. An animation that must finish before the user may act has become a modal dialog.
80
+ - **Never animate more than one thing per event.** If three things move when one changed, the user
81
+ cannot tell which one mattered.
82
+
83
+ ## Reduced motion is not an edge case
84
+
85
+ A real share of people have vestibular conditions for which large movement is nauseating, and they
86
+ have told their operating system so. Honouring it is not optional and it is not a fallback to
87
+ nothing:
88
+
89
+ - **Keep** the feedback: opacity changes, colour, instant state.
90
+ - **Remove** the travel: no sliding across the screen, no scaling, no parallax, no auto-playing loops.
91
+ - The interface must still say what happened — it just says it without moving.
92
+
93
+ An implementation with no reduced-motion branch is unfinished, the same way an interface with no
94
+ error state is unfinished.
95
+
96
+ ## Diagnosing motion that feels wrong
97
+
98
+ The complaint names the symptom; this maps it to a cause.
99
+
100
+ | Complaint | Usually | Fix |
101
+ | --- | --- | --- |
102
+ | "Sluggish" | Duration too long, or the exit matches the entrance | Cut to the range above; make exits faster |
103
+ | "Cheap" / "amateur" | Linear easing on positional movement, or bounce on entrances | Match curve to direction |
104
+ | "Janky" / "stutters" | Animating layout properties | Move to transform and opacity |
105
+ | "Distracting" | More than one thing moving per event | Animate only what changed |
106
+ | "I don't know what happened" | Motion with no origin or destination | Move it from where it came from |
107
+ | "Nice, but I'd turn it off" | No reduced-motion branch, or motion that repeats | Honour the setting; never loop what is not loading |
108
+
109
+ ## Anti-patterns → STOP
110
+
111
+ | If you're about to… | Reality / Fix |
112
+ | --- | --- |
113
+ | Add motion because the screen feels static | Static is not a defect. Motion with no question to answer is noise with a frame cost. |
114
+ | Copy a duration from a design you liked | You copied the number without its distance and its context. Take the range, verify against the feel. |
115
+ | Animate a list so every item cascades in | Stagger past ~4 items becomes a queue the user waits through. Cap it, or drop it. |
116
+ | Use a spring for a menu appearing | A bounce on an entrance reads as indecision. Springs are for what the pointer is still touching. |
117
+ | Ship motion without checking reduced-motion | It is not an edge case. Unchecked means unfinished. |
118
+ | Reach here for the visual system | Type, colour, spacing and composition are `../design/SKILL.md`. This skill only moves what that one placed. |
119
+
120
+ ## See Also
121
+
122
+ - `../design/SKILL.md` — the visual system this motion serves.
123
+ - `../design-loop/SKILL.md` — the graded critique that judges a finished screen, motion included.
124
+ - `../design-dna/SKILL.md` — turning a look, motion rules included, into a reusable identity.
125
+ - `../accessibility/SKILL.md` — the wider ground reduced-motion sits in.
@@ -0,0 +1,38 @@
1
+ skill: motion-craft
2
+
3
+ should_trigger:
4
+ - prompt: "The menu animation feels sluggish, can you make it feel snappier?"
5
+ why: "The canonical case: a complaint about feel, which this skill converts into a duration and an easing decision."
6
+ - prompt: "What duration and easing should this dropdown use?"
7
+ why: "Directly the numbers this skill owns, for an element entering in place."
8
+ - prompt: "My list animation stutters when it reflows."
9
+ why: "Jank is the symptom of animating layout properties, which is the one hard rule here."
10
+ - prompt: "Añade una animación al abrir el panel, pero que no quede cutre."
11
+ why: "Same ask in Spanish; the trigger is the intent, not the language."
12
+ - prompt: "Does this respect prefers-reduced-motion? I don't think we handled it."
13
+ why: "Reduced-motion is treated here as part of finishing the work, not as an edge case."
14
+ - prompt: "Everything on the page moves at once when I click one button."
15
+ why: "More than one thing moving per event is a named diagnosis in this skill."
16
+
17
+ should_not_trigger:
18
+ - prompt: "Design a landing page for my analytics SaaS — hero, pricing, the works."
19
+ route_to: "design"
20
+ why: "Type, colour, spacing and composition are the visual system, which this skill only moves once it exists."
21
+ - prompt: "Review this checkout screen and tell me how good it actually is, with a grade."
22
+ route_to: "design-loop"
23
+ why: "Graded critique of a finished screen is the loop's job; this skill builds motion, it does not judge screens."
24
+ - prompt: "This design came out great — turn it into a style I can reuse everywhere."
25
+ route_to: "design-dna"
26
+ why: "Codifying a look into a permanent identity is capture, not motion craft."
27
+ - prompt: "Make the site keyboard navigable and fix the contrast failures."
28
+ route_to: "accessibility"
29
+ why: "Accessibility beyond reduced-motion is its own ground; only the motion half lives here."
30
+ - prompt: "Our React app re-renders too much and the profiler shows wasted renders."
31
+ route_to: "react"
32
+ why: "A rendering performance problem is framework work, even when the symptom looks like stutter."
33
+
34
+ capability:
35
+ - prompt: "This modal slides in over 600ms with a linear curve and it feels cheap. Fix it and say why."
36
+ why: "Exercises the two most common defects at once — too slow, and linear on positional movement — and demands the reasoning, not just new numbers."
37
+ - prompt: "We animate width and height on the sidebar and it janks on mobile. What do we do instead?"
38
+ why: "The layout-vs-compositor rule is the load-bearing piece of technical content here."
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # verify.sh — the never-animate list of `motion-craft`, made mechanical.
4
+ #
5
+ # The SKILL.md declares rules as binding ("never animate layout", "never
6
+ # `transition: all`"). A rule declared binding without a mechanism that checks it
7
+ # is a decorative gate: it buys the feeling of safety without the safety. So each
8
+ # hard rule in the skill body has a row here, and each row has a fixture in
9
+ # tests/motion-craft-rules.test.js that watches it FAIL and then PASS.
10
+ #
11
+ # Static and network-free. Everything WARNS by default; --strict makes findings
12
+ # fatal, which is what CI should use.
13
+ set -u
14
+ TARGET="${1:-.}"
15
+ STRICT=0
16
+ [ "${2:-}" = "--strict" ] && STRICT=1
17
+ [ "${1:-}" = "--strict" ] && { STRICT=1; TARGET="."; }
18
+
19
+ warn_count=0
20
+ warn() { printf ' ⚠ %s\n' "$1"; warn_count=$((warn_count + 1)); }
21
+ ok() { printf ' ✓ %s\n' "$1"; }
22
+
23
+ # id <TAB> grep -E pattern <TAB> message
24
+ # One row per hard rule in SKILL.md §"What must never animate". Adding a rule to
25
+ # the prose without adding it here is what the test suite refuses to allow.
26
+ #
27
+ # Fields are TAB-separated on purpose: the patterns are extended regexes and are
28
+ # full of `|`, so a pipe delimiter silently truncated every pattern at its first
29
+ # alternation and only the one rule without alternation ever fired.
30
+ rules() {
31
+ printf '%b\n' \
32
+ "transition-all\ttransition:[[:space:]]*all\t\`transition: all\` animates layout by accident — list the exact properties" \
33
+ "layout-anim\ttransition:[^;]*(width|height|top:|left:|right:|bottom:|margin|padding)\tanimating layout properties forces reflow every frame — use transform and opacity" \
34
+ "linear-move\t(transition|animation):[^;]*(transform|translate|scale)[^;]*linear\tlinear easing on positional movement reads as mechanical — nothing physical starts at full speed" \
35
+ "long-duration\t(transition|animation):[^;]*[^0-9]([5-9][0-9][0-9]|[0-9][0-9][0-9][0-9])ms\tover 400ms stops being a transition and becomes a wait"
36
+ }
37
+
38
+ files=$(find "$TARGET" \( -name node_modules -o -name .git -o -name dist -o -name build -o -name .next \) -prune -o \
39
+ \( -name '*.css' -o -name '*.scss' -o -name '*.tsx' -o -name '*.jsx' -o -name '*.ts' -o -name '*.js' -o -name '*.vue' -o -name '*.svelte' \) -print 2>/dev/null)
40
+
41
+ echo "motion-craft — never-animate checks"
42
+ if [ -z "$files" ]; then
43
+ echo " (no style or component files found under $TARGET)"
44
+ exit 0
45
+ fi
46
+
47
+ while IFS=$'\t' read -r id pattern message; do
48
+ [ -z "$id" ] && continue
49
+ hits=$(printf '%s\n' "$files" | xargs grep -lEi -- "$pattern" 2>/dev/null | head -5)
50
+ if [ -n "$hits" ]; then
51
+ warn "$id: $message"
52
+ printf ' %s\n' $hits
53
+ else
54
+ ok "$id"
55
+ fi
56
+ done <<< "$(rules)"
57
+
58
+ # reduced-motion is a finishing condition, not an edge case: if anything animates
59
+ # at all, the project must say what it does for people who asked for less motion.
60
+ if printf '%s\n' "$files" | xargs grep -lEi -- '(transition|animation|@keyframes)' >/dev/null 2>&1; then
61
+ if printf '%s\n' "$files" | xargs grep -lEi -- 'prefers-reduced-motion' >/dev/null 2>&1; then
62
+ ok "reduced-motion: honoured somewhere"
63
+ else
64
+ warn "reduced-motion: this project animates and never mentions prefers-reduced-motion — unchecked means unfinished"
65
+ fi
66
+ fi
67
+
68
+ echo
69
+ if [ "$warn_count" -gt 0 ]; then
70
+ echo "$warn_count finding(s)."
71
+ [ "$STRICT" -eq 1 ] && exit 1
72
+ else
73
+ echo "No findings."
74
+ fi
75
+ exit 0
@@ -10,8 +10,8 @@ origin: risco
10
10
 
11
11
  **Hand-off — picking dependencies.** Which library to reach for (UI primitives, command menus,
12
12
  toasts, OTP inputs, charts, virtualization, drag and drop, state, styling, theming) is
13
- `../pick-ui-library/SKILL.md`'s — reach for it before hand-rolling a component. Animating what you
14
- install is `../animate/SKILL.md`'s.
13
+ `../ui-engineering/SKILL.md`'s — build vs adopt is decided there, before hand-rolling a component. Animating what you
14
+ install is `../motion-craft/SKILL.md`'s.
15
15
 
16
16
 
17
17
  > Build or review a fast, typed, correctly-architected **client-side** React single-page app bundled by Vite. No server runtime, no RSC tree, no framework router. Server-rendered React (App Router, server actions, SSR/SSG) is not this skill — that is `../nextjs/SKILL.md`.
@@ -10,7 +10,7 @@ origin: risco
10
10
 
11
11
  **Hand-off — the app vs its motion.** Animation and gestures in a React Native app — Reanimated
12
12
  worklets on the UI runtime, gesture handoff, sheets, press feedback, haptics — are
13
- `../animate-expo/SKILL.md`'s, whose guidance applies to bare React Native as well as Expo.
13
+ `../motion-craft/SKILL.md`'s, whose guidance is platform-agnostic and applies to bare React Native as well as Expo.
14
14
 
15
15
 
16
16
  You write the code that runs *inside* the app: render, navigate, animate, list, persist, and author native modules. Mobile is split across two skills by **verb** — run the verb test before doing anything:
@@ -79,7 +79,31 @@ Not covered → one line, skill or agent. Rules: `skill-scout`.
79
79
 
80
80
  ---
81
81
 
82
- ## 3. First contact
82
+ ## 3. A harness that is broken fixes itself
83
+
84
+ You are injected into every session, so you are the only thing that can notice a broken
85
+ harness before its owner does — and its owner usually cannot, because the symptoms name
86
+ nothing they recognise. When any of these is true, act on it **once** in the session:
87
+
88
+ - the assistant sees no skills in a project that clearly has a harness;
89
+ - `.rsc.json` exists and what it declares is not what is installed;
90
+ - the same hook seems to run several times;
91
+ - the harness is wired for an assistant that is not the one running.
92
+
93
+ Run `npx @ericrisco/rsc doctor`, and say in one line what is wrong **as a symptom**, not as
94
+ a cause. Then:
95
+
96
+ - Anything that only puts the harness back to what was already declared — dangling links, a
97
+ repeated hook, a layout no version uses — say you are fixing it and run
98
+ `npx @ericrisco/rsc repair`. Restoring is not deciding, and a recoverable copy is kept.
99
+ - Anything that would change a decision — moving to another assistant, adopting a skill a
100
+ teammate added, disarming a gate — **ask first**. A `git pull` never rewrites someone's
101
+ machine.
102
+
103
+ Offer once. A "no" holds for the session; a new session may look again, because the problem
104
+ has not gone away. Never mention it when the harness is healthy.
105
+
106
+ ## 4. First contact
83
107
 
84
108
  Before handling the first request of a session, check the workspace:
85
109
 
@@ -12,8 +12,8 @@ origin: risco
12
12
  **Hand-off — the language vs the platform.** This skill owns the **platform**: SwiftUI/UIKit views,
13
13
  app lifecycle, Xcode, iOS APIs, App Store shipping. Language-level Swift — Swift 6 data-race safety
14
14
  and approachable concurrency, actors and task groups, `some` vs `any`, ARC and performance, Swift
15
- Testing, macros — is `../write-swift/SKILL.md`'s. A concurrency error, a hang, a data race or a
16
- retain cycle goes there, not here. Interface feel, springs and materials: `../apple-design/SKILL.md`.
15
+ Testing, macros — is this skill's own ground. A concurrency error, a hang, a data race or a
16
+ retain cycle belongs here. Interface feel, springs and materials: `../motion-craft/SKILL.md`.
17
17
 
18
18
 
19
19
  Native Apple-platform apps: the code is `.swift`, the UI is SwiftUI, and the compiler enforces actor isolation. If the answer is a `@MainActor`, `@Observable`, `async let`, `#expect`, `NavigationStack`, or `@Model`, you are in the right place.
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: ui-engineering
3
+ description: "Use when the question is how interface should be BUILT — component boundaries, state that belongs in the URL vs the component, loading/empty/error states, forms, optimistic updates, build vs adopt. NOT the visual system (`design`), NOT framework mechanics (`react`/`nextjs`)."
4
+ tags: [interface, frontend, architecture, components, state, craft]
5
+ recommends: [design, motion-craft]
6
+ profiles: [core, full]
7
+ origin: risco
8
+ ---
9
+
10
+ # UI engineering — the decisions between the design and the framework
11
+
12
+ There is a layer that neither the design skill nor the framework skill owns. The design says what
13
+ the screen should be. The framework says how its API works. **In between sits every decision that
14
+ makes an interface either durable or a maintenance tax** — and it is where most interface work
15
+ actually goes wrong.
16
+
17
+ Nothing here is framework-specific. These decisions are the same in React, Vue, Svelte, SwiftUI and
18
+ plain HTML; only the syntax moves.
19
+
20
+ ## The one rule
21
+
22
+ **State goes to the outermost place that still needs it, and no further.** Almost every interface
23
+ tangle is that rule broken in one of two directions: state hoarded in a component that others need,
24
+ or state hoisted into a global store that only one component ever reads.
25
+
26
+ | Where the state lives | When that is right |
27
+ | --- | --- |
28
+ | **The URL** | Anything a user would bookmark, share, or expect the back button to restore: filters, tabs, pagination, the open item |
29
+ | **The server** | Anything that outlives the session or belongs to more than one user |
30
+ | **A shared parent** | Two siblings genuinely need it, and it dies with the screen |
31
+ | **The component** | Nobody else reads it: hover, focus, an open menu, an in-flight draft |
32
+
33
+ The most common and most expensive mistake is the first row: state that belongs in the URL kept in a
34
+ component. It looks fine until someone reloads, shares a link, or presses back — and then the
35
+ interface silently loses what the user did.
36
+
37
+ ## Every state, not just the happy one
38
+
39
+ An interface is not finished when it renders data. It is finished when it renders **all five**:
40
+
41
+ | State | The question it answers | The usual failure |
42
+ | --- | --- | --- |
43
+ | **Loading** | "Is it working?" | A spinner where the shape is already known — use the shape |
44
+ | **Empty** | "Is it broken, or is there nothing?" | Blank. It must say which, and what to do next |
45
+ | **Error** | "What now?" | A message with no action. Every denial carries its own way out |
46
+ | **Partial** | "Some of it failed" | Ignored entirely; the screen shows a half-truth |
47
+ | **Success** | "It worked" | Silence, so the user does it twice |
48
+
49
+ The empty state is the one that gets skipped, and it is the first state a new user ever sees.
50
+
51
+ ## Component boundaries
52
+
53
+ A component earns its existence by **hiding a decision**, not by being reused. Splitting for reuse
54
+ alone produces the worst interfaces: a wrapper around a wrapper, each passing props through, none
55
+ owning anything.
56
+
57
+ Draw the boundary where:
58
+
59
+ - it owns a piece of state nothing outside needs;
60
+ - it can be described in one sentence without "and";
61
+ - swapping its internals changes nothing outside it.
62
+
63
+ Do **not** draw a boundary because a file is long. Long is not a defect; a file that does two
64
+ unrelated things is, and it is usually short.
65
+
66
+ ## Forms
67
+
68
+ Forms are where interfaces most often insult people, and the rules are unglamorous:
69
+
70
+ - **Validate on blur, not on keystroke.** Telling someone their email is invalid while they are on
71
+ the third character is scolding them for not having finished.
72
+ - **Never clear what they typed.** Not on error, not on refresh, not on navigation. Their input is
73
+ theirs.
74
+ - **Disable the submit only while submitting**, never as a way of enforcing validity — a disabled
75
+ button with no reason is a dead end.
76
+ - **The error goes next to the field**, and says what to do, not what is wrong.
77
+
78
+ ## Optimistic updates
79
+
80
+ Show the result before the server confirms **only** when you can honestly undo it. That means: the
81
+ operation is idempotent, the failure is rare, and reverting it is visible and comprehensible.
82
+
83
+ Optimism on a destructive or an expensive action is a lie you will have to retract in front of the
84
+ user. Deleting, paying, sending — those wait.
85
+
86
+ ## Build or adopt
87
+
88
+ Adopting a component costs bundle size, an API you did not design, and an upgrade you will owe
89
+ later. Building costs the accessibility work you will get wrong.
90
+
91
+ The split is not about difficulty, it is about **who is the authority on the behaviour**:
92
+
93
+ - **Adopt** for behaviour with a specification you would otherwise be reimplementing badly: focus
94
+ trapping, combobox semantics, date handling, virtualisation.
95
+ - **Build** for anything that is your product's own logic wearing a UI, or where the library gives
96
+ you 60% and you fight the remaining 40% forever.
97
+
98
+ ## Anti-patterns → STOP
99
+
100
+ | If you're about to… | Reality / Fix |
101
+ | --- | --- |
102
+ | Put a filter or tab selection in component state | It belongs in the URL. Reload, share and back are part of the interface. |
103
+ | Add a global store because passing props feels tedious | Tedious is not broken. Global state that one component reads is a leak with ceremony. |
104
+ | Ship a screen with only the loaded state | Loading, empty, error and partial are not extras. The empty one is what a new user sees first. |
105
+ | Split a component because the file got long | Length is not the defect. Two unrelated responsibilities are, and they are usually short. |
106
+ | Show a delete as done before the server confirms | Optimism on a destructive action is a lie you will retract in public. |
107
+ | Reach here for how the framework's hooks work | That is `../react/SKILL.md` or `../nextjs/SKILL.md`. This skill decides what to build; those decide how to express it. |
108
+
109
+ ## See Also
110
+
111
+ - `../design/SKILL.md` — the visual system these decisions serve.
112
+ - `../motion-craft/SKILL.md` — how the pieces here move once they exist.
113
+ - `../design-loop/SKILL.md` — the graded critique that judges the result.
114
+ - `../accessibility/SKILL.md` — the ground under focus, semantics and forms.
@@ -0,0 +1,38 @@
1
+ skill: ui-engineering
2
+
3
+ should_trigger:
4
+ - prompt: "Where should the filter state live — component, context or the URL?"
5
+ why: "The canonical case: the one rule this skill exists to apply."
6
+ - prompt: "How should I split these components? The file is getting big."
7
+ why: "Component boundaries, and the file-length premise is one this skill corrects."
8
+ - prompt: "We only render the loaded state. What else does this screen need?"
9
+ why: "The five states are the checklist this skill owns."
10
+ - prompt: "¿Valido el formulario mientras escribe o al salir del campo?"
11
+ why: "Form behaviour, in Spanish; the trigger is the decision, not the language."
12
+ - prompt: "Should I use a component library for the date picker or build it?"
13
+ why: "Build vs adopt, decided by who owns the behaviour's specification."
14
+ - prompt: "Can we show the delete as done immediately and roll back if it fails?"
15
+ why: "Optimistic updates, and this is the case where the answer is no."
16
+
17
+ should_not_trigger:
18
+ - prompt: "Design the pricing page — layout, type scale, the colour system."
19
+ route_to: "design"
20
+ why: "The visual system is decided before this layer has anything to build."
21
+ - prompt: "My useEffect runs twice in development and I don't understand why."
22
+ route_to: "react"
23
+ why: "Framework mechanics. This skill decides what to build, not how the framework behaves."
24
+ - prompt: "The dropdown animation feels cheap, what duration should it use?"
25
+ route_to: "motion-craft"
26
+ why: "Once the component exists, how it moves is motion craft."
27
+ - prompt: "Grade this dashboard and tell me if it is actually good."
28
+ route_to: "design-loop"
29
+ why: "Judging a finished screen is the critique loop, not the build decisions."
30
+ - prompt: "Set up the Next.js App Router with server actions for this project."
31
+ route_to: "nextjs"
32
+ why: "Stack setup is the stack skill's; this one is framework-agnostic by construction."
33
+
34
+ capability:
35
+ - prompt: "Our search page keeps its query and filters in component state. Users complain the back button loses everything. What is wrong and what is the fix?"
36
+ why: "The single most expensive instance of the one rule, stated as a real symptom rather than as theory."
37
+ - prompt: "This list screen shows a spinner, then rows. What states are we missing and why does each matter?"
38
+ why: "Forces the five-state checklist, including the empty state that a new user sees first."
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: variant-explorer
3
+ description: "Use when someone wants OPTIONS rather than an answer — 'show me a few versions', picking between approaches by seeing them. Genuinely different candidates, cheap to throw away, trade-off named. NOT the critique that judges one (`design-loop`), NOT the spec (`specify`)."
4
+ tags: [prototyping, divergence, options, exploration, decision]
5
+ recommends: [design-loop]
6
+ profiles: [full]
7
+ origin: risco
8
+ ---
9
+
10
+ # Variant explorer — several real options, cheap enough to discard
11
+
12
+ "Show me a few versions and I'll pick" is a different request from "build this". It asks for
13
+ **divergence**: several candidates that are actually different, each cheap enough that throwing it
14
+ away costs nothing, with the difference between them stated out loud so a human can decide.
15
+
16
+ Done badly it produces the thing that wastes the most time in creative work: three variations of one
17
+ idea, dressed up as a choice.
18
+
19
+ ## The one rule
20
+
21
+ **Variants must differ in a decision, not in a detail.** If two candidates could both be reached by
22
+ editing the other, there is one candidate.
23
+
24
+ ```text
25
+ Not variants: the same layout in blue, in green, in dark
26
+ Variants: a single scrolling page · a three-step wizard · a table with inline editing
27
+ ```
28
+
29
+ The test before showing anything: **name the decision each variant is taking.** If you cannot name
30
+ it, or two share it, that set is not ready.
31
+
32
+ ## How many, and how finished
33
+
34
+ - **Three.** Two reads as a false dichotomy; five past the point where a person can hold them side by
35
+ side. Three forces the middle option to justify itself.
36
+ - **Finished enough to judge the decision, and no more.** If the decision is about structure, the
37
+ copy can be placeholder. If it is about tone, the layout can be crude. Polish that is not being
38
+ judged is polish you are asking someone to ignore.
39
+ - **Comparable.** Same content, same constraints, same fidelity across all three. A variant that is
40
+ prettier wins for reasons nobody chose.
41
+
42
+ ## Making them genuinely different
43
+
44
+ The failure mode is anchoring: the first idea contaminates the rest. Force divergence by generating
45
+ each from a different starting constraint.
46
+
47
+ | Angle | Ask |
48
+ | --- | --- |
49
+ | **Cheapest** | What if this had to ship this week? |
50
+ | **Boldest** | What if we ignored what similar products do? |
51
+ | **Fewest steps** | What if the user only did one thing? |
52
+ | **Familiar** | What if it worked exactly like the thing they already know? |
53
+ | **Inverted** | What if the primary and secondary swapped? |
54
+
55
+ Pick three angles that are actually in tension for this problem. Do not run all five — the point is
56
+ tension, not coverage.
57
+
58
+ ## Presenting them so a decision is possible
59
+
60
+ Each candidate carries three things, and no more:
61
+
62
+ 1. **What decision it takes** — one sentence, in the user's terms.
63
+ 2. **What it costs** — the honest downside, not a hedge. A variant with no downside is one you have
64
+ not thought about.
65
+ 3. **What it would take to be wrong** — the fact that would kill it.
66
+
67
+ Then: **a recommendation.** Presenting three options and standing back is not neutrality, it is
68
+ handing the work back. Say which one you would pick and why, and make it easy to overrule.
69
+
70
+ ## When this is the wrong skill
71
+
72
+ - The answer is already known and only needs building → build it. Manufacturing options to look
73
+ thorough wastes everyone's time.
74
+ - The decision is reversible and cheap → just pick one and ship. Exploration is for decisions that
75
+ are expensive to unwind.
76
+ - What is needed is a **judgement on one thing** → that is critique, not divergence.
77
+
78
+ ## Anti-patterns → STOP
79
+
80
+ | If you're about to… | Reality / Fix |
81
+ | --- | --- |
82
+ | Show the same idea in three colours | Those are not variants. Name the decision each takes; if you cannot, you have one. |
83
+ | Polish one candidate more than the others | It will win on polish, not on the decision. Same fidelity, always. |
84
+ | Present five or six to be thorough | Past three, they stop being compared and start being skimmed. |
85
+ | Give options with no recommendation | Standing back is not neutrality; it hands the work back to the person who asked. |
86
+ | Explore a decision that is cheap to reverse | Just ship one. Divergence is for what is expensive to unwind. |
87
+ | Keep iterating on the winner here | Once one is chosen, refining it is `../design-loop/SKILL.md` or the relevant build skill. |
88
+
89
+ ## See Also
90
+
91
+ - `../design-loop/SKILL.md` — the graded critique that takes over once a direction is chosen.
92
+ - `../specify/SKILL.md` — when what is missing is an agreed WHAT/WHY, not options.
93
+ - `../design/SKILL.md` — building the chosen direction properly.