@remix-run/ui 0.4.0 → 0.5.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 (189) 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 +294 -219
  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 +58 -15
  78. package/dist/runtime/frame.js +367 -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/navigation.d.ts +4 -1
  95. package/dist/runtime/navigation.js +157 -32
  96. package/dist/runtime/navigation.js.map +1 -1
  97. package/dist/runtime/reconcile.d.ts +11 -10
  98. package/dist/runtime/reconcile.js +549 -421
  99. package/dist/runtime/reconcile.js.map +1 -1
  100. package/dist/runtime/refresh.d.ts +28 -0
  101. package/dist/runtime/refresh.js +31 -0
  102. package/dist/runtime/refresh.js.map +1 -0
  103. package/dist/runtime/render.js +1 -1
  104. package/dist/runtime/render.js.map +1 -1
  105. package/dist/runtime/run.d.ts +3 -1
  106. package/dist/runtime/run.js +15 -7
  107. package/dist/runtime/run.js.map +1 -1
  108. package/dist/runtime/scheduler.js +8 -16
  109. package/dist/runtime/scheduler.js.map +1 -1
  110. package/dist/runtime/to-vnode.d.ts +2 -2
  111. package/dist/runtime/to-vnode.js +68 -20
  112. package/dist/runtime/to-vnode.js.map +1 -1
  113. package/dist/runtime/vdom.d.ts +4 -2
  114. package/dist/runtime/vdom.js +87 -42
  115. package/dist/runtime/vdom.js.map +1 -1
  116. package/dist/runtime/vnode.d.ts +138 -67
  117. package/dist/runtime/vnode.js +17 -12
  118. package/dist/runtime/vnode.js.map +1 -1
  119. package/dist/select/index.js +11 -11
  120. package/dist/select/index.js.map +1 -1
  121. package/dist/select/primitives.d.ts +1 -1
  122. package/dist/select/primitives.js +6 -6
  123. package/dist/server/stream.js +20 -27
  124. package/dist/server/stream.js.map +1 -1
  125. package/dist/shared/focus-styles.d.ts +1 -1
  126. package/dist/shared/focus-styles.js +1 -1
  127. package/dist/shared/focus-styles.js.map +1 -1
  128. package/dist/shared/listbox-popover-styles.js +1 -1
  129. package/dist/shared/style-values.js +26 -26
  130. package/dist/shared/style-values.js.map +1 -1
  131. package/dist/style/css-mixin.js +3 -3
  132. package/dist/style/index.js +2 -2
  133. package/dist/style/style.js +25 -6
  134. package/dist/style/style.js.map +1 -1
  135. package/dist/style/stylesheet.d.ts +0 -1
  136. package/dist/style/stylesheet.js +13 -54
  137. package/dist/style/stylesheet.js.map +1 -1
  138. package/dist/tabs/index.js +11 -11
  139. package/dist/tabs/index.js.map +1 -1
  140. package/dist/tabs/primitives.d.ts +1 -1
  141. package/dist/test.js +1 -1
  142. package/dist/toggle/index.js +9 -9
  143. package/dist/toggle/index.js.map +1 -1
  144. package/dist/toggle/primitives.d.ts +1 -1
  145. package/package.json +10 -7
  146. package/src/animation/animate-mixins.ts +3 -3
  147. package/src/animation/demos/color-interpolation.tsx +1 -1
  148. package/src/button/index.ts +24 -24
  149. package/src/checkbox/index.ts +10 -8
  150. package/src/combobox/index.tsx +1 -1
  151. package/src/dev/refresh.ts +1 -0
  152. package/src/index.ts +1 -1
  153. package/src/input/index.ts +8 -8
  154. package/src/radio/index.ts +6 -5
  155. package/src/runtime/client-entry-boundary.ts +54 -0
  156. package/src/runtime/component.ts +65 -16
  157. package/src/runtime/core/attributes.ts +35 -1
  158. package/src/runtime/core/children.ts +24 -2
  159. package/src/runtime/core/vnode.ts +25 -4
  160. package/src/runtime/create-element.ts +6 -5
  161. package/src/runtime/demos/readme.demo.tsx +4 -4
  162. package/src/runtime/diff-dom.ts +358 -243
  163. package/src/runtime/dom.ts +331 -214
  164. package/src/runtime/element-function.ts +1 -0
  165. package/src/runtime/event-listeners.ts +1 -1
  166. package/src/runtime/form-navigation.ts +184 -0
  167. package/src/runtime/frame-resolution.ts +13 -0
  168. package/src/runtime/frame.ts +482 -124
  169. package/src/runtime/jsx.ts +3 -5
  170. package/src/runtime/key.ts +1 -0
  171. package/src/runtime/mixins/link-mixin.ts +1 -0
  172. package/src/runtime/mixins/mixin.ts +21 -18
  173. package/src/runtime/navigation.ts +204 -30
  174. package/src/runtime/reconcile.ts +762 -759
  175. package/src/runtime/refresh.ts +46 -0
  176. package/src/runtime/run.ts +12 -2
  177. package/src/runtime/scheduler.ts +9 -29
  178. package/src/runtime/to-vnode.ts +85 -23
  179. package/src/runtime/vdom.ts +89 -70
  180. package/src/runtime/vnode.ts +192 -88
  181. package/src/select/index.tsx +9 -9
  182. package/src/server/stream.ts +22 -28
  183. package/src/shared/focus-styles.ts +1 -1
  184. package/src/shared/style-values.ts +26 -26
  185. package/src/style/style.ts +25 -6
  186. package/src/style/stylesheet.ts +32 -57
  187. package/src/tabs/index.tsx +12 -11
  188. package/src/test/README.md +151 -60
  189. 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',
@@ -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 {
@@ -89,7 +90,6 @@ interface RenderContext {
89
90
  onError: (error: unknown) => void
90
91
  parentVNode?: VNode
91
92
  styleCache: Map<string, { selector: string; css: string }>
92
- emittedStyles: Set<string>
93
93
  resolveFrame: (
94
94
  src: string,
95
95
  target?: string,
@@ -203,7 +203,6 @@ export function renderToStream(
203
203
  onError,
204
204
  resolveFrame: options?.resolveFrame ?? defaultResolveFrame,
205
205
  styleCache: new Map(),
206
- emittedStyles: new Set(),
207
206
  pendingFrames: [],
208
207
  hydrationData: new Map(),
209
208
  unresolvedHydrationData: new Map(),
@@ -436,7 +435,7 @@ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFr
436
435
  return buildElementSegment(tag, props, context, frameState)
437
436
  }
438
437
 
439
- if (typeof type === 'function') {
438
+ if (isElementFunction(type)) {
440
439
  if (type === Frame) {
441
440
  return buildFrameSegment(node, context, frameState)
442
441
  }
@@ -788,8 +787,8 @@ function sanitizeReturnedSsrMixinProps(props: ElementProps): ElementProps {
788
787
 
789
788
  function resolveReturnedSsrMixDescriptors(
790
789
  value: unknown,
791
- ): Array<{ type: Function; args: unknown[] }> | null {
792
- let descriptors: Array<{ type: Function; args: unknown[] }> = []
790
+ ): Array<{ type: ElementFunction; args: unknown[] }> | null {
791
+ let descriptors: Array<{ type: ElementFunction; args: unknown[] }> = []
793
792
  if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
794
793
  return null
795
794
  }
@@ -799,7 +798,7 @@ function resolveReturnedSsrMixDescriptors(
799
798
 
800
799
  function collectReturnedSsrMixDescriptors(
801
800
  value: unknown,
802
- output: Array<{ type: Function; args: unknown[] }>,
801
+ output: Array<{ type: ElementFunction; args: unknown[] }>,
803
802
  ): boolean {
804
803
  if (!value) {
805
804
  return true
@@ -829,7 +828,11 @@ function isSsrMixinElement(
829
828
  return '__rmxMixinElementType' in value
830
829
  }
831
830
 
832
- function isSsrMixinDescriptor(value: unknown): value is { type: Function; args: unknown[] } {
831
+ function isElementFunction(value: unknown): value is ElementFunction {
832
+ return typeof value === 'function'
833
+ }
834
+
835
+ function isSsrMixinDescriptor(value: unknown): value is { type: ElementFunction; args: unknown[] } {
833
836
  if (!value || typeof value !== 'object' || isRemixElement(value)) {
834
837
  return false
835
838
  }
@@ -839,7 +842,7 @@ function isSsrMixinDescriptor(value: unknown): value is { type: Function; args:
839
842
  }
840
843
 
841
844
  function buildComponentSegment(
842
- type: Function,
845
+ type: ElementFunction,
843
846
  props: any,
844
847
  context: RenderContext,
845
848
  componentId: string,
@@ -923,7 +926,7 @@ function createHydrationPropsReplacer(context: RenderContext, frameState: SsrFra
923
926
  }
924
927
 
925
928
  // Component function: render synchronously, then unwrap its result
926
- if (typeof type === 'function') {
929
+ if (isElementFunction(type)) {
927
930
  let vnode = createVNode(type, props)
928
931
  if (context.parentVNode) {
929
932
  vnode._parent = context.parentVNode
@@ -1180,8 +1183,6 @@ function finalizeHtml(html: string, context: RenderContext): string {
1180
1183
  }
1181
1184
  }
1182
1185
 
1183
- html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1184
-
1185
1186
  // Append aggregated hydration/frame data script at the end
1186
1187
  let rmxData = buildRmxDataScript(context)
1187
1188
  if (rmxData) {
@@ -1252,24 +1253,12 @@ function renderStyleTag(
1252
1253
  ): string {
1253
1254
  let wrappedCss = wrapStyleForLayer(selector, css, layer)
1254
1255
  if (!wrappedCss) return ''
1255
- return `<style data-rmx="${escapeHtml(selector)}">${wrappedCss}</style>`
1256
+ return `<style data-rmx="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`
1256
1257
  }
1257
1258
 
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
- })
1259
+ function escapeStyleText(css: string): string {
1260
+ // A literal "</style" closes an HTML style element even when it appears inside a CSS string.
1261
+ return css.replace(/</g, '\\3C ')
1273
1262
  }
1274
1263
 
1275
1264
  function buildRmxDataScript(context: RenderContext): string {
@@ -1303,6 +1292,12 @@ function escapeScriptJson(json: string): string {
1303
1292
  // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1304
1293
  // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
1305
1294
  // document and adopts the CSS into an adopted stylesheet.
1295
+ //
1296
+ // Style tags are intentionally NOT deduped across frame boundaries: each frame
1297
+ // owns its style rules independently on the client (per-frame refcounted
1298
+ // adoption), so every frame's HTML must carry the full set of style tags its
1299
+ // content references — even when a sibling frame or the enclosing document
1300
+ // already emitted the same selector.
1306
1301
  async function streamPendingFrames(
1307
1302
  context: RenderContext,
1308
1303
  controller: ReadableStreamDefaultController,
@@ -1323,7 +1318,6 @@ async function streamPendingFrames(
1323
1318
  try {
1324
1319
  let { html, tail } = await promise
1325
1320
  if (context.signal.aborted) return
1326
- html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1327
1321
 
1328
1322
  // Stream as a template element (first chunk only)
1329
1323
  let templateHtml = `<template id="${frameId}">${escapeTemplateContent(html)}</template>`
@@ -1,2 +1,2 @@
1
1
  export const controlFocusShadow =
2
- '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)'
2
+ '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))'
@@ -114,11 +114,11 @@ export const componentStyleValues: ComponentStyleValues = {
114
114
  },
115
115
  },
116
116
  surface: {
117
- lvl0: '#ffffff',
118
- lvl1: '#f8f8f8',
119
- lvl2: '#f5f5f5',
120
- lvl3: '#f3f3f3',
121
- lvl4: '#efefef',
117
+ lvl0: 'light-dark(#ffffff, #1a1a1a)',
118
+ lvl1: 'light-dark(#f8f8f8, #1f1f1f)',
119
+ lvl2: 'light-dark(#f5f5f5, #232323)',
120
+ lvl3: 'light-dark(#f3f3f3, #272727)',
121
+ lvl4: 'light-dark(#efefef, #2c2c2c)',
122
122
  },
123
123
  shadow: {
124
124
  xs: '0 1px 1px rgb(0 0 0 / 0.05)',
@@ -127,38 +127,38 @@ export const componentStyleValues: ComponentStyleValues = {
127
127
  },
128
128
  colors: {
129
129
  text: {
130
- primary: '#151515',
131
- secondary: '#4f4f4f',
132
- muted: '#6d6d6d',
130
+ primary: 'light-dark(#151515, #ececec)',
131
+ secondary: 'light-dark(#4f4f4f, #b3b3b3)',
132
+ muted: 'light-dark(#6d6d6d, #b3b3b3)',
133
133
  },
134
134
  border: {
135
- subtle: '#e7e7e7',
136
- default: '#d1d1d1',
135
+ subtle: 'light-dark(#e7e7e7, #333333)',
136
+ default: 'light-dark(#d1d1d1, #444444)',
137
137
  },
138
138
  focus: {
139
- ring: '#1A72FF',
139
+ ring: 'light-dark(#1A72FF, #6eaaff)',
140
140
  },
141
141
  action: {
142
142
  primary: {
143
- background: '#1A72FF',
144
- backgroundHover: '#1463e0',
145
- backgroundActive: '#0f55c9',
146
- foreground: 'rgb(255 255 255 / 0.92)',
147
- border: '#1A72FF',
143
+ background: 'light-dark(#1A72FF, #6eaaff)',
144
+ backgroundHover: 'light-dark(#1463e0, #8bbcff)',
145
+ backgroundActive: 'light-dark(#0f55c9, #a7ccff)',
146
+ foreground: 'light-dark(rgb(255 255 255 / 0.92), #151515)',
147
+ border: 'light-dark(#1A72FF, #6eaaff)',
148
148
  },
149
149
  secondary: {
150
- background: '#ffffff',
151
- backgroundHover: '#fbfbfb',
152
- backgroundActive: '#f3f3f3',
153
- foreground: '#202020',
154
- border: '#d1d1d1',
150
+ background: 'light-dark(#ffffff, #1a1a1a)',
151
+ backgroundHover: 'light-dark(#fbfbfb, #232323)',
152
+ backgroundActive: 'light-dark(#f3f3f3, #2c2c2c)',
153
+ foreground: 'light-dark(#202020, #ececec)',
154
+ border: 'light-dark(#d1d1d1, #444444)',
155
155
  },
156
156
  danger: {
157
- background: '#FF3000',
158
- backgroundHover: '#e12b00',
159
- backgroundActive: '#c52600',
160
- foreground: 'rgb(255 255 255 / 0.92)',
161
- border: '#FF3000',
157
+ background: 'light-dark(#FF3000, #ff8a70)',
158
+ backgroundHover: 'light-dark(#e12b00, #ff9f8a)',
159
+ backgroundActive: 'light-dark(#c52600, #ffb39f)',
160
+ foreground: 'light-dark(rgb(255 255 255 / 0.92), #151515)',
161
+ border: 'light-dark(#FF3000, #ff8a70)',
162
162
  },
163
163
  },
164
164
  },
@@ -19,8 +19,22 @@ export interface CSSProps extends DOMStyleProperties {
19
19
  type StyleObject = Record<string, unknown>
20
20
 
21
21
  // Convert camelCase CSS properties to kebab-case
22
+ // Property names repeat constantly across renders; cache conversions instead
23
+ // of running a regex each time.
24
+ const CAMEL_TO_KEBAB_CACHE_LIMIT = 256
25
+ const camelToKebabCache = new Map<string, string>()
26
+
22
27
  function camelToKebab(str: string): string {
23
- return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
28
+ let cached = camelToKebabCache.get(str)
29
+ if (cached === undefined) {
30
+ cached = str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
31
+ if (camelToKebabCache.size >= CAMEL_TO_KEBAB_CACHE_LIMIT) {
32
+ let oldest = camelToKebabCache.keys().next()
33
+ if (!oldest.done) camelToKebabCache.delete(oldest.value)
34
+ }
35
+ camelToKebabCache.set(str, cached)
36
+ }
37
+ return cached
24
38
  }
25
39
 
26
40
  // Properties that should remain unitless (numeric values without px)
@@ -81,18 +95,23 @@ function isKeyframesAtRule(key: string): boolean {
81
95
  )
82
96
  }
83
97
 
84
- // Generate a hash for style objects to create unique class names
98
+ // Generate a hash for style objects to create unique class names.
99
+ // Class names are content-addressed — a collision silently applies the wrong
100
+ // styles — so use two independent 32-bit FNV-1a passes (~64 bits of hash
101
+ // space) rather than a single 32-bit hash. Must be deterministic across
102
+ // server and client.
85
103
  function hashStyle(obj: any): string {
86
104
  // Sort keys to ensure consistent hashing, but include values in the string
87
105
  let sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))
88
106
  let str = JSON.stringify(sortedEntries)
89
- let hash = 0
107
+ let h1 = 0x811c9dc5
108
+ let h2 = 0xcbf29ce4
90
109
  for (let i = 0; i < str.length; i++) {
91
110
  let char = str.charCodeAt(i)
92
- hash = (hash << 5) - hash + char
93
- hash = hash & hash // Convert to 32-bit integer
111
+ h1 = Math.imul(h1 ^ char, 0x01000193) >>> 0
112
+ h2 = Math.imul(h2 ^ char, 0x01000193) >>> 0
94
113
  }
95
- return Math.abs(hash).toString(36)
114
+ return h1.toString(36) + h2.toString(36)
96
115
  }
97
116
 
98
117
  // Convert style object to CSS text