@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.
- package/README.md +0 -3
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +33 -14
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +9 -33
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/commands/test.d.ts +1 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +8 -4
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +4 -106
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/errors.d.ts +0 -6
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +0 -11
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +4 -5
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +46 -15
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/help.ts +9 -43
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/commands/test.ts +10 -4
- package/src/lib/completion.ts +4 -151
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/errors.ts +0 -12
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/template/.agents/skills/remix/SKILL.md +588 -0
- package/template/.agents/skills/remix/references/animate-elements.md +195 -0
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
- package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
- package/template/.agents/skills/remix/references/component-model.md +282 -0
- package/template/.agents/skills/remix/references/create-mixins.md +158 -0
- package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
- package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
- package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
- package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
- package/{bootstrap → template}/AGENTS.md +13 -8
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/template/app/assets/prompt-button.tsx +163 -0
- package/template/app/assets.ts +20 -0
- package/template/app/middleware/render.tsx +43 -0
- package/template/app/router.ts +20 -0
- package/template/app/routes.ts +6 -0
- package/template/app/ui/document.tsx +33 -0
- package/template/app/ui/scaffold-home-page.tsx +527 -0
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/{bootstrap → template}/server.ts +4 -5
- package/{bootstrap → template}/tsconfig.json +3 -3
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -26
- package/bootstrap/app/router.ts +0 -10
- package/bootstrap/app/routes.ts +0 -6
- package/bootstrap/app/ui/document.tsx +0 -21
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -7
- package/dist/lib/commands/skills.d.ts +0 -6
- package/dist/lib/commands/skills.d.ts.map +0 -1
- package/dist/lib/commands/skills.js +0 -222
- package/dist/lib/skills-cache.d.ts +0 -19
- package/dist/lib/skills-cache.d.ts.map +0 -1
- package/dist/lib/skills-cache.js +0 -89
- package/dist/lib/skills.d.ts +0 -30
- package/dist/lib/skills.d.ts.map +0 -1
- package/dist/lib/skills.js +0 -441
- package/src/lib/commands/skills.ts +0 -306
- package/src/lib/skills-cache.ts +0 -140
- 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.
|