diff --git a/apps/toolbox/src/app-root.xml b/apps/toolbox/src/app-root.xml
index 54e70d9760..e895c0b967 100644
--- a/apps/toolbox/src/app-root.xml
+++ b/apps/toolbox/src/app-root.xml
@@ -1,2 +1,8 @@
+
+
+
diff --git a/apps/toolbox/src/main.ts b/apps/toolbox/src/main.ts
index a4c5c529a8..bdf3214810 100644
--- a/apps/toolbox/src/main.ts
+++ b/apps/toolbox/src/main.ts
@@ -1,3 +1,6 @@
-import { Application } from '@nativescript/core';
+import { Application, SplitView } from '@nativescript/core';
-Application.run({ moduleName: 'app-root' });
+// Application.run({ moduleName: 'app-root' });
+
+SplitView.SplitStyle = 'triple';
+Application.run({ moduleName: 'split-view/split-view-root' });
diff --git a/apps/toolbox/src/pages/list-page-model-sticky.ts b/apps/toolbox/src/pages/list-page-model-sticky.ts
index 3550c2f83a..cf8b3cb9c1 100644
--- a/apps/toolbox/src/pages/list-page-model-sticky.ts
+++ b/apps/toolbox/src/pages/list-page-model-sticky.ts
@@ -1,4 +1,5 @@
import { Observable, Dialogs, DialogStrings, View, EventData, SearchEventData } from '@nativescript/core';
+import { getItemCallbacks } from '../split-view/split-view-root';
type CountryListType = Array<{ title: string; items: Array<{ name: string; code: string; flag: string; isVisible?: boolean }> }>;
export class ListPageModelSticky extends Observable {
countries: CountryListType = [
@@ -1380,11 +1381,14 @@ export class ListPageModelSticky extends Observable {
}
componentsItemTap(args): void {
- Dialogs.alert({
- title: 'Want to play?',
- message: 'Nothing to see here yet. Feel free to add more examples to play around.',
- okButtonText: DialogStrings.OK,
- });
+ const letter = this.countries[args.section];
+ console.log('Tapped on category: ' + letter.title);
+ if (letter.items?.length) {
+ const country = letter.items[args.index];
+ console.log('Tapped on country: ' + country.name);
+ // used in splitview demo
+ getItemCallbacks().forEach((callback) => callback(`${country.name} was selected.`));
+ }
}
itemLoading(args: EventData): void {
diff --git a/apps/toolbox/src/pages/list-page-sticky.xml b/apps/toolbox/src/pages/list-page-sticky.xml
index 887a568e17..d7b6f0abc2 100644
--- a/apps/toolbox/src/pages/list-page-sticky.xml
+++ b/apps/toolbox/src/pages/list-page-sticky.xml
@@ -1,9 +1,6 @@
-
-
-
-
-
+
+
diff --git a/apps/toolbox/src/split-view/split-view-primary.ts b/apps/toolbox/src/split-view/split-view-primary.ts
new file mode 100644
index 0000000000..3f5748cb28
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-primary.ts
@@ -0,0 +1,23 @@
+import { Observable, EventData, Page, SplitView, ItemEventData } from '@nativescript/core';
+import { getItemCallbacks } from './split-view-root';
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new SplitViewPrimaryModel();
+}
+
+export class SplitViewPrimaryModel extends Observable {
+ items: string[] = [];
+
+ constructor() {
+ super();
+ this.items = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
+ }
+
+ onItemTap(args: ItemEventData) {
+ console.log('args.index', args.index);
+ SplitView.getInstance()?.showSecondary();
+ getItemCallbacks().forEach((callback) => callback(this.items[args.index]));
+ }
+}
diff --git a/apps/toolbox/src/split-view/split-view-primary.xml b/apps/toolbox/src/split-view/split-view-primary.xml
new file mode 100644
index 0000000000..915ff7e692
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-primary.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/toolbox/src/split-view/split-view-root.ts b/apps/toolbox/src/split-view/split-view-root.ts
new file mode 100644
index 0000000000..9cb11fce71
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-root.ts
@@ -0,0 +1,17 @@
+import { Observable, EventData, Page } from '@nativescript/core';
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new SplitViewModel();
+}
+
+export class SplitViewModel extends Observable {}
+
+let itemCallbacks: Array<(item: any) => void> = [];
+export function setItemCallbacks(changeItem: Array<(item: any) => void>) {
+ itemCallbacks.push(...changeItem);
+}
+export function getItemCallbacks() {
+ return itemCallbacks;
+}
diff --git a/apps/toolbox/src/split-view/split-view-root.xml b/apps/toolbox/src/split-view/split-view-root.xml
new file mode 100644
index 0000000000..02d1ae3da9
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-root.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/toolbox/src/split-view/split-view-secondary.ts b/apps/toolbox/src/split-view/split-view-secondary.ts
new file mode 100644
index 0000000000..cb0a624182
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-secondary.ts
@@ -0,0 +1,35 @@
+import { Observable, EventData, Page, SplitView } from '@nativescript/core';
+import { setItemCallbacks } from './split-view-root';
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new SplitViewSecondaryModel();
+}
+
+export class SplitViewSecondaryModel extends Observable {
+ selectedItem = `Select an item from Primary.`;
+ showInspectorButton = false;
+
+ constructor() {
+ super();
+ setItemCallbacks([this.changeItem.bind(this)]);
+ SplitView.getInstance().on('inspectorChange', (args: any) => {
+ console.log('inspectorChange', args.data?.showing);
+ this.showInspectorButton = !args.data?.showing;
+ this.notifyPropertyChange('showInspectorButton', this.showInspectorButton);
+ });
+ }
+ toggle() {
+ SplitView.getInstance()?.showPrimary();
+ }
+
+ toggleInspector() {
+ SplitView.getInstance()?.showInspector();
+ }
+
+ changeItem(item: any) {
+ this.selectedItem = item;
+ this.notifyPropertyChange('selectedItem', item);
+ }
+}
diff --git a/apps/toolbox/src/split-view/split-view-secondary.xml b/apps/toolbox/src/split-view/split-view-secondary.xml
new file mode 100644
index 0000000000..75328de39f
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-secondary.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/toolbox/src/split-view/split-view-supplement.ts b/apps/toolbox/src/split-view/split-view-supplement.ts
new file mode 100644
index 0000000000..658a7b8859
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-supplement.ts
@@ -0,0 +1,24 @@
+import { Observable, EventData, Page, SplitView } from '@nativescript/core';
+import { setItemCallbacks } from './split-view-root';
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new SplitViewSupplementaryModel();
+}
+
+export class SplitViewSupplementaryModel extends Observable {
+ selectedItem = `Supplementary - Select an item.`;
+ constructor() {
+ super();
+ setItemCallbacks([this.changeItem.bind(this)]);
+ }
+ toggle() {
+ SplitView.getInstance()?.showPrimary();
+ }
+
+ changeItem(item: any) {
+ this.selectedItem = item;
+ this.notifyPropertyChange('selectedItem', item);
+ }
+}
diff --git a/apps/toolbox/src/split-view/split-view-supplement.xml b/apps/toolbox/src/split-view/split-view-supplement.xml
new file mode 100644
index 0000000000..03a99b1b37
--- /dev/null
+++ b/apps/toolbox/src/split-view/split-view-supplement.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 2b0220a47c..8589fe6c73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -239,6 +239,7 @@
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -2263,6 +2264,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -2286,6 +2288,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -5620,6 +5623,7 @@
"integrity": "sha512-clqOhLHvGXelJDq0blfrPMvJ88TTMhlxKvbuj+mxpfXCcHIYlhuHeH63u99eO4wEbVtSopOG4szpABSjRXJESw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ejs": "^3.1.7",
"enquirer": "~2.3.6",
@@ -5653,6 +5657,7 @@
"integrity": "sha512-YYxPkohOjUYpbYXgDbrgbgoyA7DlxK8pDYasVYcyEGwf9M5H5KIacgkK2EdBBcIfEzFiASKbVm817tjhKT5ndQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@nx/devkit": "21.3.7",
"@nx/js": "21.3.7",
@@ -5805,6 +5810,7 @@
"integrity": "sha512-oy+WcZqfYvOzhO+cefgwYVRIBULfVQk8J8prgw9kMuFcJRgOYXkkfB1HLdkxx+OrHGDPqs7Oe0+8KS1lilnumA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/plugin-proposal-decorators": "^7.22.7",
@@ -6209,6 +6215,7 @@
"integrity": "sha512-DIMb9Ts6w0FtKIglNEkAQ22w+b/4kx97MJDdK3tU1t0o0hG64XbYZ9xyVjnENVEkSKnSInAid/dBg+pMTgwxhA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@nx/devkit": "21.3.7",
"@zkochan/js-yaml": "0.0.7",
@@ -8062,6 +8069,7 @@
"integrity": "sha512-jYWaI2WNEKz8KZL3sExd2KVL1JMma1/J7z+9iTpv0+fRN7LGMF8VTGGuHI2bug/ztpdZU1G44FG/Kk6ElXL9CQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@swc-node/core": "^1.13.3",
"@swc-node/sourcemap-support": "^0.5.1",
@@ -8119,6 +8127,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.23"
@@ -8344,6 +8353,7 @@
"integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
@@ -8779,6 +8789,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -9904,6 +9915,7 @@
"integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -9948,6 +9960,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -10107,6 +10120,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -11055,6 +11069,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -11414,6 +11429,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -13129,31 +13145,6 @@
"node": ">= 4"
}
},
- "node_modules/encoding": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
- "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "iconv-lite": "^0.6.2"
- }
- },
- "node_modules/encoding/node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -13385,6 +13376,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -13441,6 +13433,7 @@
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -15464,7 +15457,6 @@
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"license": "ISC",
- "peer": true,
"engines": {
"node": "^10 || ^12 || >= 14"
},
@@ -16463,6 +16455,7 @@
"integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "30.0.5",
"@jest/types": "30.0.5",
@@ -18629,6 +18622,7 @@
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -18691,6 +18685,7 @@
"integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"abab": "^2.0.6",
"cssstyle": "^3.0.0",
@@ -19865,6 +19860,7 @@
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -21496,6 +21492,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@napi-rs/wasm-runtime": "0.2.4",
"@yarnpkg/lockfile": "^1.1.0",
@@ -22779,6 +22776,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -22904,7 +22902,6 @@
"integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
"dev": true,
"license": "ISC",
- "peer": true,
"engines": {
"node": "^10 || ^12 || >= 14"
},
@@ -22918,7 +22915,6 @@
"integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^7.0.0",
@@ -22937,7 +22933,6 @@
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -22952,7 +22947,6 @@
"integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"postcss-selector-parser": "^7.0.0"
},
@@ -22969,7 +22963,6 @@
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -22984,7 +22977,6 @@
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"icss-utils": "^5.0.0"
},
@@ -23043,6 +23035,7 @@
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -24204,6 +24197,7 @@
"integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
@@ -24335,6 +24329,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -25576,6 +25571,7 @@
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"dev": true,
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -25930,6 +25926,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -26622,6 +26619,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -26988,6 +26986,7 @@
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -27119,6 +27118,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -27132,6 +27132,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -27447,6 +27448,7 @@
"integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -27592,6 +27594,7 @@
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^1.2.0",
diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts
index 3f5693978c..b038a88d1b 100644
--- a/packages/core/ui/frame/index.ios.ts
+++ b/packages/core/ui/frame/index.ios.ts
@@ -129,14 +129,15 @@ export class Frame extends FrameBase {
viewController[TRANSITION] = { name: NON_ANIMATED_TRANSITION };
}
+ if (!navControllerDelegate) {
+ navControllerDelegate = UINavigationControllerDelegateImpl.new();
+ }
+
+ this._ios.controller.delegate = navControllerDelegate;
+ viewController[DELEGATE] = navControllerDelegate;
+
const nativeTransition = _getNativeTransition(navigationTransition, true, this.direction);
if (!nativeTransition && navigationTransition) {
- if (!navControllerDelegate) {
- navControllerDelegate = UINavigationControllerAnimatedDelegate.new();
- }
-
- this._ios.controller.delegate = navControllerDelegate;
- viewController[DELEGATE] = navControllerDelegate;
if (navigationTransition.instance) {
this.transitionId = navigationTransition.instance.id;
const transitionState = SharedTransition.getState(this.transitionId);
@@ -148,9 +149,6 @@ export class Frame extends FrameBase {
}, this);
}
}
- } else {
- viewController[DELEGATE] = null;
- this._ios.controller.delegate = null;
}
backstackEntry[NAV_DEPTH] = navDepth;
@@ -358,6 +356,17 @@ export class Frame extends FrameBase {
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
//
}
+
+ // Emits an event whenever the UINavigationController shows a view controller.
+ // Consumers can subscribe to 'viewControllerShown'
+ // to safely interact with the visible controller/navigationItem once it exists.
+ public _onViewControllerShown(viewController: UIViewController): void {
+ this.notify({
+ eventName: 'viewControllerShown',
+ object: this,
+ viewController,
+ });
+ }
}
const transitionDelegates = new Array();
@@ -410,7 +419,7 @@ class TransitionDelegate extends NSObject {
}
@NativeClass
-class UINavigationControllerAnimatedDelegate extends NSObject implements UINavigationControllerDelegate {
+class UINavigationControllerDelegateImpl extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];
navigationControllerAnimationControllerForOperationFromViewControllerToViewController(navigationController: UINavigationControllerImpl, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
@@ -478,6 +487,16 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
return null;
}
+
+ navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationControllerImpl, viewController: UIViewController, animated: boolean): void {
+ if (Trace.isEnabled()) {
+ Trace.write('Frame.navigationController.DID_show(' + navigationController + ', ' + viewController + ', ' + animated + ');', Trace.categories.Debug);
+ }
+ const owner = navigationController.owner;
+ if (owner) {
+ owner._onViewControllerShown(viewController);
+ }
+ }
}
@NativeClass
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index d64ccaa449..ff1315e83a 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -71,6 +71,7 @@ export { SegmentedBar, SegmentedBarItem } from './segmented-bar';
export type { SelectedIndexChangedEventData } from './segmented-bar';
export { Slider } from './slider';
export type { AccessibilityDecrementEventData, AccessibilityIncrementEventData } from './slider';
+export { SplitView } from './split-view';
export { addTaggedAdditionalCSS, removeTaggedAdditionalCSS, resolveFileNameFromUrl } from './styling/style-scope';
export { Background } from './styling/background';
diff --git a/packages/core/ui/list-view/index.d.ts b/packages/core/ui/list-view/index.d.ts
index c60d1dbedf..67536ddd8a 100644
--- a/packages/core/ui/list-view/index.d.ts
+++ b/packages/core/ui/list-view/index.d.ts
@@ -233,6 +233,11 @@ export interface ItemEventData extends EventData {
*/
index: number;
+ /**
+ * When data is sectioned (any platform that supports sections), this is the section index for the item.
+ */
+ section?: number;
+
/**
* The view that is associated to the item, for which the event is raised.
*/
diff --git a/packages/core/ui/list-view/index.ios.ts b/packages/core/ui/list-view/index.ios.ts
index bb609d0e5f..3dff3a77c3 100644
--- a/packages/core/ui/list-view/index.ios.ts
+++ b/packages/core/ui/list-view/index.ios.ts
@@ -105,6 +105,7 @@ function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eve
eventName: eventName,
object: listView,
index: indexPath.row,
+ section: indexPath.section,
view: view,
ios: cell,
android: undefined,
diff --git a/packages/core/ui/split-view/index.android.ts b/packages/core/ui/split-view/index.android.ts
new file mode 100644
index 0000000000..74165742e0
--- /dev/null
+++ b/packages/core/ui/split-view/index.android.ts
@@ -0,0 +1,7 @@
+import { SplitViewBase } from './split-view-common';
+
+export { SplitBehavior, SplitRole, SplitStyle, SplitDisplayMode } from './split-view-common';
+
+export class SplitView extends SplitViewBase {
+ // Android does not have a native SplitViewController equivalent.
+}
diff --git a/packages/core/ui/split-view/index.d.ts b/packages/core/ui/split-view/index.d.ts
new file mode 100644
index 0000000000..25382fbcfa
--- /dev/null
+++ b/packages/core/ui/split-view/index.d.ts
@@ -0,0 +1,11 @@
+import { SplitViewBase } from './split-view-common';
+
+export type { SplitBehavior, SplitRole, SplitStyle, SplitDisplayMode } from './split-view-common';
+
+/**
+ * iOS UISplitViewController-backed container.
+ * On Android, acts as a simple container.
+ *
+ * @nsView SplitView
+ */
+export class SplitView extends SplitViewBase {}
diff --git a/packages/core/ui/split-view/index.ios.ts b/packages/core/ui/split-view/index.ios.ts
new file mode 100644
index 0000000000..fc46ffd639
--- /dev/null
+++ b/packages/core/ui/split-view/index.ios.ts
@@ -0,0 +1,420 @@
+import { SplitViewBase, SplitRole, displayModeProperty, splitBehaviorProperty, preferredPrimaryColumnWidthFractionProperty, preferredSupplementaryColumnWidthFractionProperty, preferredInspectorColumnWidthFractionProperty } from './split-view-common';
+import { View } from '../core/view';
+import { layout } from '../../utils';
+import { SDK_VERSION } from '../../utils/constants';
+
+@NativeClass
+class UISplitViewControllerDelegateImpl extends NSObject implements UISplitViewControllerDelegate {
+ public static ObjCProtocols = [UISplitViewControllerDelegate];
+ static ObjCExposedMethods = {
+ toggleInspector: { returns: interop.types.void, params: [] },
+ };
+ private _owner: WeakRef;
+
+ public static initWithOwner(owner: WeakRef): UISplitViewControllerDelegateImpl {
+ const d = UISplitViewControllerDelegateImpl.new();
+ d._owner = owner;
+ return d;
+ }
+
+ splitViewControllerCollapseSecondaryViewControllerOntoPrimaryViewController(splitViewController: UISplitViewController, secondaryViewController: UIViewController, primaryViewController: UIViewController): boolean {
+ const owner = this._owner.deref();
+ if (owner) {
+ // Notify the owner about the collapse action
+ owner.onSecondaryViewCollapsed(secondaryViewController, primaryViewController);
+ }
+ return true;
+ }
+
+ splitViewControllerDidCollapse(svc: UISplitViewController): void {
+ // Can be used to notify owner if needed
+ }
+
+ splitViewControllerDidExpand(svc: UISplitViewController): void {
+ // Can be used to notify owner if needed
+ }
+
+ splitViewControllerDidHideColumn(svc: UISplitViewController, column: UISplitViewControllerColumn): void {
+ // Can be used to notify owner if needed
+ }
+
+ splitViewControllerDidShowColumn(svc: UISplitViewController, column: UISplitViewControllerColumn): void {
+ // Can be used to notify owner if needed
+ }
+
+ splitViewControllerDisplayModeForExpandingToProposedDisplayMode(svc: UISplitViewController, proposedDisplayMode: UISplitViewControllerDisplayMode): UISplitViewControllerDisplayMode {
+ return UISplitViewControllerDisplayMode.TwoBesideSecondary;
+ }
+
+ splitViewControllerTopColumnForCollapsingToProposedTopColumn(svc: UISplitViewController, proposedTopColumn: UISplitViewControllerColumn): UISplitViewControllerColumn {
+ return UISplitViewControllerColumn.Secondary;
+ }
+
+ toggleInspector(): void {
+ const owner = this._owner.deref();
+ if (owner) {
+ if (owner.inspectorShowing) {
+ owner.hideInspector();
+ } else {
+ owner.showInspector();
+ }
+ }
+ }
+}
+
+export class SplitView extends SplitViewBase {
+ static instance: SplitView;
+ static getInstance(): SplitViewBase | null {
+ return SplitView.instance;
+ }
+
+ public viewController: UISplitViewController;
+ private _delegate: UISplitViewControllerDelegateImpl;
+ // Keep role -> controller
+ private _controllers = new Map();
+ private _children = new Map();
+ inspectorShowing = false;
+
+ constructor() {
+ super();
+ this.viewController = UISplitViewController.alloc().initWithStyle(this._getSplitStyle());
+ }
+
+ createNativeView() {
+ SplitView.instance = this;
+ this._delegate = UISplitViewControllerDelegateImpl.initWithOwner(new WeakRef(this));
+ this.viewController.delegate = this._delegate;
+ this.viewController.presentsWithGesture = true;
+
+ // Apply initial preferences
+ this._applyPreferences();
+
+ return this.viewController.view;
+ }
+
+ disposeNativeView(): void {
+ super.disposeNativeView();
+ this._controllers.clear();
+ this._children.clear();
+ this.viewController = null;
+ this._delegate = null;
+ }
+
+ private _getSplitStyle() {
+ switch (SplitView.SplitStyle) {
+ case 'triple':
+ return UISplitViewControllerStyle.TripleColumn;
+ default:
+ // default to double always
+ return UISplitViewControllerStyle.DoubleColumn;
+ }
+ }
+
+ // Controller-backed container: intercept native tree operations
+ public _addViewToNativeVisualTree(child: SplitViewBase, atIndex: number): boolean {
+ const role = this._resolveRoleForChild(child, atIndex);
+ const controller = this._ensureControllerForChild(child);
+ this._children.set(role, child);
+ this._controllers.set(role, controller);
+ this._syncControllers();
+ return true;
+ }
+
+ public _removeViewFromNativeVisualTree(child: View): void {
+ const role = this._findRoleByChild(child);
+ if (role) {
+ this._children.delete(role);
+ this._controllers.delete(role);
+ this._syncControllers();
+ }
+ super._removeViewFromNativeVisualTree(child);
+ }
+
+ public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ const width = layout.getMeasureSpecSize(widthMeasureSpec);
+ const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
+ const height = layout.getMeasureSpecSize(heightMeasureSpec);
+ const heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
+
+ const horizontal = this.effectivePaddingLeft + this.effectivePaddingRight + this.effectiveBorderLeftWidth + this.effectiveBorderRightWidth;
+ const vertical = this.effectivePaddingTop + this.effectivePaddingBottom + this.effectiveBorderTopWidth + this.effectiveBorderBottomWidth;
+
+ const measuredWidth = Math.max(widthMode === layout.UNSPECIFIED ? 0 : width, this.effectiveMinWidth) + (widthMode === layout.UNSPECIFIED ? horizontal : 0);
+ const measuredHeight = Math.max(heightMode === layout.UNSPECIFIED ? 0 : height, this.effectiveMinHeight) + (heightMode === layout.UNSPECIFIED ? vertical : 0);
+
+ const widthAndState = View.resolveSizeAndState(measuredWidth, width, widthMode, 0);
+ const heightAndState = View.resolveSizeAndState(measuredHeight, height, heightMode, 0);
+ this.setMeasuredDimension(widthAndState, heightAndState);
+ }
+
+ public onRoleChanged(view: View, oldValue: SplitRole, newValue: SplitRole) {
+ // Move child mapping to new role and resync
+ const oldRole = this._findRoleByChild(view);
+ if (oldRole) {
+ const controller = this._controllers.get(oldRole);
+ this._controllers.delete(oldRole);
+ this._children.delete(oldRole);
+ if (controller) {
+ this._controllers.set(newValue, controller);
+ }
+ this._children.set(newValue, view);
+ this._syncControllers();
+ }
+ }
+
+ onSecondaryViewCollapsed(secondaryViewController: UIViewController, primaryViewController: UIViewController): void {
+ // Default implementation: do nothing.
+ // Subclasses may override to customize behavior when secondary is collapsed onto primary.
+ }
+
+ showPrimary(): void {
+ if (!this.viewController) return;
+ this.viewController.showColumn(UISplitViewControllerColumn.Primary);
+ }
+
+ hidePrimary(): void {
+ if (!this.viewController) return;
+ this.viewController.hideColumn(UISplitViewControllerColumn.Primary);
+ }
+
+ showSecondary(): void {
+ if (!this.viewController) return;
+ this.viewController.showColumn(UISplitViewControllerColumn.Secondary);
+ }
+
+ hideSecondary(): void {
+ if (!this.viewController) return;
+ this.viewController.hideColumn(UISplitViewControllerColumn.Secondary);
+ }
+
+ showSupplementary(): void {
+ if (!this.viewController) return;
+ this.viewController.showColumn(UISplitViewControllerColumn.Supplementary);
+ }
+
+ showInspector(): void {
+ if (!this.viewController) return;
+ // Guard for older OS versions by feature-detecting inspector-related API
+ if (this.viewController.preferredInspectorColumnWidthFraction !== undefined) {
+ this.viewController.showColumn(UISplitViewControllerColumn.Inspector);
+ this.notifyInspectorChange(true);
+ }
+ }
+
+ hideInspector(): void {
+ if (!this.viewController) return;
+ if (this.viewController.preferredInspectorColumnWidthFraction !== undefined) {
+ this.viewController.hideColumn(UISplitViewControllerColumn.Inspector);
+ this.notifyInspectorChange(false);
+ }
+ }
+
+ notifyInspectorChange(showing: boolean): void {
+ this.inspectorShowing = showing;
+ this.notify({
+ eventName: 'inspectorChange',
+ object: this,
+ data: { showing },
+ });
+ }
+
+ private _resolveRoleForChild(child: SplitViewBase, atIndex: number): SplitRole {
+ const explicit = SplitViewBase.getRole(child);
+ if (explicit) {
+ return explicit;
+ }
+ // Fallback by index if no explicit role set
+ return this._roleByIndex(atIndex) as SplitRole;
+ }
+
+ private _findRoleByChild(child: View): SplitRole | null {
+ for (const [role, c] of this._children.entries()) {
+ if (c === child) {
+ return role;
+ }
+ }
+ return null;
+ }
+
+ private _ensureControllerForChild(child: View): UIViewController {
+ // If child is controller-backed (Page/Frame/etc.), reuse its controller
+ const vc = (child.ios instanceof UIViewController ? (child.ios as any) : (child as any).viewController) as UIViewController | null;
+ if (vc) {
+ return vc;
+ }
+ // Fallback: basic wrapper (not expected in current usage where children are Frames/Pages)
+ const wrapper = UIViewController.new();
+ if (!wrapper.view) {
+ wrapper.view = UIView.new();
+ }
+ if (child.nativeViewProtected) {
+ wrapper.view.addSubview(child.nativeViewProtected);
+ }
+ return wrapper;
+ }
+
+ private _attachInspectorButton(): void {
+ const inspector = this._controllers.get('inspector');
+ if (!(inspector instanceof UINavigationController)) {
+ return;
+ }
+
+ const targetVC = inspector.topViewController;
+ if (!targetVC) {
+ // Subscribe to Frame event to know when the top VC is shown, then attach the button.
+ // Can only attach buttons once VC is available
+ const frameChild = this._children.get('inspector') as any;
+ if (frameChild && frameChild.on && !frameChild._inspectorVCShownHandler) {
+ frameChild._inspectorVCShownHandler = () => {
+ setTimeout(() => this._attachInspectorButton());
+ };
+ frameChild.on('viewControllerShown', frameChild._inspectorVCShownHandler.bind(this));
+ }
+ return;
+ }
+
+ // Avoid duplicates
+ if (targetVC.navigationItem.rightBarButtonItem) {
+ return;
+ }
+
+ // TODO: can provide properties to customize this
+ const cfg = UIImageSymbolConfiguration.configurationWithPointSizeWeightScale(18, UIImageSymbolWeight.Regular, UIImageSymbolScale.Medium);
+ const image = UIImage.systemImageNamedWithConfiguration('sidebar.trailing', cfg);
+ const item = UIBarButtonItem.alloc().initWithImageStyleTargetAction(image, UIBarButtonItemStyle.Plain, this._delegate, 'toggleInspector');
+ targetVC.navigationItem.rightBarButtonItem = item;
+ }
+
+ private _syncControllers(): void {
+ if (!this.viewController) {
+ return;
+ }
+ // Prefer modern API if present; otherwise fall back to setting viewControllers array
+ const primary = this._controllers.get('primary');
+ const secondary = this._controllers.get('secondary');
+ const supplementary = this._controllers.get('supplementary');
+ const inspector = this._controllers.get('inspector');
+
+ if (primary) {
+ this.viewController.setViewControllerForColumn(primary, UISplitViewControllerColumn.Primary);
+ }
+ if (secondary) {
+ this.viewController.setViewControllerForColumn(secondary, UISplitViewControllerColumn.Secondary);
+ }
+ if (supplementary) {
+ this.viewController.setViewControllerForColumn(supplementary, UISplitViewControllerColumn.Supplementary);
+ }
+ if (inspector) {
+ if (this.viewController.preferredInspectorColumnWidthFraction !== undefined) {
+ this.viewController.setViewControllerForColumn(inspector, UISplitViewControllerColumn.Inspector);
+ }
+ }
+
+ this._applyPreferences();
+ }
+
+ private _applyPreferences(): void {
+ if (!this.viewController) {
+ return;
+ }
+
+ // displayMode
+ let preferredDisplayMode = UISplitViewControllerDisplayMode.Automatic;
+ switch (this.displayMode) {
+ case 'secondaryOnly':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.SecondaryOnly;
+ break;
+ case 'oneBesideSecondary':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.OneBesideSecondary;
+ break;
+ case 'oneOverSecondary':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.OneOverSecondary;
+ break;
+ case 'twoBesideSecondary':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.TwoBesideSecondary;
+ break;
+ case 'twoOverSecondary':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.TwoOverSecondary;
+ break;
+ case 'twoDisplaceSecondary':
+ preferredDisplayMode = UISplitViewControllerDisplayMode.TwoDisplaceSecondary;
+ break;
+ }
+ this.viewController.preferredDisplayMode = preferredDisplayMode;
+
+ // splitBehavior (iOS 14+)
+ const sb = this.splitBehavior;
+ let preferredSplitBehavior = UISplitViewControllerSplitBehavior.Automatic;
+ switch (sb) {
+ case 'tile':
+ preferredSplitBehavior = UISplitViewControllerSplitBehavior.Tile;
+ break;
+ case 'overlay':
+ preferredSplitBehavior = UISplitViewControllerSplitBehavior.Overlay ?? UISplitViewControllerSplitBehavior.Automatic;
+ break;
+ case 'displace':
+ preferredSplitBehavior = UISplitViewControllerSplitBehavior.Displace ?? UISplitViewControllerSplitBehavior.Automatic;
+ break;
+ }
+ this.viewController.preferredSplitBehavior = preferredSplitBehavior;
+
+ const primary = this._controllers.get('primary');
+ const secondary = this._controllers.get('secondary');
+ const supplementary = this._controllers.get('supplementary');
+ const inspector = this._controllers.get('inspector');
+ if (secondary instanceof UINavigationController && secondary.navigationItem) {
+ // TODO: can add properties to customize this
+ secondary.navigationItem.leftBarButtonItem = this.viewController.displayModeButtonItem;
+ secondary.navigationItem.leftItemsSupplementBackButton = true;
+ }
+ if (supplementary) {
+ this.showSupplementary();
+ }
+ if (inspector) {
+ this.showInspector();
+ // Ensure the inspector column gets its toggle button as soon as the first page is shown
+ this._attachInspectorButton();
+ }
+
+ // Width fractions
+ if (typeof this.preferredPrimaryColumnWidthFraction === 'number' && !isNaN(this.preferredPrimaryColumnWidthFraction)) {
+ this.viewController.preferredPrimaryColumnWidthFraction = this.preferredPrimaryColumnWidthFraction;
+ }
+ if (SplitView.SplitStyle === 'triple') {
+ // supplementary applies in triple style
+ if (typeof this.preferredSupplementaryColumnWidthFraction === 'number' && !isNaN(this.preferredSupplementaryColumnWidthFraction)) {
+ this.viewController.preferredSupplementaryColumnWidthFraction = this.preferredSupplementaryColumnWidthFraction;
+ }
+ }
+
+ if (SDK_VERSION >= 26) {
+ // Inspector width fraction
+ const inspectorWidth = this.preferredInspectorColumnWidthFraction;
+ if (typeof inspectorWidth === 'number' && !isNaN(inspectorWidth)) {
+ this.viewController.preferredInspectorColumnWidthFraction = inspectorWidth;
+ }
+ }
+ }
+
+ [displayModeProperty.setNative](value: string) {
+ this._applyPreferences();
+ }
+
+ [splitBehaviorProperty.setNative](value: string) {
+ this._applyPreferences();
+ }
+
+ [preferredPrimaryColumnWidthFractionProperty.setNative](value: number) {
+ this._applyPreferences();
+ }
+
+ [preferredSupplementaryColumnWidthFractionProperty.setNative](value: number) {
+ this._applyPreferences();
+ }
+
+ [preferredInspectorColumnWidthFractionProperty.setNative](value: number) {
+ this._applyPreferences();
+ }
+}
diff --git a/packages/core/ui/split-view/split-view-common.ts b/packages/core/ui/split-view/split-view-common.ts
new file mode 100644
index 0000000000..6abef4f7c9
--- /dev/null
+++ b/packages/core/ui/split-view/split-view-common.ts
@@ -0,0 +1,156 @@
+import { LayoutBase } from '../layouts/layout-base';
+import { View, CSSType } from '../core/view';
+import { Property, makeParser, makeValidator } from '../core/properties';
+
+export type SplitRole = 'primary' | 'secondary' | 'supplementary' | 'inspector';
+const splitRoleConverter = makeParser(makeValidator('primary', 'secondary', 'supplementary', 'inspector'));
+
+// Note: Using 'inspector' splitRole does not (yet) require a distinct style; it's an optional trailing column.
+export type SplitStyle = 'automatic' | 'double' | 'triple';
+
+export type SplitDisplayMode = 'automatic' | 'secondaryOnly' | 'oneBesideSecondary' | 'oneOverSecondary' | 'twoBesideSecondary' | 'twoOverSecondary' | 'twoDisplaceSecondary';
+const splitDisplayModeConverter = makeParser(makeValidator('automatic', 'secondaryOnly', 'oneBesideSecondary', 'oneOverSecondary', 'twoBesideSecondary', 'twoOverSecondary', 'twoDisplaceSecondary'));
+
+export type SplitBehavior = 'automatic' | 'tile' | 'overlay' | 'displace';
+const splitBehaviorConverter = makeParser(makeValidator('automatic', 'tile', 'overlay', 'displace'));
+
+// Default child roles (helps authoring without setting splitRole on children)
+const ROLE_ORDER: SplitRole[] = ['primary', 'secondary', 'supplementary', 'inspector'];
+
+@CSSType('SplitView')
+export class SplitViewBase extends LayoutBase {
+ /**
+ * The display style for the split view controller.
+ * Must be set before bootstrapping the app.
+ */
+ static SplitStyle: SplitStyle;
+
+ static getInstance(): SplitViewBase | null {
+ // Platform-specific implementations may override
+ return null;
+ }
+
+ /** Child role (primary, secondary, supplementary, inspector) */
+ splitRole: SplitRole;
+ /** Preferred display mode */
+ displayMode: SplitDisplayMode;
+ /** Preferred split behavior (iOS 14+) */
+ splitBehavior: SplitBehavior;
+ /** Primary column width fraction (0..1) */
+ preferredPrimaryColumnWidthFraction: number;
+ /** Supplementary column width fraction (0..1, iOS 14+ triple) */
+ preferredSupplementaryColumnWidthFraction: number;
+ /** Inspector column width fraction (0..1, iOS 17+/18+ when Inspector column available) */
+ preferredInspectorColumnWidthFraction: number;
+
+ /**
+ * Get child role (primary, secondary, supplementary, inspector)
+ */
+ public static getRole(element: SplitViewBase): SplitRole {
+ return element.splitRole;
+ }
+
+ /**
+ * Set child role (primary, secondary, supplementary, inspector)
+ */
+ public static setRole(element: SplitViewBase, value: SplitRole): void {
+ element.splitRole = value;
+ }
+
+ // Called when a child's role changes; platform impls may override
+ public onRoleChanged(view: View, oldValue: SplitRole, newValue: SplitRole) {
+ this.requestLayout();
+ }
+
+ showPrimary() {
+ // Platform-specific implementations may override
+ }
+
+ hidePrimary() {
+ // Platform-specific implementations may override
+ }
+
+ showSecondary() {
+ // Platform-specific implementations may override
+ }
+
+ hideSecondary() {
+ // Platform-specific implementations may override
+ }
+
+ showSupplementary() {
+ // Platform-specific implementations may override
+ }
+
+ hideSupplementary() {
+ // Platform-specific implementations may override
+ }
+
+ showInspector() {
+ // Platform-specific implementations may override
+ }
+
+ hideInspector() {
+ // Platform-specific implementations may override
+ }
+
+ // Utility to infer a role by index when none specified
+ protected _roleByIndex(index: number): SplitRole {
+ return ROLE_ORDER[Math.max(0, Math.min(index, ROLE_ORDER.length - 1))];
+ }
+}
+
+SplitViewBase.prototype.recycleNativeView = 'auto';
+
+export const splitRoleProperty = new Property({
+ name: 'splitRole',
+ defaultValue: 'primary',
+ valueChanged: (target, oldValue, newValue) => {
+ const parent = target.parent;
+ if (parent instanceof SplitViewBase) {
+ parent.onRoleChanged(target, oldValue, newValue);
+ }
+ },
+ valueConverter: splitRoleConverter,
+});
+splitRoleProperty.register(View);
+
+export const displayModeProperty = new Property({
+ name: 'displayMode',
+ defaultValue: 'automatic',
+ affectsLayout: __APPLE__,
+ valueConverter: splitDisplayModeConverter,
+});
+displayModeProperty.register(SplitViewBase);
+
+export const splitBehaviorProperty = new Property({
+ name: 'splitBehavior',
+ defaultValue: 'automatic',
+ affectsLayout: __APPLE__,
+ valueConverter: splitBehaviorConverter,
+});
+splitBehaviorProperty.register(SplitViewBase);
+
+export const preferredPrimaryColumnWidthFractionProperty = new Property({
+ name: 'preferredPrimaryColumnWidthFraction',
+ defaultValue: 0,
+ affectsLayout: __APPLE__,
+ valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))),
+});
+preferredPrimaryColumnWidthFractionProperty.register(SplitViewBase);
+
+export const preferredSupplementaryColumnWidthFractionProperty = new Property({
+ name: 'preferredSupplementaryColumnWidthFraction',
+ defaultValue: 0,
+ affectsLayout: __APPLE__,
+ valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))),
+});
+preferredSupplementaryColumnWidthFractionProperty.register(SplitViewBase);
+
+export const preferredInspectorColumnWidthFractionProperty = new Property({
+ name: 'preferredInspectorColumnWidthFraction',
+ defaultValue: 0,
+ affectsLayout: __APPLE__,
+ valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))),
+});
+preferredInspectorColumnWidthFractionProperty.register(SplitViewBase);