@remix-run/ui 0.1.1 → 0.2.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 +33 -10
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/animation/spring.d.ts +6 -35
- package/dist/animation/spring.js +9 -8
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +7 -7
- 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.d.ts +1 -1
- 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 +86 -4
- package/dist/components/button/button.js +63 -3
- 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 +11 -11
- 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 +4 -9
- 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 +15 -24
- 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/popover/popover.d.ts +5 -5
- package/dist/components/popover/popover.js.map +1 -1
- 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 +11 -11
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +6 -6
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interactions/keydown/keydown.d.ts +1 -1
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
- package/dist/runtime/component.d.ts +18 -12
- package/dist/runtime/component.js +93 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +14 -0
- package/dist/runtime/core/attributes.js +103 -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 +170 -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 +20 -9
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +41 -47
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/jsx.js +2 -28
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/link-mixin.d.ts +5 -1
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +2 -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/mixins/ref-mixin.d.ts +2 -1
- package/dist/runtime/mixins/ref-mixin.js.map +1 -1
- package/dist/runtime/navigation.js +9 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +1 -1
- package/dist/runtime/reconcile.js +452 -197
- 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.d.ts +10 -10
- package/dist/runtime/scheduler.js +1 -1
- 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/svg-attributes.js +1 -0
- package/dist/runtime/svg-attributes.js.map +1 -1
- package/dist/runtime/to-vnode.js +7 -11
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +11 -5
- package/dist/runtime/vnode.js +6 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.d.ts +2 -0
- package/dist/server/stream.js +154 -114
- package/dist/server/stream.js.map +1 -1
- package/dist/style/css-mixin.d.ts +2 -1
- package/dist/style/css-mixin.js.map +1 -1
- package/dist/style/index.d.ts +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 +13 -11
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/contract.d.ts +24 -222
- package/dist/theme/contract.js +2 -2
- package/dist/theme/contract.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -3
- package/dist/theme/glyph-contract.js.map +1 -1
- package/dist/theme/presets/rmx-01/index.d.ts +4 -2
- package/dist/theme/presets/rmx-01/index.js.map +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/dist/utils/scroll-lock.d.ts +3 -3
- package/dist/utils/scroll-lock.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +316 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/animation/spring.ts +21 -9
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +14 -14
- 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/anchor/anchor.ts +1 -1
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
- 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 +118 -20
- 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/README.md +1 -1
- package/src/components/combobox/combobox.tsx +37 -21
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/glyph/glyph.tsx +2 -2
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +106 -110
- 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 +61 -55
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/popover/popover.ts +90 -79
- 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 +53 -36
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +2 -1
- package/src/runtime/component.ts +122 -88
- package/src/runtime/core/attributes.ts +118 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +187 -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 +20 -9
- package/src/runtime/frame.ts +71 -55
- package/src/runtime/jsx.ts +3 -30
- package/src/runtime/mixins/link-mixin.ts +88 -86
- package/src/runtime/mixins/mixin.ts +18 -5
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/mixins/ref-mixin.ts +4 -5
- package/src/runtime/navigation.ts +10 -1
- package/src/runtime/reconcile.ts +563 -223
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +20 -20
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/svg-attributes.ts +1 -0
- package/src/runtime/to-vnode.ts +7 -10
- package/src/runtime/vnode.ts +16 -9
- package/src/server/README.md +90 -0
- package/src/server/stream.ts +176 -124
- package/src/style/css-mixin.ts +6 -1
- package/src/style/index.ts +1 -2
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +88 -11
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/contract.ts +37 -5
- package/src/theme/glyph-contract.ts +3 -1
- package/src/theme/presets/rmx-01/index.ts +4 -2
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/utils/scroll-lock.ts +7 -3
package/src/style/stylesheet.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { REMIX_UI_STYLE_LAYER } from './layers.ts'
|
|
2
|
+
|
|
1
3
|
type RuleEntry = { count: number; index: number }
|
|
2
|
-
type ServerStyleSource = ParentNode | Iterable<Node>
|
|
4
|
+
export type ServerStyleSource = ParentNode | Iterable<Node>
|
|
5
|
+
|
|
6
|
+
export interface StyleManager {
|
|
7
|
+
insert(className: string, rule: string): void
|
|
8
|
+
remove(className: string): void
|
|
9
|
+
has(className: string): boolean
|
|
10
|
+
getGeneration(): number
|
|
11
|
+
reset(): void
|
|
12
|
+
adoptServerStyles(source: ServerStyleSource): Set<string>
|
|
13
|
+
replaceServerStyles(source: ServerStyleSource): void
|
|
14
|
+
selectors(): IterableIterator<string>
|
|
15
|
+
dispose(): void
|
|
16
|
+
}
|
|
3
17
|
|
|
4
18
|
const SERVER_STYLE_SELECTOR = 'style[data-rmx]'
|
|
5
|
-
const DEFAULT_STYLE_LAYER = 'rmx'
|
|
6
19
|
|
|
7
|
-
function getStyleLayerName(className: string, layer: string =
|
|
20
|
+
function getStyleLayerName(className: string, layer: string = REMIX_UI_STYLE_LAYER): string {
|
|
8
21
|
return `${layer}.${className}`
|
|
9
22
|
}
|
|
10
23
|
|
|
@@ -67,7 +80,7 @@ function getStyleSelector(styleEl: HTMLStyleElement): string | null {
|
|
|
67
80
|
return selector ? selector : null
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
export function createStyleManager(layer: string =
|
|
83
|
+
export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleManager {
|
|
71
84
|
let stylesheet: CSSStyleSheet | null = null
|
|
72
85
|
let generation = 0
|
|
73
86
|
|
|
@@ -75,6 +88,13 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
75
88
|
// Using an object to track both count and index together
|
|
76
89
|
let ruleMap = new Map<string, RuleEntry>()
|
|
77
90
|
|
|
91
|
+
// Selectors currently held by a server-style adoption ref. We track this
|
|
92
|
+
// separately from `ruleMap` so `replaceServerStyles` can release only the
|
|
93
|
+
// adoption refs of selectors absent from the next page, without disturbing
|
|
94
|
+
// selectors that exist solely because a client-side css mixin inserted them
|
|
95
|
+
// (e.g. transient UI state that the server never rendered).
|
|
96
|
+
let adoptedSelectors = new Set<string>()
|
|
97
|
+
|
|
78
98
|
function getStylesheet(): CSSStyleSheet {
|
|
79
99
|
if (!stylesheet) {
|
|
80
100
|
stylesheet = new CSSStyleSheet()
|
|
@@ -98,19 +118,27 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
function adoptServerStyleTag(styleEl: HTMLStyleElement) {
|
|
121
|
+
function adoptServerStyleTag(styleEl: HTMLStyleElement): string | undefined {
|
|
102
122
|
let selector = getStyleSelector(styleEl)
|
|
103
|
-
if (!selector) return
|
|
123
|
+
if (!selector) return undefined
|
|
104
124
|
|
|
105
125
|
if (ruleMap.has(selector)) {
|
|
126
|
+
// Already tracked. Take an adoption ref if we don't already have one —
|
|
127
|
+
// the rule may have been inserted by a client-side css mixin first and
|
|
128
|
+
// the SSR'd style tag arrived afterwards (e.g. a streamed fragment).
|
|
129
|
+
if (!adoptedSelectors.has(selector)) {
|
|
130
|
+
let entry = ruleMap.get(selector)!
|
|
131
|
+
entry.count++
|
|
132
|
+
adoptedSelectors.add(selector)
|
|
133
|
+
}
|
|
106
134
|
styleEl.remove()
|
|
107
|
-
return
|
|
135
|
+
return selector
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
let cssText = styleEl.textContent?.trim() ?? ''
|
|
111
139
|
if (cssText.length === 0) {
|
|
112
140
|
styleEl.remove()
|
|
113
|
-
return
|
|
141
|
+
return undefined
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
try {
|
|
@@ -118,9 +146,12 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
118
146
|
let index = sheet.cssRules.length
|
|
119
147
|
sheet.insertRule(cssText, index)
|
|
120
148
|
ruleMap.set(selector, { count: 1, index })
|
|
149
|
+
adoptedSelectors.add(selector)
|
|
121
150
|
styleEl.remove()
|
|
151
|
+
return selector
|
|
122
152
|
} catch {
|
|
123
153
|
// If adoption fails, keep the <style> tag in the DOM so styles still apply.
|
|
154
|
+
return undefined
|
|
124
155
|
}
|
|
125
156
|
}
|
|
126
157
|
|
|
@@ -169,6 +200,7 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
169
200
|
|
|
170
201
|
// Remove from tracking
|
|
171
202
|
ruleMap.delete(className)
|
|
203
|
+
adoptedSelectors.delete(className)
|
|
172
204
|
|
|
173
205
|
// TODO: just search and remove, stop re-indexing
|
|
174
206
|
if (!stylesheet) return
|
|
@@ -186,24 +218,69 @@ export function createStyleManager(layer: string = 'rmx') {
|
|
|
186
218
|
function reset() {
|
|
187
219
|
clearStylesheet()
|
|
188
220
|
ruleMap.clear()
|
|
221
|
+
adoptedSelectors.clear()
|
|
189
222
|
removeStylesheet()
|
|
190
223
|
generation++
|
|
191
224
|
}
|
|
192
225
|
|
|
193
|
-
function adoptServerStyles(source: ServerStyleSource) {
|
|
226
|
+
function adoptServerStyles(source: ServerStyleSource): Set<string> {
|
|
194
227
|
let styles = collectServerStyleTags(source)
|
|
228
|
+
let adopted = new Set<string>()
|
|
195
229
|
|
|
196
230
|
for (let styleEl of styles) {
|
|
197
|
-
adoptServerStyleTag(styleEl)
|
|
231
|
+
let selector = adoptServerStyleTag(styleEl)
|
|
232
|
+
if (selector) adopted.add(selector)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return adopted
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Snapshot the currently-adopted server selectors, adopt the incoming
|
|
239
|
+
// server styles additively, then release the adoption ref of any prior-only
|
|
240
|
+
// selectors.
|
|
241
|
+
//
|
|
242
|
+
// Refcount-aware semantics keep preserved DOM styled: a prior-only selector
|
|
243
|
+
// with no live css-mixin ref drops immediately, but one still referenced by
|
|
244
|
+
// an active mixin (e.g. inside a hydration region that the DOM diff skipped)
|
|
245
|
+
// stays in the stylesheet and is only fully removed when that mixin's
|
|
246
|
+
// `remove` event eventually fires. This avoids the FOUC that a hard
|
|
247
|
+
// `reset()` would cause between the style swap and the hydration re-render.
|
|
248
|
+
//
|
|
249
|
+
// Only adoption refs are considered — selectors that exist solely because a
|
|
250
|
+
// client-side css mixin inserted them (e.g. transient UI state never
|
|
251
|
+
// rendered by the server) are unaffected.
|
|
252
|
+
function replaceServerStyles(source: ServerStyleSource): void {
|
|
253
|
+
let prior = new Set(adoptedSelectors)
|
|
254
|
+
let adopted = adoptServerStyles(source)
|
|
255
|
+
for (let selector of prior) {
|
|
256
|
+
if (!adopted.has(selector)) {
|
|
257
|
+
adoptedSelectors.delete(selector)
|
|
258
|
+
remove(selector)
|
|
259
|
+
}
|
|
198
260
|
}
|
|
199
261
|
}
|
|
200
262
|
|
|
263
|
+
function selectors(): IterableIterator<string> {
|
|
264
|
+
return ruleMap.keys()
|
|
265
|
+
}
|
|
266
|
+
|
|
201
267
|
function dispose() {
|
|
202
268
|
removeStylesheet()
|
|
203
269
|
// Clear internal state
|
|
204
270
|
ruleMap.clear()
|
|
271
|
+
adoptedSelectors.clear()
|
|
205
272
|
generation++
|
|
206
273
|
}
|
|
207
274
|
|
|
208
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
insert,
|
|
277
|
+
remove,
|
|
278
|
+
has,
|
|
279
|
+
getGeneration,
|
|
280
|
+
reset,
|
|
281
|
+
adoptServerStyles,
|
|
282
|
+
replaceServerStyles,
|
|
283
|
+
selectors,
|
|
284
|
+
dispose,
|
|
285
|
+
}
|
|
209
286
|
}
|
|
@@ -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/contract.ts
CHANGED
|
@@ -4,7 +4,38 @@ export interface ThemeVariableTree {
|
|
|
4
4
|
[key: string]: string | ThemeVariableTree
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type ThemeVariableGroup<key extends string> = Readonly<Record<key, string>>
|
|
8
|
+
type ThemeActionVariableGroup = ThemeVariableGroup<
|
|
9
|
+
'background' | 'backgroundHover' | 'backgroundActive' | 'foreground' | 'border'
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
type ThemeVariableNames = {
|
|
13
|
+
readonly space: ThemeVariableGroup<'none' | 'px' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>
|
|
14
|
+
readonly radius: ThemeVariableGroup<'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>
|
|
15
|
+
readonly fontFamily: ThemeVariableGroup<'sans' | 'mono'>
|
|
16
|
+
readonly fontSize: ThemeVariableGroup<'xxxs' | 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>
|
|
17
|
+
readonly lineHeight: ThemeVariableGroup<'tight' | 'normal' | 'relaxed'>
|
|
18
|
+
readonly letterSpacing: ThemeVariableGroup<'tight' | 'normal' | 'meta' | 'wide'>
|
|
19
|
+
readonly fontWeight: ThemeVariableGroup<'normal' | 'medium' | 'semibold' | 'bold'>
|
|
20
|
+
readonly control: {
|
|
21
|
+
readonly height: ThemeVariableGroup<'sm' | 'md' | 'lg'>
|
|
22
|
+
}
|
|
23
|
+
readonly surface: ThemeVariableGroup<'lvl0' | 'lvl1' | 'lvl2' | 'lvl3' | 'lvl4'>
|
|
24
|
+
readonly shadow: ThemeVariableGroup<'xs' | 'sm' | 'md' | 'lg' | 'xl'>
|
|
25
|
+
readonly colors: {
|
|
26
|
+
readonly text: ThemeVariableGroup<'primary' | 'secondary' | 'muted' | 'link'>
|
|
27
|
+
readonly border: ThemeVariableGroup<'subtle' | 'default' | 'strong'>
|
|
28
|
+
readonly focus: ThemeVariableGroup<'ring'>
|
|
29
|
+
readonly overlay: ThemeVariableGroup<'scrim'>
|
|
30
|
+
readonly action: {
|
|
31
|
+
readonly primary: ThemeActionVariableGroup
|
|
32
|
+
readonly secondary: ThemeActionVariableGroup
|
|
33
|
+
readonly danger: ThemeActionVariableGroup
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const themeVariableNamesSource: ThemeVariableNames = {
|
|
8
39
|
space: {
|
|
9
40
|
none: '--rmx-space-none',
|
|
10
41
|
px: '--rmx-space-px',
|
|
@@ -117,7 +148,9 @@ const themeVariableNames = {
|
|
|
117
148
|
},
|
|
118
149
|
},
|
|
119
150
|
},
|
|
120
|
-
}
|
|
151
|
+
} satisfies ThemeVariableTree
|
|
152
|
+
|
|
153
|
+
export const themeVariableNames: ThemeVariableNames = themeVariableNamesSource
|
|
121
154
|
|
|
122
155
|
type MapLeaves<source, leaf> = source extends string
|
|
123
156
|
? leaf
|
|
@@ -126,6 +159,7 @@ type MapLeaves<source, leaf> = source extends string
|
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
export type ThemeValue = string | number
|
|
162
|
+
export type ThemeContract = MapLeaves<ThemeVariableNames, string>
|
|
129
163
|
export type ThemeValues = MapLeaves<typeof themeVariableNames, ThemeValue>
|
|
130
164
|
export type ThemeVars = Readonly<Record<string, string>>
|
|
131
165
|
export type CreateThemeOptions = {
|
|
@@ -146,7 +180,7 @@ export type ThemeComponent = ThemeRenderer & {
|
|
|
146
180
|
vars: ThemeVars
|
|
147
181
|
}
|
|
148
182
|
|
|
149
|
-
export const theme = createThemeContract(themeVariableNames)
|
|
183
|
+
export const theme: ThemeContract = createThemeContract(themeVariableNames)
|
|
150
184
|
|
|
151
185
|
export type ThemeUtility = ReturnType<typeof css>
|
|
152
186
|
|
|
@@ -158,8 +192,6 @@ type NestedThemeMix<value, depth extends number = 4> = depth extends 0
|
|
|
158
192
|
|
|
159
193
|
export type ThemeMix = NestedThemeMix<ThemeMixLeaf>
|
|
160
194
|
|
|
161
|
-
export { themeVariableNames }
|
|
162
|
-
|
|
163
195
|
function createThemeContract<tree extends ThemeVariableTree>(tree: tree): MapLeaves<tree, string> {
|
|
164
196
|
return mapTreeLeaves(tree, (variableName) => `var(${variableName})`) as MapLeaves<tree, string>
|
|
165
197
|
}
|
|
@@ -32,7 +32,9 @@ export type GlyphContract = Readonly<Record<GlyphName, { id: string }>>
|
|
|
32
32
|
|
|
33
33
|
const DEFAULT_GLYPH_ID_PREFIX = 'rmx-glyph'
|
|
34
34
|
|
|
35
|
-
export const glyphContract = Object.freeze(
|
|
35
|
+
export const glyphContract: GlyphContract = Object.freeze(
|
|
36
|
+
createGlyphContract(DEFAULT_GLYPH_ID_PREFIX),
|
|
37
|
+
)
|
|
36
38
|
|
|
37
39
|
function createGlyphIds(idPrefix: string): Record<GlyphName, string> {
|
|
38
40
|
return Object.fromEntries(glyphNames.map((name) => [name, `${idPrefix}-${name}`])) as Record<
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createGlyphSheet } from '../../../components/glyph/glyph.tsx'
|
|
2
2
|
import { createTheme } from '../../runtime.ts'
|
|
3
|
+
import type { GlyphSheetComponent } from '../../../components/glyph/glyph.tsx'
|
|
4
|
+
import type { ThemeComponent } from '../../contract.ts'
|
|
3
5
|
|
|
4
6
|
import { glyphValues } from './glyphs.tsx'
|
|
5
7
|
|
|
6
|
-
export const RMX_01 = createTheme({
|
|
8
|
+
export const RMX_01: ThemeComponent = createTheme({
|
|
7
9
|
space: {
|
|
8
10
|
none: '0px',
|
|
9
11
|
px: '1px',
|
|
@@ -118,4 +120,4 @@ export const RMX_01 = createTheme({
|
|
|
118
120
|
},
|
|
119
121
|
})
|
|
120
122
|
|
|
121
|
-
export const RMX_01_GLYPHS = createGlyphSheet(glyphValues)
|
|
123
|
+
export const RMX_01_GLYPHS: GlyphSheetComponent = createGlyphSheet(glyphValues)
|
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.
|
package/src/utils/scroll-lock.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMixin, on, type ElementProps } from '@remix-run/ui'
|
|
1
|
+
import { createMixin, on, type ElementProps, type MixinFactory } from '@remix-run/ui'
|
|
2
2
|
|
|
3
3
|
type ScrollLockState = {
|
|
4
4
|
count: number
|
|
@@ -10,7 +10,7 @@ type ScrollLockState = {
|
|
|
10
10
|
|
|
11
11
|
const scrollLocks = new WeakMap<Document, ScrollLockState>()
|
|
12
12
|
|
|
13
|
-
export function lockScroll(targetDocument = globalThis.document) {
|
|
13
|
+
export function lockScroll(targetDocument: Document | undefined = globalThis.document): () => void {
|
|
14
14
|
if (!targetDocument?.body || !targetDocument.defaultView) {
|
|
15
15
|
return () => {}
|
|
16
16
|
}
|
|
@@ -74,7 +74,11 @@ export function lockScroll(targetDocument = globalThis.document) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export const lockScrollOnToggle
|
|
77
|
+
export const lockScrollOnToggle: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
|
|
78
|
+
HTMLElement,
|
|
79
|
+
[],
|
|
80
|
+
ElementProps
|
|
81
|
+
>((handle) => {
|
|
78
82
|
let unlockScroll = () => {}
|
|
79
83
|
|
|
80
84
|
handle.signal.addEventListener('abort', () => {
|