@remix-run/ui 0.1.1 → 0.2.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 (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import {
4
2
  attrs,
5
3
  createElement,
@@ -8,9 +6,11 @@ import {
8
6
  on,
9
7
  ref,
10
8
  type CSSMixinDescriptor,
9
+ type Dispatched,
11
10
  type ElementProps,
12
11
  type Handle,
13
12
  type MixinHandle,
13
+ type MixinFactory,
14
14
  type Props,
15
15
  type RemixNode,
16
16
  } from '@remix-run/ui'
@@ -28,7 +28,10 @@ import type { AnchorOptions } from '../anchor/anchor.ts'
28
28
  const SELECT_CHANGE_EVENT = 'rmx:select-change' as const
29
29
  const LABEL_SWAP_DELAY_MS = 75
30
30
 
31
- type SelectChangeHandler = (event: SelectChangeEvent, signal: AbortSignal) => void | Promise<void>
31
+ type SelectChangeHandler<target extends HTMLElement> = (
32
+ event: Dispatched<SelectChangeEvent, target>,
33
+ signal: AbortSignal,
34
+ ) => void | Promise<void>
32
35
 
33
36
  declare global {
34
37
  interface HTMLElementEventMap {
@@ -74,12 +77,7 @@ export interface SelectProps extends Omit<Props<'button'>, 'children' | 'name'>
74
77
 
75
78
  export type SelectOptionProps = Props<'div'> & Omit<listbox.ListboxOption, 'id'>
76
79
 
77
- enum State {
78
- Initializing = 'initializing',
79
- Closed = 'closed',
80
- Open = 'open',
81
- Selecting = 'selecting',
82
- }
80
+ type State = 'initializing' | 'closed' | 'open' | 'selecting'
83
81
 
84
82
  type PendingChange = {
85
83
  label: string | null
@@ -109,13 +107,13 @@ interface SelectContextValue {
109
107
  unregisterTrigger: (node: HTMLButtonElement) => void
110
108
  }
111
109
 
112
- function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>) {
110
+ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>): () => RemixNode {
113
111
  let triggerRef: HTMLButtonElement | undefined
114
112
  let listboxRef: listbox.ListboxRef | undefined
115
113
  let surfaceRef: HTMLElement | undefined
116
114
  let popoverContextRef: popover.PopoverContext | undefined
117
115
 
118
- let state: State = State.Initializing
116
+ let state: State = 'initializing'
119
117
 
120
118
  let value: listbox.ListboxValue = null
121
119
  let activeValue: listbox.ListboxValue = null
@@ -128,14 +126,14 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
128
126
  let listId = `${handle.id}-list`
129
127
 
130
128
  function open() {
131
- if (state !== State.Closed || handle.props.disabled) return
132
- state = State.Open
129
+ if (state !== 'closed' || handle.props.disabled) return
130
+ state = 'open'
133
131
  activeValue = value
134
132
  handle.update()
135
133
  }
136
134
 
137
135
  function syncPopoverMinWidth() {
138
- if (state !== State.Open || !surfaceRef || !triggerRef) {
136
+ if (state !== 'open' || !surfaceRef || !triggerRef) {
139
137
  return
140
138
  }
141
139
 
@@ -165,8 +163,8 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
165
163
  }
166
164
 
167
165
  function close() {
168
- if (state !== State.Open) return
169
- state = State.Closed
166
+ if (state !== 'open') return
167
+ state = 'closed'
170
168
  handle.update()
171
169
  }
172
170
 
@@ -215,22 +213,22 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
215
213
  nextValue: listbox.ListboxValue,
216
214
  option: listbox.ListboxOption | undefined,
217
215
  ) {
218
- if (state !== State.Open) return
216
+ if (state !== 'open') return
219
217
  pendingChange = getPendingChange(nextValue, option)
220
218
  setSelectedOption(nextValue, option)
221
219
  handle.update()
222
220
  }
223
221
 
224
222
  async function settleSelectedOption() {
225
- if (state !== State.Open) return
223
+ if (state !== 'open') return
226
224
 
227
225
  let change = pendingChange
228
226
  pendingChange = null
229
- state = State.Selecting
227
+ state = 'selecting'
230
228
 
231
229
  if (!surfaceRef) {
232
230
  displayedLabel = selectedLabel
233
- state = State.Closed
231
+ state = 'closed'
234
232
  let signal = await handle.update()
235
233
  if (signal.aborted) return
236
234
  dispatchChange(change)
@@ -241,14 +239,14 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
241
239
  await wait(LABEL_SWAP_DELAY_MS) // UX delay label swap for clear value change
242
240
  if (handle.signal.aborted) return
243
241
  displayedLabel = selectedLabel
244
- state = State.Closed
242
+ state = 'closed'
245
243
  let signal = await handle.update()
246
244
  if (signal.aborted) return
247
245
  dispatchChange(change)
248
246
  }
249
247
 
250
248
  function selectTypeaheadMatch(text: string) {
251
- if (state !== State.Closed || handle.props.disabled) return
249
+ if (state !== 'closed' || handle.props.disabled) return
252
250
 
253
251
  let option = listboxRef?.matchSearchText(text, value)
254
252
  if (!option) return
@@ -266,7 +264,7 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
266
264
  nextActiveValue: listbox.ListboxValue,
267
265
  option: listbox.ListboxOption | undefined,
268
266
  ) {
269
- if (state !== State.Open) return
267
+ if (state !== 'open') return
270
268
  activeValue = nextActiveValue
271
269
  activeId = option?.id
272
270
  handle.update()
@@ -286,11 +284,11 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
286
284
  },
287
285
 
288
286
  get isExpanded() {
289
- return state === State.Open || state === State.Selecting
287
+ return state === 'open' || state === 'selecting'
290
288
  },
291
289
 
292
290
  get isOpen() {
293
- return state === State.Open
291
+ return state === 'open'
294
292
  },
295
293
 
296
294
  get listId() {
@@ -356,11 +354,11 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
356
354
  })
357
355
 
358
356
  return () => {
359
- if (state === State.Initializing) {
357
+ if (state === 'initializing') {
360
358
  selectedLabel = displayedLabel = handle.props.defaultLabel
361
359
  value = handle.props.defaultValue ?? null
362
360
  activeValue = value
363
- state = State.Closed
361
+ state = 'closed'
364
362
 
365
363
  handle.queueTask(() => {
366
364
  if (selectedId || !surfaceRef) {
@@ -399,7 +397,11 @@ function getSelectContext(handle: Handle | MixinHandle) {
399
397
  return handle.context.get(SelectProvider)
400
398
  }
401
399
 
402
- const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) => {
400
+ const triggerMixin: MixinFactory<HTMLButtonElement, [], ElementProps> = createMixin<
401
+ HTMLButtonElement,
402
+ [],
403
+ ElementProps
404
+ >((handle) => {
403
405
  let context = getSelectContext(handle)
404
406
 
405
407
  return (props) => [
@@ -431,7 +433,11 @@ const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) =
431
433
  ]
432
434
  })
433
435
 
434
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
436
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
437
+ HTMLElement,
438
+ [],
439
+ ElementProps
440
+ >((handle) => {
435
441
  let context = getSelectContext(handle)
436
442
  let popoverState = handle.context.get(popover.Context)
437
443
 
@@ -456,7 +462,11 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
456
462
  ]
457
463
  })
458
464
 
459
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
465
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
466
+ HTMLElement,
467
+ [],
468
+ ElementProps
469
+ >((handle) => {
460
470
  let context = getSelectContext(handle)
461
471
 
462
472
  return () => [
@@ -469,7 +479,11 @@ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
469
479
  ]
470
480
  })
471
481
 
472
- const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
482
+ const hiddenInputMixin: MixinFactory<HTMLInputElement, [], ElementProps> = createMixin<
483
+ HTMLInputElement,
484
+ [],
485
+ ElementProps
486
+ >((handle) => {
473
487
  let context = getSelectContext(handle)
474
488
 
475
489
  return () =>
@@ -532,17 +546,20 @@ const select = {
532
546
  trigger,
533
547
  } as const
534
548
 
535
- export function onSelectChange(handler: SelectChangeHandler, captureBoolean?: boolean) {
536
- return on<HTMLElement, typeof SELECT_CHANGE_EVENT>(SELECT_CHANGE_EVENT, handler, captureBoolean)
549
+ export function onSelectChange<target extends HTMLElement>(
550
+ handler: SelectChangeHandler<target>,
551
+ captureBoolean?: boolean,
552
+ ): ReturnType<typeof on<target, typeof SELECT_CHANGE_EVENT>> {
553
+ return on(SELECT_CHANGE_EVENT, handler, captureBoolean)
537
554
  }
538
555
 
539
- function SelectLabel(handle: Handle) {
556
+ function SelectLabel(handle: Handle): () => RemixNode {
540
557
  let context = getSelectContext(handle)
541
558
 
542
559
  return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
543
560
  }
544
561
 
545
- export function Select(handle: Handle<SelectProps>) {
562
+ export function Select(handle: Handle<SelectProps>): () => RemixNode {
546
563
  return () => {
547
564
  let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
548
565
 
@@ -568,7 +585,7 @@ export function Select(handle: Handle<SelectProps>) {
568
585
  }
569
586
  }
570
587
 
571
- export function Option(handle: Handle<SelectOptionProps>) {
588
+ export function Option(handle: Handle<SelectOptionProps>): () => RemixNode {
572
589
  return () => {
573
590
  let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
574
591
 
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import {
4
2
  attrs,
5
3
  css,
@@ -7,6 +5,7 @@ import {
7
5
  createMixin,
8
6
  on,
9
7
  type CSSMixinDescriptor,
8
+ type Dispatched,
10
9
  type ElementProps,
11
10
  type Handle,
12
11
  type MixinHandle,
@@ -18,7 +17,10 @@ import { theme } from '../../theme/theme.ts'
18
17
 
19
18
  const TABS_CHANGE_EVENT = 'rmx:tabs-change' as const
20
19
 
21
- type TabsChangeHandler = (event: TabsChangeEvent, signal: AbortSignal) => void | Promise<void>
20
+ type TabsChangeHandler<target extends HTMLElement> = (
21
+ event: Dispatched<TabsChangeEvent, target>,
22
+ signal: AbortSignal,
23
+ ) => void | Promise<void>
22
24
 
23
25
  const tabsListCss: CSSMixinDescriptor = css({
24
26
  display: 'inline-flex',
@@ -446,8 +448,11 @@ export const trigger = triggerMixin
446
448
 
447
449
  const tabs = { Context, list, panel, trigger } as const
448
450
 
449
- export function onTabsChange(handler: TabsChangeHandler, captureBoolean?: boolean) {
450
- return on<HTMLElement, typeof TABS_CHANGE_EVENT>(TABS_CHANGE_EVENT, handler, captureBoolean)
451
+ export function onTabsChange<target extends HTMLElement>(
452
+ handler: TabsChangeHandler<target>,
453
+ captureBoolean?: boolean,
454
+ ) {
455
+ return on(TABS_CHANGE_EVENT, handler, captureBoolean)
451
456
  }
452
457
 
453
458
  export function Tabs(handle: Handle<TabsProps>) {
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type { HostProps, LayoutAnimationConfig } from './runtime/dom.ts'
51
51
  export { createMixin } from './runtime/mixins/mixin.ts'
52
52
  export type {
53
53
  MixinDescriptor,
54
+ MixinFactory,
54
55
  MixinHandle,
55
56
  MixinType,
56
57
  MixInput,
@@ -58,8 +59,8 @@ export type {
58
59
  } from './runtime/mixins/mixin.ts'
59
60
  export { TypedEventTarget } from './runtime/typed-event-target.ts'
60
61
  export { addEventListeners } from './runtime/event-listeners.ts'
62
+ export type { Dispatched } from './runtime/event-listeners.ts'
61
63
  export { on } from './runtime/mixins/on-mixin.ts'
62
- export type { Dispatched } from './runtime/mixins/on-mixin.ts'
63
64
  export { link } from './runtime/mixins/link-mixin.ts'
64
65
  export { ref } from './runtime/mixins/ref-mixin.ts'
65
66
  export type { RefCallback } from './runtime/mixins/ref-mixin.ts'
@@ -118,13 +118,18 @@ export type ContextFrom<ComponentType> =
118
118
 
119
119
  /**
120
120
  * Context storage API exposed on component handles.
121
+ *
122
+ * Context values are keyed by provider component identity. `get(Component)`
123
+ * reads the nearest ancestor instance whose component function is exactly
124
+ * `Component`, so nested instances of the same provider shadow outer instances
125
+ * while different component types remain independent.
121
126
  */
122
127
  export interface Context<C> {
123
128
  /** Replaces the current context value for this component instance. */
124
129
  set(values: C): void
125
- /** Reads the context value associated with the given component type. */
130
+ /** Reads the context value from the nearest ancestor instance of the given component type. */
126
131
  get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
127
- /** Reads the context value associated with the given component key. */
132
+ /** Reads an unknown context value for an untyped lookup. */
128
133
  get(component: ElementType | symbol): unknown | undefined
129
134
  }
130
135
 
@@ -218,7 +223,14 @@ type ComponentConfig = {
218
223
  /**
219
224
  * Runtime handle returned by {@link createComponent}.
220
225
  */
221
- export type ComponentHandle = ReturnType<typeof createComponent>
226
+ export interface ComponentHandle<C = NoContext> {
227
+ frame: FrameHandle
228
+ render(nextProps: ElementProps): [RemixNode, Array<() => void>]
229
+ remove(): Array<() => void>
230
+ setScheduleUpdate(nextScheduleUpdate: () => void): void
231
+ getContextValue(): C | undefined
232
+ isRemoved(): boolean
233
+ }
222
234
 
223
235
  /**
224
236
  * Creates the internal runtime wrapper for a component instance.
@@ -226,114 +238,136 @@ export type ComponentHandle = ReturnType<typeof createComponent>
226
238
  * @param config Component runtime configuration.
227
239
  * @returns Component runtime helpers used by the reconciler.
228
240
  */
229
- export function createComponent<C = NoContext>(config: ComponentConfig) {
230
- let taskQueue: Task[] = []
231
- let renderCtrl: AbortController | null = null
232
- let connectedCtrl: AbortController | null = null
233
- let contextValue: C | undefined = undefined
234
-
235
- function getConnectedSignal() {
236
- if (!connectedCtrl) connectedCtrl = new AbortController()
237
- return connectedCtrl.signal
238
- }
239
-
240
- let getContent: null | RenderFn = null
241
- let scheduleUpdate: () => void = () => {
242
- throw new Error('scheduleUpdate not implemented')
243
- }
244
- let props = {} as ElementProps
241
+ export function createComponent<C = NoContext>(config: ComponentConfig): ComponentHandle<C> {
242
+ return new ComponentRuntime<C>(config)
243
+ }
245
244
 
246
- let context: Context<C> = {
247
- set: (value: C) => {
248
- contextValue = value
249
- },
250
- get: (type: Component) => config.getContext(type),
251
- }
245
+ class ComponentRuntime<C = NoContext> implements ComponentHandle<C> {
246
+ frame: FrameHandle
252
247
 
253
- let handle: Handle<ElementProps, C> = {
254
- id: config.id,
255
- props,
256
- update: () =>
257
- new Promise((resolve) => {
258
- taskQueue.push((signal) => resolve(signal))
259
- scheduleUpdate()
260
- }),
261
- queueTask: (task: Task) => {
262
- taskQueue.push(task)
263
- },
264
- frame: config.frame,
265
- frames: {
266
- get top() {
267
- return config.getTopFrame?.() ?? config.frame
268
- },
269
- get(name: string) {
270
- return config.getFrameByName(name)
271
- },
272
- },
273
- context: context,
274
- get signal() {
275
- return config.signal ?? getConnectedSignal()
276
- },
248
+ #config: ComponentConfig
249
+ #connectedController: AbortController | undefined
250
+ #contextValue: C | undefined
251
+ #handle: Handle<ElementProps, C>
252
+ #props = {} as ElementProps
253
+ #renderController: AbortController | undefined
254
+ #renderFn: RenderFn | undefined
255
+ #removed = false
256
+ #scheduleUpdate: () => void = () => {
257
+ throw new Error('scheduleUpdate not implemented')
277
258
  }
259
+ #tasks: Task[] = []
278
260
 
279
- function dequeueTasks(): (() => void)[] {
280
- // Only create render controller if any task expects a signal (has length >= 1)
281
- let needsSignal = taskQueue.some((task) => task.length >= 1)
282
- if (needsSignal && !renderCtrl) {
283
- renderCtrl = new AbortController()
284
- }
285
- let signal = renderCtrl?.signal
286
- return taskQueue.splice(0, taskQueue.length).map((task) => () => task(signal!))
261
+ constructor(config: ComponentConfig) {
262
+ this.#config = config
263
+ this.frame = config.frame
264
+ this.#handle = this.#createHandle()
287
265
  }
288
266
 
289
- function render(props: ElementProps): [RemixNode, Array<() => void>] {
290
- if (connectedCtrl?.signal.aborted) {
267
+ render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
268
+ if (this.#removed) {
291
269
  console.warn('render called after component was removed, potential application memory leak')
292
270
  return [null, []]
293
271
  }
294
272
 
295
- // Only abort render controller if it was initialized
296
- if (renderCtrl) {
297
- renderCtrl.abort()
298
- renderCtrl = null
299
- }
273
+ this.#abortRenderSignal()
274
+ syncProps(this.#props, nextProps)
275
+
276
+ let renderFn = this.#renderFn
300
277
 
301
- syncProps(handle.props, props)
278
+ if (renderFn === undefined) {
279
+ let result = this.#config.type(this.#handle)
302
280
 
303
- let renderContent = getContent
304
- if (!renderContent) {
305
- let result = config.type(handle)
306
281
  if (typeof result !== 'function') {
307
- let name = config.type.name || 'Anonymous'
282
+ let name = this.#config.type.name || 'Anonymous'
308
283
  throw new Error(`${name} must return a render function, received ${typeof result}`)
309
- } else {
310
- getContent = result
311
- renderContent = result
312
284
  }
313
- }
314
- if (!renderContent) {
315
- throw new Error('component render function was not initialized')
285
+
286
+ renderFn = result as RenderFn
287
+ this.#renderFn = renderFn
316
288
  }
317
289
 
318
- let node = renderContent(handle.props)
319
- return [node, dequeueTasks()]
290
+ return [renderFn(this.#props), this.#dequeueTasks()]
320
291
  }
321
292
 
322
- function remove(): (() => void)[] {
323
- connectedCtrl?.abort()
324
- renderCtrl?.abort()
325
- return dequeueTasks()
293
+ remove = (): Array<() => void> => {
294
+ if (this.#removed) return []
295
+ this.#removed = true
296
+ this.#connectedController?.abort()
297
+ this.#abortRenderSignal()
298
+ return this.#dequeueTasks(AbortSignal.abort())
326
299
  }
327
300
 
328
- function setScheduleUpdate(nextScheduleUpdate: () => void) {
329
- scheduleUpdate = nextScheduleUpdate
301
+ setScheduleUpdate = (nextScheduleUpdate: () => void): void => {
302
+ this.#scheduleUpdate = nextScheduleUpdate
330
303
  }
331
304
 
332
- function getContextValue(): C | undefined {
333
- return contextValue
305
+ getContextValue = (): C | undefined => this.#contextValue
306
+
307
+ isRemoved = (): boolean => this.#removed
308
+
309
+ #createHandle(): Handle<ElementProps, C> {
310
+ let component = this
311
+ let context: Context<C> = {
312
+ set: (value: C) => {
313
+ this.#contextValue = value
314
+ },
315
+ get: (type: ElementType | symbol) => this.#config.getContext(type as Component),
316
+ }
317
+
318
+ return {
319
+ id: this.#config.id,
320
+ props: this.#props,
321
+ update: () =>
322
+ new Promise((resolve) => {
323
+ if (component.#removed) {
324
+ resolve(AbortSignal.abort())
325
+ return
326
+ }
327
+
328
+ this.#tasks.push((signal) => resolve(signal))
329
+ this.#scheduleUpdate()
330
+ }),
331
+ queueTask: (task: Task) => {
332
+ this.#tasks.push(task)
333
+ },
334
+ frame: this.#config.frame,
335
+ frames: {
336
+ get top() {
337
+ return component.#config.getTopFrame?.() ?? component.#config.frame
338
+ },
339
+ get(name: string) {
340
+ return component.#config.getFrameByName(name)
341
+ },
342
+ },
343
+ context,
344
+ get signal() {
345
+ return component.#config.signal ?? component.#connectedSignal()
346
+ },
347
+ }
348
+ }
349
+
350
+ #connectedSignal(): AbortSignal {
351
+ this.#connectedController ??= new AbortController()
352
+ return this.#connectedController.signal
353
+ }
354
+
355
+ #abortRenderSignal(): void {
356
+ this.#renderController?.abort()
357
+ this.#renderController = undefined
334
358
  }
335
359
 
336
- return { render, remove, setScheduleUpdate, frame: config.frame, getContextValue }
360
+ #dequeueTasks(signal?: AbortSignal): Array<() => void> {
361
+ let needsSignal = signal === undefined && this.#tasks.some((task) => task.length >= 1)
362
+
363
+ if (needsSignal) {
364
+ this.#renderController ??= new AbortController()
365
+ }
366
+
367
+ signal ??= this.#renderController?.signal
368
+ let tasks = this.#tasks.splice(0, this.#tasks.length)
369
+ return tasks.map((task) => () => task(signal!))
370
+ }
337
371
  }
338
372
 
339
373
  function syncProps(target: ElementProps, next: ElementProps): void {
@@ -354,7 +388,7 @@ function syncProps(target: ElementProps, next: ElementProps): void {
354
388
  * @param handle Component handle for the frame instance.
355
389
  * @returns A placeholder render function handled by the reconciler.
356
390
  */
357
- export function Frame(handle: Handle<FrameProps, FrameHandle>) {
391
+ export function Frame(handle: Handle<FrameProps, FrameHandle>): RenderFn {
358
392
  void handle
359
393
  return () => null // reconciler renders
360
394
  }
@@ -365,7 +399,7 @@ export function Frame(handle: Handle<FrameProps, FrameHandle>) {
365
399
  * @param handle Component handle for the fragment instance.
366
400
  * @returns A placeholder render function handled by the reconciler.
367
401
  */
368
- export function Fragment(handle: Handle<FragmentProps>) {
402
+ export function Fragment(handle: Handle<FragmentProps>): RenderFn {
369
403
  void handle
370
404
  return () => null // reconciler renders
371
405
  }