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
31 changes: 31 additions & 0 deletions example/widgets/ios/IosWeatherWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,37 @@ export const IosWeatherWidget = ({ weather = DEFAULT_WEATHER }: WeatherWidgetPro
🕒 {formatTime(weather.lastUpdated)}
</Voltra.Text>
) : null}

{/* Track 1 PoC verification
- Background flips with dark/light mode (light-dark)
- Text receives system accent tint in accented rendering mode (widgetAccentable) */}
<Voltra.Spacer />
<Voltra.HStack spacing={6} style={{ marginTop: 8 }}>
<Voltra.View
style={{
backgroundColor: 'light-dark(rgba(0,0,0,0.15), rgba(255,255,255,0.15))',
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
}}
>
<Voltra.Text style={{ fontSize: 9, color: 'light-dark(#000000, #ffffff)' }}>
light-dark
</Voltra.Text>
</Voltra.View>
<Voltra.View
style={{
backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
}}
>
<Voltra.Text widgetAccentable={true} style={{ fontSize: 9, color: '#ffffff' }}>
accentable
</Voltra.Text>
</Voltra.View>
</Voltra.HStack>
</Voltra.VStack>
</Voltra.LinearGradient>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ object ShortNames {
"ul" to "underline",
"v" to "value",
"valig" to "verticalAlignment",
"wa" to "widgetAccentable",
"wt" to "weight",
"w" to "width",
"xgs" to "xAxisGridStyle",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/payload/short-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const NAME_TO_SHORT: Record<string, string> = {
value: 'v',
verticalAlignment: 'valig',
weight: 'wt',
widgetAccentable: 'wa',
width: 'w',
xAxisGridStyle: 'xgs',
xAxisVisibility: 'xav',
Expand Down Expand Up @@ -322,6 +323,7 @@ export const SHORT_TO_NAME: Record<string, string> = {
v: 'value',
valig: 'verticalAlignment',
wt: 'weight',
wa: 'widgetAccentable',
w: 'width',
xgs: 'xAxisGridStyle',
xav: 'xAxisVisibility',
Expand Down
1 change: 1 addition & 0 deletions packages/generator/data/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "1.0.0",
"shortNames": {
"style": "s",
"widgetAccentable": "wa",
"alignment": "al",
"buttonStyle": "bs",
"colors": "cls",
Expand Down
4 changes: 2 additions & 2 deletions packages/ios-client/ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ let package = Package(
.testTarget(
name: "VoltraStyleTests",
dependencies: ["VoltraStyleCore"],
path: "tests",
sources: ["JSGradientParserTests.swift"]
path: "Tests",
sources: ["JSGradientParserTests.swift", "JSColorParserTests.swift"]
),
]
)
118 changes: 118 additions & 0 deletions packages/ios-client/ios/Tests/JSColorParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import SwiftUI
@testable import VoltraStyleCore
import XCTest

final class JSColorParserTests: XCTestCase {
// MARK: - Helpers

private func assertParsed(_ value: String, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertNotNil(JSColorParser.parse(value), "Expected non-nil Color for: \(value)", file: file, line: line)
}

private func assertNil(_ value: String, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertNil(JSColorParser.parse(value), "Expected nil Color for: \(value)", file: file, line: line)
}

// MARK: - Existing formats (smoke)

func testHexColors() {
assertParsed("#fff")
assertParsed("#ffffff")
assertParsed("#ffffffff")
assertParsed("ffffff")
}

func testRGBColors() {
assertParsed("rgb(255, 0, 0)")
assertParsed("rgba(255, 0, 0, 0.5)")
assertParsed("rgb(255 0 0 / 80%)")
assertParsed("rgba(255 0 0 / 0.8)")
}

func testTrailingGarbageRejected() {
assertNil("rgba(255,0,0,0.8)garbage")
assertNil("rgb(255,0)")
assertNil("hsl(120, 100, 50%)")
}

func testReducedPresentationNeutralColors() {
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F9FAFB"))
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#6B7280"))
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("white"))
}

func testReducedPresentationSemanticAccents() {
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#34D399"))
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F87171"))
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("green"))
}

func testHSLColors() {
assertParsed("hsl(120, 100%, 50%)")
assertParsed("hsla(120, 100%, 50%, 0.5)")
assertParsed("hsl(120 100% 50% / 30%)")
}

func testNamedColors() {
assertParsed("red")
assertParsed("blue")
assertParsed("primary")
assertParsed("transparent")
}

// MARK: - light-dark()

func testLightDarkWithHexColors() {
assertParsed("light-dark(#ffffff, #000000)")
assertParsed("light-dark(#fff, #000)")
}

func testLightDarkWithRGBColors() {
assertParsed("light-dark(rgb(255, 255, 255), rgb(0, 0, 0))")
assertParsed("light-dark(rgba(255, 255, 255, 1), rgba(0, 0, 0, 0.9))")
}

func testLightDarkWithHSLColors() {
assertParsed("light-dark(hsl(0, 0%, 100%), hsl(0, 0%, 0%))")
}

func testLightDarkWithNamedColors() {
assertParsed("light-dark(white, black)")
assertParsed("light-dark(primary, secondary)")
}

func testLightDarkMixedFormats() {
assertParsed("light-dark(#ffffff, rgb(0, 0, 0))")
assertParsed("light-dark(white, #1a1a1a)")
}

func testLightDarkWithWhitespace() {
assertParsed("light-dark( #fff , #000 )")
assertParsed("light-dark( white , black )")
}

func testLightDarkReturnsAdaptiveColor() {
// light-dark() must return a non-nil Color — the adaptive UIColor wrapping
// is an implementation detail; what matters is that the result is usable.
let result = JSColorParser.parse("light-dark(#ffffff, #000000)")
XCTAssertNotNil(result)
}

func testLightDarkInvalidLightColor() {
assertNil("light-dark(notacolor, #000000)")
}

func testLightDarkInvalidDarkColor() {
assertNil("light-dark(#ffffff, notacolor)")
}

func testLightDarkMissingArgument() {
assertNil("light-dark(#ffffff)")
assertNil("light-dark()")
}

func testLightDarkNestedCommasInRGB() {
// The top-level comma split must not break on commas inside rgb()
assertParsed("light-dark(rgb(255, 255, 255), rgb(0, 0, 0))")
}
}
32 changes: 0 additions & 32 deletions packages/ios-client/ios/Tests/JSGradientParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,3 @@ final class JSGradientParserTests: XCTestCase {
}
}
}

final class JSColorParserTests: XCTestCase {
func testNamedColorsParse() {
XCTAssertNotNil(JSColorParser.parse("red"))
XCTAssertNotNil(JSColorParser.parse("green"))
XCTAssertNotNil(JSColorParser.parse("blue"))
}

func testRGBSlashSyntaxParses() {
XCTAssertNotNil(JSColorParser.parse("rgb(255 0 0 / 80%)"))
XCTAssertNotNil(JSColorParser.parse("rgba(255 0 0 / 0.8)"))
XCTAssertNotNil(JSColorParser.parse("hsl(240 100% 50% / 30%)"))
}

func testTrailingGarbageRejected() {
XCTAssertNil(JSColorParser.parse("rgba(255,0,0,0.8)garbage"))
XCTAssertNil(JSColorParser.parse("rgb(255,0)"))
XCTAssertNil(JSColorParser.parse("hsl(120, 100, 50%)"))
}

func testReducedPresentationPrimaryColorDetectionTreatsNeutralColorsAsAdaptive() {
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F9FAFB"))
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#6B7280"))
XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("white"))
}

func testReducedPresentationPrimaryColorDetectionPreservesSemanticAccents() {
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#34D399"))
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F87171"))
XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("green"))
}
}
1 change: 1 addition & 0 deletions packages/ios-client/ios/shared/ShortNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public enum ShortNames {
"ul": "underline",
"v": "value",
"valig": "verticalAlignment",
"wa": "widgetAccentable",
"wt": "weight",
"w": "width",
"xgs": "xAxisGridStyle",
Expand Down
103 changes: 54 additions & 49 deletions packages/ios-client/ios/shared/VoltraNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,77 +118,82 @@ struct VoltraElementView: View {
let element: VoltraElement

var body: some View {
switch element.type {
case "Button":
VoltraButton(element)
Group {
switch element.type {
case "Button":
VoltraButton(element)

case "Link":
VoltraLink(element)
case "Link":
VoltraLink(element)

case "VStack":
VoltraVStack(element)
case "VStack":
VoltraVStack(element)

case "HStack":
VoltraHStack(element)
case "HStack":
VoltraHStack(element)

case "View":
VoltraFlexView(element)
case "View":
VoltraFlexView(element)

case "ZStack":
VoltraZStack(element)
case "ZStack":
VoltraZStack(element)

case "Text":
VoltraText(element)
case "Text":
VoltraText(element)

case "Image":
VoltraImage(element)
case "Image":
VoltraImage(element)

case "Symbol":
VoltraSymbol(element)
case "Symbol":
VoltraSymbol(element)

case "Divider":
VoltraDivider(element)
case "Divider":
VoltraDivider(element)

case "Spacer":
VoltraSpacer(element)
case "Spacer":
VoltraSpacer(element)

case "Label":
VoltraLabel(element)
case "Label":
VoltraLabel(element)

case "Toggle":
VoltraToggle(element)
case "Toggle":
VoltraToggle(element)

case "Gauge":
VoltraGauge(element)
case "Gauge":
VoltraGauge(element)

case "LinearProgressView":
VoltraLinearProgressView(element)
case "LinearProgressView":
VoltraLinearProgressView(element)

case "CircularProgressView":
VoltraCircularProgressView(element)
case "CircularProgressView":
VoltraCircularProgressView(element)

case "Timer":
VoltraTimer(element)
case "Timer":
VoltraTimer(element)

case "GroupBox":
VoltraGroupBox(element)
case "GroupBox":
VoltraGroupBox(element)

case "LinearGradient":
VoltraLinearGradient(element)
case "LinearGradient":
VoltraLinearGradient(element)

case "GlassContainer":
VoltraGlassContainer(element)
case "GlassContainer":
VoltraGlassContainer(element)

case "Mask":
VoltraMask(element)
case "Mask":
VoltraMask(element)

case "Chart":
if #available(iOS 16.0, macOS 13.0, *) {
VoltraChart(element)
}
case "Chart":
if #available(iOS 16.0, macOS 13.0, *) {
VoltraChart(element)
}

default:
EmptyView()
default:
EmptyView()
}
}
.voltraIf(element.props?["widgetAccentable"]?.boolValue == true) { view in
view.widgetAccentable()
}
}
}
Loading
Loading