@remix-run/ui 0.1.0 → 0.1.2

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 (200) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +51 -52
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -27
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.d.ts +6 -0
  108. package/dist/runtime/navigation.js +16 -3
  109. package/dist/runtime/navigation.js.map +1 -1
  110. package/dist/runtime/reconcile.js +444 -181
  111. package/dist/runtime/reconcile.js.map +1 -1
  112. package/dist/runtime/run.d.ts +17 -2
  113. package/dist/runtime/run.js.map +1 -1
  114. package/dist/runtime/scheduler.js +10 -0
  115. package/dist/runtime/scheduler.js.map +1 -1
  116. package/dist/runtime/stream-protocol.d.ts +8 -0
  117. package/dist/runtime/stream-protocol.js +20 -0
  118. package/dist/runtime/stream-protocol.js.map +1 -0
  119. package/dist/runtime/to-vnode.js +17 -18
  120. package/dist/runtime/to-vnode.js.map +1 -1
  121. package/dist/runtime/vnode.d.ts +5 -4
  122. package/dist/runtime/vnode.js +2 -3
  123. package/dist/runtime/vnode.js.map +1 -1
  124. package/dist/server/stream.js +80 -106
  125. package/dist/server/stream.js.map +1 -1
  126. package/dist/style/layers.d.ts +2 -0
  127. package/dist/style/layers.js +3 -0
  128. package/dist/style/layers.js.map +1 -0
  129. package/dist/style/stylesheet.d.ts +3 -1
  130. package/dist/style/stylesheet.js +69 -8
  131. package/dist/style/stylesheet.js.map +1 -1
  132. package/dist/theme/glyph-contract.d.ts +1 -1
  133. package/dist/theme/runtime.js +4 -1
  134. package/dist/theme/runtime.js.map +1 -1
  135. package/package.json +6 -7
  136. package/src/animation/README.md +314 -0
  137. package/src/animation/animate-layout-mixin.ts +79 -71
  138. package/src/components/accordion/README.md +166 -0
  139. package/src/components/accordion/accordion.tsx +8 -10
  140. package/src/components/accordion/demos/card.demo.tsx +113 -0
  141. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  142. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  143. package/src/components/anchor/README.md +153 -0
  144. package/src/components/breadcrumbs/README.md +55 -0
  145. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  146. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  147. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  148. package/src/components/button/README.md +4 -4
  149. package/src/components/button/button.tsx +88 -2
  150. package/src/components/button/demos/aliases.demo.tsx +27 -0
  151. package/src/components/button/demos/basic.demo.tsx +30 -0
  152. package/src/components/button/demos/states.demo.tsx +52 -0
  153. package/src/components/combobox/combobox.tsx +8 -10
  154. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  155. package/src/components/glyph/README.md +72 -0
  156. package/src/components/listbox/README.md +115 -0
  157. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  158. package/src/components/listbox/listbox.ts +23 -32
  159. package/src/components/menu/README.md +96 -0
  160. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  161. package/src/components/menu/demos/overview.demo.tsx +76 -0
  162. package/src/components/menu/menu.tsx +35 -41
  163. package/src/components/popover/README.md +3 -3
  164. package/src/components/popover/demos/overview.demo.tsx +143 -0
  165. package/src/components/select/README.md +107 -0
  166. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  167. package/src/components/select/demos/overview.demo.tsx +51 -0
  168. package/src/components/select/select.tsx +28 -28
  169. package/src/components/tabs/tabs.tsx +10 -5
  170. package/src/index.ts +1 -1
  171. package/src/runtime/component.ts +94 -82
  172. package/src/runtime/core/attributes.ts +97 -0
  173. package/src/runtime/core/children.ts +42 -0
  174. package/src/runtime/core/props.ts +185 -0
  175. package/src/runtime/core/vnode.ts +48 -0
  176. package/src/runtime/create-element.ts +2 -1
  177. package/src/runtime/diff-dom.ts +59 -12
  178. package/src/runtime/diff-props.ts +2 -187
  179. package/src/runtime/dom.ts +4 -0
  180. package/src/runtime/frame.ts +84 -61
  181. package/src/runtime/jsx.ts +2 -30
  182. package/src/runtime/mixins/mixin.ts +9 -4
  183. package/src/runtime/mixins/on-mixin.ts +20 -12
  184. package/src/runtime/navigation.ts +24 -3
  185. package/src/runtime/reconcile.ts +552 -186
  186. package/src/runtime/run.ts +17 -1
  187. package/src/runtime/scheduler.ts +7 -0
  188. package/src/runtime/stream-protocol.ts +27 -0
  189. package/src/runtime/to-vnode.ts +15 -17
  190. package/src/runtime/vnode.ts +6 -9
  191. package/src/server/README.md +88 -0
  192. package/src/server/stream.ts +105 -117
  193. package/src/style/layers.ts +2 -0
  194. package/src/style/stylesheet.ts +75 -10
  195. package/src/test/README.md +107 -0
  196. package/src/test/utils.ts +3 -1
  197. package/src/theme/README.md +103 -0
  198. package/src/theme/runtime.ts +4 -1
  199. package/src/utils/scroll-lock/README.md +33 -0
  200. package/src/test/setup.ts +0 -7
@@ -227,113 +227,125 @@ export type ComponentHandle = ReturnType<typeof createComponent>
227
227
  * @returns Component runtime helpers used by the reconciler.
228
228
  */
229
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
230
+ return new ComponentRuntime<C>(config)
231
+ }
245
232
 
246
- let context: Context<C> = {
247
- set: (value: C) => {
248
- contextValue = value
249
- },
250
- get: (type: Component) => config.getContext(type),
251
- }
233
+ class ComponentRuntime<C = NoContext> {
234
+ frame: FrameHandle
252
235
 
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
- },
236
+ #config: ComponentConfig
237
+ #connectedController: AbortController | undefined
238
+ #contextValue: C | undefined
239
+ #handle: Handle<ElementProps, C>
240
+ #props = {} as ElementProps
241
+ #renderController: AbortController | undefined
242
+ #renderFn: RenderFn | undefined
243
+ #scheduleUpdate: () => void = () => {
244
+ throw new Error('scheduleUpdate not implemented')
277
245
  }
246
+ #tasks: Task[] = []
278
247
 
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!))
248
+ constructor(config: ComponentConfig) {
249
+ this.#config = config
250
+ this.frame = config.frame
251
+ this.#handle = this.#createHandle()
287
252
  }
288
253
 
289
- function render(props: ElementProps): [RemixNode, Array<() => void>] {
290
- if (connectedCtrl?.signal.aborted) {
254
+ render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
255
+ if (this.#connectedController?.signal.aborted) {
291
256
  console.warn('render called after component was removed, potential application memory leak')
292
257
  return [null, []]
293
258
  }
294
259
 
295
- // Only abort render controller if it was initialized
296
- if (renderCtrl) {
297
- renderCtrl.abort()
298
- renderCtrl = null
299
- }
260
+ this.#abortRenderSignal()
261
+ syncProps(this.#props, nextProps)
300
262
 
301
- syncProps(handle.props, props)
263
+ let renderFn = this.#renderFn
264
+
265
+ if (renderFn === undefined) {
266
+ let result = this.#config.type(this.#handle)
302
267
 
303
- let renderContent = getContent
304
- if (!renderContent) {
305
- let result = config.type(handle)
306
268
  if (typeof result !== 'function') {
307
- let name = config.type.name || 'Anonymous'
269
+ let name = this.#config.type.name || 'Anonymous'
308
270
  throw new Error(`${name} must return a render function, received ${typeof result}`)
309
- } else {
310
- getContent = result
311
- renderContent = result
312
271
  }
313
- }
314
- if (!renderContent) {
315
- throw new Error('component render function was not initialized')
272
+
273
+ renderFn = result as RenderFn
274
+ this.#renderFn = renderFn
316
275
  }
317
276
 
318
- let node = renderContent(handle.props)
319
- return [node, dequeueTasks()]
277
+ return [renderFn(this.#props), this.#dequeueTasks()]
320
278
  }
321
279
 
322
- function remove(): (() => void)[] {
323
- connectedCtrl?.abort()
324
- renderCtrl?.abort()
325
- return dequeueTasks()
280
+ remove = (): Array<() => void> => {
281
+ this.#connectedController?.abort()
282
+ this.#abortRenderSignal()
283
+ return this.#dequeueTasks()
326
284
  }
327
285
 
328
- function setScheduleUpdate(nextScheduleUpdate: () => void) {
329
- scheduleUpdate = nextScheduleUpdate
286
+ setScheduleUpdate = (nextScheduleUpdate: () => void): void => {
287
+ this.#scheduleUpdate = nextScheduleUpdate
330
288
  }
331
289
 
332
- function getContextValue(): C | undefined {
333
- return contextValue
290
+ getContextValue = (): C | undefined => this.#contextValue
291
+
292
+ #createHandle(): Handle<ElementProps, C> {
293
+ let component = this
294
+ let context: Context<C> = {
295
+ set: (value: C) => {
296
+ this.#contextValue = value
297
+ },
298
+ get: (type: ElementType | symbol) => this.#config.getContext(type as Component),
299
+ }
300
+
301
+ return {
302
+ id: this.#config.id,
303
+ props: this.#props,
304
+ update: () =>
305
+ new Promise((resolve) => {
306
+ this.#tasks.push((signal) => resolve(signal))
307
+ this.#scheduleUpdate()
308
+ }),
309
+ queueTask: (task: Task) => {
310
+ this.#tasks.push(task)
311
+ },
312
+ frame: this.#config.frame,
313
+ frames: {
314
+ get top() {
315
+ return component.#config.getTopFrame?.() ?? component.#config.frame
316
+ },
317
+ get(name: string) {
318
+ return component.#config.getFrameByName(name)
319
+ },
320
+ },
321
+ context,
322
+ get signal() {
323
+ return component.#config.signal ?? component.#connectedSignal()
324
+ },
325
+ }
334
326
  }
335
327
 
336
- return { render, remove, setScheduleUpdate, frame: config.frame, getContextValue }
328
+ #connectedSignal(): AbortSignal {
329
+ this.#connectedController ??= new AbortController()
330
+ return this.#connectedController.signal
331
+ }
332
+
333
+ #abortRenderSignal(): void {
334
+ this.#renderController?.abort()
335
+ this.#renderController = undefined
336
+ }
337
+
338
+ #dequeueTasks(): Array<() => void> {
339
+ let needsSignal = this.#tasks.some((task) => task.length >= 1)
340
+
341
+ if (needsSignal) {
342
+ this.#renderController ??= new AbortController()
343
+ }
344
+
345
+ let signal = this.#renderController?.signal
346
+ let tasks = this.#tasks.splice(0, this.#tasks.length)
347
+ return tasks.map((task) => () => task(signal!))
348
+ }
337
349
  }
338
350
 
339
351
  function syncProps(target: ElementProps, next: ElementProps): void {
@@ -0,0 +1,97 @@
1
+ import { normalizeCssValue } from '../../style/style.ts'
2
+ import { normalizeSvgAttribute } from '../svg-attributes.ts'
3
+ import type { ElementProps } from '../jsx.ts'
4
+
5
+ const ATTRIBUTE_FALLBACK_NAMES = new Set([
6
+ 'width',
7
+ 'height',
8
+ 'href',
9
+ 'list',
10
+ 'form',
11
+ 'tabIndex',
12
+ 'download',
13
+ 'rowSpan',
14
+ 'colSpan',
15
+ 'role',
16
+ 'popover',
17
+ ])
18
+
19
+ export const FRAMEWORK_PROPS = new Set(['children', 'mix', 'key', 'animate', 'innerHTML', 'on'])
20
+
21
+ export const SELF_CLOSING_TAGS = new Set([
22
+ 'area',
23
+ 'base',
24
+ 'br',
25
+ 'col',
26
+ 'embed',
27
+ 'hr',
28
+ 'img',
29
+ 'input',
30
+ 'link',
31
+ 'meta',
32
+ 'param',
33
+ 'source',
34
+ 'track',
35
+ 'wbr',
36
+ ])
37
+
38
+ export function isChildlessElement(name: string): boolean {
39
+ return SELF_CLOSING_TAGS.has(name)
40
+ }
41
+
42
+ export function canUseProperty(
43
+ element: Element,
44
+ name: string,
45
+ isSvg: boolean,
46
+ ): element is Element & Record<string, unknown> {
47
+ if (isSvg) return false
48
+ if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
49
+ return name in element
50
+ }
51
+
52
+ export function normalizeAttributeName(
53
+ name: string,
54
+ isSvg: boolean,
55
+ ): { ns?: string; attr: string } {
56
+ if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }
57
+
58
+ if (name === 'className') return { attr: 'class' }
59
+
60
+ if (!isSvg) {
61
+ if (name === 'htmlFor') return { attr: 'for' }
62
+ if (name === 'tabIndex') return { attr: 'tabindex' }
63
+ if (name === 'acceptCharset') return { attr: 'accept-charset' }
64
+ if (name === 'httpEquiv') return { attr: 'http-equiv' }
65
+ return { attr: name.toLowerCase() }
66
+ }
67
+
68
+ return normalizeSvgAttribute(name)
69
+ }
70
+
71
+ export function serializeStyleObject(style: Record<string, unknown>): string {
72
+ let parts: string[] = []
73
+
74
+ for (let [key, value] of Object.entries(style)) {
75
+ if (value == null) continue
76
+ if (typeof value === 'boolean') continue
77
+ if (typeof value === 'number' && !Number.isFinite(value)) continue
78
+
79
+ let cssKey = toKebabCase(key)
80
+ let cssValue = Array.isArray(value) ? value.join(', ') : normalizeCssValue(key, value)
81
+
82
+ parts.push(`${cssKey}: ${cssValue};`)
83
+ }
84
+
85
+ return parts.join(' ')
86
+ }
87
+
88
+ export function getMergedClassName(props: ElementProps): string | undefined {
89
+ let classAttr = typeof props.class === 'string' ? props.class : ''
90
+ let className = typeof props.className === 'string' ? props.className : ''
91
+ let merged = classAttr && className ? `${classAttr} ${className}` : classAttr || className
92
+ return merged || undefined
93
+ }
94
+
95
+ export function toKebabCase(value: string): string {
96
+ return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
97
+ }
@@ -0,0 +1,42 @@
1
+ import type { RemixNode } from '../jsx.ts'
2
+
3
+ export type EmptyChild = boolean | null | undefined
4
+ export type PrimitiveChild = string | number | bigint
5
+
6
+ export function isEmptyChild(value: RemixNode): value is EmptyChild {
7
+ return value == null || typeof value === 'boolean'
8
+ }
9
+
10
+ export function isPrimitiveChild(value: RemixNode): value is PrimitiveChild {
11
+ let type = typeof value
12
+ return type === 'string' || type === 'number' || type === 'bigint'
13
+ }
14
+
15
+ export function normalizeChildren(children: readonly RemixNode[]): RemixNode[] {
16
+ for (let child of children) {
17
+ if (Array.isArray(child)) {
18
+ return (children as unknown[]).flat(Infinity) as RemixNode[]
19
+ }
20
+ }
21
+
22
+ return children as RemixNode[]
23
+ }
24
+
25
+ export function packChildren(children: readonly RemixNode[]): RemixNode | undefined {
26
+ if (children.length === 0) {
27
+ return undefined
28
+ }
29
+
30
+ if (children.length === 1) {
31
+ let child = children[0]
32
+
33
+ if (child === undefined || isEmptyChild(child)) {
34
+ return undefined
35
+ }
36
+
37
+ return Array.isArray(child) ? normalizeChildren(child) : child
38
+ }
39
+
40
+ let normalized = normalizeChildren(children)
41
+ return normalized.length === 0 ? undefined : normalized
42
+ }
@@ -0,0 +1,185 @@
1
+ import type { ElementProps } from '../jsx.ts'
2
+ import {
3
+ canUseProperty,
4
+ getMergedClassName,
5
+ normalizeAttributeName,
6
+ serializeStyleObject,
7
+ toKebabCase,
8
+ } from './attributes.ts'
9
+ import { normalizeCssValue } from '../../style/style.ts'
10
+
11
+ const SVG_NS = 'http://www.w3.org/2000/svg'
12
+
13
+ function isFrameworkProp(name: string): boolean {
14
+ return (
15
+ name === 'children' ||
16
+ name === 'mix' ||
17
+ name === 'key' ||
18
+ name === 'animate' ||
19
+ name === 'innerHTML' ||
20
+ name === 'on'
21
+ )
22
+ }
23
+
24
+ function toLocalName(attrName: string): string {
25
+ let separatorIndex = attrName.indexOf(':')
26
+ if (separatorIndex === -1) return attrName
27
+ return attrName.slice(separatorIndex + 1)
28
+ }
29
+
30
+ function clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {
31
+ try {
32
+ if (name === 'value' || name === 'defaultValue') {
33
+ dom[name] = ''
34
+ return
35
+ }
36
+ if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {
37
+ dom[name] = false
38
+ return
39
+ }
40
+ if (name === 'selectedIndex') {
41
+ dom[name] = -1
42
+ }
43
+ } catch {}
44
+ }
45
+
46
+ /**
47
+ * Patches DOM attributes and selected properties from old props to new props.
48
+ *
49
+ * @param curr Previous host props.
50
+ * @param next Next host props.
51
+ * @param dom Element to patch.
52
+ */
53
+ export function patchHostProps(curr: ElementProps, next: ElementProps, dom: Element): void {
54
+ let isSvg = dom.namespaceURI === SVG_NS
55
+ let currClassName = getMergedClassName(curr)
56
+ let nextClassName = getMergedClassName(next)
57
+
58
+ if (currClassName !== nextClassName) {
59
+ if (nextClassName) {
60
+ dom.setAttribute('class', nextClassName)
61
+ } else {
62
+ dom.removeAttribute('class')
63
+ }
64
+ }
65
+
66
+ for (let name in curr) {
67
+ if (isFrameworkProp(name)) continue
68
+ if (name === 'class' || name === 'className') continue
69
+ if (name in next && next[name] != null) continue
70
+
71
+ if (canUseProperty(dom, name, isSvg)) {
72
+ clearRuntimePropertyOnRemoval(dom, name)
73
+ }
74
+
75
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
76
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
77
+ else dom.removeAttribute(attr)
78
+ }
79
+
80
+ for (let name in next) {
81
+ if (isFrameworkProp(name)) continue
82
+ if (name === 'class' || name === 'className') continue
83
+
84
+ let nextValue = next[name]
85
+ if (nextValue == null) continue
86
+
87
+ let prevValue = curr[name]
88
+ if (prevValue === nextValue) continue
89
+
90
+ if (name === 'style' && isStyleObject(nextValue)) {
91
+ if (isStyleObject(prevValue)) {
92
+ patchStyleObject(dom, prevValue, nextValue)
93
+ } else {
94
+ dom.removeAttribute('style')
95
+ patchStyleObject(dom, undefined, nextValue)
96
+ }
97
+ continue
98
+ }
99
+
100
+ patchHostProp(dom, name, nextValue, isSvg)
101
+ }
102
+ }
103
+
104
+ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolean): void {
105
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
106
+
107
+ if (attr === 'style' && isStyleObject(value)) {
108
+ patchStyleObject(dom, undefined, value)
109
+ return
110
+ }
111
+
112
+ if (attr === 'style' && typeof value === 'string') {
113
+ dom.setAttribute('style', value)
114
+ return
115
+ }
116
+
117
+ if (canUseProperty(dom, name, isSvg)) {
118
+ try {
119
+ dom[name] = value == null ? '' : value
120
+ return
121
+ } catch {}
122
+ }
123
+
124
+ if (typeof value === 'function') return
125
+
126
+ let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
127
+ if (value != null && (value !== false || isAriaOrData)) {
128
+ let attrValue = name === 'popover' && value === true ? '' : String(value)
129
+ if (ns) dom.setAttributeNS(ns, attr, attrValue)
130
+ else dom.setAttribute(attr, attrValue)
131
+ } else {
132
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
133
+ else dom.removeAttribute(attr)
134
+ }
135
+ }
136
+
137
+ function isStyleObject(value: unknown): value is Record<string, unknown> {
138
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
139
+ }
140
+
141
+ function patchStyleObject(
142
+ dom: Element,
143
+ curr: Record<string, unknown> | undefined,
144
+ next: Record<string, unknown>,
145
+ ): void {
146
+ if (!(dom instanceof HTMLElement || dom instanceof SVGElement)) {
147
+ dom.setAttribute('style', serializeStyleObject(next))
148
+ return
149
+ }
150
+
151
+ let style = dom.style
152
+ if (curr) {
153
+ for (let name in curr) {
154
+ let nextCssValue = styleValueToCss(name, next[name])
155
+ if (nextCssValue !== undefined) continue
156
+
157
+ let prevCssValue = styleValueToCss(name, curr[name])
158
+ if (prevCssValue === undefined) continue
159
+
160
+ style.removeProperty(toKebabCase(name))
161
+ }
162
+ }
163
+
164
+ for (let name in next) {
165
+ let nextCssValue = styleValueToCss(name, next[name])
166
+ if (nextCssValue === undefined) continue
167
+
168
+ let prevCssValue = curr ? styleValueToCss(name, curr[name]) : undefined
169
+ if (prevCssValue === nextCssValue) continue
170
+
171
+ style.setProperty(toKebabCase(name), nextCssValue)
172
+ }
173
+ }
174
+
175
+ function styleValueToCss(name: string, value: unknown): string | undefined {
176
+ if (value == null) return undefined
177
+ if (typeof value === 'boolean') return undefined
178
+ if (typeof value === 'number' && !Number.isFinite(value)) return undefined
179
+
180
+ if (Array.isArray(value)) {
181
+ return value.join(', ')
182
+ }
183
+
184
+ return normalizeCssValue(name, value)
185
+ }
@@ -0,0 +1,48 @@
1
+ import type { ElementProps, ElementType, RemixElement, RemixNode } from '../jsx.ts'
2
+
3
+ export function createRemixElement(
4
+ type: ElementType,
5
+ props: ElementProps | null | undefined,
6
+ key?: string,
7
+ ): RemixElement {
8
+ return {
9
+ $rmx: true,
10
+ key,
11
+ props: normalizeElementProps(props),
12
+ type,
13
+ }
14
+ }
15
+
16
+ export function isRemixElement(node: RemixNode): node is RemixElement {
17
+ return typeof node === 'object' && node !== null && '$rmx' in node
18
+ }
19
+
20
+ function normalizeElementProps(props: ElementProps | null | undefined): ElementProps {
21
+ if (!props) return {}
22
+ if (!('mix' in props)) return props
23
+
24
+ let { mix, ...rest } = props
25
+ let normalizedMix = normalizeMixValue(mix)
26
+ return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }
27
+ }
28
+
29
+ function normalizeMixValue(mix: unknown): unknown[] | undefined {
30
+ if (!mix) return undefined
31
+
32
+ let normalizedMix: unknown[] = []
33
+ flattenMixValue(mix, normalizedMix)
34
+ return normalizedMix.length === 0 ? undefined : normalizedMix
35
+ }
36
+
37
+ function flattenMixValue(mix: unknown, out: unknown[]): void {
38
+ if (!mix) return
39
+
40
+ if (!Array.isArray(mix)) {
41
+ out.push(mix)
42
+ return
43
+ }
44
+
45
+ for (let item of mix) {
46
+ flattenMixValue(item, out)
47
+ }
48
+ }
@@ -1,5 +1,6 @@
1
1
  import { jsx } from './jsx.ts'
2
2
  import type { ElementType, RemixElement } from './jsx.ts'
3
+ import { normalizeChildren } from './core/children.ts'
3
4
 
4
5
  /**
5
6
  * Creates a Remix virtual element from a JSX-like call signature.
@@ -20,7 +21,7 @@ export function createElement(
20
21
  console.error(new Error('mixin elements must not receive children'))
21
22
  }
22
23
  } else {
23
- nextProps.children = children
24
+ nextProps.children = normalizeChildren(children)
24
25
  }
25
26
 
26
27
  if (nextProps.key != null) {