Skip to content
Merged
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
1 change: 1 addition & 0 deletions android/src/main/java/voltra/generated/ShortNames.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ object ShortNames {
"cvl" to "currentValueLabel",
"dlu" to "deepLinkUrl",
"dv" to "defaultValue",
"dest" to "destination",
"dir" to "direction",
"dth" to "dither",
"dur" to "durationMs",
Expand Down
14 changes: 14 additions & 0 deletions data/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"enabled": "en",
"id": "id",
"deepLinkUrl": "dlu",
"destination": "dest",
"padding": "pad",
"text": "txt",
"paddingVertical": "pv",
Expand Down Expand Up @@ -1172,6 +1173,19 @@
"description": "Text color"
}
}
},
{
"name": "Link",
"description": "Navigable link that opens a URL when tapped",
"swiftAvailability": "iOS 14.0, macOS 11.0",
"hasChildren": true,
"parameters": {
"destination": {
"type": "string",
"optional": false,
"description": "URL to navigate to when the link is tapped"
}
}
}
]
}
51 changes: 51 additions & 0 deletions example/components/live-activities/DeepLinksLiveActivityUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import { Voltra } from 'voltra'

export function DeepLinksLiveActivityUI() {
return (
<Voltra.HStack id="deep-links-live-activity" spacing={8} style={{ padding: 16 }} alignment="top">
{/* Link Examples */}
<Voltra.VStack spacing={10} style={{ flex: 1 }}>
{/* Link with absolute URL */}
<Voltra.Link destination="myapp://orders/123">
<Voltra.HStack
spacing={8}
style={{
padding: 12,
backgroundColor: '#1E293B',
borderRadius: 10,
}}
>
<Voltra.Symbol name="bag.fill" tintColor="#F59E0B" size={20} />
<Voltra.VStack spacing={2} alignment="leading">
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 14, fontWeight: '600' }}>Order #123</Voltra.Text>
<Voltra.Text style={{ color: '#94A3B8', fontSize: 11 }}>Tap to view details</Voltra.Text>
</Voltra.VStack>
<Voltra.Spacer />
<Voltra.Symbol name="chevron.right" tintColor="#64748B" size={14} />
</Voltra.HStack>
</Voltra.Link>

{/* Link with relative path */}
<Voltra.Link destination="/settings">
<Voltra.HStack
spacing={8}
style={{
padding: 12,
backgroundColor: '#1E293B',
borderRadius: 10,
}}
>
<Voltra.Symbol name="gearshape.fill" tintColor="#8B5CF6" size={20} />
<Voltra.VStack spacing={2} alignment="leading">
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 14, fontWeight: '600' }}>Settings</Voltra.Text>
<Voltra.Text style={{ color: '#94A3B8', fontSize: 11 }}>Manage preferences</Voltra.Text>
</Voltra.VStack>
<Voltra.Spacer />
<Voltra.Symbol name="chevron.right" tintColor="#64748B" size={14} />
</Voltra.HStack>
</Voltra.Link>
</Voltra.VStack>
</Voltra.HStack>
)
}
42 changes: 42 additions & 0 deletions example/screens/live-activities/DeepLinksLiveActivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
import { useLiveActivity } from 'voltra/client'

import { DeepLinksLiveActivityUI } from '../../components/live-activities/DeepLinksLiveActivityUI'
import { LiveActivityExampleComponent } from './types'

const DeepLinksLiveActivity: LiveActivityExampleComponent = forwardRef(
({ autoUpdate = true, autoStart = false, onIsActiveChange }, ref) => {
const { start, update, end, isActive } = useLiveActivity(
{
lockScreen: {
content: <DeepLinksLiveActivityUI />,
},
island: {
keylineTint: '#3B82F6',
},
},
{
activityName: 'deep-links',
autoUpdate,
autoStart,
deepLinkUrl: '/voltraui/deep-links',
}
)

useEffect(() => {
onIsActiveChange?.(isActive)
}, [isActive, onIsActiveChange])

useImperativeHandle(ref, () => ({
start,
update,
end,
}))

return null
}
)

DeepLinksLiveActivity.displayName = 'DeepLinksLiveActivity'

export default DeepLinksLiveActivity
24 changes: 23 additions & 1 deletion example/screens/live-activities/LiveActivitiesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Card } from '~/components/Card'
import { NotificationsCard } from '~/components/NotificationsCard'
import BasicLiveActivity from '~/screens/live-activities/BasicLiveActivity'
import CompassLiveActivity from '~/screens/live-activities/CompassLiveActivity'
import DeepLinksLiveActivity from '~/screens/live-activities/DeepLinksLiveActivity'
import FlightLiveActivity from '~/screens/live-activities/FlightLiveActivity'
import LiquidGlassLiveActivity from '~/screens/live-activities/LiquidGlassLiveActivity'
import MusicPlayerLiveActivity from '~/screens/live-activities/MusicPlayerLiveActivity'
Expand All @@ -16,7 +17,15 @@ import WorkoutLiveActivity from '~/screens/live-activities/WorkoutLiveActivity'

import { LiveActivityExampleComponentRef } from './types'

type ActivityKey = 'basic' | 'stylesheet' | 'glass' | 'flight' | 'workout' | 'compass' | 'supplementalFamilies'
type ActivityKey =
| 'basic'
| 'stylesheet'
| 'glass'
| 'deepLinks'
| 'flight'
| 'workout'
| 'compass'
| 'supplementalFamilies'

const ACTIVITY_METADATA: Record<ActivityKey, { title: string; description: string }> = {
basic: {
Expand All @@ -31,6 +40,10 @@ const ACTIVITY_METADATA: Record<ActivityKey, { title: string; description: strin
title: 'Liquid Glass',
description: 'GlassContainer + VStack with glassEffect style property.',
},
deepLinks: {
title: 'Links & Navigation',
description: 'Link component for URL navigation. Supports absolute/relative URLs.',
},
flight: {
title: 'Flight Tracker',
description: 'Flight information widget with departure/arrival times, gate info, and status updates.',
Expand All @@ -54,6 +67,7 @@ const CARD_ORDER: ActivityKey[] = [
'basic',
'stylesheet',
'glass',
'deepLinks',
'flight',
'workout',
'compass',
Expand All @@ -65,6 +79,7 @@ export default function LiveActivitiesScreen() {
basic: false,
stylesheet: false,
glass: false,
deepLinks: false,
flight: false,
workout: false,
compass: false,
Expand All @@ -75,6 +90,7 @@ export default function LiveActivitiesScreen() {
const basicRef = useRef<LiveActivityExampleComponentRef>(null)
const stylesheetRef = useRef<LiveActivityExampleComponentRef>(null)
const glassRef = useRef<LiveActivityExampleComponentRef>(null)
const deepLinksRef = useRef<LiveActivityExampleComponentRef>(null)
const flightRef = useRef<LiveActivityExampleComponentRef>(null)
const workoutRef = useRef<LiveActivityExampleComponentRef>(null)
const compassRef = useRef<LiveActivityExampleComponentRef>(null)
Expand All @@ -86,6 +102,7 @@ export default function LiveActivitiesScreen() {
basic: basicRef,
stylesheet: stylesheetRef,
glass: glassRef,
deepLinks: deepLinksRef,
flight: flightRef,
workout: workoutRef,
compass: compassRef,
Expand Down Expand Up @@ -113,6 +130,10 @@ export default function LiveActivitiesScreen() {
(isActive: boolean) => handleStatusChange('glass', isActive),
[handleStatusChange]
)
const handleDeepLinksStatusChange = useCallback(
(isActive: boolean) => handleStatusChange('deepLinks', isActive),
[handleStatusChange]
)
const handleFlightStatusChange = useCallback(
(isActive: boolean) => handleStatusChange('flight', isActive),
[handleStatusChange]
Expand Down Expand Up @@ -201,6 +222,7 @@ export default function LiveActivitiesScreen() {
<BasicLiveActivity ref={basicRef} onIsActiveChange={handleBasicStatusChange} />
<MusicPlayerLiveActivity ref={stylesheetRef} onIsActiveChange={handleStylesheetStatusChange} />
<LiquidGlassLiveActivity ref={glassRef} onIsActiveChange={handleGlassStatusChange} />
<DeepLinksLiveActivity ref={deepLinksRef} onIsActiveChange={handleDeepLinksStatusChange} />
<FlightLiveActivity ref={flightRef} onIsActiveChange={handleFlightStatusChange} />
<WorkoutLiveActivity ref={workoutRef} onIsActiveChange={handleWorkoutStatusChange} />
<CompassLiveActivity ref={compassRef} onIsActiveChange={handleCompassStatusChange} />
Expand Down
4 changes: 4 additions & 0 deletions ios/shared/ComponentTypeID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum ComponentTypeID: Int, Codable {
case SPACER = 16
case DIVIDER = 17
case MASK = 18
case LINK = 19

/// Get the component name string for this ID
public var componentName: String {
Expand Down Expand Up @@ -71,6 +72,8 @@ public enum ComponentTypeID: Int, Codable {
return "Divider"
case .MASK:
return "Mask"
case .LINK:
return "Link"
}
}

Expand Down Expand Up @@ -98,6 +101,7 @@ public enum ComponentTypeID: Int, Codable {
case "Spacer": self = .SPACER
case "Divider": self = .DIVIDER
case "Mask": self = .MASK
case "Link": self = .LINK
default:
return nil
}
Expand Down
1 change: 1 addition & 0 deletions ios/shared/ShortNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public enum ShortNames {
"cvl": "currentValueLabel",
"dlu": "deepLinkUrl",
"dv": "defaultValue",
"dest": "destination",
"dir": "direction",
"dth": "dither",
"dur": "durationMs",
Expand Down
3 changes: 3 additions & 0 deletions ios/shared/VoltraNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ struct VoltraElementView: View {
case "Button":
VoltraButton(element)

case "Link":
VoltraLink(element)

case "VStack":
VoltraVStack(element)

Expand Down
15 changes: 15 additions & 0 deletions ios/ui/Generated/Parameters/LinkParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// LinkParameters.swift

// AUTO-GENERATED from data/components.json
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0

import Foundation

/// Parameters for Link component
/// Navigable link that opens a URL when tapped
public struct LinkParameters: ComponentParameters {
/// URL to navigate to when the link is tapped
public let destination: String?
}
24 changes: 19 additions & 5 deletions ios/ui/Helpers/VoltraDeepLinkResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@ enum VoltraDeepLinkResolver {
_ attributes: VoltraAttributes
) -> URL? {
if let raw = attributes.deepLinkUrl, !raw.isEmpty {
if raw.contains("://"), let url = URL(string: raw) { return url }
if let scheme = deepLinkScheme() {
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
}
return resolveUrl(raw)
}
return nil
}

/// Resolves a URL string, supporting both absolute and relative paths
/// - Parameter raw: The URL string (e.g., "myapp://path", "/path", or "path")
/// - Returns: A resolved URL, or nil if invalid
static func resolveUrl(_ raw: String) -> URL? {
guard !raw.isEmpty else { return nil }

// If it's already an absolute URL, use it as-is
if raw.contains("://"), let url = URL(string: raw) { return url }

// Otherwise, prepend the app's URL scheme
if let scheme = deepLinkScheme() {
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
}

return nil
}
}
25 changes: 25 additions & 0 deletions ios/ui/Views/VoltraLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

public struct VoltraLink: VoltraView {
public typealias Parameters = LinkParameters
public let element: VoltraElement

public init(_ element: VoltraElement) {
self.element = element
}

public var body: some View {
if let urlString = params.destination,
let url = VoltraDeepLinkResolver.resolveUrl(urlString)
{
Link(destination: url) {
element.children ?? .empty
}
.applyStyle(element.style)
} else {
// Fallback: render children without link if destination is invalid
(element.children ?? .empty)
.applyStyle(element.style)
}
}
}
5 changes: 5 additions & 0 deletions src/jsx/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
import type { LinkProps } from './props/Link.js'

export type { LinkProps }
export const Link = createVoltraComponent<LinkProps>('Link')
1 change: 1 addition & 0 deletions src/jsx/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './Image.js'
export * from './Label.js'
export * from './LinearGradient.js'
export * from './LinearProgressView.js'
export * from './Link.js'
export * from './Mask.js'
export * from './Spacer.js'
export * from './Symbol.js'
Expand Down
10 changes: 10 additions & 0 deletions src/jsx/props/Link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 🤖 AUTO-GENERATED from data/components.json
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0

import type { VoltraBaseProps } from '../baseProps'

export type LinkProps = VoltraBaseProps & {
/** URL to navigate to when the link is tapped */
destination: string
}
2 changes: 2 additions & 0 deletions src/payload/component-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const COMPONENT_NAME_TO_ID: Record<string, number> = {
Spacer: 16,
Divider: 17,
Mask: 18,
Link: 19,
}

/**
Expand All @@ -52,6 +53,7 @@ export const COMPONENT_ID_TO_NAME: Record<number, string> = {
16: 'Spacer',
17: 'Divider',
18: 'Mask',
19: 'Link',
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/payload/short-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const NAME_TO_SHORT: Record<string, string> = {
currentValueLabel: 'cvl',
deepLinkUrl: 'dlu',
defaultValue: 'dv',
destination: 'dest',
direction: 'dir',
dither: 'dth',
durationMs: 'dur',
Expand Down Expand Up @@ -181,6 +182,7 @@ export const SHORT_TO_NAME: Record<string, string> = {
cvl: 'currentValueLabel',
dlu: 'deepLinkUrl',
dv: 'defaultValue',
dest: 'destination',
dir: 'direction',
dth: 'dither',
dur: 'durationMs',
Expand Down
Loading
Loading