@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,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,262 @@
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 scheduler: {
53
+ enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void
54
+ enqueueWork(newTasks: EmptyFn[]): void
55
+ enqueueCommitPhase(newTasks: EmptyFn[]): void
56
+ enqueueTasks(newTasks: EmptyFn[]): void
57
+ addEventListener(
58
+ type: SchedulerPhaseType,
59
+ listener: SchedulerPhaseListener,
60
+ options?: AddEventListenerOptions | boolean,
61
+ ): void
62
+ removeEventListener(
63
+ type: SchedulerPhaseType,
64
+ listener: SchedulerPhaseListener,
65
+ options?: EventListenerOptions | boolean,
66
+ ): void
67
+ dequeue(): void
68
+ }
69
+
70
+ function dispatchError(error: unknown) {
71
+ console.error(error)
72
+ rootTarget.dispatchEvent(createComponentErrorEvent(error))
73
+ }
74
+
75
+ function scheduleCounterReset() {
76
+ if (resetScheduled) return
77
+ resetScheduled = true
78
+ // Reset when control returns to the event loop while still allowing
79
+ // microtask-driven flushes in the same turn to count as cascading.
80
+ setTimeout(() => {
81
+ cascadingUpdateCount = 0
82
+ resetScheduled = false
83
+ }, 0)
84
+ }
85
+
86
+ function getFrameStyleManager(vnode: CommittedComponentNode): StyleManager {
87
+ let runtime = vnode._handle?.frame.$runtime as { styleManager?: StyleManager } | undefined
88
+ return runtime?.styleManager ?? styles
89
+ }
90
+
91
+ function flush() {
92
+ if (flushing) return
93
+ flushing = true
94
+ try {
95
+ while (true) {
96
+ flushScheduled = false
97
+
98
+ let batch = new Map(scheduled)
99
+ scheduled.clear()
100
+
101
+ let hasWork =
102
+ batch.size > 0 ||
103
+ workTasks.length > 0 ||
104
+ commitPhaseTasks.length > 0 ||
105
+ postCommitTasks.length > 0
106
+ if (!hasWork) return
107
+
108
+ cascadingUpdateCount++
109
+ scheduleCounterReset()
110
+
111
+ if (cascadingUpdateCount > MAX_CASCADING_UPDATES) {
112
+ let error = new Error('handle.update() infinite loop detected')
113
+ dispatchError(error)
114
+ return
115
+ }
116
+
117
+ documentState.capture()
118
+
119
+ let updateParents = batch.size > 0 ? Array.from(new Set(batch.values())) : []
120
+ setActiveSchedulerUpdateParents(updateParents)
121
+ dispatchPhaseEvent('beforeUpdate', updateParents)
122
+
123
+ if (batch.size > 0) {
124
+ let vnodes = Array.from(batch)
125
+ let noScheduledAncestor = new Set<VNode>()
126
+
127
+ for (let [vnode, domParent] of vnodes) {
128
+ if (ancestorIsScheduled(vnode, batch, noScheduledAncestor)) continue
129
+ let handle = vnode._handle
130
+ let curr = vnode._content
131
+ let vParent = vnode._parent!
132
+ // Calculate anchor at render time from current vdom position (never stale).
133
+ // Needed for fragment self-updates that add children - without this, new children
134
+ // would be appended after siblings. The keyed diff has placement logic, but unkeyed
135
+ // diff relies on anchor for correct positioning.
136
+ let anchor = findNextSiblingDomAnchor(vnode, vParent) || undefined
137
+ try {
138
+ let updateStyles = getFrameStyleManager(vnode)
139
+ renderComponent(
140
+ handle,
141
+ curr,
142
+ vnode,
143
+ domParent,
144
+ handle.frame,
145
+ scheduler,
146
+ updateStyles,
147
+ rootTarget,
148
+ vParent,
149
+ anchor,
150
+ )
151
+ } catch (error) {
152
+ dispatchError(error)
153
+ }
154
+ }
155
+ }
156
+
157
+ flushTaskQueue(workTasks)
158
+ setActiveSchedulerUpdateParents(undefined)
159
+
160
+ // Restore selection before commit-phase lifecycle work so mixins see
161
+ // the final DOM state but still run before commit listeners and user tasks.
162
+ documentState.restore()
163
+
164
+ flushTaskQueue(commitPhaseTasks)
165
+ dispatchPhaseEvent('commit', updateParents)
166
+ flushTaskQueue(postCommitTasks)
167
+ }
168
+ } finally {
169
+ setActiveSchedulerUpdateParents(undefined)
170
+ flushing = false
171
+ }
172
+ }
173
+
174
+ function dispatchPhaseEvent(type: SchedulerPhaseType, parents: ParentNode[]) {
175
+ let event = new Event(type) as SchedulerPhaseEvent
176
+ event.parents = parents
177
+ phaseEvents.dispatchEvent(event)
178
+ }
179
+
180
+ function flushTaskQueue(queue: EmptyFn[]) {
181
+ while (queue.length > 0) {
182
+ let task = queue.shift()
183
+ if (!task) continue
184
+ try {
185
+ task()
186
+ } catch (error) {
187
+ dispatchError(error)
188
+ }
189
+ }
190
+ }
191
+
192
+ function scheduleFlush() {
193
+ if (flushScheduled || flushing) return
194
+ flushScheduled = true
195
+ queueMicrotask(flush)
196
+ }
197
+
198
+ function ancestorIsScheduled(
199
+ vnode: VNode,
200
+ batch: Map<CommittedComponentNode, ParentNode>,
201
+ safe: Set<VNode>,
202
+ ): boolean {
203
+ let path: VNode[] = []
204
+ let current = vnode._parent
205
+
206
+ while (current) {
207
+ // Already verified this node has no scheduled ancestor above it
208
+ if (safe.has(current)) {
209
+ for (let node of path) safe.add(node)
210
+ return false
211
+ }
212
+
213
+ path.push(current)
214
+
215
+ if (isCommittedComponentNode(current) && batch.has(current)) {
216
+ return true
217
+ }
218
+
219
+ current = current._parent
220
+ }
221
+
222
+ // Reached root - mark entire path as safe for future lookups
223
+ for (let node of path) safe.add(node)
224
+ return false
225
+ }
226
+
227
+ scheduler = {
228
+ enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void {
229
+ scheduled.set(vnode, domParent)
230
+ scheduleFlush()
231
+ },
232
+
233
+ enqueueWork(newTasks: EmptyFn[]): void {
234
+ workTasks.push(...newTasks)
235
+ scheduleFlush()
236
+ },
237
+
238
+ enqueueCommitPhase(newTasks: EmptyFn[]): void {
239
+ commitPhaseTasks.push(...newTasks)
240
+ scheduleFlush()
241
+ },
242
+
243
+ enqueueTasks(newTasks: EmptyFn[]): void {
244
+ postCommitTasks.push(...newTasks)
245
+ scheduleFlush()
246
+ },
247
+
248
+ addEventListener(type, listener, options) {
249
+ phaseEvents.addEventListener(type, listener, options)
250
+ },
251
+
252
+ removeEventListener(type, listener, options) {
253
+ phaseEvents.removeEventListener(type, listener, options)
254
+ },
255
+
256
+ dequeue() {
257
+ flush()
258
+ },
259
+ }
260
+
261
+ return scheduler
262
+ }
@@ -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,50 @@
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
+ return 'children' in node.props
8
+ ? Array.isArray(node.props.children)
9
+ ? node.props.children.flat(Infinity).map(toVNode)
10
+ : [toVNode(node.props.children)]
11
+ : []
12
+ }
13
+
14
+ function flattenRemixNodeArray(nodes: RemixNode[], out: RemixNode[] = []): RemixNode[] {
15
+ for (let child of nodes) {
16
+ if (Array.isArray(child)) {
17
+ flattenRemixNodeArray(child, out)
18
+ } else {
19
+ out.push(child)
20
+ }
21
+ }
22
+ return out
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 flatChildren = flattenRemixNodeArray(node)
36
+ return { type: Fragment, _children: flatChildren.map(toVNode) }
37
+ }
38
+
39
+ if (node.type === Fragment) {
40
+ return { type: Fragment, key: node.key, _children: flatMapChildrenToVNodes(node) }
41
+ }
42
+
43
+ if (isRemixElement(node)) {
44
+ // When innerHTML is set, ignore children
45
+ let children = node.props.innerHTML != null ? [] : flatMapChildrenToVNodes(node)
46
+ return { type: node.type, key: node.key, props: node.props, _children: children }
47
+ }
48
+
49
+ invariant(false, 'Unexpected RemixNode')
50
+ }