Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions nativephp.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@
"ios_renderer": "NativeUIButtonRenderer",
"self_closing": true
},
{
"type": "sign_in_with_apple_button",
"element": "Native\\Mobile\\UI\\Elements\\SignInWithAppleButton",
"blade": "Native\\Mobile\\UI\\Components\\SignInWithAppleButton",
"android_renderer": "com.nativephp.plugins.native_ui.ui.SignInWithAppleButtonRenderer",
"ios_renderer": "NativeUISignInWithAppleButtonRenderer",
"self_closing": false
},
{
"type": "progress_bar",
"element": "Native\\Mobile\\UI\\Elements\\ProgressBar",
Expand Down
2 changes: 1 addition & 1 deletion resources/android/ListItemRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ object ListItemRenderer {
// `trailing_a11y_label` wins, else humanize the machine icon name
// ("more_vert" -> "more vert") so it's never unlabeled.
val iconButtonDescription = a11yLabel.ifEmpty {
effectiveValue.replace('_', ' ').replace('-', ' ')
effectiveValue.removePrefix(com.nativephp.mobile.ui.CUSTOM_ICON_PREFIX).replace('_', ' ').replace('-', ' ')
}

return {
Expand Down
43 changes: 28 additions & 15 deletions resources/android/NavRenderers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nativephp.mobile.R
import com.nativephp.mobile.ui.CUSTOM_ICON_PREFIX
import com.nativephp.mobile.ui.MaterialIcon
import com.nativephp.mobile.ui.getIconName
import com.nativephp.mobile.ui.nativerender.LocalSafeAreaBottom
import com.nativephp.mobile.ui.nativerender.LocalSafeAreaTop
Expand Down Expand Up @@ -112,21 +114,28 @@ object TopBarRenderer {
val icon = action.props.getString("icon", "more_vert")
val url = action.props.getString("url")
val actionLabel = action.props.getString("label")
Text(
text = getIconName(icon), fontFamily = iconFont, fontSize = 24.sp, color = textColor,
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable(role = Role.Button) {
if (url.isNotEmpty()) {
try { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } catch (_: Exception) {}
} else if (action.onPress != 0) {
NativeElementBridge.sendPressEvent(action.onPress, action.id)
}
}
.let { m ->
if (actionLabel.isNotEmpty()) m.semantics { contentDescription = actionLabel } else m
val actionModifier = Modifier
.minimumInteractiveComponentSize()
.clickable(role = Role.Button) {
if (url.isNotEmpty()) {
try { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } catch (_: Exception) {}
} else if (action.onPress != 0) {
NativeElementBridge.sendPressEvent(action.onPress, action.id)
}
)
}
.let { m ->
if (actionLabel.isNotEmpty()) m.semantics { contentDescription = actionLabel } else m
}
// Custom SVG icons are drawables, not font ligatures, so they
// go through core's MaterialIcon. Font glyphs stay as they were.
if (icon.startsWith(CUSTOM_ICON_PREFIX)) {
MaterialIcon(name = icon, contentDescription = null, modifier = actionModifier, size = 24.dp, tint = textColor)
} else {
Text(
text = getIconName(icon), fontFamily = iconFont, fontSize = 24.sp, color = textColor,
modifier = actionModifier
)
}
}
}
}
Expand Down Expand Up @@ -211,7 +220,11 @@ object BottomNavRenderer {
// Box wraps the icon at center; badge / news dot anchored
// to the icon's TopEnd corner via `.align(TopEnd)`.
Box(contentAlignment = Alignment.Center) {
Text(text = getIconName(icon), fontFamily = iconFont, fontSize = 24.sp, color = itemColor, textAlign = TextAlign.Center)
if (icon.startsWith(CUSTOM_ICON_PREFIX)) {
MaterialIcon(name = icon, contentDescription = null, size = 24.dp, tint = itemColor)
} else {
Text(text = getIconName(icon), fontFamily = iconFont, fontSize = 24.sp, color = itemColor, textAlign = TextAlign.Center)
}
if (badge.isNotEmpty()) {
Box(
modifier = Modifier
Expand Down
20 changes: 20 additions & 0 deletions resources/android/SignInWithAppleButtonRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.nativephp.plugins.native_ui.ui

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.nativephp.mobile.ui.nativerender.DefaultContainerNode
import com.nativephp.mobile.ui.nativerender.NativeUINode

/**
* Sign in with Apple has no Android button, so this draws nothing unless the
* tag has children, which render as an Android-only fallback (laid out like a
* column). The iOS renderer draws Apple's button and ignores the children.
*/
object SignInWithAppleButtonRenderer {
@Composable
fun Render(node: NativeUINode, modifier: Modifier) {
if (node.children.isNotEmpty()) {
DefaultContainerNode(node, modifier)
}
}
}
14 changes: 14 additions & 0 deletions resources/boost/guidelines/core.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@
- In tests, drive pickers with the `pickDate()` / `pickTime()` /
`pickDateTime()` / `clearPicker()` macros and assert with
`assertPicker()` / `assertPickerValue()` / `assertPickerEmpty()`.
- For Sign in with Apple, use `<native:sign-in-with-apple-button
:nonce="$nonce" />`, never a `<native:button>` with an Apple logo: iOS draws
SwiftUI's `SignInWithAppleButton`, which is what App Review expects. Tapping
it runs the Apple ID request itself, so don't also start one from a plugin.
Listen for the result with `#[On(SignInWithAppleCompleted::class)]` and
`#[On(SignInWithAppleFailed::class)]` (`Native\Mobile\UI\Events`). Pass a
fresh `nonce` per attempt, the SHA-256 hex of a raw nonce you keep in memory
(never the raw value): it goes to Apple as given and the completed event
echoes it back. `scopes` is any of `email` / `full-name`. `label`
is `sign-in` (default) / `continue` / `sign-up` (NOT free text), and
`button-style` is `auto` (default, black in light mode and white in dark) /
`black` / `white` / `white-outline`. The app still needs the Sign in with
Apple entitlement from a plugin. Apple has no button on Android, so nothing
renders there unless you give the tag children as a fallback.

@verbatim
<code-snippet name="Declaring native elements in Blade" lang="blade">
Expand Down
6 changes: 3 additions & 3 deletions resources/ios/NativeUIButtonRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ private struct ButtonContent: View {
}
} else {
if !icon.isEmpty {
Image(systemName: getIconForName(icon))
NativeIconView(icon, pointSize: iconSize, relativeTo: .body)
.nuiScaledFont(size: iconSize)
// Pin the layout height to iconSize. SF Symbols report
// per-symbol intrinsic heights at the same point size,
Expand All @@ -411,7 +411,7 @@ private struct ButtonContent: View {
Text(label).nuiScaledFont(size: textSize, weight: .medium, fontName: fontName).lineSpacing(lineSpacing)
}
if !iconTrailing.isEmpty {
Image(systemName: getIconForName(iconTrailing))
NativeIconView(iconTrailing, pointSize: iconSize, relativeTo: .body)
.nuiScaledFont(size: iconSize)
.frame(height: iconSize)
}
Expand Down Expand Up @@ -465,7 +465,7 @@ private func buttonMenuItem(_ item: NativeUINode) -> some View {
}
} label: {
if !icon.isEmpty {
Label(label, systemImage: getIconForName(icon))
Label { Text(label) } icon: { NativeIcon.image(icon) }
} else {
Text(label)
}
Expand Down
2 changes: 1 addition & 1 deletion resources/ios/NativeUIChipRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct NativeUIChipRenderer: View {
}) {
HStack(spacing: 6) {
if !iconName.isEmpty {
Image(systemName: getIconForName(iconName))
NativeIconView(iconName, pointSize: 14, relativeTo: .body)
.nuiScaledFont(size: 14)
}
Text(label).nuiScaledFont(size: theme.fontSm, weight: .medium)
Expand Down
4 changes: 2 additions & 2 deletions resources/ios/NativeUIFilledTextInputRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct NativeUIFilledTextInputRenderer: View {
VStack(spacing: 0) {
HStack(spacing: 8) {
if !leadingIcon.isEmpty {
Image(systemName: getIconForName(leadingIcon))
NativeIconView(leadingIcon, pointSize: metrics.iconSize, relativeTo: .body)
.nuiScaledFont(size: metrics.iconSize)
.foregroundStyle(theme.onSurfaceVariant)
}
Expand All @@ -95,7 +95,7 @@ struct NativeUIFilledTextInputRenderer: View {
if loading {
ProgressView().controlSize(.small)
} else if !trailingIcon.isEmpty {
Image(systemName: getIconForName(trailingIcon))
NativeIconView(trailingIcon, pointSize: metrics.iconSize, relativeTo: .body)
.nuiScaledFont(size: metrics.iconSize)
.foregroundStyle(theme.onSurfaceVariant)
}
Expand Down
2 changes: 1 addition & 1 deletion resources/ios/NativeUIIconRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct NativeUIIconRenderer: View {
? darkArgb
: lightArgb

Image(systemName: getIconForName(name))
NativeIcon.image(name)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
Expand Down
16 changes: 8 additions & 8 deletions resources/ios/NativeUIListItemRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,13 @@ struct NativeUIListItemRenderer: View {
if iconBgColor != 0 {
ZStack {
Circle().fill(Color(argb: iconBgColor))
Image(systemName: getIconForName(value))
NativeIconView(value, pointSize: 18, relativeTo: .body)
.nuiScaledFont(size: 18, weight: .medium)
.foregroundColor(.white)
}
.frame(width: 40, height: 40)
} else {
Image(systemName: getIconForName(value))
NativeIcon.image(value)
.frame(width: 24, height: 24)
.foregroundColor(.secondary)
}
Expand Down Expand Up @@ -225,7 +225,7 @@ struct NativeUIListItemRenderer: View {
if !badges.isEmpty {
HStack(spacing: 6) {
ForEach(badges) { b in
Image(systemName: getIconForName(b.icon))
NativeIconView(b.icon, pointSize: 15, relativeTo: .body)
.nuiScaledFont(size: 15)
.foregroundColor(parseListItemHex(b.color) ?? .secondary)
}
Expand All @@ -239,7 +239,7 @@ struct NativeUIListItemRenderer: View {
private func buildTrailingContent(type: String, value: String, iconColor: Int, textColor: Int, checked: Bool = false, changeCb: Int = 0) -> some View {
switch type {
case "icon":
Image(systemName: getIconForName(value))
NativeIcon.image(value)
.frame(width: 24, height: 24)
.foregroundColor(iconColor != 0 ? Color(argb: iconColor) : .secondary)
case "text":
Expand All @@ -252,7 +252,7 @@ struct NativeUIListItemRenderer: View {
let trailingA11y = node.props.getString("trailing_a11y_label")
let effectiveTrailingA11y = !trailingA11y.isEmpty
? trailingA11y
: value.replacingOccurrences(of: "_", with: " ")
: (NativeIcon.customName(value) ?? value).replacingOccurrences(of: "_", with: " ")
.replacingOccurrences(of: "-", with: " ")
// When the row has `:trailing-menu` attached, the trailing
// icon button becomes a Menu trigger instead of a plain
Expand All @@ -271,7 +271,7 @@ struct NativeUIListItemRenderer: View {
// without it only the symbol's opaque pixels respond,
// which makes the ellipsis glyph (mostly empty space)
// almost un-tappable.
Image(systemName: getIconForName(value))
NativeIcon.image(value)
.frame(width: 24, height: 24)
.foregroundColor(iconColor != 0 ? Color(argb: iconColor) : .secondary)
.nuiMinTapTarget()
Expand All @@ -289,7 +289,7 @@ struct NativeUIListItemRenderer: View {
}
}
}) {
Image(systemName: getIconForName(value))
NativeIcon.image(value)
.frame(width: 24, height: 24)
.foregroundColor(iconColor != 0 ? Color(argb: iconColor) : .secondary)
.nuiMinTapTarget()
Expand Down Expand Up @@ -331,7 +331,7 @@ private func listItemMenuItem(_ item: NativeUINode) -> some View {
}
} label: {
if !icon.isEmpty {
Label(label, systemImage: getIconForName(icon))
Label { Text(label) } icon: { NativeIcon.image(icon) }
} else {
Text(label)
}
Expand Down
6 changes: 5 additions & 1 deletion resources/ios/NativeUIListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ struct NativeUIListRenderer: View {
let button = Button(role: role) {
NativeElementBridge.sendPressEvent(spec.cb, nodeId: nodeId)
} label: {
if !spec.icon.isEmpty {
if spec.icon.hasPrefix(NativeIcon.customPrefix) {
// Swipe icons are raw SF Symbol names (no getIconForName
// mapping), so only custom icons take the NativeIcon path.
Label { Text(spec.label.isEmpty ? " " : spec.label) } icon: { NativeIcon.image(spec.icon) }
} else if !spec.icon.isEmpty {
Label(spec.label.isEmpty ? " " : spec.label, systemImage: spec.icon)
} else {
Text(spec.label.isEmpty ? " " : spec.label)
Expand Down
6 changes: 3 additions & 3 deletions resources/ios/NativeUINavRenderers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ struct NativeUITopBarRenderer: View {
? a11y
: (!actionLabel.isEmpty
? actionLabel
: icon.replacingOccurrences(of: "_", with: " ")
: (NativeIcon.customName(icon) ?? icon).replacingOccurrences(of: "_", with: " ")
.replacingOccurrences(of: "-", with: " "))
Button {
if action.onPress != 0 {
NativeElementBridge.sendPressEvent(action.onPress, nodeId: action.id)
}
} label: {
Image(systemName: getIconForName(icon))
NativeIconView(icon, pointSize: 17, relativeTo: .body)
.nuiScaledFont(size: 17, weight: .semibold)
.foregroundColor(textColor)
.frame(width: 32, height: 32)
Expand Down Expand Up @@ -166,7 +166,7 @@ struct NativeUIBottomNavRenderer: View {
} label: {
VStack(spacing: 4) {
ZStack(alignment: .topTrailing) {
Image(systemName: getIconForName(icon))
NativeIconView(icon, pointSize: 24, relativeTo: .body)
.nuiScaledFont(size: 24)
if !badge.isEmpty {
Text(badge)
Expand Down
4 changes: 2 additions & 2 deletions resources/ios/NativeUIOutlinedTextInputRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ struct NativeUIOutlinedTextInputRenderer: View {

HStack(spacing: 8) {
if !leadingIcon.isEmpty {
Image(systemName: getIconForName(leadingIcon))
NativeIconView(leadingIcon, pointSize: metrics.iconSize, relativeTo: .body)
.nuiScaledFont(size: metrics.iconSize)
.foregroundStyle(fieldDecorationColor)
}
Expand Down Expand Up @@ -128,7 +128,7 @@ struct NativeUIOutlinedTextInputRenderer: View {
if loading {
ProgressView().controlSize(.small)
} else if !trailingIcon.isEmpty {
Image(systemName: getIconForName(trailingIcon))
NativeIconView(trailingIcon, pointSize: metrics.iconSize, relativeTo: .body)
.nuiScaledFont(size: metrics.iconSize)
.foregroundStyle(fieldDecorationColor)
}
Expand Down
Loading
Loading