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