@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,908 @@
1
+ import type { Context, FrameHandle } from '../component.ts'
2
+ import type { ElementProps, RemixElement } from '../jsx.ts'
3
+ import type { Scheduler } from '../scheduler.ts'
4
+ import type { SchedulerPhaseEvent } from '../scheduler.ts'
5
+ import { jsx } from '../jsx.ts'
6
+ import { TypedEventTarget } from '../typed-event-target.ts'
7
+ import { invariant } from '../invariant.ts'
8
+
9
+ type RebindNode<value, baseNode, boundNode> = value extends (
10
+ ...args: infer fnArgs
11
+ ) => infer fnResult
12
+ ? (...args: RebindTuple<fnArgs, baseNode, boundNode>) => RebindNode<fnResult, baseNode, boundNode>
13
+ : [value] extends [baseNode]
14
+ ? [baseNode] extends [value]
15
+ ? boundNode
16
+ : value
17
+ : value
18
+
19
+ type RebindTuple<args extends unknown[], baseNode, boundNode> = {
20
+ [index in keyof args]: RebindNode<args[index], baseNode, boundNode>
21
+ }
22
+
23
+ export type MixinProps<
24
+ node extends EventTarget = Element,
25
+ props extends ElementProps = ElementProps,
26
+ > = Omit<props, 'children' | 'innerHTML' | 'mix'> & {
27
+ mix?: MixValue<node, props>
28
+ }
29
+
30
+ export type MixinElement<
31
+ node extends EventTarget = Element,
32
+ props extends ElementProps = ElementProps,
33
+ > = ((
34
+ handle: { update(): Promise<AbortSignal> },
35
+ setup: unknown,
36
+ ) => (props: MixinProps<node, props>) => RemixElement) & {
37
+ __rmxMixinElementType: string
38
+ }
39
+
40
+ export type MixinInsertEvent<node extends EventTarget = Element> = Event & {
41
+ node: node
42
+ parent: ParentNode
43
+ key?: string
44
+ }
45
+
46
+ export type MixinReclaimedEvent<node extends EventTarget = Element> = Event & {
47
+ node: node
48
+ parent: ParentNode
49
+ key?: string
50
+ }
51
+
52
+ export type MixinUpdateEvent<node extends EventTarget = Element> = Event & {
53
+ node: node
54
+ }
55
+
56
+ export type MixinBeforeRemoveEvent = Event & {
57
+ persistNode(teardown: (signal: AbortSignal) => void | Promise<void>): void
58
+ }
59
+
60
+ type MixinContext = Pick<Context<Record<string, never>>, 'get'>
61
+
62
+ type MixinHandleEventMap<node extends EventTarget = Element> = {
63
+ beforeRemove: MixinBeforeRemoveEvent
64
+ reclaimed: MixinReclaimedEvent<node>
65
+ remove: Event
66
+ insert: MixinInsertEvent<node>
67
+ beforeUpdate: MixinUpdateEvent<node>
68
+ commit: MixinUpdateEvent<node>
69
+ }
70
+
71
+ /**
72
+ * Runtime handle passed to mixin setup functions.
73
+ *
74
+ * Mixin render callbacks receive host props with `children` and `innerHTML` removed.
75
+ * Returned mixin elements may patch host attributes and nested `mix`, but cannot replace
76
+ * the host subtree.
77
+ */
78
+ export type MixinHandle<
79
+ node extends EventTarget = Element,
80
+ props extends ElementProps = ElementProps,
81
+ > = TypedEventTarget<MixinHandleEventMap<node>> & {
82
+ id: string
83
+ context: MixinContext
84
+ frame: FrameHandle
85
+ element: MixinElement<node, props>
86
+ signal: AbortSignal
87
+ update(): Promise<AbortSignal>
88
+ queueTask(task: (node: node, signal: AbortSignal) => void): void
89
+ }
90
+
91
+ export function renderMixinElement<
92
+ node extends EventTarget = Element,
93
+ props extends ElementProps = ElementProps,
94
+ >(element: MixinElement<node, props>, props?: MixinProps<node, props>): RemixElement {
95
+ let { key, ...rest } = (props ?? {}) as MixinProps<node, props> & { key?: any }
96
+ return jsx(element, rest, key)
97
+ }
98
+
99
+ type MixinRuntimeType<
100
+ args extends unknown[] = [],
101
+ node extends EventTarget = Element,
102
+ props extends ElementProps = ElementProps,
103
+ > = (
104
+ handle: MixinHandle<node, props>,
105
+ type: string,
106
+ ) => ((...args: [...args, currentProps: props]) => MixinReturn<node, props>) | void
107
+
108
+ /**
109
+ * Public mixin setup function signature.
110
+ */
111
+ export type MixinType<
112
+ node extends EventTarget = Element,
113
+ args extends unknown[] = [],
114
+ props extends ElementProps = ElementProps,
115
+ > = (
116
+ handle: MixinHandle<node, props>,
117
+ type: string,
118
+ ) => ((...args: [...args, currentProps: props]) => MixinReturn<node, props>) | void
119
+
120
+ /**
121
+ * Serializable descriptor stored in the `mix` prop.
122
+ */
123
+ export type MixinDescriptor<
124
+ node extends EventTarget = Element,
125
+ args extends unknown[] = [],
126
+ props extends ElementProps = ElementProps,
127
+ > = {
128
+ type: MixinRuntimeType<args, node, props>
129
+ args: args
130
+ readonly __node?: (node: node) => void
131
+ }
132
+
133
+ type PreviousMixDepth = [0, 0, 1, 2, 3, 4]
134
+ type FalsyMixValue = false | 0 | 0n | '' | null | undefined
135
+ type NullableMixValue<descriptor> = descriptor | FalsyMixValue
136
+ type NestedMixValue<descriptor, depth extends number = 4> = depth extends 0
137
+ ? NullableMixValue<descriptor> | ReadonlyArray<NullableMixValue<descriptor>>
138
+ :
139
+ | NullableMixValue<descriptor>
140
+ | ReadonlyArray<NestedMixValue<descriptor, PreviousMixDepth[depth]>>
141
+
142
+ /**
143
+ * Accepted authoring shape for the `mix` prop on host elements.
144
+ */
145
+ export type MixInput<
146
+ node extends EventTarget = Element,
147
+ props extends ElementProps = ElementProps,
148
+ > = NestedMixValue<MixinDescriptor<node, any, props>>
149
+
150
+ /**
151
+ * Accepted value shape for the `mix` prop.
152
+ */
153
+ export type MixValue<
154
+ node extends EventTarget = Element,
155
+ props extends ElementProps = ElementProps,
156
+ > = MixinDescriptor<node, any, props> | ReadonlyArray<MixinDescriptor<node, any, props>>
157
+
158
+ type MixinReturn<node extends EventTarget = Element, props extends ElementProps = ElementProps> =
159
+ | void
160
+ | null
161
+ | RemixElement
162
+ | MixinElement<node, props>
163
+ | MixInput<node, props>
164
+
165
+ type AnyMixinType = MixinRuntimeType<unknown[], Element, ElementProps>
166
+ type AnyMixinDescriptor = MixinDescriptor<Element, unknown[], ElementProps>
167
+ type AnyMixinRunner = (
168
+ ...args: [...unknown[], currentProps: ElementProps]
169
+ ) => MixinReturn<Element, ElementProps>
170
+ type AnyMixinRunnerResult = ReturnType<AnyMixinRunner>
171
+ type AnyMixinSetupResult = ReturnType<AnyMixinType> | AnyMixinRunnerResult
172
+ type AnyMixinHandle = MixinHandle<Element, ElementProps>
173
+ type ScopedAnyMixinHandle = AnyMixinHandle & {
174
+ queueCommitTask(task: () => void): void
175
+ setActiveScope(scope?: symbol): void
176
+ dispatchScopedEvent(scope: symbol, event: Event): void
177
+ releaseScope(scope: symbol): void
178
+ }
179
+
180
+ type RunnerEntry = {
181
+ type: AnyMixinType
182
+ runner: AnyMixinRunner
183
+ scope: symbol
184
+ }
185
+
186
+ type MixinHandleFactoryOptions = {
187
+ id: string
188
+ hostType: string
189
+ frame: FrameHandle
190
+ scheduler: Scheduler
191
+ getContext: MixinContext['get']
192
+ getRuntimeSignal: () => AbortSignal
193
+ getBinding: () => MixinRuntimeBinding | undefined
194
+ }
195
+
196
+ export type MixinRuntimeBinding = {
197
+ node: Element
198
+ parent: ParentNode
199
+ key?: string
200
+ target: unknown
201
+ frame: FrameHandle
202
+ scheduler: Scheduler
203
+ enqueueUpdate(done: (signal: AbortSignal) => void): void
204
+ }
205
+
206
+ type ResolveMixedPropsInput = {
207
+ hostType: string
208
+ frame: FrameHandle
209
+ scheduler: Scheduler
210
+ getContext?: MixinContext['get']
211
+ props: ElementProps
212
+ state?: MixinRuntimeState
213
+ }
214
+
215
+ type ResolveMixedPropsOutput = {
216
+ props: ElementProps
217
+ state: MixinRuntimeState
218
+ }
219
+
220
+ export type MixinRuntimeState = {
221
+ id: string
222
+ controller?: AbortController
223
+ aborted: boolean
224
+ handle?: AnyMixinHandle
225
+ runners: RunnerEntry[]
226
+ binding?: MixinRuntimeBinding
227
+ removePrepared?: boolean
228
+ pendingRemoval?: {
229
+ signal: AbortSignal
230
+ cancel: (reason?: unknown) => void
231
+ done: Promise<void>
232
+ }
233
+ }
234
+
235
+ let mixinHandleId = 0
236
+
237
+ /**
238
+ * Creates a typed mixin factory that can be passed through the `mix` prop.
239
+ *
240
+ * @param type Mixin setup function.
241
+ * @returns A function that captures mixin arguments and returns a descriptor.
242
+ */
243
+ export function createMixin<
244
+ node extends EventTarget = Element,
245
+ args extends unknown[] = [],
246
+ props extends ElementProps = ElementProps,
247
+ >(type: MixinType<node, args, props>) {
248
+ return <boundNode extends node = node>(
249
+ ...args: RebindTuple<args, node, boundNode>
250
+ ): MixinDescriptor<boundNode, RebindTuple<args, node, boundNode>, props> => ({
251
+ type: type as unknown as MixinRuntimeType<RebindTuple<args, node, boundNode>, boundNode, props>,
252
+ args: args as RebindTuple<args, node, boundNode>,
253
+ })
254
+ }
255
+
256
+ export function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPropsOutput {
257
+ let state = input.state ?? createMixinRuntimeState()
258
+ let handle = state.handle as ScopedAnyMixinHandle | undefined
259
+ if (!handle) {
260
+ handle = createMixinHandle({
261
+ id: state.id,
262
+ hostType: input.hostType,
263
+ frame: input.frame,
264
+ scheduler: input.scheduler,
265
+ getContext: input.getContext ?? (() => undefined),
266
+ getRuntimeSignal: () => getMixinRuntimeSignal(state),
267
+ getBinding: () => state.binding,
268
+ }) as ScopedAnyMixinHandle
269
+ state.handle = handle
270
+ }
271
+ let hostType = input.hostType
272
+ let descriptors = resolveMixDescriptors(input.props)
273
+ let composedProps = withoutMix(input.props)
274
+ let mixinProps = withoutMixinTreeProps(composedProps)
275
+ let maxDescriptors = 1024
276
+
277
+ for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {
278
+ let descriptor = descriptors[index]
279
+ let entry = state.runners[index]
280
+ if (!entry || entry.type !== descriptor.type) {
281
+ if (entry) {
282
+ queueMixinRemove(handle, entry.scope)
283
+ }
284
+ let scope = Symbol('mixin-scope')
285
+ handle.setActiveScope(scope)
286
+ entry = {
287
+ scope,
288
+ type: descriptor.type as AnyMixinType,
289
+ runner: normalizeMixinRunner(
290
+ descriptor.type(handle, hostType) as AnyMixinSetupResult,
291
+ handle,
292
+ ),
293
+ }
294
+ handle.setActiveScope(undefined)
295
+ state.runners[index] = entry
296
+ let binding = state.binding
297
+ if (binding?.node) {
298
+ queueMixinInsert(handle, entry.scope, binding.node, binding.parent, binding.key)
299
+ }
300
+ }
301
+
302
+ handle.setActiveScope(entry.scope)
303
+ let result = entry.runner(...descriptor.args, mixinProps)
304
+ handle.setActiveScope(undefined)
305
+ if (!result) continue
306
+ if (isMixinElement(result)) continue
307
+
308
+ let returnedDescriptors = resolveReturnedMixDescriptors(result)
309
+ if (returnedDescriptors) {
310
+ for (let returned of returnedDescriptors) descriptors.push(returned)
311
+ continue
312
+ }
313
+
314
+ if (!isRemixElement(result)) {
315
+ console.error(new Error('mixins must return a remix element'))
316
+ continue
317
+ }
318
+
319
+ let resultType =
320
+ typeof result.type === 'string'
321
+ ? result.type
322
+ : isMixinElement(result.type)
323
+ ? result.type.__rmxMixinElementType
324
+ : null
325
+ if (resultType !== hostType) {
326
+ console.error(new Error('mixins must return an element with the same host type'))
327
+ continue
328
+ }
329
+
330
+ if (result.type !== resultType) {
331
+ result = { ...result, type: resultType }
332
+ }
333
+
334
+ let nextProps = sanitizeReturnedMixinProps(result.props)
335
+ let nestedDescriptors = resolveMixDescriptors(nextProps)
336
+ for (let nested of nestedDescriptors) descriptors.push(nested)
337
+ composedProps = composeMixinProps(composedProps, withoutMix(nextProps))
338
+ mixinProps = withoutMixinTreeProps(composedProps)
339
+ }
340
+
341
+ for (let index = descriptors.length; index < state.runners.length; index++) {
342
+ let entry = state.runners[index]
343
+ if (entry) {
344
+ handle.dispatchScopedEvent(entry.scope, new Event('remove'))
345
+ handle.releaseScope(entry.scope)
346
+ }
347
+ }
348
+
349
+ if (state.runners.length > descriptors.length) {
350
+ state.runners.length = descriptors.length
351
+ }
352
+
353
+ let nextMix = input.props.mix
354
+ return {
355
+ state,
356
+ props: {
357
+ ...composedProps,
358
+ ...(nextMix === undefined ? {} : { mix: nextMix }),
359
+ },
360
+ }
361
+ }
362
+
363
+ export function teardownMixins(state?: MixinRuntimeState) {
364
+ if (!state) return
365
+ state.binding = undefined
366
+ prepareMixinRemoval(state)
367
+ cancelPendingMixinRemoval(state)
368
+ let handle = state.handle as ScopedAnyMixinHandle | undefined
369
+ if (handle) {
370
+ handle.queueCommitTask(() => finalizeMixinTeardown(state))
371
+ return
372
+ }
373
+ finalizeMixinTeardown(state)
374
+ }
375
+
376
+ export function bindMixinRuntime(
377
+ state: MixinRuntimeState | undefined,
378
+ binding?: MixinRuntimeBinding,
379
+ options?: { dispatchReclaimed?: boolean },
380
+ ) {
381
+ if (!state) return
382
+ let previousNode = state.binding?.node
383
+ let nextBinding = binding
384
+ state.binding = nextBinding
385
+ if (!nextBinding?.node || previousNode === nextBinding.node) return
386
+ let nextNode = nextBinding.node
387
+ let handle = state.handle as ScopedAnyMixinHandle | undefined
388
+ if (!handle) return
389
+ for (let entry of state.runners) {
390
+ if (options?.dispatchReclaimed) {
391
+ queueMixinReclaimed(handle, entry.scope, nextNode, nextBinding.parent, nextBinding.key)
392
+ } else {
393
+ queueMixinInsert(handle, entry.scope, nextNode, nextBinding.parent, nextBinding.key)
394
+ }
395
+ }
396
+ }
397
+
398
+ export function prepareMixinRemoval(state?: MixinRuntimeState) {
399
+ if (!state || state.removePrepared) return state?.pendingRemoval?.done
400
+ state.removePrepared = true
401
+
402
+ let pendingRemoval: MixinRuntimeState['pendingRemoval']
403
+ let persistTeardowns: Array<(signal: AbortSignal) => void | Promise<void>> = []
404
+ let registerPersistNode = (teardown: (signal: AbortSignal) => void | Promise<void>) => {
405
+ persistTeardowns.push(teardown)
406
+ }
407
+
408
+ let handle = state.handle as ScopedAnyMixinHandle | undefined
409
+ if (!handle) return
410
+ for (let entry of state.runners) {
411
+ dispatchMixinBeforeRemove(handle, entry.scope, registerPersistNode)
412
+ }
413
+
414
+ if (persistTeardowns.length > 0) {
415
+ let controller = new AbortController()
416
+ let done = Promise.allSettled(
417
+ persistTeardowns.map((teardown) => Promise.resolve().then(() => teardown(controller.signal))),
418
+ ).then(() => {})
419
+ pendingRemoval = {
420
+ signal: controller.signal,
421
+ cancel(reason) {
422
+ controller.abort(reason)
423
+ },
424
+ done,
425
+ }
426
+ }
427
+
428
+ state.pendingRemoval = pendingRemoval
429
+ return pendingRemoval?.done
430
+ }
431
+
432
+ export function cancelPendingMixinRemoval(
433
+ state?: MixinRuntimeState,
434
+ reason: unknown = new DOMException('', 'AbortError'),
435
+ ) {
436
+ if (!state?.pendingRemoval) return
437
+ state.pendingRemoval.cancel(reason)
438
+ state.pendingRemoval = undefined
439
+ state.removePrepared = false
440
+ }
441
+
442
+ function createMixinRuntimeState(): MixinRuntimeState {
443
+ return {
444
+ id: `m${++mixinHandleId}`,
445
+ aborted: false,
446
+ runners: [],
447
+ }
448
+ }
449
+
450
+ function createMixinHandle(options: {
451
+ id: string
452
+ hostType: string
453
+ frame: FrameHandle
454
+ scheduler: Scheduler
455
+ getContext: MixinContext['get']
456
+ getRuntimeSignal: () => AbortSignal
457
+ getBinding: () => MixinRuntimeBinding | undefined
458
+ }): AnyMixinHandle {
459
+ return new MixinHandleImpl(options)
460
+ }
461
+
462
+ class MixinHandleImpl
463
+ extends TypedEventTarget<MixinHandleEventMap<Element>>
464
+ implements ScopedAnyMixinHandle
465
+ {
466
+ id: string
467
+ context: MixinContext
468
+ frame: FrameHandle
469
+ element: MixinElement<Element, ElementProps>
470
+ #options: MixinHandleFactoryOptions
471
+ #phaseListenerCounts: Record<'beforeUpdate' | 'commit', number> = {
472
+ beforeUpdate: 0,
473
+ commit: 0,
474
+ }
475
+ #activeScope?: symbol
476
+ #scopeSignals = new Map<symbol, AbortController>()
477
+ #scopeTargets = new Map<symbol, TypedEventTarget<MixinHandleEventMap<Element>>>()
478
+ #scopePhaseCounts = new Map<symbol, Record<'beforeUpdate' | 'commit', number>>()
479
+ #onSchedulerBeforeUpdate = (event: Event) => {
480
+ this.#dispatchSchedulerPhaseToHandle('beforeUpdate', event as SchedulerPhaseEvent)
481
+ }
482
+ #onSchedulerCommit = (event: Event) => {
483
+ this.#dispatchSchedulerPhaseToHandle('commit', event as SchedulerPhaseEvent)
484
+ }
485
+
486
+ constructor(options: MixinHandleFactoryOptions) {
487
+ super()
488
+ this.#options = options
489
+ this.id = options.id
490
+ this.context = {
491
+ get: options.getContext,
492
+ }
493
+ this.frame = options.frame
494
+
495
+ let element = ((_: { update(): Promise<AbortSignal> }, __: unknown) =>
496
+ (props: ElementProps) => ({
497
+ $rmx: true as const,
498
+ type: options.hostType,
499
+ key: null,
500
+ props,
501
+ })) as unknown as MixinElement<Element, ElementProps>
502
+ element.__rmxMixinElementType = options.hostType
503
+ this.element = element
504
+ }
505
+
506
+ get signal() {
507
+ let scope = this.#activeScope
508
+ invariant(
509
+ scope,
510
+ 'handle.signal is only available during mixin setup, render, or lifecycle callbacks',
511
+ )
512
+ return this.#getScopeSignal(scope)
513
+ }
514
+
515
+ addEventListener(
516
+ type: string,
517
+ listener: EventListenerOrEventListenerObject | null,
518
+ options?: AddEventListenerOptions | boolean,
519
+ ): void {
520
+ let target = this.#getActiveScopeTarget()
521
+ target.addEventListener(
522
+ type as keyof MixinHandleEventMap<Element>,
523
+ listener as EventListener,
524
+ options,
525
+ )
526
+ if (!listener || !isSchedulerPhaseType(type)) return
527
+ let scope = this.#activeScope
528
+ invariant(scope)
529
+ let scopePhaseCounts = this.#scopePhaseCounts.get(scope)
530
+ invariant(scopePhaseCounts)
531
+ scopePhaseCounts[type] += 1
532
+ this.#phaseListenerCounts[type] += 1
533
+ if (this.#phaseListenerCounts[type] !== 1) return
534
+ if (type === 'beforeUpdate') {
535
+ this.#options.scheduler.addEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)
536
+ } else {
537
+ this.#options.scheduler.addEventListener('commit', this.#onSchedulerCommit)
538
+ }
539
+ }
540
+
541
+ removeEventListener(
542
+ type: string,
543
+ listener: EventListenerOrEventListenerObject | null,
544
+ options?: EventListenerOptions | boolean,
545
+ ): void {
546
+ let target = this.#getActiveScopeTarget()
547
+ target.removeEventListener(
548
+ type as keyof MixinHandleEventMap<Element>,
549
+ listener as EventListener,
550
+ typeof options === 'boolean' ? { capture: options } : options,
551
+ )
552
+ if (!listener || !isSchedulerPhaseType(type)) return
553
+ let scope = this.#activeScope
554
+ invariant(scope)
555
+ let scopePhaseCounts = this.#scopePhaseCounts.get(scope)
556
+ invariant(scopePhaseCounts)
557
+ scopePhaseCounts[type] = Math.max(0, scopePhaseCounts[type] - 1)
558
+ this.#phaseListenerCounts[type] = Math.max(0, this.#phaseListenerCounts[type] - 1)
559
+ if (this.#phaseListenerCounts[type] !== 0) return
560
+ if (type === 'beforeUpdate') {
561
+ this.#options.scheduler.removeEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)
562
+ } else {
563
+ this.#options.scheduler.removeEventListener('commit', this.#onSchedulerCommit)
564
+ }
565
+ }
566
+
567
+ update(): Promise<AbortSignal> {
568
+ return new Promise((resolve) => {
569
+ let signal = this.#options.getRuntimeSignal()
570
+ if (signal.aborted) {
571
+ resolve(signal)
572
+ return
573
+ }
574
+ let binding = this.#options.getBinding()
575
+ if (!binding) {
576
+ resolve(signal)
577
+ return
578
+ }
579
+ binding.enqueueUpdate(resolve)
580
+ })
581
+ }
582
+
583
+ queueTask(task: (node: Element, signal: AbortSignal) => void): void {
584
+ this.#options.scheduler.enqueueTasks([
585
+ () => {
586
+ let binding = this.#options.getBinding()
587
+ invariant(binding)
588
+ task(binding.node, this.#options.getRuntimeSignal())
589
+ },
590
+ ])
591
+ }
592
+
593
+ queueCommitTask(task: () => void): void {
594
+ this.#options.scheduler.enqueueCommitPhase([task])
595
+ }
596
+
597
+ setActiveScope(scope?: symbol): void {
598
+ this.#activeScope = scope
599
+ if (!scope) return
600
+ if (this.#scopeTargets.has(scope)) return
601
+ this.#scopeSignals.set(scope, new AbortController())
602
+ this.#scopeTargets.set(scope, new TypedEventTarget<MixinHandleEventMap<Element>>())
603
+ this.#scopePhaseCounts.set(scope, { beforeUpdate: 0, commit: 0 })
604
+ }
605
+
606
+ dispatchScopedEvent(scope: symbol, event: Event): void {
607
+ let previousScope = this.#activeScope
608
+ this.#activeScope = scope
609
+ this.#scopeTargets.get(scope)?.dispatchEvent(event)
610
+ this.#activeScope = previousScope
611
+ }
612
+
613
+ releaseScope(scope: symbol): void {
614
+ let scopePhaseCounts = this.#scopePhaseCounts.get(scope)
615
+ if (scopePhaseCounts) {
616
+ this.#decrementGlobalPhaseCount('beforeUpdate', scopePhaseCounts.beforeUpdate)
617
+ this.#decrementGlobalPhaseCount('commit', scopePhaseCounts.commit)
618
+ }
619
+ this.#scopeSignals.get(scope)?.abort()
620
+ this.#scopeSignals.delete(scope)
621
+ this.#scopePhaseCounts.delete(scope)
622
+ this.#scopeTargets.delete(scope)
623
+ if (this.#activeScope === scope) {
624
+ this.#activeScope = undefined
625
+ }
626
+ }
627
+
628
+ #dispatchSchedulerPhaseToHandle(type: 'beforeUpdate' | 'commit', event: SchedulerPhaseEvent) {
629
+ let binding = this.#options.getBinding()
630
+ if (!binding) return
631
+ if (!isBindingInUpdateScope(binding, event.parents)) return
632
+ for (let [, target] of this.#scopeTargets) {
633
+ let updateEvent = new Event(type) as MixinUpdateEvent<Element>
634
+ updateEvent.node = binding.node
635
+ target.dispatchEvent(updateEvent)
636
+ }
637
+ }
638
+
639
+ #getActiveScopeTarget(): TypedEventTarget<MixinHandleEventMap<Element>> {
640
+ let scope = this.#activeScope
641
+ invariant(scope)
642
+ let target = this.#scopeTargets.get(scope)
643
+ invariant(target)
644
+ return target
645
+ }
646
+
647
+ #getScopeSignal(scope: symbol) {
648
+ let controller = this.#scopeSignals.get(scope)
649
+ invariant(controller)
650
+ return controller.signal
651
+ }
652
+
653
+ #decrementGlobalPhaseCount(type: 'beforeUpdate' | 'commit', amount: number) {
654
+ if (amount <= 0) return
655
+ this.#phaseListenerCounts[type] = Math.max(0, this.#phaseListenerCounts[type] - amount)
656
+ if (this.#phaseListenerCounts[type] !== 0) return
657
+ if (type === 'beforeUpdate') {
658
+ this.#options.scheduler.removeEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)
659
+ } else {
660
+ this.#options.scheduler.removeEventListener('commit', this.#onSchedulerCommit)
661
+ }
662
+ }
663
+ }
664
+
665
+ export function getMixinRuntimeSignal(state: MixinRuntimeState): AbortSignal {
666
+ let controller = state.controller
667
+ if (!controller) {
668
+ controller = new AbortController()
669
+ if (state.aborted) {
670
+ controller.abort()
671
+ }
672
+ state.controller = controller
673
+ }
674
+ return controller.signal
675
+ }
676
+
677
+ export function dispatchMixinBeforeUpdate(state?: MixinRuntimeState) {
678
+ dispatchMixinUpdateEvent(state, 'beforeUpdate')
679
+ }
680
+
681
+ export function dispatchMixinCommit(state?: MixinRuntimeState) {
682
+ dispatchMixinUpdateEvent(state, 'commit')
683
+ }
684
+
685
+ function dispatchMixinInsert(
686
+ handle: ScopedAnyMixinHandle,
687
+ scope: symbol,
688
+ node: Element,
689
+ parent: ParentNode,
690
+ key?: string,
691
+ ) {
692
+ let event = new Event('insert') as MixinInsertEvent<Element>
693
+ event.node = node
694
+ event.parent = parent
695
+ event.key = key
696
+ handle.dispatchScopedEvent(scope, event)
697
+ }
698
+
699
+ function dispatchMixinReclaimed(
700
+ handle: ScopedAnyMixinHandle,
701
+ scope: symbol,
702
+ node: Element,
703
+ parent: ParentNode,
704
+ key?: string,
705
+ ) {
706
+ let event = new Event('reclaimed') as MixinReclaimedEvent<Element>
707
+ event.node = node
708
+ event.parent = parent
709
+ event.key = key
710
+ handle.dispatchScopedEvent(scope, event)
711
+ }
712
+
713
+ function dispatchMixinBeforeRemove(
714
+ handle: ScopedAnyMixinHandle,
715
+ scope: symbol,
716
+ persistNode: (teardown: (signal: AbortSignal) => void | Promise<void>) => void,
717
+ ) {
718
+ let event = new Event('beforeRemove') as MixinBeforeRemoveEvent
719
+ event.persistNode = persistNode
720
+ handle.dispatchScopedEvent(scope, event)
721
+ }
722
+
723
+ function queueMixinInsert(
724
+ handle: ScopedAnyMixinHandle,
725
+ scope: symbol,
726
+ node: Element,
727
+ parent: ParentNode,
728
+ key?: string,
729
+ ) {
730
+ handle.queueCommitTask(() => {
731
+ dispatchMixinInsert(handle, scope, node, parent, key)
732
+ })
733
+ }
734
+
735
+ function queueMixinReclaimed(
736
+ handle: ScopedAnyMixinHandle,
737
+ scope: symbol,
738
+ node: Element,
739
+ parent: ParentNode,
740
+ key?: string,
741
+ ) {
742
+ handle.queueCommitTask(() => {
743
+ dispatchMixinReclaimed(handle, scope, node, parent, key)
744
+ })
745
+ }
746
+
747
+ function queueMixinRemove(handle: ScopedAnyMixinHandle, scope: symbol) {
748
+ handle.queueCommitTask(() => {
749
+ handle.dispatchScopedEvent(scope, new Event('remove'))
750
+ handle.releaseScope(scope)
751
+ })
752
+ }
753
+
754
+ function dispatchMixinRemoveEvent(state?: MixinRuntimeState) {
755
+ let runners = state?.runners
756
+ if (!runners?.length) return
757
+ let handle = state?.handle as ScopedAnyMixinHandle | undefined
758
+ if (!handle) return
759
+ for (let entry of runners) {
760
+ handle.dispatchScopedEvent(entry.scope, new Event('remove'))
761
+ }
762
+ }
763
+
764
+ function finalizeMixinTeardown(state: MixinRuntimeState) {
765
+ dispatchMixinRemoveEvent(state)
766
+ let handle = state.handle as ScopedAnyMixinHandle | undefined
767
+ if (handle) {
768
+ for (let entry of state.runners) {
769
+ handle.releaseScope(entry.scope)
770
+ }
771
+ }
772
+ state.runners.length = 0
773
+ state.aborted = true
774
+ state.controller?.abort()
775
+ state.pendingRemoval = undefined
776
+ state.removePrepared = true
777
+ state.handle = undefined
778
+ }
779
+
780
+ function dispatchMixinUpdateEvent(
781
+ state: MixinRuntimeState | undefined,
782
+ type: 'beforeUpdate' | 'commit',
783
+ ) {
784
+ let node = state?.binding?.node
785
+ if (!node) return
786
+ let runners = state?.runners
787
+ if (!runners?.length) return
788
+ let handle = state?.handle as ScopedAnyMixinHandle | undefined
789
+ if (!handle) return
790
+ for (let entry of runners) {
791
+ let event = new Event(type) as MixinUpdateEvent<Element>
792
+ event.node = node
793
+ handle.dispatchScopedEvent(entry.scope, event)
794
+ }
795
+ }
796
+
797
+ function isSchedulerPhaseType(type: string): type is 'beforeUpdate' | 'commit' {
798
+ return type === 'beforeUpdate' || type === 'commit'
799
+ }
800
+
801
+ function isBindingInUpdateScope(binding: MixinRuntimeBinding, parents: ParentNode[]): boolean {
802
+ if (parents.length === 0) return false
803
+ let node = binding.node as Node
804
+ for (let parent of parents) {
805
+ let parentNode = parent as Node
806
+ if (parentNode === node) return true
807
+ if (parentNode.contains(node)) return true
808
+ }
809
+ return false
810
+ }
811
+
812
+ function resolveMixDescriptors(props: ElementProps): AnyMixinDescriptor[] {
813
+ let mix = props.mix
814
+ if (!mix) return []
815
+ if (Array.isArray(mix)) {
816
+ if (mix.length === 0) return []
817
+ return mix.filter(Boolean) as AnyMixinDescriptor[]
818
+ }
819
+ return [mix] as AnyMixinDescriptor[]
820
+ }
821
+
822
+ function withoutMix(props: ElementProps): ElementProps {
823
+ if (!('mix' in props)) return props
824
+ let output = { ...props }
825
+ delete output.mix
826
+ return output
827
+ }
828
+
829
+ function withoutMixinTreeProps(props: ElementProps): ElementProps {
830
+ if (!('children' in props) && !('innerHTML' in props)) return props
831
+ let output = { ...props }
832
+ delete output.children
833
+ delete output.innerHTML
834
+ return output
835
+ }
836
+
837
+ function sanitizeReturnedMixinProps(props: ElementProps): ElementProps {
838
+ if (!('children' in props) && !('innerHTML' in props)) return props
839
+ console.error(new Error('mixins must not return children or innerHTML'))
840
+ return withoutMixinTreeProps(props)
841
+ }
842
+
843
+ function composeMixinProps(previous: ElementProps, next: ElementProps): ElementProps {
844
+ return { ...previous, ...next }
845
+ }
846
+
847
+ function resolveReturnedMixDescriptors(value: unknown): AnyMixinDescriptor[] | null {
848
+ let descriptors: AnyMixinDescriptor[] = []
849
+ if (!collectReturnedMixDescriptors(value, descriptors)) {
850
+ return null
851
+ }
852
+
853
+ return descriptors
854
+ }
855
+
856
+ function collectReturnedMixDescriptors(
857
+ value: unknown,
858
+ output: AnyMixinDescriptor[],
859
+ ): value is MixInput<Element, ElementProps> {
860
+ if (!value) {
861
+ return true
862
+ }
863
+
864
+ if (Array.isArray(value)) {
865
+ for (let item of value) {
866
+ if (!collectReturnedMixDescriptors(item, output)) {
867
+ return false
868
+ }
869
+ }
870
+ return true
871
+ }
872
+
873
+ if (!isMixinDescriptor(value)) {
874
+ return false
875
+ }
876
+
877
+ output.push(value)
878
+ return true
879
+ }
880
+
881
+ function isRemixElement(value: unknown): value is RemixElement {
882
+ if (!value || typeof value !== 'object') return false
883
+ return (value as { $rmx?: unknown }).$rmx === true
884
+ }
885
+
886
+ function isMixinDescriptor(value: unknown): value is AnyMixinDescriptor {
887
+ if (!value || typeof value !== 'object' || isRemixElement(value)) {
888
+ return false
889
+ }
890
+
891
+ let descriptor = value as { type?: unknown; args?: unknown }
892
+ return typeof descriptor.type === 'function' && Array.isArray(descriptor.args)
893
+ }
894
+
895
+ function isMixinElement(value: unknown): value is MixinElement<Element, ElementProps> {
896
+ if (typeof value !== 'function') return false
897
+ return '__rmxMixinElementType' in value
898
+ }
899
+
900
+ function normalizeMixinRunner(result: AnyMixinSetupResult, handle: AnyMixinHandle): AnyMixinRunner {
901
+ if (typeof result === 'function' && !isMixinElement(result)) {
902
+ return result as AnyMixinRunner
903
+ }
904
+ if (result === undefined) {
905
+ return () => handle.element
906
+ }
907
+ return () => result as AnyMixinRunnerResult
908
+ }