@ondeinference / klepon / commits / 47bac06

Add WatchBrandSystem with reusable watch UI components

Seto Elkahfi committed May 14, 2026 at 17:41 UTC 47bac064abd2f92c7214c20cf9b98975d9032dcc
4 files changed +394 -96
Klepon.xcodeproj/project.pbxproj
+58 -5
@@ -42,9 +42,11 @@
42 B1F1D1A453150D3BBB0146EF /* RecentlyViewedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45070309D2C7058BAA44CD2 /* RecentlyViewedStore.swift */; };
43 B34377720179454F9B9184C5 /* OndeGuideEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0D53A5F9D5897E15506C20 /* OndeGuideEngine.swift */; };
44 B7DE9EC8BE998E6719C88E83 /* guide_entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 248F359109A08A60F792C42D /* guide_entries.json */; };
45 + C2EA2682A6545802C06E00CC /* WatchBrandSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217E7D4050D471955FF3AB13 /* WatchBrandSystem.swift */; };
46 C709C82E7F0BCF398BCD9C2F /* ContentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2891A1AFBB59928FC5B1E66 /* ContentRepository.swift */; };
47 D99AAE1CDE51139840A52607 /* KleponMetadataRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E1C50BFBE2EF5F37941590 /* KleponMetadataRow.swift */; };
48 DA915BA8B2F732E7D62CF936 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E7B5E3D9A1906C2FED3032 /* EmptyStateView.swift */; };
49 + E45F257E3D35DC565B21B299 /* KleponColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C675ABBBA25BBA25A8DE29BE /* KleponColor.swift */; };
50 F1431179B54CEB23FFFCB2AE /* KleponApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDE2A1E73EB15970A17A894 /* KleponApp.swift */; };
51 FA423A76BE098A9911044246 /* AskSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC6E7DC56DD8E1459EFC892 /* AskSheetView.swift */; };
52 FD0E1BA888E3CE73875D58FF /* RecentSearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643D52BCEDF66CC8FE8A1234 /* RecentSearchStore.swift */; };
@@ -55,6 +57,7 @@
57 0BC0C4C22A5134DB058E41F4 /* FavoritesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesStore.swift; sourceTree = "<group>"; };
58 13418896CA28250CEEE7592E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
59 1FDDB0F88FF430ABD679AD89 /* collections.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = collections.json; sourceTree = "<group>"; };
60 + 217E7D4050D471955FF3AB13 /* WatchBrandSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBrandSystem.swift; sourceTree = "<group>"; };
61 248F359109A08A60F792C42D /* guide_entries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = guide_entries.json; sourceTree = "<group>"; };
62 2B499D3BF7AC80D31907D87F /* OndeEnvironmentBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OndeEnvironmentBootstrap.swift; sourceTree = "<group>"; };
63 3719031080B59898133810F7 /* GuideAnswerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideAnswerService.swift; sourceTree = "<group>"; };
@@ -137,6 +140,7 @@
140 054656155630B064F785B0D7 /* Assets.xcassets */,
141 67FAA69C42F1845ECB79AC16 /* Info.plist */,
142 6EFBF98118F066D3C7FA14B2 /* KleponWatchApp.swift */,
143 + 217E7D4050D471955FF3AB13 /* WatchBrandSystem.swift */,
144 4C77AF5EB4B17D087FA522F6 /* WatchContentLoader.swift */,
145 EDBB4F5D4C98D036AC52E193 /* WatchHomeView.swift */,
146 );
@@ -307,6 +311,7 @@
311 142193A63A26ACC180E1EB37 /* Sources */,
312 BC5A6BBAFB4A1B61000065D1 /* Resources */,
313 F72797E506E8FA89264A5C68 /* Frameworks */,
314 + 005255530CCC905544DBE9A8 /* Update CFBundleVersion */,
315 );
316 buildRules = (
317 );
@@ -326,6 +331,7 @@
331 buildPhases = (
332 57F190B6EB353A926A214B19 /* Sources */,
333 0DEE2337CA5E6D48C9C110A2 /* Resources */,
334 + 4CD2E0A71BF112F74656BFFD /* Update CFBundleVersion */,
335 );
336 buildRules = (
337 );
@@ -393,6 +399,49 @@
399 };
400 /* End PBXResourcesBuildPhase section */
401
402 +/* Begin PBXShellScriptBuildPhase section */
403 + 005255530CCC905544DBE9A8 /* Update CFBundleVersion */ = {
404 + isa = PBXShellScriptBuildPhase;
405 + alwaysOutOfDate = 1;
406 + buildActionMask = 2147483647;
407 + files = (
408 + );
409 + inputFileListPaths = (
410 + );
411 + inputPaths = (
412 + );
413 + name = "Update CFBundleVersion";
414 + outputFileListPaths = (
415 + );
416 + outputPaths = (
417 + );
418 + runOnlyForDeploymentPostprocessing = 0;
419 + shellPath = /bin/sh;
420 + shellScript = "set -eu\n\nBUNDLE_VERSION=\"$(date -u +\"%Y%m%d.%H%M\")\"\nINFO_PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n\nif [ ! -f \"${INFO_PLIST}\" ]; then\n echo \"error: Info.plist not found at ${INFO_PLIST}\"\n exit 1\nfi\n\nif ! /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion ${BUNDLE_VERSION}\" \"${INFO_PLIST}\" >/dev/null 2>&1; then\n /usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string ${BUNDLE_VERSION}\" \"${INFO_PLIST}\"\nfi\n\necho \"Updated CFBundleVersion to ${BUNDLE_VERSION}\"\n";
421 + showEnvVarsInLog = 0;
422 + };
423 + 4CD2E0A71BF112F74656BFFD /* Update CFBundleVersion */ = {
424 + isa = PBXShellScriptBuildPhase;
425 + alwaysOutOfDate = 1;
426 + buildActionMask = 2147483647;
427 + files = (
428 + );
429 + inputFileListPaths = (
430 + );
431 + inputPaths = (
432 + );
433 + name = "Update CFBundleVersion";
434 + outputFileListPaths = (
435 + );
436 + outputPaths = (
437 + );
438 + runOnlyForDeploymentPostprocessing = 0;
439 + shellPath = /bin/sh;
440 + shellScript = "set -eu\n\nBUNDLE_VERSION=\"$(date -u +\"%Y%m%d.%H%M\")\"\nINFO_PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n\nif [ ! -f \"${INFO_PLIST}\" ]; then\n echo \"error: Info.plist not found at ${INFO_PLIST}\"\n exit 1\nfi\n\nif ! /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion ${BUNDLE_VERSION}\" \"${INFO_PLIST}\" >/dev/null 2>&1; then\n /usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string ${BUNDLE_VERSION}\" \"${INFO_PLIST}\"\nfi\n\necho \"Updated CFBundleVersion to ${BUNDLE_VERSION}\"\n";
441 + showEnvVarsInLog = 0;
442 + };
443 +/* End PBXShellScriptBuildPhase section */
444 +
445 /* Begin PBXSourcesBuildPhase section */
446 142193A63A26ACC180E1EB37 /* Sources */ = {
447 isa = PBXSourcesBuildPhase;
@@ -436,7 +485,9 @@
485 buildActionMask = 2147483647;
486 files = (
487 03D16A2386EB68FED8D92D52 /* GuideEntry.swift in Sources */,
488 + E45F257E3D35DC565B21B299 /* KleponColor.swift in Sources */,
489 36AA193A3B7EF7CB25B6E0BE /* KleponWatchApp.swift in Sources */,
490 + C2EA2682A6545802C06E00CC /* WatchBrandSystem.swift in Sources */,
491 1E7AAEA8007BFE4ED6FE4892 /* WatchContentLoader.swift in Sources */,
492 4D63E231AC9630B2C225DF22 /* WatchHomeView.swift in Sources */,
493 );
@@ -539,16 +590,17 @@
590 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
591 GENERATE_INFOPLIST_FILE = YES;
592 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
593 + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
594 INFOPLIST_KEY_UIUserInterfaceStyle = Light;
595 LD_RUNPATH_SEARCH_PATHS = (
596 "$(inherited)",
597 "@executable_path/Frameworks",
598 );
547 - MARKETING_VERSION = 1.0.0;
599 + MARKETING_VERSION = 1.0;
600 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon;
601 SDKROOT = iphoneos;
602 SWIFT_OPTIMIZATION_LEVEL = "-O";
551 - TARGETED_DEVICE_FAMILY = "1,2";
603 + TARGETED_DEVICE_FAMILY = 1;
604 };
605 name = Release;
606 };
@@ -559,6 +611,7 @@
611 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
612 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
613 CURRENT_PROJECT_VERSION = 1;
614 + DEVELOPMENT_TEAM = 2TQF86ZACD;
615 ENABLE_USER_SCRIPT_SANDBOXING = YES;
616 "EXCLUDED_ARCHS[sdk=watchsimulator*]" = x86_64;
617 GENERATE_INFOPLIST_FILE = NO;
@@ -648,21 +701,21 @@
701 CODE_SIGN_ENTITLEMENTS = Klepon/Klepon.entitlements;
702 CODE_SIGN_IDENTITY = "iPhone Developer";
703 CURRENT_PROJECT_VERSION = 1;
651 - DEVELOPMENT_TEAM = 2TQF86ZACD;
704 ENABLE_USER_SCRIPT_SANDBOXING = YES;
705 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
706 GENERATE_INFOPLIST_FILE = YES;
707 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
708 + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
709 INFOPLIST_KEY_UIUserInterfaceStyle = Light;
710 LD_RUNPATH_SEARCH_PATHS = (
711 "$(inherited)",
712 "@executable_path/Frameworks",
713 );
661 - MARKETING_VERSION = 1.0.0;
714 + MARKETING_VERSION = 1.0;
715 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon;
716 SDKROOT = iphoneos;
717 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
665 - TARGETED_DEVICE_FAMILY = "1,2";
718 + TARGETED_DEVICE_FAMILY = 1;
719 };
720 name = Debug;
721 };
KleponWatch/WatchBrandSystem.swift new
+111
@@ -0,0 +1,111 @@
1 +import SwiftUI
2 +
3 +enum WatchKleponTypography {
4 + static let heroTitle = Font.system(size: 22, weight: .semibold, design: .serif)
5 + static let sectionTitle = Font.system(size: 16, weight: .semibold, design: .serif)
6 + static let cardTitle = Font.system(size: 17, weight: .semibold, design: .default)
7 + static let body = Font.system(size: 14, weight: .regular, design: .default)
8 + static let bodySecondary = Font.system(size: 12, weight: .regular, design: .default)
9 + static let caption = Font.system(size: 11, weight: .semibold, design: .default)
10 +}
11 +
12 +struct WatchKleponCard<Content: View>: View {
13 + let padding: CGFloat
14 + @ViewBuilder var content: Content
15 +
16 + init(padding: CGFloat = 14, @ViewBuilder content: () -> Content) {
17 + self.padding = padding
18 + self.content = content()
19 + }
20 +
21 + var body: some View {
22 + content
23 + .padding(padding)
24 + .frame(maxWidth: .infinity, alignment: .leading)
25 + .background(
26 + RoundedRectangle(cornerRadius: 22, style: .continuous)
27 + .fill(KleponColor.surface)
28 + )
29 + .overlay(
30 + RoundedRectangle(cornerRadius: 22, style: .continuous)
31 + .stroke(KleponColor.divider, lineWidth: 1)
32 + )
33 + }
34 +}
35 +
36 +struct WatchKleponSectionHeader: View {
37 + let title: String
38 + let subtitle: String?
39 +
40 + init(_ title: String, subtitle: String? = nil) {
41 + self.title = title
42 + self.subtitle = subtitle
43 + }
44 +
45 + var body: some View {
46 + VStack(alignment: .leading, spacing: 3) {
47 + Text(title)
48 + .font(WatchKleponTypography.sectionTitle)
49 + .foregroundStyle(KleponColor.textPrimary)
50 +
51 + if let subtitle {
52 + Text(subtitle)
53 + .font(WatchKleponTypography.bodySecondary)
54 + .foregroundStyle(KleponColor.textSecondary)
55 + }
56 + }
57 + .frame(maxWidth: .infinity, alignment: .leading)
58 + }
59 +}
60 +
61 +struct WatchKleponChip: View {
62 + let title: String
63 + var icon: String? = nil
64 +
65 + var body: some View {
66 + HStack(spacing: 5) {
67 + if let icon {
68 + Image(systemName: icon)
69 + .font(.system(size: 10, weight: .semibold))
70 + }
71 +
72 + Text(title)
73 + .font(WatchKleponTypography.caption)
74 + .lineLimit(1)
75 + .minimumScaleFactor(0.8)
76 + }
77 + .foregroundStyle(KleponColor.textPrimary)
78 + .padding(.horizontal, 10)
79 + .padding(.vertical, 6)
80 + .background(Capsule().fill(KleponColor.chipBackground))
81 + }
82 +}
83 +
84 +struct WatchKleponChipGrid: View {
85 + let items: [String]
86 +
87 + var body: some View {
88 + LazyVGrid(
89 + columns: [GridItem(.adaptive(minimum: 72), spacing: 8)],
90 + alignment: .leading,
91 + spacing: 8
92 + ) {
93 + ForEach(items, id: \.self) { item in
94 + WatchKleponChip(title: item)
95 + }
96 + }
97 + }
98 +}
99 +
100 +struct WatchKleponFooterCard: View {
101 + let message: String
102 +
103 + var body: some View {
104 + WatchKleponCard {
105 + Text(message)
106 + .font(WatchKleponTypography.bodySecondary)
107 + .foregroundStyle(KleponColor.textSecondary)
108 + .lineSpacing(2)
109 + }
110 + }
111 +}
KleponWatch/WatchHomeView.swift
+179 -91
@@ -4,13 +4,13 @@ private struct WatchGuideStore {
4 let entries: [GuideEntry] = WatchContentLoader().loadEntries()
5
6 var featuredEntries: [GuideEntry] {
7 - Array(entries.filter(\.isFeatured).prefix(4))
7 + Array(entries.filter(\.isFeatured).prefix(3))
8 }
9
10 var browseEntries: [GuideEntry] {
11 let featuredIDs = Set(featuredEntries.map(\.id))
12 let remaining = entries.filter { !featuredIDs.contains($0.id) }
13 - return Array((featuredEntries + remaining).prefix(8))
13 + return Array((featuredEntries + remaining).prefix(6))
14 }
15 }
16
@@ -19,144 +19,232 @@ struct WatchHomeView: View {
19
20 var body: some View {
21 NavigationStack {
22 - if store.entries.isEmpty {
23 - ScrollView {
24 - VStack(spacing: 10) {
25 - Image(systemName: "fork.knife.circle")
26 - .font(.title2)
27 - .foregroundStyle(.secondary)
28 -
29 - Text("Guide unavailable")
30 - .font(.headline)
31 -
32 - Text(
33 - "Open Klepon on iPhone first if you want the fuller guide and private follow-up answers."
34 - )
35 - .font(.footnote)
36 - .foregroundStyle(.secondary)
37 - .multilineTextAlignment(.center)
38 - }
39 - .padding()
40 - }
41 - .navigationTitle("Klepon")
42 - } else {
43 - List {
44 - if !store.featuredEntries.isEmpty {
45 - Section("Start here") {
22 + ScrollView {
23 + LazyVStack(alignment: .leading, spacing: 14) {
24 + introCard
25 +
26 + if store.entries.isEmpty {
27 + emptyStateCard
28 + } else {
29 + if !store.featuredEntries.isEmpty {
30 + WatchKleponSectionHeader(
31 + "Start here",
32 + subtitle: "A warm first taste of the guide"
33 + )
34 +
35 ForEach(store.featuredEntries) { entry in
36 NavigationLink {
37 WatchEntryDetailView(entry: entry)
38 } label: {
50 - EntryRow(entry: entry)
39 + WatchEntryCard(entry: entry)
40 }
41 + .buttonStyle(.plain)
42 }
43 }
54 - }
44
56 - Section("Browse") {
45 + WatchKleponSectionHeader(
46 + "Browse",
47 + subtitle: "A few essentials that feel good on the wrist"
48 + )
49 +
50 ForEach(store.browseEntries) { entry in
51 NavigationLink {
52 WatchEntryDetailView(entry: entry)
53 } label: {
61 - EntryRow(entry: entry)
54 + WatchEntryCard(entry: entry)
55 }
56 + .buttonStyle(.plain)
57 }
64 - }
58
66 - Section {
67 - Text(
68 - "Browse on watch. Use the iPhone app for the full guide and private follow-up answers."
59 + WatchKleponFooterCard(
60 + message:
61 + "Browse on watch. Use the iPhone app for the fuller guide and private follow-up answers."
62 )
70 - .font(.footnote)
71 - .foregroundStyle(.secondary)
63 }
64 }
74 - .navigationTitle("Klepon")
65 + .padding(.horizontal, 12)
66 + .padding(.vertical, 10)
67 + }
68 + .background(KleponColor.background.ignoresSafeArea())
69 + .navigationTitle("Klepon")
70 + }
71 + }
72 +
73 + private var introCard: some View {
74 + WatchKleponCard(padding: 16) {
75 + VStack(alignment: .leading, spacing: 10) {
76 + Text("Taste of Indonesia")
77 + .font(WatchKleponTypography.heroTitle)
78 + .foregroundStyle(KleponColor.textPrimary)
79 +
80 + Text(
81 + "A calmer way to browse dishes, flavors, and food traditions from your wrist."
82 + )
83 + .font(WatchKleponTypography.body)
84 + .foregroundStyle(KleponColor.textSecondary)
85 + .lineSpacing(2)
86 +
87 + HStack(spacing: 8) {
88 + WatchKleponChip(title: "Curated")
89 + WatchKleponChip(title: "Private", icon: "lock.fill")
90 + }
91 + }
92 + }
93 + }
94 +
95 + private var emptyStateCard: some View {
96 + WatchKleponCard {
97 + VStack(alignment: .leading, spacing: 10) {
98 + Image(systemName: "fork.knife.circle.fill")
99 + .font(.system(size: 22, weight: .semibold))
100 + .foregroundStyle(KleponColor.accent)
101 +
102 + Text("Guide unavailable")
103 + .font(WatchKleponTypography.cardTitle)
104 + .foregroundStyle(KleponColor.textPrimary)
105 +
106 + Text(
107 + "Open Klepon on iPhone first if you want the fuller guide and private follow-up answers."
108 + )
109 + .font(WatchKleponTypography.bodySecondary)
110 + .foregroundStyle(KleponColor.textSecondary)
111 + .lineSpacing(2)
112 }
113 }
114 }
115 }
116
80 -private struct EntryRow: View {
117 +private struct WatchEntryCard: View {
118 let entry: GuideEntry
119
120 var body: some View {
84 - VStack(alignment: .leading, spacing: 4) {
85 - Text(entry.title)
86 - .font(.headline)
87 -
88 - Text(entry.subtitle)
89 - .font(.footnote)
90 - .foregroundStyle(.secondary)
91 - .lineLimit(2)
121 + WatchKleponCard {
122 + VStack(alignment: .leading, spacing: 10) {
123 + HStack(alignment: .top, spacing: 10) {
124 + VStack(alignment: .leading, spacing: 6) {
125 + Text(entry.title)
126 + .font(WatchKleponTypography.cardTitle)
127 + .foregroundStyle(KleponColor.textPrimary)
128 + .lineLimit(2)
129 +
130 + Text(entry.subtitle)
131 + .font(WatchKleponTypography.bodySecondary)
132 + .foregroundStyle(KleponColor.textSecondary)
133 + .lineLimit(2)
134 + }
135 +
136 + Spacer(minLength: 0)
137 +
138 + Image(systemName: entry.kind.symbolName)
139 + .font(.system(size: 12, weight: .semibold))
140 + .foregroundStyle(KleponColor.accentWarm)
141 + .padding(8)
142 + .background(Circle().fill(KleponColor.surfaceSecondary))
143 + }
144 +
145 + HStack(spacing: 8) {
146 + WatchKleponChip(title: entry.kind.displayTitle, icon: entry.kind.symbolName)
147 +
148 + if let region = entry.region {
149 + Text(region)
150 + .font(WatchKleponTypography.caption)
151 + .foregroundStyle(KleponColor.accentWarm)
152 + .lineLimit(1)
153 + }
154 + }
155 + }
156 }
93 - .padding(.vertical, 2)
157 }
158 }
159
160 private struct WatchEntryDetailView: View {
161 let entry: GuideEntry
162
163 + private var tasteItems: [String] {
164 + Array(entry.tasteNotes.prefix(4))
165 + }
166 +
167 + private var highlightItems: [String] {
168 + Array(entry.highlights.prefix(4))
169 + }
170 +
171 var body: some View {
172 ScrollView {
102 - VStack(alignment: .leading, spacing: 12) {
103 - Text(entry.title)
104 - .font(.headline)
105 -
106 - Text(entry.subtitle)
107 - .font(.subheadline)
108 - .foregroundStyle(.secondary)
109 -
110 - if let region = entry.region {
111 - Text(region)
112 - .font(.footnote.weight(.semibold))
113 - .foregroundStyle(.secondary)
173 + LazyVStack(alignment: .leading, spacing: 12) {
174 + WatchKleponCard(padding: 16) {
175 + VStack(alignment: .leading, spacing: 10) {
176 + Text(entry.title)
177 + .font(WatchKleponTypography.heroTitle)
178 + .foregroundStyle(KleponColor.textPrimary)
179 +
180 + Text(entry.subtitle)
181 + .font(WatchKleponTypography.body)
182 + .foregroundStyle(KleponColor.textSecondary)
183 + .lineSpacing(2)
184 +
185 + HStack(spacing: 8) {
186 + WatchKleponChip(
187 + title: entry.kind.displayTitle,
188 + icon: entry.kind.symbolName
189 + )
190 +
191 + if let region = entry.region {
192 + Text(region)
193 + .font(WatchKleponTypography.caption)
194 + .foregroundStyle(KleponColor.accentWarm)
195 + .lineLimit(1)
196 + }
197 + }
198 + }
199 }
200
116 - Divider()
117 -
118 - Text(entry.summary)
119 - .font(.footnote)
120 -
121 - if !entry.tasteNotes.isEmpty {
122 - VStack(alignment: .leading, spacing: 8) {
123 - Text("Taste")
124 - .font(.footnote.weight(.semibold))
125 - .foregroundStyle(.secondary)
201 + WatchKleponCard {
202 + Text(entry.summary)
203 + .font(WatchKleponTypography.body)
204 + .foregroundStyle(KleponColor.textSecondary)
205 + .lineSpacing(2)
206 + }
207
127 - ForEach(Array(entry.tasteNotes.prefix(3)), id: \.self) { note in
128 - Text("• \(note)")
129 - .font(.footnote)
208 + if !tasteItems.isEmpty {
209 + WatchKleponCard {
210 + VStack(alignment: .leading, spacing: 10) {
211 + WatchKleponSectionHeader("Taste")
212 + WatchKleponChipGrid(items: tasteItems)
213 }
214 }
215 }
216
134 - if !entry.highlights.isEmpty {
135 - Divider()
136 -
137 - VStack(alignment: .leading, spacing: 8) {
138 - Text("Quick notes")
139 - .font(.footnote.weight(.semibold))
140 - .foregroundStyle(.secondary)
141 -
142 - ForEach(Array(entry.highlights.prefix(3)), id: \.self) { note in
143 - Text("• \(note)")
144 - .font(.footnote)
217 + if !highlightItems.isEmpty {
218 + WatchKleponCard {
219 + VStack(alignment: .leading, spacing: 10) {
220 + WatchKleponSectionHeader("Quick notes")
221 +
222 + ForEach(highlightItems, id: \.self) { note in
223 + HStack(alignment: .top, spacing: 8) {
224 + Circle()
225 + .fill(KleponColor.accentWarm)
226 + .frame(width: 5, height: 5)
227 + .padding(.top, 5)
228 +
229 + Text(note)
230 + .font(WatchKleponTypography.bodySecondary)
231 + .foregroundStyle(KleponColor.textSecondary)
232 + .lineSpacing(2)
233 + }
234 + }
235 }
236 }
237 }
238
149 - Divider()
150 -
151 - Text(
152 - "Open Klepon on iPhone if you want the fuller guide and on-device follow-up answers."
239 + WatchKleponFooterCard(
240 + message:
241 + "Open Klepon on iPhone if you want the fuller guide and on-device follow-up answers."
242 )
154 - .font(.footnote)
155 - .foregroundStyle(.secondary)
243 }
157 - .frame(maxWidth: .infinity, alignment: .leading)
158 - .padding()
244 + .padding(.horizontal, 12)
245 + .padding(.vertical, 10)
246 }
247 + .background(KleponColor.background.ignoresSafeArea())
248 .navigationTitle(entry.title)
249 }
250 }
project.yml
+46
@@ -49,6 +49,27 @@ targets:
49 dependencies:
50 - package: Onde
51 product: Onde
52 + postBuildScripts:
53 + - name: Update CFBundleVersion
54 + shell: /bin/sh
55 + basedOnDependencyAnalysis: false
56 + showEnvVars: false
57 + script: |
58 + set -eu
59 +
60 + BUNDLE_VERSION="$(date -u +"%Y%m%d.%H%M")"
61 + INFO_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
62 +
63 + if [ ! -f "${INFO_PLIST}" ]; then
64 + echo "error: Info.plist not found at ${INFO_PLIST}"
65 + exit 1
66 + fi
67 +
68 + if ! /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUNDLE_VERSION}" "${INFO_PLIST}" >/dev/null 2>&1; then
69 + /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string ${BUNDLE_VERSION}" "${INFO_PLIST}"
70 + fi
71 +
72 + echo "Updated CFBundleVersion to ${BUNDLE_VERSION}"
73 settings:
74 base:
75 PRODUCT_BUNDLE_IDENTIFIER: ai.splitfire.klepon
@@ -59,7 +80,10 @@ targets:
80 GENERATE_INFOPLIST_FILE: YES
81 INFOPLIST_KEY_UILaunchScreen_Generation: YES
82 INFOPLIST_KEY_UIUserInterfaceStyle: Light
83 + INFOPLIST_KEY_UISupportedInterfaceOrientations:
84 + - UIInterfaceOrientationPortrait
85 ENABLE_USER_SCRIPT_SANDBOXING: YES
86 + TARGETED_DEVICE_FAMILY: 1
87 "EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64
88 configs:
89 Debug:
@@ -74,8 +98,30 @@ targets:
98 sources:
99 - path: KleponWatch
100 - path: Klepon/Domain/GuideEntry.swift
101 + - path: Klepon/DesignSystem/KleponColor.swift
102 - path: Klepon/Resources/Content
103 buildPhase: resources
104 + postBuildScripts:
105 + - name: Update CFBundleVersion
106 + shell: /bin/sh
107 + basedOnDependencyAnalysis: false
108 + showEnvVars: false
109 + script: |
110 + set -eu
111 +
112 + BUNDLE_VERSION="$(date -u +"%Y%m%d.%H%M")"
113 + INFO_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
114 +
115 + if [ ! -f "${INFO_PLIST}" ]; then
116 + echo "error: Info.plist not found at ${INFO_PLIST}"
117 + exit 1
118 + fi
119 +
120 + if ! /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUNDLE_VERSION}" "${INFO_PLIST}" >/dev/null 2>&1; then
121 + /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string ${BUNDLE_VERSION}" "${INFO_PLIST}"
122 + fi
123 +
124 + echo "Updated CFBundleVersion to ${BUNDLE_VERSION}"
125 settings:
126 base:
127 PRODUCT_BUNDLE_IDENTIFIER: ai.splitfire.klepon.watchkitapp