@octane-xplat/pager 0.15.0 → 0.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/pager",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Paged swipe container for Octane xplat — web scroll-snap row + @nativescript-community/ui-pager (ViewPager2 / paging UICollectionView)",
5
5
  "keywords": [
6
6
  "android",
@@ -51,11 +51,11 @@
51
51
  "devDependencies": {
52
52
  "@nativescript-community/octane": "0.2.5",
53
53
  "@nativescript/core": "9.1.3",
54
- "@octane-xplat/macos-renderer": "0.15.0",
55
- "@octane-xplat/ui": "0.15.0",
54
+ "@octane-xplat/macos-renderer": "0.16.0",
55
+ "@octane-xplat/ui": "0.16.0",
56
56
  "@octanejs/vite-plugin": "0.1.61",
57
57
  "octane": "0.11.0",
58
- "tsrx-typegen": "0.3.0",
58
+ "tsrx-typegen": "0.3.1",
59
59
  "typescript": "5.9.3",
60
60
  "vite": "8.3.0"
61
61
  },
package/src/Pager.tsrx CHANGED
@@ -1,20 +1,27 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
- import { memo, useCallback, useEffect, useRef, useState } from 'octane';
2
+ import { memo, useCallback, useEffect, useRef, useState } from 'octane'
3
+ import { registerElement } from '@nativescript-community/octane'
4
+
5
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
6
+ import {
7
+ createPortal as createUniversalPortal,
8
+ type UniversalRenderable,
9
+ } from 'octane/universal/native'
10
+
11
+ import { Pager as NSPager } from '@nativescript-community/ui-pager'
12
+ import { GridLayout } from '@nativescript/core'
13
+ import type { PagerProps } from './props'
3
14
  import {
4
- registerElement,
5
- } from '@nativescript-community/octane';
15
+ applyEscapeProps,
16
+ nativeAccessibilityRole,
17
+ nativeAccessibilityState,
18
+ } from './escape-props.mobile'
6
19
 
7
- import type { Attributes } from '@nativescript-community/octane/intrinsics';
8
- import { createPortal as createUniversalPortal, type UniversalRenderable } from 'octane/universal/native';
9
- import { Pager as NSPager } from '@nativescript-community/ui-pager';
10
- import { GridLayout } from '@nativescript/core';
11
- import type { PagerProps } from './props';
12
- import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props.mobile';
13
- import { layoutChildProps } from './layout-child';
20
+ import { layoutChildProps } from './layout-child'
14
21
 
15
22
  declare module '@nativescript-community/octane/intrinsics' {
16
23
  interface NativeScriptElements {
17
- pager: Attributes<typeof NSPager>;
24
+ pager: Attributes<typeof NSPager>
18
25
  }
19
26
  }
20
27
 
@@ -22,168 +29,188 @@ declare module '@nativescript-community/octane/intrinsics' {
22
29
  // UICollectionView on iOS. It is a required peer: this leaf sits in the
23
30
  // shared native barrel, so every native app must declare the plugin for
24
31
  // the /ns/m dev bridge to resolve it under the app root.
25
- registerElement('pager', NSPager);
32
+ registerElement('pager', NSPager)
26
33
 
27
34
  function createPortal(children: unknown, target: unknown) {
28
- return createUniversalPortal(children as UniversalRenderable, target);
35
+ return createUniversalPortal(children as UniversalRenderable, target)
29
36
  }
30
37
 
31
38
  /** One recycled page: the adapter-owned host plus the current render data.
32
- * Its content is a portal from Pager's declaring tree. */
39
+ * Its content is a portal from Pager's declaring tree. */
33
40
  interface PagerCell {
34
- host: any;
35
- pager: any;
36
- renderItem: any;
37
- item: any;
38
- index: number;
41
+ host: any
42
+ pager: any
43
+ renderItem: any
44
+ item: any
45
+ index: number
39
46
  }
40
47
 
41
- const cellsByHost = new WeakMap<object, PagerCell>();
42
- const cellsByPager = new WeakMap<object, Set<PagerCell>>();
48
+ const cellsByHost = new WeakMap<object, PagerCell>()
49
+ const cellsByPager = new WeakMap<object, Set<PagerCell>>()
43
50
 
44
51
  function cellFor(host: any): PagerCell {
45
- let cell = cellsByHost.get(host);
52
+ let cell = cellsByHost.get(host)
46
53
  if (!cell) {
47
- cell = { host, pager: null, renderItem: null, item: undefined, index: -1 };
48
- cellsByHost.set(host, cell);
54
+ cell = { host, pager: null, renderItem: null, item: undefined, index: -1 }
55
+ cellsByHost.set(host, cell)
49
56
  }
50
57
 
51
- return cell;
58
+ return cell
52
59
  }
53
60
 
54
61
  /** `createView` runs inside the plugin's adapter — no pager instance in
55
62
  * scope, so the cell is only linked to its pager at `itemLoading` (where
56
63
  * `args.object` is the owner). */
57
64
  function createPageHost() {
58
- const host = new GridLayout();
59
- cellFor(host);
60
- return host;
65
+ const host = new GridLayout()
66
+ cellFor(host)
67
+ return host
61
68
  }
62
69
 
63
- const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }];
70
+ const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }]
64
71
  // The built-in 'default' template stays first in the plugin's internal
65
72
  // list, so the selector must name our key — a 'default' entry would match
66
73
  // the built-in (an XML string parse, which yields nothing here).
67
- const selectPageTemplate = () => 'xplat-page';
74
+ const selectPageTemplate = () => 'xplat-page'
68
75
 
69
76
  function releasePagerCells(pager: any) {
70
- const cells = cellsByPager.get(pager);
71
- if (!cells) return;
77
+ const cells = cellsByPager.get(pager)
78
+ if (!cells) {
79
+ return
80
+ }
81
+
72
82
  for (const cell of cells) {
73
- cellsByHost.delete(cell.host);
83
+ cellsByHost.delete(cell.host)
74
84
  }
75
85
 
76
- cellsByPager.delete(pager);
86
+ cellsByPager.delete(pager)
77
87
  }
78
88
 
79
89
  function PagerInner(props: PagerProps) @{
80
- const host = useRef<any>(null);
81
- const [portalCells, setPortalCells] = useState<PagerCell[]>([]);
90
+ const host = useRef<any>(null)
91
+ const [portalCells, setPortalCells] = useState<PagerCell[]>([])
82
92
  // Track selectedIndex writes across synchronous and delayed echoes.
83
93
  // The plugin raises
84
94
  // 'selectedIndexChange' for script writes exactly as for swipes, and
85
95
  // the driver's auto-mute only covers JSX prop writes, not ours.
86
- const current = useRef(props.page ?? props.defaultPage ?? 0);
87
- const pageSeeded = useRef(false);
96
+ const current = useRef(props.page ?? props.defaultPage ?? 0)
97
+ const pageSeeded = useRef(false)
88
98
 
89
99
  // Stable ref identity — an inline closure re-fires ref(null)/ref(v) per
90
100
  // render, which would bounce host.current around the effects below.
91
101
  const bindHost = useCallback((v: any) => {
92
- host.current = v;
93
- }, []);
102
+ host.current = v
103
+ }, [])
94
104
 
95
105
  const bindCell = (args: any) => {
96
- const cell = cellFor(args.view);
97
- cell.pager = args.object;
98
- let owned = cellsByPager.get(cell.pager);
106
+ const cell = cellFor(args.view)
107
+ cell.pager = args.object
108
+ let owned = cellsByPager.get(cell.pager)
99
109
  if (!owned) {
100
- owned = new Set();
101
- cellsByPager.set(cell.pager, owned);
110
+ owned = new Set()
111
+ cellsByPager.set(cell.pager, owned)
102
112
  }
103
113
 
104
- owned.add(cell);
105
- const item = args.bindingContext ?? props.items?.[args.index];
106
- if (cell.renderItem === props.renderItem && cell.index === args.index && Object.is(cell.item, item)) {
107
- return;
114
+ owned.add(cell)
115
+ const item = args.bindingContext ?? props.items?.[args.index]
116
+ if (
117
+ cell.renderItem === props.renderItem &&
118
+ cell.index === args.index &&
119
+ Object.is(cell.item, item)
120
+ ) {
121
+ return
108
122
  }
109
123
 
110
- cell.renderItem = props.renderItem;
111
- cell.item = item;
112
- cell.index = args.index;
124
+ cell.renderItem = props.renderItem
125
+ cell.item = item
126
+ cell.index = args.index
113
127
  setPortalCells((current) => {
114
- const existing = current.some((entry) => entry === cell);
115
- return existing ? [...current] : [...current, cell];
116
- });
117
- };
128
+ const existing = current.some((entry) => entry === cell)
129
+ return existing ? [...current] : [...current, cell]
130
+ })
131
+ }
118
132
 
119
133
  const disposeCell = (args: any) => {
120
- const cell = args.view && cellsByHost.get(args.view);
134
+ const cell = args.view && cellsByHost.get(args.view)
121
135
  if (cell) {
122
- cellsByHost.delete(args.view);
123
- if (cell.pager) cellsByPager.get(cell.pager)?.delete(cell);
124
- setPortalCells((current) => current.filter((entry) => entry !== cell));
136
+ cellsByHost.delete(args.view)
137
+ if (cell.pager) {
138
+ cellsByPager.get(cell.pager)?.delete(cell)
139
+ }
140
+
141
+ setPortalCells((current) => current.filter((entry) => entry !== cell))
125
142
  }
126
- };
143
+ }
127
144
 
128
145
  // One-time wiring (mount): the template set + selector are imperative —
129
146
  // as JSX props they would be re-applied on every render. The cleanup
130
147
  // captures the pager so cell roots release after the ref detaches.
131
148
  useEffect(() => {
132
- const pager = host.current;
133
- if (!pager) return;
134
- pager.itemTemplates = PAGER_TEMPLATES;
135
- pager.itemTemplateSelector = selectPageTemplate;
136
- return () => releasePagerCells(pager);
137
- }, []);
149
+ const pager = host.current
150
+ if (!pager) {
151
+ return
152
+ }
153
+
154
+ pager.itemTemplates = PAGER_TEMPLATES
155
+ pager.itemTemplateSelector = selectPageTemplate
156
+ return () => releasePagerCells(pager)
157
+ }, [])
138
158
 
139
159
  // Items and the index are imperative writes, not JSX props: `items` must
140
160
  // land before `selectedIndex` (the plugin coerces the index against the
141
161
  // page count — a write ahead of items clamps to -1), and a re-assert
142
162
  // after an items change repairs the plugin's own re-coercion.
143
163
  useEffect(() => {
144
- const pager = host.current;
145
- if (!pager) return;
146
- applyEscapeProps(pager, props);
147
- if (pager.items !== props.items) pager.items = props.items;
148
- const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage);
164
+ const pager = host.current
165
+ if (!pager) {
166
+ return
167
+ }
168
+
169
+ applyEscapeProps(pager, props)
170
+ if (pager.items !== props.items) {
171
+ pager.items = props.items
172
+ }
173
+
174
+ const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage)
149
175
  if (target !== undefined && pager.selectedIndex !== target) {
150
176
  // Track the coerced target before writing: Android may echo later.
151
- current.current = Math.max(0, Math.min(props.items.length - 1, target));
152
- pager.selectedIndex = target;
177
+ current.current = Math.max(0, Math.min(props.items.length - 1, target))
178
+ pager.selectedIndex = target
153
179
  }
154
180
 
155
- pageSeeded.current = true;
156
- });
181
+ pageSeeded.current = true
182
+ })
157
183
 
158
184
  <>
159
- <pager
160
- ref={bindHost}
161
- id={props.id}
162
- {...layoutChildProps(props)}
163
- className={['vx-pager', props.className]}
164
- style={props.style}
165
- onItemLoading={bindCell}
166
- onItemDisposing={disposeCell}
167
- onSelectedIndexChange={(args: any) => {
168
- if (args.value !== current.current) {
169
- current.current = args.value;
170
- props.onPageChange?.(args.value);
171
- }
172
- }}
173
- accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
174
- accessibilityLabel={props.accessibilityLabel}
175
- accessibilityHint={props.accessibilityHint}
176
- accessibilityValue={props.accessibilityValue}
177
- accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
178
- accessible={props.accessible}
179
- accessibilityLiveRegion={props.accessibilityLiveRegion as any}
180
- />
181
- {portalCells.map((cell) =>
182
- createPortal(
183
- <gridlayout>{cell.renderItem?.(cell.item, cell.index)}</gridlayout>,
184
- cell.host,
185
- ),
186
- )}
185
+ <pager
186
+ ref={bindHost}
187
+ id={props.id}
188
+ {...layoutChildProps(props)}
189
+ className={['vx-pager', props.className]}
190
+ onItemLoading={bindCell}
191
+ onItemDisposing={disposeCell}
192
+ onSelectedIndexChange={(args: any) => {
193
+ if (args.value !== current.current) {
194
+ current.current = args.value
195
+ props.onPageChange?.(args.value)
196
+ }
197
+ }}
198
+ accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
199
+ accessibilityLabel={props.accessibilityLabel}
200
+ accessibilityHint={props.accessibilityHint}
201
+ accessibilityValue={props.accessibilityValue}
202
+ accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
203
+ accessible={props.accessible}
204
+ accessibilityLiveRegion={props.accessibilityLiveRegion as any}
205
+ />
206
+ {portalCells.map((cell) =>
207
+ createPortal(
208
+ <masonview style={{ display: 'grid' }}>
209
+ {cell.renderItem?.(cell.item, cell.index)}
210
+ </masonview>,
211
+ cell.host,
212
+ ),
213
+ )}
187
214
  </>
188
215
  }
189
216
 
@@ -193,34 +220,36 @@ function PagerInner(props: PagerProps) @{
193
220
  // UITableView: `renderItem` should be identity-stable — a fresh closure per
194
221
  // render is deliberately not part of the compare. (`PagerInner` must precede
195
222
  // this — compiled components are consts, not hoisted functions.)
196
- const MemoPager = memo(PagerInner, (a, b) =>
197
- a.items === b.items &&
198
- a.className === b.className &&
199
- a.style === b.style &&
200
- a.id === b.id &&
201
- a.page === b.page &&
202
- a.onPageChange === b.onPageChange &&
203
- a.ios === b.ios &&
204
- a.android === b.android &&
205
- a.row === b.row &&
206
- a.col === b.col &&
207
- a.rowSpan === b.rowSpan &&
208
- a.colSpan === b.colSpan &&
209
- a.dock === b.dock &&
210
- a.left === b.left &&
211
- a.top === b.top &&
212
- a.flexGrow === b.flexGrow &&
213
- a.flexShrink === b.flexShrink &&
214
- a.alignSelf === b.alignSelf &&
215
- a.order === b.order &&
216
- a.accessible === b.accessible &&
217
- a.accessibilityRole === b.accessibilityRole &&
218
- a.accessibilityLabel === b.accessibilityLabel &&
219
- a.accessibilityHint === b.accessibilityHint &&
220
- a.accessibilityValue === b.accessibilityValue &&
221
- a.accessibilityState === b.accessibilityState &&
222
- a.accessibilityLiveRegion === b.accessibilityLiveRegion
223
- );
223
+ const MemoPager = memo(
224
+ PagerInner,
225
+ (a, b) =>
226
+ a.items === b.items &&
227
+ a.className === b.className &&
228
+ a.style === b.style &&
229
+ a.id === b.id &&
230
+ a.page === b.page &&
231
+ a.onPageChange === b.onPageChange &&
232
+ a.ios === b.ios &&
233
+ a.android === b.android &&
234
+ a.row === b.row &&
235
+ a.col === b.col &&
236
+ a.rowSpan === b.rowSpan &&
237
+ a.colSpan === b.colSpan &&
238
+ a.dock === b.dock &&
239
+ a.left === b.left &&
240
+ a.top === b.top &&
241
+ a.flexGrow === b.flexGrow &&
242
+ a.flexShrink === b.flexShrink &&
243
+ a.alignSelf === b.alignSelf &&
244
+ a.order === b.order &&
245
+ a.accessible === b.accessible &&
246
+ a.accessibilityRole === b.accessibilityRole &&
247
+ a.accessibilityLabel === b.accessibilityLabel &&
248
+ a.accessibilityHint === b.accessibilityHint &&
249
+ a.accessibilityValue === b.accessibilityValue &&
250
+ a.accessibilityState === b.accessibilityState &&
251
+ a.accessibilityLiveRegion === b.accessibilityLiveRegion,
252
+ )
224
253
 
225
254
  /** Pager — paged horizontal swiping via @nativescript-community/ui-pager
226
255
  * (ViewPager2 / paging UICollectionView), chrome-reset: one full-size cell
@@ -239,7 +268,6 @@ export function Pager(props: PagerProps) @{
239
268
  accessibilityLiveRegion={props.accessibilityLiveRegion}
240
269
  id={props.id}
241
270
  className={props.className}
242
- style={props.style}
243
271
  items={props.items}
244
272
  renderItem={props.renderItem}
245
273
  page={props.page}
@@ -1,57 +1,95 @@
1
1
  import type { LayoutChildProps, FlexContainerProps } from './props'
2
+ import { mergeStyle } from './style-merge'
2
3
 
3
4
  // start/end shorthand → flex-start/flex-end.
4
5
  const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
5
6
  const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
6
7
 
7
- /** NativeScript reads these attributes from each child view during parent
8
- * layout. Values are forwarded with NativeScript's own property names.
9
- * The flex-container props (justifyContent/alignItems/flexWrap/gap) land on
10
- * the host flexboxlayout itself — its own attributes, not child metadata. */
11
- export function layoutChildProps(props: LayoutChildProps & Partial<FlexContainerProps>) {
12
- const result: Record<string, any> = {}
13
- for (const key of [
14
- 'row',
15
- 'col',
16
- 'rowSpan',
17
- 'colSpan',
18
- 'dock',
19
- 'left',
20
- 'top',
21
- 'flexGrow',
22
- 'flexShrink',
23
- 'alignSelf',
24
- 'order',
25
- ] as const) {
26
- if (props[key] !== undefined) {
27
- result[key] = props[key]
8
+ /** Convert parent-layout metadata + the flex-container contract to the CSS
9
+ * style bag on the host masonview — mirrors layout-child.web.ts so the same
10
+ * component props compile to the same CSS on both targets. `dock` has no
11
+ * CSS equivalent on web (ignored there); on native it becomes absolute
12
+ * insets. Native adds `position: absolute` for edge-placed children since
13
+ * there is no `.vx-absolute > *` stylesheet rule on the mason engine. */
14
+ export function layoutChildProps(
15
+ props: LayoutChildProps & Partial<FlexContainerProps> & { style?: any },
16
+ baseStyle = props.style,
17
+ ) {
18
+ const style: Record<string, any> = { ...(typeof baseStyle === 'object' ? baseStyle : {}) }
19
+ if (props.row !== undefined) {
20
+ style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`
21
+ } else if (props.rowSpan !== undefined) {
22
+ style.gridRow = `span ${props.rowSpan}`
23
+ }
24
+
25
+ if (props.col !== undefined) {
26
+ style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`
27
+ } else if (props.colSpan !== undefined) {
28
+ style.gridColumn = `span ${props.colSpan}`
29
+ }
30
+
31
+ if (props.dock !== undefined) {
32
+ style.position = style.position ?? 'absolute'
33
+ if (props.dock === 'left' || props.dock === 'top' || props.dock === 'bottom') {
34
+ style.left = style.left ?? 0
35
+ }
36
+ if (props.dock === 'right' || props.dock === 'top' || props.dock === 'bottom') {
37
+ style.right = style.right ?? 0
38
+ }
39
+ if (props.dock === 'top' || props.dock === 'left' || props.dock === 'right') {
40
+ style.top = style.top ?? 0
41
+ }
42
+ if (props.dock === 'bottom' || props.dock === 'left' || props.dock === 'right') {
43
+ style.bottom = style.bottom ?? 0
28
44
  }
29
45
  }
30
46
 
31
- if (props.justifyContent !== undefined) {
32
- result.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
47
+ if (props.left !== undefined) {
48
+ style.left = props.left
49
+ }
50
+ if (props.top !== undefined) {
51
+ style.top = props.top
52
+ }
53
+ if ((props.left !== undefined || props.top !== undefined) && props.dock === undefined) {
54
+ style.position = style.position ?? 'absolute'
33
55
  }
34
56
 
35
- if (props.alignItems !== undefined) {
36
- result.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
57
+ if (props.flexGrow !== undefined) {
58
+ style.flexGrow = props.flexGrow
59
+ }
60
+ if (props.flexShrink !== undefined) {
61
+ style.flexShrink = props.flexShrink
62
+ }
63
+ if (props.alignSelf !== undefined) {
64
+ style.alignSelf = props.alignSelf
65
+ }
66
+ if (props.order !== undefined) {
67
+ style.order = props.order
37
68
  }
38
69
 
70
+ // Flex-container props (View/Row/Pressable hosts) — RN vocabulary on the
71
+ // element style.
72
+ if (props.justifyContent !== undefined) {
73
+ style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
74
+ }
75
+ if (props.alignItems !== undefined) {
76
+ style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
77
+ }
39
78
  if (props.flexWrap !== undefined) {
40
- result.flexWrap =
79
+ style.flexWrap =
41
80
  props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
42
81
  }
43
-
44
82
  if (props.gap !== undefined) {
45
- result.gap = props.gap
83
+ style.gap = props.gap
46
84
  }
47
-
48
85
  if (props.rowGap !== undefined) {
49
- result.rowGap = props.rowGap
86
+ style.rowGap = props.rowGap
50
87
  }
51
-
52
88
  if (props.columnGap !== undefined) {
53
- result.columnGap = props.columnGap
89
+ style.columnGap = props.columnGap
54
90
  }
55
91
 
56
- return result
92
+ // A string base is inline CSS — serialize the emitted keys under it so the
93
+ // authored declarations still win.
94
+ return { style: typeof baseStyle === 'string' ? mergeStyle(style, baseStyle) : style }
57
95
  }
@@ -0,0 +1,59 @@
1
+ const kebab = (key: string) => key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())
2
+
3
+ /** Anything accepted as a style value, including nested arrays. */
4
+ export type StyleInput =
5
+ | string
6
+ | Record<string, any>
7
+ | null
8
+ | undefined
9
+ | readonly StyleInput[]
10
+
11
+ /** Merge component-emitted style defaults under the authored `style` prop.
12
+ * Authored keys win; array styles fold in order; a string style is appended
13
+ * after the serialized defaults so its declarations override them too. */
14
+ export function mergeStyle(
15
+ base: Record<string, any>,
16
+ style?: Record<string, any> | readonly (Record<string, any> | null | undefined)[] | null,
17
+ ): Record<string, any>
18
+ export function mergeStyle(
19
+ base: Record<string, any>,
20
+ style?: StyleInput,
21
+ ): string | Record<string, any>
22
+ export function mergeStyle(
23
+ base: Record<string, any>,
24
+ style?: StyleInput,
25
+ ): string | Record<string, any> {
26
+ if (style == null) {
27
+ return base
28
+ }
29
+
30
+ let obj: Record<string, any> = { ...base }
31
+ const strings: string[] = []
32
+ const add = (s: StyleInput) => {
33
+ if (s == null) {
34
+ return
35
+ }
36
+ if (typeof s === 'string') {
37
+ strings.push(s)
38
+ } else if (Array.isArray(s)) {
39
+ for (const part of s) {
40
+ add(part)
41
+ }
42
+ } else {
43
+ obj = { ...obj, ...s }
44
+ }
45
+ }
46
+ add(style)
47
+
48
+ if (strings.length === 0) {
49
+ return obj
50
+ }
51
+
52
+ const prefix = Object.entries(obj)
53
+ .filter(([, v]) => v != null)
54
+ .map(([k, v]) => `${kebab(k)}:${v}`)
55
+ .join(';')
56
+ return [prefix, ...strings].filter(Boolean).join(';')
57
+ }
58
+
59
+ export type StyleProp = string | Record<string, any> | readonly StyleProp[] | null | undefined