-
Notifications
You must be signed in to change notification settings - Fork 0
Add native iOS app (SwiftUI) with full feature parity #21
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
4 commits
Select commit
Hold shift + click to select a range
66c93c3
Add native iOS app (SwiftUI) with full feature parity
auerbachb 44b8c51
Fix all CR findings: 3 critical, 9 major, 6 minor/nitpick
auerbachb 4bf5f27
Fix CR round 2: double-completion, pulse anim, @MainActor
auerbachb 2f9945d
Fix CR round 3: abandon saves session, re-entrancy, pulse anim
auerbachb 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
Large diffs are not rendered by default.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
ios/StillPoint.xcodeproj/project.xcworkspace/contents.xcworkspacedata
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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 |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import SwiftUI | ||
| import StillPointShared | ||
|
|
||
| struct BlockGridView: View { | ||
| let blocks: [BlockDef] | ||
| let elapsed: Double | ||
| let totalSeconds: Int | ||
|
|
||
| private let blockSize: CGFloat = 56 | ||
| private let blockSpacing: CGFloat = 11 | ||
| private let blockRadius: CGFloat = 10 | ||
|
|
||
| private var minuteBlocks: [BlockDef] { | ||
| blocks.filter { $0.type == .minute } | ||
| } | ||
|
|
||
| private var secondBlocks: [BlockDef] { | ||
| blocks.filter { $0.type == .second } | ||
| } | ||
|
|
||
| private var useMinuteBlocks: Bool { | ||
| totalSeconds > 120 | ||
| } | ||
|
|
||
| var body: some View { | ||
| if useMinuteBlocks { | ||
| VStack(spacing: SPSpacing.s3) { | ||
| // Minute blocks | ||
| LazyVGrid( | ||
| columns: [GridItem(.adaptive(minimum: blockSize, maximum: blockSize), spacing: blockSpacing)], | ||
| spacing: blockSpacing | ||
| ) { | ||
| ForEach(minuteBlocks) { block in | ||
| blockView(block) | ||
| } | ||
| } | ||
|
|
||
| // Divider + "final minute" label | ||
| VStack(spacing: SPSpacing.s1) { | ||
| Rectangle() | ||
| .fill(SPColor.border1) | ||
| .frame(height: 1) | ||
|
|
||
| Text("FINAL MINUTE") | ||
| .font(SPFont.mono(11)) | ||
| .foregroundStyle(Color(SPColor.fg4)) | ||
| .tracking(2) | ||
|
|
||
| // 10-second blocks | ||
| LazyVGrid( | ||
| columns: [GridItem(.adaptive(minimum: blockSize, maximum: blockSize), spacing: blockSpacing)], | ||
| spacing: blockSpacing | ||
| ) { | ||
| ForEach(secondBlocks) { block in | ||
| blockView(block) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| LazyVGrid( | ||
| columns: [GridItem(.adaptive(minimum: blockSize, maximum: blockSize), spacing: blockSpacing)], | ||
| spacing: blockSpacing | ||
| ) { | ||
| ForEach(blocks) { block in | ||
| blockView(block) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @ViewBuilder | ||
| private func blockView(_ block: BlockDef) -> some View { | ||
| let blockEnd = block.startTime + block.duration | ||
| let isFilled = elapsed >= Double(blockEnd) | ||
| let isCurrent = elapsed >= Double(block.startTime) | ||
| && elapsed < Double(blockEnd) | ||
| && elapsed < Double(totalSeconds) | ||
| let progress = isCurrent | ||
| ? (elapsed - Double(block.startTime)) / Double(block.duration) | ||
| : isFilled ? 1.0 : 0.0 | ||
|
|
||
| ZStack { | ||
| // Background | ||
| RoundedRectangle(cornerRadius: blockRadius) | ||
| .fill(SPColor.surface1) | ||
|
|
||
| // Fill from bottom | ||
| GeometryReader { geo in | ||
| VStack { | ||
| Spacer() | ||
| Rectangle() | ||
| .fill(isFilled ? LinearGradient.greenFill : LinearGradient.amberFill) | ||
| .frame(height: geo.size.height * progress) | ||
| .opacity(isFilled ? 0.85 : 0.7) | ||
| } | ||
| } | ||
| .clipShape(RoundedRectangle(cornerRadius: blockRadius)) | ||
|
|
||
| // Label | ||
| Text(block.label) | ||
| .font(SPFont.mono(13, weight: .medium)) | ||
| .foregroundStyle(isFilled ? SPColor.overlayText : Color(SPColor.fg4)) | ||
|
|
||
| // Current block pulse border — uses phaseAnimator for continuous pulse | ||
| if isCurrent { | ||
| RoundedRectangle(cornerRadius: blockRadius) | ||
| .stroke(SPColor.amberDim, lineWidth: 1) | ||
| .phaseAnimator([false, true]) { content, phase in | ||
| content.opacity(phase ? 1.0 : 0.4) | ||
| } animation: { _ in | ||
| .easeInOut(duration: 1.0) | ||
| } | ||
| } | ||
|
auerbachb marked this conversation as resolved.
|
||
| } | ||
| .frame(width: blockSize, height: blockSize) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: blockRadius) | ||
| .stroke( | ||
| isFilled ? SPColor.greenBorder : | ||
| isCurrent ? SPColor.amberBorder : | ||
| SPColor.border1, | ||
| lineWidth: 1 | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
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 |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import SwiftUI | ||
| import StillPointShared | ||
|
|
||
| /// Horizontal segmented bar showing clear (green) and thinking (amber) periods. | ||
| struct MindStateBarView: View { | ||
| let elapsed: Double | ||
| let totalSeconds: Int | ||
| let mindStateLog: [MindStateEntry] | ||
|
|
||
| var body: some View { | ||
| GeometryReader { geo in | ||
| let width = geo.size.width | ||
| let safeTotalSeconds = max(totalSeconds, 1) | ||
|
|
||
| ZStack(alignment: .leading) { | ||
| // Background track | ||
| RoundedRectangle(cornerRadius: 4) | ||
| .fill(SPColor.surface2) | ||
|
|
||
| // Segments | ||
| ForEach(Array(mindStateLog.enumerated()), id: \.offset) { index, entry in | ||
| let startFraction = entry.time / Double(safeTotalSeconds) | ||
| let endTime: Double = { | ||
| if index + 1 < mindStateLog.count { | ||
| return mindStateLog[index + 1].time | ||
| } | ||
| return min(elapsed, Double(safeTotalSeconds)) | ||
| }() | ||
| let endFraction = endTime / Double(safeTotalSeconds) | ||
|
|
||
| let segmentX = width * startFraction | ||
| let segmentWidth = max(0, width * (endFraction - startFraction)) | ||
|
|
||
| Rectangle() | ||
| .fill(entry.isClear ? SPColor.green : SPColor.amber) | ||
| .opacity(entry.isClear ? 0.5 : 0.6) | ||
| .frame(width: segmentWidth) | ||
| .offset(x: segmentX) | ||
| } | ||
| } | ||
| } | ||
| .frame(height: 8) | ||
| .clipShape(RoundedRectangle(cornerRadius: 4)) | ||
| .padding(.horizontal, SPSpacing.s4) | ||
| } | ||
| } |
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 |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import SwiftUI | ||
|
|
||
| /// Inline thought capture card that appears when the user taps "I'm thinking" | ||
| struct ThoughtCaptureView: View { | ||
| let onCapture: (String) -> Void | ||
| let onDismiss: () -> Void | ||
|
|
||
| @State private var text = "" | ||
| @FocusState private var isFocused: Bool | ||
|
|
||
| private var trimmedText: String { text.trimmingCharacters(in: .whitespacesAndNewlines) } | ||
|
|
||
| var body: some View { | ||
| VStack(spacing: SPSpacing.s2) { | ||
| HStack { | ||
| Text("capture this thought") | ||
| .font(SPFont.mono(11)) | ||
| .foregroundStyle(SPColor.amberText) | ||
| .tracking(1) | ||
| Spacer() | ||
| Button { | ||
| onDismiss() | ||
| } label: { | ||
| Image(systemName: "xmark") | ||
| .font(.system(size: 12, weight: .medium)) | ||
| .foregroundStyle(Color(SPColor.fg4)) | ||
| } | ||
| } | ||
|
|
||
| TextField("what were you thinking about?", text: $text) | ||
| .font(SPFont.serifItalic(15)) | ||
| .foregroundStyle(Color(SPColor.fg)) | ||
| .focused($isFocused) | ||
| .onSubmit { | ||
| if !trimmedText.isEmpty { | ||
| onCapture(trimmedText) | ||
| } | ||
| } | ||
|
|
||
| HStack { | ||
| Spacer() | ||
| Button { | ||
| if !trimmedText.isEmpty { | ||
| onCapture(trimmedText) | ||
| } else { | ||
| onDismiss() | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } label: { | ||
| Text(trimmedText.isEmpty ? "skip" : "save") | ||
| .font(SPFont.mono(12, weight: .medium)) | ||
| .foregroundStyle(trimmedText.isEmpty ? Color(SPColor.fg4) : SPColor.amber) | ||
| } | ||
| } | ||
| } | ||
| .padding(SPSpacing.s3) | ||
| .background(SPColor.amberBgFaint) | ||
| .clipShape(RoundedRectangle(cornerRadius: 12)) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(SPColor.amberBorderSubtle) | ||
| ) | ||
| .onAppear { isFocused = true } | ||
| } | ||
| } | ||
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 |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import SwiftUI | ||
|
|
||
| struct MainTabView: View { | ||
| let appVM: AppViewModel | ||
| @State private var selectedTab = 0 | ||
|
|
||
| var body: some View { | ||
| TabView(selection: $selectedTab) { | ||
| HomeView(appVM: appVM) | ||
| .tabItem { | ||
| Label("HOME", systemImage: "house") | ||
| } | ||
| .tag(0) | ||
|
|
||
| HistoryView(appVM: appVM) | ||
| .tabItem { | ||
| Label("PROGRESS", systemImage: "chart.bar") | ||
| } | ||
| .tag(1) | ||
|
|
||
| ThoughtJournalView() | ||
| .tabItem { | ||
| Label("JOURNAL", systemImage: "book") | ||
| } | ||
| .tag(2) | ||
|
|
||
| PublicBoardView(currentUsername: appVM.currentUser?.username) | ||
| .tabItem { | ||
| Label("BOARD", systemImage: "person.3") | ||
| } | ||
| .tag(3) | ||
|
|
||
| SettingsView(appVM: appVM) | ||
| .tabItem { | ||
| Label("SETTINGS", systemImage: "gearshape") | ||
| } | ||
| .tag(4) | ||
| } | ||
| .tint(SPColor.green) | ||
| .onAppear { | ||
| Self.configureTabBarAppearance() | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private static var tabBarConfigured = false | ||
|
|
||
| private static func configureTabBarAppearance() { | ||
| guard !tabBarConfigured else { return } | ||
| tabBarConfigured = true | ||
| let appearance = UITabBarAppearance() | ||
| appearance.configureWithOpaqueBackground() | ||
| appearance.backgroundColor = UIColor(SPColor.bg) | ||
| UITabBar.appearance().standardAppearance = appearance | ||
| UITabBar.appearance().scrollEdgeAppearance = appearance | ||
| } | ||
| } | ||
20 changes: 20 additions & 0 deletions
20
ios/StillPointApp/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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "colors" : [ | ||
| { | ||
| "color" : { | ||
| "color-space" : "srgb", | ||
| "components" : { | ||
| "alpha" : "1.000", | ||
| "blue" : "0.502", | ||
| "green" : "0.871", | ||
| "red" : "0.290" | ||
| } | ||
| }, | ||
| "idiom" : "universal" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
13 changes: 13 additions & 0 deletions
13
ios/StillPointApp/Resources/Assets.xcassets/AppIcon.appiconset/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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "images" : [ | ||
| { | ||
| "idiom" : "universal", | ||
| "platform" : "ios", | ||
| "size" : "1024x1024" | ||
| } | ||
| ], | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } | ||
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 |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
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 |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import SwiftUI | ||
| import SwiftData | ||
| import StillPointShared | ||
|
|
||
| @main | ||
| struct StillPointApp: App { | ||
| var body: some Scene { | ||
| WindowGroup { | ||
| RootView() | ||
| .preferredColorScheme(.dark) | ||
| } | ||
| .modelContainer(for: [User.self, Session.self, Thought.self]) | ||
| } | ||
| } |
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.