Initial commit: KisaniCal iOS app

Task management, Eisenhower matrix, workout logging with persistence,
and calendar with 6 view modes (Month, List, Year, Week, 3 Day, Day).
Custom floating tab bar, dynamic calendar icon, and workout data seeded.
This commit is contained in:
kutesir
2026-05-27 02:09:44 +03:00
commit f67429de0b
22 changed files with 6797 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Xcode
*.xcodeproj/xcuserdata/
*.xcworkspace/xcuserdata/
*.xcworkspace/contents.xcworkspacedata
DerivedData/
build/
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# Swift Package Manager
.build/
.swiftpm/
# CocoaPods
Pods/
Podfile.lock
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Obj-C/Swift specific
*.moved-aside
*.gcno
# Editor
.idea/
*.swp
*.swo

View File

@@ -0,0 +1,430 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; };
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */; };
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */; };
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; };
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; };
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
21B93C269F283F11B415B18C /* Components */ = {
isa = PBXGroup;
children = (
23A4491BFA50721082024756 /* SharedComponents.swift */,
);
path = Components;
sourceTree = "<group>";
};
23CBCF100C5EF55E737379CA /* Models */ = {
isa = PBXGroup;
children = (
5722CC4B59E3939724142710 /* ExerciseModels.swift */,
DCC2AFB4FA765383740767CB /* TaskItem.swift */,
);
path = Models;
sourceTree = "<group>";
};
4D2F2B3472A5A6D99F2FFCD2 = {
isa = PBXGroup;
children = (
F70DA4746C68CD405435DAB6 /* KisaniCal */,
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
);
sourceTree = "<group>";
};
51BD1B5DEDE9FAD9CA2FF6DA /* Products */ = {
isa = PBXGroup;
children = (
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
);
name = Products;
sourceTree = "<group>";
};
5E9A0E064E153429180400E6 /* Views */ = {
isa = PBXGroup;
children = (
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
72308FEE0226F45414C04DDD /* SettingsView.swift */,
C786EBC7DF879D64EB28165E /* TodayView.swift */,
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */,
);
path = Views;
sourceTree = "<group>";
};
9981C53A4E263A0DBC3DF194 /* Preview Content */ = {
isa = PBXGroup;
children = (
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
E0A03E79A679740978E61BF1 /* Theme */ = {
isa = PBXGroup;
children = (
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */,
);
path = Theme;
sourceTree = "<group>";
};
A1B2C3D4E5F6070A8B9C0D1E /* Managers */ = {
isa = PBXGroup;
children = (
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */,
);
path = Managers;
sourceTree = "<group>";
};
F70DA4746C68CD405435DAB6 /* KisaniCal */ = {
isa = PBXGroup;
children = (
72FDF9C8DD37134576356B89 /* Assets.xcassets */,
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
21B93C269F283F11B415B18C /* Components */,
A1B2C3D4E5F6070A8B9C0D1E /* Managers */,
23CBCF100C5EF55E737379CA /* Models */,
9981C53A4E263A0DBC3DF194 /* Preview Content */,
E0A03E79A679740978E61BF1 /* Theme */,
5E9A0E064E153429180400E6 /* Views */,
);
path = KisaniCal;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
297C0C2BFBA10AB52D5D49CE /* KisaniCal */ = {
isa = PBXNativeTarget;
buildConfigurationList = B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */;
buildPhases = (
82EA49FE155821D424C46912 /* Sources */,
E1F1A3EE4469B851B972E4AE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = KisaniCal;
packageProductDependencies = (
);
productName = KisaniCal;
productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
AF6DE7A812408E3742522E90 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1500;
TargetAttributes = {
297C0C2BFBA10AB52D5D49CE = {
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 4D2F2B3472A5A6D99F2FFCD2;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 51BD1B5DEDE9FAD9CA2FF6DA /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
E1F1A3EE4469B851B972E4AE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */,
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
82EA49FE155821D424C46912 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */,
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */,
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */,
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */,
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */,
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */,
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
0CEA6637F45C32A237FBA20D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = K8BLMMR883;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
20E23A89279CC26778E21856 /* 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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
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 = 16.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.0;
};
name = Release;
};
BD527AA54887489CC665FB9D /* 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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
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 = 16.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.0;
};
name = Debug;
};
F364512BEB70ECB7CB83FBFE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = K8BLMMR883;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F364512BEB70ECB7CB83FBFE /* Debug */,
0CEA6637F45C32A237FBA20D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BD527AA54887489CC665FB9D /* Debug */,
20E23A89279CC26778E21856 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = AF6DE7A812408E3742522E90 /* Project object */;
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.165",
"green" : "0.384",
"red" : "0.910"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,184 @@
import SwiftUI
// MARK: - Press Button Styles
struct PressButtonStyle: ButtonStyle {
var scale: CGFloat = 0.95
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? scale : 1.0)
.animation(KisaniSpring.micro, value: configuration.isPressed)
}
}
struct FabButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.88 : 1.0)
.opacity(configuration.isPressed ? 0.92 : 1.0)
.animation(KisaniSpring.micro, value: configuration.isPressed)
}
}
// MARK: - Icon Button (circular)
struct IButton: View {
let icon: String
var action: (() -> Void)? = nil
@Environment(\.colorScheme) private var cs
var body: some View {
Button { action?() } label: {
Image(systemName: icon)
.font(.system(size: 13, weight: .medium))
.foregroundColor(AppColors.text2(cs))
.frame(width: 34, height: 34)
.background(AppColors.surface2(cs))
.clipShape(Circle())
.overlay(Circle().stroke(AppColors.border(cs), lineWidth: 1))
}
.buttonStyle(PressButtonStyle(scale: 0.92))
}
}
// MARK: - Month Pill
struct MonthPill: View {
let title: String
var action: (() -> Void)? = nil
@Environment(\.colorScheme) private var cs
var body: some View {
Button { action?() } label: {
HStack(spacing: 6) {
Text(title)
.font(AppFonts.sans(12.5, weight: .medium))
.foregroundColor(AppColors.text(cs))
Image(systemName: "chevron.down")
.font(.system(size: 9, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
}
.padding(.horizontal, 13)
.padding(.vertical, 6)
.background(AppColors.surface2(cs))
.clipShape(Capsule())
.overlay(Capsule().stroke(AppColors.border(cs), lineWidth: 1))
}
.buttonStyle(.plain)
}
}
// MARK: - Floating Action Button
struct FAB: View {
var action: (() -> Void)? = nil
var body: some View {
Button { action?() } label: {
Image(systemName: "plus")
.font(.system(size: 20, weight: .light))
.foregroundColor(.white)
.frame(width: 46, height: 46)
.background(AppColors.accent)
.clipShape(Circle())
.shadow(color: AppColors.accent.opacity(0.45), radius: 12, x: 0, y: 4)
.shadow(color: AppColors.accent.opacity(0.14), radius: 30, x: 0, y: 0)
}
.buttonStyle(FabButtonStyle())
}
}
// MARK: - Section Label
struct SectionLabel: View {
let text: String
@Environment(\.colorScheme) private var cs
var body: some View {
Text(text)
.font(AppFonts.sans(10.5, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: - Badge
struct AppBadge: View {
let text: String
let bg: Color
let fg: Color
var body: some View {
Text(text)
.font(AppFonts.sans(9, weight: .semibold))
.foregroundColor(fg)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(bg)
.cornerRadius(5)
}
}
// MARK: - Dashed Add Button
struct DashedAddButton: View {
let label: String
let action: () -> Void
@Environment(\.colorScheme) private var cs
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
Image(systemName: "plus")
.font(.system(size: 14))
.foregroundColor(AppColors.text3(cs))
Text(label)
.font(AppFonts.sans(12, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
}
.frame(maxWidth: .infinity)
.padding(11)
.background(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.large)
.strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [5, 4]))
.foregroundColor(AppColors.borderHi(cs))
)
}
.buttonStyle(PressButtonStyle())
}
}
// MARK: - Tag Chip
struct TagChip: View {
let text: String
let color: Color
let bg: Color
var body: some View {
Text(text)
.font(AppFonts.sans(9, weight: .semibold))
.foregroundColor(color)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(bg)
.cornerRadius(5)
}
}
// MARK: - Card style modifier
struct CardStyle: ViewModifier {
@Environment(\.colorScheme) private var cs
var radius: CGFloat = AppRadius.large
var borderColor: Color? = nil
func body(content: Content) -> some View {
content
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: radius))
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(borderColor ?? AppColors.border(cs), lineWidth: 1)
)
}
}
extension View {
func cardStyle(radius: CGFloat = AppRadius.large, borderColor: Color? = nil) -> some View {
modifier(CardStyle(radius: radius, borderColor: borderColor))
}
}

258
KisaniCal/ContentView.swift Normal file
View File

@@ -0,0 +1,258 @@
import SwiftUI
struct ContentView: View {
@StateObject private var taskVM = TaskViewModel()
@StateObject private var workoutVM = WorkoutViewModel()
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.scenePhase) private var scenePhase
@AppStorage("appearanceMode") private var appearanceMode = 0
@AppStorage("showMatrixTab") private var showMatrixTab = true
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
@AppStorage("hasOnboarded") private var hasOnboarded = false
@State private var showOnboarding = false
@State private var selectedTab = 0
private var calendarIcon: Image {
let day = Calendar.current.component(.day, from: Date())
let size = CGSize(width: 26, height: 26)
let renderer = UIGraphicsImageRenderer(size: size)
let img = renderer.image { _ in
let color = UIColor.black
let lw: CGFloat = 1.5
let cr: CGFloat = 4.5
// Outer rounded rectangle body
let body = CGRect(x: lw / 2, y: 3, width: size.width - lw, height: size.height - 3 - lw / 2)
let bodyPath = UIBezierPath(roundedRect: body, cornerRadius: cr)
color.setStroke()
bodyPath.lineWidth = lw
bodyPath.stroke()
// Header divider line
let divY = body.minY + 7.5
let divPath = UIBezierPath()
divPath.move(to: CGPoint(x: body.minX + lw / 2, y: divY))
divPath.addLine(to: CGPoint(x: body.maxX - lw / 2, y: divY))
color.withAlphaComponent(0.45).setStroke()
divPath.lineWidth = 1
divPath.stroke()
// Ring bumps at top
color.setFill()
for rx in [size.width * 0.3, size.width * 0.7] {
UIBezierPath(roundedRect: CGRect(x: rx - 1.5, y: 0.5, width: 3, height: 5.5), cornerRadius: 1.5).fill()
}
// Date number centred in body below header
let s = "\(day)"
let font = UIFont.systemFont(ofSize: floor(size.height * 0.38), weight: .bold)
let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color]
let ts = s.size(withAttributes: attrs)
let contentMidY = divY + (body.maxY - divY) / 2
s.draw(at: CGPoint(x: (size.width - ts.width) / 2, y: contentMidY - ts.height / 2), withAttributes: attrs)
}
return Image(uiImage: img.withRenderingMode(.alwaysTemplate))
}
private var preferredScheme: ColorScheme? {
switch appearanceMode {
case 1: return .light
case 2: return .dark
default: return nil
}
}
var body: some View {
Group {
if hSizeClass == .regular {
iPadLayout()
} else {
iPhoneLayout()
}
}
.preferredColorScheme(preferredScheme)
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingView(isPresented: $showOnboarding)
.onDisappear { hasOnboarded = true }
}
.onAppear { if !hasOnboarded { showOnboarding = true } }
.onChange(of: scenePhase) { phase in
if phase == .active {
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
}
.onChange(of: workoutVM.doneSets) { _ in
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
.onChange(of: taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count) { _ in
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
}
// MARK: - iPhone
@ViewBuilder
private func iPhoneLayout() -> some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
TodayView()
.environmentObject(taskVM)
.environmentObject(workoutVM)
.tag(0)
.tabBarInset()
CalendarView()
.environmentObject(taskVM)
.environmentObject(workoutVM)
.tag(1)
.tabBarInset()
if showMatrixTab {
MatrixView()
.environmentObject(taskVM)
.tag(2)
.tabBarInset()
} else {
Color.clear.tag(2)
}
if showWorkoutTab {
WorkoutView()
.environmentObject(workoutVM)
.tag(3)
.tabBarInset()
} else {
Color.clear.tag(3)
}
SettingsView()
.environmentObject(workoutVM)
.tag(4)
.tabBarInset()
}
.tint(AppColors.accent)
KisaniTabBar(
selection: $selectedTab,
calIcon: calendarIcon,
showMatrix: showMatrixTab,
showWorkout: showWorkoutTab
)
.padding(.horizontal, 24)
.padding(.bottom, 8)
}
.onAppear { UITabBar.appearance().isHidden = true }
}
// MARK: - iPad
@ViewBuilder
private func iPadLayout() -> some View {
NavigationSplitView(columnVisibility: .constant(.all)) {
List {
iPadSidebarItem("Tasks", icon: "checkmark", tag: 0)
iPadSidebarItem("Calendar", icon: "calendar", tag: 1)
if showMatrixTab {
iPadSidebarItem("Matrix", icon: "square.grid.2x2", tag: 2)
}
if showWorkoutTab {
iPadSidebarItem("Workout", icon: "dumbbell", tag: 3)
}
iPadSidebarItem("Settings", icon: "gearshape", tag: 4)
}
.navigationTitle("Kisani Cal")
.listStyle(.sidebar)
} detail: {
TodayView().environmentObject(taskVM)
}
.tint(AppColors.accent)
}
private func iPadSidebarItem(_ label: String, icon: String, tag: Int) -> some View {
NavigationLink(destination: iPadDetail(tag: tag)) {
Label(label, systemImage: icon)
}
}
@ViewBuilder
private func iPadDetail(tag: Int) -> some View {
switch tag {
case 0: TodayView().environmentObject(taskVM).environmentObject(workoutVM)
case 1: CalendarView().environmentObject(taskVM).environmentObject(workoutVM)
case 2: MatrixView().environmentObject(taskVM)
case 3: WorkoutView().environmentObject(workoutVM)
default: SettingsView().environmentObject(workoutVM)
}
}
}
// MARK: - Custom Tab Bar
// Adds a transparent bottom safe-area inset equal to the floating tab bar height,
// so scroll content and FABs inside each tab clear the bar.
private extension View {
func tabBarInset() -> some View {
safeAreaInset(edge: .bottom, spacing: 0) {
Color.clear.frame(height: 62)
}
}
}
struct KisaniTabBar: View {
@Binding var selection: Int
let calIcon: Image
var showMatrix: Bool
var showWorkout: Bool
@Environment(\.colorScheme) private var cs
private struct Entry: Identifiable {
let id: Int
let sfIcon: String?
let custom: Image?
init(_ id: Int, _ icon: String) { self.id = id; sfIcon = icon; custom = nil }
init(_ id: Int, custom img: Image) { self.id = id; sfIcon = nil; custom = img }
}
private var entries: [Entry] {
var e: [Entry] = [Entry(0, "checkmark"), Entry(1, custom: calIcon)]
if showMatrix { e.append(Entry(2, "square.grid.2x2.fill")) }
if showWorkout { e.append(Entry(3, "dumbbell.fill")) }
e.append(Entry(4, "gearshape.fill"))
return e
}
var body: some View {
HStack(spacing: 0) {
ForEach(entries) { entry in
Button {
withAnimation(KisaniSpring.micro) { selection = entry.id }
} label: {
ZStack {
if selection == entry.id {
RoundedRectangle(cornerRadius: 14)
.fill(AppColors.surface2(cs))
.frame(width: 52, height: 44)
.transition(.opacity.combined(with: .scale(scale: 0.9)))
}
if let img = entry.custom {
img
.font(.system(size: 20))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
} else if let icon = entry.sfIcon {
Image(systemName: icon)
.font(.system(size: 17, weight: selection == entry.id ? .semibold : .regular))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
}
}
.frame(maxWidth: .infinity).frame(height: 50)
}
.buttonStyle(PressButtonStyle(scale: 0.88))
}
}
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 28)
.fill(AppColors.surface(cs))
.shadow(color: .black.opacity(0.10), radius: 20, x: 0, y: 6)
.shadow(color: .black.opacity(0.04), radius: 4, x: 0, y: 1)
)
}
}

View File

@@ -0,0 +1,22 @@
import SwiftUI
@main
struct KisaniCalApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
NotificationManager.shared.requestPermission()
return true
}
}

View File

@@ -0,0 +1,127 @@
import SwiftUI
import UserNotifications
final class NotificationManager: NSObject, ObservableObject {
static let shared = NotificationManager()
@Published var isAuthorized = false
private let center = UNUserNotificationCenter.current()
private let workoutPrefix = "kisani.workout.weekday"
private let tasksId = "kisani.tasks.evening"
override private init() {
super.init()
center.delegate = self
refreshAuthStatus()
}
// MARK: - Permission
func requestPermission() {
center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
DispatchQueue.main.async { self?.isAuthorized = granted }
}
}
private func refreshAuthStatus() {
center.getNotificationSettings { [weak self] settings in
DispatchQueue.main.async {
self?.isAuthorized = (settings.authorizationStatus == .authorized
|| settings.authorizationStatus == .provisional)
}
}
}
// MARK: - Reschedule (call on foreground + state changes)
func reschedule(workoutVM: WorkoutViewModel, taskVM: TaskViewModel) {
center.getNotificationSettings { [weak self] settings in
guard let self else { return }
let ok = settings.authorizationStatus == .authorized
|| settings.authorizationStatus == .provisional
guard ok else { return }
self.scheduleWorkout(workoutVM)
self.scheduleTasks(taskVM)
}
}
// MARK: - Workout (7:00 PM on each scheduled weekday)
private func scheduleWorkout(_ workoutVM: WorkoutViewModel) {
let existingIds = (1...7).map { "\(workoutPrefix).\($0)" }
center.removePendingNotificationRequests(withIdentifiers: existingIds)
let todayWeekday = Calendar.current.component(.weekday, from: Date())
for (weekday, pid) in workoutVM.schedule {
guard let program = workoutVM.programs.first(where: { $0.id == pid }) else { continue }
// Today's workout is already done skip so notification won't fire this evening
if weekday == todayWeekday && workoutVM.progress >= 1.0 { continue }
let content = UNMutableNotificationContent()
content.title = "\(program.emoji) Workout check-in"
content.body = "Did you finish your \(program.name)? Mark your sets complete."
content.sound = .default
content.userInfo = ["deeplink": "workout"]
var comps = DateComponents()
comps.weekday = weekday
comps.hour = 19
comps.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
let request = UNNotificationRequest(
identifier: "\(workoutPrefix).\(weekday)",
content: content,
trigger: trigger
)
center.add(request)
}
}
// MARK: - Urgent tasks (7:30 PM daily, only when there are open urgent tasks)
private func scheduleTasks(_ taskVM: TaskViewModel) {
center.removePendingNotificationRequests(withIdentifiers: [tasksId])
let count = taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
guard count > 0 else { return }
let content = UNMutableNotificationContent()
content.title = "Evening task check-in"
content.body = "\(count) urgent task\(count == 1 ? "" : "s") still open — mark done or reschedule."
content.sound = .default
content.userInfo = ["deeplink": "tasks"]
var comps = DateComponents()
comps.hour = 19
comps.minute = 30
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
let request = UNNotificationRequest(identifier: tasksId, content: content, trigger: trigger)
center.add(request)
}
}
// MARK: - UNUserNotificationCenterDelegate
extension NotificationManager: UNUserNotificationCenterDelegate {
// Show banners even when app is foregrounded
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound])
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
completionHandler()
}
}

View File

@@ -0,0 +1,601 @@
import Foundation
import Combine
// MARK: - Exercise Models
struct ExerciseSet: Identifiable, Codable {
var id = UUID()
var weight: Double = 0
var reps: Int = 10
var isDone: Bool = false
}
struct Exercise: Identifiable, Codable {
var id = UUID()
var name: String
var sfSymbol: String
var colorIndex: Int = 0
var sets: [ExerciseSet]
var isExpanded: Bool = true
var isDumbbell: Bool = false // per-side input total = weight × 2
var totalSets: Int { sets.count }
var doneSets: Int { sets.filter(\.isDone).count }
var isPartialDone: Bool { doneSets > 0 && doneSets < totalSets }
var isAllDone: Bool { doneSets == totalSets && totalSets > 0 }
}
// MARK: - Workout Section
struct WorkoutSection: Identifiable, Codable {
var id = UUID()
var name: String
var exercises: [Exercise]
var isExpanded: Bool = true
var totalSets: Int { exercises.reduce(0) { $0 + $1.sets.count } }
var doneSets: Int { exercises.reduce(0) { $0 + $1.doneSets } }
var exerciseCount: Int { exercises.count }
}
// MARK: - Workout Program
struct WorkoutProgram: Identifiable, Codable {
let id: UUID
var name: String
var emoji: String
var colorIndex: Int
var sections: [WorkoutSection]
var allExercises: [Exercise] { sections.flatMap(\.exercises) }
var totalSets: Int { sections.reduce(0) { $0 + $1.totalSets } }
var doneSets: Int { sections.reduce(0) { $0 + $1.doneSets } }
var totalExercises: Int { sections.reduce(0) { $0 + $1.exerciseCount } }
init(id: UUID = UUID(), name: String, emoji: String, colorIndex: Int = 0, sections: [WorkoutSection] = []) {
self.id = id; self.name = name; self.emoji = emoji
self.colorIndex = colorIndex; self.sections = sections
}
}
// MARK: - Exercise Template Library
enum ExerciseMuscle: String, CaseIterable, Identifiable {
var id: String { rawValue }
case chest = "Chest"
case back = "Back"
case legs = "Legs"
case shoulders = "Shoulders"
case arms = "Arms"
case core = "Core"
case cardio = "Cardio"
var sfSymbol: String {
switch self {
case .chest: return "figure.strengthtraining.traditional"
case .back: return "figure.pull.up"
case .legs: return "figure.squat"
case .shoulders: return "figure.arms.open"
case .arms: return "dumbbell.fill"
case .core: return "figure.core.training"
case .cardio: return "figure.run"
}
}
}
struct ExerciseTemplate: Identifiable {
let id = UUID()
let name: String
let sfSymbol: String
let muscle: ExerciseMuscle
var isDumbbell: Bool = false
}
let exerciseLibrary: [ExerciseTemplate] = [
// Chest
ExerciseTemplate(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
ExerciseTemplate(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Incline Dumbbell Press", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Decline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
ExerciseTemplate(name: "Cable Fly", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
ExerciseTemplate(name: "Chest Flys", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Push-ups", sfSymbol: "figure.gymnastics", muscle: .chest),
ExerciseTemplate(name: "Chest Dips", sfSymbol: "figure.gymnastics", muscle: .chest),
ExerciseTemplate(name: "Cable Crossover", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
// Back
ExerciseTemplate(name: "Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Lat Pulldown", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Barbell Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "Seated Cable Row", sfSymbol: "figure.rowing", muscle: .back),
ExerciseTemplate(name: "Cable Row", sfSymbol: "figure.rowing", muscle: .back),
ExerciseTemplate(name: "Incline High Row", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
ExerciseTemplate(name: "Face Pull", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
ExerciseTemplate(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "T-Bar Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "Shrugs", sfSymbol: "figure.arms.open", muscle: .back),
ExerciseTemplate(name: "Lat Pulldown Wide Grip", sfSymbol: "figure.pull.up", muscle: .back),
// Legs
ExerciseTemplate(name: "Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Hack Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Romanian Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
ExerciseTemplate(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Adductor Machine", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Hamstring Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Standing Calf Raise", sfSymbol: "figure.walk", muscle: .legs),
ExerciseTemplate(name: "Seated Calf Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Bulgarian Split Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Lunges", sfSymbol: "figure.walk", muscle: .legs),
ExerciseTemplate(name: "Stiff-Legged Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
ExerciseTemplate(name: "Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
// Shoulders
ExerciseTemplate(name: "Overhead Press", sfSymbol: "figure.arms.open", muscle: .shoulders),
ExerciseTemplate(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Arnold Press", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Lateral Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Front Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Cable Lateral Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .shoulders),
ExerciseTemplate(name: "Reverse Fly", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
// Arms
ExerciseTemplate(name: "Bicep Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Hammer Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Incline Dumbbell Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
ExerciseTemplate(name: "Preacher Curl", sfSymbol: "dumbbell.fill", muscle: .arms),
ExerciseTemplate(name: "Concentration Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
ExerciseTemplate(name: "Tricep Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
ExerciseTemplate(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
ExerciseTemplate(name: "Straight Bar Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
ExerciseTemplate(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
ExerciseTemplate(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", muscle: .arms),
ExerciseTemplate(name: "Rope Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
// Core
ExerciseTemplate(name: "Plank", sfSymbol: "figure.core.training", muscle: .core),
ExerciseTemplate(name: "Crunches", sfSymbol: "figure.core.training", muscle: .core),
ExerciseTemplate(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", muscle: .core),
ExerciseTemplate(name: "Russian Twists", sfSymbol: "figure.cross.training", muscle: .core),
ExerciseTemplate(name: "Hanging Leg Raise", sfSymbol: "figure.pull.up", muscle: .core),
ExerciseTemplate(name: "Cable Crunches", sfSymbol: "figure.cooldown", muscle: .core),
// Cardio
ExerciseTemplate(name: "Treadmill", sfSymbol: "figure.run", muscle: .cardio),
ExerciseTemplate(name: "Cycling", sfSymbol: "figure.outdoor.cycle", muscle: .cardio),
ExerciseTemplate(name: "Jump Rope", sfSymbol: "figure.jumprope", muscle: .cardio),
ExerciseTemplate(name: "Rowing Machine", sfSymbol: "figure.rowing", muscle: .cardio),
ExerciseTemplate(name: "Stair Climber", sfSymbol: "figure.stair.stepper", muscle: .cardio),
]
let exerciseSFSymbol: [String: String] = Dictionary(
uniqueKeysWithValues: exerciseLibrary.map { ($0.name, $0.sfSymbol) }
)
// MARK: - Workout View Model
class WorkoutViewModel: ObservableObject {
@Published var programs: [WorkoutProgram]
@Published var schedule: [Int: UUID]
@Published var activeProgramId: UUID
@Published var activeSections: [WorkoutSection]
@Published var streakDays: Int = 5
@Published var restTimerSeconds: Int = 60
@Published var showAddExercise: Bool = false
@Published var addExerciseTargetSectionId: UUID?
private static let kPrograms = "kisani.workout.programs"
private static let kSchedule = "kisani.workout.schedule"
private static let kActivePid = "kisani.workout.activePid"
init() {
// Attempt to restore persisted state
if let data = UserDefaults.standard.data(forKey: Self.kPrograms),
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
!saved.isEmpty {
programs = saved
var sched: [Int: UUID] = [:]
if let dict = UserDefaults.standard.dictionary(forKey: Self.kSchedule) as? [String: String] {
for (k, v) in dict {
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
}
}
schedule = sched
var pid = saved[0].id
if let str = UserDefaults.standard.string(forKey: Self.kActivePid),
let uid = UUID(uuidString: str),
saved.contains(where: { $0.id == uid }) { pid = uid }
activeProgramId = pid
activeSections = saved.first { $0.id == pid }?.sections ?? saved[0].sections
} else {
// First launch: seed with sample programs
let progs = WorkoutViewModel.samplePrograms()
programs = progs
var sched: [Int: UUID] = [:]
if progs.count >= 6 {
sched[2] = progs[0].id
sched[3] = progs[1].id
sched[4] = progs[2].id
sched[5] = progs[3].id
sched[6] = progs[4].id
sched[7] = progs[5].id
}
schedule = sched
let today = Calendar.current.component(.weekday, from: Date())
if let pid = sched[today], progs.contains(where: { $0.id == pid }) {
activeProgramId = pid
activeSections = progs.first { $0.id == pid }!.sections
} else {
activeProgramId = progs[0].id
activeSections = progs[0].sections
}
}
}
private func save() {
if let data = try? JSONEncoder().encode(programs) {
UserDefaults.standard.set(data, forKey: Self.kPrograms)
}
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
UserDefaults.standard.set(activeProgramId.uuidString, forKey: Self.kActivePid)
}
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
var workoutTitle: String { activeProgram?.name ?? "Workout" }
var workoutEmoji: String { activeProgram?.emoji ?? "💪" }
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
// MARK: - Program management
func switchProgram(to id: UUID) {
syncBack()
guard let prog = programs.first(where: { $0.id == id }) else { return }
activeProgramId = id
activeSections = prog.sections
save()
}
func addProgram(name: String, emoji: String, colorIndex: Int = 0) {
let defaultSection = WorkoutSection(name: "Main", exercises: [])
programs.append(WorkoutProgram(name: name, emoji: emoji, colorIndex: colorIndex, sections: [defaultSection]))
save()
}
func deleteProgram(_ id: UUID) {
guard id != activeProgramId, programs.count > 1 else { return }
programs.removeAll { $0.id == id }
schedule = schedule.filter { $0.value != id }
save()
}
func renameProgram(_ id: UUID, name: String, emoji: String? = nil) {
guard let idx = programs.firstIndex(where: { $0.id == id }) else { return }
programs[idx].name = name
if let emoji { programs[idx].emoji = emoji }
if id == activeProgramId { objectWillChange.send() }
save()
}
func setSchedule(weekday: Int, programId: UUID?) {
if let pid = programId { schedule[weekday] = pid }
else { schedule.removeValue(forKey: weekday) }
save()
}
func program(for date: Date) -> WorkoutProgram? {
let weekday = Calendar.current.component(.weekday, from: date)
guard let pid = schedule[weekday] else { return nil }
return programs.first { $0.id == pid }
}
// MARK: - Section management
func addSection(name: String) {
activeSections.append(WorkoutSection(name: name, exercises: []))
syncBack()
}
func deleteSection(_ id: UUID) {
activeSections.removeAll { $0.id == id }
syncBack()
}
func renameSection(_ id: UUID, name: String) {
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
activeSections[si].name = name
syncBack()
}
func moveSectionUp(_ id: UUID) {
guard let idx = activeSections.firstIndex(where: { $0.id == id }), idx > 0 else { return }
activeSections.swapAt(idx, idx - 1)
syncBack()
}
func moveSectionDown(_ id: UUID) {
guard let idx = activeSections.firstIndex(where: { $0.id == id }),
idx < activeSections.count - 1 else { return }
activeSections.swapAt(idx, idx + 1)
syncBack()
}
func toggleSectionExpanded(_ id: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
activeSections[si].isExpanded.toggle()
}
// MARK: - Exercise management
func toggleSet(sectionId: UUID, exerciseId: UUID, setId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId }),
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
else { return }
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
syncBack()
}
func addSet(sectionId: UUID, to exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
let last = activeSections[si].exercises[ei].sets.last
activeSections[si].exercises[ei].sets.append(ExerciseSet(weight: last?.weight ?? 0, reps: last?.reps ?? 10))
syncBack()
}
// Active-program convenience wrappers
func addExercise(to sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
addExercise(programId: activeProgramId, sectionId: sectionId, name: name, setsCount: setsCount, repsCount: repsCount)
}
func deleteExercise(sectionId: UUID, exerciseId: UUID) {
deleteExercise(programId: activeProgramId, sectionId: sectionId, exerciseId: exerciseId)
}
func moveExercise(sectionId: UUID, from: IndexSet, to: Int) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }) else { return }
activeSections[si].exercises.move(fromOffsets: from, toOffset: to)
syncBack()
}
// MARK: - Universal (any program) operations
func addExercise(programId: UUID, sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
let template = exerciseLibrary.first { $0.name == name }
let symbol = template?.sfSymbol ?? "figure.strengthtraining.functional"
let isDumbb = template?.isDumbbell ?? false
let sets = (0..<setsCount).map { _ in ExerciseSet(weight: 0, reps: repsCount) }
let colorIdx = programs[pi].allExercises.count % 4
programs[pi].sections[si].exercises.append(
Exercise(name: name, sfSymbol: symbol, colorIndex: colorIdx, sets: sets, isDumbbell: isDumbb)
)
if programId == activeProgramId { activeSections = programs[pi].sections }
}
func deleteExercise(programId: UUID, sectionId: UUID, exerciseId: UUID) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
programs[pi].sections[si].exercises.removeAll { $0.id == exerciseId }
if programId == activeProgramId { activeSections = programs[pi].sections }
}
func addSection(to programId: UUID, name: String) {
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
programs[pi].sections.append(WorkoutSection(name: name, exercises: []))
if programId == activeProgramId { activeSections = programs[pi].sections }
}
func deleteSection(from programId: UUID, sectionId: UUID) {
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
programs[pi].sections.removeAll { $0.id == sectionId }
if programId == activeProgramId { activeSections = programs[pi].sections }
}
func renameSection(in programId: UUID, sectionId: UUID, name: String) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
programs[pi].sections[si].name = name
if programId == activeProgramId { activeSections = programs[pi].sections }
}
func toggleExpanded(sectionId: UUID, exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
activeSections[si].exercises[ei].isExpanded.toggle()
}
func toggleDumbbell(sectionId: UUID, exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
activeSections[si].exercises[ei].isDumbbell.toggle()
syncBack()
}
private func syncBack() {
guard let idx = programs.firstIndex(where: { $0.id == activeProgramId }) else { return }
programs[idx].sections = activeSections
save()
}
// MARK: - Sample data
static func samplePrograms() -> [WorkoutProgram] {
func s(_ count: Int, reps: Int = 12) -> [ExerciseSet] {
(0..<count).map { _ in ExerciseSet(reps: reps) }
}
let warmUp = WorkoutSection(name: "Warm Up", exercises: [
Exercise(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3, reps: 10)),
Exercise(name: "Dips", sfSymbol: "figure.gymnastics", colorIndex: 0, sets: s(3, reps: 10)),
Exercise(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", colorIndex: 2, sets: s(3, reps: 10)),
])
// lbs kg conversion factor
let lb: Double = 0.453592
// Monday: Chest & Triceps
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "🏋️", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Chest", exercises: [
// Incline first 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
Exercise(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 70 * lb, reps: 12),
ExerciseSet(weight: 65 * lb, reps: 12),
ExerciseSet(weight: 45 * lb, reps: 12),
ExerciseSet(weight: 35 * lb, reps: 12),
], isDumbbell: true),
// Flat 65 / 45 / 35 lbs × 12 reps
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 65 * lb, reps: 12),
ExerciseSet(weight: 45 * lb, reps: 12),
ExerciseSet(weight: 35 * lb, reps: 12),
]),
// Chest Flys 35 / 25 / 25 lbs × 12 reps (per-side dumbbell)
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: [
ExerciseSet(weight: 35 * lb, reps: 12),
ExerciseSet(weight: 25 * lb, reps: 12),
ExerciseSet(weight: 25 * lb, reps: 12),
], isDumbbell: true),
]),
WorkoutSection(name: "Triceps", exercises: [
// Tricep Pushdown 50 / 45 / 45 kg × 12 reps
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: [
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 45, reps: 12),
ExerciseSet(weight: 45, reps: 12),
]),
// Overhead 50 kg × 3 sets
Exercise(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 50, reps: 12),
]),
// Skull Crushers 15 kg × 3 sets
Exercise(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 15, reps: 12),
ExerciseSet(weight: 15, reps: 12),
ExerciseSet(weight: 15, reps: 12),
]),
]),
])
// Tuesday: Back & Biceps
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "💪", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Back", exercises: [
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
ExerciseSet(weight: 70, reps: 12),
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
]),
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 0, sets: [
ExerciseSet(weight: 60 * lb, reps: 12),
ExerciseSet(weight: 55 * lb, reps: 12),
ExerciseSet(weight: 55 * lb, reps: 12),
]),
Exercise(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 2, sets: [
ExerciseSet(weight: 70 * lb, reps: 10),
ExerciseSet(weight: 70, reps: 10),
ExerciseSet(weight: 70, reps: 10),
], isDumbbell: true),
]),
WorkoutSection(name: "Biceps", exercises: [
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 10, reps: 12),
ExerciseSet(weight: 10, reps: 12),
ExerciseSet(weight: 10, reps: 12),
], isDumbbell: true),
Exercise(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
ExerciseSet(weight: 30, reps: 12),
ExerciseSet(weight: 30, reps: 12),
ExerciseSet(weight: 30, reps: 12),
], isDumbbell: true),
Exercise(name: "Preacher Curl", sfSymbol: "dumbbell.fill", colorIndex: 1, sets: [
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
]),
]),
])
// Wednesday: Shoulders, Triceps & Biceps
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "🔥", colorIndex: 2, sections: [
warmUp,
WorkoutSection(name: "Shoulders", exercises: [
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
Exercise(name: "Front Raises", sfSymbol: "figure.arms.open", colorIndex: 0, sets: s(3)),
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 1, sets: s(3)),
Exercise(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
]),
WorkoutSection(name: "Triceps", exercises: [
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
]),
WorkoutSection(name: "Biceps", exercises: [
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
]),
])
// Thursday: Legs & Chest
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "🦵", colorIndex: 3, sections: [
warmUp,
WorkoutSection(name: "Legs", exercises: [
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Adductor Machine",sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 0, sets: s(3)),
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
]),
WorkoutSection(name: "Chest", exercises: [
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
]),
])
// Friday: Full Body
let fri = WorkoutProgram(name: "Full Body", emoji: "", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3)),
Exercise(name: "Dumbbell Shoulder Press", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
]),
WorkoutSection(name: "Lower Body", exercises: [
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 1, sets: s(3)),
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
]),
])
// Saturday: Full Body
let sat = WorkoutProgram(name: "Full Body", emoji: "💥", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 1, sets: s(3)),
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 3, sets: s(3)),
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
]),
WorkoutSection(name: "Lower Body", exercises: [
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
]),
])
return [mon, tue, wed, thu, fri, sat]
}
}

View File

@@ -0,0 +1,151 @@
import SwiftUI
// MARK: - Task Model
struct TaskItem: Identifiable {
var id = UUID()
var title: String
var dueDate: Date?
var isComplete: Bool = false
var completedAt: Date? = nil
var quadrant: Quadrant = .urgent
var category: TaskCategory = .reminder
var taskColor: TaskColor = .orange
var isRecurring: Bool = false
}
enum Quadrant: String, CaseIterable, Identifiable {
var id: String { rawValue }
case urgent = "Urgent + Important"
case schedule = "Schedule It"
case delegate_ = "Delegate"
case eliminate = "Eliminate"
}
enum TaskCategory: String {
case reminder = "Reminder"
case birthday = "Birthday"
case domain = "Domain"
case annual = "Annual"
case custom = "Custom"
}
enum TaskColor {
case orange, green, blue, yellow
func color() -> Color {
switch self {
case .orange: return AppColors.accent
case .green: return AppColors.green
case .blue: return AppColors.blue
case .yellow: return AppColors.yellow
}
}
func soft() -> Color {
switch self {
case .orange: return AppColors.accentSoft
case .green: return AppColors.greenSoft
case .blue: return AppColors.blueSoft
case .yellow: return AppColors.yellowSoft
}
}
}
// MARK: - Task View Model
class TaskViewModel: ObservableObject {
@Published var tasks: [TaskItem] = sampleTasks
var overdueTasks: [TaskItem] {
let startOfDay = Calendar.current.startOfDay(for: Date())
return tasks.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) < startOfDay }
}
var todayTasks: [TaskItem] {
let start = Calendar.current.startOfDay(for: Date())
let end = Calendar.current.date(byAdding: .day, value: 1, to: start)!
return tasks
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= start && ($0.dueDate ?? .distantFuture) < end }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
}
var upcomingTasks: [TaskItem] {
let tomorrow = Calendar.current.date(byAdding: .day, value: 1,
to: Calendar.current.startOfDay(for: Date()))!
return tasks
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= tomorrow }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
}
var completedTodayTasks: [TaskItem] {
let startOfDay = Calendar.current.startOfDay(for: Date())
return tasks
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
}
func toggle(_ task: TaskItem) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].isComplete.toggle()
tasks[idx].completedAt = tasks[idx].isComplete ? Date() : nil
}
func tasks(for quadrant: Quadrant) -> [TaskItem] {
tasks.filter { $0.quadrant == quadrant }
}
func addTask(title: String, dueDate: Date?, quadrant: Quadrant = .urgent,
category: TaskCategory = .reminder, taskColor: TaskColor = .orange,
isRecurring: Bool = false) {
tasks.append(TaskItem(title: title, dueDate: dueDate, quadrant: quadrant,
category: category, taskColor: taskColor, isRecurring: isRecurring))
}
func postpone(_ task: TaskItem, days: Int = 1) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
let base = tasks[idx].dueDate ?? Date()
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
}
func postponeAllOverdue(days: Int = 1) {
let ids = overdueTasks.map { $0.id }
for id in ids {
guard let idx = tasks.firstIndex(where: { $0.id == id }) else { continue }
let base = tasks[idx].dueDate ?? Date()
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
}
}
func delete(_ task: TaskItem) {
tasks.removeAll { $0.id == task.id }
}
}
// MARK: - Sample Data
private let sampleTasks: [TaskItem] = [
.init(title: "Azure exam preparation",
dueDate: cal.date(byAdding: .day, value: -7, to: Date()),
quadrant: .urgent, category: .custom, taskColor: .orange),
.init(title: "Leona's Birthday",
dueDate: md(6, 17), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
.init(title: "Provoc.ug Domain Expiry",
dueDate: md(7, 3), quadrant: .urgent, category: .domain, taskColor: .blue),
.init(title: "Farouk's Birthday",
dueDate: md(11, 8), quadrant: .delegate_, category: .birthday, taskColor: .green, isRecurring: true),
.init(title: "Enoch's Birthday",
dueDate: md(2, 23, 2027), quadrant: .schedule, category: .annual, taskColor: .yellow, isRecurring: true),
.init(title: "Dad's birthday",
dueDate: md(6, 8), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
.init(title: "Moms bd",
dueDate: md(2, 15, 2027), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
.init(title: "Amooti's birthday",
dueDate: md(6, 26), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
.init(title: "Provoc hosting",
dueDate: md(11, 25, 2027), quadrant: .delegate_, category: .domain, taskColor: .blue),
.init(title: "DV lottery preps",
dueDate: md(9, 30, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
.init(title: "Catch up Umutoni",
dueDate: md(11, 15, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
.init(title: "Siraj wedding preps",
dueDate: md(8, 24, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
]
private let cal = Calendar.current
private func md(_ month: Int, _ day: Int, _ year: Int = 2026) -> Date {
var c = DateComponents(); c.year = year; c.month = month; c.day = day
return cal.date(from: c) ?? Date()
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,123 @@
import SwiftUI
// MARK: - Accent & Semantic Colors
struct AppColors {
// Fixed accent palette
static let accent = Color(r: 232, g: 98, b: 42)
static let green = Color(r: 62, g: 207, b: 142)
static let blue = Color(r: 77, g: 157, b: 224)
static let yellow = Color(r: 232, g: 184, b: 42)
static let red = Color(r: 232, g: 66, b: 66)
static let accentSoft = Color(UIColor { $0.userInterfaceStyle == .dark
? UIColor(r: 52, g: 28, b: 14) // dark: deep warm orange
: UIColor(r: 252, g: 238, b: 228) })// light: warm peach wash
static let accentGhost = accent.opacity(0.05)
static let greenSoft = Color(UIColor { $0.userInterfaceStyle == .dark
? UIColor(r: 14, g: 40, b: 28)
: UIColor(r: 220, g: 248, b: 236) })
static let blueSoft = Color(UIColor { $0.userInterfaceStyle == .dark
? UIColor(r: 14, g: 30, b: 50)
: UIColor(r: 222, g: 237, b: 252) })
static let yellowSoft = Color(UIColor { $0.userInterfaceStyle == .dark
? UIColor(r: 44, g: 34, b: 10)
: UIColor(r: 252, g: 243, b: 215) })
static let redSoft = Color(UIColor { $0.userInterfaceStyle == .dark
? UIColor(r: 50, g: 14, b: 14)
: UIColor(r: 252, g: 224, b: 224) })
// Adaptive surfaces
static func background(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 9, g: 9, b: 9)
: Color(r: 245, g: 243, b: 240)
}
static func surface(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 19, g: 19, b: 19)
: Color(r: 255, g: 253, b: 250) // warm white, not clinical pure white
}
static func surface2(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 26, g: 26, b: 26)
: Color(r: 240, g: 237, b: 232)
}
static func surface3(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 34, g: 34, b: 34)
: Color(r: 232, g: 228, b: 222)
}
static func text(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 240, g: 237, b: 232)
: Color(r: 26, g: 23, b: 20)
}
static func text2(_ cs: ColorScheme) -> Color {
text(cs).opacity(cs == .dark ? 0.44 : 0.50)
}
static func text3(_ cs: ColorScheme) -> Color {
text(cs).opacity(cs == .dark ? 0.24 : 0.34)
}
static func border(_ cs: ColorScheme) -> Color {
cs == .dark ? Color.white.opacity(0.07)
: Color.black.opacity(0.09)
}
static func borderHi(_ cs: ColorScheme) -> Color {
cs == .dark ? Color.white.opacity(0.13)
: Color.black.opacity(0.17)
}
static func phoneSheetBg(_ cs: ColorScheme) -> Color {
cs == .dark ? Color(r: 18, g: 18, b: 18)
: Color.white
}
}
private extension Color {
init(r: Double, g: Double, b: Double) {
self.init(.sRGB, red: r / 255, green: g / 255, blue: b / 255)
}
}
private extension UIColor {
convenience init(r: Int, g: Int, b: Int) {
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: 1)
}
}
// MARK: - Typography
struct AppFonts {
static func sans(_ size: CGFloat, weight: Font.Weight = .regular) -> Font {
.system(size: size, weight: weight, design: .default)
}
static func mono(_ size: CGFloat, weight: Font.Weight = .regular) -> Font {
.system(size: size, weight: weight, design: .monospaced)
}
}
// MARK: - Motion Tokens
enum KisaniSpring {
/// General entrance slight overshoot, feels alive ( 320ms settle)
static let entrance = Animation.spring(response: 0.38, dampingFraction: 0.72)
/// Exit snappier, ~20% shorter settle than entrance
static let exit = Animation.spring(response: 0.28, dampingFraction: 0.88)
/// Micro-interactions button presses, toggles, chips (< 150ms feel)
static let micro = Animation.spring(response: 0.22, dampingFraction: 0.70)
/// Snappy section expands, panel slides ( 250ms settle)
static let snappy = Animation.spring(response: 0.32, dampingFraction: 0.78)
/// Bouncy checkmarks, completion states noticeable overshoot
static let bounce = Animation.spring(response: 0.28, dampingFraction: 0.52)
}
// MARK: - Corner Radii
enum AppRadius {
static let large: CGFloat = 16
static let medium: CGFloat = 13
static let small: CGFloat = 10
static let pill: CGFloat = 20
}
// MARK: - Icon color helpers
extension Int {
var iconColor: Color {
[AppColors.accent, AppColors.blue, AppColors.green, AppColors.yellow][self % 4]
}
var iconBg: Color {
[AppColors.accentSoft, AppColors.blueSoft, AppColors.greenSoft, AppColors.yellowSoft][self % 4]
}
}

View File

@@ -0,0 +1,261 @@
import SwiftUI
// MARK: - Exercise Picker Sheet
struct ExercisePickerSheet: View {
@EnvironmentObject var vm: WorkoutViewModel
@Environment(\.colorScheme) var cs
@Environment(\.dismiss) var dismiss
let sectionId: UUID
var programId: UUID? = nil // nil = active program
@State private var selectedMuscle: ExerciseMuscle = .chest
@State private var searchText = ""
@State private var customName = ""
@State private var setsText = "3"
@State private var repsText = "10"
@State private var showCustom = false
@FocusState private var customFocused: Bool
private func addExercise(name: String) {
let sets = Int(setsText) ?? 3
let reps = Int(repsText) ?? 10
if let pid = programId {
vm.addExercise(programId: pid, sectionId: sectionId, name: name, setsCount: sets, repsCount: reps)
} else {
vm.addExercise(to: sectionId, name: name, setsCount: sets, repsCount: reps)
}
}
private var filtered: [ExerciseTemplate] {
let byMuscle = exerciseLibrary.filter { $0.muscle == selectedMuscle }
guard !searchText.isEmpty else { return byMuscle }
return byMuscle.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("Add Exercise")
.font(AppFonts.sans(17, weight: .bold))
.foregroundColor(AppColors.text(cs))
Spacer()
Button("Cancel") { dismiss() }
.font(AppFonts.sans(13))
.foregroundColor(AppColors.text3(cs))
.buttonStyle(.plain)
}
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 12)
Divider().background(AppColors.border(cs))
// Search
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.font(.system(size: 13))
.foregroundColor(AppColors.text3(cs))
TextField("Search exercises…", text: $searchText)
.font(AppFonts.sans(13))
.foregroundColor(AppColors.text(cs))
if !searchText.isEmpty {
Button { searchText = "" } label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 13))
.foregroundColor(AppColors.text3(cs))
}
.buttonStyle(.plain)
}
}
.padding(10)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.border(cs), lineWidth: 1))
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
// Category Tabs
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
ForEach(ExerciseMuscle.allCases) { muscle in
let isSel = selectedMuscle == muscle
Button { withAnimation(KisaniSpring.micro) { selectedMuscle = muscle } } label: {
HStack(spacing: 4) {
Image(systemName: muscle.sfSymbol).font(.system(size: 10))
Text(muscle.rawValue).font(AppFonts.mono(9.5, weight: .bold))
}
.foregroundColor(isSel ? AppColors.accent : AppColors.text3(cs))
.padding(.horizontal, 11).padding(.vertical, 6)
.background(isSel ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(Capsule())
.overlay(Capsule().stroke(isSel ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
}
.buttonStyle(PressButtonStyle(scale: 0.94))
}
}
.padding(.horizontal, 16)
}
.padding(.bottom, 8)
// Exercise List
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 0) {
// Custom exercise entry
if showCustom {
VStack(alignment: .leading, spacing: 8) {
Text("CUSTOM EXERCISE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
TextField("Exercise name", text: $customName)
.font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
.focused($customFocused).padding(11)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(customFocused ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
SetRepsRow(setsText: $setsText, repsText: $repsText)
Button {
let n = customName.trimmingCharacters(in: .whitespaces)
guard !n.isEmpty else { return }
addExercise(name: n)
dismiss()
} label: {
Text("Add Custom Exercise")
.font(AppFonts.sans(13, weight: .bold)).foregroundColor(.white)
.frame(maxWidth: .infinity).padding(11)
.background(!customName.trimmingCharacters(in: .whitespaces).isEmpty ? AppColors.accent : AppColors.borderHi(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
}
.buttonStyle(.plain)
.disabled(customName.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding(14)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
.overlay(RoundedRectangle(cornerRadius: AppRadius.medium).stroke(AppColors.accent.opacity(0.3), lineWidth: 1))
.padding(.horizontal, 16).padding(.bottom, 8)
}
// Library exercises
ForEach(filtered) { template in
ExerciseTemplateRow(template: template) {
addExercise(name: template.name)
dismiss()
}
Divider().background(AppColors.border(cs)).padding(.leading, 60)
}
if filtered.isEmpty && !showCustom {
VStack(spacing: 6) {
Text("No exercises found")
.font(AppFonts.sans(13, weight: .medium))
.foregroundColor(AppColors.text2(cs))
Text("Try a different category or add a custom one")
.font(AppFonts.sans(11))
.foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity).padding(.vertical, 30)
}
}
.padding(.horizontal, 16)
}
// Footer: Default sets/reps + custom toggle
Divider().background(AppColors.border(cs))
VStack(spacing: 10) {
SetRepsRow(setsText: $setsText, repsText: $repsText)
Button {
withAnimation(KisaniSpring.snappy) {
showCustom.toggle()
if showCustom { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { customFocused = true } }
}
} label: {
HStack(spacing: 6) {
Image(systemName: showCustom ? "xmark.circle" : "plus.circle")
.font(.system(size: 13))
Text(showCustom ? "Cancel Custom" : "Add Custom Exercise")
.font(AppFonts.sans(12, weight: .semibold))
}
.foregroundColor(AppColors.accent)
.frame(maxWidth: .infinity)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 16).padding(.vertical, 12)
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Exercise Template Row
private struct ExerciseTemplateRow: View {
@Environment(\.colorScheme) private var cs
let template: ExerciseTemplate
let onAdd: () -> Void
var body: some View {
Button(action: onAdd) {
HStack(spacing: 12) {
Image(systemName: template.sfSymbol)
.font(.system(size: 15, weight: .medium))
.foregroundColor(AppColors.accent)
.frame(width: 36, height: 36)
.background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 9))
Text(template.name)
.font(AppFonts.sans(13, weight: .medium))
.foregroundColor(AppColors.text(cs))
Spacer()
Image(systemName: "plus.circle.fill")
.font(.system(size: 18))
.foregroundColor(AppColors.accent.opacity(0.7))
}
.padding(.vertical, 10)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
// MARK: - Sets / Reps Row (shared)
private struct SetRepsRow: View {
@Environment(\.colorScheme) private var cs
@Binding var setsText: String
@Binding var repsText: String
var body: some View {
HStack(spacing: 8) {
HStack(spacing: 6) {
Text("Sets").font(AppFonts.mono(9.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
TextField("3", text: $setsText)
.font(AppFonts.mono(12, weight: .semibold)).foregroundColor(AppColors.text(cs))
.keyboardType(.numberPad).multilineTextAlignment(.center)
.frame(width: 36).padding(.vertical, 5).padding(.horizontal, 4)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(AppColors.border(cs), lineWidth: 1))
}
.padding(8)
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
HStack(spacing: 6) {
Text("Reps").font(AppFonts.mono(9.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
TextField("10", text: $repsText)
.font(AppFonts.mono(12, weight: .semibold)).foregroundColor(AppColors.text(cs))
.keyboardType(.numberPad).multilineTextAlignment(.center)
.frame(width: 36).padding(.vertical, 5).padding(.horizontal, 4)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(AppColors.border(cs), lineWidth: 1))
}
.padding(8)
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
Spacer()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
import SwiftUI
struct MatrixView: View {
@Environment(\.colorScheme) var cs
@EnvironmentObject var taskVM: TaskViewModel
@State private var showAddTask = false
var body: some View {
ZStack(alignment: .bottomTrailing) {
AppColors.background(cs).ignoresSafeArea()
VStack(spacing: 0) {
// Header
HStack {
Text("Eisenhower Matrix")
.font(AppFonts.sans(15, weight: .semibold))
.foregroundColor(AppColors.text(cs))
Spacer()
IButton(icon: "ellipsis")
}
.padding(.horizontal, 18)
.padding(.top, 12)
.padding(.bottom, 10)
// 2×2 Grid fixed equal quadrants
VStack(spacing: 7) {
HStack(spacing: 7) {
QuadrantCard(
roman: "I", label: "Urgent & Important",
badgeBg: AppColors.accentSoft, badgeColor: AppColors.accent,
tasks: taskVM.tasks(for: .urgent)
) { taskVM.toggle($0) }
QuadrantCard(
roman: "II", label: "Not Urgent & Important",
badgeBg: AppColors.yellowSoft, badgeColor: AppColors.yellow,
tasks: taskVM.tasks(for: .schedule)
) { taskVM.toggle($0) }
}
.frame(maxHeight: .infinity)
HStack(spacing: 7) {
QuadrantCard(
roman: "III", label: "Urgent & Unimportant",
badgeBg: AppColors.blueSoft, badgeColor: AppColors.blue,
tasks: taskVM.tasks(for: .delegate_)
) { taskVM.toggle($0) }
QuadrantCard(
roman: "IV", label: "Not Urgent & Unimportant",
badgeBg: AppColors.greenSoft, badgeColor: AppColors.green,
tasks: taskVM.tasks(for: .eliminate)
) { taskVM.toggle($0) }
}
.frame(maxHeight: .infinity)
}
.frame(maxHeight: .infinity)
.padding(.horizontal, 12)
.padding(.bottom, 8)
}
.frame(maxHeight: .infinity)
FAB { showAddTask = true }
.padding(.trailing, 20)
.padding(.bottom, 10)
}
.sheet(isPresented: $showAddTask) {
AddTaskSheet()
.environmentObject(taskVM)
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
}
}
// MARK: - Quadrant Card
struct QuadrantCard: View {
@Environment(\.colorScheme) private var cs
let roman: String
let label: String
let badgeBg: Color
let badgeColor: Color
let tasks: [TaskItem]
let onToggle: (TaskItem) -> Void
private let shortFmt: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "d MMM"
return f
}()
private let shortFmtYear: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "d/MM/yyyy"
return f
}()
private func dateLabel(_ date: Date) -> String {
let cal = Calendar.current
if cal.isDateInThisYear(date) {
return shortFmt.string(from: date)
}
return shortFmtYear.string(from: date)
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Header badge
HStack(spacing: 4) {
Text(roman)
.font(AppFonts.mono(7.5, weight: .heavy))
.foregroundColor(.white)
.frame(width: 13, height: 13)
.background(badgeColor)
.clipShape(Circle())
Text(label.uppercased())
.font(AppFonts.mono(8, weight: .heavy))
.foregroundColor(badgeColor)
.lineLimit(1)
.minimumScaleFactor(0.65)
}
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(badgeBg)
.cornerRadius(20)
.padding(.bottom, 7)
// Scrollable task list
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
ForEach(tasks) { task in
HStack(alignment: .top, spacing: 6) {
RoundedRectangle(cornerRadius: 3)
.stroke(
task.isComplete ? AppColors.border(cs) : badgeColor,
lineWidth: 1.5
)
.background(
task.isComplete
? RoundedRectangle(cornerRadius: 3)
.fill(AppColors.border(cs).opacity(0.5))
: nil
)
.overlay(
task.isComplete
? Image(systemName: "checkmark")
.font(.system(size: 6.5, weight: .bold))
.foregroundColor(AppColors.text3(cs))
: nil
)
.frame(width: 13, height: 13)
.padding(.top, 1.5)
.contentShape(Rectangle().size(CGSize(width: 28, height: 28)))
.onTapGesture {
withAnimation(KisaniSpring.bounce) { onToggle(task) }
}
VStack(alignment: .leading, spacing: 1) {
Text(task.title)
.font(AppFonts.sans(10.5))
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
.strikethrough(task.isComplete, color: AppColors.text3(cs))
.lineLimit(2)
if let d = task.dueDate {
Text(dateLabel(d))
.font(AppFonts.mono(8.5))
.foregroundColor(task.isComplete ? AppColors.text3(cs) : badgeColor)
}
}
}
.opacity(task.isComplete ? 0.38 : 1)
.padding(.vertical, 3.5)
}
}
}
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
.overlay(
RoundedRectangle(cornerRadius: AppRadius.medium)
.stroke(AppColors.border(cs), lineWidth: 1)
)
}
}
private extension Calendar {
func isDateInThisYear(_ date: Date) -> Bool {
component(.year, from: date) == component(.year, from: Date())
}
}

View File

@@ -0,0 +1,223 @@
import SwiftUI
struct OnboardingView: View {
@Environment(\.colorScheme) private var cs
@Binding var isPresented: Bool
@AppStorage("bodyWeightKg") private var bodyWeightKg: Double = 0
@AppStorage("heightCm") private var heightCm: Double = 0
@AppStorage("weightUnit") private var weightUnit: Int = 0 // 0 = kg, 1 = lbs
@State private var weightStr = ""
@State private var heightStr = ""
@FocusState private var focusedField: Field?
fileprivate enum Field { case weight, height }
private var canContinue: Bool {
(Double(weightStr) ?? 0) > 0 && (Double(heightStr) ?? 0) > 0
}
var body: some View {
ZStack {
AppColors.background(cs).ignoresSafeArea()
VStack(spacing: 0) {
Spacer()
// Brand mark
VStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 22)
.fill(AppColors.accentSoft)
.frame(width: 72, height: 72)
Image(systemName: "dumbbell.fill")
.font(.system(size: 32, weight: .semibold))
.foregroundColor(AppColors.accent)
}
VStack(spacing: 6) {
Text("Welcome to Kisani")
.font(AppFonts.sans(26, weight: .bold))
.foregroundColor(AppColors.text(cs))
Text("Personalizes your warm-up and body stats")
.font(AppFonts.sans(14))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.bottom, 40)
// Form card
VStack(spacing: 0) {
// Unit toggle
HStack(spacing: 0) {
ForEach([("kg", 0), ("lbs", 1)], id: \.1) { label, idx in
Button {
withAnimation(KisaniSpring.micro) {
// Convert existing weight string when toggling
if let val = Double(weightStr) {
let kg = weightUnit == 1 ? val / 2.20462 : val
weightUnit = idx
let display = weightUnit == 1 ? kg * 2.20462 : kg
weightStr = display.truncatingRemainder(dividingBy: 1) == 0
? "\(Int(display))"
: String(format: "%.1f", display)
} else {
weightUnit = idx
}
}
} label: {
Text(label)
.font(AppFonts.mono(12, weight: .bold))
.foregroundColor(weightUnit == idx ? AppColors.accent : AppColors.text3(cs))
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(weightUnit == idx ? AppColors.accentSoft : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 7))
}
.buttonStyle(PressButtonStyle(scale: 0.96))
}
}
.padding(4)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(AppColors.border(cs), lineWidth: 1))
.padding(.horizontal, 28)
.padding(.bottom, 16)
// Fields
VStack(spacing: 0) {
OnboardingField(
icon: "scalemass.fill",
label: "Body Weight",
unit: weightUnit == 0 ? "kg" : "lbs",
placeholder: weightUnit == 0 ? "e.g. 80" : "e.g. 176",
text: $weightStr,
keyboard: .decimalPad,
focused: $focusedField,
tag: .weight
)
Divider().background(AppColors.border(cs)).padding(.leading, 56)
OnboardingField(
icon: "ruler.fill",
label: "Height",
unit: "cm",
placeholder: "e.g. 175",
text: $heightStr,
keyboard: .numberPad,
focused: $focusedField,
tag: .height
)
}
.cardStyle()
.padding(.horizontal, 20)
}
// Hint
Text("Used for bodyweight exercises and warm-up loads.\nEdit anytime in Settings → Workout Settings.")
.font(AppFonts.sans(11))
.foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
.padding(.top, 14)
Spacer()
// CTA
Button { commit() } label: {
HStack(spacing: 8) {
Text("Get Started")
.font(AppFonts.sans(15, weight: .bold))
Image(systemName: "arrow.right")
.font(.system(size: 13, weight: .bold))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(canContinue ? AppColors.accent : AppColors.borderHi(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
}
.buttonStyle(PressButtonStyle())
.disabled(!canContinue)
.padding(.horizontal, 20)
.padding(.bottom, 36)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") { focusedField = nil }
.font(AppFonts.sans(13, weight: .semibold))
.foregroundColor(AppColors.accent)
}
}
.onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { focusedField = .weight } }
}
private func commit() {
guard canContinue else { return }
let rawWeight = Double(weightStr) ?? 0
// Always store in kg internally
bodyWeightKg = weightUnit == 1 ? rawWeight / 2.20462 : rawWeight
heightCm = Double(heightStr) ?? 0
withAnimation(KisaniSpring.exit) { isPresented = false }
}
}
// MARK: - Field Row
private struct OnboardingField: View {
@Environment(\.colorScheme) private var cs
let icon: String
let label: String
let unit: String
let placeholder: String
@Binding var text: String
let keyboard: UIKeyboardType
var focused: FocusState<OnboardingView.Field?>.Binding
let tag: OnboardingView.Field
var isFocused: Bool { focused.wrappedValue == tag }
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 15))
.foregroundColor(isFocused ? AppColors.accent : AppColors.text3(cs))
.frame(width: 32, height: 32)
.background(isFocused ? AppColors.accentSoft : AppColors.surface3(cs))
.clipShape(RoundedRectangle(cornerRadius: 8))
.animation(KisaniSpring.micro, value: isFocused)
Text(label)
.font(AppFonts.sans(13))
.foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 5) {
TextField(placeholder, text: $text)
.font(AppFonts.mono(14, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.multilineTextAlignment(.trailing)
.keyboardType(keyboard)
.focused(focused, equals: tag)
.frame(width: 70)
.onChange(of: text) { val in
let clean: String
if keyboard == .decimalPad {
clean = String(val.filter { $0.isNumber || $0 == "." }.prefix(6))
} else {
clean = String(val.filter { $0.isNumber }.prefix(3))
}
if clean != val { text = clean }
}
Text(unit)
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.text3(cs))
.frame(width: 26, alignment: .leading)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
}
}

View File

@@ -0,0 +1,765 @@
import SwiftUI
struct SettingsView: View {
@Environment(\.colorScheme) var cs
@EnvironmentObject var workoutVM: WorkoutViewModel
// Persisted settings
@AppStorage("appearanceMode") private var appearanceMode = 0
@AppStorage("soundsOn") private var soundsOn = true
@AppStorage("healthOn") private var healthOn = false
@AppStorage("showMatrixTab") private var showMatrixTab = true
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
@AppStorage("dateFormat") private var dateFormat = 0
@AppStorage("firstDayMonday") private var firstDayMonday = false
@AppStorage("showCompleted") private var showCompleted = false
@AppStorage("weightUnit") private var weightUnit = 0
@AppStorage("defaultSets") private var defaultSets = 3
@AppStorage("defaultReps") private var defaultReps = 10
@AppStorage("iCloudSync") private var iCloudSync = true
@AppStorage("googleCalSync") private var googleCalSync = false
@AppStorage("iCloudCalSync") private var iCloudCalSync = true
// Sheet presentation
@State private var showTabBar = false
@State private var showAppearance = false
@State private var showDateTime = false
@State private var showWidgets = false
@State private var showGeneral = false
@State private var showImport = false
@State private var showBackup = false
@State private var showWorkoutSettings = false
@State private var showRestTimer = false
@State private var showHelp = false
@State private var showFollow = false
@State private var showAbout = false
private var appearanceLabel: String { ["System", "Light", "Dark"][appearanceMode] }
private var restLabel: String { "\(workoutVM.restTimerSeconds)s" }
private var weightLabel: String { weightUnit == 0 ? "kg" : "lbs" }
private var dateLabel: String { ["MMM d", "MM/DD", "DD/MM"][dateFormat] }
var body: some View {
ZStack {
AppColors.background(cs).ignoresSafeArea()
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Text("Settings")
.font(AppFonts.sans(15, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.padding(.top, 10).padding(.bottom, 4)
// Profile
HStack(spacing: 12) {
ZStack {
Circle().fill(AppColors.accentSoft)
Text("RK")
.font(AppFonts.sans(15, weight: .bold))
.foregroundColor(AppColors.accent)
}
.frame(width: 44, height: 44)
.overlay(Circle().stroke(AppColors.accent.opacity(0.18), lineWidth: 1.5))
VStack(alignment: .leading, spacing: 5) {
Text("Robin Kutesa")
.font(AppFonts.sans(14, weight: .semibold))
.foregroundColor(AppColors.text(cs))
HStack(spacing: 5) {
AppBadge(
text: workoutVM.streakDays > 0 ? "\(workoutVM.streakDays)d streak 🔥" : "No streak yet",
bg: AppColors.accentSoft, fg: AppColors.accent
)
AppBadge(
text: weightUnit == 0 ? "kg" : "lbs",
bg: AppColors.surface3(cs), fg: AppColors.text2(cs)
)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 10))
.foregroundColor(AppColors.text3(cs))
}
.padding(14).cardStyle()
.padding(.horizontal, 14).padding(.top, 10).padding(.bottom, 14)
// CUSTOMIZE
SttSection(label: "Customize") {
SttNavRow(icon: "menucard", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Tab Bar", value: "\(showMatrixTab && showWorkoutTab ? "5" : showMatrixTab || showWorkoutTab ? "4" : "3") tabs") { showTabBar = true }
SttNavRow(icon: "paintpalette", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Appearance", value: appearanceLabel) { showAppearance = true }
SttToggleRow(icon: "bell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Sounds & Notifications", isOn: $soundsOn)
SttNavRow(icon: "clock", bg: AppColors.greenSoft, fg: AppColors.green, label: "Date & Time", value: dateLabel) { showDateTime = true }
SttNavRow(icon: "puzzlepiece", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Widgets") { showWidgets = true }
SttNavRow(icon: "gearshape", bg: AppColors.surface3(cs), label: "General", isLast: true) { showGeneral = true }
}
// DATA
SttSection(label: "Data") {
SttNavRow(icon: "arrow.up.right", bg: AppColors.greenSoft, fg: AppColors.green, label: "Connected Sources", value: googleCalSync || iCloudCalSync ? "Connected" : nil) { showImport = true }
SttNavRow(icon: "cloud", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Library Sync", value: iCloudSync ? "● Synced" : "Off", valueColor: iCloudSync ? AppColors.green : nil, isLast: true) { showBackup = true }
}
// FITNESS
SttSection(label: "Fitness") {
SttNavRow(icon: "dumbbell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Workout Settings", value: weightLabel) { showWorkoutSettings = true }
SttNavRow(icon: "figure.run", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Rest Timer", value: restLabel) { showRestTimer = true }
SttToggleRow(icon: "heart", bg: AppColors.greenSoft, fg: AppColors.green, label: "Health Integration", isOn: $healthOn, isLast: true)
}
// SUPPORT
SttSection(label: "Support", secondary: true) {
SttNavRow(icon: "star", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Help & Feedback") { showHelp = true }
SttNavRow(icon: "message", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Follow Us", value: "𝕏 👾 📸") { showFollow = true }
SttNavRow(icon: "info.circle", bg: AppColors.surface3(cs), label: "About", value: "v1.0.0", isLast: true) { showAbout = true }
}
Spacer().frame(height: 40)
}
}
}
.sheet(isPresented: $showTabBar) { TabBarSheet(showMatrix: $showMatrixTab, showWorkout: $showWorkoutTab).presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showAppearance) { AppearanceSheet(mode: $appearanceMode).presentationDetents([.height(290)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showDateTime) { DateTimeSheet(format: $dateFormat, mondayFirst: $firstDayMonday).presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showWidgets) { WidgetsSheet().presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showGeneral) { GeneralSheet(showCompleted: $showCompleted, mondayFirst: $firstDayMonday).presentationDetents([.height(260)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showImport) { ImportSheet(googleCal: $googleCalSync, iCloudCal: $iCloudCalSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showBackup) { BackupSheet(iCloudSync: $iCloudSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).presentationDetents([.large]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showRestTimer) { RestTimerSheet(seconds: $workoutVM.restTimerSeconds).presentationDetents([.height(340)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showHelp) { HelpSheet().presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showFollow) { FollowSheet().presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showAbout) { AboutSheet().presentationDetents([.height(340)]).presentationDragIndicator(.visible) }
}
}
// MARK: - Section
private struct SttSection<Content: View>: View {
@Environment(\.colorScheme) private var cs
let label: String
var secondary: Bool = false
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(label)
.font(AppFonts.sans(secondary ? 9.5 : 10, weight: .semibold))
.foregroundColor(secondary ? AppColors.text3(cs) : AppColors.text2(cs))
.padding(.horizontal, 18).padding(.bottom, 5)
VStack(spacing: 0) { content }
.cardStyle().padding(.horizontal, 14).padding(.bottom, secondary ? 6 : 8)
}
}
}
// MARK: - Nav Row
private struct SttNavRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let bg: Color
var fg: Color? = nil
let label: String
var value: String? = nil; var valueColor: Color? = nil; var isLast: Bool = false
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: icon).font(.system(size: 14))
.foregroundColor(fg ?? AppColors.text2(cs))
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
if let v = value {
Text(v).font(AppFonts.mono(11))
.foregroundColor(valueColor ?? AppColors.text2(cs)).lineLimit(1)
}
Image(systemName: "chevron.right").font(.system(size: 10))
.foregroundColor(AppColors.text3(cs))
}
.padding(.horizontal, 14).padding(.vertical, 11)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
}.contentShape(Rectangle())
}.buttonStyle(.plain)
}
}
// MARK: - Toggle Row
private struct SttToggleRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let bg: Color
var fg: Color? = nil
let label: String
@Binding var isOn: Bool; var isLast: Bool = false
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: icon).font(.system(size: 14))
.foregroundColor(fg ?? AppColors.text2(cs))
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
}
}
}
// MARK: - Shared Header
private struct SheetHeader: View {
@Environment(\.colorScheme) private var cs
let title: String; let onDone: () -> Void
var body: some View {
HStack {
Text(title).font(AppFonts.sans(17, weight: .bold)).foregroundColor(AppColors.text(cs))
Spacer()
Button("Done", action: onDone).font(AppFonts.sans(13, weight: .semibold))
.foregroundColor(AppColors.accent).buttonStyle(.plain)
}
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
}
}
// MARK: - Tab Bar Sheet
private struct TabBarSheet: View {
@Binding var showMatrix: Bool
@Binding var showWorkout: Bool
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Tab Bar") { dismiss() }
VStack(spacing: 0) {
TabToggleRow(icon: "checkmark", label: "Tasks", isOn: .constant(true), locked: true)
TabToggleRow(icon: "calendar", label: "Calendar", isOn: .constant(true), locked: true)
TabToggleRow(icon: "square.grid.2x2", label: "Matrix", isOn: $showMatrix, locked: false)
TabToggleRow(icon: "dumbbell", label: "Workout", isOn: $showWorkout, locked: false)
TabToggleRow(icon: "gearshape", label: "Settings", isOn: .constant(true), locked: true, isLast: true)
}
.cardStyle().padding(.horizontal, 20)
Text("Tasks, Calendar, and Settings are always visible.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
private struct TabToggleRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let label: String
@Binding var isOn: Bool; let locked: Bool; var isLast: Bool = false
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: icon).font(.system(size: 13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs)).frame(width: 22)
Text(label).font(AppFonts.sans(13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs))
Spacer()
if locked {
Text("Always on").font(AppFonts.mono(9.5)).foregroundColor(AppColors.text3(cs))
} else {
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
}
}
.padding(.horizontal, 14).padding(.vertical, 12)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 48) }
}
}
}
// MARK: - Appearance Sheet
private struct AppearanceSheet: View {
@Binding var mode: Int
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let options = [("System", "circle.lefthalf.filled"), ("Light", "sun.max"), ("Dark", "moon.stars")]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Appearance") { dismiss() }
VStack(spacing: 8) {
ForEach(options.indices, id: \.self) { i in
Button { withAnimation(KisaniSpring.micro) { mode = i } } label: {
HStack(spacing: 14) {
Image(systemName: options[i].1).font(.system(size: 15, weight: .medium))
.foregroundColor(mode == i ? AppColors.accent : AppColors.text2(cs)).frame(width: 24)
Text(options[i].0).font(AppFonts.sans(14, weight: mode == i ? .semibold : .regular))
.foregroundColor(AppColors.text(cs))
Spacer()
if mode == i { Image(systemName: "checkmark").font(.system(size: 12, weight: .bold)).foregroundColor(AppColors.accent) }
}
.padding(.horizontal, 16).padding(.vertical, 14)
.background(mode == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
.overlay(RoundedRectangle(cornerRadius: AppRadius.medium).stroke(mode == i ? AppColors.accent.opacity(0.35) : AppColors.border(cs), lineWidth: 1))
}.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Date & Time Sheet
private struct DateTimeSheet: View {
@Binding var format: Int
@Binding var mondayFirst: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let formats = ["MMM d", "MM/DD/YYYY", "DD/MM/YYYY"]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Date & Time") { dismiss() }
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 8) {
Text("DATE FORMAT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 8) {
ForEach(formats.indices, id: \.self) { i in
Button { withAnimation { format = i } } label: {
Text(formats[i]).font(AppFonts.mono(11, weight: .bold))
.foregroundColor(format == i ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 10)
.background(format == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(format == i ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
}.buttonStyle(.plain)
}
}
}
VStack(spacing: 0) {
HStack {
Text("First day of week").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Text(mondayFirst ? "Monday" : "Sunday").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle()
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Widgets Sheet
private struct WidgetsSheet: View {
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let widgets = [("Small", "1×1 — Today's task count", "square.grid.2x2"), ("Medium", "2×1 — Upcoming tasks list", "rectangle.grid.1x2"), ("Large", "2×2 — Full task view", "square.split.2x2")]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Widgets") { dismiss() }
VStack(spacing: 10) {
ForEach(widgets, id: \.0) { w in
HStack(spacing: 14) {
Image(systemName: w.2).font(.system(size: 20)).foregroundColor(AppColors.accent)
.frame(width: 44, height: 44).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(w.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(w.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
}
.padding(12).cardStyle()
}
Text("Long-press your home screen → tap + → search Kisani Cal to add widgets.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.top, 4)
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - General Sheet
private struct GeneralSheet: View {
@Binding var showCompleted: Bool
@Binding var mondayFirst: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "General") { dismiss() }
VStack(spacing: 0) {
HStack {
Text("Show completed tasks").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $showCompleted).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack {
Text("Week starts on Monday").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle().padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Import Sheet
private struct ImportSheet: View {
@Binding var googleCal: Bool
@Binding var iCloudCal: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Import & Integration") { dismiss() }
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "calendar.badge.plus").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("Google Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $googleCal).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack(spacing: 12) {
Image(systemName: "cloud").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("iCloud Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $iCloudCal).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle().padding(.horizontal, 20)
Text("Enabling integrations will sync events to your Kisani calendar.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Backup Sheet
private struct BackupSheet: View {
@Binding var iCloudSync: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Backup & Sync") { dismiss() }
VStack(spacing: 14) {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "icloud").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("iCloud Sync").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $iCloudSync).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
if iCloudSync {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack {
Text("Last synced").font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
Text("Today · just now").font(AppFonts.mono(11)).foregroundColor(AppColors.green)
}
.padding(.horizontal, 14).padding(.vertical, 10)
}
}
.cardStyle().padding(.horizontal, 20)
.animation(KisaniSpring.snappy, value: iCloudSync)
if iCloudSync {
Button {
// trigger sync
} label: {
Text("Sync Now")
.font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.accent)
.frame(maxWidth: .infinity).padding(13)
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.accent, lineWidth: 1.5))
}
.buttonStyle(.plain).padding(.horizontal, 20)
}
}
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Workout Settings Sheet
private struct WorkoutSettingsSheet: View {
@Binding var unit: Int
@Binding var sets: Int
@Binding var reps: Int
@AppStorage("bodyWeightKg") private var bodyWeightKg: Double = 80
@AppStorage("heightCm") private var heightCm: Double = 175
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
@State private var bwStr = ""
@State private var htStr = ""
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Workout Settings") { dismiss() }
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 20) {
// Body Profile
VStack(alignment: .leading, spacing: 8) {
Text("BODY PROFILE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack {
Text("Body Weight").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 4) {
TextField("80", text: $bwStr)
.font(AppFonts.mono(13, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.frame(width: 56)
.onChange(of: bwStr) { val in
let clean = String(val.filter { $0.isNumber || $0 == "." }.prefix(5))
if clean != val { bwStr = clean }
if let d = Double(clean) { bodyWeightKg = d }
}
Text("kg")
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.horizontal, 14).padding(.vertical, 11)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack {
Text("Height").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 4) {
TextField("175", text: $htStr)
.font(AppFonts.mono(13, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.multilineTextAlignment(.trailing)
.keyboardType(.numberPad)
.frame(width: 56)
.onChange(of: htStr) { val in
let clean = String(val.filter { $0.isNumber }.prefix(3))
if clean != val { htStr = clean }
if let d = Double(clean) { heightCm = d }
}
Text("cm")
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.horizontal, 14).padding(.vertical, 11)
}
.cardStyle()
}
// Weight Unit
VStack(alignment: .leading, spacing: 8) {
Text("WEIGHT UNIT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 10) {
ForEach(["kg", "lbs"].indices, id: \.self) { i in
Button { withAnimation(KisaniSpring.micro) { unit = i } } label: {
Text(["kg", "lbs"][i]).font(AppFonts.mono(13, weight: .bold))
.foregroundColor(unit == i ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 12)
.background(unit == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 9))
.overlay(RoundedRectangle(cornerRadius: 9).stroke(unit == i ? AppColors.accent : AppColors.border(cs), lineWidth: unit == i ? 1.5 : 1))
}.buttonStyle(.plain)
}
}
}
// Defaults
VStack(spacing: 0) {
StepperRow(label: "Default Sets", value: $sets, range: 1...10)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
StepperRow(label: "Default Reps", value: $reps, range: 1...30, isLast: true)
}
.cardStyle()
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}
.background(AppColors.background(cs).ignoresSafeArea())
.onAppear {
bwStr = bodyWeightKg > 0 ? (bodyWeightKg.truncatingRemainder(dividingBy: 1) == 0 ? "\(Int(bodyWeightKg))" : String(format: "%.1f", bodyWeightKg)) : ""
htStr = heightCm > 0 ? "\(Int(heightCm))" : ""
}
}
}
private struct StepperRow: View {
@Environment(\.colorScheme) private var cs
let label: String; @Binding var value: Int; let range: ClosedRange<Int>; var isLast: Bool = false
var body: some View {
HStack {
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 0) {
Button { if value > range.lowerBound { value -= 1 } } label: {
Image(systemName: "minus").font(.system(size: 13, weight: .semibold))
.foregroundColor(value > range.lowerBound ? AppColors.accent : AppColors.text3(cs))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
Text("\(value)").font(AppFonts.mono(13, weight: .bold))
.foregroundColor(AppColors.text(cs)).frame(width: 32).multilineTextAlignment(.center)
Button { if value < range.upperBound { value += 1 } } label: {
Image(systemName: "plus").font(.system(size: 13, weight: .semibold))
.foregroundColor(value < range.upperBound ? AppColors.accent : AppColors.text3(cs))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
}
.background(AppColors.surface2(cs)).clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
}
.padding(.horizontal, 14).padding(.vertical, 10)
}
}
// MARK: - Rest Timer Sheet
private struct RestTimerSheet: View {
@Binding var seconds: Int
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let options = [30, 45, 60, 90, 120, 180]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Rest Timer") { dismiss() }
Text("\(seconds)s").font(AppFonts.mono(52, weight: .bold)).foregroundColor(AppColors.accent).padding(.bottom, 20)
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 3), spacing: 10) {
ForEach(options, id: \.self) { opt in
Button { withAnimation(KisaniSpring.micro) { seconds = opt } } label: {
Text("\(opt)s").font(AppFonts.mono(14, weight: .bold))
.foregroundColor(seconds == opt ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 13)
.background(seconds == opt ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(seconds == opt ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
}.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Help & Feedback Sheet
private struct HelpSheet: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
@Environment(\.openURL) var openURL
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Help & Feedback") { dismiss() }
VStack(spacing: 10) {
HelpRow(icon: "envelope", label: "Send Feedback", sub: "Report a bug or suggest a feature") {
if let url = URL(string: "mailto:feedback@kisanicaI.app?subject=Kisani%20Cal%20Feedback") { openURL(url) }
}
HelpRow(icon: "questionmark.circle", label: "FAQ", sub: "Frequently asked questions") {}
HelpRow(icon: "book", label: "User Guide", sub: "Learn how to use Kisani Cal") {}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
private struct HelpRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let label: String; let sub: String; let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: icon).font(.system(size: 16)).foregroundColor(AppColors.accent)
.frame(width: 40, height: 40).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(label).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(sub).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Image(systemName: "chevron.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
}
.padding(12).cardStyle()
}.buttonStyle(.plain)
}
}
// MARK: - Follow Us Sheet
private struct FollowSheet: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
@Environment(\.openURL) var openURL
private let socials: [(String, String, String, String)] = [
("𝕏 Twitter / X", "Share your streaks", "at.circle", "https://x.com"),
("Discord", "Join the community", "bubble.left.and.bubble.right", "https://discord.com"),
("Instagram", "Behind the scenes", "camera", "https://instagram.com"),
]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Follow Us") { dismiss() }
VStack(spacing: 8) {
ForEach(socials, id: \.0) { s in
Button { if let url = URL(string: s.3) { openURL(url) } } label: {
HStack(spacing: 14) {
Image(systemName: s.2).font(.system(size: 16)).foregroundColor(AppColors.accent)
.frame(width: 40, height: 40).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(s.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(s.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Image(systemName: "arrow.up.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
}
.padding(12).cardStyle()
}.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - About Sheet
private struct AboutSheet: View {
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "About") { dismiss() }
VStack(spacing: 16) {
Image(systemName: "calendar.badge.clock")
.font(.system(size: 48, weight: .light)).foregroundColor(AppColors.accent)
VStack(spacing: 4) {
Text("Kisani Cal").font(AppFonts.sans(20, weight: .bold)).foregroundColor(AppColors.text(cs))
Text("Version 1.0.0").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
}
Text("A calm, focused productivity app for tasks, calendar, and fitness.")
.font(AppFonts.sans(13)).foregroundColor(AppColors.text2(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20)
}
.padding(.top, 10)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

35
PRODUCT.md Normal file
View File

@@ -0,0 +1,35 @@
# Product
## Register
product
## Users
Robin — a working professional who trains consistently 6 days a week and manages his day through a personal task system. Uses the app alone, primarily on iPhone, during morning planning and at the gym. Context is always personal: this is his own tool, not a team product.
## Product Purpose
Kisani Cal is a personal productivity and fitness companion: daily task management (Eisenhower matrix, overdue/today/upcoming), workout logging with weighted sets and bodyweight exercises, and a personal calendar. It exists because off-the-shelf tools feel impersonal. This one is his.
## Brand Personality
Calm · Personal · Sharp. Quiet confidence. Like a well-worn notebook that holds everything important but never demands attention. Nothing decorative, nothing gamified, nothing that belongs on a leaderboard.
## Anti-references
- Notion / Linear: over-engineered productivity, panel overload, too much structural chrome
- Generic fitness apps (MyFitnessPal, Fitbit): loud gradients, trophy animations, gamification badges, progress celebrations
- Apple Health / Fitness: over-polished and impersonal, feels like a device report, not yours
## Design Principles
1. Nothing shouts. Hierarchy through restraint, not decoration.
2. Personal over polished. Feels made for one person, not designed for a product screenshot.
3. Data is primary. Weights, reps, tasks are the content. The UI serves them, not the other way around.
4. Color earns its place. Orange for urgency/action, green for completion, red for overdue. Never decorative.
5. Sharp where it matters. Decisive weight contrasts and type scale, not soft or apologetic.
## Accessibility & Inclusion
Standard iOS Dynamic Type support. WCAG AA contrast minimum. Respect reduce-motion preference.

32
project.yml Normal file
View File

@@ -0,0 +1,32 @@
name: KisaniCal
options:
bundleIdPrefix: com.kutesir
deploymentTarget:
iOS: "16.0"
xcodeVersion: "15.0"
createIntermediateGroups: true
targets:
KisaniCal:
type: application
platform: iOS
deploymentTarget: "16.0"
sources:
- path: KisaniCal
settings:
base:
SWIFT_VERSION: 5.9
TARGETED_DEVICE_FAMILY: "1,2"
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
INFOPLIST_KEY_UILaunchStoryboardName: ""
INFOPLIST_KEY_CFBundleDisplayName: "Kisani Cal"
INFOPLIST_KEY_UIStatusBarStyle: UIStatusBarStyleDefault
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
CODE_SIGN_STYLE: Automatic
ENABLE_PREVIEWS: YES
INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks."