@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.
- package/README.md +7 -2
- package/lib/module/flatten-sx.js +59 -0
- package/lib/module/flatten-sx.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/resolve-cache.js +79 -0
- package/lib/module/resolve-cache.js.map +1 -0
- package/lib/module/responsive-sx.js +66 -0
- package/lib/module/responsive-sx.js.map +1 -0
- package/lib/module/responsive.types.js +4 -0
- package/lib/module/responsive.types.js.map +1 -0
- package/lib/module/sx-key.js +25 -0
- package/lib/module/sx-key.js.map +1 -0
- package/lib/module/sx-keys.js +46 -0
- package/lib/module/sx-keys.js.map +1 -0
- package/lib/module/sx-padding.js +26 -0
- package/lib/module/sx-padding.js.map +1 -0
- package/lib/module/sx-to-modifiers.android.js +168 -0
- package/lib/module/sx-to-modifiers.android.js.map +1 -0
- package/lib/module/sx-to-modifiers.ios.js +266 -0
- package/lib/module/sx-to-modifiers.ios.js.map +1 -0
- package/lib/module/sx-to-style.js +137 -0
- package/lib/module/sx-to-style.js.map +1 -0
- package/lib/module/sx.types.js +4 -0
- package/lib/module/sx.types.js.map +1 -0
- package/lib/module/use-sx-modifiers.android.js +30 -0
- package/lib/module/use-sx-modifiers.android.js.map +1 -0
- package/lib/module/use-sx-modifiers.ios.js +30 -0
- package/lib/module/use-sx-modifiers.ios.js.map +1 -0
- package/lib/module/use-sx.js +30 -0
- package/lib/module/use-sx.js.map +1 -0
- package/lib/module/value-key.js +16 -0
- package/lib/module/value-key.js.map +1 -0
- package/lib/module/view-sx-to-style.js +159 -0
- package/lib/module/view-sx-to-style.js.map +1 -0
- package/lib/module/view-sx.types.js +4 -0
- package/lib/module/view-sx.types.js.map +1 -0
- package/lib/module/with-sx.js +50 -0
- package/lib/module/with-sx.js.map +1 -0
- package/lib/typescript/flatten-sx.d.ts +15 -0
- package/lib/typescript/index.d.ts +16 -0
- package/lib/typescript/resolve-cache.d.ts +14 -0
- package/lib/typescript/responsive-sx.d.ts +80 -0
- package/lib/typescript/responsive.types.d.ts +23 -0
- package/lib/typescript/sx-key.d.ts +7 -0
- package/lib/typescript/sx-keys.d.ts +6 -0
- package/lib/typescript/sx-padding.d.ts +5 -0
- package/lib/typescript/sx-to-modifiers.d.ts +1 -0
- package/lib/typescript/sx-to-modifiers.ios.d.ts +4 -0
- package/lib/typescript/sx-to-style.d.ts +4 -0
- package/lib/typescript/sx.types.d.ts +182 -0
- package/lib/typescript/use-sx-modifiers.d.ts +1 -0
- package/lib/typescript/use-sx-modifiers.ios.d.ts +14 -0
- package/lib/typescript/use-sx.d.ts +15 -0
- package/lib/typescript/value-key.d.ts +7 -0
- package/lib/typescript/view-sx-to-style.d.ts +17 -0
- package/lib/typescript/view-sx.types.d.ts +80 -0
- package/lib/typescript/with-sx.d.ts +28 -0
- package/package.json +78 -4
- package/src/flatten-sx.ts +70 -0
- package/src/index.ts +16 -0
- package/src/resolve-cache.ts +105 -0
- package/src/responsive-sx.ts +75 -0
- package/src/responsive.types.ts +23 -0
- package/src/sx-key.ts +32 -0
- package/src/sx-keys.ts +47 -0
- package/src/sx-padding.ts +27 -0
- package/src/sx-to-modifiers.android.ts +224 -0
- package/src/sx-to-modifiers.ios.ts +322 -0
- package/src/sx-to-style.ts +142 -0
- package/src/sx.types.ts +215 -0
- package/src/use-sx-modifiers.android.ts +29 -0
- package/src/use-sx-modifiers.ios.ts +29 -0
- package/src/use-sx.ts +34 -0
- package/src/value-key.ts +18 -0
- package/src/view-sx-to-style.ts +180 -0
- package/src/view-sx.types.ts +87 -0
- 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
|
+
}
|