@remix-run/cli 0.1.0 → 0.3.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 (110) hide show
  1. package/README.md +0 -3
  2. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  3. package/dist/lib/bootstrap-project.js +32 -9
  4. package/dist/lib/cli.d.ts +29 -0
  5. package/dist/lib/cli.d.ts.map +1 -1
  6. package/dist/lib/cli.js +33 -14
  7. package/dist/lib/commands/completion.d.ts.map +1 -1
  8. package/dist/lib/commands/completion.js +5 -1
  9. package/dist/lib/commands/doctor.js +18 -18
  10. package/dist/lib/commands/help.d.ts.map +1 -1
  11. package/dist/lib/commands/help.js +9 -33
  12. package/dist/lib/commands/routes.js +3 -3
  13. package/dist/lib/commands/test.d.ts +1 -1
  14. package/dist/lib/commands/test.d.ts.map +1 -1
  15. package/dist/lib/commands/test.js +8 -4
  16. package/dist/lib/completion.d.ts.map +1 -1
  17. package/dist/lib/completion.js +4 -106
  18. package/dist/lib/controller-files.d.ts +0 -1
  19. package/dist/lib/controller-files.d.ts.map +1 -1
  20. package/dist/lib/controller-files.js +3 -5
  21. package/dist/lib/controller-ownership.d.ts +9 -9
  22. package/dist/lib/controller-ownership.d.ts.map +1 -1
  23. package/dist/lib/controller-ownership.js +56 -91
  24. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  25. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  26. package/dist/lib/doctor/controller-findings.js +15 -87
  27. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  28. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  29. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  30. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  31. package/dist/lib/doctor/controller-placeholders.js +18 -149
  32. package/dist/lib/doctor/controllers.js +1 -1
  33. package/dist/lib/doctor/project.js +60 -52
  34. package/dist/lib/doctor/types.d.ts +2 -2
  35. package/dist/lib/doctor/types.d.ts.map +1 -1
  36. package/dist/lib/errors.d.ts +0 -6
  37. package/dist/lib/errors.d.ts.map +1 -1
  38. package/dist/lib/errors.js +0 -11
  39. package/dist/lib/load-route-map-worker.js +17 -9
  40. package/dist/lib/route-map.d.ts +1 -1
  41. package/dist/lib/route-map.d.ts.map +1 -1
  42. package/dist/lib/route-map.js +29 -17
  43. package/package.json +4 -5
  44. package/src/lib/bootstrap-project.ts +39 -13
  45. package/src/lib/cli.ts +46 -15
  46. package/src/lib/commands/completion.ts +6 -1
  47. package/src/lib/commands/doctor.ts +18 -21
  48. package/src/lib/commands/help.ts +9 -43
  49. package/src/lib/commands/routes.ts +3 -3
  50. package/src/lib/commands/test.ts +10 -4
  51. package/src/lib/completion.ts +4 -151
  52. package/src/lib/controller-files.ts +4 -8
  53. package/src/lib/controller-ownership.ts +78 -141
  54. package/src/lib/doctor/controller-findings.ts +20 -97
  55. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  56. package/src/lib/doctor/controller-placeholders.ts +17 -189
  57. package/src/lib/doctor/controllers.ts +1 -1
  58. package/src/lib/doctor/project.ts +60 -52
  59. package/src/lib/doctor/types.ts +1 -5
  60. package/src/lib/errors.ts +0 -12
  61. package/src/lib/load-route-map-worker.ts +19 -10
  62. package/src/lib/route-map.ts +61 -16
  63. package/template/.agents/skills/remix/SKILL.md +588 -0
  64. package/template/.agents/skills/remix/references/animate-elements.md +195 -0
  65. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
  66. package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
  67. package/template/.agents/skills/remix/references/component-model.md +282 -0
  68. package/template/.agents/skills/remix/references/create-mixins.md +158 -0
  69. package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
  70. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  71. package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
  72. package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  73. package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
  74. package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
  75. package/{bootstrap → template}/AGENTS.md +13 -8
  76. package/template/README.md +29 -0
  77. package/template/app/actions/controller.tsx +18 -0
  78. package/template/app/assets/entry.ts +8 -0
  79. package/template/app/assets/prompt-button.tsx +163 -0
  80. package/template/app/assets.ts +20 -0
  81. package/template/app/middleware/render.tsx +43 -0
  82. package/template/app/router.ts +20 -0
  83. package/template/app/routes.ts +6 -0
  84. package/template/app/ui/document.tsx +33 -0
  85. package/template/app/ui/scaffold-home-page.tsx +527 -0
  86. package/template/gitignore +4 -0
  87. package/{bootstrap → template}/package.json +5 -6
  88. package/template/public/favicon.svg +11 -0
  89. package/{bootstrap → template}/server.ts +4 -5
  90. package/{bootstrap → template}/tsconfig.json +3 -3
  91. package/bootstrap/README.md +0 -27
  92. package/bootstrap/app/controllers/auth.tsx +0 -21
  93. package/bootstrap/app/controllers/home.tsx +0 -26
  94. package/bootstrap/app/router.ts +0 -10
  95. package/bootstrap/app/routes.ts +0 -6
  96. package/bootstrap/app/ui/document.tsx +0 -21
  97. package/bootstrap/app/ui/layout.tsx +0 -22
  98. package/bootstrap/app/utils/render.tsx +0 -7
  99. package/dist/lib/commands/skills.d.ts +0 -6
  100. package/dist/lib/commands/skills.d.ts.map +0 -1
  101. package/dist/lib/commands/skills.js +0 -222
  102. package/dist/lib/skills-cache.d.ts +0 -19
  103. package/dist/lib/skills-cache.d.ts.map +0 -1
  104. package/dist/lib/skills-cache.js +0 -89
  105. package/dist/lib/skills.d.ts +0 -30
  106. package/dist/lib/skills.d.ts.map +0 -1
  107. package/dist/lib/skills.js +0 -441
  108. package/src/lib/commands/skills.ts +0 -306
  109. package/src/lib/skills-cache.ts +0 -140
  110. package/src/lib/skills.ts +0 -706
@@ -0,0 +1,233 @@
1
+ # Middleware and Server Setup
2
+
3
+ ## What This Covers
4
+
5
+ How to compose the request lifecycle and bridge the router to a runtime. Read this when the task
6
+ involves:
7
+
8
+ - Choosing or ordering built-in middleware in the root stack
9
+ - Writing custom middleware that sets typed context values
10
+ - Adding fast-exit handling (static files, CORS preflights) versus request-enriching layers
11
+ (sessions, auth, data loading)
12
+ - Choosing when to keep the generated Node server versus switching server adapters
13
+
14
+ For data and persistence specifics, see `data-and-validation.md`. For session and auth specifics,
15
+ see `auth-and-sessions.md`.
16
+
17
+ ## Middleware Stack
18
+
19
+ Middleware runs in order for every request. Place fast-exit middleware (static files) early and
20
+ request-enriching middleware (session, auth) later.
21
+
22
+ Recommended ordering:
23
+
24
+ ```typescript
25
+ import { createRouter } from 'remix/router'
26
+ import { compression } from 'remix/middleware/compression'
27
+ import { formData } from 'remix/middleware/form-data'
28
+ import { logger } from 'remix/middleware/logger'
29
+ import { methodOverride } from 'remix/middleware/method-override'
30
+ import { session } from 'remix/middleware/session'
31
+ import { staticFiles } from 'remix/middleware/static'
32
+ import { asyncContext } from 'remix/middleware/async-context'
33
+
34
+ let middleware = []
35
+
36
+ if (process.env.NODE_ENV === 'development') {
37
+ middleware.push(logger())
38
+ }
39
+
40
+ middleware.push(compression())
41
+ middleware.push(staticFiles('./public'))
42
+ middleware.push(formData())
43
+ middleware.push(methodOverride())
44
+ middleware.push(session(cookie, storage))
45
+ middleware.push(asyncContext())
46
+ middleware.push(loadDatabase())
47
+ middleware.push(loadAuth())
48
+
49
+ let router = createRouter({ middleware })
50
+ ```
51
+
52
+ ### Built-in middleware catalog
53
+
54
+ | Middleware | Import | Use when | Notes |
55
+ | -------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
56
+ | `staticFiles(dir, opts?)` | `remix/middleware/static` | Serve files from `public/` or another directory exactly as they exist on disk | Fast exit; usually near the top |
57
+ | `compression()` | `remix/middleware/compression` | Compress text-like responses | Usually global |
58
+ | `logger()` | `remix/middleware/logger` | Log requests and responses | Often development-only; `colors` can force color output on/off |
59
+ | `cors(opts?)` | `remix/middleware/cors` | Endpoints must serve cross-origin browsers or preflight `OPTIONS` requests | Usually early so preflights can short-circuit |
60
+ | `cop(opts?)` | `remix/middleware/cop` | Reject unsafe cross-origin browser requests without synchronizer tokens | Put before session or CSRF when used |
61
+ | `formData(opts?)` | `remix/middleware/form-data` | Parse `FormData` bodies, especially forms and uploads | Needed for `_csrf` form field extraction |
62
+ | `methodOverride()` | `remix/middleware/method-override` | HTML forms need `PUT`, `PATCH`, or `DELETE` semantics | Run after form parsing |
63
+ | `session(cookie, storage)` | `remix/middleware/session` | Cookie-backed sessions | Must run before session-backed auth or CSRF |
64
+ | `csrf(opts?)` | `remix/middleware/csrf` | Session-backed form workflows need synchronizer-token CSRF protection | Requires `session()` before it |
65
+ | `asyncContext()` | `remix/middleware/async-context` | Helpers outside handlers need request context via `getContext()` | Add before helpers rely on it |
66
+ | `auth({ schemes })` | `remix/middleware/auth` | Resolve auth state into `context.get(Auth)` | Run after `session()` for session-backed auth |
67
+ | `requireAuth()` | `remix/middleware/auth` | A controller or action must reject anonymous access | Usually controller-level or action-level, not global |
68
+
69
+ ### Static files vs browser modules
70
+
71
+ - Use `staticFiles()` for files that should be served directly from disk, such as images, fonts,
72
+ or already-built assets in `public/`
73
+ - Use `remix/assets` when browser modules should be compiled and served from source files with
74
+ import rewriting, preloads, or fingerprinted URLs
75
+
76
+ ### Ordering notes
77
+
78
+ - Put fast exits early: `staticFiles()`, `cors()` preflight handling, and `cop()` when used
79
+ - Parse request bodies before middleware that depends on them, such as `methodOverride()` and form
80
+ field token extraction in `csrf()`
81
+ - Run `session()` before `csrf()` and before session-backed `auth()`
82
+ - Add `asyncContext()` before helpers or shared code call `getContext()`
83
+ - Keep route protection like `requireAuth()` at controller or action scope unless the entire app is
84
+ private
85
+
86
+ ### Common stacks
87
+
88
+ - **Session-backed HTML app** -> `compression()`, `staticFiles()`, optional `cop()`, `formData()`,
89
+ `methodOverride()`, `session()`, optional `csrf()`, `asyncContext()`, `auth({ schemes })`
90
+ - **Cross-origin API** -> `compression()`, `cors()`, optional `asyncContext()`, optional
91
+ `auth({ schemes })`
92
+ - **Upload flow** -> `compression()`, `staticFiles()`, `formData({ uploadHandler })`, then
93
+ sessions, auth, and data-loading middleware as needed
94
+
95
+ ### Middleware with options
96
+
97
+ ```typescript
98
+ // Static files with cache headers
99
+ staticFiles('./public', {
100
+ cacheControl: 'no-store, must-revalidate',
101
+ etag: false,
102
+ lastModified: false,
103
+ })
104
+
105
+ // Form data with upload handler
106
+ import type { FileUpload } from 'remix/form-data-parser'
107
+ import { createFsFileStorage } from 'remix/file-storage/fs'
108
+
109
+ let fileStorage = createFsFileStorage('./tmp/uploads')
110
+
111
+ formData({
112
+ uploadHandler(fileUpload: FileUpload) {
113
+ return fileStorage.set(fileUpload.name, fileUpload)
114
+ },
115
+ })
116
+ ```
117
+
118
+ Errors thrown or rejected by `uploadHandler` propagate directly. Catch domain-specific upload
119
+ errors at the route boundary when they should become user-facing `Response` objects.
120
+
121
+ ## Writing Custom Middleware
122
+
123
+ Middleware is a function that receives `(context, next)`. Return a `Response` to short-circuit, call
124
+ and return `next()` when you need the downstream response, or return nothing when you only set
125
+ context and want the router to continue automatically.
126
+
127
+ ### Setting context values
128
+
129
+ Use `context.set(key, value)` to add typed values accessible downstream via `context.get(key)`.
130
+
131
+ ```typescript
132
+ import type { Middleware } from 'remix/router'
133
+ import { Database } from 'remix/data-table'
134
+
135
+ export function loadDatabase(): Middleware {
136
+ return async (context, next) => {
137
+ context.set(Database, db)
138
+ return next()
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Guarding routes
144
+
145
+ ```typescript
146
+ import { Auth } from 'remix/middleware/auth'
147
+
148
+ export function requireAdmin(): Middleware {
149
+ return (context, next) => {
150
+ let auth = context.get(Auth)
151
+ if (auth.identity?.role !== 'admin') {
152
+ return new Response('Forbidden', { status: 403 })
153
+ }
154
+ return next()
155
+ }
156
+ }
157
+ ```
158
+
159
+ ### Async context for helpers
160
+
161
+ `asyncContext()` stores the request context in `AsyncLocalStorage` so helpers can reach it
162
+ without the context being threaded through every call. Wrap `getContext()` in app-specific
163
+ helpers:
164
+
165
+ ```typescript
166
+ // app/utils/context.ts
167
+ import { getContext } from 'remix/middleware/async-context'
168
+ import { Auth } from 'remix/middleware/auth'
169
+ import { Database } from 'remix/data-table'
170
+ import { Session } from 'remix/session'
171
+
172
+ export function getCurrentDb() {
173
+ return getContext().get(Database)
174
+ }
175
+
176
+ export function getCurrentSession() {
177
+ return getContext().get(Session)
178
+ }
179
+
180
+ export function getCurrentUser() {
181
+ let auth = getContext().get(Auth)
182
+ if (!auth.ok) {
183
+ throw new Error('Expected an authenticated user. Run requireAuth() before this code.')
184
+ }
185
+ return auth.identity
186
+ }
187
+
188
+ export function getCurrentUserSafely() {
189
+ let auth = getContext().get(Auth)
190
+ return auth.ok ? auth.identity : null
191
+ }
192
+ ```
193
+
194
+ ## Middleware Layers
195
+
196
+ Middleware can be applied at three levels:
197
+
198
+ 1. **Router-level** — runs for every request:
199
+
200
+ ```typescript
201
+ let router = createRouter({ middleware: [...] })
202
+ ```
203
+
204
+ 2. **Controller-level** — runs for the direct actions in one controller:
205
+
206
+ ```typescript
207
+ export default createController(routes.account, {
208
+ middleware: [requireAuth()],
209
+ actions: { ... },
210
+ })
211
+ ```
212
+
213
+ Controller middleware does not flow into other controllers. Add the middleware to each
214
+ controller that needs it.
215
+
216
+ 3. **Action-level** — runs for a single route:
217
+ ```typescript
218
+ router.get(routes.account.index, {
219
+ middleware: [requireAuth()],
220
+ handler: accountAction.handler,
221
+ })
222
+ ```
223
+
224
+ ## Node Server Setup
225
+
226
+ New apps already include a `server.ts` that adapts the app router with
227
+ `remix/node-fetch-server`. Keep that generated server unless the task specifically needs to change
228
+ runtime behavior such as host/protocol handling, TLS, HTTP/2, WebSockets, deployment lifecycle, or
229
+ test-only server setup.
230
+
231
+ Use `remix/node-fetch-server` when you want to keep owning a standard Node `http`, `https`, or
232
+ `http2` server directly. Use `remix/node-serve` and `serve()` when you want a managed
233
+ high-performance server with `server.ready`, TLS options, or uWebSockets.js setup.
@@ -0,0 +1,213 @@
1
+ # Mixins, Styling, and Events
2
+
3
+ ## What This Covers
4
+
5
+ How to attach behavior, styles, and DOM-aware setup to host elements with `mix`. Read this when the
6
+ task involves:
7
+
8
+ - DOM event handling with `on(...)`
9
+ - Static styling with `css(...)` and dynamic styling with `style`
10
+ - Imperative DOM access via `ref(...)`
11
+ - Navigation behavior on non-anchor elements with `link(...)`
12
+ - Native click, pointer, and keyboard behavior with `on(...)`, plus attributes with `attrs(...)`
13
+ - Element-level animation mixins from `remix/ui/animation`
14
+
15
+ For richer animation work (springs, tweens, layout transitions), see `animate-elements.md`. For
16
+ authoring custom mixins, see `create-mixins.md`. For component lifecycle and updates, see
17
+ `component-model.md`.
18
+
19
+ Compose behavior on host elements with `mix`. Pass a single mixin directly (`mix={on(...)}`), or
20
+ an array when composing multiple mixins (`mix={[css(...), on(...)]}`). Core mixins are imported
21
+ from `remix/ui`; animation mixins are imported from `remix/ui/animation`.
22
+
23
+ ## `on(type, handler, capture?)`
24
+
25
+ Attaches a typed DOM event handler. The handler receives the event and an `AbortSignal` that aborts
26
+ when the handler is re-entered or the component is removed — this prevents race conditions:
27
+
28
+ ```tsx
29
+ <input
30
+ mix={on('input', async (event, signal) => {
31
+ let query = event.currentTarget.value
32
+ loading = true
33
+ handle.update()
34
+
35
+ let response = await fetch(`/search?q=${query}`, { signal })
36
+ let data = await response.json()
37
+ if (signal.aborted) return
38
+
39
+ results = data.results
40
+ loading = false
41
+ handle.update()
42
+ })}
43
+ />
44
+ ```
45
+
46
+ Multiple events on the same element:
47
+
48
+ ```tsx
49
+ <form
50
+ mix={on('submit', (event) => {
51
+ event.preventDefault()
52
+ let formData = new FormData(event.currentTarget)
53
+ })}
54
+ >
55
+ ```
56
+
57
+ ## `css(styles)`
58
+
59
+ Applies generated class names for CSS object styles. Produces static CSS rules inserted into the
60
+ document. Supports pseudo-selectors, pseudo-elements, attribute selectors, descendant selectors, and
61
+ media queries using `&` to reference the current element:
62
+
63
+ ```tsx
64
+ <button
65
+ mix={css({
66
+ color: 'white',
67
+ backgroundColor: 'blue',
68
+ padding: '12px 24px',
69
+ borderRadius: '4px',
70
+ border: 'none',
71
+ cursor: 'pointer',
72
+ '&:hover': { backgroundColor: 'darkblue' },
73
+ '&:active': { transform: 'scale(0.98)' },
74
+ '&:disabled': { opacity: 0.5, cursor: 'not-allowed' },
75
+ '& .title': { fontSize: '20px', fontWeight: 'bold' },
76
+ '@media (max-width: 768px)': { width: '100%' },
77
+ })}
78
+ />
79
+ ```
80
+
81
+ ### `css(...)` vs `style` prop
82
+
83
+ Use `css(...)` for static styles, selectors, and media queries. Use `style` for dynamic values that
84
+ change often. Prefer CSS nested selectors for parent-state-affects-children over managing hover/focus
85
+ state in JavaScript:
86
+
87
+ ```tsx
88
+ <div
89
+ mix={css({
90
+ backgroundColor: 'blue', // static
91
+ '&:hover': { '& .title': { color: 'blue' } }, // parent hover → child
92
+ })}
93
+ style={{ width: `${progress}%` }} // dynamic
94
+ />
95
+ ```
96
+
97
+ ## `ref(callback)`
98
+
99
+ Calls a callback when an element is inserted. The callback receives the DOM node and an
100
+ `AbortSignal` that aborts when the element is removed:
101
+
102
+ ```tsx
103
+ <input mix={ref((node) => node.focus())} />
104
+
105
+ <div mix={ref((node, signal) => {
106
+ let observer = new ResizeObserver((entries) => {
107
+ dimensions.width = Math.round(entries[0].contentRect.width)
108
+ handle.update()
109
+ })
110
+ observer.observe(node)
111
+ signal.addEventListener('abort', () => observer.disconnect())
112
+ })} />
113
+ ```
114
+
115
+ The `ref` callback runs once when the element is first rendered, not on every update.
116
+
117
+ ## `link(href, options?)`
118
+
119
+ Adds client-side navigation behavior to any element. Makes non-anchor elements behave like Remix
120
+ navigation links:
121
+
122
+ ```tsx
123
+ <article mix={link('/courses/intro')}>
124
+ <h3>Introduction</h3>
125
+ </article>
126
+ ```
127
+
128
+ Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`),
129
+ `resetScroll`.
130
+
131
+ ## Native press and keyboard interactions
132
+
133
+ Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes
134
+ keyboard activation when the element has the right semantics:
135
+
136
+ ```tsx
137
+ <button mix={on('click', () => doAction())}>Action</button>
138
+ ```
139
+
140
+ For gesture-specific behavior, compose the pointer or keyboard events the interaction actually
141
+ needs:
142
+
143
+ ```tsx
144
+ <button
145
+ mix={[
146
+ on('pointerdown', (event) => {
147
+ event.currentTarget.setPointerCapture(event.pointerId)
148
+ }),
149
+ on('pointerup', () => doAction()),
150
+ ]}
151
+ >
152
+ Action
153
+ </button>
154
+
155
+ <div
156
+ tabIndex={0}
157
+ mix={on('keydown', (event) => {
158
+ if (event.key === 'Escape') close()
159
+ if (event.key === 'Enter' || event.key === ' ') doAction()
160
+ })}
161
+ />
162
+ ```
163
+
164
+ ## `attrs()`
165
+
166
+ Sets HTML attributes through the mixin system.
167
+
168
+ ## Animation Mixins
169
+
170
+ ### `animateEntrance(config)`
171
+
172
+ Animates an element when it is inserted into the DOM. Config specifies the **starting** style:
173
+
174
+ ```tsx
175
+ <div mix={animateEntrance({ opacity: 0, transform: 'translateY(8px)', duration: 180 })} />
176
+ ```
177
+
178
+ ### `animateExit(config)`
179
+
180
+ Animates an element when it is removed. Config specifies the **ending** style. The element is kept
181
+ in the DOM until the animation completes:
182
+
183
+ ```tsx
184
+ {
185
+ isVisible && (
186
+ <div
187
+ key="panel"
188
+ mix={[
189
+ animateEntrance({ opacity: 0, transform: 'scale(0.98)', ...spring('smooth') }),
190
+ animateExit({ opacity: 0, duration: 120, easing: 'ease-in' }),
191
+ ]}
192
+ />
193
+ )
194
+ }
195
+ ```
196
+
197
+ ### `animateLayout(config?)`
198
+
199
+ Animates layout changes (position/size) using FLIP-style transforms:
200
+
201
+ ```tsx
202
+ {
203
+ items.map((item) => (
204
+ <li key={item.id} mix={animateLayout({ duration: 220, easing: 'ease-out' })} />
205
+ ))
206
+ }
207
+ ```
208
+
209
+ Options: `duration` (default 200ms), `easing` (default spring snappy), `size` (boolean, default
210
+ true — include scale projection for size changes).
211
+
212
+ Always key elements you expect to animate. Use `...spring(preset)` to spread `duration` and
213
+ `easing` into any animation config.