@octane-xplat/smooth-corners 0.0.1 → 0.11.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/LICENSE +21 -0
- package/dist/native/index.js +2 -0
- package/dist/native/node_modules/.pnpm/@lisse_core@0.7.2/node_modules/@lisse/core/dist/chunk-BL6ZAFLB.js +477 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/native/packages/smooth-corners/src/SmoothCorners.tsrx.js +72 -0
- package/dist/native/packages/smooth-corners/src/apply.android.js +95 -0
- package/dist/native/packages/smooth-corners/src/apply.ios.js +92 -0
- package/dist/native/packages/smooth-corners/src/path.js +424 -0
- package/dist/web/index.js +2 -0
- package/dist/web/node_modules/.pnpm/@lisse_core@0.7.2/node_modules/@lisse/core/dist/chunk-BL6ZAFLB.js +477 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/packages/smooth-corners/src/SmoothCorners.web.tsrx.js +62 -0
- package/dist/web/packages/smooth-corners/src/path.js +453 -0
- package/package.json +67 -4
- package/src/SmoothCorners.macos.tsrx +177 -0
- package/src/SmoothCorners.tsrx +67 -0
- package/src/SmoothCorners.web.tsrx +71 -0
- package/src/apply.android.ts +136 -0
- package/src/apply.ios.ts +136 -0
- package/src/index.macos.ts +10 -0
- package/src/index.ts +13 -0
- package/src/index.web.ts +10 -0
- package/src/path.ts +627 -0
- package/src/props.ts +66 -0
- package/types/index.d.ts +17 -0
- package/types/props.d.ts +59 -0
- package/README.md +0 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'octane';
|
|
3
|
+
import { isIOS } from '@nativescript/core';
|
|
4
|
+
import { applySmoothClip as applyIosClip, applySmoothShadow as applyIosShadow } from './apply.ios';
|
|
5
|
+
import { applySmoothClip as applyAndroidClip, applySmoothShadow as applyAndroidShadow } from './apply.android';
|
|
6
|
+
import type { SmoothCornersProps } from './props';
|
|
7
|
+
|
|
8
|
+
const applyClip = isIOS ? applyIosClip : applyAndroidClip;
|
|
9
|
+
const applyShadow = isIOS ? applyIosShadow : applyAndroidShadow;
|
|
10
|
+
|
|
11
|
+
/** SmoothCorners — one generated corner curve, clipped natively.
|
|
12
|
+
* Two nested gridlayouts: the inner carries the clip (layer mask on iOS,
|
|
13
|
+
* drawable+outline on Android); the outer carries the iOS drop shadow
|
|
14
|
+
* (a masked layer can't show its own). Radius comes from `corners`, not
|
|
15
|
+
* `border-radius`. */
|
|
16
|
+
export function SmoothCorners(props: SmoothCornersProps) @{
|
|
17
|
+
const state = useRef<{ inner: any; outer: any; props: SmoothCornersProps }>({
|
|
18
|
+
inner: null,
|
|
19
|
+
outer: null,
|
|
20
|
+
props,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
state.current.props = props;
|
|
24
|
+
|
|
25
|
+
const apply = useCallback(() => {
|
|
26
|
+
const s = state.current;
|
|
27
|
+
if (s.inner) applyClip(s.inner, s.props);
|
|
28
|
+
if (s.outer) applyShadow(s.outer, s.props);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const bind = useCallback(
|
|
32
|
+
(which: 'inner' | 'outer') => (view: any) => {
|
|
33
|
+
const s = state.current;
|
|
34
|
+
const prev = s[which];
|
|
35
|
+
if (prev && prev !== view) {
|
|
36
|
+
prev.off?.('loaded', apply);
|
|
37
|
+
prev.off?.('layoutChanged', apply);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
s[which] = view;
|
|
41
|
+
if (!view) return;
|
|
42
|
+
view.on?.('loaded', apply);
|
|
43
|
+
view.on?.('layoutChanged', apply);
|
|
44
|
+
apply();
|
|
45
|
+
},
|
|
46
|
+
[apply],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const bindInner = useRef(bind('inner'));
|
|
50
|
+
const bindOuter = useRef(bind('outer'));
|
|
51
|
+
|
|
52
|
+
// Prop changes without a layout event still need a re-clip.
|
|
53
|
+
useEffect(() => apply());
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<gridlayout ref={bindOuter.current}>
|
|
57
|
+
<gridlayout
|
|
58
|
+
ref={bindInner.current}
|
|
59
|
+
id={props.id}
|
|
60
|
+
className={props.className}
|
|
61
|
+
style={props.style}
|
|
62
|
+
>
|
|
63
|
+
{props.children}
|
|
64
|
+
</gridlayout>
|
|
65
|
+
</gridlayout>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'octane';
|
|
2
|
+
import { smoothPathD } from './path';
|
|
3
|
+
import type { SmoothCornersProps } from './props';
|
|
4
|
+
|
|
5
|
+
/** SmoothCorners — `clip-path: path(d)` on the inner box + an SVG stroke
|
|
6
|
+
* overlay for `border`. `shadow` wraps the box in a `drop-shadow` filter so
|
|
7
|
+
* the shadow follows the clip (spread is not expressible — ignored). */
|
|
8
|
+
export function SmoothCorners(props: SmoothCornersProps) @{
|
|
9
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
10
|
+
const [size, setSize] = useState({ w: 0, h: 0 });
|
|
11
|
+
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!el) return;
|
|
15
|
+
const update = () => {
|
|
16
|
+
const r = el.getBoundingClientRect();
|
|
17
|
+
setSize((prev) =>
|
|
18
|
+
prev.w === r.width && prev.h === r.height ? prev : { w: r.width, h: r.height },
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
update();
|
|
23
|
+
const ro = new ResizeObserver(update);
|
|
24
|
+
ro.observe(el);
|
|
25
|
+
return () => ro.disconnect();
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const d = smoothPathD(size.w, size.h, props.corners);
|
|
29
|
+
|
|
30
|
+
const box = (
|
|
31
|
+
<div
|
|
32
|
+
ref={ref}
|
|
33
|
+
id={props.id}
|
|
34
|
+
className={props.className}
|
|
35
|
+
style={{
|
|
36
|
+
position: 'relative',
|
|
37
|
+
...props.style,
|
|
38
|
+
clipPath: size.w > 0 ? `path("${d}")` : undefined,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{props.children}
|
|
42
|
+
{props.border && size.w > 0 && (
|
|
43
|
+
<svg
|
|
44
|
+
style={{
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
inset: 0,
|
|
47
|
+
width: '100%',
|
|
48
|
+
height: '100%',
|
|
49
|
+
pointerEvents: 'none',
|
|
50
|
+
overflow: 'visible',
|
|
51
|
+
}}
|
|
52
|
+
viewBox={`0 0 ${size.w} ${size.h}`}
|
|
53
|
+
preserveAspectRatio="none"
|
|
54
|
+
>
|
|
55
|
+
<path
|
|
56
|
+
d={d}
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke={props.border.color}
|
|
59
|
+
// Stroke is centered on the edge — the clip removes the outer half.
|
|
60
|
+
strokeWidth={props.border.width * 2}
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const shadow = props.shadow;
|
|
68
|
+
if (!shadow) return box;
|
|
69
|
+
const filter = `drop-shadow(${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 0}px ${shadow.blur ?? 0}px ${shadow.color ?? 'rgba(0,0,0,0.35)'})`;
|
|
70
|
+
return <div style={{ display: 'grid', filter }}>{box}</div>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Android smooth-corners apply — custom Drawable paints background +
|
|
2
|
+
* border along the generated path; a ViewOutlineProvider clips children
|
|
3
|
+
* (`setPath` API 30+, `setConvexPath` 21–29 for convex curves); `shadow`
|
|
4
|
+
* maps to `elevation`, which follows the outline. The `android` global is
|
|
5
|
+
* touched only inside functions so the module is import-safe on iOS. */
|
|
6
|
+
import { Color, Utils } from '@nativescript/core'
|
|
7
|
+
import { generateCommands } from './path'
|
|
8
|
+
import type { PathCommand } from './path'
|
|
9
|
+
import type { SmoothCornersProps } from './props'
|
|
10
|
+
|
|
11
|
+
declare const android: any
|
|
12
|
+
|
|
13
|
+
const PATH_KEY = '__smoothPath'
|
|
14
|
+
|
|
15
|
+
function toAndroidPath(cmds: PathCommand[]): any {
|
|
16
|
+
const p = new android.graphics.Path()
|
|
17
|
+
for (const c of cmds) {
|
|
18
|
+
switch (c.c) {
|
|
19
|
+
case 'M':
|
|
20
|
+
p.moveTo(c.x, c.y)
|
|
21
|
+
break
|
|
22
|
+
case 'L':
|
|
23
|
+
p.lineTo(c.x, c.y)
|
|
24
|
+
break
|
|
25
|
+
case 'C':
|
|
26
|
+
p.cubicTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y)
|
|
27
|
+
break
|
|
28
|
+
case 'A': {
|
|
29
|
+
// Canvas is y-down like SVG — degrees map directly.
|
|
30
|
+
const rect = new android.graphics.RectF(c.cx - c.r, c.cy - c.r, c.cx + c.r, c.cy + c.r)
|
|
31
|
+
p.arcTo(rect, c.startDeg, c.sweepDeg, false)
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
case 'Z':
|
|
35
|
+
p.close()
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return p
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeDrawable(state: { view: any; props: SmoothCornersProps; path: any }): any {
|
|
44
|
+
return (android.graphics.drawable.Drawable as any).extend({
|
|
45
|
+
draw(canvas: any) {
|
|
46
|
+
const path = state.path
|
|
47
|
+
if (!path) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bg = state.view?.backgroundColor ?? state.view?.style?.backgroundColor
|
|
52
|
+
const border = state.props.border
|
|
53
|
+
if (bg) {
|
|
54
|
+
const paint = new android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
|
|
55
|
+
paint.setStyle(android.graphics.Paint.Style.FILL)
|
|
56
|
+
paint.setColor(new Color(bg).android)
|
|
57
|
+
canvas.drawPath(path, paint)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (border && border.width > 0 && border.color) {
|
|
61
|
+
const paint = new android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
|
|
62
|
+
paint.setStyle(android.graphics.Paint.Style.STROKE)
|
|
63
|
+
// Centered on the edge; the outline clip removes the outer half.
|
|
64
|
+
paint.setStrokeWidth(Utils.layout.toDevicePixels(border.width) * 2)
|
|
65
|
+
paint.setColor(new Color(border.color).android)
|
|
66
|
+
canvas.drawPath(path, paint)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
getOpacity() {
|
|
70
|
+
return android.graphics.PixelFormat.TRANSLUCENT
|
|
71
|
+
},
|
|
72
|
+
setAlpha(_a: number) {},
|
|
73
|
+
setColorFilter(_f: any) {},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Clip the inner box: outline provider + background/border drawable. */
|
|
78
|
+
export function applySmoothClip(view: any, props: SmoothCornersProps): void {
|
|
79
|
+
const s = view?.[PATH_KEY] ?? (view[PATH_KEY] = { view, props, path: null, drawable: null })
|
|
80
|
+
s.props = props
|
|
81
|
+
const v = view?.android as any
|
|
82
|
+
const size = view?.getActualSize?.()
|
|
83
|
+
if (!v || !size || size.width <= 0 || size.height <= 0) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
s.path = toAndroidPath(
|
|
88
|
+
generateCommands(
|
|
89
|
+
Utils.layout.toDevicePixels(size.width),
|
|
90
|
+
Utils.layout.toDevicePixels(size.height),
|
|
91
|
+
props.corners,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const path = s.path
|
|
96
|
+
v.setOutlineProvider(
|
|
97
|
+
new ((android.view.ViewOutlineProvider as any).extend({
|
|
98
|
+
getOutline(target: any, outline: any) {
|
|
99
|
+
if (!path) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (android.os.Build.VERSION.SDK_INT >= 30) {
|
|
104
|
+
outline.setPath(path)
|
|
105
|
+
} else {
|
|
106
|
+
// Concave paths throw — degrade to the rect; the drawable
|
|
107
|
+
// still draws the correct background shape.
|
|
108
|
+
try {
|
|
109
|
+
outline.setConvexPath(path)
|
|
110
|
+
} catch {
|
|
111
|
+
outline.setRect(0, 0, target.getWidth(), target.getHeight())
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}))(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
v.setClipToOutline(true)
|
|
119
|
+
|
|
120
|
+
if (!s.drawable) {
|
|
121
|
+
s.drawable = makeDrawable(s)
|
|
122
|
+
v.setBackground(s.drawable)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
s.drawable.setBounds(0, 0, v.getWidth(), v.getHeight())
|
|
126
|
+
v.invalidate()
|
|
127
|
+
|
|
128
|
+
// elevation shadows follow the outline — the curve, free.
|
|
129
|
+
if (props.shadow) {
|
|
130
|
+
v.setElevation(Utils.layout.toDevicePixels(props.shadow.blur ?? 8))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Android needs no separate shadow layer — `elevation` follows the inner
|
|
135
|
+
* view's outline directly, so the outer hook is a no-op. */
|
|
136
|
+
export function applySmoothShadow(_view: any, _props: SmoothCornersProps): void {}
|
package/src/apply.ios.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** iOS smooth-corners apply — CAShapeLayer mask on the inner view,
|
|
2
|
+
* layer.shadow* on the outer. Native globals resolve lazily inside these
|
|
3
|
+
* functions so the module is import-safe on Android bundles. */
|
|
4
|
+
import { Color } from '@nativescript/core'
|
|
5
|
+
import { generateCommands } from './path'
|
|
6
|
+
import type { PathCommand } from './path'
|
|
7
|
+
import type { SmoothCornersProps } from './props'
|
|
8
|
+
|
|
9
|
+
declare const CGPathCreateMutable: any
|
|
10
|
+
declare const CGPathMoveToPoint: any
|
|
11
|
+
declare const CGPathAddLineToPoint: any
|
|
12
|
+
declare const CGPathAddCurveToPoint: any
|
|
13
|
+
declare const CGPathAddArc: any
|
|
14
|
+
declare const CGPathCloseSubpath: any
|
|
15
|
+
declare const CAShapeLayer: any
|
|
16
|
+
|
|
17
|
+
const rad = (deg: number) => (deg * Math.PI) / 180
|
|
18
|
+
|
|
19
|
+
function toCGPath(cmds: PathCommand[]): any {
|
|
20
|
+
const p = CGPathCreateMutable()
|
|
21
|
+
for (const c of cmds) {
|
|
22
|
+
switch (c.c) {
|
|
23
|
+
case 'M':
|
|
24
|
+
CGPathMoveToPoint(p, null, c.x, c.y)
|
|
25
|
+
break
|
|
26
|
+
case 'L':
|
|
27
|
+
CGPathAddLineToPoint(p, null, c.x, c.y)
|
|
28
|
+
break
|
|
29
|
+
case 'C':
|
|
30
|
+
CGPathAddCurveToPoint(p, null, c.x1, c.y1, c.x2, c.y2, c.x, c.y)
|
|
31
|
+
break
|
|
32
|
+
case 'A':
|
|
33
|
+
// y-down convention matches CGPath on iOS: positive sweep =
|
|
34
|
+
// increasing angle. Explicit end angle; the flag picks direction.
|
|
35
|
+
CGPathAddArc(
|
|
36
|
+
p,
|
|
37
|
+
null,
|
|
38
|
+
c.cx,
|
|
39
|
+
c.cy,
|
|
40
|
+
c.r,
|
|
41
|
+
rad(c.startDeg),
|
|
42
|
+
rad(c.startDeg + c.sweepDeg),
|
|
43
|
+
c.sweepDeg < 0,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
break
|
|
47
|
+
case 'Z':
|
|
48
|
+
CGPathCloseSubpath(p)
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return p
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MASK_KEY = '__smoothMask'
|
|
57
|
+
const BORDER_KEY = '__smoothBorder'
|
|
58
|
+
|
|
59
|
+
function sizeOf(view: any): { w: number; h: number } | null {
|
|
60
|
+
const size = view?.getActualSize?.()
|
|
61
|
+
return size && size.width > 0 && size.height > 0 ? { w: size.width, h: size.height } : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clip the inner box: layer mask + optional border stroke layer. */
|
|
65
|
+
export function applySmoothClip(view: any, props: SmoothCornersProps): void {
|
|
66
|
+
const v = view?.ios
|
|
67
|
+
const size = v && sizeOf(view)
|
|
68
|
+
if (!v || !size) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const path = toCGPath(generateCommands(size.w, size.h, props.corners))
|
|
73
|
+
|
|
74
|
+
let mask = v[MASK_KEY] as any
|
|
75
|
+
if (!mask) {
|
|
76
|
+
mask = CAShapeLayer.new()
|
|
77
|
+
v[MASK_KEY] = mask
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mask.path = path
|
|
81
|
+
v.layer.mask = mask
|
|
82
|
+
|
|
83
|
+
let borderLayer = v[BORDER_KEY] as any
|
|
84
|
+
const border = props.border
|
|
85
|
+
if (border && border.width > 0 && border.color) {
|
|
86
|
+
if (!borderLayer) {
|
|
87
|
+
borderLayer = CAShapeLayer.new()
|
|
88
|
+
v[BORDER_KEY] = borderLayer
|
|
89
|
+
v.layer.addSublayer(borderLayer)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
borderLayer.frame = v.layer.bounds
|
|
93
|
+
borderLayer.path = path
|
|
94
|
+
borderLayer.fillColor = null
|
|
95
|
+
borderLayer.strokeColor = new Color(border.color).ios.CGColor
|
|
96
|
+
// Stroke centered on the edge; the mask clips the outer half.
|
|
97
|
+
borderLayer.lineWidth = border.width * 2
|
|
98
|
+
} else if (borderLayer) {
|
|
99
|
+
borderLayer.removeFromSuperlayer()
|
|
100
|
+
v[BORDER_KEY] = null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Drop shadow on the outer view (a masked layer can't show its own). */
|
|
105
|
+
export function applySmoothShadow(view: any, props: SmoothCornersProps): void {
|
|
106
|
+
const layer = view?.ios?.layer
|
|
107
|
+
if (!layer) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const shadow = props.shadow
|
|
112
|
+
if (!shadow) {
|
|
113
|
+
layer.shadowPath = null
|
|
114
|
+
layer.shadowOpacity = 0
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const size = sizeOf(view)
|
|
119
|
+
if (!size) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Spread grows the outline — regenerate larger, recenter via the offset.
|
|
124
|
+
const spread = shadow.spread ?? 0
|
|
125
|
+
const w = size.w + spread * 2
|
|
126
|
+
const h = size.h + spread * 2
|
|
127
|
+
layer.shadowPath = toCGPath(generateCommands(w, h, props.corners))
|
|
128
|
+
layer.shadowOffset = {
|
|
129
|
+
width: (shadow.offsetX ?? 0) - spread,
|
|
130
|
+
height: (shadow.offsetY ?? 0) - spread,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
layer.shadowColor = new Color(shadow.color ?? '#000000').ios.CGColor
|
|
134
|
+
layer.shadowOpacity = shadow.opacity ?? 0.35
|
|
135
|
+
layer.shadowRadius = shadow.blur ?? 0
|
|
136
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Platform barrel — extensionless specifier resolves the per-OS leaf
|
|
2
|
+
// (.ios.tsrx/.android.tsrx under native, .macos.tsrx under macos,
|
|
3
|
+
// .web.tsrx under web); same convention as @octane-xplat/ui index.shared.
|
|
4
|
+
export { SmoothCorners } from './SmoothCorners'
|
|
5
|
+
export type {
|
|
6
|
+
CornerConfig,
|
|
7
|
+
CornerCurve,
|
|
8
|
+
CornerOptions,
|
|
9
|
+
PerCornerConfig,
|
|
10
|
+
SmoothBorderConfig,
|
|
11
|
+
SmoothCornersProps,
|
|
12
|
+
SmoothShadowConfig,
|
|
13
|
+
} from './props'
|