@furystack/shades 11.1.0 → 12.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/README.md +13 -13
  3. package/esm/component-factory.spec.js +13 -5
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/index.d.ts +4 -1
  6. package/esm/components/index.d.ts.map +1 -1
  7. package/esm/components/index.js +4 -1
  8. package/esm/components/index.js.map +1 -1
  9. package/esm/components/lazy-load.d.ts +2 -4
  10. package/esm/components/lazy-load.d.ts.map +1 -1
  11. package/esm/components/lazy-load.js +40 -24
  12. package/esm/components/lazy-load.js.map +1 -1
  13. package/esm/components/lazy-load.spec.js +57 -50
  14. package/esm/components/lazy-load.spec.js.map +1 -1
  15. package/esm/components/link-to-route.d.ts +2 -0
  16. package/esm/components/link-to-route.d.ts.map +1 -1
  17. package/esm/components/link-to-route.js +3 -2
  18. package/esm/components/link-to-route.js.map +1 -1
  19. package/esm/components/link-to-route.spec.js +13 -9
  20. package/esm/components/link-to-route.spec.js.map +1 -1
  21. package/esm/components/nested-route-link.d.ts +62 -0
  22. package/esm/components/nested-route-link.d.ts.map +1 -0
  23. package/esm/components/nested-route-link.js +66 -0
  24. package/esm/components/nested-route-link.js.map +1 -0
  25. package/esm/components/nested-route-link.spec.d.ts +2 -0
  26. package/esm/components/nested-route-link.spec.d.ts.map +1 -0
  27. package/esm/components/nested-route-link.spec.js +179 -0
  28. package/esm/components/nested-route-link.spec.js.map +1 -0
  29. package/esm/components/nested-route-types.d.ts +37 -0
  30. package/esm/components/nested-route-types.d.ts.map +1 -0
  31. package/esm/components/nested-route-types.js +2 -0
  32. package/esm/components/nested-route-types.js.map +1 -0
  33. package/esm/components/nested-router.d.ts +103 -0
  34. package/esm/components/nested-router.d.ts.map +1 -0
  35. package/esm/components/nested-router.js +183 -0
  36. package/esm/components/nested-router.js.map +1 -0
  37. package/esm/components/nested-router.spec.d.ts +2 -0
  38. package/esm/components/nested-router.spec.d.ts.map +1 -0
  39. package/esm/components/nested-router.spec.js +737 -0
  40. package/esm/components/nested-router.spec.js.map +1 -0
  41. package/esm/components/route-link.d.ts +4 -0
  42. package/esm/components/route-link.d.ts.map +1 -1
  43. package/esm/components/route-link.js +5 -5
  44. package/esm/components/route-link.js.map +1 -1
  45. package/esm/components/route-link.spec.js +16 -12
  46. package/esm/components/route-link.spec.js.map +1 -1
  47. package/esm/components/router.d.ts +20 -2
  48. package/esm/components/router.d.ts.map +1 -1
  49. package/esm/components/router.js +12 -7
  50. package/esm/components/router.js.map +1 -1
  51. package/esm/components/router.spec.js +141 -74
  52. package/esm/components/router.spec.js.map +1 -1
  53. package/esm/initialize.d.ts +11 -0
  54. package/esm/initialize.d.ts.map +1 -1
  55. package/esm/initialize.js +5 -0
  56. package/esm/initialize.js.map +1 -1
  57. package/esm/jsx.d.ts +83 -2
  58. package/esm/jsx.d.ts.map +1 -1
  59. package/esm/models/children-list.d.ts +5 -1
  60. package/esm/models/children-list.d.ts.map +1 -1
  61. package/esm/models/partial-element.d.ts +12 -2
  62. package/esm/models/partial-element.d.ts.map +1 -1
  63. package/esm/models/render-options.d.ts +89 -3
  64. package/esm/models/render-options.d.ts.map +1 -1
  65. package/esm/models/selection-state.d.ts +4 -0
  66. package/esm/models/selection-state.d.ts.map +1 -1
  67. package/esm/services/location-service.d.ts +11 -0
  68. package/esm/services/location-service.d.ts.map +1 -1
  69. package/esm/services/location-service.js +11 -0
  70. package/esm/services/location-service.js.map +1 -1
  71. package/esm/services/resource-manager.d.ts +24 -0
  72. package/esm/services/resource-manager.d.ts.map +1 -1
  73. package/esm/services/resource-manager.js +36 -1
  74. package/esm/services/resource-manager.js.map +1 -1
  75. package/esm/services/resource-manager.spec.js +102 -0
  76. package/esm/services/resource-manager.spec.js.map +1 -1
  77. package/esm/services/screen-service.d.ts +81 -4
  78. package/esm/services/screen-service.d.ts.map +1 -1
  79. package/esm/services/screen-service.js +75 -4
  80. package/esm/services/screen-service.js.map +1 -1
  81. package/esm/services/screen-service.spec.js +91 -7
  82. package/esm/services/screen-service.spec.js.map +1 -1
  83. package/esm/shade-component.d.ts +17 -4
  84. package/esm/shade-component.d.ts.map +1 -1
  85. package/esm/shade-component.js +67 -5
  86. package/esm/shade-component.js.map +1 -1
  87. package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
  88. package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
  89. package/esm/shade-host-props-ref.integration.spec.js +381 -0
  90. package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
  91. package/esm/shade-resources.integration.spec.js +208 -39
  92. package/esm/shade-resources.integration.spec.js.map +1 -1
  93. package/esm/shade.d.ts +20 -17
  94. package/esm/shade.d.ts.map +1 -1
  95. package/esm/shade.js +172 -33
  96. package/esm/shade.js.map +1 -1
  97. package/esm/shade.spec.js +31 -30
  98. package/esm/shade.spec.js.map +1 -1
  99. package/esm/shades.integration.spec.js +135 -72
  100. package/esm/shades.integration.spec.js.map +1 -1
  101. package/esm/style-manager.d.ts +2 -2
  102. package/esm/style-manager.js +2 -2
  103. package/esm/svg-types.d.ts +389 -0
  104. package/esm/svg-types.d.ts.map +1 -0
  105. package/esm/svg-types.js +9 -0
  106. package/esm/svg-types.js.map +1 -0
  107. package/esm/svg.d.ts +15 -0
  108. package/esm/svg.d.ts.map +1 -0
  109. package/esm/svg.js +76 -0
  110. package/esm/svg.js.map +1 -0
  111. package/esm/svg.spec.d.ts +2 -0
  112. package/esm/svg.spec.d.ts.map +1 -0
  113. package/esm/svg.spec.js +80 -0
  114. package/esm/svg.spec.js.map +1 -0
  115. package/esm/vnode.d.ts +103 -0
  116. package/esm/vnode.d.ts.map +1 -0
  117. package/esm/vnode.integration.spec.d.ts +2 -0
  118. package/esm/vnode.integration.spec.d.ts.map +1 -0
  119. package/esm/vnode.integration.spec.js +494 -0
  120. package/esm/vnode.integration.spec.js.map +1 -0
  121. package/esm/vnode.js +453 -0
  122. package/esm/vnode.js.map +1 -0
  123. package/esm/vnode.spec.d.ts +2 -0
  124. package/esm/vnode.spec.d.ts.map +1 -0
  125. package/esm/vnode.spec.js +473 -0
  126. package/esm/vnode.spec.js.map +1 -0
  127. package/package.json +8 -9
  128. package/src/component-factory.spec.tsx +18 -5
  129. package/src/components/index.ts +4 -1
  130. package/src/components/lazy-load.spec.tsx +82 -75
  131. package/src/components/lazy-load.tsx +49 -27
  132. package/src/components/link-to-route.spec.tsx +25 -21
  133. package/src/components/link-to-route.tsx +4 -2
  134. package/src/components/nested-route-link.spec.tsx +303 -0
  135. package/src/components/nested-route-link.tsx +100 -0
  136. package/src/components/nested-route-types.ts +42 -0
  137. package/src/components/nested-router.spec.tsx +918 -0
  138. package/src/components/nested-router.tsx +260 -0
  139. package/src/components/route-link.spec.tsx +22 -18
  140. package/src/components/route-link.tsx +6 -5
  141. package/src/components/router.spec.tsx +196 -108
  142. package/src/components/router.tsx +21 -8
  143. package/src/initialize.ts +12 -0
  144. package/src/jsx.ts +129 -2
  145. package/src/models/children-list.ts +7 -1
  146. package/src/models/partial-element.ts +13 -2
  147. package/src/models/render-options.ts +90 -3
  148. package/src/models/selection-state.ts +4 -0
  149. package/src/services/location-service.tsx +11 -0
  150. package/src/services/resource-manager.spec.ts +128 -0
  151. package/src/services/resource-manager.ts +36 -1
  152. package/src/services/screen-service.spec.ts +109 -7
  153. package/src/services/screen-service.ts +81 -4
  154. package/src/shade-component.ts +72 -6
  155. package/src/shade-host-props-ref.integration.spec.tsx +460 -0
  156. package/src/shade-resources.integration.spec.tsx +276 -52
  157. package/src/shade.spec.tsx +40 -39
  158. package/src/shade.ts +186 -58
  159. package/src/shades.integration.spec.tsx +154 -80
  160. package/src/style-manager.ts +2 -2
  161. package/src/svg-types.ts +437 -0
  162. package/src/svg.spec.ts +89 -0
  163. package/src/svg.ts +78 -0
  164. package/src/vnode.integration.spec.tsx +657 -0
  165. package/src/vnode.spec.ts +579 -0
  166. package/src/vnode.ts +508 -0
@@ -1,7 +1,7 @@
1
1
  import { Injector } from '@furystack/inject'
2
2
  import { usingAsync } from '@furystack/utils'
3
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
- import { ScreenService } from './screen-service.js'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { ScreenService, ScreenSizes } from './screen-service.js'
5
5
 
6
6
  describe('ScreenService', () => {
7
7
  beforeEach(() => {
@@ -9,19 +9,121 @@ describe('ScreenService', () => {
9
9
  })
10
10
  afterEach(() => {
11
11
  document.body.innerHTML = ''
12
+ vi.restoreAllMocks()
12
13
  })
13
14
 
14
- it('Shuld be constructed', async () => {
15
+ it('Should be constructed', async () => {
15
16
  await usingAsync(new Injector(), async (i) => {
16
17
  const s = i.getInstance(ScreenService)
17
18
  expect(s).toBeInstanceOf(ScreenService)
18
19
  })
19
20
  })
20
21
 
21
- it('Shuld update state on events', async () => {
22
- await usingAsync(new Injector(), async (i) => {
23
- i.getInstance(ScreenService)
24
- /** TODO */
22
+ describe('breakpoints', () => {
23
+ it('Should have correct breakpoint definitions', async () => {
24
+ await usingAsync(new Injector(), async (i) => {
25
+ const s = i.getInstance(ScreenService)
26
+
27
+ expect(s.breakpoints.xs.minSize).toBe(0)
28
+ expect(s.breakpoints.sm.minSize).toBe(600)
29
+ expect(s.breakpoints.md.minSize).toBe(960)
30
+ expect(s.breakpoints.lg.minSize).toBe(1280)
31
+ expect(s.breakpoints.xl.minSize).toBe(1920)
32
+ })
33
+ })
34
+ })
35
+
36
+ describe('screenSize.atLeast', () => {
37
+ it('Should have observable for each screen size', async () => {
38
+ await usingAsync(new Injector(), async (i) => {
39
+ const s = i.getInstance(ScreenService)
40
+
41
+ for (const size of ScreenSizes) {
42
+ expect(s.screenSize.atLeast[size]).toBeDefined()
43
+ expect(typeof s.screenSize.atLeast[size].getValue()).toBe('boolean')
44
+ }
45
+ })
46
+ })
47
+
48
+ it('Should return true for xs on any screen size', async () => {
49
+ await usingAsync(new Injector(), async (i) => {
50
+ const s = i.getInstance(ScreenService)
51
+ // xs has minSize 0, so it should always be true
52
+ expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
53
+ })
54
+ })
55
+
56
+ it('Should update screenSize observables on window resize', async () => {
57
+ await usingAsync(new Injector(), async (i) => {
58
+ const s = i.getInstance(ScreenService)
59
+
60
+ // Mock window.innerWidth to simulate a large screen
61
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1920)
62
+
63
+ // Trigger resize event
64
+ window.dispatchEvent(new Event('resize'))
65
+
66
+ // All breakpoints should be true for 1920px width
67
+ expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
68
+ expect(s.screenSize.atLeast.sm.getValue()).toBe(true)
69
+ expect(s.screenSize.atLeast.md.getValue()).toBe(true)
70
+ expect(s.screenSize.atLeast.lg.getValue()).toBe(true)
71
+ expect(s.screenSize.atLeast.xl.getValue()).toBe(true)
72
+
73
+ // Mock a small screen
74
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(500)
75
+ window.dispatchEvent(new Event('resize'))
76
+
77
+ // Only xs should be true for 500px width
78
+ expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
79
+ expect(s.screenSize.atLeast.sm.getValue()).toBe(false)
80
+ expect(s.screenSize.atLeast.md.getValue()).toBe(false)
81
+ expect(s.screenSize.atLeast.lg.getValue()).toBe(false)
82
+ expect(s.screenSize.atLeast.xl.getValue()).toBe(false)
83
+ })
84
+ })
85
+ })
86
+
87
+ describe('orientation', () => {
88
+ it('Should have an orientation observable', async () => {
89
+ await usingAsync(new Injector(), async (i) => {
90
+ const s = i.getInstance(ScreenService)
91
+ const orientation = s.orientation.getValue()
92
+ expect(['landscape', 'portrait']).toContain(orientation)
93
+ })
94
+ })
95
+
96
+ it('Should update orientation on resize', async () => {
97
+ // Mock matchMedia before creating the service
98
+ const matchMediaMock = vi.fn()
99
+ window.matchMedia = matchMediaMock
100
+
101
+ await usingAsync(new Injector(), async (i) => {
102
+ // Set initial orientation to landscape
103
+ matchMediaMock.mockReturnValue({ matches: true } as MediaQueryList)
104
+ const s = i.getInstance(ScreenService)
105
+
106
+ // Verify initial landscape
107
+ window.dispatchEvent(new Event('resize'))
108
+ expect(s.orientation.getValue()).toBe('landscape')
109
+
110
+ // Change to portrait
111
+ matchMediaMock.mockReturnValue({ matches: false } as MediaQueryList)
112
+ window.dispatchEvent(new Event('resize'))
113
+ expect(s.orientation.getValue()).toBe('portrait')
114
+ })
115
+ })
116
+ })
117
+
118
+ describe('disposal', () => {
119
+ it('Should remove resize event listener on dispose', async () => {
120
+ const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
121
+
122
+ await usingAsync(new Injector(), async (i) => {
123
+ i.getInstance(ScreenService)
124
+ })
125
+
126
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
25
127
  })
26
128
  })
27
129
  })
@@ -1,21 +1,74 @@
1
1
  import { Injectable } from '@furystack/inject'
2
2
  import { ObservableValue } from '@furystack/utils'
3
3
 
4
+ /**
5
+ * Available screen size breakpoint identifiers, ordered from smallest to largest.
6
+ */
4
7
  export const ScreenSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
5
8
 
9
+ /**
10
+ * A screen size breakpoint identifier.
11
+ */
6
12
  export type ScreenSize = (typeof ScreenSizes)[number]
7
13
 
14
+ /**
15
+ * Breakpoint definition with name and size constraints.
16
+ */
8
17
  export type Breakpoint = { name: ScreenSize; minSize: number; maxSize?: number }
9
18
 
10
19
  /**
11
- * Service for handling screen size changes
20
+ * Service for detecting and subscribing to screen size and orientation changes.
21
+ *
22
+ * This service provides reactive observables for responsive design decisions.
23
+ * Use `screenSize.atLeast[size]` to check if the viewport is at least a certain size.
24
+ *
25
+ * **Breakpoint Thresholds:**
26
+ * - `xs`: 0px+ (all sizes)
27
+ * - `sm`: 600px+ (small tablets and up)
28
+ * - `md`: 960px+ (tablets and up)
29
+ * - `lg`: 1280px+ (desktops and up)
30
+ * - `xl`: 1920px+ (large desktops)
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const screenService = injector.getInstance(ScreenService);
35
+ *
36
+ * // Check if screen is at least medium size
37
+ * if (screenService.screenSize.atLeast.md.getValue()) {
38
+ * // Show desktop layout
39
+ * }
40
+ *
41
+ * // Subscribe to size changes for responsive behavior
42
+ * screenService.screenSize.atLeast.md.subscribe((isAtLeastMd) => {
43
+ * if (isAtLeastMd) {
44
+ * console.log('Desktop or tablet view');
45
+ * } else {
46
+ * console.log('Mobile view');
47
+ * }
48
+ * });
49
+ *
50
+ * // Get current breakpoint by checking from largest to smallest
51
+ * const getCurrentBreakpoint = (): ScreenSize => {
52
+ * if (screenService.screenSize.atLeast.xl.getValue()) return 'xl';
53
+ * if (screenService.screenSize.atLeast.lg.getValue()) return 'lg';
54
+ * if (screenService.screenSize.atLeast.md.getValue()) return 'md';
55
+ * if (screenService.screenSize.atLeast.sm.getValue()) return 'sm';
56
+ * return 'xs';
57
+ * };
58
+ *
59
+ * // Subscribe to orientation changes
60
+ * screenService.orientation.subscribe((orientation) => {
61
+ * console.log(`Screen is now in ${orientation} mode`);
62
+ * });
63
+ * ```
12
64
  */
13
65
  @Injectable({ lifetime: 'singleton' })
14
66
  export class ScreenService implements Disposable {
15
67
  private getOrientation = () => (window.matchMedia?.('(orientation:landscape').matches ? 'landscape' : 'portrait')
16
68
 
17
69
  /**
18
- * The definitions of the breakpoints
70
+ * The definitions of the breakpoint thresholds in pixels.
71
+ * Each breakpoint represents the minimum width for that size category.
19
72
  */
20
73
  public readonly breakpoints: { [K in ScreenSize]: { minSize: number } } = {
21
74
  xl: { minSize: 1920 },
@@ -25,12 +78,26 @@ export class ScreenService implements Disposable {
25
78
  xs: { minSize: 0 },
26
79
  }
27
80
 
81
+ /**
82
+ * Cleans up event listeners when the service is disposed.
83
+ */
28
84
  public [Symbol.dispose]() {
29
85
  window.removeEventListener('resize', this.onResizeListener)
30
86
  }
31
87
 
32
88
  /**
33
- * Observers for the current screen size. Will refresh on resize
89
+ * Observable values for checking if the screen is at least a certain size.
90
+ *
91
+ * Each observable emits `true` when the viewport width is >= the breakpoint threshold,
92
+ * and `false` otherwise. Values update automatically on window resize.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // Hide sidebar on small screens
97
+ * screenService.screenSize.atLeast.md.subscribe((isAtLeastMd) => {
98
+ * sidebarVisible.setValue(isAtLeastMd);
99
+ * });
100
+ * ```
34
101
  */
35
102
  public readonly screenSize: {
36
103
  atLeast: { [K in ScreenSize]: ObservableValue<boolean> }
@@ -49,7 +116,17 @@ export class ScreenService implements Disposable {
49
116
  }
50
117
 
51
118
  /**
52
- * Observable value for tracking the screen orientation
119
+ * Observable value for tracking the screen orientation.
120
+ * Emits 'landscape' or 'portrait' based on the current viewport dimensions.
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * screenService.orientation.subscribe((orientation) => {
125
+ * if (orientation === 'landscape') {
126
+ * // Adjust layout for landscape mode
127
+ * }
128
+ * });
129
+ * ```
53
130
  */
54
131
  public orientation = new ObservableValue<'landscape' | 'portrait'>(this.getOrientation())
55
132
 
@@ -1,17 +1,37 @@
1
1
  import type { ChildrenList, ShadeComponent } from './models/index.js'
2
2
  import { isShadeComponent } from './models/shade-component.js'
3
+ import { SVG_NS, isSvgTag } from './svg.js'
4
+ import { createVNode } from './vnode.js'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Render-mode toggle
8
+ // ---------------------------------------------------------------------------
9
+
10
+ let renderMode = false
3
11
 
4
12
  /**
5
- * Appends a list of items to a HTML element
13
+ * When true, the JSX factory produces VNode descriptors instead of real DOM elements.
14
+ * Set to true by `_performUpdate` before calling `render()`, then back to false after.
15
+ */
16
+ export const setRenderMode = (mode: boolean): void => {
17
+ renderMode = mode
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Real-DOM helpers (used outside render mode)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Appends a list of items to an element
6
26
  * @param el the root element
7
27
  * @param children array of items to append
8
28
  */
9
- export const appendChild = (el: HTMLElement | DocumentFragment, children: ChildrenList) => {
29
+ export const appendChild = (el: Element | DocumentFragment, children: ChildrenList) => {
10
30
  for (const child of children) {
11
31
  if (typeof child === 'string' || typeof child === 'number') {
12
32
  el.appendChild(document.createTextNode(child))
13
33
  } else {
14
- if (child instanceof HTMLElement || child instanceof DocumentFragment) {
34
+ if (child instanceof Element || child instanceof DocumentFragment) {
15
35
  el.appendChild(child)
16
36
  } else if (child instanceof Array) {
17
37
  appendChild(el, child)
@@ -44,7 +64,7 @@ export const attachStyles = (el: HTMLElement, props: unknown) => {
44
64
  export const attachDataAttributes = <TProps extends object>(el: HTMLElement, props: TProps) => {
45
65
  if (props) {
46
66
  Object.entries(props)
47
- .filter(([key]) => key.startsWith('data-'))
67
+ .filter(([key]) => key.startsWith('data-') || key.startsWith('aria-'))
48
68
  .forEach(([key, value]) => el.setAttribute(key, (value as string) || ''))
49
69
  }
50
70
  }
@@ -69,6 +89,32 @@ export const attachProps = <TProps extends object>(el: HTMLElement, props: TProp
69
89
  attachDataAttributes(el, props)
70
90
  }
71
91
 
92
+ /**
93
+ * Attaches properties to an SVG element via setAttribute.
94
+ * SVG attributes are XML-based and must be set via setAttribute,
95
+ * not via property assignment like HTML elements.
96
+ * @param el The target SVG element
97
+ * @param props The props to attach
98
+ */
99
+ export const attachSvgProps = <TProps extends object>(el: Element, props: TProps) => {
100
+ if (!props) {
101
+ return
102
+ }
103
+ for (const [key, value] of Object.entries(props)) {
104
+ if (key === 'style' && typeof value === 'object' && value !== null) {
105
+ for (const [sk, sv] of Object.entries(value as Record<string, string>)) {
106
+ ;((el as HTMLElement).style as unknown as Record<string, string>)[sk] = sv
107
+ }
108
+ } else if (key === 'className') {
109
+ el.setAttribute('class', String(value))
110
+ } else if (key.startsWith('on') && typeof value === 'function') {
111
+ ;(el as unknown as Record<string, unknown>)[key] = value
112
+ } else if (value !== null && value !== undefined && value !== false) {
113
+ el.setAttribute(key, String(value))
114
+ }
115
+ }
116
+ }
117
+
72
118
  type CreateComponentArgs<TProps> = [
73
119
  elementType: string | ShadeComponent<TProps>,
74
120
  props: TProps,
@@ -83,9 +129,14 @@ export const createComponentInner = <TProps extends object>(
83
129
  ...[elementType, props, ...children]: CreateComponentArgs<TProps>
84
130
  ) => {
85
131
  if (typeof elementType === 'string') {
86
- const el = document.createElement(elementType)
132
+ const isSvg = isSvgTag(elementType)
133
+ const el = isSvg ? document.createElementNS(SVG_NS, elementType) : document.createElement(elementType)
87
134
 
88
- attachProps(el, props)
135
+ if (isSvg) {
136
+ attachSvgProps(el, props)
137
+ } else {
138
+ attachProps(el as HTMLElement, props)
139
+ }
89
140
 
90
141
  if (children) {
91
142
  appendChild(el, children)
@@ -108,6 +159,21 @@ export const createFragmentInner = (...[_props, ...children]: CreateFragmentArgs
108
159
  }
109
160
 
110
161
  export const createComponent = <TProps extends object>(...args: CreateComponentArgs<TProps> | CreateFragmentArgs) => {
162
+ // In render mode, produce VNode descriptors instead of real DOM elements
163
+ if (renderMode) {
164
+ const [type, props, ...children] = args
165
+ // When jsxFragmentFactory === jsxFactory (both "createComponent"), the compiler
166
+ // passes createComponent itself as the first arg for fragments: createComponent(createComponent, null, ...)
167
+ if (type === null || (type as unknown) === createComponent) {
168
+ return createVNode(null, null, ...children) as unknown as ReturnType<typeof createComponentInner>
169
+ }
170
+ return createVNode(
171
+ type as string | ((...a: unknown[]) => unknown),
172
+ props as Record<string, unknown> | null,
173
+ ...children,
174
+ ) as unknown as ReturnType<typeof createComponentInner>
175
+ }
176
+
111
177
  if (args[0] === null) {
112
178
  return createFragmentInner(...args)
113
179
  }