@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.
Files changed (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. 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,2 @@
1
+ export * from './runtime/jsx.ts'
2
+ export { Fragment } from './runtime/component.ts'
@@ -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
+ }