Skip to content

Commit 01dd85b

Browse files
committed
feat(demo): add demo mode launcher and update screenshots
Add scripts/demo.sh for launching app with demo modes for screenshots. Update all documentation screenshots and README with new features: - Multiple icon styles (6 options) - Pacing indicator (flame warning) - Settings description
1 parent 533af2f commit 01dd85b

18 files changed

Lines changed: 428 additions & 0 deletions
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1600"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "68EC6CB62ED4AD85009E99EE"
19+
BuildableName = "ClaudeMeter.app"
20+
BlueprintName = "ClaudeMeter"
21+
ReferencedContainer = "container:ClaudeMeter.xcodeproj">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO"
35+
parallelizable = "YES">
36+
<BuildableReference
37+
BuildableIdentifier = "primary"
38+
BlueprintIdentifier = "68EC6CC62ED4AD87009E99EE"
39+
BuildableName = "ClaudeMeterTests.xctest"
40+
BlueprintName = "ClaudeMeterTests"
41+
ReferencedContainer = "container:ClaudeMeter.xcodeproj">
42+
</BuildableReference>
43+
</TestableReference>
44+
</Testables>
45+
</TestAction>
46+
<LaunchAction
47+
buildConfiguration = "Debug"
48+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
49+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
50+
launchStyle = "0"
51+
useCustomWorkingDirectory = "NO"
52+
ignoresPersistentStateOnLaunch = "NO"
53+
debugDocumentVersioning = "YES"
54+
debugServiceExtension = "internal"
55+
allowLocationSimulation = "YES">
56+
<BuildableProductRunnable
57+
runnableDebuggingMode = "0">
58+
<BuildableReference
59+
BuildableIdentifier = "primary"
60+
BlueprintIdentifier = "68EC6CB62ED4AD85009E99EE"
61+
BuildableName = "ClaudeMeter.app"
62+
BlueprintName = "ClaudeMeter"
63+
ReferencedContainer = "container:ClaudeMeter.xcodeproj">
64+
</BuildableReference>
65+
</BuildableProductRunnable>
66+
<CommandLineArguments>
67+
<CommandLineArgument
68+
argument = "--demo safeUsage"
69+
isEnabled = "NO">
70+
</CommandLineArgument>
71+
<CommandLineArgument
72+
argument = "--demo warningUsage"
73+
isEnabled = "NO">
74+
</CommandLineArgument>
75+
<CommandLineArgument
76+
argument = "--demo criticalUsage"
77+
isEnabled = "NO">
78+
</CommandLineArgument>
79+
<CommandLineArgument
80+
argument = "--demo exceededUsage"
81+
isEnabled = "NO">
82+
</CommandLineArgument>
83+
<CommandLineArgument
84+
argument = "--demo withSonnet"
85+
isEnabled = "NO">
86+
</CommandLineArgument>
87+
<CommandLineArgument
88+
argument = "--demo loading"
89+
isEnabled = "NO">
90+
</CommandLineArgument>
91+
<CommandLineArgument
92+
argument = "--demo error"
93+
isEnabled = "NO">
94+
</CommandLineArgument>
95+
<CommandLineArgument
96+
argument = "--demo setupWizard"
97+
isEnabled = "NO">
98+
</CommandLineArgument>
99+
</CommandLineArguments>
100+
</LaunchAction>
101+
<ProfileAction
102+
buildConfiguration = "Release"
103+
shouldUseLaunchSchemeArgsEnv = "YES"
104+
savedToolIdentifier = ""
105+
useCustomWorkingDirectory = "NO"
106+
debugDocumentVersioning = "YES">
107+
<BuildableProductRunnable
108+
runnableDebuggingMode = "0">
109+
<BuildableReference
110+
BuildableIdentifier = "primary"
111+
BlueprintIdentifier = "68EC6CB62ED4AD85009E99EE"
112+
BuildableName = "ClaudeMeter.app"
113+
BlueprintName = "ClaudeMeter"
114+
ReferencedContainer = "container:ClaudeMeter.xcodeproj">
115+
</BuildableReference>
116+
</BuildableProductRunnable>
117+
</ProfileAction>
118+
<AnalyzeAction
119+
buildConfiguration = "Debug">
120+
</AnalyzeAction>
121+
<ArchiveAction
122+
buildConfiguration = "Release"
123+
revealArchiveInOrganizer = "YES">
124+
</ArchiveAction>
125+
</Scheme>

ClaudeMeter/App/AppDelegate.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
1313
private var appModel: AppModel?
1414
private var menuBarManager: MenuBarManager?
1515

16+
#if DEBUG
17+
private var isDemoMode: Bool = false
18+
#endif
19+
1620
func configure(appModel: AppModel) {
1721
self.appModel = appModel
1822
}
1923

24+
#if DEBUG
25+
func configureDemoMode(_ enabled: Bool) {
26+
isDemoMode = enabled
27+
}
28+
#endif
29+
2030
func applicationDidFinishLaunching(_ notification: Notification) {
2131
guard let appModel else {
2232
let fallbackModel = AppModel()
@@ -34,6 +44,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
3444
private func startMenuBar(with appModel: AppModel) {
3545
let manager = MenuBarManager(appModel: appModel)
3646
menuBarManager = manager
47+
48+
#if DEBUG
49+
if isDemoMode {
50+
manager.startWithoutBootstrap()
51+
} else {
52+
manager.start()
53+
}
54+
#else
3755
manager.start()
56+
#endif
3857
}
3958
}

ClaudeMeter/App/AppModel.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,25 @@ final class AppModel {
227227
}
228228
}
229229

230+
// MARK: - Demo Mode
231+
232+
#if DEBUG
233+
/// Applies demo state for App Store screenshots.
234+
/// Skips normal bootstrap and sets state directly.
235+
func applyDemoState(
236+
usageData: UsageData?,
237+
isSetupComplete: Bool,
238+
errorMessage: String?,
239+
isLoading: Bool
240+
) {
241+
self.usageData = usageData
242+
self.isSetupComplete = isSetupComplete
243+
self.errorMessage = errorMessage
244+
self.isLoading = isLoading
245+
self.isReady = true
246+
self.hasLoadedSettings = true
247+
// Don't start refresh loop or wake observer in demo mode
248+
}
249+
#endif
250+
230251
}

ClaudeMeter/App/ClaudeMeterApp.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ struct ClaudeMeterApp: App {
1717
let model = AppModel()
1818
_appModel = State(initialValue: model)
1919
appDelegate.configure(appModel: model)
20+
21+
#if DEBUG
22+
if let demoMode = DemoMode.fromArguments() {
23+
appDelegate.configureDemoMode(true)
24+
DemoDataFactory.configure(model, for: demoMode)
25+
}
26+
#endif
2027
}
2128

2229
var body: some Scene {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// DemoDataFactory.swift
3+
// ClaudeMeter
4+
//
5+
// Created by Edd on 2026-02-02.
6+
//
7+
8+
#if DEBUG
9+
import Foundation
10+
11+
/// Factory for creating demo state for App Store screenshots.
12+
@MainActor
13+
enum DemoDataFactory {
14+
/// Configures the app model for the given demo mode.
15+
static func configure(_ appModel: AppModel, for mode: DemoMode) {
16+
switch mode {
17+
case .safeUsage:
18+
appModel.applyDemoState(
19+
usageData: makeUsageData(sessionPercentage: 42, weeklyPercentage: 10),
20+
isSetupComplete: true,
21+
errorMessage: nil,
22+
isLoading: false
23+
)
24+
25+
case .warningUsage:
26+
appModel.applyDemoState(
27+
usageData: makeUsageData(sessionPercentage: 72, weeklyPercentage: 45),
28+
isSetupComplete: true,
29+
errorMessage: nil,
30+
isLoading: false
31+
)
32+
33+
case .criticalUsage:
34+
appModel.applyDemoState(
35+
usageData: makeUsageData(sessionPercentage: 92, weeklyPercentage: 85),
36+
isSetupComplete: true,
37+
errorMessage: nil,
38+
isLoading: false
39+
)
40+
41+
case .exceededUsage:
42+
appModel.applyDemoState(
43+
usageData: makeUsageData(sessionPercentage: 105, weeklyPercentage: 100),
44+
isSetupComplete: true,
45+
errorMessage: nil,
46+
isLoading: false
47+
)
48+
49+
case .withSonnet:
50+
appModel.applyDemoState(
51+
usageData: makeUsageData(
52+
sessionPercentage: 65,
53+
weeklyPercentage: 40,
54+
sonnetPercentage: 25
55+
),
56+
isSetupComplete: true,
57+
errorMessage: nil,
58+
isLoading: false
59+
)
60+
appModel.settings.isSonnetUsageShown = true
61+
62+
case .loading:
63+
appModel.applyDemoState(
64+
usageData: nil,
65+
isSetupComplete: true,
66+
errorMessage: nil,
67+
isLoading: true
68+
)
69+
70+
case .error:
71+
appModel.applyDemoState(
72+
usageData: makeUsageData(sessionPercentage: 55, weeklyPercentage: 30),
73+
isSetupComplete: true,
74+
errorMessage: "Unable to connect to Claude.ai. Check your internet connection.",
75+
isLoading: false
76+
)
77+
78+
case .setupWizard:
79+
appModel.applyDemoState(
80+
usageData: nil,
81+
isSetupComplete: false,
82+
errorMessage: nil,
83+
isLoading: false
84+
)
85+
}
86+
}
87+
88+
/// Creates UsageData with the given percentages.
89+
private static func makeUsageData(
90+
sessionPercentage: Double,
91+
weeklyPercentage: Double,
92+
sonnetPercentage: Double? = nil
93+
) -> UsageData {
94+
let sessionResetAt = Date().addingTimeInterval(3 * 3600) // 3 hours from now
95+
let weeklyResetAt = Date().addingTimeInterval(4 * 24 * 3600) // 4 days from now
96+
97+
let sessionUsage = UsageLimit(utilization: sessionPercentage, resetAt: sessionResetAt)
98+
let weeklyUsage = UsageLimit(utilization: weeklyPercentage, resetAt: weeklyResetAt)
99+
100+
let sonnetUsage: UsageLimit?
101+
if let sonnetPercentage {
102+
sonnetUsage = UsageLimit(utilization: sonnetPercentage, resetAt: weeklyResetAt)
103+
} else {
104+
sonnetUsage = nil
105+
}
106+
107+
return UsageData(
108+
sessionUsage: sessionUsage,
109+
weeklyUsage: weeklyUsage,
110+
sonnetUsage: sonnetUsage,
111+
lastUpdated: Date()
112+
)
113+
}
114+
}
115+
#endif
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// DemoMode.swift
3+
// ClaudeMeter
4+
//
5+
// Created by Edd on 2026-02-02.
6+
//
7+
8+
#if DEBUG
9+
import Foundation
10+
11+
/// Demo modes for App Store screenshots and testing.
12+
/// Launch with `--demo <mode>` to activate.
13+
enum DemoMode: String, CaseIterable {
14+
case safeUsage
15+
case warningUsage
16+
case criticalUsage
17+
case exceededUsage
18+
case withSonnet
19+
case loading
20+
case error
21+
case setupWizard
22+
23+
static func fromArguments() -> DemoMode? {
24+
let args = CommandLine.arguments
25+
guard let index = args.firstIndex(of: "--demo"),
26+
index + 1 < args.count else {
27+
return nil
28+
}
29+
return DemoMode(rawValue: args[index + 1])
30+
}
31+
32+
var description: String {
33+
switch self {
34+
case .safeUsage: "Low usage - safe state"
35+
case .warningUsage: "Medium usage - warning state"
36+
case .criticalUsage: "High usage - critical state"
37+
case .exceededUsage: "Over limit - exceeded state"
38+
case .withSonnet: "Shows Sonnet usage card"
39+
case .loading: "Loading spinner visible"
40+
case .error: "Error banner displayed"
41+
case .setupWizard: "First-time setup screen"
42+
}
43+
}
44+
}
45+
#endif

ClaudeMeter/Views/MenuBar/MenuBarManager.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ final class MenuBarManager {
3434
}
3535
}
3636

37+
#if DEBUG
38+
/// Starts the menu bar without calling bootstrap.
39+
/// Used in demo mode when state is pre-configured.
40+
func startWithoutBootstrap() {
41+
setupStatusItem()
42+
createPopover()
43+
observeIconUpdates()
44+
observeOpenPopoverRequests()
45+
}
46+
#endif
47+
3748
deinit {
3849
if let openUsageObserver {
3950
NotificationCenter.default.removeObserver(openUsageObserver)

0 commit comments

Comments
 (0)