@remix-run/ui 0.0.0 → 0.1.1

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 (272) 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 +920 -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 +32 -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 +28 -0
  124. package/dist/runtime/navigation.js +107 -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 +1262 -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 +195 -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 +46 -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 +1011 -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 +124 -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 +1218 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +397 -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 +148 -0
  247. package/src/runtime/reconcile.ts +1757 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +269 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +51 -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 +1347 -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/utils.ts +45 -0
  262. package/src/test.ts +2 -0
  263. package/src/theme/contract.ts +183 -0
  264. package/src/theme/glyph-contract.ts +57 -0
  265. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  266. package/src/theme/presets/rmx-01/index.ts +121 -0
  267. package/src/theme/runtime.ts +116 -0
  268. package/src/theme/theme.ts +15 -0
  269. package/src/utils/flash-attribute.ts +11 -0
  270. package/src/utils/scroll-lock.ts +97 -0
  271. package/src/utils/wait-for-css-transition.ts +66 -0
  272. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,101 @@
1
+ import type { RemixNode } from './jsx.ts'
2
+ import { createRoot, type VirtualRoot, type VirtualRootOptions } from './vdom.ts'
3
+
4
+ /**
5
+ * Options for {@link render}.
6
+ */
7
+ export interface RenderOptions extends VirtualRootOptions {
8
+ /**
9
+ * The element to mount the component into. Defaults to a fresh `div` appended to
10
+ * `document.body`.
11
+ */
12
+ container?: HTMLElement
13
+ }
14
+
15
+ /**
16
+ * Result returned by {@link render}.
17
+ */
18
+ export interface RenderResult {
19
+ /**
20
+ * The element the component is mounted into.
21
+ */
22
+ container: HTMLElement
23
+
24
+ /**
25
+ * The {@link VirtualRoot} the component is rendered in. Use it to access the
26
+ * underlying scheduler or dispatch lifecycle events.
27
+ */
28
+ root: VirtualRoot
29
+
30
+ /**
31
+ * Shorthand for `container.querySelector<HTMLElement>(selector)`.
32
+ */
33
+ $: (selector: string) => HTMLElement | null
34
+
35
+ /**
36
+ * Shorthand for `container.querySelectorAll<HTMLElement>(selector)`.
37
+ */
38
+ $$: (selector: string) => NodeListOf<HTMLElement>
39
+
40
+ /**
41
+ * Runs `fn` and then flushes any pending component updates so the DOM reflects them
42
+ * before the next assertion.
43
+ */
44
+ act: (fn: () => unknown | Promise<unknown>) => Promise<void>
45
+
46
+ /**
47
+ * Disposes the root and removes the container from the DOM. Pass to `t.after()` for
48
+ * automatic cleanup at the end of a test.
49
+ */
50
+ cleanup: () => void
51
+ }
52
+
53
+ /**
54
+ * Mounts a {@link RemixNode} into a DOM container for testing.
55
+ *
56
+ * @param node - The node to render
57
+ * @param opts - {@link RenderOptions}; pass `opts.container` to render into a specific
58
+ * element instead of a fresh `div` appended to `document.body`
59
+ * @returns A {@link RenderResult} with the container, root, and helpers for querying and
60
+ * interacting with the rendered output.
61
+ */
62
+ export function render(node: RemixNode, opts: RenderOptions = {}): RenderResult {
63
+ let { container: userContainer, ...virtualRootOpts } = opts
64
+
65
+ let container: HTMLElement | undefined
66
+ if (userContainer) {
67
+ container = userContainer
68
+ } else {
69
+ container = document.createElement('div')
70
+ document.body.appendChild(container)
71
+ }
72
+
73
+ let root: VirtualRoot | undefined = createRoot(container, virtualRootOpts)
74
+ root.render(node)
75
+ root.flush()
76
+
77
+ let ctx: RenderResult = {
78
+ get container() {
79
+ if (!container) throw new Error('Test container has already been cleaned up')
80
+ return container
81
+ },
82
+ get root() {
83
+ if (!root) throw new Error('Test root has already been cleaned up')
84
+ return root
85
+ },
86
+ $: (s: string) => ctx.container.querySelector<HTMLElement>(s),
87
+ $$: (s: string) => ctx.container.querySelectorAll<HTMLElement>(s),
88
+ async act(fn: () => unknown | Promise<unknown>) {
89
+ await fn()
90
+ ctx.root.flush()
91
+ },
92
+ cleanup() {
93
+ root?.dispose()
94
+ container?.remove()
95
+ container = undefined
96
+ root = undefined
97
+ },
98
+ }
99
+
100
+ return ctx
101
+ }
@@ -0,0 +1,100 @@
1
+ import { createFrame, type Frame } from './frame.ts'
2
+ import { createScheduler } from './vdom.ts'
3
+ import { createStyleManager } from '../style/index.ts'
4
+ import type { FrameHandle } from './component.ts'
5
+ import { createComponentErrorEvent } from './error-event.ts'
6
+ import type { ComponentErrorEvent } from './error-event.ts'
7
+ import type { LoadModule, ResolveFrame } from './frame.ts'
8
+ import { startNavigationListener } from './navigation.ts'
9
+ import { TypedEventTarget } from './typed-event-target.ts'
10
+
11
+ /**
12
+ * Options for starting the client runtime with {@link run}.
13
+ */
14
+ export type RunInit = {
15
+ loadModule: LoadModule
16
+ resolveFrame?: ResolveFrame
17
+ }
18
+
19
+ /**
20
+ * Events emitted by the application runtime.
21
+ */
22
+ export type AppRuntimeEventMap = {
23
+ error: ComponentErrorEvent
24
+ }
25
+
26
+ /**
27
+ * Client runtime returned by {@link run}.
28
+ */
29
+ export type AppRuntime = TypedEventTarget<AppRuntimeEventMap> & {
30
+ ready(): Promise<void>
31
+ flush(): void
32
+ dispose(): void
33
+ }
34
+
35
+ let topFrame: Frame
36
+ /**
37
+ * Returns the top-level frame handle for the running application.
38
+ *
39
+ * @returns The top-level frame handle.
40
+ */
41
+ export function getTopFrame(): FrameHandle {
42
+ if (!topFrame) throw new Error('app runtime not initialized')
43
+ return topFrame.handle
44
+ }
45
+
46
+ const namedFrames = new Map<string, FrameHandle>()
47
+ /**
48
+ * Returns a named frame handle, falling back to the top frame when not found.
49
+ *
50
+ * @param name Name of the frame to look up.
51
+ * @returns The matching frame handle or the top frame.
52
+ */
53
+ export function getNamedFrame(name: string): FrameHandle {
54
+ return namedFrames.get(name) ?? getTopFrame()
55
+ }
56
+
57
+ /**
58
+ * Starts the client-side Remix component runtime for the current document.
59
+ *
60
+ * @param init Runtime hooks for loading modules and resolving frames.
61
+ * @returns The running application runtime.
62
+ */
63
+ export function run(init: RunInit): AppRuntime {
64
+ let styleManager = createStyleManager()
65
+ let errorTarget = new TypedEventTarget<AppRuntimeEventMap>()
66
+ let scheduler = createScheduler(document, errorTarget, styleManager)
67
+
68
+ let resolveFrame: ResolveFrame = init.resolveFrame ?? (() => '<p>resolve frame unimplemented</p>')
69
+
70
+ topFrame = createFrame(document, {
71
+ src: document.location.href,
72
+ errorTarget,
73
+ loadModule: init.loadModule,
74
+ resolveFrame,
75
+ pendingClientEntries: new Map(),
76
+ scheduler,
77
+ styleManager,
78
+ data: {},
79
+ moduleCache: new Map(),
80
+ moduleLoads: new Map(),
81
+ frameInstances: new WeakMap(),
82
+ namedFrames,
83
+ })
84
+
85
+ let appController = new AbortController()
86
+ startNavigationListener(appController.signal)
87
+ let readyPromise = topFrame.ready().catch((error) => {
88
+ errorTarget.dispatchEvent(createComponentErrorEvent(error))
89
+ throw error
90
+ })
91
+
92
+ return Object.assign(errorTarget, {
93
+ ready: () => readyPromise,
94
+ flush: () => topFrame.flush(),
95
+ dispose: () => {
96
+ appController.abort()
97
+ topFrame.dispose()
98
+ },
99
+ })
100
+ }
@@ -0,0 +1,269 @@
1
+ import { createDocumentState } from './document-state.ts'
2
+ import { createComponentErrorEvent } from './error-event.ts'
3
+ import type { CommittedComponentNode, VNode } from './vnode.ts'
4
+ import { isCommittedComponentNode } from './vnode.ts'
5
+ import {
6
+ findNextSiblingDomAnchor,
7
+ renderComponent,
8
+ setActiveSchedulerUpdateParents,
9
+ } from './reconcile.ts'
10
+ import { defaultStyleManager } from './diff-props.ts'
11
+ import type { StyleManager } from '../style/index.ts'
12
+
13
+ type EmptyFn = () => void
14
+ type SchedulerPhaseType = 'beforeUpdate' | 'commit'
15
+ type SchedulerPhaseListener = EventListenerOrEventListenerObject | null
16
+
17
+ /**
18
+ * Scheduler API used by the reconciler and frame runtime.
19
+ */
20
+ export type Scheduler = ReturnType<typeof createScheduler>
21
+
22
+ // Protect against infinite cascading updates (e.g. handle.update() during render)
23
+ const MAX_CASCADING_UPDATES = 50
24
+
25
+ export type SchedulerPhaseEvent = Event & {
26
+ parents: ParentNode[]
27
+ }
28
+
29
+ /**
30
+ * Creates the DOM update scheduler used by the component runtime.
31
+ *
32
+ * @param doc Document associated with the rendered tree.
33
+ * @param rootTarget Event target that receives runtime errors.
34
+ * @param styles Style manager used during reconciliation.
35
+ * @returns A scheduler instance.
36
+ */
37
+ export function createScheduler(
38
+ doc: Document,
39
+ rootTarget: EventTarget,
40
+ styles: StyleManager = defaultStyleManager,
41
+ ) {
42
+ let documentState = createDocumentState(doc)
43
+ let scheduled = new Map<CommittedComponentNode, ParentNode>()
44
+ let workTasks: EmptyFn[] = []
45
+ let commitPhaseTasks: EmptyFn[] = []
46
+ let postCommitTasks: EmptyFn[] = []
47
+ let flushScheduled = false
48
+ let flushing = false
49
+ let cascadingUpdateCount = 0
50
+ let resetScheduled = false
51
+ let phaseEvents = new EventTarget()
52
+ let phaseListenerCounts: Record<SchedulerPhaseType, number> = {
53
+ beforeUpdate: 0,
54
+ commit: 0,
55
+ }
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
+ }
73
+
74
+ function dispatchError(error: unknown) {
75
+ console.error(error)
76
+ rootTarget.dispatchEvent(createComponentErrorEvent(error))
77
+ }
78
+
79
+ function scheduleCounterReset() {
80
+ if (resetScheduled) return
81
+ resetScheduled = true
82
+ // Reset when control returns to the event loop while still allowing
83
+ // microtask-driven flushes in the same turn to count as cascading.
84
+ setTimeout(() => {
85
+ cascadingUpdateCount = 0
86
+ resetScheduled = false
87
+ }, 0)
88
+ }
89
+
90
+ function getFrameStyleManager(vnode: CommittedComponentNode): StyleManager {
91
+ let runtime = vnode._handle?.frame.$runtime as { styleManager?: StyleManager } | undefined
92
+ return runtime?.styleManager ?? styles
93
+ }
94
+
95
+ function flush() {
96
+ if (flushing) return
97
+ flushing = true
98
+ try {
99
+ while (true) {
100
+ flushScheduled = false
101
+
102
+ let batch = new Map(scheduled)
103
+ scheduled.clear()
104
+
105
+ let hasWork =
106
+ batch.size > 0 ||
107
+ workTasks.length > 0 ||
108
+ commitPhaseTasks.length > 0 ||
109
+ postCommitTasks.length > 0
110
+ if (!hasWork) return
111
+
112
+ cascadingUpdateCount++
113
+ scheduleCounterReset()
114
+
115
+ if (cascadingUpdateCount > MAX_CASCADING_UPDATES) {
116
+ let error = new Error('handle.update() infinite loop detected')
117
+ dispatchError(error)
118
+ return
119
+ }
120
+
121
+ documentState.capture()
122
+
123
+ let updateParents = batch.size > 0 ? Array.from(new Set(batch.values())) : []
124
+ setActiveSchedulerUpdateParents(updateParents)
125
+ dispatchPhaseEvent('beforeUpdate', updateParents)
126
+
127
+ if (batch.size > 0) {
128
+ let vnodes = Array.from(batch)
129
+ let noScheduledAncestor = new Set<VNode>()
130
+
131
+ for (let [vnode, domParent] of vnodes) {
132
+ if (ancestorIsScheduled(vnode, batch, noScheduledAncestor)) continue
133
+ let handle = vnode._handle
134
+ let curr = vnode._content
135
+ let vParent = vnode._parent!
136
+ // Calculate anchor at render time from current vdom position (never stale).
137
+ // Needed for fragment self-updates that add children - without this, new children
138
+ // would be appended after siblings. The keyed diff has placement logic, but unkeyed
139
+ // diff relies on anchor for correct positioning.
140
+ let anchor = findNextSiblingDomAnchor(vnode, vParent) || undefined
141
+ try {
142
+ let updateStyles = getFrameStyleManager(vnode)
143
+ renderComponent(
144
+ handle,
145
+ curr,
146
+ vnode,
147
+ domParent,
148
+ handle.frame,
149
+ scheduler,
150
+ updateStyles,
151
+ rootTarget,
152
+ vParent,
153
+ anchor,
154
+ )
155
+ } catch (error) {
156
+ dispatchError(error)
157
+ }
158
+ }
159
+ }
160
+
161
+ flushTaskQueue(workTasks)
162
+ setActiveSchedulerUpdateParents(undefined)
163
+
164
+ // Restore selection before commit-phase lifecycle work so mixins see
165
+ // the final DOM state but still run before commit listeners and user tasks.
166
+ documentState.restore()
167
+
168
+ flushTaskQueue(commitPhaseTasks)
169
+ dispatchPhaseEvent('commit', updateParents)
170
+ flushTaskQueue(postCommitTasks)
171
+ }
172
+ } finally {
173
+ setActiveSchedulerUpdateParents(undefined)
174
+ flushing = false
175
+ }
176
+ }
177
+
178
+ function dispatchPhaseEvent(type: SchedulerPhaseType, parents: ParentNode[]) {
179
+ if (phaseListenerCounts[type] === 0) return
180
+ let event = new Event(type) as SchedulerPhaseEvent
181
+ event.parents = parents
182
+ phaseEvents.dispatchEvent(event)
183
+ }
184
+
185
+ function flushTaskQueue(queue: EmptyFn[]) {
186
+ while (queue.length > 0) {
187
+ let task = queue.shift()
188
+ if (!task) continue
189
+ try {
190
+ task()
191
+ } catch (error) {
192
+ dispatchError(error)
193
+ }
194
+ }
195
+ }
196
+
197
+ function scheduleFlush() {
198
+ if (flushScheduled || flushing) return
199
+ flushScheduled = true
200
+ queueMicrotask(flush)
201
+ }
202
+
203
+ function ancestorIsScheduled(
204
+ vnode: VNode,
205
+ batch: Map<CommittedComponentNode, ParentNode>,
206
+ safe: Set<VNode>,
207
+ ): boolean {
208
+ let path: VNode[] = []
209
+ let current = vnode._parent
210
+
211
+ while (current) {
212
+ // Already verified this node has no scheduled ancestor above it
213
+ if (safe.has(current)) {
214
+ for (let node of path) safe.add(node)
215
+ return false
216
+ }
217
+
218
+ path.push(current)
219
+
220
+ if (isCommittedComponentNode(current) && batch.has(current)) {
221
+ return true
222
+ }
223
+
224
+ current = current._parent
225
+ }
226
+
227
+ // Reached root - mark entire path as safe for future lookups
228
+ for (let node of path) safe.add(node)
229
+ return false
230
+ }
231
+
232
+ scheduler = {
233
+ enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void {
234
+ scheduled.set(vnode, domParent)
235
+ scheduleFlush()
236
+ },
237
+
238
+ enqueueWork(newTasks: EmptyFn[]): void {
239
+ workTasks.push(...newTasks)
240
+ scheduleFlush()
241
+ },
242
+
243
+ enqueueCommitPhase(newTasks: EmptyFn[]): void {
244
+ commitPhaseTasks.push(...newTasks)
245
+ scheduleFlush()
246
+ },
247
+
248
+ enqueueTasks(newTasks: EmptyFn[]): void {
249
+ postCommitTasks.push(...newTasks)
250
+ scheduleFlush()
251
+ },
252
+
253
+ addEventListener(type, listener, options) {
254
+ if (listener) phaseListenerCounts[type]++
255
+ phaseEvents.addEventListener(type, listener, options)
256
+ },
257
+
258
+ removeEventListener(type, listener, options) {
259
+ if (listener) phaseListenerCounts[type] = Math.max(0, phaseListenerCounts[type] - 1)
260
+ phaseEvents.removeEventListener(type, listener, options)
261
+ },
262
+
263
+ dequeue() {
264
+ flush()
265
+ },
266
+ }
267
+
268
+ return scheduler
269
+ }
@@ -0,0 +1,133 @@
1
+ const XLINK_NS = 'http://www.w3.org/1999/xlink'
2
+ const XML_NS = 'http://www.w3.org/XML/1998/namespace'
3
+
4
+ const CANONICAL_CAMEL_SVG_ATTRS = new Set([
5
+ 'accentHeight',
6
+ 'attributeName',
7
+ 'attributeType',
8
+ 'baseFrequency',
9
+ 'baseProfile',
10
+ 'calcMode',
11
+ 'viewBox',
12
+ 'preserveAspectRatio',
13
+ 'externalResourcesRequired',
14
+ 'filterRes',
15
+ 'gradientUnits',
16
+ 'gradientTransform',
17
+ 'glyphRef',
18
+ 'kernelMatrix',
19
+ 'kernelUnitLength',
20
+ 'keyPoints',
21
+ 'keySplines',
22
+ 'keyTimes',
23
+ 'lengthAdjust',
24
+ 'limitingConeAngle',
25
+ 'markerHeight',
26
+ 'patternUnits',
27
+ 'patternContentUnits',
28
+ 'patternTransform',
29
+ 'markerWidth',
30
+ 'numOctaves',
31
+ 'pathLength',
32
+ 'pointsAtX',
33
+ 'pointsAtY',
34
+ 'pointsAtZ',
35
+ 'preserveAlpha',
36
+ 'clipPathUnits',
37
+ 'maskUnits',
38
+ 'maskContentUnits',
39
+ 'filterUnits',
40
+ 'primitiveUnits',
41
+ 'refX',
42
+ 'refY',
43
+ 'requiredExtensions',
44
+ 'requiredFeatures',
45
+ 'specularConstant',
46
+ 'specularExponent',
47
+ 'spreadMethod',
48
+ 'startOffset',
49
+ 'stdDeviation',
50
+ 'stitchTiles',
51
+ 'surfaceScale',
52
+ 'systemLanguage',
53
+ 'tableValues',
54
+ 'targetX',
55
+ 'targetY',
56
+ 'textLength',
57
+ 'viewTarget',
58
+ 'xChannelSelector',
59
+ 'yChannelSelector',
60
+ 'zoomAndPan',
61
+ 'edgeMode',
62
+ 'diffuseConstant',
63
+ 'markerUnits',
64
+ ])
65
+
66
+ const SVG_ATTR_ALIASES = new Map<string, string>()
67
+ for (let attr of CANONICAL_CAMEL_SVG_ATTRS) {
68
+ SVG_ATTR_ALIASES.set(camelToKebab(attr), attr)
69
+ }
70
+
71
+ const NAMESPACED_SVG_ALIASES = new Map([
72
+ ['xlinkHref', { ns: XLINK_NS, attr: 'xlink:href' }],
73
+ ['xlink:href', { ns: XLINK_NS, attr: 'xlink:href' }],
74
+ ['xlink-href', { ns: XLINK_NS, attr: 'xlink:href' }],
75
+ ['xlinkActuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],
76
+ ['xlink:actuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],
77
+ ['xlink-actuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],
78
+ ['xlinkArcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],
79
+ ['xlink:arcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],
80
+ ['xlink-arcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],
81
+ ['xlinkRole', { ns: XLINK_NS, attr: 'xlink:role' }],
82
+ ['xlink:role', { ns: XLINK_NS, attr: 'xlink:role' }],
83
+ ['xlink-role', { ns: XLINK_NS, attr: 'xlink:role' }],
84
+ ['xlinkShow', { ns: XLINK_NS, attr: 'xlink:show' }],
85
+ ['xlink:show', { ns: XLINK_NS, attr: 'xlink:show' }],
86
+ ['xlink-show', { ns: XLINK_NS, attr: 'xlink:show' }],
87
+ ['xlinkTitle', { ns: XLINK_NS, attr: 'xlink:title' }],
88
+ ['xlink:title', { ns: XLINK_NS, attr: 'xlink:title' }],
89
+ ['xlink-title', { ns: XLINK_NS, attr: 'xlink:title' }],
90
+ ['xlinkType', { ns: XLINK_NS, attr: 'xlink:type' }],
91
+ ['xlink:type', { ns: XLINK_NS, attr: 'xlink:type' }],
92
+ ['xlink-type', { ns: XLINK_NS, attr: 'xlink:type' }],
93
+ ['xmlBase', { ns: XML_NS, attr: 'xml:base' }],
94
+ ['xml:base', { ns: XML_NS, attr: 'xml:base' }],
95
+ ['xml-base', { ns: XML_NS, attr: 'xml:base' }],
96
+ ['xmlLang', { ns: XML_NS, attr: 'xml:lang' }],
97
+ ['xml:lang', { ns: XML_NS, attr: 'xml:lang' }],
98
+ ['xml-lang', { ns: XML_NS, attr: 'xml:lang' }],
99
+ ['xmlSpace', { ns: XML_NS, attr: 'xml:space' }],
100
+ ['xml:space', { ns: XML_NS, attr: 'xml:space' }],
101
+ ['xml-space', { ns: XML_NS, attr: 'xml:space' }],
102
+ ['xmlnsXlink', { attr: 'xmlns:xlink' }],
103
+ ['xmlns:xlink', { attr: 'xmlns:xlink' }],
104
+ ['xmlns-xlink', { attr: 'xmlns:xlink' }],
105
+ ])
106
+
107
+ export function normalizeSvgAttributeName(name: string): string {
108
+ let alias = SVG_ATTR_ALIASES.get(name)
109
+ if (alias) return alias
110
+
111
+ if (CANONICAL_CAMEL_SVG_ATTRS.has(name)) return name
112
+
113
+ return camelToKebab(name)
114
+ }
115
+
116
+ export function normalizeSvgAttribute(name: string): {
117
+ ns?: string
118
+ attr: string
119
+ } {
120
+ let namespaced = NAMESPACED_SVG_ALIASES.get(name)
121
+ if (namespaced) {
122
+ return namespaced
123
+ }
124
+
125
+ return { attr: normalizeSvgAttributeName(name) }
126
+ }
127
+
128
+ function camelToKebab(input: string): string {
129
+ return input
130
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
131
+ .replace(/_/g, '-')
132
+ .toLowerCase()
133
+ }
@@ -0,0 +1,51 @@
1
+ import { Fragment } from './component.ts'
2
+ import { invariant } from './invariant.ts'
3
+ import type { RemixElement, RemixNode } from './jsx.ts'
4
+ import { isRemixElement, TEXT_NODE, type VNode } from './vnode.ts'
5
+
6
+ function flatMapChildrenToVNodes(node: RemixElement): VNode[] {
7
+ if (!('children' in node.props)) return []
8
+ let children = node.props.children
9
+ if (!Array.isArray(children)) return [toVNode(children)]
10
+ let vnodes: VNode[] = []
11
+ flattenChildrenToVNodes(children, vnodes)
12
+ return vnodes
13
+ }
14
+
15
+ function flattenChildrenToVNodes(nodes: RemixNode[], out: VNode[]): void {
16
+ for (let child of nodes) {
17
+ if (Array.isArray(child)) {
18
+ flattenChildrenToVNodes(child, out)
19
+ } else {
20
+ out.push(toVNode(child))
21
+ }
22
+ }
23
+ }
24
+
25
+ export function toVNode(node: RemixNode): VNode {
26
+ if (node === null || node === undefined || typeof node === 'boolean') {
27
+ return { type: TEXT_NODE, _text: '' }
28
+ }
29
+
30
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
31
+ return { type: TEXT_NODE, _text: String(node) }
32
+ }
33
+
34
+ if (Array.isArray(node)) {
35
+ let children: VNode[] = []
36
+ flattenChildrenToVNodes(node, children)
37
+ return { type: Fragment, _children: children }
38
+ }
39
+
40
+ if (node.type === Fragment) {
41
+ return { type: Fragment, key: node.key, _children: flatMapChildrenToVNodes(node) }
42
+ }
43
+
44
+ if (isRemixElement(node)) {
45
+ // When innerHTML is set, ignore children
46
+ let children = node.props.innerHTML != null ? [] : flatMapChildrenToVNodes(node)
47
+ return { type: node.type, key: node.key, props: node.props, _children: children }
48
+ }
49
+
50
+ invariant(false, 'Unexpected RemixNode')
51
+ }