-
Notifications
You must be signed in to change notification settings - Fork 4
feat(ui): radical redesign — design tokens, TabView shell, actionable onboarding & Relay funnel #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
cff431c
feat(ui): token foundation, SyncStatus registry, global accent & widg…
psimaker 15cd407
feat(ui): shared component kit (StatusBadge, StatusRow, DetailRow, Ac…
psimaker 210c0c8
refactor(ui): retint standalone views onto semantic status tokens
psimaker 573a967
feat(ui): persistent sync-status header + hub retint (Phase 3a)
psimaker 0c39ee0
feat(ui): TabView shell — split the overloaded hub into Sync + Device…
psimaker c09790b
refactor(ui): finish app-wide retint onto semantic status tokens (Pha…
psimaker a412f0f
feat(ui): actionable onboarding pager (Phase 4a)
psimaker ad943ad
feat(ui): dedicated Relay tab + canonical SubscribePlanPicker (Phase 4b)
psimaker b01e83d
feat(ui): conflict resolution affordance, honest rescan progress, sta…
psimaker e1cb82e
feat(ui): widget VoiceOver, L10n fallback, design-token lint guardrai…
psimaker 7a74165
refactor(ui): rework Relay tab — distinct states, reframed pitch, Set…
psimaker 3088d42
refactor(ui): move Relay privacy framing behind an info button
psimaker 9c7f21e
fix(l10n): translate redesign strings to de/es/zh, unify relay termin…
psimaker ce19e74
fix(ui): resolve redesign review findings — honesty, a11y, dead code,…
psimaker 6446c7f
fix(ui): relay privacy popover truncation + close the token-lint retu…
psimaker e24536f
fix(notify): keep the sidecar alive on inactive-subscription relay 4xx
psimaker 383c1d5
fix(ui): show real Obsidian vaults on the home screen, not the sync f…
psimaker b5aea06
fix(l10n): correct translation errors and add missing status keys
psimaker 146e589
chore: remove dead localization keys and unused Swift symbols
psimaker 75ccb26
fix(l10n): shorten overflowing button and status labels (de/es/zh)
psimaker 72d11c9
docs: document the UI redesign in the unreleased 1.5.0 changelog + RE…
psimaker b8467ff
fix(ui,l10n): resolve actionable CodeRabbit findings (skip 3 nitpicks)
psimaker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
ios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| import Foundation | ||
|
|
||
| enum L10n { | ||
| /// `value: key` means a missing translation falls back to the (English) key | ||
| /// text instead of rendering the bare key — matching the widget's helper. | ||
| static func tr(_ key: String) -> String { | ||
| NSLocalizedString(key, comment: "") | ||
| NSLocalizedString(key, tableName: nil, bundle: .main, value: key, comment: "") | ||
| } | ||
|
|
||
| static func fmt(_ key: String, _ args: CVarArg...) -> String { | ||
| String(format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args) | ||
| String(format: tr(key), locale: Locale.current, arguments: args) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,191 @@ | ||
| import SwiftUI | ||
| #if canImport(UIKit) | ||
| import UIKit | ||
| #endif | ||
|
|
||
| // MARK: - Design Tokens | ||
| // | ||
| // Single source of truth for the VaultSync visual language, compiled into BOTH | ||
| // the app and the widget target (see project.yml). Because it is shared with the | ||
| // widget extension it must NOT reference app-only symbols such as `L10n`; use | ||
| // `String(localized:)` for any user-facing text so each target resolves strings | ||
| // from its own bundle. | ||
| // | ||
| // Colors are built as dynamic Display-P3 `UIColor`s so light/dark (and the | ||
| // Increase-Contrast accessibility setting) resolve automatically — this retires | ||
| // the hand-rolled `colorScheme == .dark ? … : …` opacity math that used to live | ||
| // in the views. | ||
|
|
||
| #if canImport(UIKit) | ||
| /// A Display-P3 color that resolves light/dark and optional increased-contrast | ||
| /// variants from the active trait collection. Channels are 0–255 for legibility. | ||
| private func vaultColor( | ||
| light: (CGFloat, CGFloat, CGFloat), | ||
| dark: (CGFloat, CGFloat, CGFloat), | ||
| lightHC: (CGFloat, CGFloat, CGFloat)? = nil, | ||
| darkHC: (CGFloat, CGFloat, CGFloat)? = nil | ||
| ) -> Color { | ||
| Color(uiColor: UIColor { traits in | ||
| let highContrast = traits.accessibilityContrast == .high | ||
| let channels: (CGFloat, CGFloat, CGFloat) | ||
| switch (traits.userInterfaceStyle, highContrast) { | ||
| case (.dark, true): channels = darkHC ?? dark | ||
| case (.dark, false): channels = dark | ||
| case (_, true): channels = lightHC ?? light | ||
| default: channels = light | ||
| } | ||
| return UIColor( | ||
| displayP3Red: channels.0 / 255, | ||
| green: channels.1 / 255, | ||
| blue: channels.2 / 255, | ||
| alpha: 1 | ||
| ) | ||
| }) | ||
| } | ||
| #else | ||
| private func vaultColor( | ||
| light: (CGFloat, CGFloat, CGFloat), | ||
| dark: (CGFloat, CGFloat, CGFloat), | ||
| lightHC: (CGFloat, CGFloat, CGFloat)? = nil, | ||
| darkHC: (CGFloat, CGFloat, CGFloat)? = nil | ||
| ) -> Color { | ||
| Color(red: light.0 / 255, green: light.1 / 255, blue: light.2 / 255) | ||
| } | ||
| #endif | ||
|
|
||
| // MARK: - Brand palette | ||
|
|
||
| /// Shared brand palette. Single source of truth so the app and the widget | ||
| /// render the same accent colors instead of redefining the RGB per file. | ||
| extension Color { | ||
| /// Brand teal — active / in-progress sync accent. | ||
| static let vaultTeal = Color(red: 0 / 255, green: 137 / 255, blue: 123 / 255) | ||
| /// Brand slate — muted/inactive accent. | ||
| static let vaultSlate = Color(red: 38 / 255, green: 50 / 255, blue: 56 / 255) | ||
| /// Primary interactive / affirmative-active brand accent. This is the single | ||
| /// app-wide tint (also mirrored in `AccentColor` so the asset-catalog global | ||
| /// accent matches). Used for links, selection, primary buttons, "syncing". | ||
| static let vaultAccent = vaultColor( | ||
| light: (0, 137, 123), // #00897B — the established brand teal, P3-tuned | ||
| dark: (38, 196, 176), // lifted so it stays vivid on a dark canvas | ||
| lightHC: (0, 110, 99), | ||
| darkHC: (74, 222, 202) | ||
| ) | ||
|
|
||
| /// Brand teal — kept as the historical name so existing call sites keep | ||
| /// working, now dark-aware. Identical to `vaultAccent`. | ||
| static let vaultTeal = Color.vaultAccent | ||
|
|
||
| /// Deep neutral slate, used for muted fills/surfaces. Dark-aware so fills no | ||
| /// longer need per-call-site opacity math. | ||
| static let vaultSlate = vaultColor( | ||
| light: (38, 50, 56), // #263238 | ||
| dark: (176, 190, 197) // #B0BEC5 — readable as a muted accent in dark | ||
| ) | ||
| } | ||
|
|
||
| // MARK: - Semantic status palette | ||
| // | ||
| // Six pinned meanings, each ALWAYS paired with a symbol + text label by the | ||
| // `SyncStatus` registry so status is never conveyed by color alone. | ||
|
|
||
| extension Color { | ||
| /// Idle / all-synced / connected. | ||
| static let statusSuccess = vaultColor(light: (46, 158, 107), dark: (52, 199, 127)) | ||
| /// Active transfer in progress (alias of the brand accent). | ||
| static let statusSyncing = Color.vaultAccent | ||
| /// Transient "starting/preparing" — a calm blue so it is never mistaken for | ||
| /// an error (today it is wrongly conflated with attention/orange). | ||
| static let statusStarting = vaultColor(light: (78, 124, 168), dark: (127, 168, 208)) | ||
| /// Warning / action-needed (conflicts, pending shares, setup gaps). | ||
| static let statusAttention = vaultColor(light: (224, 146, 47), dark: (242, 169, 59)) | ||
| /// Error / unreachable — reserved for genuine failures. | ||
| static let statusError = vaultColor(light: (210, 69, 59), dark: (232, 92, 82)) | ||
| /// Informational / shared-with — replaces the off-brand system blue used for | ||
| /// "Shared With" checkmarks. | ||
| static let statusInfo = vaultColor(light: (78, 111, 181), dark: (110, 143, 216)) | ||
| /// Paused / offline / inactive. | ||
| static let statusInactive = Color.secondary | ||
| } | ||
|
|
||
| // MARK: - Spacing & radius scale | ||
|
|
||
| /// 8pt soft grid. Replaces the 14-value padding literal soup. | ||
| enum VaultSpacing { | ||
| static let xs: CGFloat = 4 | ||
| static let s: CGFloat = 8 | ||
| static let m: CGFloat = 12 | ||
| static let l: CGFloat = 16 | ||
| static let xl: CGFloat = 24 | ||
| } | ||
|
|
||
| /// Continuous corner radii. Replaces the 8/10/11/12/14/22/24/28 spread. | ||
| enum VaultRadius { | ||
| static let control: CGFloat = 12 | ||
| static let card: CGFloat = 16 | ||
| static let hero: CGFloat = 28 | ||
| } | ||
|
|
||
| // MARK: - Sync status registry | ||
| // | ||
| // One canonical status type keyed by genuine sync state. Maps to a symbol, a | ||
| // semantic color, and a localized label. The widget decodes its stringly-typed | ||
| // snapshot through `fromWire(_:)` so an unknown value maps to `.attention` | ||
| // (NEVER silently to "all good"), closing the documented widget-lies bug. | ||
|
|
||
| enum SyncStatus: String, Sendable, CaseIterable { | ||
| case synced | ||
| case syncing | ||
| case starting | ||
| case attention | ||
| case error | ||
| case paused | ||
|
|
||
| /// Decode the app↔widget wire-format status string. Unknown → `.attention`. | ||
| static func fromWire(_ raw: String) -> SyncStatus { | ||
| switch raw.lowercased() { | ||
| case "idle", "synced", "ok": return .synced | ||
| case "syncing", "scanning": return .syncing | ||
| case "starting", "preparing": return .starting | ||
| case "attention", "warning", "warn": return .attention | ||
| case "error", "failed": return .error | ||
| case "paused", "inactive", "offline": return .paused | ||
| default: return .attention | ||
| } | ||
| } | ||
|
|
||
| /// Stable wire string for persisting into the shared snapshot. | ||
| var wireValue: String { rawValue } | ||
|
|
||
| var symbolName: String { | ||
| switch self { | ||
| case .synced: return "checkmark.circle.fill" | ||
| case .syncing: return "arrow.triangle.2.circlepath" | ||
| case .starting: return "hourglass" | ||
| case .attention: return "exclamationmark.triangle.fill" | ||
| case .error: return "xmark.octagon.fill" | ||
| case .paused: return "pause.circle.fill" | ||
| } | ||
| } | ||
|
|
||
| var tint: Color { | ||
| switch self { | ||
| case .synced: return .statusSuccess | ||
| case .syncing: return .statusSyncing | ||
| case .starting: return .statusStarting | ||
| case .attention: return .statusAttention | ||
| case .error: return .statusError | ||
| case .paused: return .statusInactive | ||
| } | ||
| } | ||
|
|
||
| /// Localized one-word/short label. Resolved from each target's own bundle. | ||
| var label: String { | ||
| switch self { | ||
| case .synced: return String(localized: "All Synced") | ||
| case .syncing: return String(localized: "Syncing") | ||
| case .starting: return String(localized: "Starting") | ||
| case .attention: return String(localized: "Needs Attention") | ||
| case .error: return String(localized: "Sync Error") | ||
| case .paused: return String(localized: "Paused") | ||
| } | ||
| } | ||
|
|
||
| /// True for states that should draw the user's attention (used for ordering | ||
| /// and for animating the symbol). | ||
| var isUrgent: Bool { self == .attention || self == .error } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.