@remix-run/ui 0.1.2 → 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.
- package/README.md +9 -7
- package/dist/animation/spring.d.ts +6 -35
- package/dist/animation/spring.js +9 -8
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +6 -6
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
- package/dist/components/button/button.d.ts +4 -4
- package/dist/components/button/button.js +3 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/combobox/combobox.d.ts +10 -10
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/listbox/listbox.d.ts +3 -3
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/menu.d.ts +12 -12
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/popover.d.ts +5 -5
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/select/select.d.ts +10 -10
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions/keydown/keydown.d.ts +1 -1
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
- package/dist/runtime/component.d.ts +18 -15
- package/dist/runtime/component.js +14 -5
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +3 -1
- package/dist/runtime/core/attributes.js +19 -1
- package/dist/runtime/core/attributes.js.map +1 -1
- package/dist/runtime/core/props.js +6 -5
- package/dist/runtime/core/props.js.map +1 -1
- package/dist/runtime/dom.d.ts +16 -9
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/mixins/link-mixin.d.ts +5 -1
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +2 -1
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
- package/dist/runtime/mixins/ref-mixin.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +1 -1
- package/dist/runtime/reconcile.js +19 -5
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/scheduler.d.ts +10 -10
- package/dist/runtime/scheduler.js +1 -1
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/svg-attributes.js +1 -0
- package/dist/runtime/svg-attributes.js.map +1 -1
- package/dist/runtime/to-vnode.js +2 -2
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +6 -1
- package/dist/runtime/vnode.js +4 -0
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.d.ts +2 -0
- package/dist/server/stream.js +79 -9
- package/dist/server/stream.js.map +1 -1
- package/dist/style/css-mixin.d.ts +2 -1
- package/dist/style/css-mixin.js.map +1 -1
- package/dist/style/index.d.ts +1 -1
- package/dist/style/stylesheet.d.ts +13 -13
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/contract.d.ts +24 -222
- package/dist/theme/contract.js +2 -2
- package/dist/theme/contract.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -3
- package/dist/theme/glyph-contract.js.map +1 -1
- package/dist/theme/presets/rmx-01/index.d.ts +4 -2
- package/dist/theme/presets/rmx-01/index.js.map +1 -1
- package/dist/utils/scroll-lock.d.ts +3 -3
- package/dist/utils/scroll-lock.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +7 -5
- package/src/animation/spring.ts +21 -9
- package/src/components/accordion/accordion.tsx +7 -5
- package/src/components/anchor/anchor.ts +1 -1
- package/src/components/breadcrumbs/breadcrumbs.tsx +1 -1
- package/src/components/button/button.tsx +30 -18
- package/src/components/combobox/README.md +1 -1
- package/src/components/combobox/combobox.tsx +30 -12
- package/src/components/glyph/glyph.tsx +2 -2
- package/src/components/listbox/listbox.ts +88 -83
- package/src/components/menu/menu.tsx +27 -15
- package/src/components/popover/popover.ts +90 -79
- package/src/components/select/select.tsx +26 -9
- package/src/index.ts +1 -0
- package/src/runtime/component.ts +34 -12
- package/src/runtime/core/attributes.ts +21 -0
- package/src/runtime/core/props.ts +6 -4
- package/src/runtime/dom.ts +16 -9
- package/src/runtime/jsx.ts +1 -1
- package/src/runtime/mixins/link-mixin.ts +88 -86
- package/src/runtime/mixins/mixin.ts +9 -1
- package/src/runtime/mixins/ref-mixin.ts +4 -5
- package/src/runtime/reconcile.ts +23 -14
- package/src/runtime/scheduler.ts +20 -20
- package/src/runtime/svg-attributes.ts +1 -0
- package/src/runtime/to-vnode.ts +2 -2
- package/src/runtime/vnode.ts +10 -0
- package/src/server/README.md +4 -2
- package/src/server/stream.ts +77 -8
- package/src/style/css-mixin.ts +6 -1
- package/src/style/index.ts +1 -2
- package/src/style/stylesheet.ts +14 -2
- package/src/theme/contract.ts +37 -5
- package/src/theme/glyph-contract.ts +3 -1
- package/src/theme/presets/rmx-01/index.ts +4 -2
- package/src/utils/scroll-lock.ts +7 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMixin } from './mixin.ts'
|
|
1
|
+
import { createMixin, type MixinFactory } from './mixin.ts'
|
|
2
2
|
import type { ElementProps } from '../jsx.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,8 +9,8 @@ export type RefCallback<node extends EventTarget> = (node: node, signal: AbortSi
|
|
|
9
9
|
/**
|
|
10
10
|
* Calls a callback when an element is inserted and aborts it when removed.
|
|
11
11
|
*/
|
|
12
|
-
export const ref
|
|
13
|
-
(handle) => {
|
|
12
|
+
export const ref: MixinFactory<Element, [callback: RefCallback<Element>], ElementProps> =
|
|
13
|
+
createMixin<Element, [callback: RefCallback<Element>], ElementProps>((handle) => {
|
|
14
14
|
let controller: AbortController | undefined
|
|
15
15
|
|
|
16
16
|
handle.addEventListener('insert', (event) => {
|
|
@@ -28,5 +28,4 @@ export const ref = createMixin<Element, [callback: RefCallback<Element>], Elemen
|
|
|
28
28
|
callback = nextCallback
|
|
29
29
|
return handle.element
|
|
30
30
|
}
|
|
31
|
-
}
|
|
32
|
-
)
|
|
31
|
+
})
|
package/src/runtime/reconcile.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isCommittedTextNode,
|
|
21
21
|
isFragmentNode,
|
|
22
22
|
isHostNode,
|
|
23
|
+
isNonRenderNode,
|
|
23
24
|
isTextNode,
|
|
24
25
|
findContextFromAncestry,
|
|
25
26
|
} from './vnode.ts'
|
|
@@ -421,6 +422,10 @@ export function diffVNodes(
|
|
|
421
422
|
return rootCursor
|
|
422
423
|
}
|
|
423
424
|
|
|
425
|
+
if (isNonRenderNode(curr) && isNonRenderNode(next)) {
|
|
426
|
+
return rootCursor
|
|
427
|
+
}
|
|
428
|
+
|
|
424
429
|
if (isCommittedTextNode(curr) && isTextNode(next)) {
|
|
425
430
|
diffText(curr, next, vParent)
|
|
426
431
|
return rootCursor
|
|
@@ -444,7 +449,7 @@ export function diffVNodes(
|
|
|
444
449
|
frame,
|
|
445
450
|
scheduler,
|
|
446
451
|
styles,
|
|
447
|
-
|
|
452
|
+
next,
|
|
448
453
|
rootTarget,
|
|
449
454
|
undefined,
|
|
450
455
|
anchor,
|
|
@@ -484,7 +489,7 @@ function replace(
|
|
|
484
489
|
return
|
|
485
490
|
}
|
|
486
491
|
|
|
487
|
-
let replacementAnchor = findNextSiblingDomAnchor(curr
|
|
492
|
+
let replacementAnchor = findNextSiblingDomAnchor(curr) ?? anchor
|
|
488
493
|
remove(curr, domParent, scheduler, styles)
|
|
489
494
|
insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
|
|
490
495
|
}
|
|
@@ -709,6 +714,10 @@ function insert(
|
|
|
709
714
|
? (dom: Node) => domParent.insertBefore(dom, anchor)
|
|
710
715
|
: (dom: Node) => domParent.appendChild(dom)
|
|
711
716
|
|
|
717
|
+
if (isNonRenderNode(node)) {
|
|
718
|
+
return cursor
|
|
719
|
+
}
|
|
720
|
+
|
|
712
721
|
if (isTextNode(node)) {
|
|
713
722
|
if (cursor instanceof Text) {
|
|
714
723
|
node._parent = vParent
|
|
@@ -878,17 +887,7 @@ function insert(
|
|
|
878
887
|
if (isFragmentNode(node)) {
|
|
879
888
|
// Insert fragment children in order before the same anchor
|
|
880
889
|
for (let child of node._children) {
|
|
881
|
-
cursor = insert(
|
|
882
|
-
child,
|
|
883
|
-
domParent,
|
|
884
|
-
frame,
|
|
885
|
-
scheduler,
|
|
886
|
-
styles,
|
|
887
|
-
vParent,
|
|
888
|
-
rootTarget,
|
|
889
|
-
anchor,
|
|
890
|
-
cursor,
|
|
891
|
-
)
|
|
890
|
+
cursor = insert(child, domParent, frame, scheduler, styles, node, rootTarget, anchor, cursor)
|
|
892
891
|
}
|
|
893
892
|
return cursor
|
|
894
893
|
}
|
|
@@ -1275,6 +1274,8 @@ export function renderComponent(
|
|
|
1275
1274
|
anchor?: Node,
|
|
1276
1275
|
cursor?: Node | null,
|
|
1277
1276
|
): Node | null | undefined {
|
|
1277
|
+
if (handle.isRemoved()) return cursor
|
|
1278
|
+
|
|
1278
1279
|
let [element, tasks] = handle.render(next.props)
|
|
1279
1280
|
let content = toVNode(element)
|
|
1280
1281
|
let newCursor = diffVNodes(
|
|
@@ -2020,15 +2021,23 @@ function shouldDispatchInlineMixinLifecycle(node: Node): boolean {
|
|
|
2020
2021
|
return true
|
|
2021
2022
|
}
|
|
2022
2023
|
|
|
2023
|
-
export function findNextSiblingDomAnchor(curr: VNode
|
|
2024
|
+
export function findNextSiblingDomAnchor(curr: VNode): Node | null {
|
|
2025
|
+
let vParent = curr._parent
|
|
2024
2026
|
if (!vParent || !Array.isArray(vParent._children)) return null
|
|
2025
2027
|
let children = vParent._children
|
|
2028
|
+
if (children.length === 0) return findNextSiblingDomAnchor(vParent)
|
|
2029
|
+
|
|
2026
2030
|
let idx = children.indexOf(curr)
|
|
2027
2031
|
if (idx === -1) return null
|
|
2028
2032
|
for (let i = idx + 1; i < children.length; i++) {
|
|
2029
2033
|
let dom = findFirstDomAnchor(children[i])
|
|
2030
2034
|
if (dom) return dom
|
|
2031
2035
|
}
|
|
2036
|
+
|
|
2037
|
+
if (isFragmentNode(vParent)) {
|
|
2038
|
+
return findNextSiblingDomAnchor(vParent)
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2032
2041
|
return null
|
|
2033
2042
|
}
|
|
2034
2043
|
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -17,7 +17,23 @@ type SchedulerPhaseListener = EventListenerOrEventListenerObject | null
|
|
|
17
17
|
/**
|
|
18
18
|
* Scheduler API used by the reconciler and frame runtime.
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
20
|
+
export interface Scheduler {
|
|
21
|
+
enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void
|
|
22
|
+
enqueueWork(newTasks: EmptyFn[]): void
|
|
23
|
+
enqueueCommitPhase(newTasks: EmptyFn[]): void
|
|
24
|
+
enqueueTasks(newTasks: EmptyFn[]): void
|
|
25
|
+
addEventListener(
|
|
26
|
+
type: SchedulerPhaseType,
|
|
27
|
+
listener: SchedulerPhaseListener,
|
|
28
|
+
options?: AddEventListenerOptions | boolean,
|
|
29
|
+
): void
|
|
30
|
+
removeEventListener(
|
|
31
|
+
type: SchedulerPhaseType,
|
|
32
|
+
listener: SchedulerPhaseListener,
|
|
33
|
+
options?: EventListenerOptions | boolean,
|
|
34
|
+
): void
|
|
35
|
+
dequeue(): void
|
|
36
|
+
}
|
|
21
37
|
|
|
22
38
|
// Protect against infinite cascading updates (e.g. handle.update() during render)
|
|
23
39
|
const MAX_CASCADING_UPDATES = 50
|
|
@@ -38,7 +54,7 @@ export function createScheduler(
|
|
|
38
54
|
doc: Document,
|
|
39
55
|
rootTarget: EventTarget,
|
|
40
56
|
styles: StyleManager = defaultStyleManager,
|
|
41
|
-
) {
|
|
57
|
+
): Scheduler {
|
|
42
58
|
let documentState = createDocumentState(doc)
|
|
43
59
|
let scheduled = new Map<CommittedComponentNode, ParentNode>()
|
|
44
60
|
let workTasks: EmptyFn[] = []
|
|
@@ -53,23 +69,7 @@ export function createScheduler(
|
|
|
53
69
|
beforeUpdate: 0,
|
|
54
70
|
commit: 0,
|
|
55
71
|
}
|
|
56
|
-
let scheduler:
|
|
57
|
-
enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void
|
|
58
|
-
enqueueWork(newTasks: EmptyFn[]): void
|
|
59
|
-
enqueueCommitPhase(newTasks: EmptyFn[]): void
|
|
60
|
-
enqueueTasks(newTasks: EmptyFn[]): void
|
|
61
|
-
addEventListener(
|
|
62
|
-
type: SchedulerPhaseType,
|
|
63
|
-
listener: SchedulerPhaseListener,
|
|
64
|
-
options?: AddEventListenerOptions | boolean,
|
|
65
|
-
): void
|
|
66
|
-
removeEventListener(
|
|
67
|
-
type: SchedulerPhaseType,
|
|
68
|
-
listener: SchedulerPhaseListener,
|
|
69
|
-
options?: EventListenerOptions | boolean,
|
|
70
|
-
): void
|
|
71
|
-
dequeue(): void
|
|
72
|
-
}
|
|
72
|
+
let scheduler: Scheduler
|
|
73
73
|
|
|
74
74
|
function dispatchError(error: unknown) {
|
|
75
75
|
console.error(error)
|
|
@@ -137,7 +137,7 @@ export function createScheduler(
|
|
|
137
137
|
// Needed for fragment self-updates that add children - without this, new children
|
|
138
138
|
// would be appended after siblings. The keyed diff has placement logic, but unkeyed
|
|
139
139
|
// diff relies on anchor for correct positioning.
|
|
140
|
-
let anchor = findNextSiblingDomAnchor(vnode
|
|
140
|
+
let anchor = findNextSiblingDomAnchor(vnode) || undefined
|
|
141
141
|
try {
|
|
142
142
|
let updateStyles = getFrameStyleManager(vnode)
|
|
143
143
|
renderComponent(
|
package/src/runtime/to-vnode.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Fragment } from './component.ts'
|
|
|
2
2
|
import { invariant } from './invariant.ts'
|
|
3
3
|
import { isEmptyChild, isPrimitiveChild, normalizeChildren } from './core/children.ts'
|
|
4
4
|
import type { RemixElement, RemixNode } from './jsx.ts'
|
|
5
|
-
import { isRemixElement, TEXT_NODE, type VNode } from './vnode.ts'
|
|
5
|
+
import { isRemixElement, NON_RENDER_NODE, TEXT_NODE, type VNode } from './vnode.ts'
|
|
6
6
|
|
|
7
7
|
function flatMapChildrenToVNodes(node: RemixElement): VNode[] {
|
|
8
8
|
if (!('children' in node.props)) return []
|
|
@@ -21,7 +21,7 @@ function flattenChildrenToVNodes(nodes: RemixNode[], out: VNode[]): void {
|
|
|
21
21
|
|
|
22
22
|
export function toVNode(node: RemixNode): VNode {
|
|
23
23
|
if (isEmptyChild(node)) {
|
|
24
|
-
return { type:
|
|
24
|
+
return { type: NON_RENDER_NODE }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (isPrimitiveChild(node)) {
|
package/src/runtime/vnode.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ElementProps, RemixNode } from './jsx.ts'
|
|
|
6
6
|
export { isRemixElement }
|
|
7
7
|
|
|
8
8
|
export const TEXT_NODE = Symbol('TEXT_NODE')
|
|
9
|
+
export const NON_RENDER_NODE = Symbol('NON_RENDER_NODE')
|
|
9
10
|
export const ROOT_VNODE = Symbol('ROOT_VNODE')
|
|
10
11
|
|
|
11
12
|
export type VNodeType =
|
|
@@ -13,6 +14,7 @@ export type VNodeType =
|
|
|
13
14
|
| string // host element
|
|
14
15
|
| Function // component
|
|
15
16
|
| typeof TEXT_NODE
|
|
17
|
+
| typeof NON_RENDER_NODE
|
|
16
18
|
| typeof Fragment
|
|
17
19
|
| typeof Frame
|
|
18
20
|
|
|
@@ -66,6 +68,10 @@ export type TextNode = VNode & {
|
|
|
66
68
|
_text: string
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
export type NonRenderNode = VNode & {
|
|
72
|
+
type: typeof NON_RENDER_NODE
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
export type CommittedTextNode = TextNode & {
|
|
70
76
|
_dom: Text
|
|
71
77
|
}
|
|
@@ -102,6 +108,10 @@ export function isTextNode(node: VNode): node is TextNode {
|
|
|
102
108
|
return node.type === TEXT_NODE
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
export function isNonRenderNode(node: VNode): node is NonRenderNode {
|
|
112
|
+
return node.type === NON_RENDER_NODE
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
export function isCommittedTextNode(node: VNode): node is CommittedTextNode {
|
|
106
116
|
return isTextNode(node) && node._dom instanceof Text
|
|
107
117
|
}
|
package/src/server/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Server
|
|
2
2
|
|
|
3
|
-
Remix
|
|
3
|
+
Remix UI can render to HTML on the server using two APIs:
|
|
4
4
|
|
|
5
5
|
- `renderToString` - Returns a complete HTML string. Simple, but buffers the entire response.
|
|
6
6
|
- `renderToStream` - Returns a `ReadableStream<Uint8Array>`. Sends the initial HTML immediately and streams frame content as it resolves.
|
|
@@ -26,6 +26,7 @@ import { renderToStream } from 'remix/ui/server'
|
|
|
26
26
|
|
|
27
27
|
let stream = renderToStream(<App />, {
|
|
28
28
|
frameSrc: request.url,
|
|
29
|
+
signal: request.signal,
|
|
29
30
|
resolveFrame(src, _target, context) {
|
|
30
31
|
let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
|
|
31
32
|
return fetchHtml(frameUrl)
|
|
@@ -44,6 +45,7 @@ return new Response(stream, {
|
|
|
44
45
|
|
|
45
46
|
- **`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
47
|
- **`topFrameSrc`** - Overrides the root frame URL used for `handle.frames.top.src`. This is mainly useful when calling `renderToStream()` from inside `resolveFrame()` for a nested frame render.
|
|
48
|
+
- **`signal`** - Cancels pending server rendering work. Pass `request.signal` so client disconnects can stop unresolved frame work without invoking `onError` for the disconnect itself.
|
|
47
49
|
- **`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
50
|
- **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
|
|
49
51
|
|
|
@@ -80,7 +82,7 @@ function ProductPage() {
|
|
|
80
82
|
|
|
81
83
|
## CSS
|
|
82
84
|
|
|
83
|
-
Components using the `css`
|
|
85
|
+
Components using the `css(...)` mixin through `mix` 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
86
|
|
|
85
87
|
## See Also
|
|
86
88
|
|
package/src/server/stream.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SELF_CLOSING_TAGS,
|
|
8
8
|
normalizeAttributeName,
|
|
9
9
|
serializeStyleObject,
|
|
10
|
+
shouldStringifyBooleanAttribute,
|
|
10
11
|
} from '../runtime/core/attributes.ts'
|
|
11
12
|
import { appendFlushMarker, type FlushKind, stripFlushMarkers } from '../runtime/stream-protocol.ts'
|
|
12
13
|
import { REMIX_UI_STYLE_LAYER } from '../style/layers.ts'
|
|
@@ -31,6 +32,8 @@ export interface RenderToStreamOptions {
|
|
|
31
32
|
frameSrc?: string | URL
|
|
32
33
|
/** Source URL for the top-level frame in nested frame renders. */
|
|
33
34
|
topFrameSrc?: string | URL
|
|
35
|
+
/** Signal that cancels pending server rendering work. */
|
|
36
|
+
signal?: AbortSignal
|
|
34
37
|
/** Error hook invoked when rendering work throws. */
|
|
35
38
|
onError?: (error: unknown) => void
|
|
36
39
|
/** Callback used to resolve nested frame content during streaming SSR. */
|
|
@@ -97,6 +100,7 @@ interface RenderContext {
|
|
|
97
100
|
unresolvedHydrationData: Map<string, UnresolvedHydrationData>
|
|
98
101
|
frameData: Map<string, FrameData>
|
|
99
102
|
blockingFrameTails: ReadableStream<Uint8Array>[]
|
|
103
|
+
signal: AbortSignal
|
|
100
104
|
flushKind: FlushKind
|
|
101
105
|
serverIdScope: string
|
|
102
106
|
serverIdCounter: number
|
|
@@ -192,6 +196,7 @@ export function renderToStream(
|
|
|
192
196
|
let currentFrameSrc = normalizeFrameSrc(options?.frameSrc ?? options?.topFrameSrc)
|
|
193
197
|
let topFrameSrc = normalizeFrameSrc(options?.topFrameSrc ?? currentFrameSrc)
|
|
194
198
|
let rootFrameState = createSsrFrameState(currentFrameSrc, topFrameSrc)
|
|
199
|
+
let renderAbortController = new AbortController()
|
|
195
200
|
|
|
196
201
|
let context: RenderContext = {
|
|
197
202
|
insideSvg: false,
|
|
@@ -204,21 +209,38 @@ export function renderToStream(
|
|
|
204
209
|
unresolvedHydrationData: new Map(),
|
|
205
210
|
frameData: new Map(),
|
|
206
211
|
blockingFrameTails: [],
|
|
212
|
+
signal: renderAbortController.signal,
|
|
207
213
|
flushKind: 'fragment',
|
|
208
214
|
serverIdScope: crypto.randomUUID().slice(0, 8),
|
|
209
215
|
serverIdCounter: 0,
|
|
210
216
|
}
|
|
211
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
|
+
|
|
212
231
|
return new ReadableStream({
|
|
213
232
|
async start(controller) {
|
|
214
233
|
try {
|
|
215
234
|
let root = buildSegment(node, context, rootFrameState)
|
|
216
235
|
await resolveBlocking(root)
|
|
236
|
+
if (closeIfCancelled(controller, context)) return
|
|
217
237
|
await resolveClientEntries(context, options?.resolveClientEntry)
|
|
238
|
+
if (closeIfCancelled(controller, context)) return
|
|
218
239
|
validateClientEntriesForHydration(context)
|
|
219
240
|
let html = serializeSegment(root)
|
|
220
241
|
let finalHtml = finalizeHtml(html, context)
|
|
221
242
|
let bytes = encoder.encode(appendFlushMarker(finalHtml, context.flushKind))
|
|
243
|
+
if (closeIfCancelled(controller, context)) return
|
|
222
244
|
controller.enqueue(bytes)
|
|
223
245
|
|
|
224
246
|
// If we have any tails from blocking frame streams, stream them now.
|
|
@@ -226,7 +248,7 @@ export function renderToStream(
|
|
|
226
248
|
// that must come after the initial document chunk.
|
|
227
249
|
let tailPromise =
|
|
228
250
|
context.blockingFrameTails.length > 0
|
|
229
|
-
? streamByteStreams(context.blockingFrameTails, controller, context
|
|
251
|
+
? streamByteStreams(context.blockingFrameTails, controller, context)
|
|
230
252
|
: Promise.resolve()
|
|
231
253
|
|
|
232
254
|
// If we have pending non-blocking frames, stream them as they resolve
|
|
@@ -237,15 +259,44 @@ export function renderToStream(
|
|
|
237
259
|
|
|
238
260
|
await Promise.all([tailPromise, pendingPromise])
|
|
239
261
|
|
|
262
|
+
if (closeIfCancelled(controller, context)) return
|
|
240
263
|
controller.close()
|
|
241
264
|
} catch (error) {
|
|
265
|
+
if (isSignalAbortError(context.signal, error)) {
|
|
266
|
+
closeStream(controller)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
242
269
|
onError(error)
|
|
243
270
|
controller.error(error)
|
|
244
271
|
}
|
|
245
272
|
},
|
|
273
|
+
cancel(reason) {
|
|
274
|
+
cancel(reason)
|
|
275
|
+
},
|
|
246
276
|
})
|
|
247
277
|
}
|
|
248
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
|
+
|
|
249
300
|
function defaultResolveFrame(): never {
|
|
250
301
|
throw new Error('No resolveFrame provided')
|
|
251
302
|
}
|
|
@@ -428,6 +479,9 @@ function buildFrameSegment(props: any, context: RenderContext, frameState: SsrFr
|
|
|
428
479
|
let framePromise = Promise.resolve(
|
|
429
480
|
context.resolveFrame(props.src, props.name, resolveFrameContext),
|
|
430
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(() => {})
|
|
431
485
|
context.pendingFrames.push({ frameId, promise: framePromise })
|
|
432
486
|
} else {
|
|
433
487
|
seg.pending = Promise.resolve(
|
|
@@ -529,11 +583,15 @@ function renderAttributes(props: any, isSvg: boolean, excludedProps?: Set<string
|
|
|
529
583
|
if (excludedProps?.has(key)) continue
|
|
530
584
|
|
|
531
585
|
let value = props[key]
|
|
532
|
-
if (value === undefined || value === null || value === false) continue
|
|
533
|
-
|
|
534
586
|
let attrName = transformAttributeName(key, isSvg)
|
|
587
|
+
let shouldStringifyBoolean = shouldStringifyBooleanAttribute(attrName)
|
|
588
|
+
if (value === undefined || value === null || (value === false && !shouldStringifyBoolean)) {
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
535
591
|
|
|
536
|
-
if (value ===
|
|
592
|
+
if (typeof value === 'boolean' && shouldStringifyBoolean) {
|
|
593
|
+
attrs += ` ${attrName}="${escapeHtml(String(value))}"`
|
|
594
|
+
} else if (value === true) {
|
|
537
595
|
attrs += ` ${attrName}`
|
|
538
596
|
} else {
|
|
539
597
|
attrs += ` ${attrName}="${escapeHtml(String(value))}"`
|
|
@@ -1248,26 +1306,33 @@ async function streamPendingFrames(
|
|
|
1248
1306
|
let processedFrames = new Set<string>()
|
|
1249
1307
|
|
|
1250
1308
|
while (true) {
|
|
1309
|
+
if (context.signal.aborted) break
|
|
1310
|
+
|
|
1251
1311
|
let batch = context.pendingFrames.filter(({ frameId }) => !processedFrames.has(frameId))
|
|
1252
1312
|
if (batch.length === 0) break
|
|
1253
1313
|
|
|
1254
1314
|
await Promise.all(
|
|
1255
1315
|
batch.map(async ({ frameId, promise }) => {
|
|
1316
|
+
if (context.signal.aborted) return
|
|
1256
1317
|
processedFrames.add(frameId)
|
|
1257
1318
|
try {
|
|
1258
1319
|
let { html, tail } = await promise
|
|
1320
|
+
if (context.signal.aborted) return
|
|
1259
1321
|
html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
|
|
1260
1322
|
|
|
1261
1323
|
// Stream as a template element (first chunk only)
|
|
1262
1324
|
let templateHtml = `<template id="${frameId}">${escapeTemplateContent(html)}</template>`
|
|
1325
|
+
if (context.signal.aborted) return
|
|
1263
1326
|
controller.enqueue(encoder.encode(templateHtml))
|
|
1264
1327
|
|
|
1265
1328
|
// Forward any additional chunks from a stream-valued resolveFrame result.
|
|
1266
1329
|
if (tail) {
|
|
1267
|
-
await streamByteStreams([tail], controller, context
|
|
1330
|
+
await streamByteStreams([tail], controller, context)
|
|
1268
1331
|
}
|
|
1269
1332
|
} catch (error) {
|
|
1270
|
-
context.
|
|
1333
|
+
if (!isSignalAbortError(context.signal, error)) {
|
|
1334
|
+
context.onError(error)
|
|
1335
|
+
}
|
|
1271
1336
|
}
|
|
1272
1337
|
}),
|
|
1273
1338
|
)
|
|
@@ -1277,19 +1342,23 @@ async function streamPendingFrames(
|
|
|
1277
1342
|
async function streamByteStreams(
|
|
1278
1343
|
streams: ReadableStream<Uint8Array>[],
|
|
1279
1344
|
controller: ReadableStreamDefaultController,
|
|
1280
|
-
|
|
1345
|
+
context: RenderContext,
|
|
1281
1346
|
): Promise<void> {
|
|
1282
1347
|
await Promise.all(
|
|
1283
1348
|
streams.map(async (stream) => {
|
|
1284
1349
|
let reader = stream.getReader()
|
|
1285
1350
|
try {
|
|
1286
1351
|
while (true) {
|
|
1352
|
+
if (context.signal.aborted) break
|
|
1287
1353
|
let { done, value } = await reader.read()
|
|
1288
1354
|
if (done) break
|
|
1355
|
+
if (context.signal.aborted) break
|
|
1289
1356
|
controller.enqueue(value)
|
|
1290
1357
|
}
|
|
1291
1358
|
} catch (error) {
|
|
1292
|
-
|
|
1359
|
+
if (!isSignalAbortError(context.signal, error)) {
|
|
1360
|
+
context.onError(error)
|
|
1361
|
+
}
|
|
1293
1362
|
} finally {
|
|
1294
1363
|
reader.releaseLock()
|
|
1295
1364
|
}
|
package/src/style/css-mixin.ts
CHANGED
|
@@ -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
|
|
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 = {}
|
package/src/style/index.ts
CHANGED
|
@@ -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'
|
package/src/style/stylesheet.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { REMIX_UI_STYLE_LAYER } from './layers.ts'
|
|
2
2
|
|
|
3
3
|
type RuleEntry = { count: number; index: number }
|
|
4
|
-
type ServerStyleSource = ParentNode | Iterable<Node>
|
|
4
|
+
export type ServerStyleSource = ParentNode | Iterable<Node>
|
|
5
|
+
|
|
6
|
+
export interface StyleManager {
|
|
7
|
+
insert(className: string, rule: string): void
|
|
8
|
+
remove(className: string): void
|
|
9
|
+
has(className: string): boolean
|
|
10
|
+
getGeneration(): number
|
|
11
|
+
reset(): void
|
|
12
|
+
adoptServerStyles(source: ServerStyleSource): Set<string>
|
|
13
|
+
replaceServerStyles(source: ServerStyleSource): void
|
|
14
|
+
selectors(): IterableIterator<string>
|
|
15
|
+
dispose(): void
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
const SERVER_STYLE_SELECTOR = 'style[data-rmx]'
|
|
7
19
|
|
|
@@ -68,7 +80,7 @@ function getStyleSelector(styleEl: HTMLStyleElement): string | null {
|
|
|
68
80
|
return selector ? selector : null
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER) {
|
|
83
|
+
export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleManager {
|
|
72
84
|
let stylesheet: CSSStyleSheet | null = null
|
|
73
85
|
let generation = 0
|
|
74
86
|
|
package/src/theme/contract.ts
CHANGED
|
@@ -4,7 +4,38 @@ export interface ThemeVariableTree {
|
|
|
4
4
|
[key: string]: string | ThemeVariableTree
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type ThemeVariableGroup<key extends string> = Readonly<Record<key, string>>
|
|
8
|
+
type ThemeActionVariableGroup = ThemeVariableGroup<
|
|
9
|
+
'background' | 'backgroundHover' | 'backgroundActive' | 'foreground' | 'border'
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
type ThemeVariableNames = {
|
|
13
|
+
readonly space: ThemeVariableGroup<'none' | 'px' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>
|
|
14
|
+
readonly radius: ThemeVariableGroup<'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>
|
|
15
|
+
readonly fontFamily: ThemeVariableGroup<'sans' | 'mono'>
|
|
16
|
+
readonly fontSize: ThemeVariableGroup<'xxxs' | 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>
|
|
17
|
+
readonly lineHeight: ThemeVariableGroup<'tight' | 'normal' | 'relaxed'>
|
|
18
|
+
readonly letterSpacing: ThemeVariableGroup<'tight' | 'normal' | 'meta' | 'wide'>
|
|
19
|
+
readonly fontWeight: ThemeVariableGroup<'normal' | 'medium' | 'semibold' | 'bold'>
|
|
20
|
+
readonly control: {
|
|
21
|
+
readonly height: ThemeVariableGroup<'sm' | 'md' | 'lg'>
|
|
22
|
+
}
|
|
23
|
+
readonly surface: ThemeVariableGroup<'lvl0' | 'lvl1' | 'lvl2' | 'lvl3' | 'lvl4'>
|
|
24
|
+
readonly shadow: ThemeVariableGroup<'xs' | 'sm' | 'md' | 'lg' | 'xl'>
|
|
25
|
+
readonly colors: {
|
|
26
|
+
readonly text: ThemeVariableGroup<'primary' | 'secondary' | 'muted' | 'link'>
|
|
27
|
+
readonly border: ThemeVariableGroup<'subtle' | 'default' | 'strong'>
|
|
28
|
+
readonly focus: ThemeVariableGroup<'ring'>
|
|
29
|
+
readonly overlay: ThemeVariableGroup<'scrim'>
|
|
30
|
+
readonly action: {
|
|
31
|
+
readonly primary: ThemeActionVariableGroup
|
|
32
|
+
readonly secondary: ThemeActionVariableGroup
|
|
33
|
+
readonly danger: ThemeActionVariableGroup
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const themeVariableNamesSource: ThemeVariableNames = {
|
|
8
39
|
space: {
|
|
9
40
|
none: '--rmx-space-none',
|
|
10
41
|
px: '--rmx-space-px',
|
|
@@ -117,7 +148,9 @@ const themeVariableNames = {
|
|
|
117
148
|
},
|
|
118
149
|
},
|
|
119
150
|
},
|
|
120
|
-
}
|
|
151
|
+
} satisfies ThemeVariableTree
|
|
152
|
+
|
|
153
|
+
export const themeVariableNames: ThemeVariableNames = themeVariableNamesSource
|
|
121
154
|
|
|
122
155
|
type MapLeaves<source, leaf> = source extends string
|
|
123
156
|
? leaf
|
|
@@ -126,6 +159,7 @@ type MapLeaves<source, leaf> = source extends string
|
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
export type ThemeValue = string | number
|
|
162
|
+
export type ThemeContract = MapLeaves<ThemeVariableNames, string>
|
|
129
163
|
export type ThemeValues = MapLeaves<typeof themeVariableNames, ThemeValue>
|
|
130
164
|
export type ThemeVars = Readonly<Record<string, string>>
|
|
131
165
|
export type CreateThemeOptions = {
|
|
@@ -146,7 +180,7 @@ export type ThemeComponent = ThemeRenderer & {
|
|
|
146
180
|
vars: ThemeVars
|
|
147
181
|
}
|
|
148
182
|
|
|
149
|
-
export const theme = createThemeContract(themeVariableNames)
|
|
183
|
+
export const theme: ThemeContract = createThemeContract(themeVariableNames)
|
|
150
184
|
|
|
151
185
|
export type ThemeUtility = ReturnType<typeof css>
|
|
152
186
|
|
|
@@ -158,8 +192,6 @@ type NestedThemeMix<value, depth extends number = 4> = depth extends 0
|
|
|
158
192
|
|
|
159
193
|
export type ThemeMix = NestedThemeMix<ThemeMixLeaf>
|
|
160
194
|
|
|
161
|
-
export { themeVariableNames }
|
|
162
|
-
|
|
163
195
|
function createThemeContract<tree extends ThemeVariableTree>(tree: tree): MapLeaves<tree, string> {
|
|
164
196
|
return mapTreeLeaves(tree, (variableName) => `var(${variableName})`) as MapLeaves<tree, string>
|
|
165
197
|
}
|
|
@@ -32,7 +32,9 @@ export type GlyphContract = Readonly<Record<GlyphName, { id: string }>>
|
|
|
32
32
|
|
|
33
33
|
const DEFAULT_GLYPH_ID_PREFIX = 'rmx-glyph'
|
|
34
34
|
|
|
35
|
-
export const glyphContract = Object.freeze(
|
|
35
|
+
export const glyphContract: GlyphContract = Object.freeze(
|
|
36
|
+
createGlyphContract(DEFAULT_GLYPH_ID_PREFIX),
|
|
37
|
+
)
|
|
36
38
|
|
|
37
39
|
function createGlyphIds(idPrefix: string): Record<GlyphName, string> {
|
|
38
40
|
return Object.fromEntries(glyphNames.map((name) => [name, `${idPrefix}-${name}`])) as Record<
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createGlyphSheet } from '../../../components/glyph/glyph.tsx'
|
|
2
2
|
import { createTheme } from '../../runtime.ts'
|
|
3
|
+
import type { GlyphSheetComponent } from '../../../components/glyph/glyph.tsx'
|
|
4
|
+
import type { ThemeComponent } from '../../contract.ts'
|
|
3
5
|
|
|
4
6
|
import { glyphValues } from './glyphs.tsx'
|
|
5
7
|
|
|
6
|
-
export const RMX_01 = createTheme({
|
|
8
|
+
export const RMX_01: ThemeComponent = createTheme({
|
|
7
9
|
space: {
|
|
8
10
|
none: '0px',
|
|
9
11
|
px: '1px',
|
|
@@ -118,4 +120,4 @@ export const RMX_01 = createTheme({
|
|
|
118
120
|
},
|
|
119
121
|
})
|
|
120
122
|
|
|
121
|
-
export const RMX_01_GLYPHS = createGlyphSheet(glyphValues)
|
|
123
|
+
export const RMX_01_GLYPHS: GlyphSheetComponent = createGlyphSheet(glyphValues)
|