@pyreon/elements 0.12.12 → 0.12.14

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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Pure positioning helpers for the Overlay component. Split out from
3
+ * `useOverlay.tsx` so the SSR-fallback branches (`typeof window === 'undefined'`)
4
+ * can be exercised directly by tests that stub `globalThis.window` — the
5
+ * `useOverlay` hook itself runs these via event handlers registered inside
6
+ * `onMount`, which are unreachable during module-level test imports in
7
+ * happy-dom (where `window` is always defined).
8
+ */
9
+
10
+ export type OverlayPosition = Partial<{
11
+ top: number | string
12
+ bottom: number | string
13
+ left: number | string
14
+ right: number | string
15
+ }>
16
+
17
+ export type Align = 'bottom' | 'top' | 'left' | 'right'
18
+ export type AlignX = 'left' | 'center' | 'right'
19
+ export type AlignY = 'bottom' | 'top' | 'center'
20
+
21
+ export type PositionResult = {
22
+ pos: OverlayPosition
23
+ resolvedAlignX: AlignX
24
+ resolvedAlignY: AlignY
25
+ }
26
+
27
+ const sel = <T,>(cond: boolean, a: T, b: T): T => (cond ? a : b)
28
+
29
+ export const calcDropdownVertical = (
30
+ c: DOMRect,
31
+ t: DOMRect,
32
+ align: 'top' | 'bottom',
33
+ alignX: AlignX,
34
+ offsetX: number,
35
+ offsetY: number,
36
+ ): PositionResult => {
37
+ // SSR-fallback: positioning only runs in the mounted browser context, but
38
+ // the explicit guard documents the SSR-safety contract at the callsite
39
+ // and lets `no-window-in-ssr` prove it locally. Return shape mirrors the
40
+ // "no element" path below (empty `pos`, alignment preserved).
41
+ if (typeof window === 'undefined') return { pos: {}, resolvedAlignX: alignX, resolvedAlignY: align }
42
+ const pos: OverlayPosition = {}
43
+
44
+ const topPos = t.top - offsetY - c.height
45
+ const bottomPos = t.bottom + offsetY
46
+ const leftPos = t.left + offsetX
47
+ const rightPos = t.right - offsetX - c.width
48
+
49
+ const fitsTop = topPos >= 0
50
+ const fitsBottom = bottomPos + c.height <= window.innerHeight
51
+ const fitsLeft = leftPos + c.width <= window.innerWidth
52
+ const fitsRight = rightPos >= 0
53
+
54
+ const useTop = sel(align === 'top', fitsTop, !fitsBottom)
55
+ pos.top = sel(useTop, topPos, bottomPos)
56
+ const resolvedAlignY: AlignY = sel(useTop, 'top', 'bottom')
57
+
58
+ let resolvedAlignX: AlignX = alignX
59
+ if (alignX === 'left') {
60
+ pos.left = sel(fitsLeft, leftPos, rightPos)
61
+ resolvedAlignX = sel(fitsLeft, 'left', 'right')
62
+ } else if (alignX === 'right') {
63
+ pos.left = sel(fitsRight, rightPos, leftPos)
64
+ resolvedAlignX = sel(fitsRight, 'right', 'left')
65
+ } else {
66
+ const center = t.left + (t.right - t.left) / 2 - c.width / 2
67
+ const fitsCL = center >= 0
68
+ const fitsCR = center + c.width <= window.innerWidth
69
+
70
+ if (fitsCL && fitsCR) {
71
+ resolvedAlignX = 'center'
72
+ pos.left = center
73
+ } else if (fitsCL) {
74
+ resolvedAlignX = 'left'
75
+ pos.left = leftPos
76
+ } else if (fitsCR) {
77
+ resolvedAlignX = 'right'
78
+ pos.left = rightPos
79
+ }
80
+ }
81
+
82
+ return { pos, resolvedAlignX, resolvedAlignY }
83
+ }
84
+
85
+ export const calcDropdownHorizontal = (
86
+ c: DOMRect,
87
+ t: DOMRect,
88
+ align: 'left' | 'right',
89
+ alignY: AlignY,
90
+ offsetX: number,
91
+ offsetY: number,
92
+ ): PositionResult => {
93
+ if (typeof window === 'undefined') return { pos: {}, resolvedAlignX: align, resolvedAlignY: alignY }
94
+ const pos: OverlayPosition = {}
95
+
96
+ const leftPos = t.left - offsetX - c.width
97
+ const rightPos = t.right + offsetX
98
+ const topPos = t.top + offsetY
99
+ const bottomPos = t.bottom - offsetY - c.height
100
+
101
+ const fitsLeft = leftPos >= 0
102
+ const fitsRight = rightPos + c.width <= window.innerWidth
103
+ const fitsTop = topPos + c.height <= window.innerHeight
104
+ const fitsBottom = bottomPos >= 0
105
+
106
+ const useLeft = sel(align === 'left', fitsLeft, !fitsRight)
107
+ pos.left = sel(useLeft, leftPos, rightPos)
108
+ const resolvedAlignX: AlignX = sel(useLeft, 'left', 'right')
109
+
110
+ let resolvedAlignY: AlignY = alignY
111
+ if (alignY === 'top') {
112
+ pos.top = sel(fitsTop, topPos, bottomPos)
113
+ resolvedAlignY = sel(fitsTop, 'top', 'bottom')
114
+ } else if (alignY === 'bottom') {
115
+ pos.top = sel(fitsBottom, bottomPos, topPos)
116
+ resolvedAlignY = sel(fitsBottom, 'bottom', 'top')
117
+ } else {
118
+ const center = t.top + (t.bottom - t.top) / 2 - c.height / 2
119
+ const fitsCT = center >= 0
120
+ const fitsCB = center + c.height <= window.innerHeight
121
+
122
+ if (fitsCT && fitsCB) {
123
+ resolvedAlignY = 'center'
124
+ pos.top = center
125
+ } else if (fitsCT) {
126
+ resolvedAlignY = 'top'
127
+ pos.top = topPos
128
+ } else if (fitsCB) {
129
+ resolvedAlignY = 'bottom'
130
+ pos.top = bottomPos
131
+ }
132
+ }
133
+
134
+ return { pos, resolvedAlignX, resolvedAlignY }
135
+ }
136
+
137
+ export const calcModalPos = (
138
+ c: DOMRect,
139
+ alignX: AlignX,
140
+ alignY: AlignY,
141
+ offsetX: number,
142
+ offsetY: number,
143
+ ): OverlayPosition => {
144
+ if (typeof window === 'undefined') return {}
145
+ const pos: OverlayPosition = {}
146
+
147
+ switch (alignX) {
148
+ case 'right':
149
+ pos.right = offsetX
150
+ break
151
+ case 'left':
152
+ pos.left = offsetX
153
+ break
154
+ case 'center':
155
+ pos.left = window.innerWidth / 2 - c.width / 2
156
+ break
157
+ default:
158
+ pos.right = offsetX
159
+ }
160
+
161
+ switch (alignY) {
162
+ case 'top':
163
+ pos.top = offsetY
164
+ break
165
+ case 'center':
166
+ pos.top = window.innerHeight / 2 - c.height / 2
167
+ break
168
+ case 'bottom':
169
+ pos.bottom = offsetY
170
+ break
171
+ default:
172
+ pos.top = offsetY
173
+ }
174
+
175
+ return pos
176
+ }
177
+
178
+ export const adjustForAncestor = (
179
+ pos: OverlayPosition,
180
+ ancestor: { top: number; left: number },
181
+ ): OverlayPosition => {
182
+ if (ancestor.top === 0 && ancestor.left === 0) return pos
183
+
184
+ const result = { ...pos }
185
+ if (typeof result.top === 'number') result.top -= ancestor.top
186
+ if (typeof result.bottom === 'number') result.bottom += ancestor.top
187
+ if (typeof result.left === 'number') result.left -= ancestor.left
188
+ if (typeof result.right === 'number') result.right += ancestor.left
189
+
190
+ return result
191
+ }
@@ -12,17 +12,16 @@ import { throttle } from '@pyreon/ui-core'
12
12
  import { value } from '@pyreon/unistyle'
13
13
  import { IS_DEVELOPMENT } from '../utils'
14
14
  import Provider, { useOverlayContext } from './context'
15
-
16
- type OverlayPosition = Partial<{
17
- top: number | string
18
- bottom: number | string
19
- left: number | string
20
- right: number | string
21
- }>
22
-
23
- type Align = 'bottom' | 'top' | 'left' | 'right'
24
- type AlignX = 'left' | 'center' | 'right'
25
- type AlignY = 'bottom' | 'top' | 'center'
15
+ import {
16
+ adjustForAncestor,
17
+ calcDropdownHorizontal,
18
+ calcDropdownVertical,
19
+ calcModalPos,
20
+ type Align,
21
+ type AlignX,
22
+ type AlignY,
23
+ type OverlayPosition,
24
+ } from './positioning'
26
25
 
27
26
  export type UseOverlayProps = Partial<{
28
27
  isOpen: boolean
@@ -44,179 +43,15 @@ export type UseOverlayProps = Partial<{
44
43
  onClose: () => void
45
44
  }>
46
45
 
47
- type PositionResult = {
48
- pos: OverlayPosition
49
- resolvedAlignX: AlignX
50
- resolvedAlignY: AlignY
51
- }
52
-
53
46
  // Reference counter for nested modals sharing document.body overflow lock.
54
47
  let modalOverflowCount = 0
55
48
 
56
- const sel = <T,>(cond: boolean, a: T, b: T): T => (cond ? a : b)
57
-
58
49
  const devWarn = (msg: string) => {
59
50
  if (!IS_DEVELOPMENT) return
60
51
  // oxlint-disable-next-line no-console
61
52
  console.warn(msg)
62
53
  }
63
54
 
64
- const calcDropdownVertical = (
65
- c: DOMRect,
66
- t: DOMRect,
67
- align: 'top' | 'bottom',
68
- alignX: AlignX,
69
- offsetX: number,
70
- offsetY: number,
71
- ): PositionResult => {
72
- const pos: OverlayPosition = {}
73
-
74
- const topPos = t.top - offsetY - c.height
75
- const bottomPos = t.bottom + offsetY
76
- const leftPos = t.left + offsetX
77
- const rightPos = t.right - offsetX - c.width
78
-
79
- const fitsTop = topPos >= 0
80
- const fitsBottom = bottomPos + c.height <= window.innerHeight
81
- const fitsLeft = leftPos + c.width <= window.innerWidth
82
- const fitsRight = rightPos >= 0
83
-
84
- const useTop = sel(align === 'top', fitsTop, !fitsBottom)
85
- pos.top = sel(useTop, topPos, bottomPos)
86
- const resolvedAlignY: AlignY = sel(useTop, 'top', 'bottom')
87
-
88
- let resolvedAlignX: AlignX = alignX
89
- if (alignX === 'left') {
90
- pos.left = sel(fitsLeft, leftPos, rightPos)
91
- resolvedAlignX = sel(fitsLeft, 'left', 'right')
92
- } else if (alignX === 'right') {
93
- pos.left = sel(fitsRight, rightPos, leftPos)
94
- resolvedAlignX = sel(fitsRight, 'right', 'left')
95
- } else {
96
- const center = t.left + (t.right - t.left) / 2 - c.width / 2
97
- const fitsCL = center >= 0
98
- const fitsCR = center + c.width <= window.innerWidth
99
-
100
- if (fitsCL && fitsCR) {
101
- resolvedAlignX = 'center'
102
- pos.left = center
103
- } else if (fitsCL) {
104
- resolvedAlignX = 'left'
105
- pos.left = leftPos
106
- } else if (fitsCR) {
107
- resolvedAlignX = 'right'
108
- pos.left = rightPos
109
- }
110
- }
111
-
112
- return { pos, resolvedAlignX, resolvedAlignY }
113
- }
114
-
115
- const calcDropdownHorizontal = (
116
- c: DOMRect,
117
- t: DOMRect,
118
- align: 'left' | 'right',
119
- alignY: AlignY,
120
- offsetX: number,
121
- offsetY: number,
122
- ): PositionResult => {
123
- const pos: OverlayPosition = {}
124
-
125
- const leftPos = t.left - offsetX - c.width
126
- const rightPos = t.right + offsetX
127
- const topPos = t.top + offsetY
128
- const bottomPos = t.bottom - offsetY - c.height
129
-
130
- const fitsLeft = leftPos >= 0
131
- const fitsRight = rightPos + c.width <= window.innerWidth
132
- const fitsTop = topPos + c.height <= window.innerHeight
133
- const fitsBottom = bottomPos >= 0
134
-
135
- const useLeft = sel(align === 'left', fitsLeft, !fitsRight)
136
- pos.left = sel(useLeft, leftPos, rightPos)
137
- const resolvedAlignX: AlignX = sel(useLeft, 'left', 'right')
138
-
139
- let resolvedAlignY: AlignY = alignY
140
- if (alignY === 'top') {
141
- pos.top = sel(fitsTop, topPos, bottomPos)
142
- resolvedAlignY = sel(fitsTop, 'top', 'bottom')
143
- } else if (alignY === 'bottom') {
144
- pos.top = sel(fitsBottom, bottomPos, topPos)
145
- resolvedAlignY = sel(fitsBottom, 'bottom', 'top')
146
- } else {
147
- const center = t.top + (t.bottom - t.top) / 2 - c.height / 2
148
- const fitsCT = center >= 0
149
- const fitsCB = center + c.height <= window.innerHeight
150
-
151
- if (fitsCT && fitsCB) {
152
- resolvedAlignY = 'center'
153
- pos.top = center
154
- } else if (fitsCT) {
155
- resolvedAlignY = 'top'
156
- pos.top = topPos
157
- } else if (fitsCB) {
158
- resolvedAlignY = 'bottom'
159
- pos.top = bottomPos
160
- }
161
- }
162
-
163
- return { pos, resolvedAlignX, resolvedAlignY }
164
- }
165
-
166
- const calcModalPos = (
167
- c: DOMRect,
168
- alignX: AlignX,
169
- alignY: AlignY,
170
- offsetX: number,
171
- offsetY: number,
172
- ): OverlayPosition => {
173
- const pos: OverlayPosition = {}
174
-
175
- switch (alignX) {
176
- case 'right':
177
- pos.right = offsetX
178
- break
179
- case 'left':
180
- pos.left = offsetX
181
- break
182
- case 'center':
183
- pos.left = window.innerWidth / 2 - c.width / 2
184
- break
185
- default:
186
- pos.right = offsetX
187
- }
188
-
189
- switch (alignY) {
190
- case 'top':
191
- pos.top = offsetY
192
- break
193
- case 'center':
194
- pos.top = window.innerHeight / 2 - c.height / 2
195
- break
196
- case 'bottom':
197
- pos.bottom = offsetY
198
- break
199
- default:
200
- pos.top = offsetY
201
- }
202
-
203
- return pos
204
- }
205
-
206
- const adjustForAncestor = (
207
- pos: OverlayPosition,
208
- ancestor: { top: number; left: number },
209
- ): OverlayPosition => {
210
- if (ancestor.top === 0 && ancestor.left === 0) return pos
211
-
212
- const result = { ...pos }
213
- if (typeof result.top === 'number') result.top -= ancestor.top
214
- if (typeof result.bottom === 'number') result.bottom += ancestor.top
215
- if (typeof result.left === 'number') result.left -= ancestor.left
216
- if (typeof result.right === 'number') result.right += ancestor.left
217
-
218
- return result
219
- }
220
55
 
221
56
  type ComputeResult = {
222
57
  pos: OverlayPosition
@@ -374,6 +209,7 @@ const useOverlay = ({
374
209
 
375
210
  // Position calculation helpers
376
211
  const getAncestorOffset = () => {
212
+ if (typeof document === 'undefined') return { top: 0, left: 0 }
377
213
  if (position !== 'absolute' || !contentEl) {
378
214
  return { top: 0, left: 0 }
379
215
  }
@@ -460,6 +296,7 @@ const useOverlay = ({
460
296
  // Set up all event listeners on mount, clean up on unmount
461
297
  // --------------------------------------------------------------------------
462
298
  const setupListeners = () => {
299
+ if (typeof window === 'undefined') return () => {}
463
300
  const cleanups: (() => void)[] = []
464
301
 
465
302
  // Click-based open/close
@@ -0,0 +1,59 @@
1
+ /** @jsxImportSource @pyreon/core */
2
+ import { describe, expect, it } from 'vitest'
3
+ import { signal } from '@pyreon/reactivity'
4
+ import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
5
+ import { Element } from '../Element'
6
+ import { Portal } from '../Portal'
7
+ import { Text } from '../Text'
8
+
9
+ describe('@pyreon/elements browser smoke', () => {
10
+ it('Element mounts into real DOM with structural rendering', () => {
11
+ const { container, unmount } = mountInBrowser(
12
+ <Element tag="div" data-id="el"><span>hello</span></Element>,
13
+ )
14
+ const el = container.querySelector('[data-id="el"]')
15
+ expect(el?.tagName.toLowerCase()).toBe('div')
16
+ expect(el?.querySelector('span')?.textContent).toBe('hello')
17
+ unmount()
18
+ })
19
+
20
+ it('Element forwards a reactive text child to the DOM', async () => {
21
+ const label = signal('hello')
22
+ const { container, unmount } = mountInBrowser(
23
+ <Element tag="div">
24
+ <span data-id="lbl">{() => label()}</span>
25
+ </Element>,
26
+ )
27
+ const lbl = container.querySelector('[data-id="lbl"]')
28
+ expect(lbl?.textContent).toBe('hello')
29
+ label.set('world')
30
+ await flush()
31
+ expect(lbl?.textContent).toBe('world')
32
+ unmount()
33
+ })
34
+
35
+ it('Text renders as inline element', () => {
36
+ const { container, unmount } = mountInBrowser(<Text tag="span" data-id="t">hi</Text>)
37
+ const el = container.querySelector('[data-id="t"]')
38
+ expect(el?.tagName.toLowerCase()).toBe('span')
39
+ expect(el?.textContent).toBe('hi')
40
+ unmount()
41
+ })
42
+
43
+ it('Portal projects children to document.body by default', () => {
44
+ const { unmount } = mountInBrowser(
45
+ <Portal>
46
+ <div data-portal-id="p">portal-content</div>
47
+ </Portal>,
48
+ )
49
+ const projected = document.querySelector('[data-portal-id="p"]')
50
+ expect(projected?.textContent).toBe('portal-content')
51
+ unmount()
52
+ expect(document.querySelector('[data-portal-id="p"]')).toBeNull()
53
+ })
54
+
55
+ it('runs in a real browser — `typeof process` is undefined, `import.meta.env.DEV` is true', () => {
56
+ expect(typeof process).toBe('undefined')
57
+ expect(import.meta.env.DEV).toBe(true)
58
+ })
59
+ })
@@ -0,0 +1,90 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import {
3
+ adjustForAncestor,
4
+ calcDropdownHorizontal,
5
+ calcDropdownVertical,
6
+ calcModalPos,
7
+ } from '../Overlay/positioning'
8
+
9
+ const rect = (top: number, left: number, width: number, height: number): DOMRect => ({
10
+ top,
11
+ left,
12
+ right: left + width,
13
+ bottom: top + height,
14
+ width,
15
+ height,
16
+ x: left,
17
+ y: top,
18
+ toJSON: () => ({ top, left, width, height }),
19
+ })
20
+
21
+ describe('positioning helpers — browser path (happy-dom)', () => {
22
+ it('calcDropdownVertical: fits below → positions under the trigger', () => {
23
+ const trigger = rect(100, 200, 80, 30) // top=100
24
+ const content = rect(0, 0, 60, 40)
25
+ const result = calcDropdownVertical(content, trigger, 'bottom', 'left', 0, 4)
26
+ expect(result.resolvedAlignY).toBe('bottom')
27
+ expect(result.pos.top).toBe(trigger.bottom + 4)
28
+ })
29
+
30
+ it('calcDropdownHorizontal: flips to right when left does not fit', () => {
31
+ const trigger = rect(100, 10, 80, 30) // near left edge
32
+ const content = rect(0, 0, 120, 40) // wider than trigger.left
33
+ const result = calcDropdownHorizontal(content, trigger, 'left', 'top', 0, 0)
34
+ expect(result.resolvedAlignX).toBe('right')
35
+ })
36
+
37
+ it('calcModalPos: centers horizontally and vertically', () => {
38
+ const content = rect(0, 0, 400, 300)
39
+ const pos = calcModalPos(content, 'center', 'center', 0, 0)
40
+ expect(typeof pos.left).toBe('number')
41
+ expect(typeof pos.top).toBe('number')
42
+ })
43
+
44
+ it('adjustForAncestor: subtracts ancestor offset from absolute positions', () => {
45
+ const adjusted = adjustForAncestor({ top: 100, left: 200 }, { top: 30, left: 50 })
46
+ expect(adjusted.top).toBe(70)
47
+ expect(adjusted.left).toBe(150)
48
+ })
49
+
50
+ it('adjustForAncestor: returns input unchanged when ancestor is origin', () => {
51
+ const input = { top: 10, left: 20 }
52
+ expect(adjustForAncestor(input, { top: 0, left: 0 })).toBe(input)
53
+ })
54
+ })
55
+
56
+ // SSR-fallback path: the positioning helpers are only reachable via mounted
57
+ // event handlers in production, so `typeof window === 'undefined'` never
58
+ // fires in browser tests. We force the fallback by stubbing `globalThis.window`
59
+ // to `undefined` for the duration of a single test. happy-dom resets on
60
+ // teardown; we restore the original window afterwards.
61
+ describe('positioning helpers — SSR fallback (window undefined)', () => {
62
+ const realWindow = globalThis.window
63
+ const realWindowDesc = Object.getOwnPropertyDescriptor(globalThis, 'window')
64
+
65
+ afterEach(() => {
66
+ if (realWindowDesc) Object.defineProperty(globalThis, 'window', realWindowDesc)
67
+ else (globalThis as { window?: unknown }).window = realWindow
68
+ })
69
+
70
+ it('calcDropdownVertical returns alignment-preserving empty fallback', () => {
71
+ Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
72
+ const result = calcDropdownVertical(rect(0, 0, 60, 40), rect(100, 200, 80, 30), 'bottom', 'left', 0, 0)
73
+ expect(result.pos).toEqual({})
74
+ expect(result.resolvedAlignX).toBe('left')
75
+ expect(result.resolvedAlignY).toBe('bottom')
76
+ })
77
+
78
+ it('calcDropdownHorizontal returns alignment-preserving empty fallback', () => {
79
+ Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
80
+ const result = calcDropdownHorizontal(rect(0, 0, 60, 40), rect(100, 200, 80, 30), 'right', 'top', 0, 0)
81
+ expect(result.pos).toEqual({})
82
+ expect(result.resolvedAlignX).toBe('right')
83
+ expect(result.resolvedAlignY).toBe('top')
84
+ })
85
+
86
+ it('calcModalPos returns empty object', () => {
87
+ Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
88
+ expect(calcModalPos(rect(0, 0, 400, 300), 'center', 'center', 0, 0)).toEqual({})
89
+ })
90
+ })
@@ -4,6 +4,7 @@
4
4
  * fix (parent + child Styled) because these HTML elements do not natively
5
5
  * support `display: flex` consistently across browsers.
6
6
  */
7
+ import { splitProps } from '@pyreon/core'
7
8
  import { IS_DEVELOPMENT } from '../../utils'
8
9
  import Styled from './styled'
9
10
  import type { Props } from './types'
@@ -11,65 +12,76 @@ import { isWebFixNeeded } from './utils'
11
12
 
12
13
  const DEV_PROPS: Record<string, string> = IS_DEVELOPMENT ? { 'data-pyr-element': 'Element' } : {}
13
14
 
14
- const Component = ({
15
- children,
16
- tag,
17
- block,
18
- extendCss,
19
- direction,
20
- alignX,
21
- alignY,
22
- equalCols,
23
- isInline,
24
- ref,
25
- ...props
26
- }: Partial<Props> & { ref?: any }) => {
27
- const COMMON_PROPS = {
28
- ...props,
29
- ...DEV_PROPS,
30
- ref,
31
- as: tag,
32
- }
33
-
34
- const needsFix = !props.dangerouslySetInnerHTML && isWebFixNeeded(tag)
15
+ // Layout / ref keys consumed by Wrapper itself. Everything else is forwarded
16
+ // onto the underlying DOM node. Listed as a tuple so `splitProps` narrows
17
+ // `own` correctly while preserving reactive prop tracking on both halves.
18
+ const OWN_KEYS: Array<keyof Props | 'ref'> = [
19
+ 'children',
20
+ 'tag',
21
+ 'block',
22
+ 'extendCss',
23
+ 'direction',
24
+ 'alignX',
25
+ 'alignY',
26
+ 'equalCols',
27
+ 'isInline',
28
+ 'ref',
29
+ 'dangerouslySetInnerHTML',
30
+ ]
35
31
 
36
- const normalElement = {
37
- block,
38
- direction,
39
- alignX,
40
- alignY,
41
- equalCols,
42
- extraStyles: extendCss,
43
- }
32
+ const Component = (props: Partial<Props> & { ref?: unknown }) => {
33
+ const [own, rest] = splitProps(props, OWN_KEYS)
44
34
 
45
- const parentFixElement = {
46
- parentFix: true as const,
47
- block,
48
- extraStyles: extendCss,
35
+ const commonProps = {
36
+ ...rest,
37
+ ...DEV_PROPS,
38
+ ref: own.ref,
39
+ as: own.tag,
49
40
  }
50
41
 
51
- const childFixElement = {
52
- childFix: true as const,
53
- direction,
54
- alignX,
55
- alignY,
56
- equalCols,
57
- }
42
+ const needsFix = !own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag)
58
43
 
59
44
  if (!needsFix) {
60
45
  return (
61
- <Styled {...COMMON_PROPS} $element={normalElement}>
62
- {children}
46
+ <Styled
47
+ {...commonProps}
48
+ $element={{
49
+ block: own.block,
50
+ direction: own.direction,
51
+ alignX: own.alignX,
52
+ alignY: own.alignY,
53
+ equalCols: own.equalCols,
54
+ extraStyles: own.extendCss,
55
+ }}
56
+ >
57
+ {own.children}
63
58
  </Styled>
64
59
  )
65
60
  }
66
61
 
67
- const asTag = isInline ? 'span' : 'div'
62
+ const asTag = own.isInline ? 'span' : 'div'
68
63
 
69
64
  return (
70
- <Styled {...COMMON_PROPS} $element={parentFixElement}>
71
- <Styled as={asTag} $childFix $element={childFixElement}>
72
- {children}
65
+ <Styled
66
+ {...commonProps}
67
+ $element={{
68
+ parentFix: true as const,
69
+ block: own.block,
70
+ extraStyles: own.extendCss,
71
+ }}
72
+ >
73
+ <Styled
74
+ as={asTag}
75
+ $childFix
76
+ $element={{
77
+ childFix: true as const,
78
+ direction: own.direction,
79
+ alignX: own.alignX,
80
+ alignY: own.alignY,
81
+ equalCols: own.equalCols,
82
+ }}
83
+ >
84
+ {own.children}
73
85
  </Styled>
74
86
  </Styled>
75
87
  )