From 2d1b0a10978c4d1a11a2ac841c7251aaf3f430c3 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Fri, 29 May 2026 22:26:04 -0300 Subject: [PATCH 01/22] initial testing --- .../com/nativephp/mobile/ui/NativeTopBar.kt | 153 ++++++++++++------ .../com/nativephp/mobile/ui/NativeUIModels.kt | 3 +- .../NativePHP/NativeUI/NativeTopBar.swift | 43 ++++- .../NativePHP/NativeUI/NativeUIModels.swift | 3 +- .../Components/Navigation/TopBarAction.php | 4 +- 5 files changed, 150 insertions(+), 56 deletions(-) diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt index de18776b..40271271 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt @@ -38,6 +38,27 @@ fun NativeTopBar( val backgroundColor = data.backgroundColor?.let { parseColor(it) } val textColor = data.textColor?.let { parseColor(it) } val actions = data.children?.mapNotNull { it.data } ?: emptyList() + val handleActionClick: (TopBarAction) -> Unit = { action -> + Log.d(TAG, "⚡ Action clicked: ${action.label ?: action.id}") + action.url?.let { url -> + if (isExternalUrl(url)) { + Log.d(TAG, "🌐 Opening external URL in browser: $url") + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } catch (e: Exception) { + Log.e(TAG, "Failed to open external URL: $url", e) + } + } else { + Log.d(TAG, "📱 Opening internal URL in WebView: $url") + onNavigate(url) + } + } + action.event?.let { + // Dispatch event if specified + Log.d(TAG, "📢 Dispatching event: $it") + } + } // Split actions into visible (max 3) and overflow val visibleActions = actions.take(3) @@ -83,34 +104,47 @@ fun NativeTopBar( actions = { // Render visible actions (max 3) visibleActions.forEach { action -> - IconButton( - onClick = { - Log.d(TAG, "⚡ Action clicked: ${action.label ?: action.id}") - action.url?.let { url -> - if (isExternalUrl(url)) { - Log.d(TAG, "🌐 Opening external URL in browser: $url") - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) - } catch (e: Exception) { - Log.e(TAG, "Failed to open external URL: $url", e) + val actionChildren = action.children?.mapNotNull { it.data } ?: emptyList() + + if (actionChildren.isNotEmpty()) { + val showActionMenu = remember(action.id) { mutableStateOf(false) } + + IconButton(onClick = { showActionMenu.value = true }) { + MaterialIcon( + name = action.icon, + contentDescription = action.label ?: action.id, + tint = textColor ?: MaterialTheme.colorScheme.onSurface + ) + } + + DropdownMenu( + expanded = showActionMenu.value, + onDismissRequest = { showActionMenu.value = false } + ) { + actionChildren.forEach { child -> + DropdownMenuItem( + text = { Text(child.label ?: child.id) }, + onClick = { + showActionMenu.value = false + handleActionClick(child) + }, + leadingIcon = { + MaterialIcon( + name = child.icon, + contentDescription = child.label ?: child.id + ) } - } else { - Log.d(TAG, "📱 Opening internal URL in WebView: $url") - onNavigate(url) - } - } - action.event?.let { - // Dispatch event if specified - Log.d(TAG, "📢 Dispatching event: $it") + ) } } - ) { - MaterialIcon( - name = action.icon, - contentDescription = action.label ?: action.id, - tint = textColor ?: MaterialTheme.colorScheme.onSurface - ) + } else { + IconButton(onClick = { handleActionClick(action) }) { + MaterialIcon( + name = action.icon, + contentDescription = action.label ?: action.id, + tint = textColor ?: MaterialTheme.colorScheme.onSurface + ) + } } } @@ -129,36 +163,51 @@ fun NativeTopBar( onDismissRequest = { showOverflowMenu.value = false } ) { overflowActions.forEach { action -> - DropdownMenuItem( - text = { Text(action.label ?: action.id) }, - onClick = { - showOverflowMenu.value = false - Log.d(TAG, "⚡ Overflow action clicked: ${action.label ?: action.id}") - action.url?.let { url -> - if (isExternalUrl(url)) { - Log.d(TAG, "🌐 Opening external URL in browser: $url") - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) - } catch (e: Exception) { - Log.e(TAG, "Failed to open external URL: $url", e) - } - } else { - Log.d(TAG, "📱 Opening internal URL in WebView: $url") - onNavigate(url) - } - } - action.event?.let { - Log.d(TAG, "📢 Dispatching event: $it") + val actionChildren = action.children?.mapNotNull { it.data } ?: emptyList() + + if (actionChildren.isNotEmpty()) { + DropdownMenuItem( + text = { Text(action.label ?: action.id) }, + onClick = {}, + enabled = false, + leadingIcon = { + MaterialIcon( + name = action.icon, + contentDescription = action.label ?: action.id + ) } - }, - leadingIcon = { - MaterialIcon( - name = action.icon, - contentDescription = action.label ?: action.id + ) + + actionChildren.forEach { child -> + DropdownMenuItem( + text = { Text(child.label ?: child.id) }, + onClick = { + showOverflowMenu.value = false + handleActionClick(child) + }, + leadingIcon = { + MaterialIcon( + name = child.icon, + contentDescription = child.label ?: child.id + ) + } ) } - ) + } else { + DropdownMenuItem( + text = { Text(action.label ?: action.id) }, + onClick = { + showOverflowMenu.value = false + handleActionClick(action) + }, + leadingIcon = { + MaterialIcon( + name = action.icon, + contentDescription = action.label ?: action.id + ) + } + ) + } } } } diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt index 3437c43f..e918b601 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt @@ -158,7 +158,8 @@ data class TopBarAction( val icon: String, val label: String? = null, val url: String? = null, - val event: String? = null + val event: String? = null, + val children: List? = null ) /** diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 84be2408..b1971907 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,9 +85,46 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] + context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil + let childActions = action.data.children ?? [] + + if !childActions.isEmpty { + if #available(iOS 26.0, *) { + let menuItems: [UIAction] = childActions.compactMap { child in + guard let childUrl = child.data.url, !childUrl.isEmpty else { + return nil + } + + let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil + + return UIAction(title: child.data.label, image: childImage) { _ in + context.coordinator.navigate(to: childUrl) + } + } + + if !menuItems.isEmpty { + let menu = UIMenu(title: "", children: menuItems) + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } + + continue + } + } + + guard let actionUrl = action.data.url, !actionUrl.isEmpty else { + continue + } // Create button with both image and title when available let button = UIBarButtonItem( @@ -101,7 +138,7 @@ struct NativeTopBar: UIViewRepresentable { button.accessibilityIdentifier = action.data.id // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = action.data.url + context.coordinator.actionUrls[action.data.id] = actionUrl barButtonItems.append(button) } @@ -171,5 +208,9 @@ struct NativeTopBar: UIViewRepresentable { // Navigate to the URL using the proper navigation callback onNavigate(url) } + + func navigate(to url: String) { + onNavigate(url) + } } } diff --git a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift index e9b22c7d..f28767a3 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift @@ -217,8 +217,9 @@ struct TopBarActionComponent: Codable, Equatable { struct TopBarAction: Codable, Equatable { let id: String let label: String - let url: String + let url: String? let icon: String + let children: [TopBarActionComponent]? } // MARK: - NativeUI Parser diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index d342c8b3..dca95e18 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -8,6 +8,8 @@ class TopBarAction extends EdgeComponent { protected string $type = 'top_bar_action'; + protected bool $hasChildren = true; + public function __construct( public ?string $id = null, public ?string $icon = null, @@ -18,7 +20,7 @@ public function __construct( protected function requiredProps(): array { - return ['id', 'icon', 'label', 'url']; + return ['id', 'icon', 'label']; } protected function toNativeProps(): array From 8ef8d722b8a9f475eec19c28bc76f39a79a85110 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Fri, 29 May 2026 22:41:18 -0300 Subject: [PATCH 02/22] Create TopBarGroup.php --- .../Components/Navigation/TopBarGroup.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Edge/Components/Navigation/TopBarGroup.php diff --git a/src/Edge/Components/Navigation/TopBarGroup.php b/src/Edge/Components/Navigation/TopBarGroup.php new file mode 100644 index 00000000..6fe1fe42 --- /dev/null +++ b/src/Edge/Components/Navigation/TopBarGroup.php @@ -0,0 +1,32 @@ + $this->id, + 'icon' => $this->icon, + 'label' => $this->label, + ], fn ($value) => $value !== null && $value !== ''); + } +} From 0cfa0a1170fba2e57a9c9b637f4438947e3183c1 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 00:57:03 -0300 Subject: [PATCH 03/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index b1971907..672c9a3d 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -92,7 +92,7 @@ struct NativeTopBar: UIViewRepresentable { let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 26.0, *) { + if #available(iOS 14.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -107,15 +107,23 @@ struct NativeTopBar: UIViewRepresentable { if !menuItems.isEmpty { let menu = UIMenu(title: "", children: menuItems) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) + if #available(iOS 15.0, *) { + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } else { + let menuButton = makeMenuButton(title: action.data.label, image: image, menu: menu) + let buttonItem = UIBarButtonItem(customView: menuButton) + buttonItem.accessibilityLabel = action.data.label + buttonItem.accessibilityIdentifier = action.data.id + barButtonItems.append(buttonItem) + } } continue @@ -183,6 +191,21 @@ struct NativeTopBar: UIViewRepresentable { Coordinator(uiState: uiState, onNavigate: onNavigate) } + @available(iOS 14.0, *) + private func makeMenuButton(title: String?, image: UIImage?, menu: UIMenu) -> UIButton { + let button = UIButton(type: .system) + button.setTitle(title, for: .normal) + button.setImage(image, for: .normal) + button.menu = menu + button.showsMenuAsPrimaryAction = true + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + if image != nil && (title?.isEmpty == false) { + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4) + } + return button + } + class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void From b686563253c24c161e382d227f210c97f5ee11d9 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 01:43:53 -0300 Subject: [PATCH 04/22] Revert "Update NativeTopBar.swift" This reverts commit 0cfa0a1170fba2e57a9c9b637f4438947e3183c1. --- .../NativePHP/NativeUI/NativeTopBar.swift | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 672c9a3d..b1971907 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -92,7 +92,7 @@ struct NativeTopBar: UIViewRepresentable { let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 14.0, *) { + if #available(iOS 26.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -107,23 +107,15 @@ struct NativeTopBar: UIViewRepresentable { if !menuItems.isEmpty { let menu = UIMenu(title: "", children: menuItems) - if #available(iOS 15.0, *) { - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) - } else { - let menuButton = makeMenuButton(title: action.data.label, image: image, menu: menu) - let buttonItem = UIBarButtonItem(customView: menuButton) - buttonItem.accessibilityLabel = action.data.label - buttonItem.accessibilityIdentifier = action.data.id - barButtonItems.append(buttonItem) - } + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) } continue @@ -191,21 +183,6 @@ struct NativeTopBar: UIViewRepresentable { Coordinator(uiState: uiState, onNavigate: onNavigate) } - @available(iOS 14.0, *) - private func makeMenuButton(title: String?, image: UIImage?, menu: UIMenu) -> UIButton { - let button = UIButton(type: .system) - button.setTitle(title, for: .normal) - button.setImage(image, for: .normal) - button.menu = menu - button.showsMenuAsPrimaryAction = true - button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) - if image != nil && (title?.isEmpty == false) { - button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4) - } - return button - } - class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void From ab2b38a00ee809193cc3e8b2cfcd9cf3e490d3a4 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 10:36:01 -0300 Subject: [PATCH 05/22] testing icon no longer being requirement --- src/Edge/Components/Navigation/TopBarAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index dca95e18..b54b828a 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -20,7 +20,7 @@ public function __construct( protected function requiredProps(): array { - return ['id', 'icon', 'label']; + return ['id', 'label']; } protected function toNativeProps(): array From 5f058939239b619e03f535c20f0cbfd12ea590ae Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 10:39:46 -0300 Subject: [PATCH 06/22] Revert "testing icon no longer being requirement" This reverts commit ab2b38a00ee809193cc3e8b2cfcd9cf3e490d3a4. --- src/Edge/Components/Navigation/TopBarAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index b54b828a..dca95e18 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -20,7 +20,7 @@ public function __construct( protected function requiredProps(): array { - return ['id', 'label']; + return ['id', 'icon', 'label']; } protected function toNativeProps(): array From 5b9b8e61cd2850f3182fb313268d09fff3ce01c7 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 11:08:30 -0300 Subject: [PATCH 07/22] composer format --- src/Commands/PluginValidateCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Commands/PluginValidateCommand.php b/src/Commands/PluginValidateCommand.php index 66c41b14..273cb597 100644 --- a/src/Commands/PluginValidateCommand.php +++ b/src/Commands/PluginValidateCommand.php @@ -320,6 +320,7 @@ protected function findSwiftFile(string $basePath, string $className): ?string } $flatPath = $basePath.'/resources/ios/'.$class.'.swift'; + return $this->files->exists($flatPath) ? $flatPath : null; } From 349f737305b21322bdbbf1c8871bc6a540d6615f Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 11:59:07 -0300 Subject: [PATCH 08/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 68 ++++++++----------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index b1971907..33a16d25 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,60 +85,57 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] - context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 26.0, *) { - let menuItems: [UIAction] = childActions.compactMap { child in - guard let childUrl = child.data.url, !childUrl.isEmpty else { - return nil - } - - let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil - - return UIAction(title: child.data.label, image: childImage) { _ in - context.coordinator.navigate(to: childUrl) - } + let menuItems: [UIAction] = childActions.compactMap { child in + guard let childUrl = child.data.url, !childUrl.isEmpty else { + return nil } - if !menuItems.isEmpty { - let menu = UIMenu(title: "", children: menuItems) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) + let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil + + return UIAction(title: child.data.label, image: childImage) { _ in + context.coordinator.navigate(to: childUrl) } + } - continue + if !menuItems.isEmpty { + let menu = UIMenu(title: "", children: menuItems) + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) } + + continue } guard let actionUrl = action.data.url, !actionUrl.isEmpty else { continue } - // Create button with both image and title when available + let actionHandler = UIAction(title: action.data.label, image: image) { _ in + context.coordinator.navigate(to: actionUrl) + } + let button = UIBarButtonItem( title: action.data.label, image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) + primaryAction: actionHandler, + menu: nil ) button.accessibilityLabel = action.data.label button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl barButtonItems.append(button) } @@ -186,7 +183,6 @@ struct NativeTopBar: UIViewRepresentable { class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void - var actionUrls: [String: String] = [:] init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState @@ -199,16 +195,6 @@ struct NativeTopBar: UIViewRepresentable { } } - @objc func actionTapped(_ sender: UIBarButtonItem) { - guard let actionId = sender.accessibilityIdentifier, - let url = actionUrls[actionId] else { - return - } - - // Navigate to the URL using the proper navigation callback - onNavigate(url) - } - func navigate(to url: String) { onNavigate(url) } From 3fb14a4f54d427ea90a06cc12ef15cf95d673e80 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:03:22 -0300 Subject: [PATCH 09/22] Revert "Update NativeTopBar.swift" This reverts commit 349f737305b21322bdbbf1c8871bc6a540d6615f. --- .../NativePHP/NativeUI/NativeTopBar.swift | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 33a16d25..b1971907 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,57 +85,60 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] + context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - let menuItems: [UIAction] = childActions.compactMap { child in - guard let childUrl = child.data.url, !childUrl.isEmpty else { - return nil - } + if #available(iOS 26.0, *) { + let menuItems: [UIAction] = childActions.compactMap { child in + guard let childUrl = child.data.url, !childUrl.isEmpty else { + return nil + } - let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil + let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil - return UIAction(title: child.data.label, image: childImage) { _ in - context.coordinator.navigate(to: childUrl) + return UIAction(title: child.data.label, image: childImage) { _ in + context.coordinator.navigate(to: childUrl) + } } - } - if !menuItems.isEmpty { - let menu = UIMenu(title: "", children: menuItems) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) - } + if !menuItems.isEmpty { + let menu = UIMenu(title: "", children: menuItems) + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } - continue + continue + } } guard let actionUrl = action.data.url, !actionUrl.isEmpty else { continue } - let actionHandler = UIAction(title: action.data.label, image: image) { _ in - context.coordinator.navigate(to: actionUrl) - } - + // Create button with both image and title when available let button = UIBarButtonItem( title: action.data.label, image: image, - primaryAction: actionHandler, - menu: nil + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) ) button.accessibilityLabel = action.data.label button.accessibilityIdentifier = action.data.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[action.data.id] = actionUrl barButtonItems.append(button) } @@ -183,6 +186,7 @@ struct NativeTopBar: UIViewRepresentable { class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void + var actionUrls: [String: String] = [:] init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState @@ -195,6 +199,16 @@ struct NativeTopBar: UIViewRepresentable { } } + @objc func actionTapped(_ sender: UIBarButtonItem) { + guard let actionId = sender.accessibilityIdentifier, + let url = actionUrls[actionId] else { + return + } + + // Navigate to the URL using the proper navigation callback + onNavigate(url) + } + func navigate(to url: String) { onNavigate(url) } From ea9e33ae1ff201e15d74846209b9af48dfbf87bd Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:04:50 -0300 Subject: [PATCH 10/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index b1971907..7d18611a 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,14 +85,13 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] - context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 26.0, *) { + if #available(iOS 14.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -127,7 +126,7 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - let button = UIBarButtonItem( + let button = TopBarButtonItem( title: action.data.label, image: image, target: context.coordinator, @@ -136,9 +135,7 @@ struct NativeTopBar: UIViewRepresentable { button.accessibilityLabel = action.data.label button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl + button.actionUrl = actionUrl barButtonItems.append(button) } @@ -186,7 +183,6 @@ struct NativeTopBar: UIViewRepresentable { class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void - var actionUrls: [String: String] = [:] init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState @@ -200,8 +196,8 @@ struct NativeTopBar: UIViewRepresentable { } @objc func actionTapped(_ sender: UIBarButtonItem) { - guard let actionId = sender.accessibilityIdentifier, - let url = actionUrls[actionId] else { + guard let button = sender as? TopBarButtonItem, + let url = button.actionUrl else { return } @@ -213,4 +209,8 @@ struct NativeTopBar: UIViewRepresentable { onNavigate(url) } } + + final class TopBarButtonItem: UIBarButtonItem { + var actionUrl: String? + } } From 5168419eeac912901fe1ae3efab62abde975b534 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:08:47 -0300 Subject: [PATCH 11/22] Revert "Update NativeTopBar.swift" This reverts commit ea9e33ae1ff201e15d74846209b9af48dfbf87bd. --- .../NativePHP/NativeUI/NativeTopBar.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 7d18611a..b1971907 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,13 +85,14 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] + context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 14.0, *) { + if #available(iOS 26.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -126,7 +127,7 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - let button = TopBarButtonItem( + let button = UIBarButtonItem( title: action.data.label, image: image, target: context.coordinator, @@ -135,7 +136,9 @@ struct NativeTopBar: UIViewRepresentable { button.accessibilityLabel = action.data.label button.accessibilityIdentifier = action.data.id - button.actionUrl = actionUrl + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[action.data.id] = actionUrl barButtonItems.append(button) } @@ -183,6 +186,7 @@ struct NativeTopBar: UIViewRepresentable { class Coordinator: NSObject, UINavigationBarDelegate { let uiState: NativeUIState let onNavigate: (String) -> Void + var actionUrls: [String: String] = [:] init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState @@ -196,8 +200,8 @@ struct NativeTopBar: UIViewRepresentable { } @objc func actionTapped(_ sender: UIBarButtonItem) { - guard let button = sender as? TopBarButtonItem, - let url = button.actionUrl else { + guard let actionId = sender.accessibilityIdentifier, + let url = actionUrls[actionId] else { return } @@ -209,8 +213,4 @@ struct NativeTopBar: UIViewRepresentable { onNavigate(url) } } - - final class TopBarButtonItem: UIBarButtonItem { - var actionUrl: String? - } } From 07e205db74f65c217de04c7247097e36bcdbcb95 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:11:34 -0300 Subject: [PATCH 12/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index b1971907..f0f6759a 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -92,7 +92,7 @@ struct NativeTopBar: UIViewRepresentable { let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 26.0, *) { + if #available(iOS 14.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -127,19 +127,33 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - let button = UIBarButtonItem( - title: action.data.label, - image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) - ) - - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl - barButtonItems.append(button) + if #available(iOS 14.0, *) { + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: UIAction(title: action.data.label, image: image) { _ in + context.coordinator.navigate(to: actionUrl) + }, + menu: nil + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } else { + let button = UIBarButtonItem( + title: action.data.label, + image: image, + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) + ) + + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[action.data.id] = actionUrl + barButtonItems.append(button) + } } navItem.rightBarButtonItems = barButtonItems From fb878ca30f9d74c0835a8a94d1b89285b8797efa Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:19:21 -0300 Subject: [PATCH 13/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 114 ++++++++---------- 1 file changed, 49 insertions(+), 65 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index f0f6759a..6e453668 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,41 +85,46 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] - context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 14.0, *) { - let menuItems: [UIAction] = childActions.compactMap { child in - guard let childUrl = child.data.url, !childUrl.isEmpty else { - return nil - } - - let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil - - return UIAction(title: child.data.label, image: childImage) { _ in - context.coordinator.navigate(to: childUrl) - } + let menuItems: [UIAction] = childActions.compactMap { child in + guard let childUrl = child.data.url, !childUrl.isEmpty else { + return nil } - if !menuItems.isEmpty { - let menu = UIMenu(title: "", children: menuItems) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) + let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil + + return UIAction( + title: child.data.label, + image: childImage, + identifier: UIAction.Identifier(child.data.id) + ) { _ in + context.coordinator.navigate(to: childUrl) } + } - continue + if !menuItems.isEmpty { + let menu = UIMenu( + title: "", + identifier: UIMenu.Identifier(action.data.id), + children: menuItems + ) + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) } + + continue } guard let actionUrl = action.data.url, !actionUrl.isEmpty else { @@ -127,33 +132,23 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - if #available(iOS 14.0, *) { - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: UIAction(title: action.data.label, image: image) { _ in - context.coordinator.navigate(to: actionUrl) - }, - menu: nil - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) - } else { - let button = UIBarButtonItem( - title: action.data.label, - image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) - ) - - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl - barButtonItems.append(button) + let primaryAction = UIAction( + title: action.data.label, + image: image, + identifier: UIAction.Identifier(action.data.id) + ) { _ in + context.coordinator.navigate(to: actionUrl) } + + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: primaryAction, + menu: nil + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) } navItem.rightBarButtonItems = barButtonItems @@ -202,6 +197,10 @@ struct NativeTopBar: UIViewRepresentable { let onNavigate: (String) -> Void var actionUrls: [String: String] = [:] + init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { + self.uiState = uiState + self.onNavigate = onNavigate + init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState self.onNavigate = onNavigate @@ -211,20 +210,5 @@ struct NativeTopBar: UIViewRepresentable { withAnimation(.easeInOut(duration: 0.3)) { uiState.openSidebar() } - } - - @objc func actionTapped(_ sender: UIBarButtonItem) { - guard let actionId = sender.accessibilityIdentifier, - let url = actionUrls[actionId] else { - return - } - - // Navigate to the URL using the proper navigation callback - onNavigate(url) - } - - func navigate(to url: String) { - onNavigate(url) - } } } From ef87d4e8d79ff069f7558cc7be351c0222cd4aa0 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:28:16 -0300 Subject: [PATCH 14/22] Revert "Update NativeTopBar.swift" This reverts commit fb878ca30f9d74c0835a8a94d1b89285b8797efa. --- .../NativePHP/NativeUI/NativeTopBar.swift | 114 ++++++++++-------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 6e453668..f0f6759a 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -85,46 +85,41 @@ struct NativeTopBar: UIViewRepresentable { // Update right bar buttons (actions) if let actions = topBarData.children, !actions.isEmpty { var barButtonItems: [UIBarButtonItem] = [] + context.coordinator.actionUrls.removeAll() for action in actions { let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil let childActions = action.data.children ?? [] if !childActions.isEmpty { - let menuItems: [UIAction] = childActions.compactMap { child in - guard let childUrl = child.data.url, !childUrl.isEmpty else { - return nil - } + if #available(iOS 14.0, *) { + let menuItems: [UIAction] = childActions.compactMap { child in + guard let childUrl = child.data.url, !childUrl.isEmpty else { + return nil + } - let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil + let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil - return UIAction( - title: child.data.label, - image: childImage, - identifier: UIAction.Identifier(child.data.id) - ) { _ in - context.coordinator.navigate(to: childUrl) + return UIAction(title: child.data.label, image: childImage) { _ in + context.coordinator.navigate(to: childUrl) + } } - } - if !menuItems.isEmpty { - let menu = UIMenu( - title: "", - identifier: UIMenu.Identifier(action.data.id), - children: menuItems - ) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) - } + if !menuItems.isEmpty { + let menu = UIMenu(title: "", children: menuItems) + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } - continue + continue + } } guard let actionUrl = action.data.url, !actionUrl.isEmpty else { @@ -132,23 +127,33 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - let primaryAction = UIAction( - title: action.data.label, - image: image, - identifier: UIAction.Identifier(action.data.id) - ) { _ in - context.coordinator.navigate(to: actionUrl) + if #available(iOS 14.0, *) { + let button = UIBarButtonItem( + title: action.data.label, + image: image, + primaryAction: UIAction(title: action.data.label, image: image) { _ in + context.coordinator.navigate(to: actionUrl) + }, + menu: nil + ) + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + barButtonItems.append(button) + } else { + let button = UIBarButtonItem( + title: action.data.label, + image: image, + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) + ) + + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[action.data.id] = actionUrl + barButtonItems.append(button) } - - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: primaryAction, - menu: nil - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) } navItem.rightBarButtonItems = barButtonItems @@ -197,10 +202,6 @@ struct NativeTopBar: UIViewRepresentable { let onNavigate: (String) -> Void var actionUrls: [String: String] = [:] - init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { - self.uiState = uiState - self.onNavigate = onNavigate - init(uiState: NativeUIState, onNavigate: @escaping (String) -> Void) { self.uiState = uiState self.onNavigate = onNavigate @@ -210,5 +211,20 @@ struct NativeTopBar: UIViewRepresentable { withAnimation(.easeInOut(duration: 0.3)) { uiState.openSidebar() } + } + + @objc func actionTapped(_ sender: UIBarButtonItem) { + guard let actionId = sender.accessibilityIdentifier, + let url = actionUrls[actionId] else { + return + } + + // Navigate to the URL using the proper navigation callback + onNavigate(url) + } + + func navigate(to url: String) { + onNavigate(url) + } } } From f51cff85bea793efe6f5738d03c50d77cb43aff2 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Sat, 30 May 2026 12:28:24 -0300 Subject: [PATCH 15/22] Revert "Update NativeTopBar.swift" This reverts commit 07e205db74f65c217de04c7247097e36bcdbcb95. --- .../NativePHP/NativeUI/NativeTopBar.swift | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index f0f6759a..b1971907 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -92,7 +92,7 @@ struct NativeTopBar: UIViewRepresentable { let childActions = action.data.children ?? [] if !childActions.isEmpty { - if #available(iOS 14.0, *) { + if #available(iOS 26.0, *) { let menuItems: [UIAction] = childActions.compactMap { child in guard let childUrl = child.data.url, !childUrl.isEmpty else { return nil @@ -127,33 +127,19 @@ struct NativeTopBar: UIViewRepresentable { } // Create button with both image and title when available - if #available(iOS 14.0, *) { - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: UIAction(title: action.data.label, image: image) { _ in - context.coordinator.navigate(to: actionUrl) - }, - menu: nil - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) - } else { - let button = UIBarButtonItem( - title: action.data.label, - image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) - ) - - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl - barButtonItems.append(button) - } + let button = UIBarButtonItem( + title: action.data.label, + image: image, + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) + ) + + button.accessibilityLabel = action.data.label + button.accessibilityIdentifier = action.data.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[action.data.id] = actionUrl + barButtonItems.append(button) } navItem.rightBarButtonItems = barButtonItems From e1ecefaf8dc147c81e78a899f947d51ff207925f Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:55:15 -0300 Subject: [PATCH 16/22] new features - top bar section - action role (destructive only) - action subtitle --- .../NativePHP/NativeUI/NativeTopBar.swift | 148 +++++++++++++----- .../NativePHP/NativeUI/NativeUIModels.swift | 58 ++++++- .../Components/Navigation/TopBarAction.php | 4 + .../Components/Navigation/TopBarSection.php | 28 ++++ 4 files changed, 194 insertions(+), 44 deletions(-) create mode 100644 src/Edge/Components/Navigation/TopBarSection.php diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index b1971907..a17b8cb8 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -88,58 +88,53 @@ struct NativeTopBar: UIViewRepresentable { context.coordinator.actionUrls.removeAll() for action in actions { - let image = !action.data.icon.isEmpty ? UIImage(systemName: getIconForName(action.data.icon)) : nil - let childActions = action.data.children ?? [] - - if !childActions.isEmpty { - if #available(iOS 26.0, *) { - let menuItems: [UIAction] = childActions.compactMap { child in - guard let childUrl = child.data.url, !childUrl.isEmpty else { - return nil + switch action.data { + case .action(let actionData): + let image = !actionData.icon.isEmpty ? UIImage(systemName: getIconForName(actionData.icon)) : nil + let childActions = actionData.children ?? [] + + if !childActions.isEmpty { + if #available(iOS 26.0, *) { + let menuElements = buildMenuElements(from: childActions, context: context) + + if !menuElements.isEmpty { + let menu = UIMenu(title: "", children: menuElements) + let button = UIBarButtonItem( + title: actionData.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = actionData.label + button.accessibilityIdentifier = actionData.id + barButtonItems.append(button) } - let childImage = !child.data.icon.isEmpty ? UIImage(systemName: getIconForName(child.data.icon)) : nil - - return UIAction(title: child.data.label, image: childImage) { _ in - context.coordinator.navigate(to: childUrl) - } - } - - if !menuItems.isEmpty { - let menu = UIMenu(title: "", children: menuItems) - let button = UIBarButtonItem( - title: action.data.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - barButtonItems.append(button) + continue } + } + guard let actionUrl = actionData.url, !actionUrl.isEmpty else { continue } - } - guard let actionUrl = action.data.url, !actionUrl.isEmpty else { + // Create button with both image and title when available + let button = UIBarButtonItem( + title: actionData.label, + image: image, + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) + ) + + button.accessibilityLabel = actionData.label + button.accessibilityIdentifier = actionData.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[actionData.id] = actionUrl + barButtonItems.append(button) + default: continue } - - // Create button with both image and title when available - let button = UIBarButtonItem( - title: action.data.label, - image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) - ) - - button.accessibilityLabel = action.data.label - button.accessibilityIdentifier = action.data.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[action.data.id] = actionUrl - barButtonItems.append(button) } navItem.rightBarButtonItems = barButtonItems @@ -214,3 +209,70 @@ struct NativeTopBar: UIViewRepresentable { } } } + +private func buildMenuElements( + from components: [TopBarActionComponent], + context: NativeTopBar.Context +) -> [UIMenuElement] { + var elements: [UIMenuElement] = [] + + for component in components { + switch component.data { + case .divider: + if let separator = menuSeparatorElement() { + elements.append(separator) + } + case .section(let section): + let sectionElements = buildMenuElements(from: section.children ?? [], context: context) + if !sectionElements.isEmpty { + let sectionMenu = UIMenu(title: section.title, options: .displayInline, children: sectionElements) + elements.append(sectionMenu) + } + case .action(let action): + guard let url = action.url, !url.isEmpty else { + continue + } + + let image = !action.icon.isEmpty ? UIImage(systemName: getIconForName(action.icon)) : nil + var attributes: UIMenuElement.Attributes = [] + if action.role == "destructive" { + attributes.insert(.destructive) + } + + let menuAction = UIAction( + title: action.label, + image: image, + identifier: nil, + discoverabilityTitle: nil, + attributes: attributes, + state: .off + ) { _ in + context.coordinator.navigate(to: url) + } + + if #available(iOS 15.0, *), + let subtitle = action.subtitle, + !subtitle.isEmpty { + menuAction.subtitle = subtitle + } + + elements.append(menuAction) + default: + continue + } + } + + return elements +} + +private func menuSeparatorElement() -> UIMenuElement? { + let separatorSelector = NSSelectorFromString("separator") + + guard let menuElementType = NSClassFromString("UIMenuElement") as? NSObject.Type, + menuElementType.responds(to: separatorSelector), + let result = menuElementType.perform(separatorSelector)?.takeUnretainedValue() as? UIMenuElement else { + return nil + } + + return result +} diff --git a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift index f28767a3..b7abc2ab 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift @@ -211,17 +211,73 @@ struct TopBarData: Codable, Equatable { struct TopBarActionComponent: Codable, Equatable { let type: String - let data: TopBarAction + let data: TopBarActionData? + + enum CodingKeys: String, CodingKey { + case type, data + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(String.self, forKey: .type) + + switch type { + case "top_bar_section": + if let section = try? container.decode(TopBarSection.self, forKey: .data) { + data = .section(section) + } else { + data = nil + } + case "horizontal_divider": + data = .divider + case "top_bar_spacer": + data = .spacer + default: + if let action = try? container.decode(TopBarAction.self, forKey: .data) { + data = .action(action) + } else { + data = nil + } + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + + switch data { + case .action(let action): + try container.encode(action, forKey: .data) + case .section(let section): + try container.encode(section, forKey: .data) + case .divider, .spacer, .none: + try container.encodeNil(forKey: .data) + } + } } struct TopBarAction: Codable, Equatable { let id: String let label: String + let subtitle: String? let url: String? let icon: String + let role: String? let children: [TopBarActionComponent]? } +struct TopBarSection: Codable, Equatable { + let title: String + let children: [TopBarActionComponent]? +} + +enum TopBarActionData: Equatable { + case action(TopBarAction) + case section(TopBarSection) + case divider + case spacer +} + // MARK: - NativeUI Parser class NativeUIParser { static func parse(_ jsonString: String) -> [NativeComponent] { diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index dca95e18..76b20e33 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -14,8 +14,10 @@ public function __construct( public ?string $id = null, public ?string $icon = null, public ?string $label = null, + public ?string $subtitle = null, public ?string $url = null, public ?string $event = null, + public ?string $role = null, ) {} protected function requiredProps(): array @@ -29,8 +31,10 @@ protected function toNativeProps(): array 'id' => $this->id, 'icon' => $this->icon, 'label' => $this->label, + 'subtitle' => $this->subtitle, 'url' => $this->url, 'event' => $this->event, + 'role' => $this->role, ]; } } diff --git a/src/Edge/Components/Navigation/TopBarSection.php b/src/Edge/Components/Navigation/TopBarSection.php new file mode 100644 index 00000000..1de35c3e --- /dev/null +++ b/src/Edge/Components/Navigation/TopBarSection.php @@ -0,0 +1,28 @@ + $this->title, + ]; + } +} From 1078077d655fd053eb43cafc774d6202fb071de5 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:00:02 -0300 Subject: [PATCH 17/22] up --- .../NativePHP/NativeUI/NativeTopBar.swift | 18 +----------------- .../NativePHP/NativeUI/NativeUIModels.swift | 8 +------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index a17b8cb8..5c6be60d 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -218,10 +218,6 @@ private func buildMenuElements( for component in components { switch component.data { - case .divider: - if let separator = menuSeparatorElement() { - elements.append(separator) - } case .section(let section): let sectionElements = buildMenuElements(from: section.children ?? [], context: context) if !sectionElements.isEmpty { @@ -263,16 +259,4 @@ private func buildMenuElements( } return elements -} - -private func menuSeparatorElement() -> UIMenuElement? { - let separatorSelector = NSSelectorFromString("separator") - - guard let menuElementType = NSClassFromString("UIMenuElement") as? NSObject.Type, - menuElementType.responds(to: separatorSelector), - let result = menuElementType.perform(separatorSelector)?.takeUnretainedValue() as? UIMenuElement else { - return nil - } - - return result -} +} \ No newline at end of file diff --git a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift index b7abc2ab..007d9e41 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeUIModels.swift @@ -228,10 +228,6 @@ struct TopBarActionComponent: Codable, Equatable { } else { data = nil } - case "horizontal_divider": - data = .divider - case "top_bar_spacer": - data = .spacer default: if let action = try? container.decode(TopBarAction.self, forKey: .data) { data = .action(action) @@ -250,7 +246,7 @@ struct TopBarActionComponent: Codable, Equatable { try container.encode(action, forKey: .data) case .section(let section): try container.encode(section, forKey: .data) - case .divider, .spacer, .none: + case .none: try container.encodeNil(forKey: .data) } } @@ -274,8 +270,6 @@ struct TopBarSection: Codable, Equatable { enum TopBarActionData: Equatable { case action(TopBarAction) case section(TopBarSection) - case divider - case spacer } // MARK: - NativeUI Parser From a322e8b9920599b67058374c7822df2fc223269b Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:02:44 -0300 Subject: [PATCH 18/22] Update NativeTopBar.swift --- .../NativePHP/NativeUI/NativeTopBar.swift | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift index 5c6be60d..e3b10968 100644 --- a/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift +++ b/resources/xcode/NativePHP/NativeUI/NativeTopBar.swift @@ -88,53 +88,52 @@ struct NativeTopBar: UIViewRepresentable { context.coordinator.actionUrls.removeAll() for action in actions { - switch action.data { - case .action(let actionData): - let image = !actionData.icon.isEmpty ? UIImage(systemName: getIconForName(actionData.icon)) : nil - let childActions = actionData.children ?? [] - - if !childActions.isEmpty { - if #available(iOS 26.0, *) { - let menuElements = buildMenuElements(from: childActions, context: context) - - if !menuElements.isEmpty { - let menu = UIMenu(title: "", children: menuElements) - let button = UIBarButtonItem( - title: actionData.label, - image: image, - primaryAction: nil, - menu: menu - ) - button.accessibilityLabel = actionData.label - button.accessibilityIdentifier = actionData.id - barButtonItems.append(button) - } - - continue + guard case let .action(actionData) = action.data else { + continue + } + + let image = !actionData.icon.isEmpty ? UIImage(systemName: getIconForName(actionData.icon)) : nil + let childActions = actionData.children ?? [] + + if !childActions.isEmpty { + if #available(iOS 26.0, *) { + let menuElements = buildMenuElements(from: childActions, context: context) + + if !menuElements.isEmpty { + let menu = UIMenu(title: "", children: menuElements) + let button = UIBarButtonItem( + title: actionData.label, + image: image, + primaryAction: nil, + menu: menu + ) + button.accessibilityLabel = actionData.label + button.accessibilityIdentifier = actionData.id + barButtonItems.append(button) } - } - guard let actionUrl = actionData.url, !actionUrl.isEmpty else { continue } + } - // Create button with both image and title when available - let button = UIBarButtonItem( - title: actionData.label, - image: image, - target: context.coordinator, - action: #selector(Coordinator.actionTapped(_:)) - ) - - button.accessibilityLabel = actionData.label - button.accessibilityIdentifier = actionData.id - - // Store the URL in the button's tag by storing it in coordinator - context.coordinator.actionUrls[actionData.id] = actionUrl - barButtonItems.append(button) - default: + guard let actionUrl = actionData.url, !actionUrl.isEmpty else { continue } + + // Create button with both image and title when available + let button = UIBarButtonItem( + title: actionData.label, + image: image, + target: context.coordinator, + action: #selector(Coordinator.actionTapped(_:)) + ) + + button.accessibilityLabel = actionData.label + button.accessibilityIdentifier = actionData.id + + // Store the URL in the button's tag by storing it in coordinator + context.coordinator.actionUrls[actionData.id] = actionUrl + barButtonItems.append(button) } navItem.rightBarButtonItems = barButtonItems From df85f172bd8be5cba526734d6c31837723129f44 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:38:01 -0300 Subject: [PATCH 19/22] android top-bar-section and action subtitle --- .../com/nativephp/mobile/ui/NativeTopBar.kt | 185 +++++++++++------- .../com/nativephp/mobile/ui/NativeUIModels.kt | 36 ++-- 2 files changed, 130 insertions(+), 91 deletions(-) diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt index 40271271..b908d951 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt @@ -4,7 +4,9 @@ import android.content.Intent import android.net.Uri import android.util.Log import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.* @@ -12,6 +14,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch private const val TAG = "NativeTopBar" @@ -37,8 +40,11 @@ fun NativeTopBar( val data = topBarData!! val backgroundColor = data.backgroundColor?.let { parseColor(it) } val textColor = data.textColor?.let { parseColor(it) } - val actions = data.children?.mapNotNull { it.data } ?: emptyList() - val handleActionClick: (TopBarAction) -> Unit = { action -> + + // Filter out top-level sections as standard action bar icons cannot logically be sections + val topLevelComponents = data.children?.filter { it.type != "top_bar_section" } ?: emptyList() + + val handleActionClick: (TopBarActionData) -> Unit = { action -> Log.d(TAG, "⚡ Action clicked: ${action.label ?: action.id}") action.url?.let { url -> if (isExternalUrl(url)) { @@ -61,8 +67,8 @@ fun NativeTopBar( } // Split actions into visible (max 3) and overflow - val visibleActions = actions.take(3) - val overflowActions = actions.drop(3) + val visibleComponents = topLevelComponents.take(3) + val overflowComponents = topLevelComponents.drop(3) val showOverflowMenu = remember { mutableStateOf(false) } TopAppBar( @@ -103,15 +109,16 @@ fun NativeTopBar( }, actions = { // Render visible actions (max 3) - visibleActions.forEach { action -> - val actionChildren = action.children?.mapNotNull { it.data } ?: emptyList() + visibleComponents.forEach { component -> + val action = component.data + val actionChildren = action.children ?: emptyList() if (actionChildren.isNotEmpty()) { val showActionMenu = remember(action.id) { mutableStateOf(false) } IconButton(onClick = { showActionMenu.value = true }) { MaterialIcon( - name = action.icon, + name = action.icon ?: "menu", contentDescription = action.label ?: action.id, tint = textColor ?: MaterialTheme.colorScheme.onSurface ) @@ -121,26 +128,17 @@ fun NativeTopBar( expanded = showActionMenu.value, onDismissRequest = { showActionMenu.value = false } ) { - actionChildren.forEach { child -> - DropdownMenuItem( - text = { Text(child.label ?: child.id) }, - onClick = { - showActionMenu.value = false - handleActionClick(child) - }, - leadingIcon = { - MaterialIcon( - name = child.icon, - contentDescription = child.label ?: child.id - ) - } - ) - } + BuildMenuElements( + components = actionChildren, + textColor = textColor, + onDismiss = { showActionMenu.value = false }, + handleActionClick = handleActionClick + ) } } else { IconButton(onClick = { handleActionClick(action) }) { MaterialIcon( - name = action.icon, + name = action.icon ?: "error", contentDescription = action.label ?: action.id, tint = textColor ?: MaterialTheme.colorScheme.onSurface ) @@ -149,7 +147,7 @@ fun NativeTopBar( } // Overflow menu if more than 3 actions - if (overflowActions.isNotEmpty()) { + if (overflowComponents.isNotEmpty()) { IconButton(onClick = { showOverflowMenu.value = true }) { MaterialIcon( name = "more_vert", @@ -162,53 +160,12 @@ fun NativeTopBar( expanded = showOverflowMenu.value, onDismissRequest = { showOverflowMenu.value = false } ) { - overflowActions.forEach { action -> - val actionChildren = action.children?.mapNotNull { it.data } ?: emptyList() - - if (actionChildren.isNotEmpty()) { - DropdownMenuItem( - text = { Text(action.label ?: action.id) }, - onClick = {}, - enabled = false, - leadingIcon = { - MaterialIcon( - name = action.icon, - contentDescription = action.label ?: action.id - ) - } - ) - - actionChildren.forEach { child -> - DropdownMenuItem( - text = { Text(child.label ?: child.id) }, - onClick = { - showOverflowMenu.value = false - handleActionClick(child) - }, - leadingIcon = { - MaterialIcon( - name = child.icon, - contentDescription = child.label ?: child.id - ) - } - ) - } - } else { - DropdownMenuItem( - text = { Text(action.label ?: action.id) }, - onClick = { - showOverflowMenu.value = false - handleActionClick(action) - }, - leadingIcon = { - MaterialIcon( - name = action.icon, - contentDescription = action.label ?: action.id - ) - } - ) - } - } + BuildMenuElements( + components = overflowComponents, + textColor = textColor, + onDismiss = { showOverflowMenu.value = false }, + handleActionClick = handleActionClick + ) } } }, @@ -220,6 +177,92 @@ fun NativeTopBar( ) } +/** + * Recursively builds dropdown menu elements handling Sections and Action groupings. + */ +@Composable +private fun ColumnScope.BuildMenuElements( + components: List, + textColor: Color?, + onDismiss: () -> Unit, + handleActionClick: (TopBarActionData) -> Unit +) { + components.forEach { component -> + if (component.type == "top_bar_section") { + val section = component.data + + // Section Title + section.title?.let { title -> + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = (textColor ?: MaterialTheme.colorScheme.onSurface).copy(alpha = 0.6f), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + // Section Children + section.children?.let { children -> + BuildMenuElements(children, textColor, onDismiss, handleActionClick) + } + + // Appends a subtle divider after groups for cleaner structure + Divider(color = (textColor ?: MaterialTheme.colorScheme.onSurface).copy(alpha = 0.1f)) + + } else { + val action = component.data + val hasChildren = !action.children.isNullOrEmpty() + + val iconContent: @Composable (() -> Unit)? = if (!action.icon.isNullOrEmpty()) { + { + MaterialIcon( + name = action.icon, + contentDescription = action.label ?: action.id, + tint = textColor ?: MaterialTheme.colorScheme.onSurface + ) + } + } else null + + if (hasChildren) { + // Render an unclickable header, then recursively append its children. + DropdownMenuItem( + text = { ActionTextContent(action, textColor) }, + onClick = {}, + enabled = false, + leadingIcon = iconContent + ) + BuildMenuElements(action.children!!, textColor, onDismiss, handleActionClick) + } else { + DropdownMenuItem( + text = { ActionTextContent(action, textColor) }, + onClick = { + onDismiss() + handleActionClick(action) + }, + leadingIcon = iconContent + ) + } + } + } +} + +/** + * Text renderer component supporting trailing subtitles natively in the DropdownMenuItem. + */ +@Composable +private fun ActionTextContent(action: TopBarActionData, textColor: Color?) { + Column { + Text(action.label ?: action.id ?: "") + action.subtitle?.let { subtitle -> + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = (textColor ?: MaterialTheme.colorScheme.onSurface).copy(alpha = 0.7f) + ) + } + } +} + /** * Check if a URL is external (not a relative path or localhost) */ diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt index e918b601..ec77cd77 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeUIModels.kt @@ -66,9 +66,6 @@ data class SideNavData( /** * Side nav child component - can be an item, group, or divider - * For items: type="side_nav_item", data contains SideNavItem - * For groups: type="side_nav_group", data contains SideNavGroup - * For dividers: type="horizontal_divider", data is null/empty */ data class SideNavChild( val type: String, @@ -143,22 +140,26 @@ data class TopBarData( ) /** - * Top bar action as a component (wraps TopBarAction data) + * Top bar action as a component (wraps TopBarActionData) */ data class TopBarActionComponent( val type: String, - val data: TopBarAction + val data: TopBarActionData ) /** - * Top bar action item data + * Universal data class to support both standard Actions and nested Sections. + * Properties not relevant to a specific component type (like 'title' for actions) simply decode as null. */ -data class TopBarAction( - val id: String, - val icon: String, +data class TopBarActionData( + val id: String? = null, + val icon: String? = null, val label: String? = null, + val subtitle: String? = null, + val title: String? = null, // Used primarily by sections val url: String? = null, val event: String? = null, + val role: String? = null, // Parsed but safely ignored by UI as requested val children: List? = null ) @@ -170,13 +171,13 @@ data class FabData( val icon: String, val url: String? = null, val event: String? = null, - val size: String? = "regular", // "small", "regular", "large", "extended" - val position: String? = "end", // "end", "center", "start" + val size: String? = "regular", + val position: String? = "end", @SerializedName("bottom_offset") - val bottomOffset: Int? = null, // Offset from bottom in dp - val elevation: Int? = null, // Elevation in dp + val bottomOffset: Int? = null, + val elevation: Int? = null, @SerializedName("corner_radius") - val cornerRadius: Int? = null, // Corner radius in dp (default: circular) + val cornerRadius: Int? = null, @SerializedName("container_color") val containerColor: String? = null, @SerializedName("content_color") @@ -201,21 +202,16 @@ object NativeUIParser { return try { Log.d("NativeUIParser", "parseFromObject called with type: ${obj.javaClass.name}") - // Convert the object to JsonElement - // Handle org.json types (from bridge) separately from Gson types val jsonTree = when (obj) { is JSONArray -> { - // Convert org.json.JSONArray to Gson JsonElement Log.d("NativeUIParser", "Converting JSONArray: ${obj.toString()}") JsonParser.parseString(obj.toString()) } is JSONObject -> { - // Convert org.json.JSONObject to Gson JsonElement Log.d("NativeUIParser", "Converting JSONObject: ${obj.toString()}") JsonParser.parseString(obj.toString()) } else -> { - // For other types, use Gson's toJsonTree Log.d("NativeUIParser", "Using toJsonTree for: ${obj.javaClass.name}") gson.toJsonTree(obj) } @@ -289,4 +285,4 @@ object NativeUIParser { null } } -} +} \ No newline at end of file From 246c3130266395ff8736f1dbd0166978fb36f306 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:43:01 -0300 Subject: [PATCH 20/22] visual bug fix removal of weird empty space in case section title is empty --- .../java/com/nativephp/mobile/ui/NativeTopBar.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt index b908d951..e07291c1 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/NativeTopBar.kt @@ -191,10 +191,10 @@ private fun ColumnScope.BuildMenuElements( if (component.type == "top_bar_section") { val section = component.data - // Section Title - section.title?.let { title -> + // Section Title - FIXED: Now checks for empty or blank strings + if (!section.title.isNullOrBlank()) { Text( - text = title, + text = section.title, style = MaterialTheme.typography.labelMedium, color = (textColor ?: MaterialTheme.colorScheme.onSurface).copy(alpha = 0.6f), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) @@ -213,10 +213,10 @@ private fun ColumnScope.BuildMenuElements( val action = component.data val hasChildren = !action.children.isNullOrEmpty() - val iconContent: @Composable (() -> Unit)? = if (!action.icon.isNullOrEmpty()) { + val iconContent: @Composable (() -> Unit)? = if (!action.icon.isNullOrBlank()) { { MaterialIcon( - name = action.icon, + name = action.icon!!, contentDescription = action.label ?: action.id, tint = textColor ?: MaterialTheme.colorScheme.onSurface ) @@ -266,7 +266,7 @@ private fun ActionTextContent(action: TopBarActionData, textColor: Color?) { /** * Check if a URL is external (not a relative path or localhost) */ -private fun isExternalUrl(url: String): Boolean { +private fun isExternalUrl(url: String): Boolean { return (url.startsWith("http://") || url.startsWith("https://")) && !url.contains("127.0.0.1") && !url.contains("localhost") From bb9bf872716b66917be82f05a5de507aa15d4ed7 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:29:40 -0300 Subject: [PATCH 21/22] Update TopBarAction.php --- src/Edge/Components/Navigation/TopBarAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index 76b20e33..5a64ed7e 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -22,7 +22,7 @@ public function __construct( protected function requiredProps(): array { - return ['id', 'icon', 'label']; + return ['id', 'icon', 'label', 'url']; } protected function toNativeProps(): array From 5610bf091a6adf9b3b01d65a9a53ba18cc117e77 Mon Sep 17 00:00:00 2001 From: Arthur Ydalgo <25585428+ArthurYdalgo@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:30:45 -0300 Subject: [PATCH 22/22] Update TopBarAction.php --- src/Edge/Components/Navigation/TopBarAction.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Edge/Components/Navigation/TopBarAction.php b/src/Edge/Components/Navigation/TopBarAction.php index 5a64ed7e..4979038f 100644 --- a/src/Edge/Components/Navigation/TopBarAction.php +++ b/src/Edge/Components/Navigation/TopBarAction.php @@ -8,8 +8,6 @@ class TopBarAction extends EdgeComponent { protected string $type = 'top_bar_action'; - protected bool $hasChildren = true; - public function __construct( public ?string $id = null, public ?string $icon = null,