@octane-xplat/smooth-corners 0.11.0 → 0.11.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "a73ede6a-87a6-4e23-b24a-5ffcf68b06a4",
3
+ "buildId": "d3b248bd-af3e-48e8-a61d-6ca4b8a2cd10",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,6 +1,7 @@
1
1
  import { applySmoothClip, applySmoothShadow } from "./apply.ios.js";
2
2
  import { applySmoothClip as applySmoothClip$1, applySmoothShadow as applySmoothShadow$1 } from "./apply.android.js";
3
- import { defineUniversalComponent, hookSlots, universalPlan, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
3
+ import { outerLayoutProps, paintedStyle } from "./layout-child.js";
4
+ import { defineUniversalComponent, hookSlots, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
4
5
  import { isIOS } from "@nativescript/core";
5
6
  //#region src/SmoothCorners.tsrx
6
7
  var _hs$ = /* @__PURE__ */ hookSlots(6);
@@ -13,7 +14,7 @@ var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
13
14
  var __octaneUniversalPlan0 = universalPlan("nativescript", {
14
15
  "kind": "host",
15
16
  "type": "gridlayout",
16
- "bindings": [["ref", 0]],
17
+ "propsSlot": 0,
17
18
  "children": [{
18
19
  "kind": "host",
19
20
  "type": "gridlayout",
@@ -60,11 +61,15 @@ var SmoothCorners = defineUniversalComponent("nativescript", function SmoothCorn
60
61
  const bindOuter = useRef(bind("outer"), _h$4);
61
62
  useEffect(() => apply(), [apply], _h$5);
62
63
  return universalValue(__octaneUniversalPlan0, [
63
- bindOuter.current,
64
+ universalProps([[
65
+ "set",
66
+ "ref",
67
+ bindOuter.current
68
+ ], ["spread", outerLayoutProps(props)]]),
64
69
  bindInner.current,
65
70
  props.id,
66
71
  props.className,
67
- props.style,
72
+ paintedStyle(props.style),
68
73
  props.children
69
74
  ]);
70
75
  }, { "module": "@nativescript-community/octane" });
@@ -0,0 +1,61 @@
1
+ //#region src/layout-child.ts
2
+ var OUTER_STYLE_KEYS = [
3
+ "width",
4
+ "height",
5
+ "margin",
6
+ "marginTop",
7
+ "marginRight",
8
+ "marginBottom",
9
+ "marginLeft"
10
+ ];
11
+ var MARGIN_KEYS = [
12
+ "margin",
13
+ "marginTop",
14
+ "marginRight",
15
+ "marginBottom",
16
+ "marginLeft"
17
+ ];
18
+ /** The inner box's `style` — everything except margins, which belong to the
19
+ * element's border box and move out to the wrapper. Width/height stay: the
20
+ * AppKit inner stack cannot stretch along its parent's main axis, so it
21
+ * must keep its own size. */
22
+ function paintedStyle(style) {
23
+ if (!style || typeof style !== "object") return style;
24
+ const next = { ...style };
25
+ for (const key of MARGIN_KEYS) delete next[key];
26
+ return next;
27
+ }
28
+ var CHILD_PROPS_ALWAYS = [
29
+ "row",
30
+ "col",
31
+ "rowSpan",
32
+ "colSpan",
33
+ "left",
34
+ "top",
35
+ "horizontalAlignment",
36
+ "verticalAlignment",
37
+ "flexGrow",
38
+ "flexShrink",
39
+ "alignSelf",
40
+ "order"
41
+ ];
42
+ var CHILD_PROPS_IF_SET = [
43
+ "dock",
44
+ "right",
45
+ "bottom"
46
+ ];
47
+ /** Parent-layout metadata — must land on the OUTER wrapper, the node the
48
+ * parent layout reads as its child. Values forward under the host's own
49
+ * attribute names. */
50
+ function outerLayoutProps(props) {
51
+ const result = {};
52
+ for (const key of CHILD_PROPS_ALWAYS) result[key] = props[key];
53
+ for (const key of CHILD_PROPS_IF_SET) if (props[key] !== void 0) result[key] = props[key];
54
+ const source = props.style && typeof props.style === "object" ? props.style : {};
55
+ const style = {};
56
+ for (const key of OUTER_STYLE_KEYS) if (source[key] !== void 0) style[key] = source[key];
57
+ result.style = Object.keys(style).length > 0 ? style : void 0;
58
+ return result;
59
+ }
60
+ //#endregion
61
+ export { outerLayoutProps, paintedStyle };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "64149c71-b12b-459c-92f1-7c1b0088c73e",
3
+ "buildId": "7a7269e8-3ddf-49fe-9789-f0e9d6622a7c",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,3 +1,4 @@
1
+ import { innerLayoutProps, outerLayoutProps, paintedStyle } from "./layout-child.web.js";
1
2
  import { smoothPathD } from "./path.js";
2
3
  import { createElement, createScopedElement, createScopedValue, hookSlots, positionalChildren, useLayoutEffect, useRef, useState } from "octane";
3
4
  //#region src/SmoothCorners.web.tsrx
@@ -24,13 +25,18 @@ var SmoothCorners = function SmoothCorners(props, __s, __extra) {
24
25
  return () => ro.disconnect();
25
26
  }, [], _hs$ + 2);
26
27
  const d = smoothPathD(size.w, size.h, props.corners);
28
+ const shadow = props.shadow;
29
+ const inner = innerLayoutProps(props).style;
30
+ const outer = outerLayoutProps(props).style;
27
31
  const box = createScopedValue(() => createScopedElement("div", {
28
32
  ref,
29
33
  id: props.id,
30
34
  className: props.className,
31
35
  style: {
32
36
  position: "relative",
33
- ...props.style,
37
+ ...paintedStyle(props.style),
38
+ ...inner,
39
+ ...shadow ? {} : outer,
34
40
  clipPath: size.w > 0 ? `path("${d}")` : void 0
35
41
  }
36
42
  }, () => positionalChildren([props.children, props.border && size.w > 0 && createScopedValue(() => createScopedElement("svg", {
@@ -50,11 +56,11 @@ var SmoothCorners = function SmoothCorners(props, __s, __extra) {
50
56
  stroke: props.border.color,
51
57
  strokeWidth: props.border.width * 2
52
58
  }))))])));
53
- const shadow = props.shadow;
54
59
  if (!shadow) return box ?? null;
55
60
  const filter = `drop-shadow(${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 0}px ${shadow.blur ?? 0}px ${shadow.color ?? "rgba(0,0,0,0.35)"})`;
56
61
  return createScopedValue(() => createScopedElement("div", { style: {
57
62
  display: "grid",
63
+ ...outer,
58
64
  filter
59
65
  } }, () => box));
60
66
  };
@@ -0,0 +1,94 @@
1
+ //#region src/layout-child.web.ts
2
+ var FLEX_JUSTIFY = {
3
+ start: "flex-start",
4
+ end: "flex-end"
5
+ };
6
+ var FLEX_ALIGN = {
7
+ start: "flex-start",
8
+ end: "flex-end"
9
+ };
10
+ var JUSTIFY_SELF = {
11
+ left: "start",
12
+ center: "center",
13
+ middle: "center",
14
+ right: "end",
15
+ stretch: "stretch"
16
+ };
17
+ var ALIGN_SELF = {
18
+ top: "start",
19
+ center: "center",
20
+ middle: "center",
21
+ bottom: "end",
22
+ stretch: "stretch"
23
+ };
24
+ var OUTER_STYLE_KEYS = [
25
+ "width",
26
+ "height",
27
+ "margin",
28
+ "marginTop",
29
+ "marginRight",
30
+ "marginBottom",
31
+ "marginLeft"
32
+ ];
33
+ var MARGIN_KEYS = [
34
+ "margin",
35
+ "marginTop",
36
+ "marginRight",
37
+ "marginBottom",
38
+ "marginLeft"
39
+ ];
40
+ /** The inner box's `style` — everything except margins, which belong to the
41
+ * element's border box and move out to the wrapper. */
42
+ function paintedStyle(style) {
43
+ if (!style || typeof style !== "object") return style;
44
+ const next = { ...style };
45
+ for (const key of MARGIN_KEYS) delete next[key];
46
+ return next;
47
+ }
48
+ /** Parent-layout metadata folded into CSS on the outermost element — the
49
+ * shadow wrapper when present, the clipped box otherwise. Native's `dock`
50
+ * has no CSS equivalent and is ignored on web. Grid indices are zero-based
51
+ * in the shared API, matching NativeScript. */
52
+ function outerLayoutProps(props) {
53
+ const style = {};
54
+ if (props.row !== void 0) style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`;
55
+ else if (props.rowSpan !== void 0) style.gridRow = `span ${props.rowSpan}`;
56
+ if (props.col !== void 0) style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`;
57
+ else if (props.colSpan !== void 0) style.gridColumn = `span ${props.colSpan}`;
58
+ for (const key of [
59
+ "left",
60
+ "top",
61
+ "right",
62
+ "bottom"
63
+ ]) if (props[key] !== void 0) style[key] = props[key];
64
+ if (props.flexGrow !== void 0) style.flexGrow = props.flexGrow;
65
+ if (props.flexShrink !== void 0) style.flexShrink = props.flexShrink;
66
+ if (props.alignSelf !== void 0) style.alignSelf = props.alignSelf;
67
+ if (props.order !== void 0) style.order = props.order;
68
+ if (props.horizontalAlignment !== void 0) style.justifySelf = JUSTIFY_SELF[props.horizontalAlignment] ?? props.horizontalAlignment;
69
+ if (props.verticalAlignment !== void 0) style.alignSelf = ALIGN_SELF[props.verticalAlignment] ?? props.verticalAlignment;
70
+ if (props.style && typeof props.style === "object") {
71
+ for (const key of OUTER_STYLE_KEYS) if (props.style[key] !== void 0) style[key] = props.style[key];
72
+ }
73
+ return { style };
74
+ }
75
+ /** Flex-container props folded into the inner box's style. Emits
76
+ * `display: flex` only when a flex prop is present so a bare SmoothCorners
77
+ * keeps normal block flow; the default direction is column to match the
78
+ * renderers' stack default. */
79
+ function innerLayoutProps(props) {
80
+ if (!(props.flexDirection !== void 0 || props.justifyContent !== void 0 || props.alignItems !== void 0 || props.flexWrap !== void 0 || props.gap !== void 0 || props.rowGap !== void 0 || props.columnGap !== void 0)) return { style: {} };
81
+ const style = {
82
+ display: "flex",
83
+ flexDirection: props.flexDirection ?? "column"
84
+ };
85
+ if (props.justifyContent !== void 0) style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent;
86
+ if (props.alignItems !== void 0) style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems;
87
+ if (props.flexWrap !== void 0) style.flexWrap = props.flexWrap === true ? "wrap" : props.flexWrap === false ? "nowrap" : props.flexWrap;
88
+ if (props.gap !== void 0) style.gap = props.gap;
89
+ if (props.rowGap !== void 0) style.rowGap = props.rowGap;
90
+ if (props.columnGap !== void 0) style.columnGap = props.columnGap;
91
+ return { style };
92
+ }
93
+ //#endregion
94
+ export { innerLayoutProps, outerLayoutProps, paintedStyle };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/smooth-corners",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Exact-match smooth corners for Octane xplat — one generated curve (Rosenfeld continuous, Figma squircle, superellipse, clothoid) clipped natively on web, iOS, Android, and macOS",
5
5
  "keywords": [
6
6
  "clip-path",
@@ -43,7 +43,7 @@
43
43
  "devDependencies": {
44
44
  "@nativescript-community/octane": "0.2.4",
45
45
  "@nativescript/core": "9.1.2",
46
- "@octane-xplat/macos-renderer": "0.11.0",
46
+ "@octane-xplat/macos-renderer": "0.11.1",
47
47
  "@octanejs/vite-plugin": "0.1.61",
48
48
  "@tsrx/typescript-plugin": "0.4.11",
49
49
  "octane": "0.6.3",
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @octane-xplat/macos-renderer */
2
2
  import { useCallback, useRef } from 'octane';
3
+ import { innerLayoutProps, outerLayoutProps, paintedStyle } from './layout-child';
3
4
  import { flipCommandsY, generateCommands } from './path';
4
5
  import type { PathCommand } from './path';
5
6
  import type { SmoothCornersProps } from './props';
@@ -145,7 +146,10 @@ function applyOuter(view: any, w: number, h: number, props: SmoothCornersProps):
145
146
  }
146
147
 
147
148
  /** SmoothCorners on the AppKit renderer — NSBezierPath → CAShapeLayer mask
148
- * on the inner box; the outer view carries the drop shadow. */
149
+ * on the inner box; the outer view carries the drop shadow. Parent layouts
150
+ * read positioning metadata (left/top/row/col/…) off the outer wrapper's
151
+ * props; flex-container props (flexDirection/gap/…) land on the inner
152
+ * stack that owns the children. */
149
153
  export function SmoothCorners(props: SmoothCornersProps) @{
150
154
  const state = useRef<{ props: SmoothCornersProps }>({ props });
151
155
  state.current.props = props;
@@ -163,12 +167,13 @@ export function SmoothCorners(props: SmoothCornersProps) @{
163
167
  );
164
168
 
165
169
  return (
166
- <flexboxlayout onLayoutChanged={outerLayout}>
170
+ <flexboxlayout {...outerLayoutProps(props)} onLayoutChanged={outerLayout}>
167
171
  <flexboxlayout
172
+ {...innerLayoutProps(props)}
168
173
  onLayoutChanged={innerLayout}
169
174
  id={props.id}
170
175
  className={props.className}
171
- style={props.style}
176
+ style={paintedStyle(props.style)}
172
177
  >
173
178
  {props.children}
174
179
  </flexboxlayout>
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from 'octane';
3
3
  import { isIOS } from '@nativescript/core';
4
4
  import { applySmoothClip as applyIosClip, applySmoothShadow as applyIosShadow } from './apply.ios';
5
5
  import { applySmoothClip as applyAndroidClip, applySmoothShadow as applyAndroidShadow } from './apply.android';
6
+ import { outerLayoutProps, paintedStyle } from './layout-child';
6
7
  import type { SmoothCornersProps } from './props';
7
8
 
8
9
  const applyClip = isIOS ? applyIosClip : applyAndroidClip;
@@ -12,7 +13,10 @@ const applyShadow = isIOS ? applyIosShadow : applyAndroidShadow;
12
13
  * Two nested gridlayouts: the inner carries the clip (layer mask on iOS,
13
14
  * drawable+outline on Android); the outer carries the iOS drop shadow
14
15
  * (a masked layer can't show its own). Radius comes from `corners`, not
15
- * `border-radius`. */
16
+ * `border-radius`. Parent-layout metadata lands on the outer grid — the
17
+ * node the parent reads as its child. The flex-container props are
18
+ * web/macOS-only here: the inner host is a gridlayout, which has no flex
19
+ * vocabulary. */
16
20
  export function SmoothCorners(props: SmoothCornersProps) @{
17
21
  const state = useRef<{ inner: any; outer: any; props: SmoothCornersProps }>({
18
22
  inner: null,
@@ -53,12 +57,12 @@ export function SmoothCorners(props: SmoothCornersProps) @{
53
57
  useEffect(() => apply());
54
58
 
55
59
  return (
56
- <gridlayout ref={bindOuter.current}>
60
+ <gridlayout ref={bindOuter.current} {...outerLayoutProps(props)}>
57
61
  <gridlayout
58
62
  ref={bindInner.current}
59
63
  id={props.id}
60
64
  className={props.className}
61
- style={props.style}
65
+ style={paintedStyle(props.style)}
62
66
  >
63
67
  {props.children}
64
68
  </gridlayout>
@@ -1,4 +1,5 @@
1
1
  import { useLayoutEffect, useRef, useState } from 'octane';
2
+ import { innerLayoutProps, outerLayoutProps, paintedStyle } from './layout-child.web';
2
3
  import { smoothPathD } from './path';
3
4
  import type { SmoothCornersProps } from './props';
4
5
 
@@ -26,6 +27,12 @@ export function SmoothCorners(props: SmoothCornersProps) @{
26
27
  }, []);
27
28
 
28
29
  const d = smoothPathD(size.w, size.h, props.corners);
30
+ const shadow = props.shadow;
31
+ // Flex props style the clipped box; parent-layout metadata styles the
32
+ // outermost element — the shadow wrapper when present, the box itself
33
+ // otherwise.
34
+ const inner = innerLayoutProps(props).style;
35
+ const outer = outerLayoutProps(props).style;
29
36
 
30
37
  const box = (
31
38
  <div
@@ -34,7 +41,9 @@ export function SmoothCorners(props: SmoothCornersProps) @{
34
41
  className={props.className}
35
42
  style={{
36
43
  position: 'relative',
37
- ...props.style,
44
+ ...paintedStyle(props.style),
45
+ ...inner,
46
+ ...(shadow ? {} : outer),
38
47
  clipPath: size.w > 0 ? `path("${d}")` : undefined,
39
48
  }}
40
49
  >
@@ -64,8 +73,7 @@ export function SmoothCorners(props: SmoothCornersProps) @{
64
73
  </div>
65
74
  );
66
75
 
67
- const shadow = props.shadow;
68
76
  if (!shadow) return box;
69
77
  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>;
78
+ return <div style={{ display: 'grid', ...outer, filter }}>{box}</div>;
71
79
  }
@@ -0,0 +1,137 @@
1
+ import type { FlexContainerProps, LayoutChildProps, SmoothCornersProps } from './props'
2
+
3
+ // start/end shorthand → flex-start/flex-end (NativeScript's value names; the
4
+ // AppKit renderer accepts both forms).
5
+ const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
6
+ const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
7
+
8
+ // The element's box geometry — parent layouts read the child's own style for
9
+ // size and margins, so these keys must land on the outer wrapper. The rest
10
+ // of `style` stays on the inner box (it is the clipped, painted surface).
11
+ const OUTER_STYLE_KEYS = [
12
+ 'width',
13
+ 'height',
14
+ 'margin',
15
+ 'marginTop',
16
+ 'marginRight',
17
+ 'marginBottom',
18
+ 'marginLeft',
19
+ ] as const
20
+
21
+ const MARGIN_KEYS = [
22
+ 'margin',
23
+ 'marginTop',
24
+ 'marginRight',
25
+ 'marginBottom',
26
+ 'marginLeft',
27
+ ] as const
28
+
29
+ /** The inner box's `style` — everything except margins, which belong to the
30
+ * element's border box and move out to the wrapper. Width/height stay: the
31
+ * AppKit inner stack cannot stretch along its parent's main axis, so it
32
+ * must keep its own size. */
33
+ export function paintedStyle(style: any) {
34
+ if (!style || typeof style !== 'object') {
35
+ return style
36
+ }
37
+
38
+ const next = { ...style }
39
+ for (const key of MARGIN_KEYS) {
40
+ delete next[key]
41
+ }
42
+
43
+ return next
44
+ }
45
+
46
+ // Whitelisted by the AppKit flexboxlayout prop handler — safe to emit even
47
+ // as `undefined`, which is how removals propagate: the hosts merge update
48
+ // bags over the node's recorded props, so a vanished key would keep its
49
+ // stale value and parent layouts would never see the removal.
50
+ const CHILD_PROPS_ALWAYS = [
51
+ 'row',
52
+ 'col',
53
+ 'rowSpan',
54
+ 'colSpan',
55
+ 'left',
56
+ 'top',
57
+ 'horizontalAlignment',
58
+ 'verticalAlignment',
59
+ 'flexGrow',
60
+ 'flexShrink',
61
+ 'alignSelf',
62
+ 'order',
63
+ ] as const
64
+
65
+ // Not whitelisted on the AppKit host (the prop-warning gap, GH#9/#10) —
66
+ // emitted only when set so a bare SmoothCorners does not warn every mount.
67
+ const CHILD_PROPS_IF_SET = ['dock', 'right', 'bottom'] as const
68
+
69
+ /** Parent-layout metadata — must land on the OUTER wrapper, the node the
70
+ * parent layout reads as its child. Values forward under the host's own
71
+ * attribute names. */
72
+ export function outerLayoutProps(props: LayoutChildProps & { style?: any }) {
73
+ const result: Record<string, any> = {}
74
+ for (const key of CHILD_PROPS_ALWAYS) {
75
+ result[key] = props[key]
76
+ }
77
+
78
+ for (const key of CHILD_PROPS_IF_SET) {
79
+ if (props[key] !== undefined) {
80
+ result[key] = props[key]
81
+ }
82
+ }
83
+
84
+ const source = props.style && typeof props.style === 'object' ? props.style : {}
85
+ const style: Record<string, any> = {}
86
+ for (const key of OUTER_STYLE_KEYS) {
87
+ if (source[key] !== undefined) {
88
+ style[key] = source[key]
89
+ }
90
+ }
91
+
92
+ // Explicit undefined clears a previously mirrored geometry bag on update.
93
+ result.style = Object.keys(style).length > 0 ? style : undefined
94
+ return result
95
+ }
96
+
97
+ /** Flex-container props — land on the INNER host that owns the children.
98
+ * Used by the macOS leaf (inner flexboxlayout); the native leaf's inner
99
+ * gridlayout has no flex vocabulary and does not consume these. */
100
+ export function innerLayoutProps(
101
+ props: FlexContainerProps & Pick<SmoothCornersProps, 'flexDirection'>,
102
+ ) {
103
+ const result: Record<string, any> = {}
104
+ // Explicit undefined so removals propagate through the merge-style update.
105
+ result.flexDirection = props.flexDirection
106
+ result.justifyContent =
107
+ props.justifyContent === undefined
108
+ ? undefined
109
+ : (FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent)
110
+
111
+ result.alignItems =
112
+ props.alignItems === undefined
113
+ ? undefined
114
+ : (FLEX_ALIGN[props.alignItems] ?? props.alignItems)
115
+
116
+ result.flexWrap =
117
+ props.flexWrap === undefined
118
+ ? undefined
119
+ : props.flexWrap === true
120
+ ? 'wrap'
121
+ : props.flexWrap === false
122
+ ? 'nowrap'
123
+ : props.flexWrap
124
+
125
+ result.gap = props.gap
126
+ // rowGap/columnGap have no AppKit stack equivalent and are not whitelisted
127
+ // — emit only when set, matching the child-prop treatment above.
128
+ if (props.rowGap !== undefined) {
129
+ result.rowGap = props.rowGap
130
+ }
131
+
132
+ if (props.columnGap !== undefined) {
133
+ result.columnGap = props.columnGap
134
+ }
135
+
136
+ return result
137
+ }
@@ -0,0 +1,171 @@
1
+ import type { FlexContainerProps, LayoutChildProps, SmoothCornersProps } from './props'
2
+
3
+ // start/end shorthand → flex-start/flex-end (valid in both engines).
4
+ const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
5
+ const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
6
+
7
+ // Grid-cell alignment vocabulary → CSS self-alignment.
8
+ const JUSTIFY_SELF: Record<string, string> = {
9
+ left: 'start',
10
+ center: 'center',
11
+ middle: 'center',
12
+ right: 'end',
13
+ stretch: 'stretch',
14
+ }
15
+
16
+ const ALIGN_SELF: Record<string, string> = {
17
+ top: 'start',
18
+ center: 'center',
19
+ middle: 'center',
20
+ bottom: 'end',
21
+ stretch: 'stretch',
22
+ }
23
+
24
+ // The element's box geometry — belongs on the outermost element so parent
25
+ // layouts size the whole component, not the clipped box inside it.
26
+ const OUTER_STYLE_KEYS = [
27
+ 'width',
28
+ 'height',
29
+ 'margin',
30
+ 'marginTop',
31
+ 'marginRight',
32
+ 'marginBottom',
33
+ 'marginLeft',
34
+ ] as const
35
+
36
+ const MARGIN_KEYS = [
37
+ 'margin',
38
+ 'marginTop',
39
+ 'marginRight',
40
+ 'marginBottom',
41
+ 'marginLeft',
42
+ ] as const
43
+
44
+ /** The inner box's `style` — everything except margins, which belong to the
45
+ * element's border box and move out to the wrapper. */
46
+ export function paintedStyle(style: any) {
47
+ if (!style || typeof style !== 'object') {
48
+ return style
49
+ }
50
+
51
+ const next = { ...style }
52
+ for (const key of MARGIN_KEYS) {
53
+ delete next[key]
54
+ }
55
+
56
+ return next
57
+ }
58
+
59
+ /** Parent-layout metadata folded into CSS on the outermost element — the
60
+ * shadow wrapper when present, the clipped box otherwise. Native's `dock`
61
+ * has no CSS equivalent and is ignored on web. Grid indices are zero-based
62
+ * in the shared API, matching NativeScript. */
63
+ export function outerLayoutProps(props: LayoutChildProps & { style?: any }) {
64
+ const style: Record<string, any> = {}
65
+ if (props.row !== undefined) {
66
+ style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`
67
+ } else if (props.rowSpan !== undefined) {
68
+ style.gridRow = `span ${props.rowSpan}`
69
+ }
70
+
71
+ if (props.col !== undefined) {
72
+ style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`
73
+ } else if (props.colSpan !== undefined) {
74
+ style.gridColumn = `span ${props.colSpan}`
75
+ }
76
+
77
+ for (const key of ['left', 'top', 'right', 'bottom'] as const) {
78
+ if (props[key] !== undefined) {
79
+ style[key] = props[key]
80
+ }
81
+ }
82
+
83
+ if (props.flexGrow !== undefined) {
84
+ style.flexGrow = props.flexGrow
85
+ }
86
+
87
+ if (props.flexShrink !== undefined) {
88
+ style.flexShrink = props.flexShrink
89
+ }
90
+
91
+ if (props.alignSelf !== undefined) {
92
+ style.alignSelf = props.alignSelf
93
+ }
94
+
95
+ if (props.order !== undefined) {
96
+ style.order = props.order
97
+ }
98
+
99
+ if (props.horizontalAlignment !== undefined) {
100
+ style.justifySelf = JUSTIFY_SELF[props.horizontalAlignment] ?? props.horizontalAlignment
101
+ }
102
+
103
+ // CSS align-self is shared by flex and grid children — the grid-specific
104
+ // prop wins when both are set.
105
+ if (props.verticalAlignment !== undefined) {
106
+ style.alignSelf = ALIGN_SELF[props.verticalAlignment] ?? props.verticalAlignment
107
+ }
108
+
109
+ if (props.style && typeof props.style === 'object') {
110
+ for (const key of OUTER_STYLE_KEYS) {
111
+ if (props.style[key] !== undefined) {
112
+ style[key] = props.style[key]
113
+ }
114
+ }
115
+ }
116
+
117
+ return { style }
118
+ }
119
+
120
+ /** Flex-container props folded into the inner box's style. Emits
121
+ * `display: flex` only when a flex prop is present so a bare SmoothCorners
122
+ * keeps normal block flow; the default direction is column to match the
123
+ * renderers' stack default. */
124
+ export function innerLayoutProps(
125
+ props: FlexContainerProps & Pick<SmoothCornersProps, 'flexDirection'>,
126
+ ) {
127
+ const hasFlex =
128
+ props.flexDirection !== undefined ||
129
+ props.justifyContent !== undefined ||
130
+ props.alignItems !== undefined ||
131
+ props.flexWrap !== undefined ||
132
+ props.gap !== undefined ||
133
+ props.rowGap !== undefined ||
134
+ props.columnGap !== undefined
135
+
136
+ if (!hasFlex) {
137
+ return { style: {} }
138
+ }
139
+
140
+ const style: Record<string, any> = {
141
+ display: 'flex',
142
+ flexDirection: props.flexDirection ?? 'column',
143
+ }
144
+
145
+ if (props.justifyContent !== undefined) {
146
+ style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
147
+ }
148
+
149
+ if (props.alignItems !== undefined) {
150
+ style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
151
+ }
152
+
153
+ if (props.flexWrap !== undefined) {
154
+ style.flexWrap =
155
+ props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
156
+ }
157
+
158
+ if (props.gap !== undefined) {
159
+ style.gap = props.gap
160
+ }
161
+
162
+ if (props.rowGap !== undefined) {
163
+ style.rowGap = props.rowGap
164
+ }
165
+
166
+ if (props.columnGap !== undefined) {
167
+ style.columnGap = props.columnGap
168
+ }
169
+
170
+ return { style }
171
+ }
package/src/props.ts CHANGED
@@ -49,7 +49,45 @@ export interface SmoothShadowConfig {
49
49
  spread?: number
50
50
  }
51
51
 
52
- export interface SmoothCornersProps {
52
+ /** Metadata read by parent layouts — the shared LayoutChildProps contract,
53
+ * plus the edges and grid-cell alignments the AppKit absolute/grid parents
54
+ * consume (`right`/`bottom`, `horizontalAlignment`/`verticalAlignment`).
55
+ * On SmoothCorners these land on the outer wrapper — the node the parent
56
+ * layout sees as its child. Native forwards the attached attributes;
57
+ * `right`/`bottom` are honored by the AppKit absolute layout and web CSS
58
+ * only (NativeScript AbsoluteLayout reads left/top). */
59
+ export interface LayoutChildProps {
60
+ row?: number
61
+ col?: number
62
+ rowSpan?: number
63
+ colSpan?: number
64
+ dock?: 'left' | 'top' | 'right' | 'bottom'
65
+ left?: number
66
+ top?: number
67
+ right?: number
68
+ bottom?: number
69
+ horizontalAlignment?: 'left' | 'center' | 'middle' | 'right' | 'stretch'
70
+ verticalAlignment?: 'top' | 'center' | 'middle' | 'bottom' | 'stretch'
71
+ flexGrow?: number
72
+ flexShrink?: number
73
+ alignSelf?: string
74
+ order?: number
75
+ }
76
+
77
+ /** Flex-container props for the element's own children — RN vocabulary,
78
+ * applied to the inner flex host on web and the AppKit macOS leaf.
79
+ * The native leaf's inner container is a gridlayout, so these are
80
+ * web/macOS-only there. `gap` is a dip number (px on web). */
81
+ export interface FlexContainerProps {
82
+ justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'
83
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
84
+ flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse'
85
+ gap?: number | string
86
+ rowGap?: number | string
87
+ columnGap?: number | string
88
+ }
89
+
90
+ export interface SmoothCornersProps extends LayoutChildProps, FlexContainerProps {
53
91
  /** Radius/curve configuration — the single source of truth. `border-radius`
54
92
  * in `style`/`className` is ignored on this element on every target. */
55
93
  corners: CornerOptions
@@ -59,6 +97,9 @@ export interface SmoothCornersProps {
59
97
  * offsets/spread); iOS/macOS use `shadowPath`; web uses
60
98
  * `filter: drop-shadow` (no spread). */
61
99
  shadow?: SmoothShadowConfig
100
+ /** Main-axis direction of the inner flex host — the renderer default is
101
+ * column. Web/macOS-only like the rest of FlexContainerProps. */
102
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
62
103
  className?: any
63
104
  style?: any
64
105
  id?: string
package/types/props.d.ts CHANGED
@@ -42,7 +42,43 @@ export interface SmoothShadowConfig {
42
42
  /** Spread is web/iOS/macOS only — Android elevation cannot express it. */
43
43
  spread?: number;
44
44
  }
45
- export interface SmoothCornersProps {
45
+ /** Metadata read by parent layouts — the shared LayoutChildProps contract,
46
+ * plus the edges and grid-cell alignments the AppKit absolute/grid parents
47
+ * consume (`right`/`bottom`, `horizontalAlignment`/`verticalAlignment`).
48
+ * On SmoothCorners these land on the outer wrapper — the node the parent
49
+ * layout sees as its child. Native forwards the attached attributes;
50
+ * `right`/`bottom` are honored by the AppKit absolute layout and web CSS
51
+ * only (NativeScript AbsoluteLayout reads left/top). */
52
+ export interface LayoutChildProps {
53
+ row?: number;
54
+ col?: number;
55
+ rowSpan?: number;
56
+ colSpan?: number;
57
+ dock?: 'left' | 'top' | 'right' | 'bottom';
58
+ left?: number;
59
+ top?: number;
60
+ right?: number;
61
+ bottom?: number;
62
+ horizontalAlignment?: 'left' | 'center' | 'middle' | 'right' | 'stretch';
63
+ verticalAlignment?: 'top' | 'center' | 'middle' | 'bottom' | 'stretch';
64
+ flexGrow?: number;
65
+ flexShrink?: number;
66
+ alignSelf?: string;
67
+ order?: number;
68
+ }
69
+ /** Flex-container props for the element's own children — RN vocabulary,
70
+ * applied to the inner flex host on web and the AppKit macOS leaf.
71
+ * The native leaf's inner container is a gridlayout, so these are
72
+ * web/macOS-only there. `gap` is a dip number (px on web). */
73
+ export interface FlexContainerProps {
74
+ justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
75
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
76
+ flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse';
77
+ gap?: number | string;
78
+ rowGap?: number | string;
79
+ columnGap?: number | string;
80
+ }
81
+ export interface SmoothCornersProps extends LayoutChildProps, FlexContainerProps {
46
82
  /** Radius/curve configuration — the single source of truth. `border-radius`
47
83
  * in `style`/`className` is ignored on this element on every target. */
48
84
  corners: CornerOptions;
@@ -52,6 +88,9 @@ export interface SmoothCornersProps {
52
88
  * offsets/spread); iOS/macOS use `shadowPath`; web uses
53
89
  * `filter: drop-shadow` (no spread). */
54
90
  shadow?: SmoothShadowConfig;
91
+ /** Main-axis direction of the inner flex host — the renderer default is
92
+ * column. Web/macOS-only like the rest of FlexContainerProps. */
93
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
55
94
  className?: any;
56
95
  style?: any;
57
96
  id?: string;