feat: add in-app toast system with notification fallback for denied permissions

This commit is contained in:
saravanakumardb1 2026-02-27 21:21:46 -08:00
parent 28dfa9f929
commit a1120a56e8
8 changed files with 925 additions and 8 deletions

26
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Xcode
*.xcodeproj/xcuserdata/
*.xcodeproj/project.xcworkspace/xcuserdata/
*.xcworkspace/xcuserdata/
DerivedData/
build/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# Swift Package Manager
.build/
Packages/
# CocoaPods (if ever used)
Pods/
# OS
.DS_Store
*.swp

View File

@ -0,0 +1,641 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
006E8EA280AC7CAC5495951E /* TimerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089BC5A87E29D38DF3DFDFA2 /* TimerStore.swift */; };
00BEA900EAE0226532824AE8 /* AlarmOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F991E825657AE91E039404AD /* AlarmOverlay.swift */; };
06789A5C995DCA4F35A4B9BD /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FACEF504403985D3BE08E29 /* FormatTests.swift */; };
1D1DFA5585694A7BE76109E4 /* TimerEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93579D521BE5D6682B129A5C /* TimerEngine.swift */; };
25D1EC51D100F93A735C6819 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FDD9567F18B987402B5FF1 /* ContentView.swift */; };
2A6837C7ACB5B9B8B632DD4D /* NotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34C1AC330B0CF41C37A771C /* NotificationScheduler.swift */; };
33FE05A7CC65C5A251D5C686 /* CascadeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3C28A0A106D3A02642F48 /* CascadeTests.swift */; };
3BD65B9D32D36D682CBC5B2A /* TimerListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1475DA4BEB57A6C1BDC13C27 /* TimerListSection.swift */; };
3CE4B6F56C02F4962A2ECC2C /* PomodoroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5346BBF21044F83F007D7E32 /* PomodoroView.swift */; };
61FB6A2572B7299661705DE2 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC158E525F3F703034E5542 /* Format.swift */; };
6770745F2FBB9478094DC205 /* UrgencyBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA00B60532FC72462812693 /* UrgencyBadge.swift */; };
6F3699234F74C1DB6E71D245 /* QuickTimerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D520D46C5759E8780334D623 /* QuickTimerSheet.swift */; };
809719965B906AE51EA53C8B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9019324C6A38DF943E1FF6 /* SettingsView.swift */; };
8EF5C79BDF55D1F04851C3C0 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C2F36FE2E4FEAD385B6860 /* TimelineView.swift */; };
9CE528A21B258A6554DA8A46 /* ChronoMindApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD88A07063BD00F2DC6BB753 /* ChronoMindApp.swift */; };
A77A6BFC98AD7A661CFE98DB /* FiringCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC734D1505143775DCBDF18 /* FiringCard.swift */; };
ABF5A4BB75D914A38A2D55CD /* TimerCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492793555CC220F85136F2B4 /* TimerCard.swift */; };
AE00522D3CE642B0C608E8A2 /* CountdownRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE814566D06D5ED5DE214765 /* CountdownRing.swift */; };
B586A949950B02D3499C828E /* TimeBlindness.swift in Sources */ = {isa = PBXBuildFile; fileRef = D899FC687E51E9606B813E4F /* TimeBlindness.swift */; };
C81D5D94A6F8E29C82EDD923 /* ChronoMindTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAF2993B0DDD3D4334378B4 /* ChronoMindTheme.swift */; };
CB7642D0D833F6D45B532AF6 /* Cascade.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE44586214BA0A4BEDA939F7 /* Cascade.swift */; };
CFA4D8E650083161CC184D90 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453FAEB975BAA8307C8AED26 /* HistoryView.swift */; };
D23D2EBC51D60E3D6C91DDAF /* TimerEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F23285C9339D8EEDF95CF1 /* TimerEngineTests.swift */; };
D488C32C9586E6BA10C17337 /* CreateTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33631FC9DC72BE316E763755 /* CreateTimerView.swift */; };
DFBCDAD7322F6552D82EC73C /* HapticEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4535FFCA7608DECAFC332C5 /* HapticEngine.swift */; };
E364FCB29C50C5780AB6BDED /* Urgency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8775EEA5055E7416149B8384 /* Urgency.swift */; };
EC0CEB1B4418DCD090CD431B /* CascadeProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA0B050D86B6910385A7A7B /* CascadeProgressBar.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
5BB5DCEADCC953EE536BCB14 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B148EC54D0D3F16627E44282 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 78243758404E574753DB494E;
remoteInfo = ChronoMind;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
089BC5A87E29D38DF3DFDFA2 /* TimerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerStore.swift; sourceTree = "<group>"; };
0BAF2993B0DDD3D4334378B4 /* ChronoMindTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronoMindTheme.swift; sourceTree = "<group>"; };
0CC734D1505143775DCBDF18 /* FiringCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiringCard.swift; sourceTree = "<group>"; };
1475DA4BEB57A6C1BDC13C27 /* TimerListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerListSection.swift; sourceTree = "<group>"; };
1CC158E525F3F703034E5542 /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = "<group>"; };
23F23285C9339D8EEDF95CF1 /* TimerEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerEngineTests.swift; sourceTree = "<group>"; };
33631FC9DC72BE316E763755 /* CreateTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTimerView.swift; sourceTree = "<group>"; };
453FAEB975BAA8307C8AED26 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
492793555CC220F85136F2B4 /* TimerCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerCard.swift; sourceTree = "<group>"; };
52A8E7A04EFCDCABA3F05E28 /* ChronoMind.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChronoMind.entitlements; sourceTree = "<group>"; };
52FDD9567F18B987402B5FF1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
5346BBF21044F83F007D7E32 /* PomodoroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PomodoroView.swift; sourceTree = "<group>"; };
6FACEF504403985D3BE08E29 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
8775EEA5055E7416149B8384 /* Urgency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Urgency.swift; sourceTree = "<group>"; };
93579D521BE5D6682B129A5C /* TimerEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerEngine.swift; sourceTree = "<group>"; };
9AA00B60532FC72462812693 /* UrgencyBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrgencyBadge.swift; sourceTree = "<group>"; };
9EA0B050D86B6910385A7A7B /* CascadeProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CascadeProgressBar.swift; sourceTree = "<group>"; };
A5D8865FAFACFB57899D5AF4 /* ChronoMind.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ChronoMind.app; sourceTree = BUILT_PRODUCTS_DIR; };
AE44586214BA0A4BEDA939F7 /* Cascade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cascade.swift; sourceTree = "<group>"; };
BD88A07063BD00F2DC6BB753 /* ChronoMindApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronoMindApp.swift; sourceTree = "<group>"; };
BEE32B08F25A4438A6B69FB6 /* ChronoMindTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = ChronoMindTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C4535FFCA7608DECAFC332C5 /* HapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticEngine.swift; sourceTree = "<group>"; };
CEC3C28A0A106D3A02642F48 /* CascadeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CascadeTests.swift; sourceTree = "<group>"; };
D34C1AC330B0CF41C37A771C /* NotificationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationScheduler.swift; sourceTree = "<group>"; };
D520D46C5759E8780334D623 /* QuickTimerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTimerSheet.swift; sourceTree = "<group>"; };
D899FC687E51E9606B813E4F /* TimeBlindness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeBlindness.swift; sourceTree = "<group>"; };
DA9019324C6A38DF943E1FF6 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
E7C2F36FE2E4FEAD385B6860 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
EE814566D06D5ED5DE214765 /* CountdownRing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownRing.swift; sourceTree = "<group>"; };
F991E825657AE91E039404AD /* AlarmOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmOverlay.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
237EFEB5E79C17C58AE4B02B /* Views */ = {
isa = PBXGroup;
children = (
EBD394FC41C27417B6272B2B /* Components */,
EC4D2D1AC883572D82016E7A /* CreateTimer */,
444BD7BD82654D6A717BE39B /* Focus */,
4DBB188595CF9B880D09EFA8 /* History */,
2ABA85855E88EF8E5AE2C296 /* Settings */,
583EC1D56685251E7A9410B8 /* Timeline */,
);
path = Views;
sourceTree = "<group>";
};
2ABA85855E88EF8E5AE2C296 /* Settings */ = {
isa = PBXGroup;
children = (
DA9019324C6A38DF943E1FF6 /* SettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
3C5FDEC2037E5CC602490C47 /* Store */ = {
isa = PBXGroup;
children = (
089BC5A87E29D38DF3DFDFA2 /* TimerStore.swift */,
);
path = Store;
sourceTree = "<group>";
};
444BD7BD82654D6A717BE39B /* Focus */ = {
isa = PBXGroup;
children = (
5346BBF21044F83F007D7E32 /* PomodoroView.swift */,
);
path = Focus;
sourceTree = "<group>";
};
468A5761EC0508501FAF2294 /* Theme */ = {
isa = PBXGroup;
children = (
0BAF2993B0DDD3D4334378B4 /* ChronoMindTheme.swift */,
);
path = Theme;
sourceTree = "<group>";
};
4DBB188595CF9B880D09EFA8 /* History */ = {
isa = PBXGroup;
children = (
453FAEB975BAA8307C8AED26 /* HistoryView.swift */,
);
path = History;
sourceTree = "<group>";
};
583EC1D56685251E7A9410B8 /* Timeline */ = {
isa = PBXGroup;
children = (
E7C2F36FE2E4FEAD385B6860 /* TimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
5AB52EC93294F076818CB0DA /* Notifications */ = {
isa = PBXGroup;
children = (
D34C1AC330B0CF41C37A771C /* NotificationScheduler.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
739D982BE81B9C296ED1A1C4 = {
isa = PBXGroup;
children = (
ADC7E8817D3AC5CB0D19CE06 /* ChronoMind */,
BA684DC5A43403F4E5BF4DBB /* ChronoMindTests */,
D989A0D19D230F73B67AC744 /* Products */,
);
sourceTree = "<group>";
};
889806888E26EEDFA679B318 /* Shared */ = {
isa = PBXGroup;
children = (
AEBBD21D7F7F55BAA99CA1C5 /* Haptics */,
5AB52EC93294F076818CB0DA /* Notifications */,
3C5FDEC2037E5CC602490C47 /* Store */,
468A5761EC0508501FAF2294 /* Theme */,
BF15F58057A7B3864EDDE601 /* TimerEngine */,
);
path = Shared;
sourceTree = "<group>";
};
ADC7E8817D3AC5CB0D19CE06 /* ChronoMind */ = {
isa = PBXGroup;
children = (
B7064A49165283316A44BD46 /* App */,
889806888E26EEDFA679B318 /* Shared */,
237EFEB5E79C17C58AE4B02B /* Views */,
52A8E7A04EFCDCABA3F05E28 /* ChronoMind.entitlements */,
);
path = ChronoMind;
sourceTree = "<group>";
};
AEBBD21D7F7F55BAA99CA1C5 /* Haptics */ = {
isa = PBXGroup;
children = (
C4535FFCA7608DECAFC332C5 /* HapticEngine.swift */,
);
path = Haptics;
sourceTree = "<group>";
};
B7064A49165283316A44BD46 /* App */ = {
isa = PBXGroup;
children = (
BD88A07063BD00F2DC6BB753 /* ChronoMindApp.swift */,
52FDD9567F18B987402B5FF1 /* ContentView.swift */,
);
path = App;
sourceTree = "<group>";
};
BA684DC5A43403F4E5BF4DBB /* ChronoMindTests */ = {
isa = PBXGroup;
children = (
CEC3C28A0A106D3A02642F48 /* CascadeTests.swift */,
6FACEF504403985D3BE08E29 /* FormatTests.swift */,
23F23285C9339D8EEDF95CF1 /* TimerEngineTests.swift */,
);
path = ChronoMindTests;
sourceTree = "<group>";
};
BF15F58057A7B3864EDDE601 /* TimerEngine */ = {
isa = PBXGroup;
children = (
AE44586214BA0A4BEDA939F7 /* Cascade.swift */,
1CC158E525F3F703034E5542 /* Format.swift */,
D899FC687E51E9606B813E4F /* TimeBlindness.swift */,
93579D521BE5D6682B129A5C /* TimerEngine.swift */,
8775EEA5055E7416149B8384 /* Urgency.swift */,
);
path = TimerEngine;
sourceTree = "<group>";
};
D989A0D19D230F73B67AC744 /* Products */ = {
isa = PBXGroup;
children = (
A5D8865FAFACFB57899D5AF4 /* ChronoMind.app */,
BEE32B08F25A4438A6B69FB6 /* ChronoMindTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
EBD394FC41C27417B6272B2B /* Components */ = {
isa = PBXGroup;
children = (
F991E825657AE91E039404AD /* AlarmOverlay.swift */,
9EA0B050D86B6910385A7A7B /* CascadeProgressBar.swift */,
EE814566D06D5ED5DE214765 /* CountdownRing.swift */,
0CC734D1505143775DCBDF18 /* FiringCard.swift */,
492793555CC220F85136F2B4 /* TimerCard.swift */,
1475DA4BEB57A6C1BDC13C27 /* TimerListSection.swift */,
9AA00B60532FC72462812693 /* UrgencyBadge.swift */,
);
path = Components;
sourceTree = "<group>";
};
EC4D2D1AC883572D82016E7A /* CreateTimer */ = {
isa = PBXGroup;
children = (
33631FC9DC72BE316E763755 /* CreateTimerView.swift */,
D520D46C5759E8780334D623 /* QuickTimerSheet.swift */,
);
path = CreateTimer;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
110092E4140109FF0073897B /* ChronoMindTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = B6C1A84B6C793EB62BA4DA19 /* Build configuration list for PBXNativeTarget "ChronoMindTests" */;
buildPhases = (
90C450C530D3DA144CDB7FBC /* Sources */,
);
buildRules = (
);
dependencies = (
3630F4DD33FE96360A4DA8F4 /* PBXTargetDependency */,
);
name = ChronoMindTests;
packageProductDependencies = (
);
productName = ChronoMindTests;
productReference = BEE32B08F25A4438A6B69FB6 /* ChronoMindTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
78243758404E574753DB494E /* ChronoMind */ = {
isa = PBXNativeTarget;
buildConfigurationList = E15654FE3118301BF65DA240 /* Build configuration list for PBXNativeTarget "ChronoMind" */;
buildPhases = (
30FEA9533F75E1C796E0E198 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = ChronoMind;
packageProductDependencies = (
);
productName = ChronoMind;
productReference = A5D8865FAFACFB57899D5AF4 /* ChronoMind.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
B148EC54D0D3F16627E44282 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1600;
TargetAttributes = {
110092E4140109FF0073897B = {
DevelopmentTeam = 748N7QPX7J;
ProvisioningStyle = Automatic;
};
78243758404E574753DB494E = {
DevelopmentTeam = 748N7QPX7J;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 900EBDCE1F40012242E88499 /* Build configuration list for PBXProject "ChronoMind" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 739D982BE81B9C296ED1A1C4;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
projectDirPath = "";
projectRoot = "";
targets = (
78243758404E574753DB494E /* ChronoMind */,
110092E4140109FF0073897B /* ChronoMindTests */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
30FEA9533F75E1C796E0E198 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
00BEA900EAE0226532824AE8 /* AlarmOverlay.swift in Sources */,
CB7642D0D833F6D45B532AF6 /* Cascade.swift in Sources */,
EC0CEB1B4418DCD090CD431B /* CascadeProgressBar.swift in Sources */,
9CE528A21B258A6554DA8A46 /* ChronoMindApp.swift in Sources */,
C81D5D94A6F8E29C82EDD923 /* ChronoMindTheme.swift in Sources */,
25D1EC51D100F93A735C6819 /* ContentView.swift in Sources */,
AE00522D3CE642B0C608E8A2 /* CountdownRing.swift in Sources */,
D488C32C9586E6BA10C17337 /* CreateTimerView.swift in Sources */,
A77A6BFC98AD7A661CFE98DB /* FiringCard.swift in Sources */,
61FB6A2572B7299661705DE2 /* Format.swift in Sources */,
DFBCDAD7322F6552D82EC73C /* HapticEngine.swift in Sources */,
CFA4D8E650083161CC184D90 /* HistoryView.swift in Sources */,
2A6837C7ACB5B9B8B632DD4D /* NotificationScheduler.swift in Sources */,
3CE4B6F56C02F4962A2ECC2C /* PomodoroView.swift in Sources */,
6F3699234F74C1DB6E71D245 /* QuickTimerSheet.swift in Sources */,
809719965B906AE51EA53C8B /* SettingsView.swift in Sources */,
B586A949950B02D3499C828E /* TimeBlindness.swift in Sources */,
8EF5C79BDF55D1F04851C3C0 /* TimelineView.swift in Sources */,
ABF5A4BB75D914A38A2D55CD /* TimerCard.swift in Sources */,
1D1DFA5585694A7BE76109E4 /* TimerEngine.swift in Sources */,
3BD65B9D32D36D682CBC5B2A /* TimerListSection.swift in Sources */,
006E8EA280AC7CAC5495951E /* TimerStore.swift in Sources */,
E364FCB29C50C5780AB6BDED /* Urgency.swift in Sources */,
6770745F2FBB9478094DC205 /* UrgencyBadge.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
90C450C530D3DA144CDB7FBC /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33FE05A7CC65C5A251D5C686 /* CascadeTests.swift in Sources */,
06789A5C995DCA4F35A4B9BD /* FormatTests.swift in Sources */,
D23D2EBC51D60E3D6C91DDAF /* TimerEngineTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3630F4DD33FE96360A4DA8F4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 78243758404E574753DB494E /* ChronoMind */;
targetProxy = 5BB5DCEADCC953EE536BCB14 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
188B92F03833DCED4C79777E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = ChronoMind/ChronoMind.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_GENERATION_MODE = GeneratedFile;
INFOPLIST_KEY_CFBundleDisplayName = ChronoMind;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.chronomind.app;
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
1916861B34C6412F352A467C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = ChronoMind/ChronoMind.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_GENERATION_MODE = GeneratedFile;
INFOPLIST_KEY_CFBundleDisplayName = ChronoMind;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.chronomind.app;
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
1D8CF2BF99A3D83D8146403D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.chronomind.tests;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChronoMind.app/ChronoMind";
};
name = Debug;
};
2FAE12CBA4AF17056BAF8CBD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.chronomind.tests;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChronoMind.app/ChronoMind";
};
name = Release;
};
9554092DCB210E9D537B30FC /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 748N7QPX7J;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
DF2F9E2B2834FDEE0C6C59C0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 748N7QPX7J;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
900EBDCE1F40012242E88499 /* Build configuration list for PBXProject "ChronoMind" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DF2F9E2B2834FDEE0C6C59C0 /* Debug */,
9554092DCB210E9D537B30FC /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
B6C1A84B6C793EB62BA4DA19 /* Build configuration list for PBXNativeTarget "ChronoMindTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1D8CF2BF99A3D83D8146403D /* Debug */,
2FAE12CBA4AF17056BAF8CBD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
E15654FE3118301BF65DA240 /* Build configuration list for PBXNativeTarget "ChronoMind" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1916861B34C6412F352A467C /* Debug */,
188B92F03833DCED4C79777E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = B148EC54D0D3F16627E44282 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "78243758404E574753DB494E"
BuildableName = "ChronoMind.app"
BlueprintName = "ChronoMind"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "110092E4140109FF0073897B"
BuildableName = "ChronoMindTests.xctest"
BlueprintName = "ChronoMindTests"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "78243758404E574753DB494E"
BuildableName = "ChronoMind.app"
BlueprintName = "ChronoMind"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "110092E4140109FF0073897B"
BuildableName = "ChronoMindTests.xctest"
BlueprintName = "ChronoMindTests"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "78243758404E574753DB494E"
BuildableName = "ChronoMind.app"
BlueprintName = "ChronoMind"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "78243758404E574753DB494E"
BuildableName = "ChronoMind.app"
BlueprintName = "ChronoMind"
ReferencedContainer = "container:ChronoMind.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -136,15 +136,12 @@ final class CMNotificationManager: ObservableObject {
UNUserNotificationCenter.current().add(request)
}
private func soundForUrgency(_ urgency: UrgencyLevel, isWarning: Bool) -> UNNotificationSound {
private func soundForUrgency(_ urgency: UrgencyLevel, isWarning: Bool) -> UNNotificationSound? {
let config = getUrgencyConfig(urgency)
guard config.soundEnabled else { return .none }
guard config.soundEnabled else { return nil }
if urgency == .critical && !isWarning {
// Critical fires get the default critical alert sound
if #available(iOS 12.0, *) {
return .defaultCritical
}
return .defaultCritical
}
return .default

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css";
import { ToastContainer } from "@/components/Toast";
const inter = Inter({
variable: "--font-inter",
@ -38,6 +39,7 @@ export default function RootLayout({
className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
>
{children}
<ToastContainer />
</body>
</html>
);

View File

@ -0,0 +1,108 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { X, Bell, AlertTriangle, CheckCircle, Info } from 'lucide-react';
export interface ToastMessage {
id: string;
type: 'info' | 'warning' | 'success' | 'alarm';
title: string;
body?: string;
duration?: number; // ms, 0 = sticky
}
// Global toast state — simple pub/sub
type Listener = (toasts: ToastMessage[]) => void;
let toasts: ToastMessage[] = [];
const listeners = new Set<Listener>();
function notify() {
listeners.forEach((l) => l([...toasts]));
}
export function showToast(toast: Omit<ToastMessage, 'id'>) {
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const msg: ToastMessage = { ...toast, id };
toasts = [...toasts, msg];
notify();
const duration = toast.duration ?? 5000;
if (duration > 0) {
setTimeout(() => dismissToast(id), duration);
}
}
export function dismissToast(id: string) {
toasts = toasts.filter((t) => t.id !== id);
notify();
}
const ICONS = {
info: Info,
warning: AlertTriangle,
success: CheckCircle,
alarm: Bell,
};
const COLORS = {
info: { bg: 'var(--cm-accent)', text: '#fff' },
warning: { bg: 'var(--cm-warning)', text: '#000' },
success: { bg: 'var(--cm-success)', text: '#000' },
alarm: { bg: 'var(--cm-critical)', text: '#fff' },
};
export function ToastContainer() {
const [items, setItems] = useState<ToastMessage[]>([]);
useEffect(() => {
listeners.add(setItems);
return () => { listeners.delete(setItems); };
}, []);
const handleDismiss = useCallback((id: string) => dismissToast(id), []);
if (items.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
{items.map((toast) => {
const Icon = ICONS[toast.type];
const colors = COLORS[toast.type];
return (
<div
key={toast.id}
className="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg border animate-in slide-in-from-right"
style={{
backgroundColor: 'var(--cm-bg-elevated)',
borderColor: 'var(--cm-border)',
}}
>
<div
className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: colors.bg }}
>
<Icon size={16} style={{ color: colors.text }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold" style={{ color: 'var(--cm-text-primary)' }}>
{toast.title}
</p>
{toast.body && (
<p className="text-xs mt-0.5" style={{ color: 'var(--cm-text-secondary)' }}>
{toast.body}
</p>
)}
</div>
<button
onClick={() => handleDismiss(toast.id)}
className="flex-shrink-0 p-1 rounded cursor-pointer"
style={{ color: 'var(--cm-text-tertiary)' }}
>
<X size={14} />
</button>
</div>
);
})}
</div>
);
}

View File

@ -27,8 +27,24 @@ export function sendNotification(
urgency: UrgencyLevel,
options?: { tag?: string; onClick?: () => void }
): Notification | null {
if (typeof window === 'undefined' || !('Notification' in window)) return null;
if (Notification.permission !== 'granted') return null;
if (typeof window === 'undefined') return null;
// If notifications not available or denied, fall back to in-app toast
if (!('Notification' in window) || Notification.permission !== 'granted') {
// Lazy import to avoid circular deps
import('../components/Toast').then(({ showToast }) => {
const toastType = urgency === 'critical' ? 'alarm' as const
: urgency === 'important' ? 'warning' as const
: 'info' as const;
showToast({
type: toastType,
title,
body,
duration: urgency === 'critical' ? 0 : 5000,
});
});
return null;
}
const config = getUrgencyConfig(urgency);