@octane-xplat/sheet 0.0.1 → 0.9.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 ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,37 @@
1
- # @octane-xplat/sheet
1
+ # `@octane-xplat/sheet`
2
2
 
3
- Version 0.0.1 is a reservation stub so the automated release can publish this package name. It has no supported API. Use a later release for the implementation.
3
+ An install boundary for platform-specific modal bottom sheets: a SwiftUI
4
+ `.sheet` presentation on iOS, a Material 3 `ModalBottomSheet` on Android,
5
+ a fixed-position DOM panel on web, and a real `NSWindow` sheet
6
+ (`beginSheet`) on macOS.
7
+
8
+ Install the package in the app that renders a control. Import
9
+ `SwiftUIBottomSheet` from `@octane-xplat/sheet/ios`,
10
+ `MaterialBottomSheet` from `@octane-xplat/sheet/android`, `BottomSheet`
11
+ from `@octane-xplat/sheet/web`, or `AppKitSheet` from
12
+ `@octane-xplat/sheet/macos` in the matching platform-suffixed file.
13
+ There is no shared sheet API at the package root.
14
+
15
+ Sheet content is a render fn whose octane output mounts into a detached
16
+ root — the same embedding mechanism as `@octane-xplat/context-menu`'s
17
+ trigger. On iOS the registered view resolves through
18
+ `NativeScriptViewFactory`; on Android through the leaf's
19
+ `XplatViewRegistry` + `AndroidView` inside the sheet's dialog window.
20
+
21
+ - iOS: `open` (controlled `isPresented`), `onDismissed`,
22
+ `onPresentedChange`, `fitToContents` (ported size-reader →
23
+ `.presentationDetents([.height])`), `detents`
24
+ (`medium`/`large`/`fraction`/`height`), `showDragIndicator`,
25
+ `interactiveDismissDisabled`.
26
+ - Android: `open` + `onDismissed`, `skipPartiallyExpanded` (the M3 detent
27
+ model: partial vs fully expanded), `showDragHandle`,
28
+ `sheetGesturesEnabled`, `shouldDismissOnBackPress`,
29
+ `shouldDismissOnClickOutside`, and `containerColor`/`contentColor`/
30
+ `scrimColor` hex strings. Activity-owned key events are forwarded while
31
+ the sheet's dialog is open (ported from Expo).
32
+ - Web: scrim + bottom panel, scrim-click dismiss.
33
+
34
+ The native implementations are adapted from `@expo/ui` (MIT,
35
+ `packages/expo-ui` sdk-57): `ios/BottomSheetView.swift` and
36
+ `android/.../ui/ModalBottomSheetView.kt`. Attribution headers sit on the
37
+ ported files.
@@ -0,0 +1,87 @@
1
+ import { createNativeScriptRoot, defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useEffect, useRef, useState } from "@nativescript-community/octane";
2
+ import { Application, GridLayout } from "@nativescript/core";
3
+ import { ComposeDataDriver, JetpackCompose, registerJetpackCompose } from "@nativescript/jetpack-compose";
4
+ //#region src/MaterialBottomSheet.android.tsrx
5
+ var _hs$ = /* @__PURE__ */ hookSlots(5);
6
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
7
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
8
+ var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
9
+ var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
10
+ var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
11
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
12
+ "kind": "range",
13
+ "children": [{
14
+ "kind": "slot",
15
+ "slot": 0
16
+ }]
17
+ });
18
+ var __octaneUniversalPlan1 = universalPlan("nativescript", {
19
+ "kind": "host",
20
+ "type": "xplatandroidsheet",
21
+ "props": { "composeId": "xplat-material-sheet" },
22
+ "bindings": [
23
+ ["id", 0],
24
+ ["className", 1],
25
+ ["style", 2],
26
+ ["data", 3],
27
+ ["onComposeEvent", 4]
28
+ ]
29
+ });
30
+ registerElement("xplatandroidsheet", JetpackCompose);
31
+ registerJetpackCompose("xplat-material-sheet", (owner) => new ComposeDataDriver(new com.octanexplat.sheet.XplatSheetProvider(), owner));
32
+ var seq = 0;
33
+ var ContentRoot = defineUniversalComponent("nativescript", function ContentRoot(props) {
34
+ return universalValue(__octaneUniversalPlan0, [props.render()]);
35
+ }, { "module": "@nativescript-community/octane" });
36
+ var MaterialBottomSheet = defineUniversalComponent("nativescript", function MaterialBottomSheet(props) {
37
+ const contentId = useRef("", _h$0);
38
+ if (!contentId.current) contentId.current = `xplatsheet${++seq}`;
39
+ const mounted = useRef(null, _h$1);
40
+ const [ready, setReady] = useState(false, _h$2);
41
+ useEffect(() => {
42
+ const host = new GridLayout();
43
+ const context = Application.android.foregroundActivity ?? Application.android.startActivity;
44
+ const nativeHost = host;
45
+ nativeHost._setupAsRootView(context);
46
+ nativeHost.callLoaded();
47
+ const root = createNativeScriptRoot(host);
48
+ if (props.content) root.render(ContentRoot, { render: props.content });
49
+ com.octanexplat.sheet.XplatViewRegistry.register(contentId.current, host.android);
50
+ mounted.current = {
51
+ host,
52
+ root
53
+ };
54
+ setReady(true);
55
+ return () => {
56
+ com.octanexplat.sheet.XplatViewRegistry.unregister(contentId.current);
57
+ root.unmount?.();
58
+ mounted.current = null;
59
+ setReady(false);
60
+ };
61
+ }, [], _h$3);
62
+ useEffect(() => {
63
+ if (props.content) mounted.current?.root?.render(ContentRoot, { render: props.content });
64
+ }, [props.content], _h$4);
65
+ return universalValue(__octaneUniversalPlan1, [
66
+ props.id,
67
+ props.className,
68
+ props.style,
69
+ {
70
+ open: props.open ?? false ? 1 : 0,
71
+ contentId: ready && props.content ? contentId.current : "",
72
+ skipPartiallyExpanded: props.skipPartiallyExpanded ?? false ? 1 : 0,
73
+ showDragHandle: props.showDragHandle ?? true ? 1 : 0,
74
+ sheetGesturesEnabled: props.sheetGesturesEnabled ?? true ? 1 : 0,
75
+ shouldDismissOnBackPress: props.shouldDismissOnBackPress ?? true ? 1 : 0,
76
+ shouldDismissOnClickOutside: props.shouldDismissOnClickOutside ?? true ? 1 : 0,
77
+ containerColor: props.containerColor,
78
+ contentColor: props.contentColor,
79
+ scrimColor: props.scrimColor
80
+ },
81
+ (event) => {
82
+ if (event.data?.dismissed) props.onDismissed?.();
83
+ }
84
+ ]);
85
+ }, { "module": "@nativescript-community/octane" });
86
+ //#endregion
87
+ export { MaterialBottomSheet };
@@ -0,0 +1,97 @@
1
+ import { createNativeScriptRoot, defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useEffect, useRef } from "@nativescript-community/octane";
2
+ import { SwiftUI, SwiftUIManager, UIDataDriver, registerSwiftUI } from "@nativescript/swift-ui";
3
+ import { GridLayout } from "@nativescript/core";
4
+ //#region src/SwiftUIBottomSheet.ios.tsrx
5
+ var _hs$ = /* @__PURE__ */ hookSlots(3);
6
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
7
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
8
+ var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
9
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
10
+ "kind": "range",
11
+ "children": [{
12
+ "kind": "slot",
13
+ "slot": 0
14
+ }]
15
+ });
16
+ var __octaneUniversalPlan1 = universalPlan("nativescript", {
17
+ "kind": "host",
18
+ "type": "xplatiossheet",
19
+ "props": { "swiftId": "xplat-swiftui-sheet" },
20
+ "bindings": [
21
+ ["id", 0],
22
+ ["className", 1],
23
+ ["style", 2],
24
+ ["data", 3],
25
+ ["onSwiftUIEvent", 4]
26
+ ]
27
+ });
28
+ registerElement("xplatiossheet", SwiftUI);
29
+ registerSwiftUI("xplat-swiftui-sheet", (owner) => new UIDataDriver(new XplatSheetProvider(), owner));
30
+ var hosts = /* @__PURE__ */ new Map();
31
+ var roots = /* @__PURE__ */ new Map();
32
+ function mountSubtree(id, render) {
33
+ let host = hosts.get(id);
34
+ if (!host) {
35
+ host = new GridLayout();
36
+ hosts.set(id, host);
37
+ SwiftUIManager.registerNativeScriptViews({ [id]: true }, {
38
+ create: (fullId) => hosts.get(fullId.split("-")[0]),
39
+ destroy: (fullId) => {
40
+ hosts.delete(fullId.split("-")[0]);
41
+ }
42
+ });
43
+ }
44
+ let root = roots.get(id);
45
+ if (!root) {
46
+ root = createNativeScriptRoot(host);
47
+ roots.set(id, root);
48
+ }
49
+ root.render(ContentRoot, { render });
50
+ }
51
+ function unmountSubtree(id) {
52
+ roots.get(id)?.unmount?.();
53
+ roots.delete(id);
54
+ hosts.delete(id);
55
+ }
56
+ var ContentRoot = defineUniversalComponent("nativescript", function ContentRoot(props) {
57
+ return universalValue(__octaneUniversalPlan0, [props.render()]);
58
+ }, { "module": "@nativescript-community/octane" });
59
+ var seq = 0;
60
+ var SwiftUIBottomSheet = defineUniversalComponent("nativescript", function SwiftUIBottomSheet(props) {
61
+ const contentId = useRef("", _h$0);
62
+ if (!contentId.current) contentId.current = `xplatsheet${++seq}`;
63
+ useEffect(() => {
64
+ if (props.content) mountSubtree(contentId.current, props.content);
65
+ return () => {
66
+ unmountSubtree(contentId.current);
67
+ };
68
+ }, [], _h$1);
69
+ useEffect(() => {
70
+ if (props.content) mountSubtree(contentId.current, props.content);
71
+ }, [props.content], _h$2);
72
+ return universalValue(__octaneUniversalPlan1, [
73
+ props.id,
74
+ props.className,
75
+ props.style,
76
+ {
77
+ isPresented: props.open ?? false,
78
+ contentId: props.content ? contentId.current : "",
79
+ fitToContents: props.fitToContents ?? false,
80
+ detents: props.detents ?? ["medium"],
81
+ customHeight: props.customHeight,
82
+ customFraction: props.customFraction,
83
+ showDragIndicator: props.showDragIndicator ?? false,
84
+ interactiveDismissDisabled: props.interactiveDismissDisabled ?? false
85
+ },
86
+ (event) => {
87
+ if (event.data?.dismissed) {
88
+ props.onDismissed?.();
89
+ return;
90
+ }
91
+ const presented = event.data?.isPresented;
92
+ if (typeof presented === "boolean") props.onPresentedChange?.(presented);
93
+ }
94
+ ]);
95
+ }, { "module": "@nativescript-community/octane" });
96
+ //#endregion
97
+ export { SwiftUIBottomSheet };
@@ -0,0 +1,2 @@
1
+ import { MaterialBottomSheet } from "../MaterialBottomSheet.android.tsrx.js";
2
+ export { MaterialBottomSheet };
@@ -0,0 +1,2 @@
1
+ import { SwiftUIBottomSheet } from "../SwiftUIBottomSheet.ios.tsrx.js";
2
+ export { SwiftUIBottomSheet };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "a708cea8-c79d-46a5-ae17-04112d1be0ac",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,45 @@
1
+ import { Fragment, createElement, createScopedElement, createScopedValue, positionalChildren } from "octane";
2
+ //#region src/BottomSheet.web.tsrx
3
+ var BottomSheet = function BottomSheet(props, __s, __extra) {
4
+ const open = props.open ?? false;
5
+ (props.shouldDismissOnClickOutside ?? true) || props.shouldDismissOnBackPress;
6
+ if (!open) return positionalChildren([]);
7
+ return createScopedElement(Fragment, {}, () => positionalChildren([createScopedValue(() => createElement("div", {
8
+ style: {
9
+ position: "fixed",
10
+ inset: 0,
11
+ background: "rgb(0 0 0 / 0.4)",
12
+ zIndex: 999
13
+ },
14
+ onClick: () => {
15
+ if (props.shouldDismissOnClickOutside ?? true) props.onDismissed?.();
16
+ }
17
+ })), createScopedValue(() => createScopedElement("div", {
18
+ role: "dialog",
19
+ "aria-modal": "true",
20
+ style: {
21
+ position: "fixed",
22
+ left: 0,
23
+ right: 0,
24
+ bottom: 0,
25
+ zIndex: 1e3,
26
+ background: "var(--sheet-bg, Canvas)",
27
+ color: "var(--sheet-fg, CanvasText)",
28
+ borderRadius: "16px 16px 0 0",
29
+ boxShadow: "0 -4px 24px rgb(0 0 0 / 0.2)",
30
+ maxHeight: props.skipPartiallyExpanded ? "90vh" : "60vh",
31
+ overflowY: "auto"
32
+ }
33
+ }, () => positionalChildren([(props.showDragHandle ?? true) && createScopedValue(() => createScopedElement("div", { style: {
34
+ display: "flex",
35
+ justifyContent: "center",
36
+ padding: "8px 0"
37
+ } }, () => createScopedValue(() => createElement("div", { style: {
38
+ width: 36,
39
+ height: 4,
40
+ borderRadius: 2,
41
+ background: "color-mix(in srgb, CanvasText 30%, transparent)"
42
+ } })))), props.content?.()])))]));
43
+ };
44
+ //#endregion
45
+ export { BottomSheet };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "e60f4b8c-acc0-404d-89f0-5ee4a3e3365b",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ import { BottomSheet } from "../BottomSheet.web.tsrx.js";
2
+ export { BottomSheet };
package/package.json CHANGED
@@ -1,18 +1,74 @@
1
1
  {
2
2
  "name": "@octane-xplat/sheet",
3
- "version": "0.0.1",
3
+ "version": "0.9.0",
4
4
  "description": "Platform-specific modal bottom sheet controls for Octane xplat",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/octane-xplat/octane-xplat.git",
7
+ "url": "https://github.com/octane-xplat/octane-xplat",
8
8
  "directory": "packages/sheet"
9
9
  },
10
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/sheet",
11
- "main": "./index.js",
12
- "types": "./index.d.ts",
13
10
  "files": [
14
- "README.md",
15
- "index.js",
16
- "index.d.ts"
17
- ]
18
- }
11
+ "dist",
12
+ "types",
13
+ "src",
14
+ "platforms"
15
+ ],
16
+ "type": "module",
17
+ "exports": {
18
+ "./ios": {
19
+ "types": "./types/ios/index.d.ts",
20
+ "native": "./dist/native/ios/index.js"
21
+ },
22
+ "./android": {
23
+ "types": "./types/android/index.d.ts",
24
+ "native": "./dist/native/android/index.js"
25
+ },
26
+ "./web": {
27
+ "types": "./types/web/index.d.ts",
28
+ "web": "./dist/web/web/index.js"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@nativescript/jetpack-compose": "1.0.0-beta.0",
36
+ "@nativescript/swift-ui": "4.0.2"
37
+ },
38
+ "devDependencies": {
39
+ "@nativescript-community/octane": "0.2.4",
40
+ "@nativescript/core": "9.1.2",
41
+ "@octanejs/vite-plugin": "0.1.60",
42
+ "octane": "0.5.0",
43
+ "tsrx-typegen": "0.2.1",
44
+ "typescript": "5.9.3",
45
+ "vite": "8.3.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@nativescript-community/octane": ">=0.2.1 <1",
49
+ "@nativescript/core": ">=9.1.0 <10",
50
+ "octane": ">=0.1.51 <1"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "@nativescript-community/octane": {
54
+ "optional": true
55
+ },
56
+ "@nativescript/core": {
57
+ "optional": true
58
+ },
59
+ "octane": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "nativescript": {
64
+ "platforms": {
65
+ "android": "9.1.0",
66
+ "ios": "9.1.0"
67
+ }
68
+ },
69
+ "scripts": {
70
+ "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json",
71
+ "test:packed": "node tests/packed-consumer.mjs",
72
+ "pack:check": "tsrx-typegen --pack-check"
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ apply plugin: 'org.jetbrains.kotlin.plugin.compose'
2
+
3
+ android {
4
+ buildFeatures {
5
+ compose true
6
+ }
7
+ kotlinOptions {
8
+ jvmTarget = '17'
9
+ }
10
+ }
11
+
12
+ dependencies {
13
+ implementation platform('androidx.compose:compose-bom:2024.12.01')
14
+ implementation 'androidx.compose.material3:material3'
15
+ }
@@ -0,0 +1,198 @@
1
+ // Adapted from @expo/ui (MIT) — Copyright 2025-present 650 Industries.
2
+ // Upstream: expo/packages/expo-ui/android/.../ui/ModalBottomSheetView.kt
3
+ // (sdk-57). The ExpoModules glue (ComposeProps records, async-function
4
+ // handles, slot views, ModifierRegistry) is replaced by the
5
+ // @nativescript/jetpack-compose provider contract: updateData(Map) + onEvent.
6
+ // The sheet's content is a registered NativeScript view hosted through
7
+ // AndroidView — XplatViewRegistry mirrors the context-menu leaf's registry
8
+ // (same mechanism, kept leaf-local while the shared-bridge question is open).
9
+
10
+ @file:OptIn(ExperimentalMaterial3Api::class)
11
+
12
+ package com.octanexplat.sheet
13
+
14
+ import android.app.Activity
15
+ import android.app.Dialog
16
+ import android.content.Context
17
+ import android.view.KeyEvent
18
+ import android.view.View
19
+ import android.view.ViewGroup
20
+ import android.view.inputmethod.InputMethodManager
21
+ import androidx.compose.foundation.layout.Box
22
+ import androidx.compose.foundation.layout.fillMaxWidth
23
+ import androidx.compose.material3.BottomSheetDefaults
24
+ import androidx.compose.material3.ExperimentalMaterial3Api
25
+ import androidx.compose.material3.ModalBottomSheet
26
+ import androidx.compose.material3.ModalBottomSheetProperties
27
+ import androidx.compose.material3.contentColorFor
28
+ import androidx.compose.material3.rememberModalBottomSheetState
29
+ import androidx.compose.runtime.Composable
30
+ import androidx.compose.runtime.DisposableEffect
31
+ import androidx.compose.runtime.getValue
32
+ import androidx.compose.runtime.mutableStateOf
33
+ import androidx.compose.runtime.setValue
34
+ import androidx.compose.ui.Modifier
35
+ import androidx.compose.ui.platform.ComposeView
36
+ import androidx.compose.ui.platform.LocalView
37
+ import androidx.compose.ui.viewinterop.AndroidView
38
+ import androidx.compose.ui.window.DialogWindowProvider
39
+ import org.json.JSONObject
40
+ import java.lang.ref.WeakReference
41
+
42
+ /** id → NativeScript view registry, callable directly from JS
43
+ * (`com.octanexplat.sheet.XplatViewRegistry.register`). Same mechanism as
44
+ * the context-menu leaf's registry — the sheet content subtree embeds in
45
+ * the modal sheet's dialog window through AndroidView. Weak refs; JS owns
46
+ * the lifecycle and unregisters on unmount. */
47
+ object XplatViewRegistry {
48
+ private val views = HashMap<String, WeakReference<View>>()
49
+
50
+ @JvmStatic
51
+ fun register(id: String, view: View) {
52
+ views[id] = WeakReference(view)
53
+ }
54
+
55
+ @JvmStatic
56
+ fun unregister(id: String) {
57
+ views.remove(id)
58
+ }
59
+
60
+ @JvmStatic
61
+ fun get(id: String): View? = views[id]?.get()
62
+ }
63
+
64
+ private data class SheetUiState(
65
+ val open: Boolean = false,
66
+ val contentId: String = "",
67
+ val skipPartiallyExpanded: Boolean = false,
68
+ val showDragHandle: Boolean = true,
69
+ val sheetGesturesEnabled: Boolean = true,
70
+ val shouldDismissOnBackPress: Boolean = true,
71
+ val shouldDismissOnClickOutside: Boolean = true,
72
+ val containerColor: Int? = null,
73
+ val contentColor: Int? = null,
74
+ val scrimColor: Int? = null,
75
+ )
76
+
77
+ private fun Any?.asBoolean(): Boolean = when (this) {
78
+ is Boolean -> this
79
+ is Number -> toInt() != 0
80
+ else -> false
81
+ }
82
+
83
+ private fun Any?.asColorInt(): Int? = when (this) {
84
+ is Number -> toInt()
85
+ is String -> runCatching { android.graphics.Color.parseColor(this) }.getOrNull()
86
+ else -> null
87
+ }
88
+
89
+ /** True while the input method has an active connection to a view that
90
+ * accepts text — ported from Expo's ModalBottomSheetView. */
91
+ private fun Activity.isAcceptingText(): Boolean {
92
+ val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
93
+ return inputMethodManager?.isAcceptingText == true
94
+ }
95
+
96
+ /** Ported: the sheet lives in its own dialog window, so key events that the
97
+ * activity owns (e.g. dev-menu triggers) stop reaching it while the sheet
98
+ * is open — forward non-dismiss keys back, skipping while a field accepts
99
+ * text so typing can't trigger activity shortcuts. */
100
+ @Composable
101
+ private fun ForwardKeyEventsToActivity(activity: Activity?) {
102
+ val dialog = (LocalView.current.parent as? DialogWindowProvider)?.window?.callback as? Dialog
103
+ if (dialog == null || activity == null) {
104
+ return
105
+ }
106
+ DisposableEffect(dialog, activity) {
107
+ dialog.setOnKeyListener { _, keyCode, event ->
108
+ val isForwardable = event.action == KeyEvent.ACTION_UP &&
109
+ keyCode != KeyEvent.KEYCODE_BACK &&
110
+ keyCode != KeyEvent.KEYCODE_ESCAPE &&
111
+ !activity.isAcceptingText()
112
+ isForwardable && activity.onKeyUp(keyCode, event)
113
+ }
114
+ onDispose {
115
+ dialog.setOnKeyListener(null)
116
+ }
117
+ }
118
+ }
119
+
120
+ @Composable
121
+ private fun SheetContent(state: SheetUiState, activity: Activity?, onDismiss: () -> Unit) {
122
+ if (!state.open) {
123
+ return
124
+ }
125
+ val sheetState = rememberModalBottomSheetState(state.skipPartiallyExpanded)
126
+ val resolvedContainer = state.containerColor?.let { androidx.compose.ui.graphics.Color(it) }
127
+ ?: BottomSheetDefaults.ContainerColor
128
+ val resolvedContent = state.contentColor?.let { androidx.compose.ui.graphics.Color(it) }
129
+ ?: contentColorFor(resolvedContainer)
130
+ val resolvedScrim = state.scrimColor?.let { androidx.compose.ui.graphics.Color(it) }
131
+ ?: BottomSheetDefaults.ScrimColor
132
+
133
+ // This material3 predates sheetGesturesEnabled and
134
+ // shouldDismissOnClickOutside — only shouldDismissOnBackPress is
135
+ // carried through properties.
136
+ ModalBottomSheet(
137
+ sheetState = sheetState,
138
+ onDismissRequest = onDismiss,
139
+ containerColor = resolvedContainer,
140
+ contentColor = resolvedContent,
141
+ scrimColor = resolvedScrim,
142
+ dragHandle = if (state.showDragHandle) {
143
+ { BottomSheetDefaults.DragHandle() }
144
+ } else {
145
+ null
146
+ },
147
+ properties = ModalBottomSheetProperties(
148
+ shouldDismissOnBackPress = state.shouldDismissOnBackPress,
149
+ ),
150
+ ) {
151
+ ForwardKeyEventsToActivity(activity)
152
+ Box(modifier = Modifier.fillMaxWidth()) {
153
+ val contentId = state.contentId
154
+ if (contentId.isNotEmpty()) {
155
+ AndroidView(
156
+ factory = { ctx ->
157
+ XplatViewRegistry.get(contentId) ?: View(ctx).apply {
158
+ layoutParams = ViewGroup.LayoutParams(0, 0)
159
+ }
160
+ },
161
+ )
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ class XplatSheetProvider {
168
+ private var state by mutableStateOf(SheetUiState())
169
+ var onEvent: ((Any) -> Unit)? = null
170
+
171
+ fun generateComposeView(view: ComposeView) {
172
+ view.setContent {
173
+ androidx.compose.material3.MaterialTheme {
174
+ SheetContent(
175
+ state = state,
176
+ activity = (view.context as? Activity),
177
+ onDismiss = { onEvent?.invoke(JSONObject().put("dismissed", true)) },
178
+ )
179
+ }
180
+ }
181
+ }
182
+
183
+ fun updateData(input: Map<Any, Any>) {
184
+ val data = input["data"] as? Map<*, *> ?: input
185
+ state = SheetUiState(
186
+ open = data["open"].asBoolean(),
187
+ contentId = data["contentId"] as? String ?: "",
188
+ skipPartiallyExpanded = data["skipPartiallyExpanded"].asBoolean(),
189
+ showDragHandle = if (data.containsKey("showDragHandle")) data["showDragHandle"].asBoolean() else true,
190
+ sheetGesturesEnabled = if (data.containsKey("sheetGesturesEnabled")) data["sheetGesturesEnabled"].asBoolean() else true,
191
+ shouldDismissOnBackPress = if (data.containsKey("shouldDismissOnBackPress")) data["shouldDismissOnBackPress"].asBoolean() else true,
192
+ shouldDismissOnClickOutside = if (data.containsKey("shouldDismissOnClickOutside")) data["shouldDismissOnClickOutside"].asBoolean() else true,
193
+ containerColor = data["containerColor"].asColorInt(),
194
+ contentColor = data["contentColor"].asColorInt(),
195
+ scrimColor = data["scrimColor"].asColorInt(),
196
+ )
197
+ }
198
+ }
Binary file
@@ -0,0 +1,188 @@
1
+ // Adapted from @expo/ui (MIT) — Copyright 2025-present 650 Industries.
2
+ // Upstream: expo/packages/expo-ui/ios/BottomSheetView.swift (sdk-57).
3
+ // The ExpoModules glue (@Field props record, EventDispatcher, children slot
4
+ // views) is replaced by the @nativescript/swift-ui provider contract:
5
+ // updateData(NSDictionary) + onEvent. The sheet's content is a registered
6
+ // NativeScript view resolved through NativeScriptViewFactory — the same
7
+ // HostedNativeScriptView the context-menu leaf re-declares (the plugin's
8
+ // NativeScriptView struct is module-internal).
9
+
10
+ import SwiftUI
11
+
12
+ private final class SheetModel: ObservableObject {
13
+ @Published var isPresented: Bool = false
14
+ @Published var contentId: String = ""
15
+ @Published var fitToContents: Bool = false
16
+ @Published var detents: [String] = ["medium"]
17
+ @Published var customHeight: Double? = nil
18
+ @Published var customFraction: Double? = nil
19
+ @Published var showDragIndicator: Bool = false
20
+ @Published var interactiveDismissDisabled: Bool = false
21
+ var onPresentedChange: ((Bool) -> Void)?
22
+ var onDismiss: (() -> Void)?
23
+ }
24
+
25
+ // Same shape as the plugin's NativeScriptViewRepresentable — unique instance
26
+ // id resolved through the core-owned NativeScriptViewFactory.
27
+ private struct HostedNativeScriptView: UIViewRepresentable {
28
+ let id: String
29
+
30
+ init(id: String) {
31
+ self.id = id + "-" + UUID().uuidString
32
+ }
33
+
34
+ func makeUIView(context: Context) -> UIView {
35
+ return NativeScriptViewFactory.shared?.getViewById(id) ?? UIView()
36
+ }
37
+
38
+ func updateUIView(_ uiView: UIView, context: Context) {}
39
+ }
40
+
41
+ private struct SizePreferenceKey: PreferenceKey {
42
+ static var defaultValue: CGSize?
43
+
44
+ static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
45
+ guard let nextValue = nextValue() else {
46
+ return
47
+ }
48
+ value = nextValue
49
+ }
50
+ }
51
+
52
+ private struct ReadSizeModifier: ViewModifier {
53
+ private var sizeView: some View {
54
+ GeometryReader { geometry in
55
+ Color.clear
56
+ .preference(key: SizePreferenceKey.self, value: geometry.size)
57
+ .allowsHitTesting(false)
58
+ }
59
+ }
60
+
61
+ func body(content: Content) -> some View {
62
+ content.background(sizeView)
63
+ }
64
+ }
65
+
66
+ @available(iOS 14.0, *)
67
+ private struct SheetContent: View {
68
+ @ObservedObject var model: SheetModel
69
+ @State private var isPresented: Bool = false
70
+ @State private var childrenSize: CGSize = .zero
71
+
72
+ private func handleSizeChange(_ size: CGSize) {
73
+ guard childrenSize != size else { return }
74
+ childrenSize = size
75
+ }
76
+
77
+ @available(iOS 16.0, tvOS 16.0, *)
78
+ private var presentationDetents: Set<PresentationDetent> {
79
+ if model.fitToContents, childrenSize.height > 0 {
80
+ return [.height(childrenSize.height)]
81
+ }
82
+ var result: Set<PresentationDetent> = []
83
+ for detent in model.detents {
84
+ switch detent {
85
+ case "large": result.insert(.large)
86
+ case "fraction": result.insert(.fraction(model.customFraction ?? 0.5))
87
+ case "height": result.insert(.height(model.customHeight ?? 300))
88
+ default: result.insert(.medium)
89
+ }
90
+ }
91
+ return result.isEmpty ? [.medium] : result
92
+ }
93
+
94
+ var body: some View {
95
+ Color.clear
96
+ .frame(width: 0, height: 0)
97
+ .sheet(isPresented: $isPresented, onDismiss: {
98
+ model.onDismiss?()
99
+ }) {
100
+ let content = HostedNativeScriptView(id: model.contentId)
101
+ .modifier(ReadSizeModifier())
102
+ .onPreferenceChange(SizePreferenceKey.self) { size in
103
+ if let size { handleSizeChange(size) }
104
+ }
105
+ if #available(iOS 16.0, tvOS 16.0, *) {
106
+ content
107
+ .presentationDetents(presentationDetents)
108
+ .presentationDragIndicator(model.showDragIndicator ? .visible : .hidden)
109
+ .interactiveDismissDisabled(model.interactiveDismissDisabled)
110
+ } else {
111
+ content
112
+ }
113
+ }
114
+ .onChange(of: isPresented) { newIsPresented in
115
+ if model.isPresented == newIsPresented {
116
+ return
117
+ }
118
+ model.onPresentedChange?(newIsPresented)
119
+ }
120
+ .onChange(of: model.isPresented) { newValue in
121
+ isPresented = newValue
122
+ }
123
+ .onAppear {
124
+ isPresented = model.isPresented
125
+ }
126
+ }
127
+ }
128
+
129
+ @objc(XplatSheetProvider)
130
+ public final class XplatSheetProvider: UIViewController, SwiftUIProvider {
131
+ private let model = SheetModel()
132
+
133
+ public required init() {
134
+ super.init(nibName: nil, bundle: nil)
135
+ model.onPresentedChange = { [weak self] presented in
136
+ self?.onEvent?(["isPresented": presented])
137
+ }
138
+ model.onDismiss = { [weak self] in
139
+ self?.onEvent?(["dismissed": true])
140
+ }
141
+ }
142
+
143
+ public required init?(coder: NSCoder) {
144
+ super.init(coder: coder)
145
+ model.onPresentedChange = { [weak self] presented in
146
+ self?.onEvent?(["isPresented": presented])
147
+ }
148
+ model.onDismiss = { [weak self] in
149
+ self?.onEvent?(["dismissed": true])
150
+ }
151
+ }
152
+
153
+ public override func viewDidLoad() {
154
+ super.viewDidLoad()
155
+ if #available(iOS 14.0, *) {
156
+ setupSwiftUIView(content: SheetContent(model: model))
157
+ }
158
+ }
159
+
160
+ public func updateData(data: NSDictionary) {
161
+ if let presented = data["isPresented"] as? NSNumber {
162
+ model.isPresented = presented.boolValue
163
+ }
164
+ if let contentId = data["contentId"] as? String {
165
+ model.contentId = contentId
166
+ }
167
+ if let fit = data["fitToContents"] as? NSNumber {
168
+ model.fitToContents = fit.boolValue
169
+ }
170
+ if let detents = data["detents"] as? [String] {
171
+ model.detents = detents
172
+ }
173
+ if let height = data["customHeight"] as? NSNumber {
174
+ model.customHeight = height.doubleValue
175
+ }
176
+ if let fraction = data["customFraction"] as? NSNumber {
177
+ model.customFraction = fraction.doubleValue
178
+ }
179
+ if let indicator = data["showDragIndicator"] as? NSNumber {
180
+ model.showDragIndicator = indicator.boolValue
181
+ }
182
+ if let disable = data["interactiveDismissDisabled"] as? NSNumber {
183
+ model.interactiveDismissDisabled = disable.boolValue
184
+ }
185
+ }
186
+
187
+ public var onEvent: ((NSDictionary) -> Void)?
188
+ }
@@ -0,0 +1,53 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import { useEffect, useRef } from 'octane'
3
+ import type { AppKitSheetProps } from './macos/types'
4
+
5
+ const host = (): any => (globalThis as any).__xplatAppKit ?? {}
6
+
7
+ function ContentRoot(props: { render?: () => any }) @{
8
+ <>{props.render?.()}</>;
9
+ }
10
+
11
+ /** macOS sheet — presents the `content` subtree in a real AppKit window
12
+ * sheet (`beginSheet` on the leaf's parent window), falling back to a
13
+ * floating window when no parent exists. `open` controls presentation;
14
+ * closing the sheet window (Esc/close button) or `open`→false fires
15
+ * `onDismissed`. Detents don't exist on macOS — the window sizes to the
16
+ * content's fitting size. */
17
+ export function AppKitSheet(props: AppKitSheetProps) @{
18
+ const anchor = useRef<any>(null)
19
+ const presented = useRef<any>(null)
20
+
21
+ useEffect(() => {
22
+ if (!props.open) return
23
+ if (presented.current) {
24
+ presented.current.update({ render: props.content })
25
+ return
26
+ }
27
+
28
+ const view = anchor.current
29
+ const present = host().presentSheet
30
+ if (!view || typeof present !== 'function') return
31
+ presented.current = present(view, {
32
+ component: ContentRoot,
33
+ props: { render: props.content },
34
+ onDismissed: () => {
35
+ presented.current = null
36
+ props.onDismissed?.()
37
+ },
38
+ })
39
+
40
+ return () => {
41
+ const entry = presented.current
42
+ presented.current = null
43
+ entry?.close()
44
+ }
45
+ }, [props.open])
46
+
47
+ // Push content render-fn changes into the presented sheet while open.
48
+ useEffect(() => {
49
+ presented.current?.update?.({ render: props.content })
50
+ })
51
+
52
+ <flexboxlayout ref={(v: any) => { anchor.current = v }} id={props.id} />;
53
+ }
@@ -0,0 +1,48 @@
1
+ /** @jsxImportSource octane */
2
+ import type { BottomSheetProps } from './web/types'
3
+
4
+ /** Web bottom sheet: a fixed-position panel over a scrim. `open` controls
5
+ * visibility; `onDismissed` fires on scrim click / Escape. */
6
+ export function BottomSheet(props: BottomSheetProps) @{
7
+ const open = props.open ?? false
8
+ const dismissable = (props.shouldDismissOnClickOutside ?? true) || (props.shouldDismissOnBackPress ?? true)
9
+
10
+ if (!open) {
11
+ return <></>
12
+ }
13
+
14
+ <>
15
+ <div
16
+ style={{ position: 'fixed', inset: 0, background: 'rgb(0 0 0 / 0.4)', zIndex: 999 }}
17
+ onClick={() => {
18
+ if (props.shouldDismissOnClickOutside ?? true) {
19
+ props.onDismissed?.()
20
+ }
21
+ }}
22
+ />
23
+ <div
24
+ role="dialog"
25
+ aria-modal="true"
26
+ style={{
27
+ position: 'fixed',
28
+ left: 0,
29
+ right: 0,
30
+ bottom: 0,
31
+ zIndex: 1000,
32
+ background: 'var(--sheet-bg, Canvas)',
33
+ color: 'var(--sheet-fg, CanvasText)',
34
+ borderRadius: '16px 16px 0 0',
35
+ boxShadow: '0 -4px 24px rgb(0 0 0 / 0.2)',
36
+ maxHeight: props.skipPartiallyExpanded ? '90vh' : '60vh',
37
+ overflowY: 'auto',
38
+ }}
39
+ >
40
+ {(props.showDragHandle ?? true) && (
41
+ <div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}>
42
+ <div style={{ width: 36, height: 4, borderRadius: 2, background: 'color-mix(in srgb, CanvasText 30%, transparent)' }} />
43
+ </div>
44
+ )}
45
+ {props.content?.()}
46
+ </div>
47
+ </>;
48
+ }
@@ -0,0 +1,102 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane'
3
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
4
+ import { ComposeDataDriver, JetpackCompose, registerJetpackCompose } from '@nativescript/jetpack-compose'
5
+ import { Application, GridLayout } from '@nativescript/core'
6
+ import { createNativeScriptRoot } from '@nativescript-community/octane'
7
+ import { useEffect, useRef, useState } from 'octane'
8
+ import type { UniversalComponent } from 'octane/universal'
9
+ import type { MaterialBottomSheetProps } from './android/types'
10
+
11
+ declare const com: any
12
+
13
+ declare module '@nativescript-community/octane/intrinsics' {
14
+ interface NativeScriptElements {
15
+ xplatandroidsheet: Attributes<typeof JetpackCompose>
16
+ }
17
+ }
18
+
19
+ registerElement('xplatandroidsheet', JetpackCompose)
20
+ registerJetpackCompose('xplat-material-sheet', (owner: any) =>
21
+ new ComposeDataDriver(new com.octanexplat.sheet.XplatSheetProvider(), owner),
22
+ )
23
+
24
+ // Dash-free ids — the registry keys feed the provider's AndroidView factory.
25
+ let seq = 0
26
+
27
+ function ContentRoot(props: { render: () => any }) @{
28
+ <>{props.render()}</>;
29
+ }
30
+
31
+ /** Renders a Material 3 ModalBottomSheet whose content is a detached
32
+ * octane subtree hosted via XplatViewRegistry + AndroidView inside the
33
+ * sheet's dialog window. */
34
+ export function MaterialBottomSheet(props: MaterialBottomSheetProps) @{
35
+ const contentId = useRef<string>('')
36
+ if (!contentId.current) {
37
+ contentId.current = `xplatsheet${++seq}`
38
+ }
39
+
40
+ const mounted = useRef<{ host: any; root: any } | null>(null)
41
+ const [ready, setReady] = useState(false)
42
+
43
+ useEffect(() => {
44
+ const host = new GridLayout()
45
+ // See context-menu: _setupAsRootView needs a real Context on Android.
46
+
47
+ const context = (Application.android as any).foregroundActivity ?? (Application.android as any).startActivity
48
+ const nativeHost = host as any
49
+
50
+ nativeHost._setupAsRootView(context)
51
+ nativeHost.callLoaded()
52
+ const root = createNativeScriptRoot(host)
53
+ if (props.content) {
54
+ root.render(ContentRoot as unknown as UniversalComponent, { render: props.content })
55
+ }
56
+
57
+ com.octanexplat.sheet.XplatViewRegistry.register(contentId.current, host.android)
58
+ mounted.current = { host, root }
59
+ // Gate contentId on registration — AndroidView's factory resolves the
60
+ // host once contentId arrives in updateData.
61
+ setReady(true)
62
+
63
+ return () => {
64
+ com.octanexplat.sheet.XplatViewRegistry.unregister(contentId.current)
65
+ root.unmount?.()
66
+ mounted.current = null
67
+ setReady(false)
68
+ }
69
+ }, [])
70
+
71
+ useEffect(() => {
72
+ if (props.content) {
73
+ mounted.current?.root?.render(ContentRoot as unknown as UniversalComponent, {
74
+ render: props.content,
75
+ })
76
+ }
77
+ }, [props.content])
78
+
79
+ <xplatandroidsheet
80
+ id={props.id}
81
+ className={props.className}
82
+ style={props.style}
83
+ composeId="xplat-material-sheet"
84
+ data={{
85
+ open: (props.open ?? false) ? 1 : 0,
86
+ contentId: ready && props.content ? contentId.current : '',
87
+ skipPartiallyExpanded: (props.skipPartiallyExpanded ?? false) ? 1 : 0,
88
+ showDragHandle: (props.showDragHandle ?? true) ? 1 : 0,
89
+ sheetGesturesEnabled: (props.sheetGesturesEnabled ?? true) ? 1 : 0,
90
+ shouldDismissOnBackPress: (props.shouldDismissOnBackPress ?? true) ? 1 : 0,
91
+ shouldDismissOnClickOutside: (props.shouldDismissOnClickOutside ?? true) ? 1 : 0,
92
+ containerColor: props.containerColor,
93
+ contentColor: props.contentColor,
94
+ scrimColor: props.scrimColor,
95
+ }}
96
+ onComposeEvent={(event: any) => {
97
+ if (event.data?.dismissed) {
98
+ props.onDismissed?.()
99
+ }
100
+ }}
101
+ />;
102
+ }
@@ -0,0 +1,119 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane'
3
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
4
+ import { SwiftUI, UIDataDriver, registerSwiftUI, SwiftUIManager } from '@nativescript/swift-ui'
5
+ import { GridLayout } from '@nativescript/core'
6
+ import { createNativeScriptRoot } from '@nativescript-community/octane'
7
+ import { useEffect, useRef } from 'octane'
8
+ import type { UniversalComponent, UniversalRoot } from 'octane/universal'
9
+ import type { SwiftUIBottomSheetProps } from './ios/types'
10
+
11
+ declare const XplatSheetProvider: any
12
+
13
+ declare module '@nativescript-community/octane/intrinsics' {
14
+ interface NativeScriptElements {
15
+ xplatiossheet: Attributes<typeof SwiftUI>
16
+ }
17
+ }
18
+
19
+ registerElement('xplatiossheet', SwiftUI)
20
+ registerSwiftUI('xplat-swiftui-sheet', (owner: any) =>
21
+ new UIDataDriver(new XplatSheetProvider(), owner),
22
+ )
23
+
24
+ // Host views keyed by dash-free registry id — same mechanism as the
25
+ // context-menu leaf (kept leaf-local while the shared-bridge question is open).
26
+ const hosts = new Map<string, GridLayout>()
27
+ const roots = new Map<string, UniversalRoot>()
28
+
29
+ function mountSubtree(id: string, render: () => any): void {
30
+ let host = hosts.get(id)
31
+ if (!host) {
32
+ host = new GridLayout()
33
+ hosts.set(id, host)
34
+
35
+ SwiftUIManager.registerNativeScriptViews(
36
+ { [id]: true },
37
+ {
38
+ create: (fullId: string) => hosts.get(fullId.split('-')[0]) as any,
39
+ destroy: (fullId: string) => {
40
+ hosts.delete(fullId.split('-')[0])
41
+ },
42
+ } as any,
43
+ )
44
+ }
45
+
46
+ let root = roots.get(id)
47
+ if (!root) {
48
+ root = createNativeScriptRoot(host)
49
+ roots.set(id, root)
50
+ }
51
+
52
+ root.render(ContentRoot as unknown as UniversalComponent, { render })
53
+ }
54
+
55
+ function unmountSubtree(id: string): void {
56
+ roots.get(id)?.unmount?.()
57
+ roots.delete(id)
58
+ hosts.delete(id)
59
+ }
60
+
61
+ function ContentRoot(props: { render: () => any }) @{
62
+ <>{props.render()}</>;
63
+ }
64
+
65
+ let seq = 0
66
+
67
+ /** Renders a SwiftUI .sheet whose content is a detached octane subtree
68
+ * embedded through NativeScriptViewFactory-by-id. `fitToContents` uses the
69
+ * ported size-reader → `.presentationDetents([.height(size)])`. */
70
+ export function SwiftUIBottomSheet(props: SwiftUIBottomSheetProps) @{
71
+ const contentId = useRef<string>('')
72
+ if (!contentId.current) {
73
+ contentId.current = `xplatsheet${++seq}`
74
+ }
75
+
76
+ useEffect(() => {
77
+ if (props.content) {
78
+ mountSubtree(contentId.current, props.content)
79
+ }
80
+
81
+ return () => {
82
+ unmountSubtree(contentId.current)
83
+ }
84
+ }, [])
85
+
86
+ useEffect(() => {
87
+ if (props.content) {
88
+ mountSubtree(contentId.current, props.content)
89
+ }
90
+ }, [props.content])
91
+
92
+ <xplatiossheet
93
+ id={props.id}
94
+ className={props.className}
95
+ style={props.style}
96
+ swiftId="xplat-swiftui-sheet"
97
+ data={{
98
+ isPresented: props.open ?? false,
99
+ contentId: props.content ? contentId.current : '',
100
+ fitToContents: props.fitToContents ?? false,
101
+ detents: props.detents ?? ['medium'],
102
+ customHeight: props.customHeight,
103
+ customFraction: props.customFraction,
104
+ showDragIndicator: props.showDragIndicator ?? false,
105
+ interactiveDismissDisabled: props.interactiveDismissDisabled ?? false,
106
+ }}
107
+ onSwiftUIEvent={(event: any) => {
108
+ if (event.data?.dismissed) {
109
+ props.onDismissed?.()
110
+ return
111
+ }
112
+
113
+ const presented = event.data?.isPresented
114
+ if (typeof presented === 'boolean') {
115
+ props.onPresentedChange?.(presented)
116
+ }
117
+ }}
118
+ />;
119
+ }
@@ -0,0 +1,2 @@
1
+ export { MaterialBottomSheet } from '../MaterialBottomSheet.android.tsrx'
2
+ export type { MaterialBottomSheetProps } from './types'
@@ -0,0 +1,27 @@
1
+ export interface MaterialBottomSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ /** Controls sheet visibility. */
6
+ open?: boolean
7
+ /** Called when the sheet asks to close (swipe, scrim, back). */
8
+ onDismissed?: () => void
9
+ /** Skip the half-expanded detent; only fully expanded/hidden remain. */
10
+ skipPartiallyExpanded?: boolean
11
+ /** Show the drag handle affordance. @default true */
12
+ showDragHandle?: boolean
13
+ /** Allow swipe gestures on the sheet surface. @default true
14
+ * Not supported by the material3 version the app resolves — prop is
15
+ * accepted but ignored. */
16
+ sheetGesturesEnabled?: boolean
17
+ /** @default true */
18
+ shouldDismissOnBackPress?: boolean
19
+ /** @default true — accepted but ignored on the resolved material3. */
20
+ shouldDismissOnClickOutside?: boolean
21
+ containerColor?: string
22
+ contentColor?: string
23
+ scrimColor?: string
24
+ /** Render fn producing the sheet content — hosted inside the sheet's
25
+ * dialog window through AndroidView. */
26
+ content?: () => any
27
+ }
@@ -0,0 +1,2 @@
1
+ export { SwiftUIBottomSheet } from '../SwiftUIBottomSheet.ios.tsrx'
2
+ export type { SwiftUIBottomSheetProps, SwiftUISheetDetent } from './types'
@@ -0,0 +1,26 @@
1
+ export type SwiftUISheetDetent = 'medium' | 'large' | 'fraction' | 'height'
2
+
3
+ export interface SwiftUIBottomSheetProps {
4
+ id?: string
5
+ className?: any
6
+ style?: any
7
+ /** Controls sheet presentation. */
8
+ open?: boolean
9
+ /** Reports the platform-side presented state (echo-suppressed). */
10
+ onPresentedChange?: (presented: boolean) => void
11
+ /** Called when the sheet dismisses. */
12
+ onDismissed?: () => void
13
+ /** Measure content height and use it as the single detent (iOS 16+). */
14
+ fitToContents?: boolean
15
+ /** Detent set — 'fraction' reads `customFraction`, 'height' reads
16
+ * `customHeight` (iOS 16+). @default ['medium'] */
17
+ detents?: SwiftUISheetDetent[]
18
+ customFraction?: number
19
+ customHeight?: number
20
+ /** Show the grabber. @default false (SwiftUI default is hidden). */
21
+ showDragIndicator?: boolean
22
+ interactiveDismissDisabled?: boolean
23
+ /** Render fn producing the sheet content — hosted inside the sheet
24
+ * presentation through NativeScriptViewFactory. */
25
+ content?: () => any
26
+ }
@@ -0,0 +1,2 @@
1
+ export { AppKitSheet } from '../AppKitSheet.macos.tsrx'
2
+ export type { AppKitSheetProps } from './types'
@@ -0,0 +1,12 @@
1
+ export interface AppKitSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ /** Controls sheet presentation. */
6
+ open?: boolean
7
+ /** Called when the sheet window closes (close button or `open`→false). */
8
+ onDismissed?: () => void
9
+ /** Render fn producing the sheet content — hosted in the sheet's own
10
+ * octane root inside the presented window. */
11
+ content?: () => any
12
+ }
@@ -0,0 +1,2 @@
1
+ export { BottomSheet } from '../BottomSheet.web.tsrx'
2
+ export type { BottomSheetProps } from './types'
@@ -0,0 +1,16 @@
1
+ export interface BottomSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ open?: boolean
6
+ onDismissed?: () => void
7
+ skipPartiallyExpanded?: boolean
8
+ showDragHandle?: boolean
9
+ sheetGesturesEnabled?: boolean
10
+ shouldDismissOnBackPress?: boolean
11
+ shouldDismissOnClickOutside?: boolean
12
+ containerColor?: string
13
+ contentColor?: string
14
+ scrimColor?: string
15
+ content?: () => any
16
+ }
@@ -0,0 +1,18 @@
1
+ export interface MaterialBottomSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ open?: boolean
6
+ onDismissed?: () => void
7
+ skipPartiallyExpanded?: boolean
8
+ showDragHandle?: boolean
9
+ sheetGesturesEnabled?: boolean
10
+ shouldDismissOnBackPress?: boolean
11
+ shouldDismissOnClickOutside?: boolean
12
+ containerColor?: string
13
+ contentColor?: string
14
+ scrimColor?: string
15
+ content?: () => any
16
+ }
17
+
18
+ export declare function MaterialBottomSheet(props: MaterialBottomSheetProps): unknown
@@ -0,0 +1,19 @@
1
+ export type SwiftUISheetDetent = 'medium' | 'large' | 'fraction' | 'height'
2
+
3
+ export interface SwiftUIBottomSheetProps {
4
+ id?: string
5
+ className?: any
6
+ style?: any
7
+ open?: boolean
8
+ onPresentedChange?: (presented: boolean) => void
9
+ onDismissed?: () => void
10
+ fitToContents?: boolean
11
+ detents?: SwiftUISheetDetent[]
12
+ customFraction?: number
13
+ customHeight?: number
14
+ showDragIndicator?: boolean
15
+ interactiveDismissDisabled?: boolean
16
+ content?: () => any
17
+ }
18
+
19
+ export declare function SwiftUIBottomSheet(props: SwiftUIBottomSheetProps): unknown
@@ -0,0 +1,10 @@
1
+ export interface AppKitSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ open?: boolean
6
+ onDismissed?: () => void
7
+ content?: () => any
8
+ }
9
+
10
+ export declare function AppKitSheet(props: AppKitSheetProps): unknown
@@ -0,0 +1,18 @@
1
+ export interface BottomSheetProps {
2
+ id?: string
3
+ className?: any
4
+ style?: any
5
+ open?: boolean
6
+ onDismissed?: () => void
7
+ skipPartiallyExpanded?: boolean
8
+ showDragHandle?: boolean
9
+ sheetGesturesEnabled?: boolean
10
+ shouldDismissOnBackPress?: boolean
11
+ shouldDismissOnClickOutside?: boolean
12
+ containerColor?: string
13
+ contentColor?: string
14
+ scrimColor?: string
15
+ content?: () => any
16
+ }
17
+
18
+ export declare function BottomSheet(props: BottomSheetProps): unknown
package/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {}
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Reservation stub; implementation will arrive in a later release.
2
- module.exports = {}