@remix-run/ui 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -2,7 +2,15 @@ import type { ComponentHandle, FrameHandle, Key, RemixNode } from '../runtime/co
2
2
  import type { ElementType, ElementProps, RemixElement } from '../runtime/jsx.ts'
3
3
  import { Fragment, createComponent, createFrameHandle, Frame } from '../runtime/component.ts'
4
4
  import { isEntry, type EntryComponent } from '../runtime/client-entries.ts'
5
- import { normalizeSvgAttribute } from '../runtime/svg-attributes.ts'
5
+ import {
6
+ FRAMEWORK_PROPS as RUNTIME_FRAMEWORK_PROPS,
7
+ SELF_CLOSING_TAGS,
8
+ normalizeAttributeName,
9
+ serializeStyleObject,
10
+ shouldStringifyBooleanAttribute,
11
+ } from '../runtime/core/attributes.ts'
12
+ import { appendFlushMarker, type FlushKind, stripFlushMarkers } from '../runtime/stream-protocol.ts'
13
+ import { REMIX_UI_STYLE_LAYER } from '../style/layers.ts'
6
14
 
7
15
  interface VNode {
8
16
  type: ElementType
@@ -24,6 +32,8 @@ export interface RenderToStreamOptions {
24
32
  frameSrc?: string | URL
25
33
  /** Source URL for the top-level frame in nested frame renders. */
26
34
  topFrameSrc?: string | URL
35
+ /** Signal that cancels pending server rendering work. */
36
+ signal?: AbortSignal
27
37
  /** Error hook invoked when rendering work throws. */
28
38
  onError?: (error: unknown) => void
29
39
  /** Callback used to resolve nested frame content during streaming SSR. */
@@ -90,6 +100,8 @@ interface RenderContext {
90
100
  unresolvedHydrationData: Map<string, UnresolvedHydrationData>
91
101
  frameData: Map<string, FrameData>
92
102
  blockingFrameTails: ReadableStream<Uint8Array>[]
103
+ signal: AbortSignal
104
+ flushKind: FlushKind
93
105
  serverIdScope: string
94
106
  serverIdCounter: number
95
107
  }
@@ -114,54 +126,32 @@ type Segment =
114
126
  pending?: Promise<void>
115
127
  }
116
128
 
117
- const SELF_CLOSING_TAGS = new Set([
118
- 'area',
119
- 'base',
120
- 'br',
121
- 'col',
122
- 'embed',
123
- 'hr',
124
- 'img',
125
- 'input',
126
- 'link',
127
- 'meta',
128
- 'param',
129
- 'source',
130
- 'track',
131
- 'wbr',
132
- ])
133
-
134
- const DEFAULT_STYLE_LAYER = 'rmx'
129
+ const TEXTAREA_VALUE_PROPS = new Set(['value', 'defaultValue'])
130
+ const INPUT_DEFAULT_PROPS = new Set(['defaultValue', 'defaultChecked'])
131
+
135
132
  const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
136
133
 
137
134
  function stripDoctypeMarkup(html: string): string {
138
135
  return html.replace(DOCTYPE_PATTERN, '')
139
136
  }
140
137
 
141
- function getStyleLayerName(selector: string, layer: string = DEFAULT_STYLE_LAYER): string {
138
+ function hasRenderableHtml(html: string): boolean {
139
+ return stripDoctypeMarkup(html).trim() !== ''
140
+ }
141
+
142
+ function emptyReadableStream(): ReadableStream<Uint8Array> {
143
+ return new ReadableStream({
144
+ start(controller) {
145
+ controller.close()
146
+ },
147
+ })
148
+ }
149
+
150
+ function getStyleLayerName(selector: string, layer: string = REMIX_UI_STYLE_LAYER): string {
142
151
  return `${layer}.${selector}`
143
152
  }
144
153
 
145
- const NUMERIC_CSS_PROPS = new Set([
146
- 'z-index',
147
- 'opacity',
148
- 'flex-grow',
149
- 'flex-shrink',
150
- 'flex-order',
151
- 'grid-area',
152
- 'grid-row',
153
- 'grid-column',
154
- 'font-weight',
155
- 'line-height',
156
- 'order',
157
- 'orphans',
158
- 'widows',
159
- 'zoom',
160
- 'columns',
161
- 'column-count',
162
- ])
163
-
164
- const FRAMEWORK_PROPS = new Set(['children', 'innerHTML', 'on', 'key', 'mix'])
154
+ const SSR_OMITTED_PROPS = RUNTIME_FRAMEWORK_PROPS
165
155
 
166
156
  const ssrSignal = Object.freeze({
167
157
  get aborted() {
@@ -206,6 +196,7 @@ export function renderToStream(
206
196
  let currentFrameSrc = normalizeFrameSrc(options?.frameSrc ?? options?.topFrameSrc)
207
197
  let topFrameSrc = normalizeFrameSrc(options?.topFrameSrc ?? currentFrameSrc)
208
198
  let rootFrameState = createSsrFrameState(currentFrameSrc, topFrameSrc)
199
+ let renderAbortController = new AbortController()
209
200
 
210
201
  let context: RenderContext = {
211
202
  insideSvg: false,
@@ -218,20 +209,38 @@ export function renderToStream(
218
209
  unresolvedHydrationData: new Map(),
219
210
  frameData: new Map(),
220
211
  blockingFrameTails: [],
212
+ signal: renderAbortController.signal,
213
+ flushKind: 'fragment',
221
214
  serverIdScope: crypto.randomUUID().slice(0, 8),
222
215
  serverIdCounter: 0,
223
216
  }
224
217
 
218
+ function cancel(reason: unknown): void {
219
+ if (!renderAbortController.signal.aborted) {
220
+ renderAbortController.abort(reason)
221
+ }
222
+ }
223
+
224
+ let signal = options?.signal
225
+ if (signal?.aborted) {
226
+ cancel(signal.reason)
227
+ } else {
228
+ signal?.addEventListener('abort', () => cancel(signal.reason), { once: true })
229
+ }
230
+
225
231
  return new ReadableStream({
226
232
  async start(controller) {
227
233
  try {
228
234
  let root = buildSegment(node, context, rootFrameState)
229
235
  await resolveBlocking(root)
236
+ if (closeIfCancelled(controller, context)) return
230
237
  await resolveClientEntries(context, options?.resolveClientEntry)
238
+ if (closeIfCancelled(controller, context)) return
231
239
  validateClientEntriesForHydration(context)
232
240
  let html = serializeSegment(root)
233
241
  let finalHtml = finalizeHtml(html, context)
234
- let bytes = encoder.encode(finalHtml)
242
+ let bytes = encoder.encode(appendFlushMarker(finalHtml, context.flushKind))
243
+ if (closeIfCancelled(controller, context)) return
235
244
  controller.enqueue(bytes)
236
245
 
237
246
  // If we have any tails from blocking frame streams, stream them now.
@@ -239,7 +248,7 @@ export function renderToStream(
239
248
  // that must come after the initial document chunk.
240
249
  let tailPromise =
241
250
  context.blockingFrameTails.length > 0
242
- ? streamByteStreams(context.blockingFrameTails, controller, context.onError)
251
+ ? streamByteStreams(context.blockingFrameTails, controller, context)
243
252
  : Promise.resolve()
244
253
 
245
254
  // If we have pending non-blocking frames, stream them as they resolve
@@ -250,15 +259,44 @@ export function renderToStream(
250
259
 
251
260
  await Promise.all([tailPromise, pendingPromise])
252
261
 
262
+ if (closeIfCancelled(controller, context)) return
253
263
  controller.close()
254
264
  } catch (error) {
265
+ if (isSignalAbortError(context.signal, error)) {
266
+ closeStream(controller)
267
+ return
268
+ }
255
269
  onError(error)
256
270
  controller.error(error)
257
271
  }
258
272
  },
273
+ cancel(reason) {
274
+ cancel(reason)
275
+ },
259
276
  })
260
277
  }
261
278
 
279
+ function isSignalAbortError(signal: AbortSignal, error: unknown): boolean {
280
+ return signal.aborted && error === signal.reason
281
+ }
282
+
283
+ function closeIfCancelled(
284
+ controller: ReadableStreamDefaultController,
285
+ context: RenderContext,
286
+ ): boolean {
287
+ if (!context.signal.aborted) return false
288
+ closeStream(controller)
289
+ return true
290
+ }
291
+
292
+ function closeStream(controller: ReadableStreamDefaultController): void {
293
+ try {
294
+ controller.close()
295
+ } catch {
296
+ // The consumer may already have cancelled the stream.
297
+ }
298
+ }
299
+
262
300
  function defaultResolveFrame(): never {
263
301
  throw new Error('No resolveFrame provided')
264
302
  }
@@ -289,24 +327,28 @@ function createServerComponentId(context: RenderContext): string {
289
327
  return `s${context.serverIdScope}-${context.serverIdCounter}`
290
328
  }
291
329
 
292
- async function splitFirstChunk(
293
- stream: ReadableStream<Uint8Array>,
294
- ): Promise<{ first: Uint8Array; tail: ReadableStream<Uint8Array> }> {
330
+ async function splitFirstChunk(stream: ReadableStream<Uint8Array>): Promise<ResolvedFrameHtml> {
295
331
  let reader = stream.getReader()
332
+ let decoder = new TextDecoder()
296
333
 
297
- let { value, done } = await reader.read()
298
- if (done || !value) {
299
- reader.releaseLock()
300
- return {
301
- first: new Uint8Array(),
302
- tail: new ReadableStream({
303
- start(controller) {
304
- controller.close()
305
- },
306
- }),
334
+ let first: Uint8Array | undefined
335
+ while (true) {
336
+ let { value, done } = await reader.read()
337
+ if (done || !value) break
338
+
339
+ let text = decoder.decode(value, { stream: true })
340
+ if (hasRenderableHtml(text)) {
341
+ first = value
342
+ break
307
343
  }
308
344
  }
309
345
 
346
+ if (!first) {
347
+ decoder.decode()
348
+ reader.releaseLock()
349
+ return { html: '', tail: emptyReadableStream() }
350
+ }
351
+
310
352
  let released = false
311
353
  function release() {
312
354
  if (released) return
@@ -334,17 +376,15 @@ async function splitFirstChunk(
334
376
  },
335
377
  })
336
378
 
337
- return { first: value, tail }
379
+ return { html: stripFlushMarkers(stripDoctypeMarkup(decoder.decode(first))), tail }
338
380
  }
339
381
 
340
382
  async function resolveFrameHtml(
341
383
  input: string | ReadableStream<Uint8Array>,
342
384
  ): Promise<ResolvedFrameHtml> {
343
- if (typeof input === 'string') return { html: stripDoctypeMarkup(input) }
385
+ if (typeof input === 'string') return { html: stripFlushMarkers(stripDoctypeMarkup(input)) }
344
386
 
345
- let decoder = new TextDecoder()
346
- let { first, tail } = await splitFirstChunk(input)
347
- return { html: stripDoctypeMarkup(decoder.decode(first)), tail }
387
+ return await splitFirstChunk(input)
348
388
  }
349
389
 
350
390
  function isRemixElement(node: unknown): node is RemixElement {
@@ -385,6 +425,7 @@ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFr
385
425
  let tag = type
386
426
 
387
427
  if (tag === 'html') {
428
+ context.flushKind = 'document'
388
429
  return buildElementSegment(tag, props, context, frameState)
389
430
  }
390
431
 
@@ -438,6 +479,9 @@ function buildFrameSegment(props: any, context: RenderContext, frameState: SsrFr
438
479
  let framePromise = Promise.resolve(
439
480
  context.resolveFrame(props.src, props.name, resolveFrameContext),
440
481
  ).then(async (resolved) => resolveFrameHtml(resolved))
482
+ // The response stream can be cancelled before pending frames are drained.
483
+ // Keep the promise observed so request aborts don't become unhandled.
484
+ framePromise.catch(() => {})
441
485
  context.pendingFrames.push({ frameId, promise: framePromise })
442
486
  } else {
443
487
  seg.pending = Promise.resolve(
@@ -464,7 +508,15 @@ function buildElementSegment(
464
508
  let processedProps = processStyleProps(mixedProps)
465
509
  // Determine namespace context for the current element and its children
466
510
  let currentIsSvg = context.insideSvg || tag === 'svg'
467
- let attrs = renderAttributes(processedProps, currentIsSvg)
511
+
512
+ if (!currentIsSvg && tag === 'textarea') {
513
+ return buildTextareaElementSegment(tag, processedProps)
514
+ }
515
+
516
+ let attrs =
517
+ !currentIsSvg && tag === 'input'
518
+ ? renderInputAttributes(processedProps)
519
+ : renderAttributes(processedProps, currentIsSvg)
468
520
 
469
521
  if (SELF_CLOSING_TAGS.has(tag)) {
470
522
  return staticSeg(`<${tag}${attrs} />`)
@@ -485,6 +537,27 @@ function buildElementSegment(
485
537
  return compositeSeg([open, children, close])
486
538
  }
487
539
 
540
+ function buildTextareaElementSegment(tag: string, props: any): Segment {
541
+ let attrs = renderAttributes(props, false, TEXTAREA_VALUE_PROPS)
542
+ let value = props.value ?? props.defaultValue ?? ''
543
+ return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`)
544
+ }
545
+
546
+ function renderInputAttributes(props: any): string {
547
+ let value =
548
+ props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value
549
+ let checked =
550
+ props.checked === undefined && props.defaultChecked !== undefined
551
+ ? props.defaultChecked
552
+ : props.checked
553
+ let inputProps = {
554
+ ...props,
555
+ ...(value === undefined ? {} : { value }),
556
+ ...(checked === undefined ? {} : { checked }),
557
+ }
558
+ return renderAttributes(inputProps, false, INPUT_DEFAULT_PROPS)
559
+ }
560
+
488
561
  function buildHeadElementSegment(
489
562
  tag: string,
490
563
  props: any,
@@ -502,18 +575,23 @@ function buildHeadElementSegment(
502
575
  return compositeSeg([open, children, close])
503
576
  }
504
577
 
505
- function renderAttributes(props: any, isSvg: boolean): string {
578
+ function renderAttributes(props: any, isSvg: boolean, excludedProps?: Set<string>): string {
506
579
  let attrs = ''
507
580
 
508
581
  for (let key in props) {
509
- if (FRAMEWORK_PROPS.has(key)) continue
582
+ if (SSR_OMITTED_PROPS.has(key)) continue
583
+ if (excludedProps?.has(key)) continue
510
584
 
511
585
  let value = props[key]
512
- if (value === undefined || value === null || value === false) continue
513
-
514
586
  let attrName = transformAttributeName(key, isSvg)
587
+ let shouldStringifyBoolean = shouldStringifyBooleanAttribute(attrName)
588
+ if (value === undefined || value === null || (value === false && !shouldStringifyBoolean)) {
589
+ continue
590
+ }
515
591
 
516
- if (value === true) {
592
+ if (typeof value === 'boolean' && shouldStringifyBoolean) {
593
+ attrs += ` ${attrName}="${escapeHtml(String(value))}"`
594
+ } else if (value === true) {
517
595
  attrs += ` ${attrName}`
518
596
  } else {
519
597
  attrs += ` ${attrName}="${escapeHtml(String(value))}"`
@@ -798,7 +876,11 @@ function buildComponentSegment(
798
876
  let [renderedNode] = handle.render(props)
799
877
  let childContext = { ...context, parentVNode: vnode }
800
878
 
801
- return buildSegment(renderedNode, childContext, frameState)
879
+ let rendered = buildSegment(renderedNode, childContext, frameState)
880
+ if (childContext.flushKind === 'document') {
881
+ context.flushKind = 'document'
882
+ }
883
+ return rendered
802
884
  }
803
885
 
804
886
  function createHydrationPropsReplacer(context: RenderContext, frameState: SsrFrameState) {
@@ -1063,24 +1145,11 @@ function escapeTemplateContent(html: string): string {
1063
1145
  }
1064
1146
 
1065
1147
  function transformAttributeName(name: string, isSvg: boolean): string {
1066
- // aria-/data- pass through
1067
- if (name.startsWith('aria-') || name.startsWith('data-')) return name
1068
-
1069
- // HTML mappings
1070
- if (name === 'className') return 'class'
1071
- if (!isSvg) {
1072
- if (name === 'htmlFor') return 'for'
1073
- if (name === 'tabIndex') return 'tabindex'
1074
- if (name === 'acceptCharset') return 'accept-charset'
1075
- if (name === 'httpEquiv') return 'http-equiv'
1076
- return name.toLowerCase()
1077
- }
1078
-
1079
- return normalizeSvgAttribute(name).attr
1148
+ return normalizeAttributeName(name, isSvg).attr
1080
1149
  }
1081
1150
 
1082
1151
  function finalizeHtml(html: string, context: RenderContext): string {
1083
- let hasHtmlRoot = html.trimStart().toLowerCase().startsWith('<html')
1152
+ let hasHtmlRoot = context.flushKind === 'document'
1084
1153
 
1085
1154
  let styles = collectStyleTags(context)
1086
1155
  if (styles) {
@@ -1164,7 +1233,7 @@ function collectStyleTags(context: RenderContext): string {
1164
1233
  function wrapStyleForLayer(
1165
1234
  selector: string,
1166
1235
  css: string,
1167
- layer: string = DEFAULT_STYLE_LAYER,
1236
+ layer: string = REMIX_UI_STYLE_LAYER,
1168
1237
  ): string {
1169
1238
  let trimmed = css.trim()
1170
1239
  if (!trimmed) return ''
@@ -1174,7 +1243,7 @@ function wrapStyleForLayer(
1174
1243
  function renderStyleTag(
1175
1244
  selector: string,
1176
1245
  css: string,
1177
- layer: string = DEFAULT_STYLE_LAYER,
1246
+ layer: string = REMIX_UI_STYLE_LAYER,
1178
1247
  ): string {
1179
1248
  let wrappedCss = wrapStyleForLayer(selector, css, layer)
1180
1249
  if (!wrappedCss) return ''
@@ -1225,36 +1294,6 @@ function escapeScriptJson(json: string): string {
1225
1294
  return json.replace(/</g, '\\u003c')
1226
1295
  }
1227
1296
 
1228
- function serializeStyleObject(style: Record<string, any>): string {
1229
- let parts: string[] = []
1230
-
1231
- for (let [key, value] of Object.entries(style)) {
1232
- if (value == null) continue
1233
- if (typeof value === 'boolean') continue
1234
- if (typeof value === 'number' && !Number.isFinite(value)) continue
1235
-
1236
- // Convert camelCase to kebab-case
1237
- let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
1238
-
1239
- // Add px to numeric values where appropriate
1240
- let shouldAppendPx =
1241
- typeof value === 'number' &&
1242
- value !== 0 &&
1243
- !NUMERIC_CSS_PROPS.has(cssKey) &&
1244
- !cssKey.startsWith('--')
1245
-
1246
- let cssValue = shouldAppendPx
1247
- ? `${value}px`
1248
- : Array.isArray(value)
1249
- ? value.join(', ')
1250
- : String(value)
1251
-
1252
- parts.push(`${cssKey}: ${cssValue};`)
1253
- }
1254
-
1255
- return parts.join(' ')
1256
- }
1257
-
1258
1297
  // Frame styles work end-to-end when frame handlers use their own `renderToStream`:
1259
1298
  // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1260
1299
  // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
@@ -1267,26 +1306,33 @@ async function streamPendingFrames(
1267
1306
  let processedFrames = new Set<string>()
1268
1307
 
1269
1308
  while (true) {
1309
+ if (context.signal.aborted) break
1310
+
1270
1311
  let batch = context.pendingFrames.filter(({ frameId }) => !processedFrames.has(frameId))
1271
1312
  if (batch.length === 0) break
1272
1313
 
1273
1314
  await Promise.all(
1274
1315
  batch.map(async ({ frameId, promise }) => {
1316
+ if (context.signal.aborted) return
1275
1317
  processedFrames.add(frameId)
1276
1318
  try {
1277
1319
  let { html, tail } = await promise
1320
+ if (context.signal.aborted) return
1278
1321
  html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1279
1322
 
1280
1323
  // Stream as a template element (first chunk only)
1281
1324
  let templateHtml = `<template id="${frameId}">${escapeTemplateContent(html)}</template>`
1325
+ if (context.signal.aborted) return
1282
1326
  controller.enqueue(encoder.encode(templateHtml))
1283
1327
 
1284
1328
  // Forward any additional chunks from a stream-valued resolveFrame result.
1285
1329
  if (tail) {
1286
- await streamByteStreams([tail], controller, context.onError)
1330
+ await streamByteStreams([tail], controller, context)
1287
1331
  }
1288
1332
  } catch (error) {
1289
- context.onError(error)
1333
+ if (!isSignalAbortError(context.signal, error)) {
1334
+ context.onError(error)
1335
+ }
1290
1336
  }
1291
1337
  }),
1292
1338
  )
@@ -1296,19 +1342,23 @@ async function streamPendingFrames(
1296
1342
  async function streamByteStreams(
1297
1343
  streams: ReadableStream<Uint8Array>[],
1298
1344
  controller: ReadableStreamDefaultController,
1299
- onError: (error: unknown) => void,
1345
+ context: RenderContext,
1300
1346
  ): Promise<void> {
1301
1347
  await Promise.all(
1302
1348
  streams.map(async (stream) => {
1303
1349
  let reader = stream.getReader()
1304
1350
  try {
1305
1351
  while (true) {
1352
+ if (context.signal.aborted) break
1306
1353
  let { done, value } = await reader.read()
1307
1354
  if (done) break
1355
+ if (context.signal.aborted) break
1308
1356
  controller.enqueue(value)
1309
1357
  }
1310
1358
  } catch (error) {
1311
- onError(error)
1359
+ if (!isSignalAbortError(context.signal, error)) {
1360
+ context.onError(error)
1361
+ }
1312
1362
  } finally {
1313
1363
  reader.releaseLock()
1314
1364
  }
@@ -1337,11 +1387,13 @@ async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
1337
1387
  * @returns Rendered HTML.
1338
1388
  */
1339
1389
  export async function renderToString(node: RemixNode): Promise<string> {
1340
- return drain(
1341
- renderToStream(node, {
1342
- onError(error) {
1343
- throw error
1344
- },
1345
- }),
1390
+ return stripFlushMarkers(
1391
+ await drain(
1392
+ renderToStream(node, {
1393
+ onError(error) {
1394
+ throw error
1395
+ },
1396
+ }),
1397
+ ),
1346
1398
  )
1347
1399
  }
@@ -1,4 +1,5 @@
1
1
  import { createMixin, renderMixinElement } from '../runtime/mixins/mixin.ts'
2
+ import type { MixinFactory } from '../runtime/mixins/mixin.ts'
2
3
  import type { MixinDescriptor } from '../runtime/mixins/mixin.ts'
3
4
  import type { ElementProps } from '../runtime/jsx.ts'
4
5
  import { invariant } from '../runtime/invariant.ts'
@@ -20,7 +21,11 @@ const clientStyleCache: StyleCache = new Map()
20
21
  /**
21
22
  * Applies generated class names for CSS object styles.
22
23
  */
23
- export const css = createMixin<Element, [styles: CSSProps], ElementProps>((handle) => {
24
+ export const css: MixinFactory<Element, [styles: CSSProps], ElementProps> = createMixin<
25
+ Element,
26
+ [styles: CSSProps],
27
+ ElementProps
28
+ >((handle) => {
24
29
  let activeSelector = ''
25
30
  let activeGeneration = -1
26
31
  let currentStyles: CSSProps = {}
@@ -6,5 +6,4 @@ export type {
6
6
  } from './style.ts'
7
7
  export { processStyleClass, normalizeCssValue } from './style.ts'
8
8
  export { createStyleManager }
9
-
10
- export type StyleManager = ReturnType<typeof createStyleManager>
9
+ export type { ServerStyleSource, StyleManager } from './stylesheet.ts'
@@ -0,0 +1,2 @@
1
+ export const REMIX_UI_STYLE_LAYER = 'rmx'
2
+ export const REMIX_UI_RESET_LAYER = 'rmx-reset'