@remix-run/ui 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- package/src/utils/wait.ts +5 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createMixin, on, type ElementProps } from '@remix-run/ui'
|
|
2
|
+
|
|
3
|
+
const HIDDEN_TYPEAHEAD_TIMEOUT = 750
|
|
4
|
+
|
|
5
|
+
export type SearchValue = string | string[]
|
|
6
|
+
|
|
7
|
+
type HiddenTypeaheadHandler = (text: string) => void
|
|
8
|
+
|
|
9
|
+
export const hiddenTypeahead = createMixin<
|
|
10
|
+
HTMLElement,
|
|
11
|
+
[onTypeahead: HiddenTypeaheadHandler],
|
|
12
|
+
ElementProps
|
|
13
|
+
>((handle) => {
|
|
14
|
+
let text = ''
|
|
15
|
+
let timeoutId = 0
|
|
16
|
+
|
|
17
|
+
function clearTypeahead() {
|
|
18
|
+
clearTimeout(timeoutId)
|
|
19
|
+
timeoutId = 0
|
|
20
|
+
text = ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function updateTypeahead(nextText: string, onTypeahead: HiddenTypeaheadHandler) {
|
|
24
|
+
text = nextText
|
|
25
|
+
onTypeahead(text)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handle.addEventListener('remove', clearTypeahead)
|
|
29
|
+
|
|
30
|
+
return (onTypeahead) => [
|
|
31
|
+
on('focusout', (event) => {
|
|
32
|
+
if (
|
|
33
|
+
event.relatedTarget instanceof Node &&
|
|
34
|
+
event.currentTarget.contains(event.relatedTarget)
|
|
35
|
+
) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clearTypeahead()
|
|
40
|
+
}),
|
|
41
|
+
on('keydown', (event) => {
|
|
42
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
43
|
+
clearTimeout(timeoutId)
|
|
44
|
+
updateTypeahead(text + event.key.toLowerCase(), onTypeahead)
|
|
45
|
+
timeoutId = window.setTimeout(clearTypeahead, HIDDEN_TYPEAHEAD_TIMEOUT)
|
|
46
|
+
} else if (event.key === 'Escape') {
|
|
47
|
+
clearTypeahead()
|
|
48
|
+
} else if (event.key === 'Backspace' && text.length > 0) {
|
|
49
|
+
clearTimeout(timeoutId)
|
|
50
|
+
updateTypeahead(text.slice(0, -1), onTypeahead)
|
|
51
|
+
timeoutId = window.setTimeout(clearTypeahead, HIDDEN_TYPEAHEAD_TIMEOUT)
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
]
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export function itemMatchesSearchText<item>(
|
|
58
|
+
item: item,
|
|
59
|
+
text: string,
|
|
60
|
+
getSearchValues: (item: item) => SearchValue,
|
|
61
|
+
) {
|
|
62
|
+
let values = getSearchValues(item)
|
|
63
|
+
let searchValues = Array.isArray(values) ? values : [values]
|
|
64
|
+
let normalizedText = text.toLowerCase()
|
|
65
|
+
|
|
66
|
+
return searchValues.some((value) => value.toLowerCase().startsWith(normalizedText))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function matchNextItemBySearchText<item>(
|
|
70
|
+
text: string,
|
|
71
|
+
items: item[],
|
|
72
|
+
options: {
|
|
73
|
+
fromIndex: number
|
|
74
|
+
getSearchValues: (item: item) => SearchValue
|
|
75
|
+
},
|
|
76
|
+
) {
|
|
77
|
+
if (text === '') {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let offset = 1; offset <= items.length; offset++) {
|
|
82
|
+
let item = items[(options.fromIndex + offset + items.length) % items.length]
|
|
83
|
+
if (itemMatchesSearchText(item, text, options.getSearchValues)) {
|
|
84
|
+
return item
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Handle, NoContext, RemixNode, RenderFn } from './component.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializable primitive types that can be passed as props to entry components
|
|
5
|
+
*/
|
|
6
|
+
export type SerializablePrimitive = string | number | boolean | null | undefined
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serializable object types that can be passed as props to entry components
|
|
10
|
+
*/
|
|
11
|
+
export type SerializableObject = {
|
|
12
|
+
[key: string]: SerializableValue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serializable array types that can be passed as props to entry components
|
|
17
|
+
*/
|
|
18
|
+
export type SerializableArray = SerializableValue[]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* All serializable values that can be passed as props to entry components.
|
|
22
|
+
* This includes primitives, objects, arrays, and Remix Elements.
|
|
23
|
+
*/
|
|
24
|
+
export type SerializableValue =
|
|
25
|
+
| SerializablePrimitive
|
|
26
|
+
| SerializableObject
|
|
27
|
+
| SerializableArray
|
|
28
|
+
| RemixNode
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Constraint that ensures all properties in an object are serializable.
|
|
32
|
+
*/
|
|
33
|
+
export type SerializableProps = {
|
|
34
|
+
[K in string]: SerializableValue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Metadata added to entry components
|
|
39
|
+
*/
|
|
40
|
+
export type EntryMetadata = {
|
|
41
|
+
$entry: true
|
|
42
|
+
$entryId: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An entry component preserves the exact function type with added metadata
|
|
47
|
+
*/
|
|
48
|
+
export type EntryComponent<props extends SerializableProps = {}, context = NoContext> = ((
|
|
49
|
+
handle: Handle<props, context>,
|
|
50
|
+
) => RenderFn<props>) &
|
|
51
|
+
EntryMetadata
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Marks a component as a client entry for client-side hydration.
|
|
55
|
+
*
|
|
56
|
+
* @param entryId Module URL with optional export name (format: "/js/module.js#ExportName") by
|
|
57
|
+
* default, or a custom entry identifier when paired with `resolveClientEntry`
|
|
58
|
+
* @param component Component function that will be hydrated on the client
|
|
59
|
+
* @returns The component augmented with entry metadata
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* export const Counter = clientEntry(
|
|
64
|
+
* '/js/counter.js#Counter',
|
|
65
|
+
* function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
|
|
66
|
+
* let count = handle.props.initialCount ?? 0
|
|
67
|
+
*
|
|
68
|
+
* return () => (
|
|
69
|
+
* <button
|
|
70
|
+
* type="button"
|
|
71
|
+
* mix={[
|
|
72
|
+
* on('click', () => {
|
|
73
|
+
* count++
|
|
74
|
+
* handle.update()
|
|
75
|
+
* }),
|
|
76
|
+
* ]}
|
|
77
|
+
* >
|
|
78
|
+
* {handle.props.label} {count}
|
|
79
|
+
* </button>
|
|
80
|
+
* )
|
|
81
|
+
* }
|
|
82
|
+
* )
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function clientEntry<props extends SerializableProps = {}, context = NoContext>(
|
|
86
|
+
entryId: string,
|
|
87
|
+
component: (handle: Handle<props, context>) => RenderFn,
|
|
88
|
+
): EntryComponent<props, context>
|
|
89
|
+
export function clientEntry<props extends SerializableProps = {}, context = NoContext>(
|
|
90
|
+
entryId: string,
|
|
91
|
+
component: (handle: Handle<Record<string, never>, context>) => RenderFn<props>,
|
|
92
|
+
): EntryComponent<props, context>
|
|
93
|
+
|
|
94
|
+
// Implementation
|
|
95
|
+
export function clientEntry(entryId: string, component: any): any {
|
|
96
|
+
if (!entryId) {
|
|
97
|
+
throw new Error('clientEntry() requires an entry ID')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Augment the component with entry metadata
|
|
101
|
+
component.$entry = true
|
|
102
|
+
component.$entryId = entryId
|
|
103
|
+
|
|
104
|
+
return component
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Type guard to check if a component is an entry component
|
|
109
|
+
*
|
|
110
|
+
* @param component The component to check
|
|
111
|
+
* @returns True if the component has entry metadata
|
|
112
|
+
*/
|
|
113
|
+
export function isEntry(component: unknown): component is EntryComponent {
|
|
114
|
+
return Boolean(component && typeof component === 'function' && (component as any).$entry === true)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Logs a client-hydration mismatch to the console.
|
|
119
|
+
*
|
|
120
|
+
* @param msg Message parts to forward to the logger.
|
|
121
|
+
*/
|
|
122
|
+
export function logHydrationMismatch(...msg: any[]) {
|
|
123
|
+
console.error('Hydration mismatch:', ...msg)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Advances a DOM cursor past consecutive comment nodes.
|
|
128
|
+
*
|
|
129
|
+
* @param cursor Starting DOM node.
|
|
130
|
+
* @returns The first non-comment node, or `null` when none remains.
|
|
131
|
+
*/
|
|
132
|
+
export function skipComments(cursor: Node | null): Node | null {
|
|
133
|
+
while (cursor && cursor.nodeType === Node.COMMENT_NODE) {
|
|
134
|
+
cursor = cursor.nextSibling
|
|
135
|
+
}
|
|
136
|
+
return cursor
|
|
137
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { ElementProps, ElementType, RemixNode, Renderable } from './jsx.ts'
|
|
2
|
+
import { TypedEventTarget } from './typed-event-target.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Task queued to run after a component update completes.
|
|
6
|
+
*/
|
|
7
|
+
export type Task = (signal: AbortSignal) => void
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runtime handle passed to component setup functions.
|
|
11
|
+
*/
|
|
12
|
+
export interface Handle<Props = Record<string, never>, ContextValue = NoContext> {
|
|
13
|
+
/**
|
|
14
|
+
* Stable identifier per component instance. Useful for HTML APIs like
|
|
15
|
+
* htmlFor, aria-owns, etc. so consumers don't have to supply an id.
|
|
16
|
+
*/
|
|
17
|
+
id: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stable props object for the component instance. The object identity does not
|
|
21
|
+
* change across updates, but its values are updated before each render.
|
|
22
|
+
*/
|
|
23
|
+
props: Props
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set and get values in an element tree for indirect ancestor/descendant
|
|
27
|
+
* communication.
|
|
28
|
+
*/
|
|
29
|
+
context: Context<ContextValue>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Schedules an update for the component to render again. Returns a promise
|
|
33
|
+
* that resolves with an AbortSignal after the update completes. The signal
|
|
34
|
+
* is aborted when the component re-renders or is removed.
|
|
35
|
+
*
|
|
36
|
+
* @returns A promise that resolves with an AbortSignal after the update
|
|
37
|
+
*/
|
|
38
|
+
update(): Promise<AbortSignal>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schedules a task to run after the next update.
|
|
42
|
+
*
|
|
43
|
+
* @param task
|
|
44
|
+
*/
|
|
45
|
+
queueTask(task: Task): void
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The component's closest frame
|
|
49
|
+
*/
|
|
50
|
+
frame: FrameHandle
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Access named frames in the current runtime tree.
|
|
54
|
+
*/
|
|
55
|
+
frames: {
|
|
56
|
+
/**
|
|
57
|
+
* The root frame for the current runtime tree.
|
|
58
|
+
*/
|
|
59
|
+
readonly top: FrameHandle
|
|
60
|
+
get(name: string): FrameHandle | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A signal indicating the connected status of the component. When the
|
|
65
|
+
* component is disconnected from the tree the signal will be aborted.
|
|
66
|
+
* Useful for setup scope cleanup.
|
|
67
|
+
*
|
|
68
|
+
* @example Clear a timer
|
|
69
|
+
* ```ts
|
|
70
|
+
* function Clock(handle: Handle) {
|
|
71
|
+
* let interval = setInterval(() => {
|
|
72
|
+
* if (handle.signal.aborted) {
|
|
73
|
+
* clearInterval(interval)
|
|
74
|
+
* return
|
|
75
|
+
* }
|
|
76
|
+
* handle.update()
|
|
77
|
+
* }, 1000)
|
|
78
|
+
* return () => <span>{new Date().toString()}</span>
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* Because signals are event targets, you can also add an event instead.
|
|
83
|
+
* ```ts
|
|
84
|
+
* function Clock(handle: Handle) {
|
|
85
|
+
* let interval = setInterval(handle.update)
|
|
86
|
+
* handle.signal.addEventListener("abort", () => clearInterval(interval))
|
|
87
|
+
* return () => <span>{new Date().toString()}</span>
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* You don't need to check both this.signal and a render/event signal as
|
|
92
|
+
* render/event signals are aborted when the component disconnects.
|
|
93
|
+
*/
|
|
94
|
+
signal: AbortSignal
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Default Handle context so types must be declared explicitly.
|
|
99
|
+
*/
|
|
100
|
+
export type NoContext = Record<string, never>
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Component factory shape used by the Remix component runtime.
|
|
104
|
+
*/
|
|
105
|
+
export type Component<Props = ElementProps, ContextValue = NoContext> = (
|
|
106
|
+
handle: Handle<Props, ContextValue>,
|
|
107
|
+
) => RenderFn
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Infers the context provided by a component or handle-compatible function.
|
|
111
|
+
*/
|
|
112
|
+
export type ContextFrom<ComponentType> =
|
|
113
|
+
ComponentType extends Component<any, infer Provided>
|
|
114
|
+
? Provided
|
|
115
|
+
: ComponentType extends (handle: Handle<any, infer Provided>, ...args: any[]) => any
|
|
116
|
+
? Provided
|
|
117
|
+
: never
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Context storage API exposed on component handles.
|
|
121
|
+
*/
|
|
122
|
+
export interface Context<C> {
|
|
123
|
+
/** Replaces the current context value for this component instance. */
|
|
124
|
+
set(values: C): void
|
|
125
|
+
/** Reads the context value associated with the given component type. */
|
|
126
|
+
get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
|
|
127
|
+
/** Reads the context value associated with the given component key. */
|
|
128
|
+
get(component: ElementType | symbol): unknown | undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Content that can be rendered into a frame.
|
|
133
|
+
*/
|
|
134
|
+
export type FrameContent = ReadableStream<Uint8Array> | string | RemixNode
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Events emitted by frame handles during reloads.
|
|
138
|
+
*/
|
|
139
|
+
export type FrameHandleEventMap = {
|
|
140
|
+
reloadStart: Event
|
|
141
|
+
reloadComplete: Event
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Public API for interacting with a frame instance.
|
|
146
|
+
*/
|
|
147
|
+
export type FrameHandle = TypedEventTarget<FrameHandleEventMap> & {
|
|
148
|
+
src: string
|
|
149
|
+
reload(): Promise<AbortSignal>
|
|
150
|
+
replace(content: FrameContent): Promise<void>
|
|
151
|
+
// Internal runtime context used by client-rendered Frame reconciliation.
|
|
152
|
+
$runtime?: unknown
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Props accepted by the built-in {@link Frame} component.
|
|
157
|
+
*/
|
|
158
|
+
export interface FrameProps {
|
|
159
|
+
/** Optional frame name used for targeted navigation and lookups. */
|
|
160
|
+
name?: string
|
|
161
|
+
/** Source URL used when the frame loads or reloads its content. */
|
|
162
|
+
src: string
|
|
163
|
+
/** Fallback content to render while the frame is pending. */
|
|
164
|
+
fallback?: Renderable
|
|
165
|
+
/** Event handlers invoked for events dispatched from the frame element. */
|
|
166
|
+
on?: Record<string, (event: Event, signal: AbortSignal) => void | Promise<void>>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Component factory function that receives a handle and returns a render function.
|
|
171
|
+
*/
|
|
172
|
+
export type ComponentFn<Props = Record<string, never>, ContextValue = NoContext> = (
|
|
173
|
+
handle: Handle<Props, ContextValue>,
|
|
174
|
+
) => RenderFn
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Render function returned by a component factory.
|
|
178
|
+
*/
|
|
179
|
+
export type RenderFn<Props = ElementProps> = (props: Props) => RemixNode
|
|
180
|
+
|
|
181
|
+
export type { RemixNode } from './jsx.ts'
|
|
182
|
+
|
|
183
|
+
// Handle is already exported as an interface above, no need to re-export
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Props accepted by the built-in {@link Fragment} component.
|
|
187
|
+
*/
|
|
188
|
+
export interface FragmentProps {
|
|
189
|
+
/** Child nodes to render without adding an extra host element. */
|
|
190
|
+
children?: RemixNode
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Mapping of built-in component names to their prop shapes.
|
|
195
|
+
*/
|
|
196
|
+
export interface BuiltinElements {
|
|
197
|
+
/** Props accepted by the built-in fragment component. */
|
|
198
|
+
Fragment: FragmentProps
|
|
199
|
+
/** Props accepted by the built-in frame component. */
|
|
200
|
+
Frame: FrameProps
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Key type used to stabilize host elements and components during reconciliation.
|
|
205
|
+
*/
|
|
206
|
+
export type Key = string | number | bigint
|
|
207
|
+
|
|
208
|
+
type ComponentConfig = {
|
|
209
|
+
id: string
|
|
210
|
+
type: Function
|
|
211
|
+
frame: FrameHandle
|
|
212
|
+
getContext: (type: Component) => unknown
|
|
213
|
+
getFrameByName: (name: string) => FrameHandle | undefined
|
|
214
|
+
getTopFrame?: () => FrameHandle | undefined
|
|
215
|
+
signal?: AbortSignal
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Runtime handle returned by {@link createComponent}.
|
|
220
|
+
*/
|
|
221
|
+
export type ComponentHandle = ReturnType<typeof createComponent>
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Creates the internal runtime wrapper for a component instance.
|
|
225
|
+
*
|
|
226
|
+
* @param config Component runtime configuration.
|
|
227
|
+
* @returns Component runtime helpers used by the reconciler.
|
|
228
|
+
*/
|
|
229
|
+
export function createComponent<C = NoContext>(config: ComponentConfig) {
|
|
230
|
+
let taskQueue: Task[] = []
|
|
231
|
+
let renderCtrl: AbortController | null = null
|
|
232
|
+
let connectedCtrl: AbortController | null = null
|
|
233
|
+
let contextValue: C | undefined = undefined
|
|
234
|
+
|
|
235
|
+
function getConnectedSignal() {
|
|
236
|
+
if (!connectedCtrl) connectedCtrl = new AbortController()
|
|
237
|
+
return connectedCtrl.signal
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let getContent: null | RenderFn = null
|
|
241
|
+
let scheduleUpdate: () => void = () => {
|
|
242
|
+
throw new Error('scheduleUpdate not implemented')
|
|
243
|
+
}
|
|
244
|
+
let props = {} as ElementProps
|
|
245
|
+
|
|
246
|
+
let context: Context<C> = {
|
|
247
|
+
set: (value: C) => {
|
|
248
|
+
contextValue = value
|
|
249
|
+
},
|
|
250
|
+
get: (type: Component) => config.getContext(type),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let handle: Handle<ElementProps, C> = {
|
|
254
|
+
id: config.id,
|
|
255
|
+
props,
|
|
256
|
+
update: () =>
|
|
257
|
+
new Promise((resolve) => {
|
|
258
|
+
taskQueue.push((signal) => resolve(signal))
|
|
259
|
+
scheduleUpdate()
|
|
260
|
+
}),
|
|
261
|
+
queueTask: (task: Task) => {
|
|
262
|
+
taskQueue.push(task)
|
|
263
|
+
},
|
|
264
|
+
frame: config.frame,
|
|
265
|
+
frames: {
|
|
266
|
+
get top() {
|
|
267
|
+
return config.getTopFrame?.() ?? config.frame
|
|
268
|
+
},
|
|
269
|
+
get(name: string) {
|
|
270
|
+
return config.getFrameByName(name)
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
context: context,
|
|
274
|
+
get signal() {
|
|
275
|
+
return config.signal ?? getConnectedSignal()
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function dequeueTasks(): (() => void)[] {
|
|
280
|
+
// Only create render controller if any task expects a signal (has length >= 1)
|
|
281
|
+
let needsSignal = taskQueue.some((task) => task.length >= 1)
|
|
282
|
+
if (needsSignal && !renderCtrl) {
|
|
283
|
+
renderCtrl = new AbortController()
|
|
284
|
+
}
|
|
285
|
+
let signal = renderCtrl?.signal
|
|
286
|
+
return taskQueue.splice(0, taskQueue.length).map((task) => () => task(signal!))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function render(props: ElementProps): [RemixNode, Array<() => void>] {
|
|
290
|
+
if (connectedCtrl?.signal.aborted) {
|
|
291
|
+
console.warn('render called after component was removed, potential application memory leak')
|
|
292
|
+
return [null, []]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Only abort render controller if it was initialized
|
|
296
|
+
if (renderCtrl) {
|
|
297
|
+
renderCtrl.abort()
|
|
298
|
+
renderCtrl = null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
syncProps(handle.props, props)
|
|
302
|
+
|
|
303
|
+
let renderContent = getContent
|
|
304
|
+
if (!renderContent) {
|
|
305
|
+
let result = config.type(handle)
|
|
306
|
+
if (typeof result !== 'function') {
|
|
307
|
+
let name = config.type.name || 'Anonymous'
|
|
308
|
+
throw new Error(`${name} must return a render function, received ${typeof result}`)
|
|
309
|
+
} else {
|
|
310
|
+
getContent = result
|
|
311
|
+
renderContent = result
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!renderContent) {
|
|
315
|
+
throw new Error('component render function was not initialized')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let node = renderContent(handle.props)
|
|
319
|
+
return [node, dequeueTasks()]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function remove(): (() => void)[] {
|
|
323
|
+
connectedCtrl?.abort()
|
|
324
|
+
renderCtrl?.abort()
|
|
325
|
+
return dequeueTasks()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function setScheduleUpdate(nextScheduleUpdate: () => void) {
|
|
329
|
+
scheduleUpdate = nextScheduleUpdate
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getContextValue(): C | undefined {
|
|
333
|
+
return contextValue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { render, remove, setScheduleUpdate, frame: config.frame, getContextValue }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function syncProps(target: ElementProps, next: ElementProps): void {
|
|
340
|
+
for (let key in target) {
|
|
341
|
+
if (!(key in next)) {
|
|
342
|
+
delete target[key]
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (let key in next) {
|
|
347
|
+
target[key] = next[key]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Built-in component used to render nested frame content.
|
|
353
|
+
*
|
|
354
|
+
* @param handle Component handle for the frame instance.
|
|
355
|
+
* @returns A placeholder render function handled by the reconciler.
|
|
356
|
+
*/
|
|
357
|
+
export function Frame(handle: Handle<FrameProps, FrameHandle>) {
|
|
358
|
+
void handle
|
|
359
|
+
return () => null // reconciler renders
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Built-in component used to group children without adding a host element.
|
|
364
|
+
*
|
|
365
|
+
* @param handle Component handle for the fragment instance.
|
|
366
|
+
* @returns A placeholder render function handled by the reconciler.
|
|
367
|
+
*/
|
|
368
|
+
export function Fragment(handle: Handle<FragmentProps>) {
|
|
369
|
+
void handle
|
|
370
|
+
return () => null // reconciler renders
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Creates a frame handle with default no-op implementations for testing and internal wiring.
|
|
375
|
+
*
|
|
376
|
+
* @param def Partial frame-handle implementation to merge with the defaults.
|
|
377
|
+
* @returns A frame handle object.
|
|
378
|
+
*/
|
|
379
|
+
export function createFrameHandle(
|
|
380
|
+
def?: Partial<{
|
|
381
|
+
src: string
|
|
382
|
+
replace: FrameHandle['replace']
|
|
383
|
+
reload: FrameHandle['reload']
|
|
384
|
+
$runtime: FrameHandle['$runtime']
|
|
385
|
+
}>,
|
|
386
|
+
): FrameHandle {
|
|
387
|
+
return Object.assign(
|
|
388
|
+
new TypedEventTarget<FrameHandleEventMap>(),
|
|
389
|
+
{
|
|
390
|
+
src: '/',
|
|
391
|
+
replace: notImplemented('replace not implemented'),
|
|
392
|
+
reload: notImplemented('reload not implemented'),
|
|
393
|
+
},
|
|
394
|
+
def,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function notImplemented(msg: string) {
|
|
399
|
+
return (): never => {
|
|
400
|
+
throw new Error(msg)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx } from './jsx.ts'
|
|
2
|
+
import type { ElementType, RemixElement } from './jsx.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Remix virtual element from a JSX-like call signature.
|
|
6
|
+
*
|
|
7
|
+
* @param type Host tag, component function, or mixin host placeholder.
|
|
8
|
+
* @param props Element props.
|
|
9
|
+
* @param children Child nodes.
|
|
10
|
+
* @returns A Remix virtual element.
|
|
11
|
+
*/
|
|
12
|
+
export function createElement(
|
|
13
|
+
type: ElementType,
|
|
14
|
+
props?: Record<string, any>,
|
|
15
|
+
...children: any[]
|
|
16
|
+
): RemixElement {
|
|
17
|
+
let nextProps = { ...(props ?? {}) }
|
|
18
|
+
if (isMixinElementType(type)) {
|
|
19
|
+
if (children.length > 0) {
|
|
20
|
+
console.error(new Error('mixin elements must not receive children'))
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
nextProps.children = children
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (nextProps.key != null) {
|
|
27
|
+
let { key, ...rest } = nextProps
|
|
28
|
+
return jsx(type, rest, key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return jsx(type, nextProps)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isMixinElementType(
|
|
35
|
+
type: ElementType,
|
|
36
|
+
): type is ((...args: unknown[]) => unknown) & { __rmxMixinElementType: string } {
|
|
37
|
+
return typeof type === 'function' && '__rmxMixinElementType' in type
|
|
38
|
+
}
|