@remix-run/ui 0.4.0 → 0.6.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.
Files changed (195) hide show
  1. package/README.md +74 -1
  2. package/dist/accordion/index.js +2 -2
  3. package/dist/accordion/primitives.d.ts +1 -1
  4. package/dist/accordion/primitives.js.map +1 -1
  5. package/dist/anchor/index.js.map +1 -1
  6. package/dist/animation/animate-layout-mixin.js +1 -1
  7. package/dist/animation/animate-layout-mixin.js.map +1 -1
  8. package/dist/animation/animate-mixins.js +2 -2
  9. package/dist/animation/animate-mixins.js.map +1 -1
  10. package/dist/animation/demos/color-interpolation.js +1 -1
  11. package/dist/animation/demos/color-interpolation.js.map +1 -1
  12. package/dist/animation/demos/drag-release.d.ts +1 -1
  13. package/dist/animation/demos/hold-to-confirm.d.ts +3 -3
  14. package/dist/animation/demos/material-ripple.js +1 -3
  15. package/dist/animation/demos/material-ripple.js.map +1 -1
  16. package/dist/animation/index.js +4 -4
  17. package/dist/animation/spring.js.map +1 -1
  18. package/dist/breadcrumbs/index.js +2 -2
  19. package/dist/button/index.js +25 -25
  20. package/dist/button/index.js.map +1 -1
  21. package/dist/checkbox/index.js +11 -9
  22. package/dist/checkbox/index.js.map +1 -1
  23. package/dist/combobox/index.js +4 -4
  24. package/dist/combobox/index.js.map +1 -1
  25. package/dist/combobox/primitives.d.ts +1 -1
  26. package/dist/combobox/primitives.js +5 -5
  27. package/dist/combobox/primitives.js.map +1 -1
  28. package/dist/dev/refresh.d.ts +1 -0
  29. package/dist/dev/refresh.js +2 -0
  30. package/dist/dev/refresh.js.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +14 -14
  33. package/dist/input/index.js +9 -9
  34. package/dist/input/index.js.map +1 -1
  35. package/dist/jsx-runtime.js +2 -2
  36. package/dist/listbox/index.js +2 -2
  37. package/dist/menu/hover-aim.js.map +1 -1
  38. package/dist/menu/index.js +3 -3
  39. package/dist/menu/primitives.d.ts +1 -1
  40. package/dist/menu/primitives.js +6 -6
  41. package/dist/menu/primitives.js.map +1 -1
  42. package/dist/popover/index.js +3 -3
  43. package/dist/popover/scroll-lock.js.map +1 -1
  44. package/dist/radio/index.js +7 -7
  45. package/dist/radio/index.js.map +1 -1
  46. package/dist/runtime/client-entry-boundary.d.ts +14 -0
  47. package/dist/runtime/client-entry-boundary.js +29 -0
  48. package/dist/runtime/client-entry-boundary.js.map +1 -0
  49. package/dist/runtime/component.d.ts +19 -4
  50. package/dist/runtime/component.js +39 -9
  51. package/dist/runtime/component.js.map +1 -1
  52. package/dist/runtime/core/attributes.js +36 -3
  53. package/dist/runtime/core/attributes.js.map +1 -1
  54. package/dist/runtime/core/children.d.ts +2 -0
  55. package/dist/runtime/core/children.js +26 -2
  56. package/dist/runtime/core/children.js.map +1 -1
  57. package/dist/runtime/core/props.js +2 -2
  58. package/dist/runtime/core/vnode.d.ts +3 -3
  59. package/dist/runtime/core/vnode.js +22 -1
  60. package/dist/runtime/core/vnode.js.map +1 -1
  61. package/dist/runtime/create-element.d.ts +2 -2
  62. package/dist/runtime/create-element.js +3 -2
  63. package/dist/runtime/create-element.js.map +1 -1
  64. package/dist/runtime/diff-dom.js +307 -220
  65. package/dist/runtime/diff-dom.js.map +1 -1
  66. package/dist/runtime/diff-props.js +3 -3
  67. package/dist/runtime/dom.d.ts +12 -0
  68. package/dist/runtime/element-function.d.ts +3 -0
  69. package/dist/runtime/element-function.js +2 -0
  70. package/dist/runtime/element-function.js.map +1 -0
  71. package/dist/runtime/form-navigation.d.ts +25 -0
  72. package/dist/runtime/form-navigation.js +132 -0
  73. package/dist/runtime/form-navigation.js.map +1 -0
  74. package/dist/runtime/frame-resolution.d.ts +5 -0
  75. package/dist/runtime/frame-resolution.js +10 -0
  76. package/dist/runtime/frame-resolution.js.map +1 -0
  77. package/dist/runtime/frame.d.ts +59 -15
  78. package/dist/runtime/frame.js +383 -114
  79. package/dist/runtime/frame.js.map +1 -1
  80. package/dist/runtime/jsx.d.ts +1 -1
  81. package/dist/runtime/jsx.js +1 -1
  82. package/dist/runtime/jsx.js.map +1 -1
  83. package/dist/runtime/key.d.ts +1 -0
  84. package/dist/runtime/key.js +2 -0
  85. package/dist/runtime/key.js.map +1 -0
  86. package/dist/runtime/mixins/attrs-mixin.js +2 -2
  87. package/dist/runtime/mixins/link-mixin.js +4 -3
  88. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  89. package/dist/runtime/mixins/mixin.d.ts +15 -10
  90. package/dist/runtime/mixins/mixin.js +4 -4
  91. package/dist/runtime/mixins/mixin.js.map +1 -1
  92. package/dist/runtime/mixins/on-mixin.js +1 -1
  93. package/dist/runtime/mixins/ref-mixin.js +1 -1
  94. package/dist/runtime/module-preloader.d.ts +8 -0
  95. package/dist/runtime/module-preloader.js +95 -0
  96. package/dist/runtime/module-preloader.js.map +1 -0
  97. package/dist/runtime/navigation.d.ts +4 -1
  98. package/dist/runtime/navigation.js +157 -32
  99. package/dist/runtime/navigation.js.map +1 -1
  100. package/dist/runtime/reconcile.d.ts +11 -10
  101. package/dist/runtime/reconcile.js +549 -421
  102. package/dist/runtime/reconcile.js.map +1 -1
  103. package/dist/runtime/refresh.d.ts +28 -0
  104. package/dist/runtime/refresh.js +31 -0
  105. package/dist/runtime/refresh.js.map +1 -0
  106. package/dist/runtime/render.js +1 -1
  107. package/dist/runtime/render.js.map +1 -1
  108. package/dist/runtime/run.d.ts +3 -1
  109. package/dist/runtime/run.js +15 -7
  110. package/dist/runtime/run.js.map +1 -1
  111. package/dist/runtime/scheduler.js +8 -16
  112. package/dist/runtime/scheduler.js.map +1 -1
  113. package/dist/runtime/to-vnode.d.ts +2 -2
  114. package/dist/runtime/to-vnode.js +68 -20
  115. package/dist/runtime/to-vnode.js.map +1 -1
  116. package/dist/runtime/vdom.d.ts +4 -2
  117. package/dist/runtime/vdom.js +87 -42
  118. package/dist/runtime/vdom.js.map +1 -1
  119. package/dist/runtime/vnode.d.ts +138 -67
  120. package/dist/runtime/vnode.js +17 -12
  121. package/dist/runtime/vnode.js.map +1 -1
  122. package/dist/select/index.js +11 -11
  123. package/dist/select/index.js.map +1 -1
  124. package/dist/select/primitives.d.ts +1 -1
  125. package/dist/select/primitives.js +6 -6
  126. package/dist/server/stream.d.ts +2 -0
  127. package/dist/server/stream.js +74 -29
  128. package/dist/server/stream.js.map +1 -1
  129. package/dist/shared/focus-styles.d.ts +1 -1
  130. package/dist/shared/focus-styles.js +1 -1
  131. package/dist/shared/focus-styles.js.map +1 -1
  132. package/dist/shared/listbox-popover-styles.js +1 -1
  133. package/dist/shared/style-values.js +26 -26
  134. package/dist/shared/style-values.js.map +1 -1
  135. package/dist/style/css-mixin.js +3 -3
  136. package/dist/style/index.js +2 -2
  137. package/dist/style/style.js +25 -6
  138. package/dist/style/style.js.map +1 -1
  139. package/dist/style/stylesheet.d.ts +0 -1
  140. package/dist/style/stylesheet.js +13 -54
  141. package/dist/style/stylesheet.js.map +1 -1
  142. package/dist/tabs/index.js +11 -11
  143. package/dist/tabs/index.js.map +1 -1
  144. package/dist/tabs/primitives.d.ts +1 -1
  145. package/dist/test.js +1 -1
  146. package/dist/toggle/index.js +9 -9
  147. package/dist/toggle/index.js.map +1 -1
  148. package/dist/toggle/primitives.d.ts +1 -1
  149. package/package.json +10 -7
  150. package/src/animation/animate-mixins.ts +3 -3
  151. package/src/animation/demos/color-interpolation.tsx +1 -1
  152. package/src/button/index.ts +24 -24
  153. package/src/checkbox/index.ts +10 -8
  154. package/src/combobox/index.tsx +1 -1
  155. package/src/dev/refresh.ts +1 -0
  156. package/src/index.ts +1 -1
  157. package/src/input/index.ts +8 -8
  158. package/src/radio/index.ts +6 -5
  159. package/src/runtime/client-entry-boundary.ts +54 -0
  160. package/src/runtime/component.ts +65 -16
  161. package/src/runtime/core/attributes.ts +35 -1
  162. package/src/runtime/core/children.ts +24 -2
  163. package/src/runtime/core/vnode.ts +25 -4
  164. package/src/runtime/create-element.ts +6 -5
  165. package/src/runtime/demos/readme.demo.tsx +4 -4
  166. package/src/runtime/diff-dom.ts +370 -244
  167. package/src/runtime/dom.ts +331 -214
  168. package/src/runtime/element-function.ts +1 -0
  169. package/src/runtime/event-listeners.ts +1 -1
  170. package/src/runtime/form-navigation.ts +184 -0
  171. package/src/runtime/frame-resolution.ts +13 -0
  172. package/src/runtime/frame.ts +500 -124
  173. package/src/runtime/jsx.ts +3 -5
  174. package/src/runtime/key.ts +1 -0
  175. package/src/runtime/mixins/link-mixin.ts +1 -0
  176. package/src/runtime/mixins/mixin.ts +21 -18
  177. package/src/runtime/module-preloader.ts +115 -0
  178. package/src/runtime/navigation.ts +204 -30
  179. package/src/runtime/reconcile.ts +762 -759
  180. package/src/runtime/refresh.ts +46 -0
  181. package/src/runtime/run.ts +12 -2
  182. package/src/runtime/scheduler.ts +9 -29
  183. package/src/runtime/to-vnode.ts +85 -23
  184. package/src/runtime/vdom.ts +89 -70
  185. package/src/runtime/vnode.ts +192 -88
  186. package/src/select/index.tsx +9 -9
  187. package/src/server/README.md +13 -0
  188. package/src/server/stream.ts +93 -30
  189. package/src/shared/focus-styles.ts +1 -1
  190. package/src/shared/style-values.ts +26 -26
  191. package/src/style/style.ts +25 -6
  192. package/src/style/stylesheet.ts +32 -57
  193. package/src/tabs/index.tsx +12 -11
  194. package/src/test/README.md +151 -60
  195. package/src/toggle/index.tsx +9 -7
@@ -1,7 +1,13 @@
1
- import type { ComponentHandle, Component } from './component.ts'
2
- import { Fragment, Frame } from './component.ts'
1
+ import type { ComponentHandle, Fragment, Frame, FrameHandle, FrameProps } from './component.ts'
3
2
  import { isRemixElement } from './core/vnode.ts'
4
- import type { ElementProps, RemixNode } from './jsx.ts'
3
+ import type { Frame as FrameInstance } from './frame.ts'
4
+ import type { RemixNode } from './jsx.ts'
5
+ import type { ElementFunction } from './element-function.ts'
6
+ import type { Key } from './key.ts'
7
+ import type { MixinRuntimeState, MixinRuntimeValue } from './mixins/mixin.ts'
8
+ import type { OnMixinDescriptor } from './mixins/on-mixin.ts'
9
+ import type { Scheduler } from './scheduler.ts'
10
+ import type { StyleManager } from '../style/index.ts'
5
11
 
6
12
  export { isRemixElement }
7
13
 
@@ -9,136 +15,234 @@ export const TEXT_NODE = Symbol('TEXT_NODE')
9
15
  export const NON_RENDER_NODE = Symbol('NON_RENDER_NODE')
10
16
  export const ROOT_VNODE = Symbol('ROOT_VNODE')
11
17
 
12
- export type VNodeType =
13
- | typeof ROOT_VNODE
14
- | string // host element
15
- | Function // component
16
- | typeof TEXT_NODE
17
- | typeof NON_RENDER_NODE
18
- | typeof Fragment
19
- | typeof Frame
20
-
21
- export type VNode<T extends VNodeType = VNodeType> = {
22
- type: T
23
- props?: ElementProps
24
- _mixedProps?: ElementProps
25
- key?: string
26
-
27
- // _prefixes assigned during reconciliation
28
- _parent?: VNode
29
- _children?: VNode[]
30
- _dom?: unknown
31
- _controller?: AbortController
32
- _mixState?: unknown
33
- _directEventDescriptors?: unknown
34
- _directEventState?: unknown
35
- _controlledState?: unknown
36
- _svg?: boolean
37
- // Range roots render between comment boundary markers
38
- _rangeStart?: Node
39
- _rangeEnd?: Node
40
- _pendingHydrationComponentId?: string
41
- _frameInstance?: unknown
42
- _frameFallbackRoot?: { render: (element: RemixNode) => void; dispose: () => void }
43
- _frameResolveToken?: number
44
- _frameResolveController?: AbortController
45
- _frameResolved?: boolean
18
+ export type VNodeKind = 'root' | 'empty' | 'text' | 'fragment' | 'host' | 'component' | 'frame'
46
19
 
47
- // TEXT_NODE
48
- _text?: string
49
-
50
- // Component
51
- _handle?: ComponentHandle
52
- _id?: string
53
- _content?: VNode
20
+ export type RuntimeElementProps = {
21
+ [name: string]: unknown
22
+ }
54
23
 
55
- // Mixin-persisted node removal state
56
- _persistedByMixins?: boolean
57
- _persistedParentByMixins?: ParentNode
58
- _persistedRemovalToken?: number
24
+ export type RuntimeHostProps = RuntimeElementProps & {
25
+ children?: RemixNode
26
+ innerHTML?: string
27
+ mix?: MixinRuntimeValue
59
28
  }
60
29
 
61
- export type FragmentNode = VNode & {
62
- type: typeof Fragment
63
- _children: VNode[]
30
+ type InputNodeBase<kind extends VNodeKind, type> = {
31
+ kind: kind
32
+ type: type
33
+ key?: Key
34
+ _parent?: never
64
35
  }
65
36
 
66
- export type TextNode = VNode & {
67
- type: typeof TEXT_NODE
37
+ export type NonRenderNode = InputNodeBase<'empty', typeof NON_RENDER_NODE>
38
+
39
+ export type TextNode = InputNodeBase<'text', typeof TEXT_NODE> & {
68
40
  _text: string
69
41
  }
70
42
 
71
- export type NonRenderNode = VNode & {
72
- type: typeof NON_RENDER_NODE
43
+ export type FragmentNode = InputNodeBase<'fragment', typeof Fragment> & {
44
+ _children: VNodeInput[]
45
+ }
46
+
47
+ export type HostNode = InputNodeBase<'host', string> & {
48
+ props: RuntimeHostProps
49
+ _children: VNodeInput[]
50
+ }
51
+
52
+ export type ComponentNode = InputNodeBase<'component', ElementFunction> & {
53
+ props: RuntimeElementProps
54
+ }
55
+
56
+ export type FrameNode = InputNodeBase<'frame', typeof Frame> & {
57
+ props: RuntimeElementProps & FrameProps
73
58
  }
74
59
 
75
- export type CommittedTextNode = TextNode & {
60
+ export type VNodeInput =
61
+ | NonRenderNode
62
+ | TextNode
63
+ | FragmentNode
64
+ | HostNode
65
+ | ComponentNode
66
+ | FrameNode
67
+
68
+ type MountedNodeBase = {
69
+ _parent: VNodeParent
70
+ _svg: boolean
71
+ }
72
+
73
+ type CommittedNodeBase<kind extends VNodeKind, type> = {
74
+ kind: kind
75
+ type: type
76
+ key?: Key
77
+ } & MountedNodeBase
78
+
79
+ export type CommittedNonRenderNode = CommittedNodeBase<'empty', typeof NON_RENDER_NODE>
80
+
81
+ export type CommittedTextNode = CommittedNodeBase<'text', typeof TEXT_NODE> & {
82
+ _text: string
76
83
  _dom: Text
77
84
  }
78
85
 
79
- export type HostNode = VNode & {
86
+ export type ControlledReflectionState = {
87
+ disposed: boolean
88
+ listenersAttached: boolean
89
+ pendingRestoreVersion: number
90
+ managesValue: boolean
91
+ managesChecked: boolean
92
+ hasControlledValue: boolean
93
+ controlledValue: unknown
94
+ hasControlledChecked: boolean
95
+ controlledChecked: unknown
96
+ onInput(): void
97
+ onChange(): void
98
+ }
99
+
100
+ export type DirectEventBinding = {
80
101
  type: string
81
- props: ElementProps
82
- _children: VNode[]
102
+ handler: (event: Event, signal: AbortSignal) => void | Promise<void>
103
+ capture: boolean
104
+ stableHandler: ((event: Event) => void) | null
105
+ reentry: AbortController | null
106
+ }
107
+
108
+ export type DirectEventState = {
109
+ bindings: DirectEventBinding[]
110
+ }
111
+
112
+ export type HostPersistenceState = {
113
+ parent: ParentNode
114
+ token: number
83
115
  }
84
116
 
85
- export type CommittedHostNode = HostNode & {
117
+ export type CommittedHostNode = CommittedNodeBase<'host', string> & {
118
+ props: RuntimeHostProps
119
+ _children: CommittedVNode[]
86
120
  _dom: Element
87
- _controller?: AbortController
121
+ _mixedProps: RuntimeHostProps
122
+ _mixState?: MixinRuntimeState
123
+ _directEventDescriptors?: OnMixinDescriptor[]
124
+ _directEventState?: DirectEventState
125
+ _controlledState?: ControlledReflectionState
126
+ _persistence?: HostPersistenceState
88
127
  }
89
128
 
90
- export type ComponentNode = VNode & {
91
- type: Function
92
- props: ElementProps
93
- _handle: ComponentHandle
129
+ export type CommittedFragmentNode = CommittedNodeBase<'fragment', typeof Fragment> & {
130
+ _children: CommittedVNode[]
94
131
  }
95
132
 
96
- export type CommittedComponentNode = VNode & {
97
- type: Function
98
- props: ElementProps
99
- _content: VNode
133
+ export type MountingComponentNode = CommittedNodeBase<'component', ElementFunction> & {
134
+ props: RuntimeElementProps
100
135
  _handle: ComponentHandle
136
+ _context: ReconcileContext
137
+ }
138
+
139
+ export type CommittedComponentNode = MountingComponentNode & {
140
+ _content: CommittedVNode
101
141
  }
102
142
 
103
- export function isFragmentNode(node: VNode): node is FragmentNode {
104
- return node.type === Fragment
143
+ export interface FrameFallbackRoot {
144
+ render(element: RemixNode): void
145
+ dispose(): void
105
146
  }
106
147
 
107
- export function isTextNode(node: VNode): node is TextNode {
108
- return node.type === TEXT_NODE
148
+ export type FrameMountState = {
149
+ instance: FrameInstance
150
+ fallbackRoot?: FrameFallbackRoot
151
+ resolveToken: number
152
+ resolveController?: AbortController
153
+ resolved: boolean
109
154
  }
110
155
 
111
- export function isNonRenderNode(node: VNode): node is NonRenderNode {
112
- return node.type === NON_RENDER_NODE
156
+ export type CommittedFrameNode = CommittedNodeBase<'frame', typeof Frame> & {
157
+ props: RuntimeElementProps & FrameProps
158
+ _rangeStart: Comment
159
+ _rangeEnd: Comment
160
+ _state: FrameMountState
161
+ }
162
+
163
+ export type CommittedVNode =
164
+ | CommittedNonRenderNode
165
+ | CommittedTextNode
166
+ | CommittedFragmentNode
167
+ | CommittedHostNode
168
+ | CommittedComponentNode
169
+ | CommittedFrameNode
170
+
171
+ export type RootVNode = {
172
+ kind: 'root'
173
+ type: typeof ROOT_VNODE
174
+ _children: CommittedVNode[]
175
+ _svg: boolean
176
+ _rangeStart?: Node
177
+ _rangeEnd?: Node
178
+ _pendingHydrationComponentId?: string
113
179
  }
114
180
 
115
- export function isCommittedTextNode(node: VNode): node is CommittedTextNode {
116
- return isTextNode(node) && node._dom instanceof Text
181
+ export interface ReconcileContext {
182
+ frame: FrameHandle
183
+ scheduler: Scheduler
184
+ styles: StyleManager
185
+ rootTarget: EventTarget
117
186
  }
118
187
 
119
- export function isHostNode(node: VNode): node is HostNode {
120
- return typeof node.type === 'string'
188
+ export type VNodeParent =
189
+ | RootVNode
190
+ | CommittedFragmentNode
191
+ | CommittedHostNode
192
+ | MountingComponentNode
193
+ | CommittedComponentNode
194
+
195
+ export type VNode = VNodeInput | CommittedVNode | RootVNode | MountingComponentNode
196
+
197
+ export function isFragmentNode(node: VNode): node is FragmentNode | CommittedFragmentNode {
198
+ return node.kind === 'fragment'
121
199
  }
122
200
 
123
- export function isCommittedHostNode(node: VNode): node is CommittedHostNode {
124
- return isHostNode(node) && node._dom instanceof Element
201
+ export function isTextNode(node: VNode): node is TextNode | CommittedTextNode {
202
+ return node.kind === 'text'
125
203
  }
126
204
 
127
- export function isComponentNode(node: VNode): node is ComponentNode {
128
- return typeof node.type === 'function' && node.type !== Frame
205
+ export function isNonRenderNode(node: VNode): node is NonRenderNode | CommittedNonRenderNode {
206
+ return node.kind === 'empty'
207
+ }
208
+
209
+ export function isCommittedTextNode(node: CommittedVNode): node is CommittedTextNode {
210
+ return node.kind === 'text'
211
+ }
212
+
213
+ export function isHostNode(node: VNode): node is HostNode | CommittedHostNode {
214
+ return node.kind === 'host'
215
+ }
216
+
217
+ export function isCommittedHostNode(node: CommittedVNode): node is CommittedHostNode {
218
+ return node.kind === 'host'
219
+ }
220
+
221
+ export function isComponentNode(
222
+ node: VNode,
223
+ ): node is ComponentNode | MountingComponentNode | CommittedComponentNode {
224
+ return node.kind === 'component'
129
225
  }
130
226
 
131
227
  export function isCommittedComponentNode(node: VNode): node is CommittedComponentNode {
132
- return isComponentNode(node) && node._content !== undefined
228
+ return node.kind === 'component' && '_content' in node
229
+ }
230
+
231
+ export function isFrameNode(node: VNode): node is FrameNode | CommittedFrameNode {
232
+ return node.kind === 'frame'
233
+ }
234
+
235
+ export function isCommittedFrameNode(node: CommittedVNode): node is CommittedFrameNode {
236
+ return node.kind === 'frame'
133
237
  }
134
238
 
135
- export function findContextFromAncestry(node: VNode, type: Component): unknown {
136
- let current: VNode | undefined = node
239
+ export function findContextFromAncestry(node: VNodeParent, type: ElementFunction): unknown {
240
+ let current: VNodeParent | undefined = node
137
241
  while (current) {
138
- if (current.type === type && isComponentNode(current)) {
242
+ if (current.kind === 'component' && current.type === type) {
139
243
  return current._handle.getContextValue()
140
244
  }
141
- current = current._parent
245
+ current = '_parent' in current ? current._parent : undefined
142
246
  }
143
247
  return undefined
144
248
  }
@@ -30,10 +30,10 @@ export type SelectOptionProps = Props<'div'> & {
30
30
  }
31
31
 
32
32
  const selectTriggerShadow =
33
- '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.12)'
33
+ '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.2))'
34
34
 
35
35
  const selectTriggerFocusShadow =
36
- '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px #3573F6, 0 0 0 4px rgba(53, 115, 246, 0.1), 0 6px 32px 4px rgba(53, 115, 246, 0.08), inset 0 0 8px 1px rgba(53, 115, 246, 0.05)'
36
+ '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px light-dark(#3573F6, #6eaaff), 0 0 0 4px light-dark(rgba(53, 115, 246, 0.1), rgba(110, 170, 255, 0.18)), 0 6px 32px 4px light-dark(rgba(53, 115, 246, 0.08), rgba(110, 170, 255, 0.14)), inset 0 0 8px 1px light-dark(rgba(53, 115, 246, 0.05), rgba(110, 170, 255, 0.1))'
37
37
 
38
38
  const selectTriggerCss: CSSMixinDescriptor = css({
39
39
  appearance: 'none',
@@ -51,9 +51,9 @@ const selectTriggerCss: CSSMixinDescriptor = css({
51
51
  gap: '6px',
52
52
  border: 0,
53
53
  borderRadius: '8px',
54
- background: '#FFFFFF',
54
+ background: 'light-dark(#FFFFFF, #1a1a1a)',
55
55
  boxShadow: selectTriggerShadow,
56
- color: '#101010',
56
+ color: 'light-dark(#101010, #ececec)',
57
57
  fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
58
58
  fontStyle: 'normal',
59
59
  fontWeight: 400,
@@ -62,15 +62,15 @@ const selectTriggerCss: CSSMixinDescriptor = css({
62
62
  fontFeatureSettings: '"ss01" on, "cv01" on',
63
63
  letterSpacing: 0,
64
64
  textAlign: 'left',
65
- textShadow: '0 1px 0 #FFFFFF',
65
+ textShadow: '0 1px 0 light-dark(#FFFFFF, rgb(0 0 0 / 0.35))',
66
66
  whiteSpace: 'nowrap',
67
67
  '&:hover, &:focus-visible, &[aria-expanded="true"], &[aria-expanded="true"]:hover, &[aria-expanded="true"]:focus-visible':
68
68
  {
69
- background: '#FFFFFF',
70
- color: '#101010',
69
+ background: 'light-dark(#FFFFFF, #1a1a1a)',
70
+ color: 'light-dark(#101010, #ececec)',
71
71
  },
72
72
  '&:active': {
73
- background: '#FFFFFF',
73
+ background: 'light-dark(#FFFFFF, #1a1a1a)',
74
74
  },
75
75
  '&:focus-visible': {
76
76
  outline: 0,
@@ -99,7 +99,7 @@ const triggerIconCss: CSSMixinDescriptor = css({
99
99
  justifyContent: 'center',
100
100
  width: '16px',
101
101
  height: '16px',
102
- color: '#707070',
102
+ color: 'light-dark(#707070, #b3b3b3)',
103
103
  flex: 'none',
104
104
  '& > svg': {
105
105
  display: 'block',
@@ -23,6 +23,7 @@ Renders a component tree to a streaming response. The initial HTML is sent immed
23
23
 
24
24
  ```tsx
25
25
  import { renderToStream } from 'remix/ui/server'
26
+ import { assetServer } from './assets.ts'
26
27
 
27
28
  let stream = renderToStream(<App />, {
28
29
  frameSrc: request.url,
@@ -31,6 +32,17 @@ let stream = renderToStream(<App />, {
31
32
  let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
32
33
  return fetchHtml(frameUrl)
33
34
  },
35
+ async resolveClientEntry(entryId, component) {
36
+ let [href, preloads] = await Promise.all([
37
+ assetServer.getHref(entryId),
38
+ assetServer.getPreloads(entryId),
39
+ ])
40
+ return {
41
+ href,
42
+ exportName: entryId.split('#')[1] || component.name,
43
+ preloads,
44
+ }
45
+ },
34
46
  onError(error) {
35
47
  console.error(error)
36
48
  },
@@ -47,6 +59,7 @@ return new Response(stream, {
47
59
  - **`topFrameSrc`** - Overrides the root frame URL used for `handle.frames.top.src`. This is mainly useful when calling `renderToStream()` from inside `resolveFrame()` for a nested frame render.
48
60
  - **`signal`** - Cancels pending server rendering work. Pass `request.signal` so client disconnects can stop unresolved frame work without invoking `onError` for the disconnect itself.
49
61
  - **`resolveFrame(src, target, context)`** - Called when a `<Frame>` needs its content. Return a string of HTML, a `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the URL for the frame that contains the `<Frame>`, and `context.topFrameSrc` is the outer document URL. Required if your component tree contains `<Frame>` elements.
62
+ - **`resolveClientEntry(entryId, component)`** - Resolves the public module URL, export name, and optional module preload hrefs for a hydrated client entry.
50
63
  - **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
51
64
 
52
65
  When you render nested frame responses with `renderToStream()` inside `resolveFrame()`, pass `frameSrc` for the frame being rendered and carry `topFrameSrc` forward from the parent context. That preserves `handle.frames.top.src` across the whole SSR frame tree.
@@ -1,5 +1,6 @@
1
1
  import type { ComponentHandle, FrameHandle, Key, RemixNode } from '../runtime/component.ts'
2
2
  import type { ElementType, ElementProps, RemixElement } from '../runtime/jsx.ts'
3
+ import type { ElementFunction } from '../runtime/element-function.ts'
3
4
  import { Fragment, createComponent, createFrameHandle, Frame } from '../runtime/component.ts'
4
5
  import { isEntry, type EntryComponent } from '../runtime/client-entries.ts'
5
6
  import {
@@ -76,6 +77,8 @@ interface UnresolvedHydrationData {
76
77
  interface ResolvedClientEntry {
77
78
  href: string
78
79
  exportName: string
80
+ /** Browser module hrefs to begin preloading before hydrating this entry. */
81
+ preloads?: readonly string[]
79
82
  }
80
83
 
81
84
  interface FrameData {
@@ -89,7 +92,6 @@ interface RenderContext {
89
92
  onError: (error: unknown) => void
90
93
  parentVNode?: VNode
91
94
  styleCache: Map<string, { selector: string; css: string }>
92
- emittedStyles: Set<string>
93
95
  resolveFrame: (
94
96
  src: string,
95
97
  target?: string,
@@ -99,6 +101,7 @@ interface RenderContext {
99
101
  hydrationData: Map<string, HydrationData>
100
102
  unresolvedHydrationData: Map<string, UnresolvedHydrationData>
101
103
  frameData: Map<string, FrameData>
104
+ modulePreloadTags: Set<string>
102
105
  blockingFrameTails: ReadableStream<Uint8Array>[]
103
106
  signal: AbortSignal
104
107
  flushKind: FlushKind
@@ -203,11 +206,11 @@ export function renderToStream(
203
206
  onError,
204
207
  resolveFrame: options?.resolveFrame ?? defaultResolveFrame,
205
208
  styleCache: new Map(),
206
- emittedStyles: new Set(),
207
209
  pendingFrames: [],
208
210
  hydrationData: new Map(),
209
211
  unresolvedHydrationData: new Map(),
210
212
  frameData: new Map(),
213
+ modulePreloadTags: new Set(),
211
214
  blockingFrameTails: [],
212
215
  signal: renderAbortController.signal,
213
216
  flushKind: 'fragment',
@@ -436,7 +439,7 @@ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFr
436
439
  return buildElementSegment(tag, props, context, frameState)
437
440
  }
438
441
 
439
- if (typeof type === 'function') {
442
+ if (isElementFunction(type)) {
440
443
  if (type === Frame) {
441
444
  return buildFrameSegment(node, context, frameState)
442
445
  }
@@ -493,6 +496,7 @@ function buildFrameSegment(
493
496
  context.resolveFrame(props.src, props.name, resolveFrameContext),
494
497
  ).then(async (resolved) => {
495
498
  let { html, tail } = await resolveFrameHtml(resolved)
499
+ html = hoistModulePreloadsFromFrameHead(html, context)
496
500
  seg.content = staticSeg(html)
497
501
  if (tail) {
498
502
  context.blockingFrameTails.push(tail)
@@ -788,8 +792,8 @@ function sanitizeReturnedSsrMixinProps(props: ElementProps): ElementProps {
788
792
 
789
793
  function resolveReturnedSsrMixDescriptors(
790
794
  value: unknown,
791
- ): Array<{ type: Function; args: unknown[] }> | null {
792
- let descriptors: Array<{ type: Function; args: unknown[] }> = []
795
+ ): Array<{ type: ElementFunction; args: unknown[] }> | null {
796
+ let descriptors: Array<{ type: ElementFunction; args: unknown[] }> = []
793
797
  if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
794
798
  return null
795
799
  }
@@ -799,7 +803,7 @@ function resolveReturnedSsrMixDescriptors(
799
803
 
800
804
  function collectReturnedSsrMixDescriptors(
801
805
  value: unknown,
802
- output: Array<{ type: Function; args: unknown[] }>,
806
+ output: Array<{ type: ElementFunction; args: unknown[] }>,
803
807
  ): boolean {
804
808
  if (!value) {
805
809
  return true
@@ -829,7 +833,11 @@ function isSsrMixinElement(
829
833
  return '__rmxMixinElementType' in value
830
834
  }
831
835
 
832
- function isSsrMixinDescriptor(value: unknown): value is { type: Function; args: unknown[] } {
836
+ function isElementFunction(value: unknown): value is ElementFunction {
837
+ return typeof value === 'function'
838
+ }
839
+
840
+ function isSsrMixinDescriptor(value: unknown): value is { type: ElementFunction; args: unknown[] } {
833
841
  if (!value || typeof value !== 'object' || isRemixElement(value)) {
834
842
  return false
835
843
  }
@@ -839,7 +847,7 @@ function isSsrMixinDescriptor(value: unknown): value is { type: Function; args:
839
847
  }
840
848
 
841
849
  function buildComponentSegment(
842
- type: Function,
850
+ type: ElementFunction,
843
851
  props: any,
844
852
  context: RenderContext,
845
853
  componentId: string,
@@ -923,7 +931,7 @@ function createHydrationPropsReplacer(context: RenderContext, frameState: SsrFra
923
931
  }
924
932
 
925
933
  // Component function: render synchronously, then unwrap its result
926
- if (typeof type === 'function') {
934
+ if (isElementFunction(type)) {
927
935
  let vnode = createVNode(type, props)
928
936
  if (context.parentVNode) {
929
937
  vnode._parent = context.parentVNode
@@ -1070,6 +1078,10 @@ async function resolveClientEntries(
1070
1078
  moduleUrl: resolvedEntry.href,
1071
1079
  props,
1072
1080
  })
1081
+
1082
+ for (let preload of resolvedEntry.preloads ?? []) {
1083
+ context.modulePreloadTags.add(createModulePreloadTag(preload))
1084
+ }
1073
1085
  }
1074
1086
 
1075
1087
  context.unresolvedHydrationData.clear()
@@ -1092,6 +1104,19 @@ function validateResolvedClientEntry(
1092
1104
  if (!resolvedEntry.exportName) {
1093
1105
  throw new Error(`resolveClientEntry must return a non-empty exportName. Received "${entryId}".`)
1094
1106
  }
1107
+
1108
+ if (resolvedEntry.preloads !== undefined) {
1109
+ if (!Array.isArray(resolvedEntry.preloads)) {
1110
+ throw new Error(`resolveClientEntry preloads must be an array. Received "${entryId}".`)
1111
+ }
1112
+ for (let preload of resolvedEntry.preloads) {
1113
+ if (typeof preload !== 'string' || preload.length === 0) {
1114
+ throw new Error(
1115
+ `resolveClientEntry preloads must contain non-empty strings. Received "${entryId}".`,
1116
+ )
1117
+ }
1118
+ }
1119
+ }
1095
1120
  }
1096
1121
 
1097
1122
  function validateClientEntriesForHydration(context: RenderContext): void {
@@ -1156,9 +1181,10 @@ function transformAttributeName(name: string, isSvg: boolean): string {
1156
1181
  function finalizeHtml(html: string, context: RenderContext): string {
1157
1182
  let hasHtmlRoot = context.flushKind === 'document'
1158
1183
 
1184
+ let preloads = collectModulePreloadTags(context)
1159
1185
  let styles = collectStyleTags(context)
1160
- if (styles) {
1161
- let headContent = styles
1186
+ if (preloads || styles) {
1187
+ let headContent = preloads + styles
1162
1188
  if (hasHtmlRoot) {
1163
1189
  // For HTML root, inject into existing head or create one
1164
1190
  let headCloseIndex = html.indexOf('</head>')
@@ -1180,8 +1206,6 @@ function finalizeHtml(html: string, context: RenderContext): string {
1180
1206
  }
1181
1207
  }
1182
1208
 
1183
- html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1184
-
1185
1209
  // Append aggregated hydration/frame data script at the end
1186
1210
  let rmxData = buildRmxDataScript(context)
1187
1211
  if (rmxData) {
@@ -1206,6 +1230,52 @@ function finalizeHtml(html: string, context: RenderContext): string {
1206
1230
  return html
1207
1231
  }
1208
1232
 
1233
+ const FRAME_HEAD_OPEN_TAG = '<head>'
1234
+ const FRAME_HEAD_CLOSE_TAG = '</head>'
1235
+ const MARKED_MODULE_PRELOAD_START = '<link data-rmx rel="modulepreload" href="'
1236
+ const MODULE_PRELOAD_END = '" />'
1237
+
1238
+ function createModulePreloadTag(href: string): string {
1239
+ return `${MARKED_MODULE_PRELOAD_START}${escapeHtml(href)}${MODULE_PRELOAD_END}`
1240
+ }
1241
+
1242
+ function collectModulePreloadTags(context: RenderContext): string {
1243
+ return Array.from(context.modulePreloadTags).join('')
1244
+ }
1245
+
1246
+ function hoistModulePreloadsFromFrameHead(html: string, context: RenderContext): string {
1247
+ if (!html.startsWith(FRAME_HEAD_OPEN_TAG)) return html
1248
+
1249
+ let tags: string[] = []
1250
+ let remainingHeadStart = FRAME_HEAD_OPEN_TAG.length
1251
+ while (html.startsWith(MARKED_MODULE_PRELOAD_START, remainingHeadStart)) {
1252
+ let tagEnd = html.indexOf(
1253
+ MODULE_PRELOAD_END,
1254
+ remainingHeadStart + MARKED_MODULE_PRELOAD_START.length,
1255
+ )
1256
+ if (tagEnd === -1) return html
1257
+
1258
+ tagEnd += MODULE_PRELOAD_END.length
1259
+ tags.push(html.slice(remainingHeadStart, tagEnd))
1260
+ remainingHeadStart = tagEnd
1261
+ }
1262
+
1263
+ if (tags.length === 0) return html
1264
+
1265
+ let headClose = html.indexOf(FRAME_HEAD_CLOSE_TAG, remainingHeadStart)
1266
+ if (headClose === -1) return html
1267
+
1268
+ for (let tag of tags) {
1269
+ context.modulePreloadTags.add(tag)
1270
+ }
1271
+
1272
+ if (remainingHeadStart === headClose) {
1273
+ return html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length)
1274
+ }
1275
+
1276
+ return FRAME_HEAD_OPEN_TAG + html.slice(remainingHeadStart)
1277
+ }
1278
+
1209
1279
  function processStyleProps(props: any): any {
1210
1280
  let processedProps = { ...props }
1211
1281
  let classAttr = typeof props.class === 'string' ? props.class : ''
@@ -1252,24 +1322,12 @@ function renderStyleTag(
1252
1322
  ): string {
1253
1323
  let wrappedCss = wrapStyleForLayer(selector, css, layer)
1254
1324
  if (!wrappedCss) return ''
1255
- return `<style data-rmx="${escapeHtml(selector)}">${wrappedCss}</style>`
1325
+ return `<style data-rmx="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`
1256
1326
  }
1257
1327
 
1258
- function readStyleTagAttribute(attrs: string, name: string): string | null {
1259
- let match = attrs.match(new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`))
1260
- if (!match) return null
1261
- return match[1] ?? match[2] ?? null
1262
- }
1263
-
1264
- function dedupeServerStyleTagsInHtml(html: string, seenStyles: Set<string>): string {
1265
- return html.replace(/<style\b([^>]*)>[\s\S]*?<\/style>/gi, (match, attrs) => {
1266
- let selector = readStyleTagAttribute(attrs, 'data-rmx')
1267
- if (!selector) return match
1268
-
1269
- if (seenStyles.has(selector)) return ''
1270
- seenStyles.add(selector)
1271
- return match
1272
- })
1328
+ function escapeStyleText(css: string): string {
1329
+ // A literal "</style" closes an HTML style element even when it appears inside a CSS string.
1330
+ return css.replace(/</g, '\\3C ')
1273
1331
  }
1274
1332
 
1275
1333
  function buildRmxDataScript(context: RenderContext): string {
@@ -1303,6 +1361,12 @@ function escapeScriptJson(json: string): string {
1303
1361
  // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1304
1362
  // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
1305
1363
  // document and adopts the CSS into an adopted stylesheet.
1364
+ //
1365
+ // Style tags are intentionally NOT deduped across frame boundaries: each frame
1366
+ // owns its style rules independently on the client (per-frame refcounted
1367
+ // adoption), so every frame's HTML must carry the full set of style tags its
1368
+ // content references — even when a sibling frame or the enclosing document
1369
+ // already emitted the same selector.
1306
1370
  async function streamPendingFrames(
1307
1371
  context: RenderContext,
1308
1372
  controller: ReadableStreamDefaultController,
@@ -1323,7 +1387,6 @@ async function streamPendingFrames(
1323
1387
  try {
1324
1388
  let { html, tail } = await promise
1325
1389
  if (context.signal.aborted) return
1326
- html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1327
1390
 
1328
1391
  // Stream as a template element (first chunk only)
1329
1392
  let templateHtml = `<template id="${frameId}">${escapeTemplateContent(html)}</template>`