@hzblj/kara-sx 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +7 -2
  2. package/lib/module/flatten-sx.js +59 -0
  3. package/lib/module/flatten-sx.js.map +1 -0
  4. package/lib/module/index.js +19 -0
  5. package/lib/module/index.js.map +1 -0
  6. package/lib/module/package.json +1 -0
  7. package/lib/module/resolve-cache.js +79 -0
  8. package/lib/module/resolve-cache.js.map +1 -0
  9. package/lib/module/responsive-sx.js +66 -0
  10. package/lib/module/responsive-sx.js.map +1 -0
  11. package/lib/module/responsive.types.js +4 -0
  12. package/lib/module/responsive.types.js.map +1 -0
  13. package/lib/module/sx-key.js +25 -0
  14. package/lib/module/sx-key.js.map +1 -0
  15. package/lib/module/sx-keys.js +46 -0
  16. package/lib/module/sx-keys.js.map +1 -0
  17. package/lib/module/sx-padding.js +26 -0
  18. package/lib/module/sx-padding.js.map +1 -0
  19. package/lib/module/sx-to-modifiers.android.js +168 -0
  20. package/lib/module/sx-to-modifiers.android.js.map +1 -0
  21. package/lib/module/sx-to-modifiers.ios.js +266 -0
  22. package/lib/module/sx-to-modifiers.ios.js.map +1 -0
  23. package/lib/module/sx-to-style.js +137 -0
  24. package/lib/module/sx-to-style.js.map +1 -0
  25. package/lib/module/sx.types.js +4 -0
  26. package/lib/module/sx.types.js.map +1 -0
  27. package/lib/module/use-sx-modifiers.android.js +30 -0
  28. package/lib/module/use-sx-modifiers.android.js.map +1 -0
  29. package/lib/module/use-sx-modifiers.ios.js +30 -0
  30. package/lib/module/use-sx-modifiers.ios.js.map +1 -0
  31. package/lib/module/use-sx.js +30 -0
  32. package/lib/module/use-sx.js.map +1 -0
  33. package/lib/module/value-key.js +16 -0
  34. package/lib/module/value-key.js.map +1 -0
  35. package/lib/module/view-sx-to-style.js +159 -0
  36. package/lib/module/view-sx-to-style.js.map +1 -0
  37. package/lib/module/view-sx.types.js +4 -0
  38. package/lib/module/view-sx.types.js.map +1 -0
  39. package/lib/module/with-sx.js +50 -0
  40. package/lib/module/with-sx.js.map +1 -0
  41. package/lib/typescript/flatten-sx.d.ts +15 -0
  42. package/lib/typescript/index.d.ts +16 -0
  43. package/lib/typescript/resolve-cache.d.ts +14 -0
  44. package/lib/typescript/responsive-sx.d.ts +80 -0
  45. package/lib/typescript/responsive.types.d.ts +23 -0
  46. package/lib/typescript/sx-key.d.ts +7 -0
  47. package/lib/typescript/sx-keys.d.ts +6 -0
  48. package/lib/typescript/sx-padding.d.ts +5 -0
  49. package/lib/typescript/sx-to-modifiers.d.ts +1 -0
  50. package/lib/typescript/sx-to-modifiers.ios.d.ts +4 -0
  51. package/lib/typescript/sx-to-style.d.ts +4 -0
  52. package/lib/typescript/sx.types.d.ts +182 -0
  53. package/lib/typescript/use-sx-modifiers.d.ts +1 -0
  54. package/lib/typescript/use-sx-modifiers.ios.d.ts +14 -0
  55. package/lib/typescript/use-sx.d.ts +15 -0
  56. package/lib/typescript/value-key.d.ts +7 -0
  57. package/lib/typescript/view-sx-to-style.d.ts +17 -0
  58. package/lib/typescript/view-sx.types.d.ts +80 -0
  59. package/lib/typescript/with-sx.d.ts +28 -0
  60. package/package.json +78 -4
  61. package/src/flatten-sx.ts +70 -0
  62. package/src/index.ts +16 -0
  63. package/src/resolve-cache.ts +105 -0
  64. package/src/responsive-sx.ts +75 -0
  65. package/src/responsive.types.ts +23 -0
  66. package/src/sx-key.ts +32 -0
  67. package/src/sx-keys.ts +47 -0
  68. package/src/sx-padding.ts +27 -0
  69. package/src/sx-to-modifiers.android.ts +224 -0
  70. package/src/sx-to-modifiers.ios.ts +322 -0
  71. package/src/sx-to-style.ts +142 -0
  72. package/src/sx.types.ts +215 -0
  73. package/src/use-sx-modifiers.android.ts +29 -0
  74. package/src/use-sx-modifiers.ios.ts +29 -0
  75. package/src/use-sx.ts +34 -0
  76. package/src/value-key.ts +18 -0
  77. package/src/view-sx-to-style.ts +180 -0
  78. package/src/view-sx.types.ts +87 -0
  79. package/src/with-sx.tsx +51 -0
@@ -0,0 +1,224 @@
1
+ import {
2
+ align,
3
+ alpha,
4
+ animateContentSize,
5
+ type BuiltinShape,
6
+ background,
7
+ blur,
8
+ clip,
9
+ defaultMinSize,
10
+ dropShadow,
11
+ fillMaxHeight,
12
+ fillMaxWidth,
13
+ graphicsLayer,
14
+ height as heightModifier,
15
+ imePadding,
16
+ innerShadow,
17
+ type ModifierConfig,
18
+ matchParentSize,
19
+ offset,
20
+ padding,
21
+ rotate,
22
+ Shapes,
23
+ weight,
24
+ width as widthModifier,
25
+ wrapContentHeight,
26
+ wrapContentWidth,
27
+ zIndex as zIndexModifier,
28
+ } from '@expo/ui/jetpack-compose/modifiers'
29
+ import {opacity, radii, resolvePaletteColor, resolveShadow, type Theme, zIndex} from '@hzblj/kara-theme'
30
+ import type {ColorValue} from 'react-native'
31
+ import type {AndroidSx, FrameAlignment, Sx} from './sx.types'
32
+ import {sxPadding} from './sx-padding'
33
+
34
+ /**
35
+ * The `full` radius token maps to `Circle` — Compose's `CircleShape` is a 50% rounding, so on any
36
+ * box it draws the same capsule the iOS resolver gets from SwiftUI's `Capsule`.
37
+ */
38
+ const shapeOf = (sx: Sx) => {
39
+ if (sx.borderRadius === 'full') {
40
+ return Shapes.Circle
41
+ }
42
+
43
+ return sx.borderRadius ? Shapes.RoundedCorner(radii[sx.borderRadius]) : Shapes.Rectangle
44
+ }
45
+
46
+ /**
47
+ * A stroke that traces the box's own shape. `border` takes no shape, so on a rounded box it would
48
+ * draw a square — an inner shadow with no blur and the width as its spread is the same inset stroke
49
+ * that `strokeBorder` paints on iOS, and it follows the corner radius.
50
+ */
51
+ const outline = (color: ColorValue, width: number, shape: BuiltinShape): ModifierConfig =>
52
+ innerShadow(shape, {color, radius: 0, spread: width})
53
+
54
+ /**
55
+ * Compose has no single transform modifier, so `scale` goes through `graphicsLayer` while `rotate` and
56
+ * `offset` keep their own — `rotate` is the readable one for the Z axis, and `offset` moves the box
57
+ * before it is drawn rather than after, which is what the React Native and SwiftUI paths also do.
58
+ */
59
+ const effectModifiers = (sx: Sx): ModifierConfig[] => {
60
+ const modifiers: ModifierConfig[] = []
61
+
62
+ if (sx.offsetX !== undefined || sx.offsetY !== undefined) {
63
+ modifiers.push(offset(sx.offsetX ?? 0, sx.offsetY ?? 0))
64
+ }
65
+
66
+ if (sx.rotate !== undefined) {
67
+ modifiers.push(rotate(sx.rotate))
68
+ }
69
+
70
+ if (sx.scale !== undefined) {
71
+ const scale = typeof sx.scale === 'number' ? {x: sx.scale, y: sx.scale} : sx.scale
72
+ modifiers.push(graphicsLayer({scaleX: scale.x ?? 1, scaleY: scale.y ?? 1}))
73
+ }
74
+
75
+ if (sx.blur !== undefined) {
76
+ modifiers.push(blur(sx.blur))
77
+ }
78
+
79
+ if (sx.opacity) {
80
+ modifiers.push(alpha(opacity[sx.opacity]))
81
+ }
82
+
83
+ if (sx.zIndex) {
84
+ modifiers.push(zIndexModifier(zIndex[sx.zIndex]))
85
+ }
86
+
87
+ return modifiers
88
+ }
89
+
90
+ /**
91
+ * `wrapContentWidth`/`Height` take an alignment, and `true` means "wrap, don't care where" — Compose's
92
+ * own default. Passing the flag straight through as the alignment would not type-check.
93
+ */
94
+ const wrapAlignment = <T extends string>(value: T | boolean | undefined): T | undefined =>
95
+ typeof value === 'boolean' ? undefined : value
96
+
97
+ /** The `android` bucket: Compose-only modifiers, which the SwiftUI resolver has no counterpart for. */
98
+ const androidModifiers = (android: AndroidSx | undefined): ModifierConfig[] => {
99
+ if (!android) {
100
+ return []
101
+ }
102
+
103
+ const modifiers: ModifierConfig[] = []
104
+
105
+ if (android.align) {
106
+ modifiers.push(align(android.align))
107
+ }
108
+
109
+ if (android.fillMaxWidth !== undefined) {
110
+ modifiers.push(fillMaxWidth(android.fillMaxWidth))
111
+ }
112
+
113
+ if (android.fillMaxHeight !== undefined) {
114
+ modifiers.push(fillMaxHeight(android.fillMaxHeight))
115
+ }
116
+
117
+ if (android.weight !== undefined) {
118
+ modifiers.push(weight(android.weight))
119
+ }
120
+
121
+ if (android.wrapContentWidth) {
122
+ modifiers.push(wrapContentWidth(wrapAlignment(android.wrapContentWidth)))
123
+ }
124
+
125
+ if (android.wrapContentHeight) {
126
+ modifiers.push(wrapContentHeight(wrapAlignment(android.wrapContentHeight)))
127
+ }
128
+
129
+ if (android.matchParentSize) {
130
+ modifiers.push(matchParentSize())
131
+ }
132
+
133
+ if (android.imePadding) {
134
+ modifiers.push(imePadding())
135
+ }
136
+
137
+ if (android.animateContentSize) {
138
+ modifiers.push(animateContentSize())
139
+ }
140
+
141
+ if (android.modifiers) {
142
+ modifiers.push(...android.modifiers)
143
+ }
144
+
145
+ return modifiers
146
+ }
147
+
148
+ export const sxToModifiers = (sx: Sx, theme: Theme, _alignment: FrameAlignment = 'topLeading'): ModifierConfig[] => {
149
+ const modifiers: ModifierConfig[] = []
150
+ const shape = shapeOf(sx)
151
+ const shadow = sx.shadow ? resolveShadow(theme, sx.shadow) : undefined
152
+ const geometry = shadow?.geometry
153
+ const shadowColors = shadow?.colors
154
+
155
+ if (sx.wide) {
156
+ modifiers.push(fillMaxWidth())
157
+ }
158
+
159
+ if (sx.tall) {
160
+ modifiers.push(fillMaxHeight())
161
+ }
162
+
163
+ if (sx.w !== undefined) {
164
+ modifiers.push(widthModifier(sx.w))
165
+ }
166
+
167
+ if (sx.h !== undefined) {
168
+ modifiers.push(heightModifier(sx.h))
169
+ }
170
+
171
+ if (sx.minW !== undefined || sx.minH !== undefined) {
172
+ modifiers.push(defaultMinSize({minHeight: sx.minH, minWidth: sx.minW}))
173
+ }
174
+
175
+ if (geometry?.drop && shadowColors?.drop) {
176
+ modifiers.push(
177
+ dropShadow(shape, {
178
+ color: resolvePaletteColor(theme, shadowColors.drop),
179
+ offsetY: geometry.drop.y,
180
+ radius: geometry.drop.radius,
181
+ })
182
+ )
183
+ }
184
+
185
+ // A draw modifier is only clipped by a `clip` earlier in the chain, so the clip has to come
186
+ // between the drop shadow — which must paint outside the box — and everything drawn within it.
187
+ if (sx.borderRadius) {
188
+ modifiers.push(clip(shape))
189
+ }
190
+
191
+ if (sx.bg) {
192
+ modifiers.push(background(theme.color(sx.bg)))
193
+ }
194
+
195
+ if (geometry?.inset && shadowColors?.inset) {
196
+ modifiers.push(
197
+ innerShadow(shape, {
198
+ color: resolvePaletteColor(theme, shadowColors.inset),
199
+ radius: geometry.inset.radius,
200
+ })
201
+ )
202
+ }
203
+
204
+ if (sx.borderWidth !== undefined && sx.borderColor) {
205
+ modifiers.push(outline(theme.color(sx.borderColor), sx.borderWidth, shape))
206
+ }
207
+
208
+ if (geometry?.ring && shadowColors?.ring) {
209
+ modifiers.push(outline(resolvePaletteColor(theme, shadowColors.ring), geometry.ring.width, shape))
210
+ }
211
+
212
+ const boxPadding = sxPadding(sx)
213
+
214
+ if (boxPadding) {
215
+ modifiers.push(
216
+ padding(boxPadding.leading ?? 0, boxPadding.top ?? 0, boxPadding.trailing ?? 0, boxPadding.bottom ?? 0)
217
+ )
218
+ }
219
+
220
+ modifiers.push(...effectModifiers(sx))
221
+ modifiers.push(...androidModifiers(sx.android))
222
+
223
+ return modifiers
224
+ }
@@ -0,0 +1,322 @@
1
+ import {
2
+ aspectRatio,
3
+ background,
4
+ blur,
5
+ brightness,
6
+ clipped,
7
+ clipShape,
8
+ colorInvert,
9
+ containerRelativeFrame,
10
+ contentTransition,
11
+ contrast,
12
+ controlSize,
13
+ fixedSize,
14
+ frame,
15
+ glassEffect,
16
+ grayscale,
17
+ hidden,
18
+ hueRotation,
19
+ ignoreSafeArea,
20
+ layoutPriority,
21
+ luminanceToAlpha,
22
+ type ModifierConfig,
23
+ offset,
24
+ opacity as opacityModifier,
25
+ padding,
26
+ privacySensitive,
27
+ redacted,
28
+ rotationEffect,
29
+ saturation,
30
+ scaleEffect,
31
+ shadow,
32
+ strokeBorder,
33
+ tint,
34
+ unredacted,
35
+ zIndex as zIndexModifier,
36
+ } from '@expo/ui/swift-ui/modifiers'
37
+ import {opacity, radii, resolvePaletteColor, resolveShadow, type Theme, zIndex} from '@hzblj/kara-theme'
38
+ import type {ColorValue} from 'react-native'
39
+ import type {FrameAlignment, IosSx, Sx} from './sx.types'
40
+ import {sxPadding} from './sx-padding'
41
+
42
+ /**
43
+ * The one shape every decoration traces — clip, strokes, drop shadow and glass all take it, so the
44
+ * box reads as a single surface. The `full` radius token maps to a real capsule: SwiftUI's
45
+ * `RoundedRectangle` degenerates when the radius outgrows the box, where `Capsule` stays clean.
46
+ */
47
+ type BoxShape = {
48
+ cornerRadius?: number
49
+ shape: 'capsule' | 'rectangle' | 'roundedRectangle'
50
+ }
51
+
52
+ const boxShape = (sx: Sx): BoxShape => {
53
+ if (sx.borderRadius === 'full') {
54
+ return {shape: 'capsule'}
55
+ }
56
+
57
+ return sx.borderRadius ? {cornerRadius: radii[sx.borderRadius], shape: 'roundedRectangle'} : {shape: 'rectangle'}
58
+ }
59
+
60
+ /**
61
+ * A stroke that traces the box's own shape. `.border` always draws a square, so on a rounded box the
62
+ * following `.clipShape` cuts its corners off — `strokeBorder` follows the shape and stays inside.
63
+ */
64
+ const outline = (color: ColorValue, width: number, box: BoxShape): ModifierConfig =>
65
+ strokeBorder({
66
+ color,
67
+ cornerRadius: box.cornerRadius,
68
+ shape: box.shape,
69
+ style: {lineWidth: width},
70
+ })
71
+
72
+ /**
73
+ * `inset` is Android-only: SwiftUI has no inner-shadow modifier, and standing in with `.glassEffect`
74
+ * paints a whole Liquid Glass material over the box — a washed-out plate, not a shadow.
75
+ */
76
+ const shadowModifiers = (sx: Sx, theme: Theme, box: BoxShape): ModifierConfig[] => {
77
+ if (!sx.shadow) {
78
+ return []
79
+ }
80
+
81
+ const {colors, geometry} = resolveShadow(theme, sx.shadow)
82
+ const modifiers: ModifierConfig[] = []
83
+
84
+ if (geometry.ring && colors.ring) {
85
+ modifiers.push(outline(resolvePaletteColor(theme, colors.ring), geometry.ring.width, box))
86
+ }
87
+
88
+ if (geometry.drop && colors.drop) {
89
+ modifiers.push(
90
+ shadow({
91
+ color: resolvePaletteColor(theme, colors.drop),
92
+ radius: geometry.drop.radius,
93
+ y: geometry.drop.y,
94
+ })
95
+ )
96
+ }
97
+
98
+ return modifiers
99
+ }
100
+
101
+ /**
102
+ * The native `FrameModifier` applies either the exact `width`/`height` pair or the min/max set —
103
+ * never both, so one call mixing them would silently drop its min/max half. Each set therefore gets
104
+ * its own `frame`: the exact one first, then the flexible one for the axes left free.
105
+ */
106
+ const frameModifiers = (sx: Sx, alignment: FrameAlignment): ModifierConfig[] => {
107
+ const modifiers: ModifierConfig[] = []
108
+
109
+ if (sx.w !== undefined || sx.h !== undefined) {
110
+ modifiers.push(frame({alignment, height: sx.h, width: sx.w}))
111
+ }
112
+
113
+ if (sx.wide || sx.tall || sx.minW !== undefined || sx.minH !== undefined) {
114
+ modifiers.push(
115
+ frame({
116
+ alignment,
117
+ maxHeight: sx.tall ? Infinity : undefined,
118
+ maxWidth: sx.wide ? Infinity : undefined,
119
+ minHeight: sx.minH,
120
+ minWidth: sx.minW,
121
+ })
122
+ )
123
+ }
124
+
125
+ return modifiers
126
+ }
127
+
128
+ /** Transforms and filters, which SwiftUI applies to the box after it has been laid out and painted. */
129
+ const effectModifiers = (sx: Sx): ModifierConfig[] => {
130
+ const modifiers: ModifierConfig[] = []
131
+
132
+ if (sx.offsetX !== undefined || sx.offsetY !== undefined) {
133
+ modifiers.push(offset({x: sx.offsetX, y: sx.offsetY}))
134
+ }
135
+
136
+ if (sx.rotate !== undefined) {
137
+ modifiers.push(rotationEffect(sx.rotate))
138
+ }
139
+
140
+ if (sx.scale !== undefined) {
141
+ modifiers.push(scaleEffect(typeof sx.scale === 'number' ? sx.scale : {x: sx.scale.x ?? 1, y: sx.scale.y ?? 1}))
142
+ }
143
+
144
+ if (sx.blur !== undefined) {
145
+ modifiers.push(blur(sx.blur))
146
+ }
147
+
148
+ if (sx.opacity) {
149
+ modifiers.push(opacityModifier(opacity[sx.opacity]))
150
+ }
151
+
152
+ if (sx.zIndex) {
153
+ modifiers.push(zIndexModifier(zIndex[sx.zIndex]))
154
+ }
155
+
156
+ return modifiers
157
+ }
158
+
159
+ const FIXED_SIZE_AXES = {
160
+ both: {horizontal: true, vertical: true},
161
+ horizontal: {horizontal: true},
162
+ vertical: {vertical: true},
163
+ } as const
164
+
165
+ /**
166
+ * Liquid Glass from either form of the `glass` key. With no shape of its own, the glass follows the
167
+ * box's `borderRadius` — a plate that ignored the box's corners would read as a different surface.
168
+ */
169
+ const glassModifier = (glass: NonNullable<IosSx['glass']>, theme: Theme, box: BoxShape) => {
170
+ const config = typeof glass === 'string' ? {variant: glass} : glass
171
+ const followsBox = config.shape === undefined && box.shape !== 'rectangle'
172
+
173
+ return glassEffect({
174
+ cornerRadius: config.cornerRadius ?? (followsBox ? box.cornerRadius : undefined),
175
+ glass: {
176
+ interactive: config.interactive,
177
+ tint: config.tint === undefined ? undefined : theme.color(config.tint),
178
+ variant: config.variant,
179
+ },
180
+ shape: followsBox ? box.shape : config.shape,
181
+ })
182
+ }
183
+
184
+ /** The `ios` bucket: SwiftUI-only modifiers, which the Compose resolver has no counterpart for. */
185
+ const iosModifiers = (ios: IosSx | undefined, theme: Theme, box: BoxShape): ModifierConfig[] => {
186
+ if (!ios) {
187
+ return []
188
+ }
189
+
190
+ const modifiers: ModifierConfig[] = []
191
+
192
+ if (ios.aspectRatio !== undefined) {
193
+ modifiers.push(aspectRatio({contentMode: 'fit', ratio: ios.aspectRatio}))
194
+ }
195
+
196
+ // The ideal sizes live in the native frame modifier's min/max branch, so they need their own
197
+ // `frame` call — one with an exact width or height would never reach them.
198
+ if (ios.idealW !== undefined || ios.idealH !== undefined) {
199
+ modifiers.push(frame({idealHeight: ios.idealH, idealWidth: ios.idealW}))
200
+ }
201
+
202
+ if (ios.containerRelativeFrame) {
203
+ modifiers.push(containerRelativeFrame(ios.containerRelativeFrame))
204
+ }
205
+
206
+ if (ios.ignoreSafeArea) {
207
+ modifiers.push(ignoreSafeArea(ios.ignoreSafeArea === true ? undefined : ios.ignoreSafeArea))
208
+ }
209
+
210
+ if (ios.glass) {
211
+ modifiers.push(glassModifier(ios.glass, theme, box))
212
+ }
213
+
214
+ if (ios.tint) {
215
+ modifiers.push(tint(theme.color(ios.tint)))
216
+ }
217
+
218
+ if (ios.controlSize) {
219
+ modifiers.push(controlSize(ios.controlSize))
220
+ }
221
+
222
+ if (ios.layoutPriority !== undefined) {
223
+ modifiers.push(layoutPriority(ios.layoutPriority))
224
+ }
225
+
226
+ if (ios.fixedSize) {
227
+ modifiers.push(fixedSize(FIXED_SIZE_AXES[ios.fixedSize]))
228
+ }
229
+
230
+ if (ios.clipped) {
231
+ modifiers.push(clipped(true))
232
+ }
233
+
234
+ if (ios.hidden) {
235
+ modifiers.push(hidden(true))
236
+ }
237
+
238
+ if (ios.redacted) {
239
+ modifiers.push(redacted(ios.redacted))
240
+ }
241
+
242
+ if (ios.unredacted) {
243
+ modifiers.push(unredacted())
244
+ }
245
+
246
+ if (ios.privacySensitive) {
247
+ modifiers.push(privacySensitive(true))
248
+ }
249
+
250
+ if (ios.saturation !== undefined) {
251
+ modifiers.push(saturation(ios.saturation))
252
+ }
253
+
254
+ if (ios.brightness !== undefined) {
255
+ modifiers.push(brightness(ios.brightness))
256
+ }
257
+
258
+ if (ios.contrast !== undefined) {
259
+ modifiers.push(contrast(ios.contrast))
260
+ }
261
+
262
+ if (ios.grayscale !== undefined) {
263
+ modifiers.push(grayscale(ios.grayscale))
264
+ }
265
+
266
+ if (ios.hueRotation !== undefined) {
267
+ modifiers.push(hueRotation(ios.hueRotation))
268
+ }
269
+
270
+ if (ios.colorInvert) {
271
+ modifiers.push(colorInvert(true))
272
+ }
273
+
274
+ if (ios.luminanceToAlpha) {
275
+ modifiers.push(luminanceToAlpha())
276
+ }
277
+
278
+ if (ios.contentTransition) {
279
+ modifiers.push(
280
+ contentTransition(ios.contentTransition.type, {
281
+ countsDown: ios.contentTransition.countsDown,
282
+ })
283
+ )
284
+ }
285
+
286
+ if (ios.modifiers) {
287
+ modifiers.push(...ios.modifiers)
288
+ }
289
+
290
+ return modifiers
291
+ }
292
+
293
+ export const sxToModifiers = (sx: Sx, theme: Theme, alignment: FrameAlignment = 'topLeading'): ModifierConfig[] => {
294
+ const modifiers: ModifierConfig[] = []
295
+ const boxPadding = sxPadding(sx)
296
+ const box = boxShape(sx)
297
+
298
+ if (boxPadding) {
299
+ modifiers.push(padding(boxPadding))
300
+ }
301
+
302
+ modifiers.push(...frameModifiers(sx, alignment))
303
+
304
+ if (sx.bg) {
305
+ modifiers.push(background(theme.color(sx.bg)))
306
+ }
307
+
308
+ // Clip before the strokes and the drop shadow, so all three trace the rounded box, not its bounds.
309
+ if (box.shape !== 'rectangle') {
310
+ modifiers.push(clipShape(box.shape, box.cornerRadius))
311
+ }
312
+
313
+ if (sx.borderWidth !== undefined && sx.borderColor) {
314
+ modifiers.push(outline(theme.color(sx.borderColor), sx.borderWidth, box))
315
+ }
316
+
317
+ modifiers.push(...shadowModifiers(sx, theme, box))
318
+ modifiers.push(...effectModifiers(sx))
319
+ modifiers.push(...iosModifiers(sx.ios, theme, box))
320
+
321
+ return modifiers
322
+ }
@@ -0,0 +1,142 @@
1
+ import {opacity, radii, resolvePaletteColor, resolveShadow, type Theme, zIndex} from '@hzblj/kara-theme'
2
+ import {StyleSheet, type ViewStyle} from 'react-native'
3
+ import type {StyleSx} from './sx.types'
4
+ import {sxPadding} from './sx-padding'
5
+
6
+ /**
7
+ * React Native takes transforms as one ordered array, so — unlike the modifier paths, where each is
8
+ * its own entry — they have to be collected before any of them can be written to the style.
9
+ */
10
+ /** React Native also accepts `transform` as a CSS string; `Extract` keeps the array form's element. */
11
+ type Transform = Extract<NonNullable<ViewStyle['transform']>, readonly unknown[]>[number]
12
+
13
+ const transform = (sx: StyleSx): ViewStyle['transform'] => {
14
+ const transforms: Transform[] = []
15
+
16
+ if (sx.offsetX !== undefined) {
17
+ transforms.push({translateX: sx.offsetX})
18
+ }
19
+
20
+ if (sx.offsetY !== undefined) {
21
+ transforms.push({translateY: sx.offsetY})
22
+ }
23
+
24
+ if (sx.rotate !== undefined) {
25
+ transforms.push({rotate: `${sx.rotate}deg`})
26
+ }
27
+
28
+ if (typeof sx.scale === 'number') {
29
+ transforms.push({scale: sx.scale})
30
+ } else if (sx.scale !== undefined) {
31
+ if (sx.scale.x !== undefined) {
32
+ transforms.push({scaleX: sx.scale.x})
33
+ }
34
+
35
+ if (sx.scale.y !== undefined) {
36
+ transforms.push({scaleY: sx.scale.y})
37
+ }
38
+ }
39
+
40
+ return transforms.length > 0 ? transforms : undefined
41
+ }
42
+
43
+ /**
44
+ * A React Native view has a single border slot, so a ring shadow rides it — an explicit
45
+ * `borderColor`/`borderWidth` on the same box is written later and wins, where the modifier paths
46
+ * draw the ring and the border as two separate strokes.
47
+ */
48
+ const shadowStyle = (sx: StyleSx, theme: Theme): ViewStyle => {
49
+ if (!sx.shadow) {
50
+ return {}
51
+ }
52
+
53
+ const {colors, geometry} = resolveShadow(theme, sx.shadow)
54
+ const style: ViewStyle = {}
55
+
56
+ if (geometry.ring && colors.ring) {
57
+ style.borderColor = resolvePaletteColor(theme, colors.ring)
58
+ style.borderWidth = Math.max(geometry.ring.width, StyleSheet.hairlineWidth)
59
+ }
60
+
61
+ if (geometry.drop && colors.drop) {
62
+ style.elevation = geometry.drop.y * 2
63
+ style.shadowColor = resolvePaletteColor(theme, colors.drop)
64
+ style.shadowOffset = {height: geometry.drop.y, width: 0}
65
+ style.shadowOpacity = 1
66
+ style.shadowRadius = geometry.drop.radius
67
+ }
68
+
69
+ return style
70
+ }
71
+
72
+ export const sxToStyle = (sx: StyleSx, theme: Theme): ViewStyle => {
73
+ const style: ViewStyle = {...shadowStyle(sx, theme)}
74
+ const padding = sxPadding(sx)
75
+
76
+ if (padding) {
77
+ style.paddingTop = padding.top
78
+ style.paddingBottom = padding.bottom
79
+ style.paddingLeft = padding.leading
80
+ style.paddingRight = padding.trailing
81
+ }
82
+
83
+ if (sx.wide) {
84
+ style.width = '100%'
85
+ }
86
+
87
+ if (sx.tall) {
88
+ style.height = '100%'
89
+ }
90
+
91
+ if (sx.w !== undefined) {
92
+ style.width = sx.w
93
+ }
94
+
95
+ if (sx.h !== undefined) {
96
+ style.height = sx.h
97
+ }
98
+
99
+ if (sx.minW !== undefined) {
100
+ style.minWidth = sx.minW
101
+ }
102
+
103
+ if (sx.minH !== undefined) {
104
+ style.minHeight = sx.minH
105
+ }
106
+
107
+ if (sx.bg) {
108
+ style.backgroundColor = theme.color(sx.bg)
109
+ }
110
+
111
+ if (sx.borderColor) {
112
+ style.borderColor = theme.color(sx.borderColor)
113
+ }
114
+
115
+ if (sx.borderWidth !== undefined) {
116
+ style.borderWidth = sx.borderWidth
117
+ }
118
+
119
+ if (sx.borderRadius) {
120
+ style.borderRadius = radii[sx.borderRadius]
121
+ }
122
+
123
+ if (sx.opacity) {
124
+ style.opacity = opacity[sx.opacity]
125
+ }
126
+
127
+ if (sx.zIndex) {
128
+ style.zIndex = zIndex[sx.zIndex]
129
+ }
130
+
131
+ if (sx.blur !== undefined) {
132
+ style.filter = [{blur: sx.blur}]
133
+ }
134
+
135
+ const transforms = transform(sx)
136
+
137
+ if (transforms) {
138
+ style.transform = transforms
139
+ }
140
+
141
+ return style
142
+ }