From f481cda86cec93ea6151e7cec0aa98bb51e19cc0 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 24 Sep 2026 09:57:52 -0400 Subject: [PATCH 1/2] feat: add SwiftUI Status, Notice and Loading --- README.md | 4 +- brand-provenance.json | 8 +-- package.json | 2 +- .../kotlin/com/origin89/ui/GeneratedTokens.kt | 5 +- .../Sources/Origin89UI/GeneratedTokens.swift | 6 +- .../swiftui/Sources/Origin89UI/Loading.swift | 49 ++++++++++++++ .../swiftui/Sources/Origin89UI/Notice.swift | 66 +++++++++++++++++++ .../swiftui/Sources/Origin89UI/Status.swift | 66 +++++++++++++++++++ .../swiftui/Sources/Origin89UI/Theme.swift | 22 +++++++ .../Origin89UITests/ComponentTests.swift | 49 ++++++++++++++ .../Tests/Origin89UITests/ThemeTests.swift | 2 +- pnpm-lock.yaml | 10 +-- scripts/generate-brand.mjs | 2 +- 13 files changed, 272 insertions(+), 19 deletions(-) create mode 100644 platforms/swiftui/Sources/Origin89UI/Loading.swift create mode 100644 platforms/swiftui/Sources/Origin89UI/Notice.swift create mode 100644 platforms/swiftui/Sources/Origin89UI/Status.swift create mode 100644 platforms/swiftui/Tests/Origin89UITests/ComponentTests.swift diff --git a/README.md b/README.md index a54bb77..271fee2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Shared interface foundations with native implementations for React, SwiftUI and | Platform | Package | Available base | | --- | --- | --- | | Web | `@origin89/ui-react` | Reading, equipment illustrations, styles | -| Apple | Swift package `Origin89UI` | Native Reading, dynamic type, fonts and light/dark tokens | +| Apple | Swift package `Origin89UI` | Native Reading, Status, Notice, Loading, dynamic type, fonts and light/dark tokens | | Android | `com.origin89:ui-compose` | Native Compose Reading, fonts and light/dark tokens | All three distinguish current, stale and unknown values. Zero is a valid measurement; non-finite values display as unknown. Shared JSON examples exercise these states across platforms. This is a usable foundation, not a complete mobile application or a shared screen runtime. @@ -27,7 +27,7 @@ React: install `@origin89/ui-react` from npm. Import `Reading` or `EquipmentIllu ``` -Swift: add `https://github.com/origin89hq/ui` as a Swift package dependency with an exact version such as `0.2.0`, link `Origin89UI`, then use `Origin89Reading`. Brand colours follow light and dark mode through `.foregroundStyle(.origin89.muted)` or `Color.origin89.surface`; fixed palettes stay available as `Origin89Tokens.light` and `.dark`. Text styles such as `.font(.origin89Label)` register the bundled fonts. Native previews include current, zero, stale and unknown examples. +Swift: add `https://github.com/origin89hq/ui` as a Swift package dependency with an exact version such as `0.2.0`, link `Origin89UI`, then use `Origin89Reading`, `Origin89Status`, `Origin89Notice` or `Origin89Loading`. Brand colours follow light and dark mode through `.foregroundStyle(.origin89.muted)` or `Color.origin89.surface`; fixed palettes stay available as `Origin89Tokens.light` and `.dark`. Text styles such as `.font(.origin89Label)` register the bundled fonts. Native previews include current, zero, stale and unknown examples. Android: the `platforms/compose` Gradle project builds an Android library. Run `./gradlew :ui:publishToMavenLocal` there, add `mavenLocal()` to a consuming development project and depend on `com.origin89:ui-compose:0.1.0`. Release AAR and source artifacts are also attached to GitHub releases; Maven Central publication is not configured yet. diff --git a/brand-provenance.json b/brand-provenance.json index f62a850..9588cee 100644 --- a/brand-provenance.json +++ b/brand-provenance.json @@ -1,10 +1,10 @@ { "package": "@origin89/brand", - "version": "0.3.1", + "version": "0.4.0", "sources": [ { "file": "brand.json", - "sha256": "5838f019d20b187cdf344f5a49b46633056813006b9a38aeea2187a524e9cbe2" + "sha256": "64b268aa10e8c5da46bb1a10c5064b5178a01900afa00617c8dabfe838ce999f" }, { "file": "fonts/InterTight-400.ttf", @@ -30,11 +30,11 @@ "outputs": [ { "file": "platforms/swiftui/Sources/Origin89UI/GeneratedTokens.swift", - "sha256": "6eada2bbe7fe83ed219d450d175457c7272d8b57068b59a9e45c882d820e1ed9" + "sha256": "80f37f82ee9d7702ae8ac6399da544710ad1e13c45a360fc1d5d1788b6b764d9" }, { "file": "platforms/compose/ui/src/main/kotlin/com/origin89/ui/GeneratedTokens.kt", - "sha256": "4d589f53b7b4994327267add9198cc616dcf97a01e6ba42c58b883e13acb5501" + "sha256": "429a3727bb1a6974fbb124f403f57d5ef627569d6ab85f15558d53a2e977bac6" }, { "file": "platforms/swiftui/Sources/Origin89UI/Resources/InterTight-400.ttf", diff --git a/package.json b/package.json index 3abb8b3..194e7dd 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "release:publish": "changeset publish" }, "devDependencies": { - "@origin89/brand": "0.3.1", + "@origin89/brand": "0.4.0", "@biomejs/biome": "2.5.13", "typescript": "6.0.3", "@changesets/cli": "3.0.2" diff --git a/platforms/compose/ui/src/main/kotlin/com/origin89/ui/GeneratedTokens.kt b/platforms/compose/ui/src/main/kotlin/com/origin89/ui/GeneratedTokens.kt index 8344743..fce628d 100644 --- a/platforms/compose/ui/src/main/kotlin/com/origin89/ui/GeneratedTokens.kt +++ b/platforms/compose/ui/src/main/kotlin/com/origin89/ui/GeneratedTokens.kt @@ -1,4 +1,4 @@ -// Generated from @origin89/brand 0.3.1; run pnpm brand:generate. +// Generated from @origin89/brand 0.4.0; run pnpm brand:generate. package com.origin89.ui import androidx.compose.ui.graphics.Color data class Origin89Palette( @@ -15,6 +15,7 @@ data class Origin89Palette( val actionLit: Color, val link: Color, val onFill: Color, + val signal: Color, val nominal: Color, val warning: Color, val nominalDeep: Color, @@ -38,6 +39,7 @@ object Origin89Tokens { actionLit = Color(0xFF3F61B3), link = Color(0xFF2B4A97), onFill = Color(0xFFFFFFFF), + signal = Color(0xFF5771B2), nominal = Color(0xFF4C7D5F), warning = Color(0xFF9F6601), nominalDeep = Color(0xFF4C7D5F), @@ -60,6 +62,7 @@ object Origin89Tokens { actionLit = Color(0xFF3F61B3), link = Color(0xFF6279AD), onFill = Color(0xFFFFFFFF), + signal = Color(0xFF7F9CE0), nominal = Color(0xFF2F9D64), warning = Color(0xFFE9A13C), nominalDeep = Color(0xFF168C54), diff --git a/platforms/swiftui/Sources/Origin89UI/GeneratedTokens.swift b/platforms/swiftui/Sources/Origin89UI/GeneratedTokens.swift index ca715c3..88f7e1f 100644 --- a/platforms/swiftui/Sources/Origin89UI/GeneratedTokens.swift +++ b/platforms/swiftui/Sources/Origin89UI/GeneratedTokens.swift @@ -1,4 +1,4 @@ -// Generated from @origin89/brand 0.3.1; run pnpm brand:generate. +// Generated from @origin89/brand 0.4.0; run pnpm brand:generate. import SwiftUI public struct Origin89Palette: Sendable { public let page: Color @@ -14,6 +14,7 @@ public struct Origin89Palette: Sendable { public let actionLit: Color public let link: Color public let onFill: Color + public let signal: Color public let nominal: Color public let warning: Color public let nominalDeep: Color @@ -37,6 +38,7 @@ public enum Origin89Tokens { actionLit: Color(red: 63/255.0, green: 97/255.0, blue: 179/255.0), link: Color(red: 43/255.0, green: 74/255.0, blue: 151/255.0), onFill: Color(red: 255/255.0, green: 255/255.0, blue: 255/255.0), + signal: Color(red: 87/255.0, green: 113/255.0, blue: 178/255.0), nominal: Color(red: 76/255.0, green: 125/255.0, blue: 95/255.0), warning: Color(red: 159/255.0, green: 102/255.0, blue: 1/255.0), nominalDeep: Color(red: 76/255.0, green: 125/255.0, blue: 95/255.0), @@ -59,6 +61,7 @@ public enum Origin89Tokens { actionLit: Color(red: 63/255.0, green: 97/255.0, blue: 179/255.0), link: Color(red: 98/255.0, green: 121/255.0, blue: 173/255.0), onFill: Color(red: 255/255.0, green: 255/255.0, blue: 255/255.0), + signal: Color(red: 127/255.0, green: 156/255.0, blue: 224/255.0), nominal: Color(red: 47/255.0, green: 157/255.0, blue: 100/255.0), warning: Color(red: 233/255.0, green: 161/255.0, blue: 60/255.0), nominalDeep: Color(red: 22/255.0, green: 140/255.0, blue: 84/255.0), @@ -82,6 +85,7 @@ public enum Origin89Tokens { actionLit: Color(light: light.actionLit, dark: dark.actionLit), link: Color(light: light.link, dark: dark.link), onFill: Color(light: light.onFill, dark: dark.onFill), + signal: Color(light: light.signal, dark: dark.signal), nominal: Color(light: light.nominal, dark: dark.nominal), warning: Color(light: light.warning, dark: dark.warning), nominalDeep: Color(light: light.nominalDeep, dark: dark.nominalDeep), diff --git a/platforms/swiftui/Sources/Origin89UI/Loading.swift b/platforms/swiftui/Sources/Origin89UI/Loading.swift new file mode 100644 index 0000000..3477507 --- /dev/null +++ b/platforms/swiftui/Sources/Origin89UI/Loading.swift @@ -0,0 +1,49 @@ +import SwiftUI + +/// Work in progress. The bars are the shape of what is coming rather than a bare spinner, and +/// both the spinner and the bars stop under Reduce Motion. +public struct Origin89Loading: View { + public let label: String + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var pulse = false + @State private var spin = false + public init(_ label: String = "Loading\u{2026}") { + self.label = label + } + public var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 10) { + ZStack { + Circle().strokeBorder(.origin89.lineStrong, lineWidth: 2) + Circle().inset(by: 1).trim(from: 0, to: 0.25).stroke(.origin89.signal, lineWidth: 2) + } + .frame(width: 13, height: 13) + .rotationEffect(.degrees(spin ? 360 : 0)) + .accessibilityHidden(true) + Text(label).font(.origin89Label).foregroundStyle(.origin89.muted) + } + .accessibilityElement(children: .combine) + VStack(alignment: .leading, spacing: 16) { + ForEach(0..<4, id: \.self) { bar in + GeometryReader { proxy in + Rectangle().fill(.origin89.line) + .frame(width: bar.isMultiple(of: 2) ? proxy.size.width : proxy.size.width * 0.8) + } + .frame(height: 32) + } + } + .opacity(reduceMotion ? 0.5 : (pulse ? 0.7 : 0.35)) + .accessibilityHidden(true) + } + .padding(.vertical, 24) + .onAppear { + guard !reduceMotion else { return } + withAnimation(.easeInOut(duration: 1.6).repeatForever(autoreverses: true)) { pulse = true } + withAnimation(.linear(duration: 0.9).repeatForever(autoreverses: false)) { spin = true } + } + } +} + +#Preview("Loading") { + Origin89Loading("Reading the network section\u{2026}").padding() +} diff --git a/platforms/swiftui/Sources/Origin89UI/Notice.swift b/platforms/swiftui/Sources/Origin89UI/Notice.swift new file mode 100644 index 0000000..f7c6889 --- /dev/null +++ b/platforms/swiftui/Sources/Origin89UI/Notice.swift @@ -0,0 +1,66 @@ +import SwiftUI + +public enum Origin89NoticeTone: Sendable, CaseIterable { + case info, alarm + + var color: KeyPath { + switch self { + case .info: \.info + case .alarm: \.alarm + } + } + var text: KeyPath { + switch self { + case .info: \.muted + case .alarm: \.alarm + } + } + /// An alarm announces itself; an info notice waits to be read, so VoiceOver is not interrupted. + var announces: Bool { + switch self { + case .info: false + case .alarm: true + } + } +} + +/// A line of consequence above the thing it concerns, such as a refusal from the controller. +public struct Origin89Notice: View { + public let message: String + public let tone: Origin89NoticeTone + public init(_ message: String, tone: Origin89NoticeTone = .info) { + self.message = message + self.tone = tone + } + public var body: some View { + let shape = BevelRectangle(corner: 10) + Text(message) + .font(.origin89Label) + .lineSpacing(4) + .foregroundStyle(Origin89Tokens.adaptive[keyPath: tone.text]) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 14) + .padding(.horizontal, 16) + .background(.origin89.surface) + .overlay(alignment: .leading) { + Rectangle().fill(Origin89Tokens.adaptive[keyPath: tone.color]).frame(width: 3) + } + .clipShape(shape) + .overlay { shape.strokeBorder(.origin89.line, lineWidth: 1) } + .accessibilityElement(children: .combine) + .onAppear(perform: announce) + .onChange(of: message) { announce() } + } + private func announce() { + guard tone.announces else { return } + AccessibilityNotification.Announcement(message).post() + } +} + +#Preview("Tones") { + VStack(spacing: 12) { + Origin89Notice("Open the pairing window at the panel, then pair within 120 seconds.") + Origin89Notice("The controller refused the proof. Check the QR code and try again.", tone: .alarm) + }.padding() +} diff --git a/platforms/swiftui/Sources/Origin89UI/Status.swift b/platforms/swiftui/Sources/Origin89UI/Status.swift new file mode 100644 index 0000000..8a44050 --- /dev/null +++ b/platforms/swiftui/Sources/Origin89UI/Status.swift @@ -0,0 +1,66 @@ +import SwiftUI + +/// How a status chip looks. The tone is the presentation, not the meaning: the application maps +/// its own states onto these. Each tone pairs its hue with a border style and a mark, so the chip +/// still reads without colour. +public enum Origin89StatusTone: Sendable, CaseIterable { + case nominal, alarm, warning, info, faint + + var mark: String { + switch self { + case .nominal: "\u{2713}" + case .alarm: "!" + case .warning, .info, .faint: "\u{2022}" + } + } + var color: KeyPath { + switch self { + case .nominal: \.nominal + case .alarm: \.alarm + case .warning: \.warning + case .info: \.info + case .faint: \.faint + } + } + var border: StrokeStyle { + switch self { + case .nominal, .info: StrokeStyle(lineWidth: 1) + case .alarm: StrokeStyle(lineWidth: 2) + case .warning: StrokeStyle(lineWidth: 1, dash: [4, 3]) + case .faint: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [0.5, 2.5]) + } + } +} + +/// A state chip, such as a setup step that is pending, running, done or refused. +public struct Origin89Status: View { + public let title: String + public let tone: Origin89StatusTone + public init(_ title: String, tone: Origin89StatusTone = .faint) { + self.title = title + self.tone = tone + } + public var body: some View { + HStack(spacing: 6) { + Text(tone.mark).accessibilityHidden(true) + Text(title) + } + .font(.origin89Data) + .lineLimit(1) + .padding(.vertical, 3) + .padding(.horizontal, 8) + .overlay { Rectangle().strokeBorder(style: tone.border) } + .foregroundStyle(Origin89Tokens.adaptive[keyPath: tone.color]) + .accessibilityElement(children: .combine) + } +} + +#Preview("Tones") { + VStack(alignment: .leading, spacing: 12) { + Origin89Status("Paired", tone: .nominal) + Origin89Status("Wrong proof", tone: .alarm) + Origin89Status("Window closing", tone: .warning) + Origin89Status("Pairing", tone: .info) + Origin89Status("Not started") + }.padding() +} diff --git a/platforms/swiftui/Sources/Origin89UI/Theme.swift b/platforms/swiftui/Sources/Origin89UI/Theme.swift index 1d70956..0d3d8d9 100644 --- a/platforms/swiftui/Sources/Origin89UI/Theme.swift +++ b/platforms/swiftui/Sources/Origin89UI/Theme.swift @@ -49,3 +49,25 @@ public extension Font { return .custom(name, size: size, relativeTo: style) } } + +/// The brand's bevelled corner: a straight cut `corner` points along each edge. +struct BevelRectangle: InsettableShape { + var corner: CGFloat + var inset: CGFloat = 0 + func path(in rect: CGRect) -> Path { + let r = rect.insetBy(dx: inset, dy: inset) + let c = max(0, min(corner - inset, r.width / 2, r.height / 2)) + var path = Path() + path.move(to: CGPoint(x: r.minX + c, y: r.minY)) + path.addLine(to: CGPoint(x: r.maxX - c, y: r.minY)) + path.addLine(to: CGPoint(x: r.maxX, y: r.minY + c)) + path.addLine(to: CGPoint(x: r.maxX, y: r.maxY - c)) + path.addLine(to: CGPoint(x: r.maxX - c, y: r.maxY)) + path.addLine(to: CGPoint(x: r.minX + c, y: r.maxY)) + path.addLine(to: CGPoint(x: r.minX, y: r.maxY - c)) + path.addLine(to: CGPoint(x: r.minX, y: r.minY + c)) + path.closeSubpath() + return path + } + func inset(by amount: CGFloat) -> BevelRectangle { BevelRectangle(corner: corner, inset: inset + amount) } +} diff --git a/platforms/swiftui/Tests/Origin89UITests/ComponentTests.swift b/platforms/swiftui/Tests/Origin89UITests/ComponentTests.swift new file mode 100644 index 0000000..978e2e8 --- /dev/null +++ b/platforms/swiftui/Tests/Origin89UITests/ComponentTests.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Testing +@testable import Origin89UI + +@Test func statusTonesStayDistinctWithoutColour() { + // Mark and border together must tell every tone apart, as they do on the web. + let looks = Origin89StatusTone.allCases.map { "\($0.mark)|\($0.border.lineWidth)|\($0.border.dash)" } + #expect(Set(looks).count == Origin89StatusTone.allCases.count) + #expect(Origin89StatusTone.nominal.mark == "\u{2713}") + #expect(Origin89StatusTone.alarm.mark == "!") + #expect(Origin89StatusTone.alarm.border.lineWidth == 2) +} +@Test func statusTonesUseTheirBrandColour() { + let expected: [Origin89StatusTone: KeyPath] = [ + .nominal: \.nominal, .alarm: \.alarm, .warning: \.warning, .info: \.info, .faint: \.faint, + ] + for tone in Origin89StatusTone.allCases { #expect(tone.color == expected[tone], "\(tone)") } +} +@Test func onlyAlarmNoticesAnnounce() { + #expect(Origin89NoticeTone.alarm.announces) + #expect(!Origin89NoticeTone.info.announces) + #expect(Origin89NoticeTone.alarm.text == \Origin89Palette.alarm) + #expect(Origin89NoticeTone.info.text == \Origin89Palette.muted) +} +@MainActor @Test func defaultsMatchTheWeb() { + #expect(Origin89Status("Pending").tone == .faint) + #expect(Origin89Notice("Note").tone == .info) + #expect(Origin89Loading().label == "Loading\u{2026}") +} +@Test func bevelCutsEveryCornerAndClampsToSmallRects() { + let square = BevelRectangle(corner: 10).path(in: CGRect(x: 0, y: 0, width: 100, height: 40)) + #expect(!square.contains(CGPoint(x: 1, y: 1))) + #expect(!square.contains(CGPoint(x: 99, y: 39))) + #expect(square.contains(CGPoint(x: 50, y: 20))) + let tiny = BevelRectangle(corner: 10).path(in: CGRect(x: 0, y: 0, width: 6, height: 6)) + #expect(tiny.contains(CGPoint(x: 3, y: 3))) + #expect(BevelRectangle(corner: 10).inset(by: 12).path(in: CGRect(x: 0, y: 0, width: 100, height: 40)).contains(CGPoint(x: 50, y: 20))) +} +@MainActor @Test(arguments: [ColorScheme.light, .dark]) +func componentsRenderInBothSchemes(scheme: ColorScheme) throws { + let views: [AnyView] = Origin89StatusTone.allCases.map { AnyView(Origin89Status("Paired", tone: $0)) } + + Origin89NoticeTone.allCases.map { AnyView(Origin89Notice("", tone: $0)) } + + [AnyView(Origin89Notice(String(repeating: "Wrong proof. ", count: 40), tone: .alarm)), AnyView(Origin89Loading(""))] + for view in views { + let renderer = ImageRenderer(content: view.frame(width: 320).environment(\.colorScheme, scheme)) + let image = try #require(renderer.cgImage) + #expect(image.width > 0 && image.height > 0) + } +} diff --git a/platforms/swiftui/Tests/Origin89UITests/ThemeTests.swift b/platforms/swiftui/Tests/Origin89UITests/ThemeTests.swift index bd469ca..40d5190 100644 --- a/platforms/swiftui/Tests/Origin89UITests/ThemeTests.swift +++ b/platforms/swiftui/Tests/Origin89UITests/ThemeTests.swift @@ -24,7 +24,7 @@ private func same(_ a: Color.Resolved, _ b: Color.Resolved) -> Bool { @MainActor func adaptiveColorsFollowTheScheme(scheme: ColorScheme) { let expected = colors(scheme == .dark ? Origin89Tokens.dark : Origin89Tokens.light) let adaptive = colors(Origin89Tokens.adaptive) - #expect(adaptive.count == 20) + #expect(adaptive.count == 21) #expect(adaptive.map(\.0) == expected.map(\.0)) for ((name, color), (_, fixed)) in zip(adaptive, expected) { #expect(same(resolved(color, in: scheme), resolved(fixed, in: scheme)), "\(name) in \(scheme)") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bbeeb9..bcf7007 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,8 +116,8 @@ importers: specifier: 3.0.2 version: 3.0.2 '@origin89/brand': - specifier: 0.3.1 - version: 0.3.1 + specifier: 0.4.0 + version: 0.4.0 typescript: specifier: 6.0.3 version: 6.0.3 @@ -281,10 +281,6 @@ packages: resolution: {integrity: sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==} engines: {node: '>=20.0.0'} - '@origin89/brand@0.3.1': - resolution: {integrity: sha512-H0d49I0nWyYmq9CPr/4e8/lo1yRUYyaEMhqqNxxvM5xvk3ujnnvSZP02t1Zp7ncCjwRW4ihVzPmL5RXIwxP13Q==} - engines: {node: '>=22.12.0'} - '@origin89/brand@0.4.0': resolution: {integrity: sha512-vX9eehWsu+7J81k685wKlR+G+VtyfMnk0bk9JVM1YNfCWtD0hQLe8fC+xGzLTu2kFJUQWFW3qWh3uwNhS7sbBQ==} engines: {node: '>=22.12.0'} @@ -562,8 +558,6 @@ snapshots: tinyglobby: 0.2.17 yaml: 2.9.1 - '@origin89/brand@0.3.1': {} - '@origin89/brand@0.4.0': {} '@pnpm/deps.graph-sequencer@1100.0.1': {} diff --git a/scripts/generate-brand.mjs b/scripts/generate-brand.mjs index 335d3f4..7a3cf39 100644 --- a/scripts/generate-brand.mjs +++ b/scripts/generate-brand.mjs @@ -7,7 +7,7 @@ const brandRoot = new URL("node_modules/@origin89/brand/", root); const brandBytes = await readFile(new URL("brand.json", brandRoot)); const brand = JSON.parse(brandBytes); const version = JSON.parse(await readFile(new URL("package.json", brandRoot), "utf8")).version; -assert.equal(version, "0.3.1"); +assert.equal(version, "0.4.0"); const outputs = new Map(); const camel = (value) => value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); const keys = Object.keys(brand.colour.themes.light); From a425670caeee3ee621ae05c9e3c345e46ee8aae7 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 24 Sep 2026 10:06:31 -0400 Subject: [PATCH 2/2] docs: point Swift consumers at the latest tag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 271fee2..257f76a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ React: install `@origin89/ui-react` from npm. Import `Reading` or `EquipmentIllu ``` -Swift: add `https://github.com/origin89hq/ui` as a Swift package dependency with an exact version such as `0.2.0`, link `Origin89UI`, then use `Origin89Reading`, `Origin89Status`, `Origin89Notice` or `Origin89Loading`. Brand colours follow light and dark mode through `.foregroundStyle(.origin89.muted)` or `Color.origin89.surface`; fixed palettes stay available as `Origin89Tokens.light` and `.dark`. Text styles such as `.font(.origin89Label)` register the bundled fonts. Native previews include current, zero, stale and unknown examples. +Swift: add `https://github.com/origin89hq/ui` as a Swift package dependency pinned to the latest plain `X.Y.Z` [tag](https://github.com/origin89hq/ui/tags) with `exact:`, link `Origin89UI`, then use `Origin89Reading`, `Origin89Status`, `Origin89Notice` or `Origin89Loading`. Brand colours follow light and dark mode through `.foregroundStyle(.origin89.muted)` or `Color.origin89.surface`; fixed palettes stay available as `Origin89Tokens.light` and `.dark`. Text styles such as `.font(.origin89Label)` register the bundled fonts. Native previews include current, zero, stale and unknown examples. Android: the `platforms/compose` Gradle project builds an Android library. Run `./gradlew :ui:publishToMavenLocal` there, add `mavenLocal()` to a consuming development project and depend on `com.origin89:ui-compose:0.1.0`. Release AAR and source artifacts are also attached to GitHub releases; Maven Central publication is not configured yet.