@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.
Files changed (200) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +51 -52
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -27
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.d.ts +6 -0
  108. package/dist/runtime/navigation.js +16 -3
  109. package/dist/runtime/navigation.js.map +1 -1
  110. package/dist/runtime/reconcile.js +444 -181
  111. package/dist/runtime/reconcile.js.map +1 -1
  112. package/dist/runtime/run.d.ts +17 -2
  113. package/dist/runtime/run.js.map +1 -1
  114. package/dist/runtime/scheduler.js +10 -0
  115. package/dist/runtime/scheduler.js.map +1 -1
  116. package/dist/runtime/stream-protocol.d.ts +8 -0
  117. package/dist/runtime/stream-protocol.js +20 -0
  118. package/dist/runtime/stream-protocol.js.map +1 -0
  119. package/dist/runtime/to-vnode.js +17 -18
  120. package/dist/runtime/to-vnode.js.map +1 -1
  121. package/dist/runtime/vnode.d.ts +5 -4
  122. package/dist/runtime/vnode.js +2 -3
  123. package/dist/runtime/vnode.js.map +1 -1
  124. package/dist/server/stream.js +80 -106
  125. package/dist/server/stream.js.map +1 -1
  126. package/dist/style/layers.d.ts +2 -0
  127. package/dist/style/layers.js +3 -0
  128. package/dist/style/layers.js.map +1 -0
  129. package/dist/style/stylesheet.d.ts +3 -1
  130. package/dist/style/stylesheet.js +69 -8
  131. package/dist/style/stylesheet.js.map +1 -1
  132. package/dist/theme/glyph-contract.d.ts +1 -1
  133. package/dist/theme/runtime.js +4 -1
  134. package/dist/theme/runtime.js.map +1 -1
  135. package/package.json +6 -7
  136. package/src/animation/README.md +314 -0
  137. package/src/animation/animate-layout-mixin.ts +79 -71
  138. package/src/components/accordion/README.md +166 -0
  139. package/src/components/accordion/accordion.tsx +8 -10
  140. package/src/components/accordion/demos/card.demo.tsx +113 -0
  141. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  142. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  143. package/src/components/anchor/README.md +153 -0
  144. package/src/components/breadcrumbs/README.md +55 -0
  145. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  146. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  147. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  148. package/src/components/button/README.md +4 -4
  149. package/src/components/button/button.tsx +88 -2
  150. package/src/components/button/demos/aliases.demo.tsx +27 -0
  151. package/src/components/button/demos/basic.demo.tsx +30 -0
  152. package/src/components/button/demos/states.demo.tsx +52 -0
  153. package/src/components/combobox/combobox.tsx +8 -10
  154. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  155. package/src/components/glyph/README.md +72 -0
  156. package/src/components/listbox/README.md +115 -0
  157. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  158. package/src/components/listbox/listbox.ts +23 -32
  159. package/src/components/menu/README.md +96 -0
  160. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  161. package/src/components/menu/demos/overview.demo.tsx +76 -0
  162. package/src/components/menu/menu.tsx +35 -41
  163. package/src/components/popover/README.md +3 -3
  164. package/src/components/popover/demos/overview.demo.tsx +143 -0
  165. package/src/components/select/README.md +107 -0
  166. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  167. package/src/components/select/demos/overview.demo.tsx +51 -0
  168. package/src/components/select/select.tsx +28 -28
  169. package/src/components/tabs/tabs.tsx +10 -5
  170. package/src/index.ts +1 -1
  171. package/src/runtime/component.ts +94 -82
  172. package/src/runtime/core/attributes.ts +97 -0
  173. package/src/runtime/core/children.ts +42 -0
  174. package/src/runtime/core/props.ts +185 -0
  175. package/src/runtime/core/vnode.ts +48 -0
  176. package/src/runtime/create-element.ts +2 -1
  177. package/src/runtime/diff-dom.ts +59 -12
  178. package/src/runtime/diff-props.ts +2 -187
  179. package/src/runtime/dom.ts +4 -0
  180. package/src/runtime/frame.ts +84 -61
  181. package/src/runtime/jsx.ts +2 -30
  182. package/src/runtime/mixins/mixin.ts +9 -4
  183. package/src/runtime/mixins/on-mixin.ts +20 -12
  184. package/src/runtime/navigation.ts +24 -3
  185. package/src/runtime/reconcile.ts +552 -186
  186. package/src/runtime/run.ts +17 -1
  187. package/src/runtime/scheduler.ts +7 -0
  188. package/src/runtime/stream-protocol.ts +27 -0
  189. package/src/runtime/to-vnode.ts +15 -17
  190. package/src/runtime/vnode.ts +6 -9
  191. package/src/server/README.md +88 -0
  192. package/src/server/stream.ts +105 -117
  193. package/src/style/layers.ts +2 -0
  194. package/src/style/stylesheet.ts +75 -10
  195. package/src/test/README.md +107 -0
  196. package/src/test/utils.ts +3 -1
  197. package/src/theme/README.md +103 -0
  198. package/src/theme/runtime.ts +4 -1
  199. package/src/utils/scroll-lock/README.md +33 -0
  200. package/src/test/setup.ts +0 -7
@@ -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 = DEFAULT_STYLE_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 = 'rmx') {
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 { insert, remove, has, getGeneration, reset, adoptServerStyles, dispose }
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.
@@ -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
- blocks.push(serializeThemeResetCss(selector))
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/test/setup.ts DELETED
@@ -1,7 +0,0 @@
1
- if (typeof document !== 'undefined' && !document.adoptedStyleSheets) {
2
- Object.defineProperty(document, 'adoptedStyleSheets', {
3
- configurable: true,
4
- value: [],
5
- writable: true,
6
- })
7
- }