@remix-run/ui 0.1.0 → 0.1.2
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 +24 -3
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +3 -3
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +82 -0
- package/dist/components/button/button.js +60 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +3 -3
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +1 -6
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +5 -14
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +3 -3
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +3 -3
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +8 -5
- package/dist/runtime/component.js +84 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +12 -0
- package/dist/runtime/core/attributes.js +85 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +169 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +4 -0
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +51 -52
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.js +2 -27
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/navigation.d.ts +6 -0
- package/dist/runtime/navigation.js +16 -3
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +444 -181
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +10 -0
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/to-vnode.js +17 -18
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +5 -4
- package/dist/runtime/vnode.js +2 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.js +80 -106
- package/dist/server/stream.js.map +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +3 -1
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/package.json +6 -7
- package/src/animation/README.md +314 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +8 -10
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +88 -2
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/combobox.tsx +8 -10
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +23 -32
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +35 -41
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +28 -28
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +1 -1
- package/src/runtime/component.ts +94 -82
- package/src/runtime/core/attributes.ts +97 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +185 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +4 -0
- package/src/runtime/frame.ts +84 -61
- package/src/runtime/jsx.ts +2 -30
- package/src/runtime/mixins/mixin.ts +9 -4
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/navigation.ts +24 -3
- package/src/runtime/reconcile.ts +552 -186
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +7 -0
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/to-vnode.ts +15 -17
- package/src/runtime/vnode.ts +6 -9
- package/src/server/README.md +88 -0
- package/src/server/stream.ts +105 -117
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +75 -10
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/test/setup.ts +0 -7
package/src/style/stylesheet.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { REMIX_UI_STYLE_LAYER } from './layers.ts'
|
|
2
|
+
|
|
1
3
|
type RuleEntry = { count: number; index: number }
|
|
2
4
|
type ServerStyleSource = ParentNode | Iterable<Node>
|
|
3
5
|
|
|
4
6
|
const SERVER_STYLE_SELECTOR = 'style[data-rmx]'
|
|
5
|
-
const DEFAULT_STYLE_LAYER = 'rmx'
|
|
6
7
|
|
|
7
|
-
function getStyleLayerName(className: string, layer: string =
|
|
8
|
+
function getStyleLayerName(className: string, layer: string = REMIX_UI_STYLE_LAYER): string {
|
|
8
9
|
return `${layer}.${className}`
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -67,7 +68,7 @@ function getStyleSelector(styleEl: HTMLStyleElement): string | null {
|
|
|
67
68
|
return selector ? selector : null
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export function createStyleManager(layer: string =
|
|
71
|
+
export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER) {
|
|
71
72
|
let stylesheet: CSSStyleSheet | null = null
|
|
72
73
|
let generation = 0
|
|
73
74
|
|
|
@@ -75,6 +76,13 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
75
76
|
// Using an object to track both count and index together
|
|
76
77
|
let ruleMap = new Map<string, RuleEntry>()
|
|
77
78
|
|
|
79
|
+
// Selectors currently held by a server-style adoption ref. We track this
|
|
80
|
+
// separately from `ruleMap` so `replaceServerStyles` can release only the
|
|
81
|
+
// adoption refs of selectors absent from the next page, without disturbing
|
|
82
|
+
// selectors that exist solely because a client-side css mixin inserted them
|
|
83
|
+
// (e.g. transient UI state that the server never rendered).
|
|
84
|
+
let adoptedSelectors = new Set<string>()
|
|
85
|
+
|
|
78
86
|
function getStylesheet(): CSSStyleSheet {
|
|
79
87
|
if (!stylesheet) {
|
|
80
88
|
stylesheet = new CSSStyleSheet()
|
|
@@ -98,19 +106,27 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
function adoptServerStyleTag(styleEl: HTMLStyleElement) {
|
|
109
|
+
function adoptServerStyleTag(styleEl: HTMLStyleElement): string | undefined {
|
|
102
110
|
let selector = getStyleSelector(styleEl)
|
|
103
|
-
if (!selector) return
|
|
111
|
+
if (!selector) return undefined
|
|
104
112
|
|
|
105
113
|
if (ruleMap.has(selector)) {
|
|
114
|
+
// Already tracked. Take an adoption ref if we don't already have one —
|
|
115
|
+
// the rule may have been inserted by a client-side css mixin first and
|
|
116
|
+
// the SSR'd style tag arrived afterwards (e.g. a streamed fragment).
|
|
117
|
+
if (!adoptedSelectors.has(selector)) {
|
|
118
|
+
let entry = ruleMap.get(selector)!
|
|
119
|
+
entry.count++
|
|
120
|
+
adoptedSelectors.add(selector)
|
|
121
|
+
}
|
|
106
122
|
styleEl.remove()
|
|
107
|
-
return
|
|
123
|
+
return selector
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
let cssText = styleEl.textContent?.trim() ?? ''
|
|
111
127
|
if (cssText.length === 0) {
|
|
112
128
|
styleEl.remove()
|
|
113
|
-
return
|
|
129
|
+
return undefined
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
try {
|
|
@@ -118,9 +134,12 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
118
134
|
let index = sheet.cssRules.length
|
|
119
135
|
sheet.insertRule(cssText, index)
|
|
120
136
|
ruleMap.set(selector, { count: 1, index })
|
|
137
|
+
adoptedSelectors.add(selector)
|
|
121
138
|
styleEl.remove()
|
|
139
|
+
return selector
|
|
122
140
|
} catch {
|
|
123
141
|
// If adoption fails, keep the <style> tag in the DOM so styles still apply.
|
|
142
|
+
return undefined
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
|
|
@@ -169,6 +188,7 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
169
188
|
|
|
170
189
|
// Remove from tracking
|
|
171
190
|
ruleMap.delete(className)
|
|
191
|
+
adoptedSelectors.delete(className)
|
|
172
192
|
|
|
173
193
|
// TODO: just search and remove, stop re-indexing
|
|
174
194
|
if (!stylesheet) return
|
|
@@ -186,24 +206,69 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
186
206
|
function reset() {
|
|
187
207
|
clearStylesheet()
|
|
188
208
|
ruleMap.clear()
|
|
209
|
+
adoptedSelectors.clear()
|
|
189
210
|
removeStylesheet()
|
|
190
211
|
generation++
|
|
191
212
|
}
|
|
192
213
|
|
|
193
|
-
function adoptServerStyles(source: ServerStyleSource) {
|
|
214
|
+
function adoptServerStyles(source: ServerStyleSource): Set<string> {
|
|
194
215
|
let styles = collectServerStyleTags(source)
|
|
216
|
+
let adopted = new Set<string>()
|
|
195
217
|
|
|
196
218
|
for (let styleEl of styles) {
|
|
197
|
-
adoptServerStyleTag(styleEl)
|
|
219
|
+
let selector = adoptServerStyleTag(styleEl)
|
|
220
|
+
if (selector) adopted.add(selector)
|
|
198
221
|
}
|
|
222
|
+
|
|
223
|
+
return adopted
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Snapshot the currently-adopted server selectors, adopt the incoming
|
|
227
|
+
// server styles additively, then release the adoption ref of any prior-only
|
|
228
|
+
// selectors.
|
|
229
|
+
//
|
|
230
|
+
// Refcount-aware semantics keep preserved DOM styled: a prior-only selector
|
|
231
|
+
// with no live css-mixin ref drops immediately, but one still referenced by
|
|
232
|
+
// an active mixin (e.g. inside a hydration region that the DOM diff skipped)
|
|
233
|
+
// stays in the stylesheet and is only fully removed when that mixin's
|
|
234
|
+
// `remove` event eventually fires. This avoids the FOUC that a hard
|
|
235
|
+
// `reset()` would cause between the style swap and the hydration re-render.
|
|
236
|
+
//
|
|
237
|
+
// Only adoption refs are considered — selectors that exist solely because a
|
|
238
|
+
// client-side css mixin inserted them (e.g. transient UI state never
|
|
239
|
+
// rendered by the server) are unaffected.
|
|
240
|
+
function replaceServerStyles(source: ServerStyleSource): void {
|
|
241
|
+
let prior = new Set(adoptedSelectors)
|
|
242
|
+
let adopted = adoptServerStyles(source)
|
|
243
|
+
for (let selector of prior) {
|
|
244
|
+
if (!adopted.has(selector)) {
|
|
245
|
+
adoptedSelectors.delete(selector)
|
|
246
|
+
remove(selector)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function selectors(): IterableIterator<string> {
|
|
252
|
+
return ruleMap.keys()
|
|
199
253
|
}
|
|
200
254
|
|
|
201
255
|
function dispose() {
|
|
202
256
|
removeStylesheet()
|
|
203
257
|
// Clear internal state
|
|
204
258
|
ruleMap.clear()
|
|
259
|
+
adoptedSelectors.clear()
|
|
205
260
|
generation++
|
|
206
261
|
}
|
|
207
262
|
|
|
208
|
-
return {
|
|
263
|
+
return {
|
|
264
|
+
insert,
|
|
265
|
+
remove,
|
|
266
|
+
has,
|
|
267
|
+
getGeneration,
|
|
268
|
+
reset,
|
|
269
|
+
adoptServerStyles,
|
|
270
|
+
replaceServerStyles,
|
|
271
|
+
selectors,
|
|
272
|
+
dispose,
|
|
273
|
+
}
|
|
209
274
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Test
|
|
2
|
+
|
|
3
|
+
When writing tests, use `root.flush()` to synchronously execute all pending updates and tasks. This ensures the DOM and component state are fully synchronized before making assertions.
|
|
4
|
+
|
|
5
|
+
## Basic Testing Pattern
|
|
6
|
+
|
|
7
|
+
The main use case is flushing after events that call `handle.update()`. Since updates are asynchronous, you need to flush to ensure the DOM reflects the changes:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
function Counter(handle: Handle) {
|
|
11
|
+
let count = 0
|
|
12
|
+
|
|
13
|
+
return () => (
|
|
14
|
+
<button
|
|
15
|
+
mix={[
|
|
16
|
+
on('click', () => {
|
|
17
|
+
count++
|
|
18
|
+
handle.update()
|
|
19
|
+
}),
|
|
20
|
+
]}
|
|
21
|
+
>
|
|
22
|
+
Count: {count}
|
|
23
|
+
</button>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// In your test
|
|
28
|
+
let container = document.createElement('div')
|
|
29
|
+
let root = createRoot(container)
|
|
30
|
+
|
|
31
|
+
root.render(<Counter />)
|
|
32
|
+
root.flush() // Ensure initial render completes
|
|
33
|
+
|
|
34
|
+
let button = container.querySelector('button')
|
|
35
|
+
button.click() // Triggers handle.update()
|
|
36
|
+
root.flush() // Flush to apply the update
|
|
37
|
+
|
|
38
|
+
expect(container.textContent).toBe('Count: 1')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Why Flush After Initial Render?
|
|
42
|
+
|
|
43
|
+
You should also flush after the initial `root.render()` to ensure event listeners are attached and the DOM is ready for interaction:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
let root = createRoot(container)
|
|
47
|
+
root.render(<MyComponent />)
|
|
48
|
+
root.flush() // Event listeners now attached
|
|
49
|
+
|
|
50
|
+
// Safe to interact
|
|
51
|
+
container.querySelector('button').click()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Testing Async Operations
|
|
55
|
+
|
|
56
|
+
For components with async operations in `queueTask`, flush after each step:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
function AsyncLoader(handle: Handle) {
|
|
60
|
+
let data: string | null = null
|
|
61
|
+
|
|
62
|
+
handle.queueTask(async (signal) => {
|
|
63
|
+
let response = await fetch('/api/data', { signal })
|
|
64
|
+
let json = await response.json()
|
|
65
|
+
if (signal.aborted) return
|
|
66
|
+
data = json.value
|
|
67
|
+
handle.update()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return () => <div>{data ?? 'Loading...'}</div>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// In your test (with mocked fetch)
|
|
74
|
+
let root = createRoot(container)
|
|
75
|
+
root.render(<AsyncLoader />)
|
|
76
|
+
root.flush()
|
|
77
|
+
|
|
78
|
+
expect(container.textContent).toBe('Loading...')
|
|
79
|
+
|
|
80
|
+
// After fetch resolves
|
|
81
|
+
await waitForFetch()
|
|
82
|
+
root.flush()
|
|
83
|
+
|
|
84
|
+
expect(container.textContent).toBe('Expected data')
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Testing Component Removal
|
|
88
|
+
|
|
89
|
+
Use `root.dispose()` to clean up and verify cleanup behavior:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
let root = createRoot(container)
|
|
93
|
+
root.render(<MyComponent />)
|
|
94
|
+
root.flush()
|
|
95
|
+
|
|
96
|
+
// Verify setup behavior
|
|
97
|
+
expect(container.querySelector('.content')).toBeTruthy()
|
|
98
|
+
|
|
99
|
+
// Remove and verify cleanup
|
|
100
|
+
root.dispose()
|
|
101
|
+
expect(container.innerHTML).toBe('')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## See Also
|
|
105
|
+
|
|
106
|
+
- [Getting Started](https://github.com/remix-run/remix/blob/main/packages/ui/docs/getting-started.md) - Root methods reference
|
|
107
|
+
- [Handle API](https://github.com/remix-run/remix/blob/main/packages/ui/docs/handle.md) - `handle.queueTask()` behavior
|
package/src/test/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripFlushMarkers } from '../runtime/stream-protocol.ts'
|
|
2
|
+
|
|
1
3
|
export type Assert<T extends true> = T
|
|
2
4
|
|
|
3
5
|
export type Equal<X, Y> =
|
|
@@ -14,7 +16,7 @@ export async function drain(stream: ReadableStream<Uint8Array>): Promise<string>
|
|
|
14
16
|
html += decoder.decode(value)
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
return html
|
|
19
|
+
return stripFlushMarkers(html)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function readChunks(stream: ReadableStream<Uint8Array>): AsyncGenerator<string, void, void> {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# theme
|
|
2
|
+
|
|
3
|
+
`theme` provides Remix UI design tokens, theme creation, glyph contracts, and the built-in `RMX_01` preset. Use it to install CSS custom properties once and consume typed token references in component styles.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { createTheme, theme } from 'remix/ui/theme'
|
|
9
|
+
import { css } from 'remix/ui'
|
|
10
|
+
|
|
11
|
+
let Theme = createTheme({
|
|
12
|
+
space: {
|
|
13
|
+
none: '0px',
|
|
14
|
+
px: '1px',
|
|
15
|
+
xs: '2px',
|
|
16
|
+
sm: '4px',
|
|
17
|
+
md: '8px',
|
|
18
|
+
lg: '12px',
|
|
19
|
+
xl: '16px',
|
|
20
|
+
xxl: '24px',
|
|
21
|
+
},
|
|
22
|
+
radius: { none: '0px', sm: '4px', md: '8px', lg: '12px', xl: '16px', full: '9999px' },
|
|
23
|
+
fontFamily: { sans: 'Inter, sans-serif', mono: 'monospace' },
|
|
24
|
+
fontSize: {
|
|
25
|
+
xxxs: '10px',
|
|
26
|
+
xxs: '11px',
|
|
27
|
+
xs: '12px',
|
|
28
|
+
sm: '14px',
|
|
29
|
+
md: '16px',
|
|
30
|
+
lg: '18px',
|
|
31
|
+
xl: '20px',
|
|
32
|
+
xxl: '28px',
|
|
33
|
+
},
|
|
34
|
+
lineHeight: { tight: '1.2', normal: '1.5', relaxed: '1.7' },
|
|
35
|
+
letterSpacing: { tight: '-0.02em', normal: '0', meta: '0.06em', wide: '0.08em' },
|
|
36
|
+
fontWeight: { normal: '400', medium: '500', semibold: '600', bold: '700' },
|
|
37
|
+
control: { height: { sm: '28px', md: '32px', lg: '36px' } },
|
|
38
|
+
surface: { lvl0: '#fff', lvl1: '#f8fafc', lvl2: '#f5f5f5', lvl3: '#f1f5f9', lvl4: '#e9eef6' },
|
|
39
|
+
shadow: { xs: 'none', sm: 'none', md: 'none', lg: 'none', xl: 'none' },
|
|
40
|
+
colors: {
|
|
41
|
+
text: { primary: '#111827', secondary: '#374151', muted: '#6b7280', link: '#2563eb' },
|
|
42
|
+
border: { subtle: '#e5e7eb', default: '#d1d5db', strong: '#9ca3af' },
|
|
43
|
+
focus: { ring: '#3b82f6' },
|
|
44
|
+
overlay: { scrim: 'rgb(0 0 0 / 0.45)' },
|
|
45
|
+
action: {
|
|
46
|
+
primary: {
|
|
47
|
+
background: '#2563eb',
|
|
48
|
+
backgroundHover: '#1d4ed8',
|
|
49
|
+
backgroundActive: '#1e40af',
|
|
50
|
+
foreground: '#fff',
|
|
51
|
+
border: '#2563eb',
|
|
52
|
+
},
|
|
53
|
+
secondary: {
|
|
54
|
+
background: '#fff',
|
|
55
|
+
backgroundHover: '#f8fafc',
|
|
56
|
+
backgroundActive: '#f1f5f9',
|
|
57
|
+
foreground: '#111827',
|
|
58
|
+
border: '#d1d5db',
|
|
59
|
+
},
|
|
60
|
+
danger: {
|
|
61
|
+
background: '#dc2626',
|
|
62
|
+
backgroundHover: '#b91c1c',
|
|
63
|
+
backgroundActive: '#991b1b',
|
|
64
|
+
foreground: '#fff',
|
|
65
|
+
border: '#dc2626',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
let card = css({
|
|
72
|
+
backgroundColor: theme.surface.lvl0,
|
|
73
|
+
color: theme.colors.text.primary,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
function Layout() {
|
|
77
|
+
return (
|
|
78
|
+
<body>
|
|
79
|
+
<Theme />
|
|
80
|
+
<article mix={card}>Project status</article>
|
|
81
|
+
</body>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## `theme.*`
|
|
87
|
+
|
|
88
|
+
- `theme`: typed CSS variable reference contract, such as `theme.space.md` and `theme.colors.text.primary`.
|
|
89
|
+
- `createTheme(values, options?)`: creates a style component with `cssText`, `selector`, `values`, `vars`, and `Style`.
|
|
90
|
+
- `RMX_01`: built-in theme component.
|
|
91
|
+
- `RMX_01_GLYPHS`: built-in glyph sheet component.
|
|
92
|
+
- `glyphContract` and `glyphNames`: stable glyph ids and supported glyph names.
|
|
93
|
+
- `ThemeValues`, `ThemeVars`, `ThemeComponent`, `ThemeStyleProps`, `CreateThemeOptions`, and `ThemeMix`: public TypeScript types for custom themes.
|
|
94
|
+
- `GlyphName`, `GlyphSymbol`, and `GlyphValues`: public TypeScript types for glyph contracts.
|
|
95
|
+
|
|
96
|
+
## Behavior Notes
|
|
97
|
+
|
|
98
|
+
- `theme` values are CSS variable references, not raw token values.
|
|
99
|
+
- `createTheme` serializes token values into CSS custom properties and renders a `<style data-rmx-theme>` tag.
|
|
100
|
+
- The default selector is `:root`; pass `selector` for scoped themes.
|
|
101
|
+
- The base reset is included by default, emitted in `rmx-reset`, and can be disabled with `reset: false`.
|
|
102
|
+
- The built-in components consume this token contract through their style mixins.
|
|
103
|
+
- Render `<RMX_01 />` and `<RMX_01_GLYPHS />` once when using the built-in theme and glyph preset.
|
package/src/theme/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createElement, type Handle } from '@remix-run/ui'
|
|
2
2
|
|
|
3
|
+
import { REMIX_UI_RESET_LAYER, REMIX_UI_STYLE_LAYER } from '../style/layers.ts'
|
|
3
4
|
import {
|
|
4
5
|
theme,
|
|
5
6
|
themeVariableNames,
|
|
@@ -76,7 +77,9 @@ function serializeThemeCss(selector: string, vars: ThemeVars, options: { reset:
|
|
|
76
77
|
let blocks = [`${selector} {\n${lines}\n}`]
|
|
77
78
|
|
|
78
79
|
if (options.reset) {
|
|
79
|
-
|
|
80
|
+
let resetCss = serializeThemeResetCss(selector)
|
|
81
|
+
blocks.push(`@layer ${REMIX_UI_RESET_LAYER}, ${REMIX_UI_STYLE_LAYER};`)
|
|
82
|
+
blocks.push(`@layer ${REMIX_UI_RESET_LAYER} { ${resetCss} }`)
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
return blocks.join('\n\n')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# scroll-lock
|
|
2
|
+
|
|
3
|
+
`scroll-lock` locks document scrolling while a floating or modal surface is open. Use `lockScroll` directly for imperative flows or `lockScrollOnToggle` for popover-style elements that emit `beforetoggle`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { lockScroll, lockScrollOnToggle } from 'remix/ui/scroll-lock'
|
|
9
|
+
|
|
10
|
+
function DialogSurface() {
|
|
11
|
+
return <div popover="auto" mix={lockScrollOnToggle()} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function openModal() {
|
|
15
|
+
let unlock = lockScroll()
|
|
16
|
+
|
|
17
|
+
// Later, when the modal closes:
|
|
18
|
+
unlock()
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
- `lockScroll(document?)`: locks the target document and returns an idempotent unlock function.
|
|
25
|
+
- `lockScrollOnToggle()`: mixin that locks on `beforetoggle` open, unlocks on close, and releases the lock when the host unmounts.
|
|
26
|
+
|
|
27
|
+
## Behavior Notes
|
|
28
|
+
|
|
29
|
+
- The lock stores and restores the document element's inline `overflow` and `scrollbarGutter`.
|
|
30
|
+
- Scroll position is restored when the last active lock is released.
|
|
31
|
+
- Multiple locks are reference-counted, so the document stays locked until every unlock function has run.
|
|
32
|
+
- When a scrollbar is present and computed `scrollbar-gutter` is `auto`, the document reserves a stable gutter while locked.
|
|
33
|
+
- `lockScrollOnToggle` uses the host element's owner document.
|