remix 3.0.0-rc.4 → 3.0.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/INDEX.md +6 -21
- package/README.md +1 -1
- package/dist/component/dev/refresh.d.ts +2 -0
- package/dist/component/dev/refresh.d.ts.map +1 -0
- package/dist/{ui → component}/dev/refresh.js +1 -1
- package/dist/component/jsx-dev-runtime.d.ts +2 -0
- package/dist/component/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/{ui → component}/jsx-dev-runtime.js +1 -1
- package/dist/component/jsx-runtime.d.ts +2 -0
- package/dist/component/jsx-runtime.d.ts.map +1 -0
- package/dist/{ui → component}/jsx-runtime.js +1 -1
- package/dist/component/server.d.ts +2 -0
- package/dist/component/server.d.ts.map +1 -0
- package/dist/{ui → component}/server.js +1 -1
- package/dist/component/test.d.ts +2 -0
- package/dist/component/test.d.ts.map +1 -0
- package/dist/{ui → component}/test.js +1 -1
- package/dist/component-hmr/assets.d.ts +2 -0
- package/dist/component-hmr/assets.d.ts.map +1 -0
- package/dist/{ui-hmr.js → component-hmr/assets.js} +1 -1
- package/dist/component-hmr/node.d.ts +3 -0
- package/dist/component-hmr/node.d.ts.map +1 -0
- package/dist/{ui-hmr → component-hmr}/node.js +1 -1
- package/dist/component-hmr/runtime/browser.d.ts +2 -0
- package/dist/component-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/{ui-hmr → component-hmr}/runtime/browser.js +1 -1
- package/dist/component-hmr/runtime/server.d.ts +2 -0
- package/dist/component-hmr/runtime/server.d.ts.map +1 -0
- package/dist/{ui-hmr → component-hmr}/runtime/server.js +1 -1
- package/dist/component-hmr.d.ts +2 -0
- package/dist/component-hmr.d.ts.map +1 -0
- package/dist/{ui-hmr/assets.js → component-hmr.js} +1 -1
- package/dist/component.d.ts +2 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/{ui/anchor.js → component.js} +1 -1
- package/guides/01-start-here.md +20 -20
- package/guides/02-routing-and-controllers.md +1 -1
- package/guides/03-request-handling.md +1 -1
- package/guides/04-rendering-ui.md +41 -307
- package/guides/05-interactivity.md +42 -37
- package/guides/06-streaming-ui-with-frames.md +28 -70
- package/guides/07-animation.md +41 -35
- package/guides/08-data-and-validation.md +423 -27
- package/guides/09-forms-and-mutations.md +250 -25
- package/guides/13-testing.md +10 -10
- package/guides/14-cli-and-tooling.md +2 -2
- package/package.json +96 -180
- package/src/assets/README.md +12 -12
- package/src/cli/README.md +3 -3
- package/src/{ui → component}/README.md +31 -75
- package/src/{ui → component}/dev/refresh.ts +1 -1
- package/src/{ui → component}/jsx-dev-runtime.ts +1 -1
- package/src/{ui → component}/jsx-runtime.ts +1 -1
- package/src/{ui → component}/server/README.md +8 -8
- package/src/{ui → component}/server.ts +1 -1
- package/src/{ui → component}/test/README.md +6 -6
- package/src/{ui → component}/test.ts +1 -1
- package/src/{ui-hmr → component-hmr}/README.md +13 -13
- package/src/{ui-hmr → component-hmr}/assets.ts +1 -1
- package/src/{ui-hmr → component-hmr}/node.ts +1 -1
- package/src/{ui-hmr → component-hmr}/runtime/browser.ts +1 -1
- package/src/{ui-hmr → component-hmr}/runtime/server.ts +1 -1
- package/{dist/ui/accordion.js → src/component-hmr.ts} +1 -1
- package/{dist/ui/tabs.js → src/component.ts} +1 -1
- package/src/data-table/README.md +2 -1
- package/src/multiple-import-maps-polyfill/README.md +3 -3
- package/src/node-hmr/README.md +6 -6
- package/src/render-middleware/README.md +5 -5
- package/src/spa/README.md +4 -4
- package/src/test/README.md +4 -4
- package/dist/ui/accordion/primitives.d.ts +0 -2
- package/dist/ui/accordion/primitives.d.ts.map +0 -1
- package/dist/ui/accordion/primitives.js +0 -2
- package/dist/ui/accordion.d.ts +0 -2
- package/dist/ui/accordion.d.ts.map +0 -1
- package/dist/ui/anchor.d.ts +0 -2
- package/dist/ui/anchor.d.ts.map +0 -1
- package/dist/ui/animation.d.ts +0 -2
- package/dist/ui/animation.d.ts.map +0 -1
- package/dist/ui/animation.js +0 -2
- package/dist/ui/breadcrumbs.d.ts +0 -2
- package/dist/ui/breadcrumbs.d.ts.map +0 -1
- package/dist/ui/breadcrumbs.js +0 -2
- package/dist/ui/button.d.ts +0 -3
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -3
- package/dist/ui/checkbox.d.ts +0 -3
- package/dist/ui/checkbox.d.ts.map +0 -1
- package/dist/ui/checkbox.js +0 -3
- package/dist/ui/combobox/primitives.d.ts +0 -2
- package/dist/ui/combobox/primitives.d.ts.map +0 -1
- package/dist/ui/combobox/primitives.js +0 -2
- package/dist/ui/combobox.d.ts +0 -2
- package/dist/ui/combobox.d.ts.map +0 -1
- package/dist/ui/combobox.js +0 -2
- package/dist/ui/dev/refresh.d.ts +0 -2
- package/dist/ui/dev/refresh.d.ts.map +0 -1
- package/dist/ui/input.d.ts +0 -3
- package/dist/ui/input.d.ts.map +0 -1
- package/dist/ui/input.js +0 -3
- package/dist/ui/jsx-dev-runtime.d.ts +0 -2
- package/dist/ui/jsx-dev-runtime.d.ts.map +0 -1
- package/dist/ui/jsx-runtime.d.ts +0 -2
- package/dist/ui/jsx-runtime.d.ts.map +0 -1
- package/dist/ui/listbox.d.ts +0 -2
- package/dist/ui/listbox.d.ts.map +0 -1
- package/dist/ui/listbox.js +0 -2
- package/dist/ui/menu/primitives.d.ts +0 -2
- package/dist/ui/menu/primitives.d.ts.map +0 -1
- package/dist/ui/menu/primitives.js +0 -2
- package/dist/ui/menu.d.ts +0 -2
- package/dist/ui/menu.d.ts.map +0 -1
- package/dist/ui/menu.js +0 -2
- package/dist/ui/popover.d.ts +0 -2
- package/dist/ui/popover.d.ts.map +0 -1
- package/dist/ui/popover.js +0 -2
- package/dist/ui/radio.d.ts +0 -3
- package/dist/ui/radio.d.ts.map +0 -1
- package/dist/ui/radio.js +0 -3
- package/dist/ui/select/primitives.d.ts +0 -2
- package/dist/ui/select/primitives.d.ts.map +0 -1
- package/dist/ui/select/primitives.js +0 -2
- package/dist/ui/select.d.ts +0 -2
- package/dist/ui/select.d.ts.map +0 -1
- package/dist/ui/select.js +0 -2
- package/dist/ui/server.d.ts +0 -2
- package/dist/ui/server.d.ts.map +0 -1
- package/dist/ui/tabs/primitives.d.ts +0 -2
- package/dist/ui/tabs/primitives.d.ts.map +0 -1
- package/dist/ui/tabs/primitives.js +0 -2
- package/dist/ui/tabs.d.ts +0 -2
- package/dist/ui/tabs.d.ts.map +0 -1
- package/dist/ui/test.d.ts +0 -2
- package/dist/ui/test.d.ts.map +0 -1
- package/dist/ui/toggle/primitives.d.ts +0 -2
- package/dist/ui/toggle/primitives.d.ts.map +0 -1
- package/dist/ui/toggle/primitives.js +0 -2
- package/dist/ui/toggle.d.ts +0 -3
- package/dist/ui/toggle.d.ts.map +0 -1
- package/dist/ui/toggle.js +0 -3
- package/dist/ui-hmr/assets.d.ts +0 -2
- package/dist/ui-hmr/assets.d.ts.map +0 -1
- package/dist/ui-hmr/node.d.ts +0 -3
- package/dist/ui-hmr/node.d.ts.map +0 -1
- package/dist/ui-hmr/runtime/browser.d.ts +0 -2
- package/dist/ui-hmr/runtime/browser.d.ts.map +0 -1
- package/dist/ui-hmr/runtime/server.d.ts +0 -2
- package/dist/ui-hmr/runtime/server.d.ts.map +0 -1
- package/dist/ui-hmr.d.ts +0 -2
- package/dist/ui-hmr.d.ts.map +0 -1
- package/dist/ui.d.ts +0 -2
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js +0 -2
- package/src/ui/accordion/README.md +0 -202
- package/src/ui/accordion/primitives.ts +0 -2
- package/src/ui/accordion.ts +0 -2
- package/src/ui/anchor/README.md +0 -196
- package/src/ui/anchor.ts +0 -2
- package/src/ui/animation/README.md +0 -318
- package/src/ui/animation.ts +0 -2
- package/src/ui/breadcrumbs/README.md +0 -55
- package/src/ui/breadcrumbs.ts +0 -2
- package/src/ui/button/README.md +0 -44
- package/src/ui/button.ts +0 -3
- package/src/ui/checkbox/README.md +0 -59
- package/src/ui/checkbox.ts +0 -3
- package/src/ui/combobox/README.md +0 -194
- package/src/ui/combobox/primitives.ts +0 -2
- package/src/ui/combobox.ts +0 -2
- package/src/ui/input/README.md +0 -52
- package/src/ui/input.ts +0 -3
- package/src/ui/listbox/README.md +0 -83
- package/src/ui/listbox.ts +0 -2
- package/src/ui/menu/README.md +0 -161
- package/src/ui/menu/primitives.ts +0 -2
- package/src/ui/menu.ts +0 -2
- package/src/ui/popover/README.md +0 -103
- package/src/ui/popover.ts +0 -2
- package/src/ui/radio/README.md +0 -53
- package/src/ui/radio.ts +0 -3
- package/src/ui/select/README.md +0 -117
- package/src/ui/select/primitives.ts +0 -2
- package/src/ui/select.ts +0 -2
- package/src/ui/tabs/README.md +0 -141
- package/src/ui/tabs/primitives.ts +0 -2
- package/src/ui/tabs.ts +0 -2
- package/src/ui/toggle/README.md +0 -56
- package/src/ui/toggle/primitives.ts +0 -2
- package/src/ui/toggle.ts +0 -3
- package/src/ui-hmr.ts +0 -2
- package/src/ui.ts +0 -2
|
@@ -17,7 +17,7 @@ A Remix component is a setup function that returns a render function. Setup runs
|
|
|
17
17
|
creates the component. Render runs immediately after setup and again whenever the component updates.
|
|
18
18
|
|
|
19
19
|
```tsx filename=app/ui/album-heading.tsx
|
|
20
|
-
import type { Handle } from "remix/
|
|
20
|
+
import type { Handle } from "remix/component";
|
|
21
21
|
|
|
22
22
|
type AlbumHeadingProps = {
|
|
23
23
|
artist: string;
|
|
@@ -48,7 +48,7 @@ components, strings, numbers, booleans, `null`, `undefined`, and nested arrays o
|
|
|
48
48
|
Fragments let you return siblings without adding another DOM element:
|
|
49
49
|
|
|
50
50
|
```tsx filename=app/ui/album-byline.tsx
|
|
51
|
-
import type { Handle } from "remix/
|
|
51
|
+
import type { Handle } from "remix/component";
|
|
52
52
|
|
|
53
53
|
export function AlbumByline(handle: Handle<{ artist: string; year: number }>) {
|
|
54
54
|
return () => (
|
|
@@ -64,8 +64,8 @@ The same component can render to HTML on the server, mount into a client-only ro
|
|
|
64
64
|
a server-rendered page. Only the last two cases execute it in the browser.
|
|
65
65
|
|
|
66
66
|
```tsx
|
|
67
|
-
import { css, on } from "remix/
|
|
68
|
-
import type { Handle } from "remix/
|
|
67
|
+
import { css, on } from "remix/component";
|
|
68
|
+
import type { Handle } from "remix/component";
|
|
69
69
|
|
|
70
70
|
export function ComponentModelDemo() {
|
|
71
71
|
return () => (
|
|
@@ -142,7 +142,7 @@ Destructuring the object is safe. Destructuring one of its properties in setup c
|
|
|
142
142
|
initial value.
|
|
143
143
|
|
|
144
144
|
```tsx
|
|
145
|
-
import type { Handle } from "remix/
|
|
145
|
+
import type { Handle } from "remix/component";
|
|
146
146
|
|
|
147
147
|
function AlbumTitle(handle: Handle<{ title: string }>) {
|
|
148
148
|
let { props } = handle;
|
|
@@ -156,8 +156,8 @@ Local state is ordinary JavaScript declared in setup scope. Store values that af
|
|
|
156
156
|
derive everything else inside render:
|
|
157
157
|
|
|
158
158
|
```tsx filename=app/ui/album-list.tsx
|
|
159
|
-
import { on } from "remix/
|
|
160
|
-
import type { Handle } from "remix/
|
|
159
|
+
import { on } from "remix/component";
|
|
160
|
+
import type { Handle } from "remix/component";
|
|
161
161
|
|
|
162
162
|
type Album = {
|
|
163
163
|
id: string;
|
|
@@ -203,7 +203,7 @@ list changes order. [Interactivity](/interactivity/) covers event handlers, upda
|
|
|
203
203
|
needs to connect a label, input, description, or ARIA relationship without requiring an `id` prop:
|
|
204
204
|
|
|
205
205
|
```tsx
|
|
206
|
-
import type { Handle } from "remix/
|
|
206
|
+
import type { Handle } from "remix/component";
|
|
207
207
|
|
|
208
208
|
function AlbumSearch(handle: Handle) {
|
|
209
209
|
return () => (
|
|
@@ -219,7 +219,7 @@ Use component context when descendants need a value that does not belong on ever
|
|
|
219
219
|
component. The provider type is the context key, so `get()` remains typed:
|
|
220
220
|
|
|
221
221
|
```tsx filename=app/ui/catalog.tsx
|
|
222
|
-
import type { Handle, RemixNode } from "remix/
|
|
222
|
+
import type { Handle, RemixNode } from "remix/component";
|
|
223
223
|
|
|
224
224
|
type CatalogContext = {
|
|
225
225
|
currency: "USD" | "EUR";
|
|
@@ -293,8 +293,8 @@ Pages should render a complete document through one shared component. The defaul
|
|
|
293
293
|
`app/actions/document.tsx`:
|
|
294
294
|
|
|
295
295
|
```tsx filename=app/actions/document.tsx
|
|
296
|
-
import type { Handle, RemixNode } from "remix/
|
|
297
|
-
import { ImportMap } from "remix/
|
|
296
|
+
import type { Handle, RemixNode } from "remix/component";
|
|
297
|
+
import { ImportMap } from "remix/component/server";
|
|
298
298
|
|
|
299
299
|
import { scriptEntry } from "../assets.ts";
|
|
300
300
|
|
|
@@ -347,8 +347,8 @@ Use `css(...)` for static rules. It supports pseudo-selectors, pseudo-elements,
|
|
|
347
347
|
attribute selectors, and media queries with normal CSS nesting:
|
|
348
348
|
|
|
349
349
|
```tsx filename=app/ui/album-card.tsx
|
|
350
|
-
import { css } from "remix/
|
|
351
|
-
import type { Handle } from "remix/
|
|
350
|
+
import { css } from "remix/component";
|
|
351
|
+
import type { Handle } from "remix/component";
|
|
352
352
|
|
|
353
353
|
const cardStyle = css({
|
|
354
354
|
border: "1px solid #d6d6d6",
|
|
@@ -385,8 +385,8 @@ During server rendering, Remix collects generated rules, deduplicates them, and
|
|
|
385
385
|
JavaScript to receive its component styles.
|
|
386
386
|
|
|
387
387
|
```tsx
|
|
388
|
-
import { css } from "remix/
|
|
389
|
-
import type { Handle } from "remix/
|
|
388
|
+
import { css } from "remix/component";
|
|
389
|
+
import type { Handle } from "remix/component";
|
|
390
390
|
|
|
391
391
|
export function StylingCardDemo() {
|
|
392
392
|
return () => (
|
|
@@ -519,8 +519,8 @@ const productButtonStyles = css({
|
|
|
519
519
|
|
|
520
520
|
## Cascade layers and app-owned design tokens {#theme-tokens-and-cascade-layers}
|
|
521
521
|
|
|
522
|
-
Generated `css(...)` rules
|
|
523
|
-
|
|
522
|
+
Generated `css(...)` rules live in the native `rmx` cascade layer. If your app uses its own layers,
|
|
523
|
+
declare the complete order once:
|
|
524
524
|
|
|
525
525
|
```css filename=app/actions/public/app.css
|
|
526
526
|
@layer base, rmx, app;
|
|
@@ -545,309 +545,43 @@ Generated `css(...)` rules, including styles from first-party UI components, liv
|
|
|
545
545
|
}
|
|
546
546
|
```
|
|
547
547
|
|
|
548
|
-
Layers before `rmx` provide defaults that
|
|
549
|
-
|
|
550
|
-
|
|
548
|
+
Layers before `rmx` provide defaults that generated styles can override. Layers after `rmx` can
|
|
549
|
+
override generated styles deliberately. Unlayered author CSS outranks normal layered CSS, so use it
|
|
550
|
+
intentionally when the rest of the app has an explicit layer order.
|
|
551
551
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
TypeScript values.
|
|
552
|
+
Keep brand colors, spacing, typography, radii, and other design tokens in app-owned CSS custom
|
|
553
|
+
properties or TypeScript values.
|
|
555
554
|
|
|
556
|
-
##
|
|
555
|
+
## Optional headless UI primitives {#headless-ui-primitives}
|
|
557
556
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
557
|
+
`@remix-run/ui` is a separate package of headless, accessible interaction primitives. The package
|
|
558
|
+
is currently unstable and versioned independently. It is not available through the `remix` package.
|
|
559
|
+
Install it when an app needs reusable behavior for controls such as accordions, comboboxes,
|
|
560
|
+
listboxes, menus, popovers, selects, tabs, or toggles:
|
|
561
561
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
shows how to choose that boundary.
|
|
565
|
-
|
|
566
|
-
| Level | Subpaths | What the app owns |
|
|
567
|
-
| ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
568
|
-
| Style mixins | `button`, `input`, `checkbox`, `radio`, `toggle` | The native control, its label, layout, and state. |
|
|
569
|
-
| Composed controls | `accordion`, `breadcrumbs`, `combobox`, `menu`, `select`, `tabs` | The surrounding page and the values passed into the control. |
|
|
570
|
-
| Headless primitives | `popover`, `listbox`, `anchor`, and each available `/primitives` subpath | Markup and styling while Remix supplies focused behavior. |
|
|
571
|
-
|
|
572
|
-
The [`remix/ui` API overview](../src/ui/README.md) links to the complete API
|
|
573
|
-
for every subpath. The sections below show how to choose among them.
|
|
574
|
-
|
|
575
|
-
### Style mixins for native controls {#style-mixins-keep-native-controls-native}
|
|
576
|
-
|
|
577
|
-
Style mixins keep native form behavior in the element you render:
|
|
578
|
-
|
|
579
|
-
```tsx filename=app/ui/album-actions.tsx
|
|
580
|
-
import button from "remix/ui/button";
|
|
581
|
-
import checkbox from "remix/ui/checkbox";
|
|
582
|
-
import { css } from "remix/ui";
|
|
583
|
-
|
|
584
|
-
const actionRowStyle = css({
|
|
585
|
-
display: "flex",
|
|
586
|
-
alignItems: "center",
|
|
587
|
-
gap: "0.75rem",
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
export function AlbumActions() {
|
|
591
|
-
return () => (
|
|
592
|
-
<div mix={actionRowStyle}>
|
|
593
|
-
<button mix={button({ tone: "primary" })}>Add to cart</button>
|
|
594
|
-
<label>
|
|
595
|
-
<input mix={checkbox()} name="gift" type="checkbox" />
|
|
596
|
-
This is a gift
|
|
597
|
-
</label>
|
|
598
|
-
</div>
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
The checkbox remains a checkbox, participates in `FormData`, and gets its keyboard behavior from the
|
|
604
|
-
browser. The mixin supplies visuals. Compose an array in `mix` when a host also needs app-owned styles
|
|
605
|
-
or behavior.
|
|
606
|
-
|
|
607
|
-
```tsx
|
|
608
|
-
import { css } from "remix/ui";
|
|
609
|
-
import button from "remix/ui/button";
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* @name Button Basic
|
|
613
|
-
* @description The button mixin applies neutral, primary, or ghost pill styling to button-like hosts.
|
|
614
|
-
* @layout center
|
|
615
|
-
*/
|
|
616
|
-
export function ButtonBasic() {
|
|
617
|
-
return () => (
|
|
618
|
-
<div mix={buttonDemoCss}>
|
|
619
|
-
<section mix={toneSectionCss}>
|
|
620
|
-
<h2 mix={toneLabelCss}>Neutral</h2>
|
|
621
|
-
<div mix={buttonRowCss}>
|
|
622
|
-
<button mix={button()}>Medium</button>
|
|
623
|
-
<button mix={button({ size: "lg" })}>Large</button>
|
|
624
|
-
<button aria-pressed="true" mix={button()}>
|
|
625
|
-
Pressed
|
|
626
|
-
</button>
|
|
627
|
-
<button disabled mix={button()}>
|
|
628
|
-
Disabled
|
|
629
|
-
</button>
|
|
630
|
-
</div>
|
|
631
|
-
</section>
|
|
632
|
-
|
|
633
|
-
<section mix={toneSectionCss}>
|
|
634
|
-
<h2 mix={toneLabelCss}>Primary</h2>
|
|
635
|
-
<div mix={buttonRowCss}>
|
|
636
|
-
<button mix={button({ tone: "primary" })}>Medium</button>
|
|
637
|
-
<button mix={button({ size: "lg", tone: "primary" })}>Large</button>
|
|
638
|
-
<button aria-pressed="true" mix={button({ tone: "primary" })}>
|
|
639
|
-
Pressed
|
|
640
|
-
</button>
|
|
641
|
-
<button disabled mix={button({ tone: "primary" })}>
|
|
642
|
-
Disabled
|
|
643
|
-
</button>
|
|
644
|
-
</div>
|
|
645
|
-
</section>
|
|
646
|
-
|
|
647
|
-
<section mix={toneSectionCss}>
|
|
648
|
-
<h2 mix={toneLabelCss}>Ghost</h2>
|
|
649
|
-
<div mix={buttonRowCss}>
|
|
650
|
-
<button mix={button({ tone: "ghost" })}>Medium</button>
|
|
651
|
-
<button mix={button({ size: "lg", tone: "ghost" })}>Large</button>
|
|
652
|
-
<button aria-pressed="true" mix={button({ tone: "ghost" })}>
|
|
653
|
-
Pressed
|
|
654
|
-
</button>
|
|
655
|
-
<button disabled mix={button({ tone: "ghost" })}>
|
|
656
|
-
Disabled
|
|
657
|
-
</button>
|
|
658
|
-
</div>
|
|
659
|
-
</section>
|
|
660
|
-
</div>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const buttonDemoCss = css({
|
|
665
|
-
display: "grid",
|
|
666
|
-
gap: "22px",
|
|
667
|
-
width: "min(100%, 34rem)",
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
const toneSectionCss = css({
|
|
671
|
-
display: "grid",
|
|
672
|
-
gap: "8px",
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
const toneLabelCss = css({
|
|
676
|
-
margin: 0,
|
|
677
|
-
fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
|
|
678
|
-
fontSize: "11px",
|
|
679
|
-
lineHeight: "14px",
|
|
680
|
-
fontWeight: 600,
|
|
681
|
-
letterSpacing: 0,
|
|
682
|
-
color: "rgba(16, 16, 16, 0.58)",
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
const buttonRowCss = css({
|
|
686
|
-
display: "flex",
|
|
687
|
-
flexWrap: "wrap",
|
|
688
|
-
alignItems: "center",
|
|
689
|
-
gap: "10px",
|
|
690
|
-
});
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### Composed controls for common interactions {#composed-components-cover-common-product-ui}
|
|
694
|
-
|
|
695
|
-
Composed controls own the relationships among several elements. For example, `Accordion` connects
|
|
696
|
-
triggers to content regions and manages disclosure state:
|
|
697
|
-
|
|
698
|
-
```tsx filename=app/ui/shipping-details.tsx
|
|
699
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "remix/ui/accordion";
|
|
700
|
-
|
|
701
|
-
export function ShippingDetails() {
|
|
702
|
-
return () => (
|
|
703
|
-
<Accordion defaultValue="returns">
|
|
704
|
-
<AccordionItem value="delivery">
|
|
705
|
-
<AccordionTrigger>Delivery</AccordionTrigger>
|
|
706
|
-
<AccordionContent>Ships in two business days.</AccordionContent>
|
|
707
|
-
</AccordionItem>
|
|
708
|
-
<AccordionItem value="returns">
|
|
709
|
-
<AccordionTrigger>Returns</AccordionTrigger>
|
|
710
|
-
<AccordionContent>Return unopened albums within 30 days.</AccordionContent>
|
|
711
|
-
</AccordionItem>
|
|
712
|
-
</Accordion>
|
|
713
|
-
);
|
|
714
|
-
}
|
|
562
|
+
```sh
|
|
563
|
+
npm i @remix-run/ui
|
|
715
564
|
```
|
|
716
565
|
|
|
717
|
-
|
|
718
|
-
prop and change callback when the parent must own that state. The exact names differ by component:
|
|
719
|
-
for example, accordion uses `defaultValue`/`value`, while tabs uses
|
|
720
|
-
`defaultActiveTab`/`activeTab`.
|
|
721
|
-
|
|
722
|
-
Selection and disclosure events bubble where a component's API documents them, so a parent can
|
|
723
|
-
observe changes without threading a callback through every item. Controls that accept `name`, such
|
|
724
|
-
as `Select` and `Combobox`, render a hidden input so the selected value participates in a normal form.
|
|
725
|
-
Disabled state, accessible names, focus movement, and keyboard behavior remain part of each
|
|
726
|
-
component's contract.
|
|
566
|
+
Import each primitive from its package subpath. For example:
|
|
727
567
|
|
|
728
568
|
```tsx
|
|
729
|
-
import { css } from "remix/
|
|
730
|
-
import
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* @name Accordion Overview
|
|
734
|
-
* @description A single-open disclosure list that keeps settings, billing, or notification rules in one calm section.
|
|
735
|
-
* @layout center
|
|
736
|
-
* @order 1
|
|
737
|
-
*/
|
|
738
|
-
export function AccordionOverview() {
|
|
739
|
-
return () => (
|
|
740
|
-
<Accordion defaultValue="account">
|
|
741
|
-
<AccordionItem value="account">
|
|
742
|
-
<AccordionTrigger>Account defaults</AccordionTrigger>
|
|
743
|
-
<AccordionContent>
|
|
744
|
-
<p mix={bodyTextCss}>
|
|
745
|
-
Keep billing contacts, email summaries, and workspace naming rules in one calm
|
|
746
|
-
disclosure list without adding another card layer.
|
|
747
|
-
</p>
|
|
748
|
-
</AccordionContent>
|
|
749
|
-
</AccordionItem>
|
|
750
|
-
|
|
751
|
-
<AccordionItem value="billing">
|
|
752
|
-
<AccordionTrigger>Billing schedule</AccordionTrigger>
|
|
753
|
-
<AccordionContent>
|
|
754
|
-
<p mix={bodyTextCss}>
|
|
755
|
-
Review invoice timing, payment methods, and renewal reminders with the same spacing and
|
|
756
|
-
typography used elsewhere in the system.
|
|
757
|
-
</p>
|
|
758
|
-
</AccordionContent>
|
|
759
|
-
</AccordionItem>
|
|
760
|
-
|
|
761
|
-
<AccordionItem value="notifications">
|
|
762
|
-
<AccordionTrigger>Notification rules</AccordionTrigger>
|
|
763
|
-
<AccordionContent>
|
|
764
|
-
<p mix={bodyTextCss}>
|
|
765
|
-
Use single mode when only one details panel should stay open at a time in a compact
|
|
766
|
-
settings or details view.
|
|
767
|
-
</p>
|
|
768
|
-
</AccordionContent>
|
|
769
|
-
</AccordionItem>
|
|
770
|
-
</Accordion>
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const bodyTextCss = css({
|
|
775
|
-
margin: 0,
|
|
776
|
-
fontSize: "13px",
|
|
777
|
-
lineHeight: "1.65",
|
|
778
|
-
color: "#4f4f4f",
|
|
779
|
-
});
|
|
569
|
+
import { css } from "remix/component";
|
|
570
|
+
import * as accordion from "@remix-run/ui/accordion";
|
|
780
571
|
```
|
|
781
572
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
* @name Select Overview
|
|
788
|
-
* @description A styled select control with a searchable dropdown and accessible label.
|
|
789
|
-
* @layout center
|
|
790
|
-
*/
|
|
791
|
-
export function SelectOverview() {
|
|
792
|
-
return () => (
|
|
793
|
-
<div mix={stackCss}>
|
|
794
|
-
<label for="fruit-select" mix={labelCss}>
|
|
795
|
-
Choose a fruit
|
|
796
|
-
</label>
|
|
797
|
-
<Select
|
|
798
|
-
id="fruit-select"
|
|
799
|
-
defaultLabel="Banana"
|
|
800
|
-
defaultValue="banana"
|
|
801
|
-
name="fruit"
|
|
802
|
-
mix={selectCss}
|
|
803
|
-
>
|
|
804
|
-
<Option label="Apple" value="apple" />
|
|
805
|
-
<Option label="Apricot" value="apricot" />
|
|
806
|
-
<Option label="Banana" value="banana" />
|
|
807
|
-
<Option label="Blackberry" value="blackberry" />
|
|
808
|
-
<Option label="Blackcurrant" value="blackcurrant" />
|
|
809
|
-
<Option label="Blueberry" value="blueberry" />
|
|
810
|
-
<Option label="Boysenberry" value="boysenberry" />
|
|
811
|
-
<Option label="Cantaloupe" value="cantaloupe" />
|
|
812
|
-
</Select>
|
|
813
|
-
</div>
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
const selectCss = css({
|
|
818
|
-
width: "16rem",
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
const stackCss = css({
|
|
822
|
-
display: "flex",
|
|
823
|
-
flexDirection: "column",
|
|
824
|
-
gap: "8px",
|
|
825
|
-
width: "100%",
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
const labelCss = css({
|
|
829
|
-
margin: 0,
|
|
830
|
-
fontSize: "12px",
|
|
831
|
-
fontWeight: "600",
|
|
832
|
-
color: "#151515",
|
|
833
|
-
});
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
### Headless primitives for custom markup {#primitives-keep-behavior-reusable-when-markup-changes}
|
|
837
|
-
|
|
838
|
-
Reach for primitives when the product requires different markup, not merely different colors or
|
|
839
|
-
spacing. `popover` supplies anchored-surface behavior, `listbox` supplies option highlighting and
|
|
840
|
-
selection, and `anchor` handles lower-level floating-element placement. Accordion, combobox, menu,
|
|
841
|
-
select, tabs, and toggle also expose `/primitives` subpaths.
|
|
573
|
+
The primitives supply structural attributes, keyboard interaction, focus management, and state
|
|
574
|
+
coordination. Your app supplies the markup and all visual styling. Controls that handle browser
|
|
575
|
+
events must also be inside a `clientEntry(...)` boundary, either directly or through an interactive
|
|
576
|
+
ancestor. The next chapter shows how to choose that boundary.
|
|
842
577
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
participates in a form.
|
|
578
|
+
Preserve native elements when they fit, keep labels and ARIA relationships intact, and test pointer
|
|
579
|
+
and keyboard behavior. A custom select, for example, still needs a provider, trigger, popover,
|
|
580
|
+
list, options, and hidden input if it participates in a form.
|
|
847
581
|
|
|
848
582
|
```tsx
|
|
849
|
-
import { css } from "remix/
|
|
850
|
-
import * as accordion from "remix/ui/accordion
|
|
583
|
+
import { css } from "remix/component";
|
|
584
|
+
import * as accordion from "@remix-run/ui/accordion";
|
|
851
585
|
|
|
852
586
|
/**
|
|
853
587
|
* @name Accordion Primitives
|
|
@@ -18,7 +18,7 @@ Begin with links and forms that already describe the request the server should r
|
|
|
18
18
|
form has a real action and method:
|
|
19
19
|
|
|
20
20
|
```tsx filename=app/actions/albums/edit/public/album-edit-form.tsx
|
|
21
|
-
import type { Handle } from "remix/
|
|
21
|
+
import type { Handle } from "remix/component";
|
|
22
22
|
|
|
23
23
|
import { routes } from "../../../../routes.ts";
|
|
24
24
|
|
|
@@ -72,8 +72,8 @@ it needs rather than moving the whole page into browser code.
|
|
|
72
72
|
can remain server-only while its form hydrates:
|
|
73
73
|
|
|
74
74
|
```tsx filename=app/actions/albums/edit/public/album-edit-form.tsx lines=[1,13-15]
|
|
75
|
-
import { clientEntry } from "remix/
|
|
76
|
-
import type { Handle } from "remix/
|
|
75
|
+
import { clientEntry } from "remix/component";
|
|
76
|
+
import type { Handle } from "remix/component";
|
|
77
77
|
|
|
78
78
|
import { routes } from "../../../../routes.ts";
|
|
79
79
|
|
|
@@ -123,7 +123,7 @@ import {
|
|
|
123
123
|
importModule,
|
|
124
124
|
preloadShim,
|
|
125
125
|
} from "remix/multiple-import-maps-polyfill";
|
|
126
|
-
import { run } from "remix/
|
|
126
|
+
import { run } from "remix/component";
|
|
127
127
|
|
|
128
128
|
let app = run({
|
|
129
129
|
async loadModule(moduleUrl, exportName) {
|
|
@@ -163,8 +163,8 @@ The returned app runtime has three lifecycle methods:
|
|
|
163
163
|
`run()` hydrates the server-rendered page. It is not a second application router. Browser requests
|
|
164
164
|
still go to the route actions that own the corresponding server behavior.
|
|
165
165
|
|
|
166
|
-
[Streaming UI with Frames](/streaming-ui-with-frames/)
|
|
167
|
-
|
|
166
|
+
[Streaming UI with Frames](/streaming-ui-with-frames/) uses the default browser resolver for
|
|
167
|
+
route-owned regions that load or reload independently.
|
|
168
168
|
|
|
169
169
|
## Mount client-only UI with createRoot {#client-only-roots}
|
|
170
170
|
|
|
@@ -172,8 +172,8 @@ Use `createRoot(container)` when there is no server-rendered component to hydrat
|
|
|
172
172
|
imperative widget mounted into a container created by another application.
|
|
173
173
|
|
|
174
174
|
```tsx filename=app/actions/public/support-widget.tsx
|
|
175
|
-
import { createRoot } from "remix/
|
|
176
|
-
import type { Handle } from "remix/
|
|
175
|
+
import { createRoot } from "remix/component";
|
|
176
|
+
import type { Handle } from "remix/component";
|
|
177
177
|
|
|
178
178
|
function SupportWidget(_handle: Handle) {
|
|
179
179
|
return () => <a href="/support">Contact support</a>;
|
|
@@ -200,8 +200,8 @@ Component-local UI state lives in setup scope. Change it in an event handler, th
|
|
|
200
200
|
`handle.update()`:
|
|
201
201
|
|
|
202
202
|
```tsx
|
|
203
|
-
import { on } from "remix/
|
|
204
|
-
import type { Handle } from "remix/
|
|
203
|
+
import { on } from "remix/component";
|
|
204
|
+
import type { Handle } from "remix/component";
|
|
205
205
|
|
|
206
206
|
function Counter(handle: Handle<{ initialCount: number }>) {
|
|
207
207
|
let count = handle.props.initialCount;
|
|
@@ -233,8 +233,8 @@ throws an error. If rendering discovers work that should run after the commit, s
|
|
|
233
233
|
with `handle.queueTask()`.
|
|
234
234
|
|
|
235
235
|
```tsx
|
|
236
|
-
import { on, ref } from "remix/
|
|
237
|
-
import type { Handle } from "remix/
|
|
236
|
+
import { on, ref } from "remix/component";
|
|
237
|
+
import type { Handle } from "remix/component";
|
|
238
238
|
|
|
239
239
|
function RenameButton(handle: Handle) {
|
|
240
240
|
let editing = false;
|
|
@@ -294,7 +294,7 @@ the list is sorted or a random value created during render. Matching keys let Re
|
|
|
294
294
|
and component instances instead of pairing state with the wrong item.
|
|
295
295
|
|
|
296
296
|
```tsx
|
|
297
|
-
import { on, type Handle } from "remix/
|
|
297
|
+
import { on, type Handle } from "remix/component";
|
|
298
298
|
|
|
299
299
|
type ListItem = {
|
|
300
300
|
id: string;
|
|
@@ -398,8 +398,8 @@ component when an application model changes.
|
|
|
398
398
|
`currentTarget` is inferred from that host:
|
|
399
399
|
|
|
400
400
|
```tsx
|
|
401
|
-
import { on } from "remix/
|
|
402
|
-
import type { Handle } from "remix/
|
|
401
|
+
import { on } from "remix/component";
|
|
402
|
+
import type { Handle } from "remix/component";
|
|
403
403
|
|
|
404
404
|
function AlbumForm(_handle: Handle) {
|
|
405
405
|
return () => (
|
|
@@ -436,8 +436,8 @@ value. A controlled input gets its current value from component state instead. I
|
|
|
436
436
|
copies the DOM value into that state and updates the component:
|
|
437
437
|
|
|
438
438
|
```tsx filename=app/ui/public/title-inputs.tsx
|
|
439
|
-
import { on } from "remix/
|
|
440
|
-
import type { Handle } from "remix/
|
|
439
|
+
import { on } from "remix/component";
|
|
440
|
+
import type { Handle } from "remix/component";
|
|
441
441
|
|
|
442
442
|
const initialTitle = "Thriller";
|
|
443
443
|
|
|
@@ -489,17 +489,22 @@ The `mix` prop attaches reusable behavior and styles to host elements. Pass one
|
|
|
489
489
|
array when the element needs several:
|
|
490
490
|
|
|
491
491
|
```tsx
|
|
492
|
-
import
|
|
493
|
-
import {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
492
|
+
import { attrs, css, on, ref } from "remix/component";
|
|
493
|
+
import type { Handle } from "remix/component";
|
|
494
|
+
|
|
495
|
+
const saveButtonStyle = css({
|
|
496
|
+
minWidth: "8rem",
|
|
497
|
+
minHeight: "2.5rem",
|
|
498
|
+
border: 0,
|
|
499
|
+
borderRadius: "999px",
|
|
500
|
+
background: "#111",
|
|
501
|
+
color: "#fff",
|
|
502
|
+
});
|
|
497
503
|
|
|
498
504
|
function SaveButton(_handle: Handle) {
|
|
499
505
|
return () => (
|
|
500
506
|
<button
|
|
501
507
|
mix={[
|
|
502
|
-
button({ tone: "primary" }),
|
|
503
508
|
saveButtonStyle,
|
|
504
509
|
attrs({ type: "button" }),
|
|
505
510
|
ref((node) => console.log("Mounted", node)),
|
|
@@ -530,8 +535,8 @@ recreating static mixins for every state change.
|
|
|
530
535
|
observers and element-owned listeners to that signal:
|
|
531
536
|
|
|
532
537
|
```tsx
|
|
533
|
-
import { ref } from "remix/
|
|
534
|
-
import type { Handle } from "remix/
|
|
538
|
+
import { ref } from "remix/component";
|
|
539
|
+
import type { Handle } from "remix/component";
|
|
535
540
|
|
|
536
541
|
function MeasuredPanel(handle: Handle) {
|
|
537
542
|
let width = 0;
|
|
@@ -565,8 +570,8 @@ native `addEventListener()`. Schedule browser-only setup after the client commit
|
|
|
565
570
|
`handle.signal` so the listener is removed when the component disconnects:
|
|
566
571
|
|
|
567
572
|
```tsx
|
|
568
|
-
import { clientEntry } from "remix/
|
|
569
|
-
import type { Handle } from "remix/
|
|
573
|
+
import { clientEntry } from "remix/component";
|
|
574
|
+
import type { Handle } from "remix/component";
|
|
570
575
|
|
|
571
576
|
export const ViewportWidth = clientEntry(import.meta.url, function ViewportWidth(handle: Handle) {
|
|
572
577
|
let width: number | undefined;
|
|
@@ -610,8 +615,8 @@ the signal. In this example, the server passes `routes.albums.search.href()` as
|
|
|
610
615
|
route returns a short text result. The browser component does not hard-code the endpoint:
|
|
611
616
|
|
|
612
617
|
```tsx filename=app/ui/public/album-search.tsx
|
|
613
|
-
import { clientEntry, on } from "remix/
|
|
614
|
-
import type { Handle } from "remix/
|
|
618
|
+
import { clientEntry, on } from "remix/component";
|
|
619
|
+
import type { Handle } from "remix/component";
|
|
615
620
|
|
|
616
621
|
export const AlbumSearch = clientEntry(
|
|
617
622
|
import.meta.url,
|
|
@@ -674,7 +679,7 @@ operations in ordinary TypeScript that the component calls.
|
|
|
674
679
|
A small model makes that boundary concrete. This cart owns its items and quantity changes:
|
|
675
680
|
|
|
676
681
|
```ts filename=app/ui/public/cart-model.ts
|
|
677
|
-
import { TypedEventTarget } from "remix/
|
|
682
|
+
import { TypedEventTarget } from "remix/component";
|
|
678
683
|
|
|
679
684
|
export interface CartItem {
|
|
680
685
|
id: string;
|
|
@@ -711,8 +716,8 @@ The component creates the model from serializable props, listens for changes, an
|
|
|
711
716
|
open state for its details panel:
|
|
712
717
|
|
|
713
718
|
```tsx filename=app/ui/public/cart.tsx
|
|
714
|
-
import { clientEntry, on } from "remix/
|
|
715
|
-
import type { Handle } from "remix/
|
|
719
|
+
import { clientEntry, on } from "remix/component";
|
|
720
|
+
import type { Handle } from "remix/component";
|
|
716
721
|
|
|
717
722
|
import { CartModel, type CartItem } from "./cart-model.ts";
|
|
718
723
|
|
|
@@ -763,7 +768,7 @@ navigations through the browser's Navigation API. Cross-origin links, downloads,
|
|
|
763
768
|
browser cannot intercept continue as normal document requests.
|
|
764
769
|
|
|
765
770
|
```tsx filename=app/ui/album-link.tsx
|
|
766
|
-
import type { Handle } from "remix/
|
|
771
|
+
import type { Handle } from "remix/component";
|
|
767
772
|
|
|
768
773
|
import { routes } from "../routes.ts";
|
|
769
774
|
|
|
@@ -778,8 +783,8 @@ Use `navigate(href, options)` when navigation begins in code. Apply `link(href,
|
|
|
778
783
|
host that should behave like a link, such as a card whose whole surface is interactive:
|
|
779
784
|
|
|
780
785
|
```tsx filename=app/ui/album-navigation.tsx
|
|
781
|
-
import { link, navigate, on } from "remix/
|
|
782
|
-
import type { Handle } from "remix/
|
|
786
|
+
import { link, navigate, on } from "remix/component";
|
|
787
|
+
import type { Handle } from "remix/component";
|
|
783
788
|
|
|
784
789
|
import { routes } from "../routes.ts";
|
|
785
790
|
|
|
@@ -916,7 +921,7 @@ typed event for consumers.
|
|
|
916
921
|
This `longPress()` mixin turns a held pointer into an `app:longpress` event:
|
|
917
922
|
|
|
918
923
|
```tsx filename=app/ui/public/long-press.tsx
|
|
919
|
-
import { createMixin, on } from "remix/
|
|
924
|
+
import { createMixin, on } from "remix/component";
|
|
920
925
|
|
|
921
926
|
export const longPressType = "app:longpress" as const;
|
|
922
927
|
|
|
@@ -979,7 +984,7 @@ Consume the semantic event with `on(...)` like any other DOM event:
|
|
|
979
984
|
```
|
|
980
985
|
|
|
981
986
|
Namespace custom event names to avoid collisions, and keep one-off behavior in the component that
|
|
982
|
-
uses it. The [`remix/
|
|
987
|
+
uses it. The [`remix/component` API overview](../src/component/README.md) covers the full
|
|
983
988
|
mixin API.
|
|
984
989
|
|
|
985
990
|
The next chapter uses the same server renderer and browser runtime to stream and reload route-owned
|