@remix-run/ui 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +41 -47
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -28
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.js +9 -1
  108. package/dist/runtime/navigation.js.map +1 -1
  109. package/dist/runtime/reconcile.js +433 -192
  110. package/dist/runtime/reconcile.js.map +1 -1
  111. package/dist/runtime/run.d.ts +17 -2
  112. package/dist/runtime/run.js.map +1 -1
  113. package/dist/runtime/stream-protocol.d.ts +8 -0
  114. package/dist/runtime/stream-protocol.js +20 -0
  115. package/dist/runtime/stream-protocol.js.map +1 -0
  116. package/dist/runtime/to-vnode.js +5 -9
  117. package/dist/runtime/to-vnode.js.map +1 -1
  118. package/dist/runtime/vnode.d.ts +5 -4
  119. package/dist/runtime/vnode.js +2 -3
  120. package/dist/runtime/vnode.js.map +1 -1
  121. package/dist/server/stream.js +76 -106
  122. package/dist/server/stream.js.map +1 -1
  123. package/dist/style/layers.d.ts +2 -0
  124. package/dist/style/layers.js +3 -0
  125. package/dist/style/layers.js.map +1 -0
  126. package/dist/style/stylesheet.d.ts +3 -1
  127. package/dist/style/stylesheet.js +69 -8
  128. package/dist/style/stylesheet.js.map +1 -1
  129. package/dist/theme/glyph-contract.d.ts +1 -1
  130. package/dist/theme/runtime.js +4 -1
  131. package/dist/theme/runtime.js.map +1 -1
  132. package/package.json +3 -3
  133. package/src/animation/README.md +314 -0
  134. package/src/animation/animate-layout-mixin.ts +79 -71
  135. package/src/components/accordion/README.md +166 -0
  136. package/src/components/accordion/accordion.tsx +8 -10
  137. package/src/components/accordion/demos/card.demo.tsx +113 -0
  138. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  139. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  140. package/src/components/anchor/README.md +153 -0
  141. package/src/components/breadcrumbs/README.md +55 -0
  142. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  143. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  144. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  145. package/src/components/button/README.md +4 -4
  146. package/src/components/button/button.tsx +88 -2
  147. package/src/components/button/demos/aliases.demo.tsx +27 -0
  148. package/src/components/button/demos/basic.demo.tsx +30 -0
  149. package/src/components/button/demos/states.demo.tsx +52 -0
  150. package/src/components/combobox/combobox.tsx +8 -10
  151. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  152. package/src/components/glyph/README.md +72 -0
  153. package/src/components/listbox/README.md +115 -0
  154. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  155. package/src/components/listbox/listbox.ts +23 -32
  156. package/src/components/menu/README.md +96 -0
  157. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  158. package/src/components/menu/demos/overview.demo.tsx +76 -0
  159. package/src/components/menu/menu.tsx +35 -41
  160. package/src/components/popover/README.md +3 -3
  161. package/src/components/popover/demos/overview.demo.tsx +143 -0
  162. package/src/components/select/README.md +107 -0
  163. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  164. package/src/components/select/demos/overview.demo.tsx +51 -0
  165. package/src/components/select/select.tsx +28 -28
  166. package/src/components/tabs/tabs.tsx +10 -5
  167. package/src/index.ts +1 -1
  168. package/src/runtime/component.ts +94 -82
  169. package/src/runtime/core/attributes.ts +97 -0
  170. package/src/runtime/core/children.ts +42 -0
  171. package/src/runtime/core/props.ts +185 -0
  172. package/src/runtime/core/vnode.ts +48 -0
  173. package/src/runtime/create-element.ts +2 -1
  174. package/src/runtime/diff-dom.ts +59 -12
  175. package/src/runtime/diff-props.ts +2 -187
  176. package/src/runtime/dom.ts +4 -0
  177. package/src/runtime/frame.ts +71 -55
  178. package/src/runtime/jsx.ts +2 -29
  179. package/src/runtime/mixins/mixin.ts +9 -4
  180. package/src/runtime/mixins/on-mixin.ts +20 -12
  181. package/src/runtime/navigation.ts +10 -1
  182. package/src/runtime/reconcile.ts +540 -209
  183. package/src/runtime/run.ts +17 -1
  184. package/src/runtime/stream-protocol.ts +27 -0
  185. package/src/runtime/to-vnode.ts +5 -8
  186. package/src/runtime/vnode.ts +6 -9
  187. package/src/server/README.md +88 -0
  188. package/src/server/stream.ts +99 -116
  189. package/src/style/layers.ts +2 -0
  190. package/src/style/stylesheet.ts +75 -10
  191. package/src/test/README.md +107 -0
  192. package/src/test/utils.ts +3 -1
  193. package/src/theme/README.md +103 -0
  194. package/src/theme/runtime.ts +4 -1
  195. package/src/utils/scroll-lock/README.md +33 -0
@@ -11,8 +11,21 @@ import { TypedEventTarget } from './typed-event-target.ts'
11
11
  /**
12
12
  * Options for starting the client runtime with {@link run}.
13
13
  */
14
- export type RunInit = {
14
+ export interface RunInit {
15
+ /**
16
+ * Loads the named browser module export for a hydrated `clientEntry()`.
17
+ *
18
+ * Implementations usually call dynamic `import(moduleUrl)` and return
19
+ * `mod[exportName]`.
20
+ */
15
21
  loadModule: LoadModule
22
+
23
+ /**
24
+ * Resolves browser-loaded `<Frame>` content.
25
+ *
26
+ * Omit this only when the runtime never needs to load or reload frames in the
27
+ * browser.
28
+ */
16
29
  resolveFrame?: ResolveFrame
17
30
  }
18
31
 
@@ -27,8 +40,11 @@ export type AppRuntimeEventMap = {
27
40
  * Client runtime returned by {@link run}.
28
41
  */
29
42
  export type AppRuntime = TypedEventTarget<AppRuntimeEventMap> & {
43
+ /** Resolves after the current document finishes hydrating. */
30
44
  ready(): Promise<void>
45
+ /** Flushes any queued component updates synchronously. */
31
46
  flush(): void
47
+ /** Stops runtime listeners and disposes the top-level frame. */
32
48
  dispose(): void
33
49
  }
34
50
 
@@ -0,0 +1,27 @@
1
+ export type FlushKind = 'document' | 'fragment'
2
+
3
+ const FLUSH_MARKER_PATTERN = /<!--\s*rmx:flush\s+(document|fragment)\s*-->/g
4
+
5
+ export function appendFlushMarker(html: string, kind: FlushKind): string {
6
+ return `${html}<!-- rmx:flush ${kind} -->`
7
+ }
8
+
9
+ export function stripFlushMarkers(html: string): string {
10
+ FLUSH_MARKER_PATTERN.lastIndex = 0
11
+ return html.replace(FLUSH_MARKER_PATTERN, '')
12
+ }
13
+
14
+ export function findFlushMarker(
15
+ html: string,
16
+ startIndex: number,
17
+ ): { index: number; endIndex: number; kind: FlushKind } | undefined {
18
+ FLUSH_MARKER_PATTERN.lastIndex = startIndex
19
+ let match = FLUSH_MARKER_PATTERN.exec(html)
20
+ if (!match) return undefined
21
+
22
+ return {
23
+ index: match.index,
24
+ endIndex: FLUSH_MARKER_PATTERN.lastIndex,
25
+ kind: match[1] as FlushKind,
26
+ }
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { Fragment } from './component.ts'
2
2
  import { invariant } from './invariant.ts'
3
+ import { isEmptyChild, isPrimitiveChild, normalizeChildren } from './core/children.ts'
3
4
  import type { RemixElement, RemixNode } from './jsx.ts'
4
5
  import { isRemixElement, TEXT_NODE, type VNode } from './vnode.ts'
5
6
 
@@ -13,21 +14,17 @@ function flatMapChildrenToVNodes(node: RemixElement): VNode[] {
13
14
  }
14
15
 
15
16
  function flattenChildrenToVNodes(nodes: RemixNode[], out: VNode[]): void {
16
- for (let child of nodes) {
17
- if (Array.isArray(child)) {
18
- flattenChildrenToVNodes(child, out)
19
- } else {
20
- out.push(toVNode(child))
21
- }
17
+ for (let child of normalizeChildren(nodes)) {
18
+ out.push(toVNode(child))
22
19
  }
23
20
  }
24
21
 
25
22
  export function toVNode(node: RemixNode): VNode {
26
- if (node === null || node === undefined || typeof node === 'boolean') {
23
+ if (isEmptyChild(node)) {
27
24
  return { type: TEXT_NODE, _text: '' }
28
25
  }
29
26
 
30
- if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
27
+ if (isPrimitiveChild(node)) {
31
28
  return { type: TEXT_NODE, _text: String(node) }
32
29
  }
33
30
 
@@ -1,6 +1,9 @@
1
1
  import type { ComponentHandle, Component } from './component.ts'
2
2
  import { Fragment, Frame } from './component.ts'
3
- import type { ElementProps, RemixElement, RemixNode } from './jsx.ts'
3
+ import { isRemixElement } from './core/vnode.ts'
4
+ import type { ElementProps, RemixNode } from './jsx.ts'
5
+
6
+ export { isRemixElement }
4
7
 
5
8
  export const TEXT_NODE = Symbol('TEXT_NODE')
6
9
  export const ROOT_VNODE = Symbol('ROOT_VNODE')
@@ -25,6 +28,8 @@ export type VNode<T extends VNodeType = VNodeType> = {
25
28
  _dom?: unknown
26
29
  _controller?: AbortController
27
30
  _mixState?: unknown
31
+ _directEventDescriptors?: unknown
32
+ _directEventState?: unknown
28
33
  _controlledState?: unknown
29
34
  _svg?: boolean
30
35
  // Range roots render between comment boundary markers
@@ -37,10 +42,6 @@ export type VNode<T extends VNodeType = VNodeType> = {
37
42
  _frameResolveController?: AbortController
38
43
  _frameResolved?: boolean
39
44
 
40
- // Internal diffing fields
41
- _index?: number
42
- _flags?: number
43
-
44
45
  // TEXT_NODE
45
46
  _text?: string
46
47
 
@@ -121,10 +122,6 @@ export function isCommittedComponentNode(node: VNode): node is CommittedComponen
121
122
  return isComponentNode(node) && node._content !== undefined
122
123
  }
123
124
 
124
- export function isRemixElement(node: RemixNode): node is RemixElement {
125
- return typeof node === 'object' && node !== null && '$rmx' in node
126
- }
127
-
128
125
  export function findContextFromAncestry(node: VNode, type: Component): unknown {
129
126
  let current: VNode | undefined = node
130
127
  while (current) {
@@ -0,0 +1,88 @@
1
+ # Server
2
+
3
+ Remix Component can render to HTML on the server using two APIs:
4
+
5
+ - `renderToString` - Returns a complete HTML string. Simple, but buffers the entire response.
6
+ - `renderToStream` - Returns a `ReadableStream<Uint8Array>`. Sends the initial HTML immediately and streams frame content as it resolves.
7
+
8
+ Both are exported from `remix/ui/server`.
9
+
10
+ ## renderToString
11
+
12
+ Renders a component tree to a complete HTML string. Use this when you need the full output before responding (e.g., generating static pages or embedding HTML in an email).
13
+
14
+ ```tsx
15
+ import { renderToString } from 'remix/ui/server'
16
+
17
+ let html = await renderToString(<App />)
18
+ ```
19
+
20
+ ## renderToStream
21
+
22
+ Renders a component tree to a streaming response. The initial HTML is sent immediately. Any `<Frame>` components with a `fallback` prop will render the fallback first, then stream the resolved content as it becomes available.
23
+
24
+ ```tsx
25
+ import { renderToStream } from 'remix/ui/server'
26
+
27
+ let stream = renderToStream(<App />, {
28
+ frameSrc: request.url,
29
+ resolveFrame(src, _target, context) {
30
+ let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
31
+ return fetchHtml(frameUrl)
32
+ },
33
+ onError(error) {
34
+ console.error(error)
35
+ },
36
+ })
37
+
38
+ return new Response(stream, {
39
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
40
+ })
41
+ ```
42
+
43
+ ### Options
44
+
45
+ - **`frameSrc`** - Seeds SSR frame state for the current render. When provided, server-rendered components can read `handle.frame.src` and `handle.frames.top.src` during SSR.
46
+ - **`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.
47
+ - **`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.
48
+ - **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
49
+
50
+ 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.
51
+
52
+ ### Streaming behavior
53
+
54
+ When the stream encounters a `<Frame>` component:
55
+
56
+ - **Without `fallback`** (blocking): The frame content is awaited before the initial HTML chunk is sent. The resolved content appears inline.
57
+ - **With `fallback`** (non-blocking): The fallback is rendered inline in the initial chunk. Once the frame resolves, a `<template>` element containing the real content is streamed at the end of the response. The client swaps it in automatically.
58
+
59
+ This means the first chunk always contains a complete, renderable page. Slow data sources don't block the initial paint.
60
+
61
+ ## Head content
62
+
63
+ To render content into the document head during SSR, use an explicit `<head>` element:
64
+
65
+ ```tsx
66
+ function ProductPage() {
67
+ return () => (
68
+ <html>
69
+ <head>
70
+ <title>Product Name</title>
71
+ <meta name="description" content="A great product" />
72
+ </head>
73
+ <body>
74
+ <h1>Product Name</h1>
75
+ </body>
76
+ </html>
77
+ )
78
+ }
79
+ ```
80
+
81
+ ## CSS
82
+
83
+ Components using the `css` prop have their styles collected during rendering and emitted as a single `<style>` tag in the `<head>`. No client-side style injection is needed for server-rendered content.
84
+
85
+ ## See Also
86
+
87
+ - [Hydration](https://github.com/remix-run/remix/blob/main/packages/ui/docs/hydration.md) - Making server-rendered components interactive on the client
88
+ - [Frames](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md) - Streaming partial server UI with `<Frame>`
@@ -2,7 +2,14 @@ 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
+ } from '../runtime/core/attributes.ts'
11
+ import { appendFlushMarker, type FlushKind, stripFlushMarkers } from '../runtime/stream-protocol.ts'
12
+ import { REMIX_UI_STYLE_LAYER } from '../style/layers.ts'
6
13
 
7
14
  interface VNode {
8
15
  type: ElementType
@@ -90,6 +97,7 @@ interface RenderContext {
90
97
  unresolvedHydrationData: Map<string, UnresolvedHydrationData>
91
98
  frameData: Map<string, FrameData>
92
99
  blockingFrameTails: ReadableStream<Uint8Array>[]
100
+ flushKind: FlushKind
93
101
  serverIdScope: string
94
102
  serverIdCounter: number
95
103
  }
@@ -114,54 +122,32 @@ type Segment =
114
122
  pending?: Promise<void>
115
123
  }
116
124
 
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'
125
+ const TEXTAREA_VALUE_PROPS = new Set(['value', 'defaultValue'])
126
+ const INPUT_DEFAULT_PROPS = new Set(['defaultValue', 'defaultChecked'])
127
+
135
128
  const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
136
129
 
137
130
  function stripDoctypeMarkup(html: string): string {
138
131
  return html.replace(DOCTYPE_PATTERN, '')
139
132
  }
140
133
 
141
- function getStyleLayerName(selector: string, layer: string = DEFAULT_STYLE_LAYER): string {
134
+ function hasRenderableHtml(html: string): boolean {
135
+ return stripDoctypeMarkup(html).trim() !== ''
136
+ }
137
+
138
+ function emptyReadableStream(): ReadableStream<Uint8Array> {
139
+ return new ReadableStream({
140
+ start(controller) {
141
+ controller.close()
142
+ },
143
+ })
144
+ }
145
+
146
+ function getStyleLayerName(selector: string, layer: string = REMIX_UI_STYLE_LAYER): string {
142
147
  return `${layer}.${selector}`
143
148
  }
144
149
 
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'])
150
+ const SSR_OMITTED_PROPS = RUNTIME_FRAMEWORK_PROPS
165
151
 
166
152
  const ssrSignal = Object.freeze({
167
153
  get aborted() {
@@ -218,6 +204,7 @@ export function renderToStream(
218
204
  unresolvedHydrationData: new Map(),
219
205
  frameData: new Map(),
220
206
  blockingFrameTails: [],
207
+ flushKind: 'fragment',
221
208
  serverIdScope: crypto.randomUUID().slice(0, 8),
222
209
  serverIdCounter: 0,
223
210
  }
@@ -231,7 +218,7 @@ export function renderToStream(
231
218
  validateClientEntriesForHydration(context)
232
219
  let html = serializeSegment(root)
233
220
  let finalHtml = finalizeHtml(html, context)
234
- let bytes = encoder.encode(finalHtml)
221
+ let bytes = encoder.encode(appendFlushMarker(finalHtml, context.flushKind))
235
222
  controller.enqueue(bytes)
236
223
 
237
224
  // If we have any tails from blocking frame streams, stream them now.
@@ -289,24 +276,28 @@ function createServerComponentId(context: RenderContext): string {
289
276
  return `s${context.serverIdScope}-${context.serverIdCounter}`
290
277
  }
291
278
 
292
- async function splitFirstChunk(
293
- stream: ReadableStream<Uint8Array>,
294
- ): Promise<{ first: Uint8Array; tail: ReadableStream<Uint8Array> }> {
279
+ async function splitFirstChunk(stream: ReadableStream<Uint8Array>): Promise<ResolvedFrameHtml> {
295
280
  let reader = stream.getReader()
281
+ let decoder = new TextDecoder()
296
282
 
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
- }),
283
+ let first: Uint8Array | undefined
284
+ while (true) {
285
+ let { value, done } = await reader.read()
286
+ if (done || !value) break
287
+
288
+ let text = decoder.decode(value, { stream: true })
289
+ if (hasRenderableHtml(text)) {
290
+ first = value
291
+ break
307
292
  }
308
293
  }
309
294
 
295
+ if (!first) {
296
+ decoder.decode()
297
+ reader.releaseLock()
298
+ return { html: '', tail: emptyReadableStream() }
299
+ }
300
+
310
301
  let released = false
311
302
  function release() {
312
303
  if (released) return
@@ -334,17 +325,15 @@ async function splitFirstChunk(
334
325
  },
335
326
  })
336
327
 
337
- return { first: value, tail }
328
+ return { html: stripFlushMarkers(stripDoctypeMarkup(decoder.decode(first))), tail }
338
329
  }
339
330
 
340
331
  async function resolveFrameHtml(
341
332
  input: string | ReadableStream<Uint8Array>,
342
333
  ): Promise<ResolvedFrameHtml> {
343
- if (typeof input === 'string') return { html: stripDoctypeMarkup(input) }
334
+ if (typeof input === 'string') return { html: stripFlushMarkers(stripDoctypeMarkup(input)) }
344
335
 
345
- let decoder = new TextDecoder()
346
- let { first, tail } = await splitFirstChunk(input)
347
- return { html: stripDoctypeMarkup(decoder.decode(first)), tail }
336
+ return await splitFirstChunk(input)
348
337
  }
349
338
 
350
339
  function isRemixElement(node: unknown): node is RemixElement {
@@ -385,6 +374,7 @@ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFr
385
374
  let tag = type
386
375
 
387
376
  if (tag === 'html') {
377
+ context.flushKind = 'document'
388
378
  return buildElementSegment(tag, props, context, frameState)
389
379
  }
390
380
 
@@ -464,7 +454,15 @@ function buildElementSegment(
464
454
  let processedProps = processStyleProps(mixedProps)
465
455
  // Determine namespace context for the current element and its children
466
456
  let currentIsSvg = context.insideSvg || tag === 'svg'
467
- let attrs = renderAttributes(processedProps, currentIsSvg)
457
+
458
+ if (!currentIsSvg && tag === 'textarea') {
459
+ return buildTextareaElementSegment(tag, processedProps)
460
+ }
461
+
462
+ let attrs =
463
+ !currentIsSvg && tag === 'input'
464
+ ? renderInputAttributes(processedProps)
465
+ : renderAttributes(processedProps, currentIsSvg)
468
466
 
469
467
  if (SELF_CLOSING_TAGS.has(tag)) {
470
468
  return staticSeg(`<${tag}${attrs} />`)
@@ -485,6 +483,27 @@ function buildElementSegment(
485
483
  return compositeSeg([open, children, close])
486
484
  }
487
485
 
486
+ function buildTextareaElementSegment(tag: string, props: any): Segment {
487
+ let attrs = renderAttributes(props, false, TEXTAREA_VALUE_PROPS)
488
+ let value = props.value ?? props.defaultValue ?? ''
489
+ return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`)
490
+ }
491
+
492
+ function renderInputAttributes(props: any): string {
493
+ let value =
494
+ props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value
495
+ let checked =
496
+ props.checked === undefined && props.defaultChecked !== undefined
497
+ ? props.defaultChecked
498
+ : props.checked
499
+ let inputProps = {
500
+ ...props,
501
+ ...(value === undefined ? {} : { value }),
502
+ ...(checked === undefined ? {} : { checked }),
503
+ }
504
+ return renderAttributes(inputProps, false, INPUT_DEFAULT_PROPS)
505
+ }
506
+
488
507
  function buildHeadElementSegment(
489
508
  tag: string,
490
509
  props: any,
@@ -502,11 +521,12 @@ function buildHeadElementSegment(
502
521
  return compositeSeg([open, children, close])
503
522
  }
504
523
 
505
- function renderAttributes(props: any, isSvg: boolean): string {
524
+ function renderAttributes(props: any, isSvg: boolean, excludedProps?: Set<string>): string {
506
525
  let attrs = ''
507
526
 
508
527
  for (let key in props) {
509
- if (FRAMEWORK_PROPS.has(key)) continue
528
+ if (SSR_OMITTED_PROPS.has(key)) continue
529
+ if (excludedProps?.has(key)) continue
510
530
 
511
531
  let value = props[key]
512
532
  if (value === undefined || value === null || value === false) continue
@@ -798,7 +818,11 @@ function buildComponentSegment(
798
818
  let [renderedNode] = handle.render(props)
799
819
  let childContext = { ...context, parentVNode: vnode }
800
820
 
801
- return buildSegment(renderedNode, childContext, frameState)
821
+ let rendered = buildSegment(renderedNode, childContext, frameState)
822
+ if (childContext.flushKind === 'document') {
823
+ context.flushKind = 'document'
824
+ }
825
+ return rendered
802
826
  }
803
827
 
804
828
  function createHydrationPropsReplacer(context: RenderContext, frameState: SsrFrameState) {
@@ -1063,24 +1087,11 @@ function escapeTemplateContent(html: string): string {
1063
1087
  }
1064
1088
 
1065
1089
  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
1090
+ return normalizeAttributeName(name, isSvg).attr
1080
1091
  }
1081
1092
 
1082
1093
  function finalizeHtml(html: string, context: RenderContext): string {
1083
- let hasHtmlRoot = html.trimStart().toLowerCase().startsWith('<html')
1094
+ let hasHtmlRoot = context.flushKind === 'document'
1084
1095
 
1085
1096
  let styles = collectStyleTags(context)
1086
1097
  if (styles) {
@@ -1164,7 +1175,7 @@ function collectStyleTags(context: RenderContext): string {
1164
1175
  function wrapStyleForLayer(
1165
1176
  selector: string,
1166
1177
  css: string,
1167
- layer: string = DEFAULT_STYLE_LAYER,
1178
+ layer: string = REMIX_UI_STYLE_LAYER,
1168
1179
  ): string {
1169
1180
  let trimmed = css.trim()
1170
1181
  if (!trimmed) return ''
@@ -1174,7 +1185,7 @@ function wrapStyleForLayer(
1174
1185
  function renderStyleTag(
1175
1186
  selector: string,
1176
1187
  css: string,
1177
- layer: string = DEFAULT_STYLE_LAYER,
1188
+ layer: string = REMIX_UI_STYLE_LAYER,
1178
1189
  ): string {
1179
1190
  let wrappedCss = wrapStyleForLayer(selector, css, layer)
1180
1191
  if (!wrappedCss) return ''
@@ -1225,36 +1236,6 @@ function escapeScriptJson(json: string): string {
1225
1236
  return json.replace(/</g, '\\u003c')
1226
1237
  }
1227
1238
 
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
1239
  // Frame styles work end-to-end when frame handlers use their own `renderToStream`:
1259
1240
  // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1260
1241
  // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
@@ -1337,11 +1318,13 @@ async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
1337
1318
  * @returns Rendered HTML.
1338
1319
  */
1339
1320
  export async function renderToString(node: RemixNode): Promise<string> {
1340
- return drain(
1341
- renderToStream(node, {
1342
- onError(error) {
1343
- throw error
1344
- },
1345
- }),
1321
+ return stripFlushMarkers(
1322
+ await drain(
1323
+ renderToStream(node, {
1324
+ onError(error) {
1325
+ throw error
1326
+ },
1327
+ }),
1328
+ ),
1346
1329
  )
1347
1330
  }
@@ -0,0 +1,2 @@
1
+ export const REMIX_UI_STYLE_LAYER = 'rmx'
2
+ export const REMIX_UI_RESET_LAYER = 'rmx-reset'