@remix-run/cli 0.3.1 → 0.3.3
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/dist/lib/bootstrap-project.js +7 -4
- package/package.json +3 -3
- package/src/lib/bootstrap-project.ts +9 -6
- package/template/.agents/skills/remix/SKILL.md +116 -263
- package/template/.agents/skills/remix/references/animate-elements.md +8 -17
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +14 -33
- package/template/.agents/skills/remix/references/auth-and-sessions.md +13 -34
- package/template/.agents/skills/remix/references/component-model.md +11 -26
- package/template/.agents/skills/remix/references/create-mixins.md +4 -9
- package/template/.agents/skills/remix/references/data-and-validation.md +54 -77
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +23 -56
- package/template/.agents/skills/remix/references/middleware-and-server.md +32 -48
- package/template/.agents/skills/remix/references/mixins-styling-events.md +14 -32
- package/template/.agents/skills/remix/references/routing-and-controllers.md +30 -59
- package/template/.agents/skills/remix/references/testing-patterns.md +13 -32
- package/template/app/ui/document.tsx +22 -17
- package/template/app/ui/scaffold-home-page.tsx +56 -47
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when
|
|
6
|
-
the task involves:
|
|
5
|
+
Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Defining or changing the URL surface of the app
|
|
9
8
|
- Writing or reorganizing controllers and actions
|
|
@@ -11,9 +10,7 @@ the task involves:
|
|
|
11
10
|
- Returning a `Response` for HTML, redirects, JSON, or errors
|
|
12
11
|
- Generating internal URLs with `.href()`
|
|
13
12
|
|
|
14
|
-
The companion reference for shaping `Request` bodies, validating input, and dealing with persisted
|
|
15
|
-
data is `data-and-validation.md`. For request lifecycle and middleware ordering, see
|
|
16
|
-
`middleware-and-server.md`.
|
|
13
|
+
The companion reference for shaping `Request` bodies, validating input, and dealing with persisted data is `data-and-validation.md`. For request lifecycle and middleware ordering, see `middleware-and-server.md`.
|
|
17
14
|
|
|
18
15
|
## Route Builders
|
|
19
16
|
|
|
@@ -21,10 +18,7 @@ Import all route builders from `remix/routes`.
|
|
|
21
18
|
|
|
22
19
|
### `route(prefix, map)` — nested route group
|
|
23
20
|
|
|
24
|
-
Adds a URL prefix to all children. Can also be called as `route(map)` without a prefix for a
|
|
25
|
-
top-level grouping. Inside `route(...)`, a nested map may be either a `route('prefix', { ... })`
|
|
26
|
-
call (when you want a shared URL prefix) or a plain object literal (when each leaf already owns
|
|
27
|
-
its absolute path).
|
|
21
|
+
Adds a URL prefix to all children. Can also be called as `route(map)` without a prefix for a top-level grouping. Inside `route(...)`, a nested map may be either a `route('prefix', { ... })` call (when you want a shared URL prefix) or a plain object literal (when each leaf already owns its absolute path).
|
|
28
22
|
|
|
29
23
|
```typescript
|
|
30
24
|
import { route, get, post } from 'remix/routes'
|
|
@@ -58,8 +52,7 @@ export const routes = route({
|
|
|
58
52
|
|
|
59
53
|
### `form(path, options?)` — form route
|
|
60
54
|
|
|
61
|
-
Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action`
|
|
62
|
-
(POST) by default.
|
|
55
|
+
Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action` (POST) by default.
|
|
63
56
|
|
|
64
57
|
```typescript
|
|
65
58
|
contact: form('contact')
|
|
@@ -92,9 +85,7 @@ redirect(routes.account.orders.show.href({ orderId: '42' }))
|
|
|
92
85
|
|
|
93
86
|
## Actions
|
|
94
87
|
|
|
95
|
-
An action is the handler for one leaf route. In Remix app code, actions should live in controllers.
|
|
96
|
-
Use `Action` only when a reusable helper needs to type one action before it is added to a
|
|
97
|
-
controller or when you are doing low-level router wiring outside the `app/actions` convention:
|
|
88
|
+
An action is the handler for one leaf route. In Remix app code, actions should live in controllers. Use `Action` only when a reusable helper needs to type one action before it is added to a controller or when you are doing low-level router wiring outside the `app/actions` convention:
|
|
98
89
|
|
|
99
90
|
```typescript
|
|
100
91
|
import { createAction } from 'remix/router'
|
|
@@ -117,21 +108,23 @@ The handler receives a context object with:
|
|
|
117
108
|
- `url` — the request URL
|
|
118
109
|
- `request` — the raw `Request`
|
|
119
110
|
|
|
120
|
-
Actions with
|
|
111
|
+
Actions with action middleware:
|
|
121
112
|
|
|
122
113
|
```typescript
|
|
114
|
+
import { createAction } from 'remix/router'
|
|
123
115
|
import { requireAuth } from 'remix/middleware/auth'
|
|
124
116
|
|
|
125
|
-
|
|
117
|
+
export const account = createAction(routes.account.index, {
|
|
126
118
|
middleware: [requireAuth()],
|
|
127
|
-
handler
|
|
119
|
+
handler(context) {
|
|
120
|
+
return render(<AccountPage />)
|
|
121
|
+
},
|
|
128
122
|
})
|
|
129
123
|
```
|
|
130
124
|
|
|
131
125
|
## Returning Responses
|
|
132
126
|
|
|
133
|
-
An action returns a `Response`. The shape of that response is part of the route contract, and
|
|
134
|
-
choosing it well saves a lot of glue elsewhere.
|
|
127
|
+
An action returns a `Response`. The shape of that response is part of the route contract, and choosing it well saves a lot of glue elsewhere.
|
|
135
128
|
|
|
136
129
|
### Render HTML
|
|
137
130
|
|
|
@@ -147,8 +140,7 @@ async handler({ get }) {
|
|
|
147
140
|
|
|
148
141
|
### Redirect after a mutation
|
|
149
142
|
|
|
150
|
-
For state-changing routes (POST, PUT, PATCH, DELETE), the canonical reply is a redirect to the
|
|
151
|
-
resulting page. Pass `303` explicitly when you want a POST-redirect-GET flow:
|
|
143
|
+
For state-changing routes (POST, PUT, PATCH, DELETE), the canonical reply is a redirect to the resulting page. Pass `303` explicitly when you want a POST-redirect-GET flow:
|
|
152
144
|
|
|
153
145
|
```typescript
|
|
154
146
|
import { redirect } from 'remix/response/redirect'
|
|
@@ -167,13 +159,11 @@ async create({ get }) {
|
|
|
167
159
|
}
|
|
168
160
|
```
|
|
169
161
|
|
|
170
|
-
This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements
|
|
171
|
-
on top.
|
|
162
|
+
This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements on top.
|
|
172
163
|
|
|
173
164
|
### Return an error response
|
|
174
165
|
|
|
175
|
-
For expected failures — validation, conflict, not found — return a `Response` directly. Reserve
|
|
176
|
-
thrown errors for genuinely unexpected failures.
|
|
166
|
+
For expected failures — validation, conflict, not found — return a `Response` directly. Reserve thrown errors for genuinely unexpected failures.
|
|
177
167
|
|
|
178
168
|
```typescript
|
|
179
169
|
async show({ get, params }) {
|
|
@@ -198,9 +188,7 @@ if (!parsed.success) {
|
|
|
198
188
|
|
|
199
189
|
### Return JSON
|
|
200
190
|
|
|
201
|
-
For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling
|
|
202
|
-
APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when
|
|
203
|
-
typed header accessors make the response clearer:
|
|
191
|
+
For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when typed header accessors make the response clearer:
|
|
204
192
|
|
|
205
193
|
```typescript
|
|
206
194
|
import Headers from 'remix/headers'
|
|
@@ -214,20 +202,13 @@ return new Response(JSON.stringify({ results }), {
|
|
|
214
202
|
})
|
|
215
203
|
```
|
|
216
204
|
|
|
217
|
-
If you find yourself returning JSON for what is really a browser form submission, prefer the
|
|
218
|
-
redirect-after-POST pattern instead. JSON-only mutation endpoints make it harder to support
|
|
219
|
-
non-JS clients, harder to share rendering logic, and easier for the client to drift out of sync
|
|
220
|
-
with the server.
|
|
205
|
+
If you find yourself returning JSON for what is really a browser form submission, prefer the redirect-after-POST pattern instead. JSON-only mutation endpoints make it harder to support non-JS clients, harder to share rendering logic, and easier for the client to drift out of sync with the server.
|
|
221
206
|
|
|
222
207
|
## Controllers
|
|
223
208
|
|
|
224
|
-
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct
|
|
225
|
-
leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not
|
|
226
|
-
belong inside a controller's `actions`; map those route maps with their own controllers.
|
|
209
|
+
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
|
|
227
210
|
|
|
228
|
-
Configure `RouterTypes.context` with your app context in the router module, then use
|
|
229
|
-
`createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your
|
|
230
|
-
middleware stack without repeating a type clause on every controller.
|
|
211
|
+
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
|
|
231
212
|
|
|
232
213
|
```typescript
|
|
233
214
|
import { createController } from 'remix/router'
|
|
@@ -284,8 +265,7 @@ Because `account` is a nested route map, it is not an action key in the root con
|
|
|
284
265
|
|
|
285
266
|
### Nested route maps
|
|
286
267
|
|
|
287
|
-
Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`.
|
|
288
|
-
Directory names under `app/actions/` are route-map keys, not URL path segments.
|
|
268
|
+
Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`. Directory names under `app/actions/` are route-map keys, not URL path segments.
|
|
289
269
|
|
|
290
270
|
```typescript
|
|
291
271
|
// app/actions/account/controller.tsx
|
|
@@ -328,8 +308,7 @@ router.map(routes.account.settings, accountSettingsController)
|
|
|
328
308
|
|
|
329
309
|
### Controller middleware
|
|
330
310
|
|
|
331
|
-
The `middleware` array on a controller runs only for the direct actions in that controller, before
|
|
332
|
-
action-level middleware. It does not apply to other controllers.
|
|
311
|
+
The `middleware` array on a controller runs only for the direct actions in that controller, before action middleware. It does not apply to other controllers.
|
|
333
312
|
|
|
334
313
|
```typescript
|
|
335
314
|
export default createController(routes.admin, {
|
|
@@ -342,8 +321,7 @@ export default createController(routes.admin, {
|
|
|
342
321
|
|
|
343
322
|
## Registering Routes
|
|
344
323
|
|
|
345
|
-
Use `router.map` for route maps and controllers. Map each nested route map explicitly. Use verb
|
|
346
|
-
methods only for low-level router wiring outside the `app/actions` controller convention.
|
|
324
|
+
Use `router.map` for route maps and controllers. Map each nested route map explicitly. Use verb methods only for low-level router wiring outside the `app/actions` controller convention.
|
|
347
325
|
|
|
348
326
|
```typescript
|
|
349
327
|
let router = createRouter({ middleware })
|
|
@@ -363,23 +341,16 @@ router.post(routes.logout, logoutAction)
|
|
|
363
341
|
|
|
364
342
|
## Typed Context
|
|
365
343
|
|
|
366
|
-
Define an `AppContext` type from your
|
|
367
|
-
`createAction()` and `createController()`:
|
|
344
|
+
Define an `AppContext` type from your router, then make it the default context used by `createAction()` and `createController()`:
|
|
368
345
|
|
|
369
346
|
```typescript
|
|
370
|
-
import
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
]
|
|
378
|
-
|
|
379
|
-
export type AppContext<params extends AnyParams = {}> = ContextWithParams<
|
|
380
|
-
MiddlewareContext<RootMiddleware>,
|
|
381
|
-
params
|
|
382
|
-
>
|
|
347
|
+
import { createRouter, type RouterContext } from 'remix/router'
|
|
348
|
+
|
|
349
|
+
export const router = createRouter({
|
|
350
|
+
middleware: [formData(), session(cookie, storage), loadDatabase(), loadAuth()],
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
export type AppContext = RouterContext<typeof router>
|
|
383
354
|
|
|
384
355
|
declare module 'remix/router' {
|
|
385
356
|
interface RouterTypes {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when
|
|
6
|
-
the task involves:
|
|
5
|
+
How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Driving the router with `router.fetch(new Request(...))` and asserting on the returned `Response`
|
|
9
8
|
- Building a fresh router per test for session, storage, or database isolation
|
|
@@ -12,24 +11,18 @@ the task involves:
|
|
|
12
11
|
- Using adjacent CLI checks such as `remix routes`, `remix doctor`, and `remix version`
|
|
13
12
|
- Choosing which layer to test for a given behavior
|
|
14
13
|
|
|
15
|
-
For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see
|
|
16
|
-
`component-model.md`.
|
|
14
|
+
For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see `component-model.md`.
|
|
17
15
|
|
|
18
16
|
## Two Shapes
|
|
19
17
|
|
|
20
|
-
Remix tests run with `remix test`, use `remix/test` for the test framework, and use
|
|
21
|
-
`remix/assert` for assertions. Two main shapes:
|
|
18
|
+
Remix tests run with `remix test`, use `remix/test` for the test framework, and use `remix/assert` for assertions. Two main shapes:
|
|
22
19
|
|
|
23
|
-
- **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert
|
|
24
|
-
|
|
25
|
-
- **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use
|
|
26
|
-
`createRoot(...)` directly when you need lower-level root control.
|
|
20
|
+
- **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert on the returned `Response`. No DOM, no browser harness.
|
|
21
|
+
- **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use `createRoot(...)` directly when you need lower-level root control.
|
|
27
22
|
|
|
28
23
|
## Server / Router Tests
|
|
29
24
|
|
|
30
|
-
Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router
|
|
31
|
-
per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays
|
|
32
|
-
isolated.
|
|
25
|
+
Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays isolated.
|
|
33
26
|
|
|
34
27
|
```ts
|
|
35
28
|
import * as assert from 'remix/assert'
|
|
@@ -49,10 +42,7 @@ describe('home', () => {
|
|
|
49
42
|
})
|
|
50
43
|
```
|
|
51
44
|
|
|
52
|
-
Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route
|
|
53
|
-
definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need
|
|
54
|
-
a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the
|
|
55
|
-
router.
|
|
45
|
+
Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the router.
|
|
56
46
|
|
|
57
47
|
```ts
|
|
58
48
|
import { createMemorySessionStorage } from 'remix/session-storage/memory'
|
|
@@ -64,8 +54,7 @@ let router = createBookstoreRouter({
|
|
|
64
54
|
})
|
|
65
55
|
```
|
|
66
56
|
|
|
67
|
-
Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real
|
|
68
|
-
HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
|
|
57
|
+
Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
|
|
69
58
|
|
|
70
59
|
```ts
|
|
71
60
|
import { createTestServer } from 'remix/node-fetch-server/test'
|
|
@@ -102,16 +91,11 @@ export default {
|
|
|
102
91
|
}
|
|
103
92
|
```
|
|
104
93
|
|
|
105
|
-
Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery
|
|
106
|
-
would otherwise enter generated output, symlinked workspaces, or other paths that should not
|
|
107
|
-
produce tests.
|
|
94
|
+
Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests.
|
|
108
95
|
|
|
109
96
|
## Component Tests
|
|
110
97
|
|
|
111
|
-
Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container,
|
|
112
|
-
flushes the initial render, and returns `act(...)` so interactions can flush pending updates before
|
|
113
|
-
assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control
|
|
114
|
-
over root rendering, flushing, or disposal.
|
|
98
|
+
Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container, flushes the initial render, and returns `act(...)` so interactions can flush pending updates before assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control over root rendering, flushing, or disposal.
|
|
115
99
|
|
|
116
100
|
### Basic pattern
|
|
117
101
|
|
|
@@ -130,8 +114,7 @@ result.cleanup()
|
|
|
130
114
|
|
|
131
115
|
### Why act / flush
|
|
132
116
|
|
|
133
|
-
- **After initial render** — ensures event listeners are attached and the DOM is ready for
|
|
134
|
-
interaction.
|
|
117
|
+
- **After initial render** — ensures event listeners are attached and the DOM is ready for interaction.
|
|
135
118
|
- **After interactions** — applies updates from `handle.update()` calls triggered by events.
|
|
136
119
|
- **After async work resolves** — applies updates from resolved `queueTask(...)` callbacks.
|
|
137
120
|
|
|
@@ -152,8 +135,7 @@ assert.equal(result.container.textContent, 'Expected data')
|
|
|
152
135
|
|
|
153
136
|
### Component removal
|
|
154
137
|
|
|
155
|
-
Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup
|
|
156
|
-
behavior:
|
|
138
|
+
Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup behavior:
|
|
157
139
|
|
|
158
140
|
```tsx
|
|
159
141
|
let result = render(<MyComponent />)
|
|
@@ -168,5 +150,4 @@ assert.throws(() => result.$('.content'), /cleaned up/)
|
|
|
168
150
|
|
|
169
151
|
- Prefer real DOM interactions over mocking framework behavior.
|
|
170
152
|
- Avoid testing implementation-only markers unless they are the only stable synchronization point.
|
|
171
|
-
- One representative flow proving a behavior is better than repeating the same assertion across many
|
|
172
|
-
paths.
|
|
153
|
+
- One representative flow proving a behavior is better than repeating the same assertion across many paths.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
2
|
+
import { css } from 'remix/ui'
|
|
2
3
|
|
|
3
4
|
import { routes } from '../routes.ts'
|
|
4
5
|
|
|
@@ -10,22 +11,26 @@ export interface DocumentProps {
|
|
|
10
11
|
|
|
11
12
|
const DEFAULT_TITLE = readAppDisplayName('%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%')
|
|
12
13
|
|
|
13
|
-
export function Document() {
|
|
14
|
-
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
export function Document(handle: Handle<DocumentProps>) {
|
|
15
|
+
return () => {
|
|
16
|
+
let { children, head, title = DEFAULT_TITLE } = handle.props
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charSet="utf-8" />
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
24
|
+
<title>{title}</title>
|
|
25
|
+
{head}
|
|
26
|
+
</head>
|
|
27
|
+
<body mix={css({ margin: 0 })}>
|
|
28
|
+
{children}
|
|
29
|
+
<script type="module" src={routes.assets.href({ path: 'app/assets/entry.ts' })}></script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
function readAppDisplayName(value: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Delete this file and put your own home page in app/actions/controller.tsx
|
|
2
|
-
import {
|
|
2
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
3
|
+
import { css } from 'remix/ui'
|
|
3
4
|
|
|
4
5
|
import { PromptButton } from '../assets/prompt-button.tsx'
|
|
5
6
|
import { Document } from './document.tsx'
|
|
@@ -203,54 +204,62 @@ function CodingWithAiCard() {
|
|
|
203
204
|
)
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
function CardLink() {
|
|
207
|
-
return (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
background: '
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
207
|
+
function CardLink(handle: Handle<{ href: string; icon: RemixNode; label: string }>) {
|
|
208
|
+
return () => {
|
|
209
|
+
let { href, icon, label } = handle.props
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<a
|
|
213
|
+
href={href}
|
|
214
|
+
mix={css({
|
|
215
|
+
display: 'flex',
|
|
216
|
+
gap: '16px',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
padding: '16px',
|
|
219
|
+
borderRadius: '12px',
|
|
220
|
+
color: 'var(--text-primary)',
|
|
221
|
+
textDecoration: 'none',
|
|
222
|
+
background: 'transparent',
|
|
223
|
+
transition: 'background-color 150ms ease, color 150ms ease',
|
|
224
|
+
'&:hover, &:focus-visible': {
|
|
225
|
+
background: 'var(--surface-4)',
|
|
226
|
+
color: 'var(--brand-blue)',
|
|
227
|
+
outline: 'none',
|
|
228
|
+
},
|
|
229
|
+
})}
|
|
230
|
+
>
|
|
231
|
+
<IconSlot>{icon}</IconSlot>
|
|
232
|
+
<span mix={css({ fontSize: '14px', lineHeight: 1.5, whiteSpace: 'nowrap' })}>{label}</span>
|
|
233
|
+
</a>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
231
236
|
}
|
|
232
237
|
|
|
233
|
-
function IconSlot() {
|
|
234
|
-
return (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
238
|
+
function IconSlot(handle: Handle<{ children: RemixNode; rotated?: boolean }>) {
|
|
239
|
+
return () => {
|
|
240
|
+
let { children, rotated = false } = handle.props
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<span
|
|
244
|
+
aria-hidden="true"
|
|
245
|
+
mix={css({
|
|
246
|
+
flex: '0 0 24px',
|
|
247
|
+
width: '24px',
|
|
248
|
+
display: 'flex',
|
|
249
|
+
alignItems: 'center',
|
|
250
|
+
justifyContent: rotated ? 'center' : 'flex-start',
|
|
251
|
+
'& svg': {
|
|
252
|
+
width: '20px',
|
|
253
|
+
height: '20px',
|
|
254
|
+
display: 'block',
|
|
255
|
+
...(rotated ? { transform: 'rotate(180deg)' } : {}),
|
|
256
|
+
},
|
|
257
|
+
})}
|
|
258
|
+
>
|
|
259
|
+
{children}
|
|
260
|
+
</span>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
function Footer() {
|