P0.1: iOS reference artifacts (colors, assets, screens inventory)
This commit is contained in:
57
docs/ios-parity/assets.json
Normal file
57
docs/ios-parity/assets.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"image_sets": [
|
||||
{
|
||||
"name": "icon",
|
||||
"path": "iosApp/iosApp/Assets.xcassets/icon.imageset",
|
||||
"files": [
|
||||
"HoneyDue-01-Standard@2x.png"
|
||||
],
|
||||
"format": "png"
|
||||
},
|
||||
{
|
||||
"name": "outline",
|
||||
"path": "iosApp/iosApp/Assets.xcassets/outline.imageset",
|
||||
"files": [
|
||||
"outline.pdf"
|
||||
],
|
||||
"format": "pdf"
|
||||
},
|
||||
{
|
||||
"name": "tab_view",
|
||||
"path": "iosApp/iosApp/Assets.xcassets/tab_view.imageset",
|
||||
"files": [
|
||||
"outline_1x.png",
|
||||
"outline_2x.png",
|
||||
"outline_3x.png"
|
||||
],
|
||||
"format": "png"
|
||||
}
|
||||
],
|
||||
"app_icons": [
|
||||
{
|
||||
"name": "AppIcon",
|
||||
"path": "iosApp/iosApp/Assets.xcassets/AppIcon.appiconset",
|
||||
"sizes": [
|
||||
"1024x1024 (universal)",
|
||||
"1024x1024 (universal)",
|
||||
"1024x1024 (universal)"
|
||||
],
|
||||
"files": [
|
||||
"HoneyDue-01-Standard@2x.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AppIcon",
|
||||
"path": "iosApp/HoneyDue/Assets.xcassets/AppIcon.appiconset",
|
||||
"sizes": [
|
||||
"1024x1024 (universal)",
|
||||
"1024x1024 (universal)",
|
||||
"1024x1024 (universal)"
|
||||
],
|
||||
"files": [
|
||||
"icon.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
"widget_assets": []
|
||||
}
|
||||
423
docs/ios-parity/colors.json
Normal file
423
docs/ios-parity/colors.json
Normal file
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"themes": {
|
||||
"Default": {
|
||||
"Primary": {
|
||||
"light": "#007AFF",
|
||||
"dark": "#0A84FF"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#5AC8FA",
|
||||
"dark": "#64D2FF"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#FF9500",
|
||||
"dark": "#FF9F0A"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#FF3B30",
|
||||
"dark": "#FF453A"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#1C1C1C"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#F2F7F7",
|
||||
"dark": "#2C2C2C"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#FFFFFF"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#3D3D3D99",
|
||||
"dark": "#EBEBEB99"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Crimson": {
|
||||
"Primary": {
|
||||
"light": "#B51E28",
|
||||
"dark": "#FF827D"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#992E38",
|
||||
"dark": "#FA9994"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#E36100",
|
||||
"dark": "#FFB56B"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#F6EEEC",
|
||||
"dark": "#1B1216"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#DECFCC",
|
||||
"dark": "#412F39"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Desert": {
|
||||
"Primary": {
|
||||
"light": "#B0614A",
|
||||
"dark": "#F2B594"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#9E7D61",
|
||||
"dark": "#EBD1B0"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#D1942E",
|
||||
"dark": "#FFD96B"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#F6F1EB",
|
||||
"dark": "#201C17"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#E6D9C7",
|
||||
"dark": "#4A4138"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Forest": {
|
||||
"Primary": {
|
||||
"light": "#2D5016",
|
||||
"dark": "#94C76B"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#6B8E23",
|
||||
"dark": "#B0D182"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#FFD700",
|
||||
"dark": "#FFD700"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#ECEFE3",
|
||||
"dark": "#191E18"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#C1C9AE",
|
||||
"dark": "#384436"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Lavender": {
|
||||
"Primary": {
|
||||
"light": "#6B418B",
|
||||
"dark": "#D1B0E3"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#8B61B0",
|
||||
"dark": "#DEBFEB"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#E34A82",
|
||||
"dark": "#FF9EC7"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#F2F0F5",
|
||||
"dark": "#18141E"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#D9D1E0",
|
||||
"dark": "#393142"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Midnight": {
|
||||
"Primary": {
|
||||
"light": "#1E4A94",
|
||||
"dark": "#82B5EB"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#2E61B0",
|
||||
"dark": "#94C7F2"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#4A94E3",
|
||||
"dark": "#9ED9FF"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#EEF1F7",
|
||||
"dark": "#121720"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#CCD6E3",
|
||||
"dark": "#303849"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Mint": {
|
||||
"Primary": {
|
||||
"light": "#38B094",
|
||||
"dark": "#94F2D9"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#61C7B0",
|
||||
"dark": "#BFFAEB"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#2E9EB0",
|
||||
"dark": "#6BEBF2"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#EEF6F1",
|
||||
"dark": "#172020"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#D1E3D9",
|
||||
"dark": "#384A4A"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Monochrome": {
|
||||
"Primary": {
|
||||
"light": "#333333",
|
||||
"dark": "#E6E6E6"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#666666",
|
||||
"dark": "#BFBFBF"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#999999",
|
||||
"dark": "#D1D1D1"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#F1F1F1",
|
||||
"dark": "#171717"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#D5D5D5",
|
||||
"dark": "#3C3C3C"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Ocean": {
|
||||
"Primary": {
|
||||
"light": "#006B8F",
|
||||
"dark": "#4AB5D1"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#008B8B",
|
||||
"dark": "#61D1C7"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#FF7F50",
|
||||
"dark": "#FF7F50"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#E5ECF2",
|
||||
"dark": "#171B23"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#BDCBD6",
|
||||
"dark": "#323B4C"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Sunset": {
|
||||
"Primary": {
|
||||
"light": "#FF4500",
|
||||
"dark": "#FF9E61"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#FF6347",
|
||||
"dark": "#FFAD7D"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#FFD700",
|
||||
"dark": "#FFD700"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#F7F1E8",
|
||||
"dark": "#211914"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#DCD0BB",
|
||||
"dark": "#433329"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"Teal": {
|
||||
"Primary": {
|
||||
"light": "#07A0C3",
|
||||
"dark": "#61CCE3"
|
||||
},
|
||||
"Secondary": {
|
||||
"light": "#0055A5",
|
||||
"dark": "#61A6D9"
|
||||
},
|
||||
"Accent": {
|
||||
"light": "#F0C808",
|
||||
"dark": "#F0C808"
|
||||
},
|
||||
"Error": {
|
||||
"light": "#DD1C1A",
|
||||
"dark": "#FF5344"
|
||||
},
|
||||
"BackgroundPrimary": {
|
||||
"light": "#FFF1D0",
|
||||
"dark": "#0A1929"
|
||||
},
|
||||
"BackgroundSecondary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#1A2F3F"
|
||||
},
|
||||
"TextPrimary": {
|
||||
"light": "#111111",
|
||||
"dark": "#F5F5F5"
|
||||
},
|
||||
"TextSecondary": {
|
||||
"light": "#444444",
|
||||
"dark": "#C7C7C7"
|
||||
},
|
||||
"TextOnPrimary": {
|
||||
"light": "#FFFFFF",
|
||||
"dark": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget": {}
|
||||
}
|
||||
409
docs/ios-parity/screens.json
Normal file
409
docs/ios-parity/screens.json
Normal file
@@ -0,0 +1,409 @@
|
||||
{
|
||||
"screens": [
|
||||
{
|
||||
"name": "ForgotPasswordView",
|
||||
"path": "iosApp/iosApp/PasswordReset/ForgotPasswordView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "LoginView",
|
||||
"path": "iosApp/iosApp/Login/LoginView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "RegisterView",
|
||||
"path": "iosApp/iosApp/Register/RegisterView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "ResetPasswordView",
|
||||
"path": "iosApp/iosApp/PasswordReset/ResetPasswordView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "VerifyEmailView",
|
||||
"path": "iosApp/iosApp/VerifyEmail/VerifyEmailView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "VerifyResetCodeView",
|
||||
"path": "iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift",
|
||||
"category": "auth"
|
||||
},
|
||||
{
|
||||
"name": "ContractorDetailView",
|
||||
"path": "iosApp/iosApp/Contractor/ContractorDetailView.swift",
|
||||
"category": "contractor"
|
||||
},
|
||||
{
|
||||
"name": "ContractorsListView",
|
||||
"path": "iosApp/iosApp/Contractor/ContractorsListView.swift",
|
||||
"category": "contractor"
|
||||
},
|
||||
{
|
||||
"name": "AddDocumentView",
|
||||
"path": "iosApp/iosApp/Documents/AddDocumentView.swift",
|
||||
"category": "document"
|
||||
},
|
||||
{
|
||||
"name": "DocumentDetailView",
|
||||
"path": "iosApp/iosApp/Documents/DocumentDetailView.swift",
|
||||
"category": "document"
|
||||
},
|
||||
{
|
||||
"name": "DocumentFormView",
|
||||
"path": "iosApp/iosApp/Documents/DocumentFormView.swift",
|
||||
"category": "document"
|
||||
},
|
||||
{
|
||||
"name": "DocumentsWarrantiesView",
|
||||
"path": "iosApp/iosApp/Documents/DocumentsWarrantiesView.swift",
|
||||
"category": "document"
|
||||
},
|
||||
{
|
||||
"name": "EditDocumentView",
|
||||
"path": "iosApp/iosApp/Documents/EditDocumentView.swift",
|
||||
"category": "document"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingCreateAccountView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingFirstTaskView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingJoinResidenceView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingJoinResidenceView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingNameResidenceView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingSubscriptionView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingSubscriptionView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingValuePropsView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingVerifyEmailView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingVerifyEmailView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingWelcomeView",
|
||||
"path": "iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift",
|
||||
"category": "onboarding"
|
||||
},
|
||||
{
|
||||
"name": "AnimationTestingView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/AnimationTestingView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "FireworkCheckmarkView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/TaskAnimations.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "ImplodeCheckmarkView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/TaskAnimations.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "NotificationPreferencesView",
|
||||
"path": "iosApp/iosApp/Profile/NotificationPreferencesView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "ProfileTabView",
|
||||
"path": "iosApp/iosApp/Profile/ProfileTabView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "ProfileView",
|
||||
"path": "iosApp/iosApp/Profile/ProfileView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "RippleCheckmarkView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/TaskAnimations.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "StarburstCheckmarkView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/TaskAnimations.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "TestColumnView",
|
||||
"path": "iosApp/iosApp/Profile/AnimationTesting/AnimationTestingView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "ThemeSelectionView",
|
||||
"path": "iosApp/iosApp/Profile/ThemeSelectionView.swift",
|
||||
"category": "profile"
|
||||
},
|
||||
{
|
||||
"name": "AddResidenceView",
|
||||
"path": "iosApp/iosApp/AddResidenceView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "EditResidenceView",
|
||||
"path": "iosApp/iosApp/EditResidenceView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "EmptyResidencesView",
|
||||
"path": "iosApp/iosApp/Subviews/Residence/EmptyResidencesView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "JoinResidenceView",
|
||||
"path": "iosApp/iosApp/Residence/JoinResidenceView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "ManageUsersView",
|
||||
"path": "iosApp/iosApp/Residence/ManageUsersView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "ResidenceDetailView",
|
||||
"path": "iosApp/iosApp/Residence/ResidenceDetailView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "ResidenceFormView",
|
||||
"path": "iosApp/iosApp/ResidenceFormView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "ResidencesListView",
|
||||
"path": "iosApp/iosApp/Residence/ResidencesListView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "SummaryStatView",
|
||||
"path": "iosApp/iosApp/Subviews/Residence/SummaryStatView.swift",
|
||||
"category": "residence"
|
||||
},
|
||||
{
|
||||
"name": "AnimatedHoneyDueIconView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "AsyncEmptyStateView",
|
||||
"path": "iosApp/iosApp/Core/AsyncContentView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "DefaultErrorView",
|
||||
"path": "iosApp/iosApp/Core/AsyncContentView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "DefaultLoadingView",
|
||||
"path": "iosApp/iosApp/Core/AsyncContentView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "EmptyStateView",
|
||||
"path": "iosApp/iosApp/Documents/Components/EmptyStateView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "ErrorMessageView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/ErrorMessageView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "ErrorView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/ErrorView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "HoneyDueIconView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "HoneycombSummaryView",
|
||||
"path": "iosApp/iosApp/Shared/Components/HoneycombCompletionGrid.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "ImageThumbnailView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "InlineLoadingView",
|
||||
"path": "iosApp/iosApp/Core/LoadingOverlay.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "MainTabView",
|
||||
"path": "iosApp/iosApp/MainTabView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "RootView",
|
||||
"path": "iosApp/iosApp/RootView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "SkeletonView",
|
||||
"path": "iosApp/iosApp/Core/LoadingOverlay.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "StandardEmptyStateView",
|
||||
"path": "iosApp/iosApp/Shared/Components/SharedEmptyStateView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "StandardLoadingView",
|
||||
"path": "iosApp/iosApp/Shared/Extensions/ViewExtensions.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "StatView",
|
||||
"path": "iosApp/iosApp/Subviews/Common/StatView.swift",
|
||||
"category": "shared"
|
||||
},
|
||||
{
|
||||
"name": "FeatureComparisonView",
|
||||
"path": "iosApp/iosApp/Subscription/FeatureComparisonView.swift",
|
||||
"category": "subscription"
|
||||
},
|
||||
{
|
||||
"name": "PromoContentView",
|
||||
"path": "iosApp/iosApp/Subscription/UpgradePromptView.swift",
|
||||
"category": "subscription"
|
||||
},
|
||||
{
|
||||
"name": "UpgradeFeatureView",
|
||||
"path": "iosApp/iosApp/Subscription/UpgradeFeatureView.swift",
|
||||
"category": "subscription"
|
||||
},
|
||||
{
|
||||
"name": "UpgradePromptView",
|
||||
"path": "iosApp/iosApp/Subscription/UpgradePromptView.swift",
|
||||
"category": "subscription"
|
||||
},
|
||||
{
|
||||
"name": "AddTaskView",
|
||||
"path": "iosApp/iosApp/Task/AddTaskView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "AddTaskWithResidenceView",
|
||||
"path": "iosApp/iosApp/Task/AddTaskWithResidenceView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "AllTasksView",
|
||||
"path": "iosApp/iosApp/Task/AllTasksView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "CompleteTaskView",
|
||||
"path": "iosApp/iosApp/Task/CompleteTaskView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "CompletionCardView",
|
||||
"path": "iosApp/iosApp/Subviews/Task/CompletionCardView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "ContractorPickerView",
|
||||
"path": "iosApp/iosApp/Task/CompleteTaskView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "DynamicTaskColumnView",
|
||||
"path": "iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "EditTaskView",
|
||||
"path": "iosApp/iosApp/Task/EditTaskView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "EmptyTasksView",
|
||||
"path": "iosApp/iosApp/Subviews/Task/EmptyTasksView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "SwipeHintView",
|
||||
"path": "iosApp/iosApp/Subviews/Task/TasksSection.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "TaskFormView",
|
||||
"path": "iosApp/iosApp/Task/TaskFormView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "TaskSuggestionsView",
|
||||
"path": "iosApp/iosApp/Task/TaskSuggestionsView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "TaskTemplatesBrowserView",
|
||||
"path": "iosApp/iosApp/Task/TaskTemplatesBrowserView.swift",
|
||||
"category": "task"
|
||||
},
|
||||
{
|
||||
"name": "FreeWidgetView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "HoneyDueEntryView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "LargeWidgetView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "MediumWidgetView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "OrganicStatsView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "OrganicTaskRowView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
},
|
||||
{
|
||||
"name": "SmallWidgetView",
|
||||
"path": "iosApp/HoneyDue/HoneyDue.swift",
|
||||
"category": "widget"
|
||||
}
|
||||
]
|
||||
}
|
||||
147
scripts/extract_ios_assets.py
Normal file
147
scripts/extract_ios_assets.py
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inventory iOS Asset Catalogs (imagesets + appiconsets) for parity tracking.
|
||||
|
||||
Scans both production asset catalogs:
|
||||
- iosApp/iosApp/Assets.xcassets/
|
||||
- iosApp/HoneyDue/Assets.xcassets/
|
||||
|
||||
Skips build/DerivedData output (PostHog examples etc.).
|
||||
|
||||
Output schema:
|
||||
{
|
||||
"image_sets": [
|
||||
{"name": "outline", "path": "...", "files": ["outline.pdf"], "format": "pdf"},
|
||||
...
|
||||
],
|
||||
"app_icons": [
|
||||
{"name": "AppIcon", "path": "...", "sizes": ["1024x1024", ...]}
|
||||
],
|
||||
"widget_assets": [
|
||||
{ ...same shape as image_sets... }
|
||||
]
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
MAIN_XCASSETS = REPO_ROOT / "iosApp" / "iosApp" / "Assets.xcassets"
|
||||
WIDGET_XCASSETS = REPO_ROOT / "iosApp" / "HoneyDue" / "Assets.xcassets"
|
||||
OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "assets.json"
|
||||
|
||||
_IMAGE_EXTS = {".pdf", ".png", ".jpg", ".jpeg", ".svg", ".heic"}
|
||||
|
||||
|
||||
def infer_format(files: list[str]) -> str:
|
||||
exts = {Path(f).suffix.lower().lstrip(".") for f in files}
|
||||
image_exts = exts & {"pdf", "png", "jpg", "jpeg", "svg", "heic"}
|
||||
if not image_exts:
|
||||
return "unknown"
|
||||
if len(image_exts) == 1:
|
||||
return next(iter(image_exts))
|
||||
return "mixed(" + ",".join(sorted(image_exts)) + ")"
|
||||
|
||||
|
||||
def list_asset_files(dir_path: Path) -> list[str]:
|
||||
out: list[str] = []
|
||||
for entry in sorted(dir_path.iterdir()):
|
||||
if entry.is_file() and entry.suffix.lower() in _IMAGE_EXTS:
|
||||
out.append(entry.name)
|
||||
return out
|
||||
|
||||
|
||||
def describe_imageset(imageset_dir: Path) -> dict[str, Any]:
|
||||
name = imageset_dir.name[: -len(".imageset")]
|
||||
files = list_asset_files(imageset_dir)
|
||||
return {
|
||||
"name": name,
|
||||
"path": str(imageset_dir.relative_to(REPO_ROOT)),
|
||||
"files": files,
|
||||
"format": infer_format(files),
|
||||
}
|
||||
|
||||
|
||||
def describe_appicon(appicon_dir: Path) -> dict[str, Any]:
|
||||
name = appicon_dir.name[: -len(".appiconset")]
|
||||
contents = appicon_dir / "Contents.json"
|
||||
sizes: list[str] = []
|
||||
files = list_asset_files(appicon_dir)
|
||||
if contents.is_file():
|
||||
try:
|
||||
data = json.loads(contents.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
for image in data.get("images", []):
|
||||
size = image.get("size")
|
||||
scale = image.get("scale")
|
||||
idiom = image.get("idiom")
|
||||
if size:
|
||||
label = size
|
||||
if scale:
|
||||
label = f"{label}@{scale}"
|
||||
if idiom:
|
||||
label = f"{label} ({idiom})"
|
||||
sizes.append(label)
|
||||
return {
|
||||
"name": name,
|
||||
"path": str(appicon_dir.relative_to(REPO_ROOT)),
|
||||
"sizes": sizes,
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def walk_catalog(root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
imagesets: list[dict[str, Any]] = []
|
||||
appicons: list[dict[str, Any]] = []
|
||||
if not root.is_dir():
|
||||
return imagesets, appicons
|
||||
for dirpath, dirnames, _ in root.walk() if hasattr(root, "walk") else _walk(root):
|
||||
p = Path(dirpath)
|
||||
if p.name.endswith(".imageset"):
|
||||
imagesets.append(describe_imageset(p))
|
||||
dirnames[:] = [] # don't recurse inside
|
||||
elif p.name.endswith(".appiconset"):
|
||||
appicons.append(describe_appicon(p))
|
||||
dirnames[:] = []
|
||||
imagesets.sort(key=lambda x: x["name"])
|
||||
appicons.sort(key=lambda x: x["name"])
|
||||
return imagesets, appicons
|
||||
|
||||
|
||||
def _walk(root: Path):
|
||||
"""Fallback walker for Python < 3.12 where Path.walk is unavailable."""
|
||||
import os
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
yield dirpath, dirnames, filenames
|
||||
|
||||
|
||||
def main() -> int:
|
||||
main_images, main_icons = walk_catalog(MAIN_XCASSETS)
|
||||
widget_images, widget_icons = walk_catalog(WIDGET_XCASSETS)
|
||||
|
||||
output = {
|
||||
"image_sets": main_images,
|
||||
"app_icons": main_icons + widget_icons,
|
||||
"widget_assets": widget_images,
|
||||
}
|
||||
|
||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with OUT_PATH.open("w", encoding="utf-8") as f:
|
||||
json.dump(output, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"[extract_ios_assets] wrote {OUT_PATH}")
|
||||
print(
|
||||
f" image_sets={len(output['image_sets'])} "
|
||||
f"app_icons={len(output['app_icons'])} "
|
||||
f"widget_assets={len(output['widget_assets'])}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
260
scripts/extract_ios_colors.py
Normal file
260
scripts/extract_ios_colors.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract color values from iOS Asset Catalogs into a machine-readable JSON file.
|
||||
|
||||
Scans two asset catalogs:
|
||||
- iosApp/iosApp/Assets.xcassets/Colors/** (11 themes x 9 colors)
|
||||
- iosApp/HoneyDue/Assets.xcassets/** (widget accent + bg)
|
||||
|
||||
Each color Contents.json defines up to two `colors` entries:
|
||||
- one universal (light-mode default)
|
||||
- one with appearances=[luminosity:dark]
|
||||
|
||||
Component values may be:
|
||||
- float strings "0.000" .. "1.000" -> multiply by 255 and round
|
||||
- hex strings "0xHH" -> parse as int
|
||||
|
||||
sRGB-only. If any display-p3 entry is encountered it is recorded
|
||||
separately (at the top level) so Android implementers can decide how to
|
||||
handle them; values are otherwise passed through as-is.
|
||||
|
||||
Output schema (see plan in rc-android-ios-parity.md):
|
||||
{
|
||||
"displayP3_themes": [ ... optional ... ],
|
||||
"themes": { <ThemeName>: { <ColorName>: { "light": "#RRGGBB", "dark": "#RRGGBB" } } },
|
||||
"widget": { <ColorName>: { "light": "#RRGGBB", "dark": "#RRGGBB" } }
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
COLOR_ROOT = REPO_ROOT / "iosApp" / "iosApp" / "Assets.xcassets" / "Colors"
|
||||
WIDGET_ROOT = REPO_ROOT / "iosApp" / "HoneyDue" / "Assets.xcassets"
|
||||
OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "colors.json"
|
||||
|
||||
EXPECTED_THEME_COUNT = 11
|
||||
EXPECTED_COLORS_PER_THEME = 9
|
||||
EXPECTED_COLOR_NAMES = {
|
||||
"Primary",
|
||||
"Secondary",
|
||||
"Accent",
|
||||
"Error",
|
||||
"BackgroundPrimary",
|
||||
"BackgroundSecondary",
|
||||
"TextPrimary",
|
||||
"TextSecondary",
|
||||
"TextOnPrimary",
|
||||
}
|
||||
|
||||
|
||||
def component_to_int(value: str | float | int) -> int:
|
||||
"""Convert a color component (hex string, float-as-string, or numeric) to 0..255."""
|
||||
if isinstance(value, (int, float)):
|
||||
if 0.0 <= float(value) <= 1.0 and not (isinstance(value, int) and value > 1):
|
||||
return round(float(value) * 255)
|
||||
return int(value)
|
||||
s = str(value).strip()
|
||||
if s.lower().startswith("0x"):
|
||||
return int(s, 16)
|
||||
# float like "0.478" or "1.000"
|
||||
f = float(s)
|
||||
if 0.0 <= f <= 1.0:
|
||||
return round(f * 255)
|
||||
return int(f)
|
||||
|
||||
|
||||
def hex_string(r: int, g: int, b: int, a: float) -> str:
|
||||
if abs(a - 1.0) < 1e-6:
|
||||
return f"#{r:02X}{g:02X}{b:02X}"
|
||||
a_int = round(a * 255) if 0.0 <= a <= 1.0 else int(a)
|
||||
return f"#{r:02X}{g:02X}{b:02X}{a_int:02X}"
|
||||
|
||||
|
||||
def alpha_value(value: Any) -> float:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
s = str(value).strip()
|
||||
if s.lower().startswith("0x"):
|
||||
return int(s, 16) / 255.0
|
||||
return float(s)
|
||||
|
||||
|
||||
def parse_colorset(contents_path: Path) -> tuple[str | None, str | None, str | None]:
|
||||
"""Return (light_hex, dark_hex, color_space) for a .colorset/Contents.json.
|
||||
|
||||
Returns (None, None, None) if the colorset has no color data (e.g. Xcode
|
||||
placeholder `AccentColor` with only idiom but no components).
|
||||
"""
|
||||
with contents_path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
light = None
|
||||
dark = None
|
||||
color_space = None
|
||||
|
||||
for entry in data.get("colors", []):
|
||||
color = entry.get("color")
|
||||
if not color:
|
||||
continue
|
||||
components = color.get("components") or {}
|
||||
if not components:
|
||||
continue
|
||||
color_space = color.get("color-space") or color_space
|
||||
r = component_to_int(components.get("red", 0))
|
||||
g = component_to_int(components.get("green", 0))
|
||||
b = component_to_int(components.get("blue", 0))
|
||||
a = alpha_value(components.get("alpha", 1.0))
|
||||
hex_str = hex_string(r, g, b, a)
|
||||
|
||||
appearances = entry.get("appearances") or []
|
||||
is_dark = any(
|
||||
a.get("appearance") == "luminosity" and a.get("value") == "dark"
|
||||
for a in appearances
|
||||
)
|
||||
if is_dark:
|
||||
dark = hex_str
|
||||
else:
|
||||
light = hex_str
|
||||
|
||||
if light is None and dark is None:
|
||||
return None, None, None
|
||||
# If one variant is missing, mirror it
|
||||
if light is None:
|
||||
light = dark
|
||||
if dark is None:
|
||||
dark = light
|
||||
return light, dark, color_space
|
||||
|
||||
|
||||
def extract_theme_colors() -> tuple[dict[str, dict[str, dict[str, str]]], set[str]]:
|
||||
themes: dict[str, dict[str, dict[str, str]]] = {}
|
||||
display_p3_themes: set[str] = set()
|
||||
|
||||
if not COLOR_ROOT.is_dir():
|
||||
raise SystemExit(f"color root not found: {COLOR_ROOT}")
|
||||
|
||||
for theme_dir in sorted(COLOR_ROOT.iterdir()):
|
||||
if not theme_dir.is_dir():
|
||||
continue
|
||||
theme_name = theme_dir.name
|
||||
theme_colors: dict[str, dict[str, str]] = {}
|
||||
|
||||
for colorset_dir in sorted(theme_dir.iterdir()):
|
||||
if not colorset_dir.is_dir() or not colorset_dir.name.endswith(".colorset"):
|
||||
continue
|
||||
color_name = colorset_dir.name[: -len(".colorset")]
|
||||
contents_path = colorset_dir / "Contents.json"
|
||||
if not contents_path.is_file():
|
||||
continue
|
||||
light, dark, cs = parse_colorset(contents_path)
|
||||
if light is None:
|
||||
continue
|
||||
theme_colors[color_name] = {"light": light, "dark": dark}
|
||||
if cs and "display-p3" in cs.lower():
|
||||
display_p3_themes.add(theme_name)
|
||||
|
||||
if theme_colors:
|
||||
themes[theme_name] = theme_colors
|
||||
|
||||
return themes, display_p3_themes
|
||||
|
||||
|
||||
def extract_widget_colors() -> dict[str, dict[str, str]]:
|
||||
widget: dict[str, dict[str, str]] = {}
|
||||
if not WIDGET_ROOT.is_dir():
|
||||
print(f"[warn] widget asset root missing: {WIDGET_ROOT}", file=sys.stderr)
|
||||
return widget
|
||||
for entry in sorted(WIDGET_ROOT.iterdir()):
|
||||
if not entry.is_dir() or not entry.name.endswith(".colorset"):
|
||||
continue
|
||||
color_name = entry.name[: -len(".colorset")]
|
||||
contents_path = entry / "Contents.json"
|
||||
if not contents_path.is_file():
|
||||
continue
|
||||
light, dark, _ = parse_colorset(contents_path)
|
||||
if light is None:
|
||||
# Asset catalog placeholder with no concrete color; skip.
|
||||
continue
|
||||
widget[color_name] = {"light": light, "dark": dark}
|
||||
return widget
|
||||
|
||||
|
||||
def main() -> int:
|
||||
themes, display_p3 = extract_theme_colors()
|
||||
widget = extract_widget_colors()
|
||||
|
||||
# Validation
|
||||
errors: list[str] = []
|
||||
if len(themes) != EXPECTED_THEME_COUNT:
|
||||
errors.append(
|
||||
f"Expected {EXPECTED_THEME_COUNT} themes, got {len(themes)}: "
|
||||
f"{sorted(themes.keys())}"
|
||||
)
|
||||
for name, colors in sorted(themes.items()):
|
||||
if len(colors) != EXPECTED_COLORS_PER_THEME:
|
||||
errors.append(
|
||||
f"Theme {name!r} has {len(colors)} colors, expected "
|
||||
f"{EXPECTED_COLORS_PER_THEME}: {sorted(colors.keys())}"
|
||||
)
|
||||
missing = EXPECTED_COLOR_NAMES - set(colors.keys())
|
||||
extra = set(colors.keys()) - EXPECTED_COLOR_NAMES
|
||||
if missing:
|
||||
errors.append(f"Theme {name!r} missing colors: {sorted(missing)}")
|
||||
if extra:
|
||||
errors.append(f"Theme {name!r} has unexpected colors: {sorted(extra)}")
|
||||
|
||||
if errors:
|
||||
print("[extract_ios_colors] VALIDATION FAILED:", file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f" - {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
output: dict[str, Any] = {}
|
||||
if display_p3:
|
||||
output["displayP3_themes"] = sorted(display_p3)
|
||||
# Preserve key order: Primary, Secondary, Accent, Error, Backgrounds, Text
|
||||
color_order = [
|
||||
"Primary",
|
||||
"Secondary",
|
||||
"Accent",
|
||||
"Error",
|
||||
"BackgroundPrimary",
|
||||
"BackgroundSecondary",
|
||||
"TextPrimary",
|
||||
"TextSecondary",
|
||||
"TextOnPrimary",
|
||||
]
|
||||
ordered_themes: dict[str, dict[str, dict[str, str]]] = {}
|
||||
# Put Default first if present
|
||||
theme_order = sorted(themes.keys(), key=lambda n: (n != "Default", n))
|
||||
for theme_name in theme_order:
|
||||
ordered_themes[theme_name] = {
|
||||
cname: themes[theme_name][cname]
|
||||
for cname in color_order
|
||||
if cname in themes[theme_name]
|
||||
}
|
||||
output["themes"] = ordered_themes
|
||||
output["widget"] = widget
|
||||
|
||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with OUT_PATH.open("w", encoding="utf-8") as f:
|
||||
json.dump(output, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"[extract_ios_colors] wrote {OUT_PATH}")
|
||||
print(
|
||||
f" themes={len(ordered_themes)} "
|
||||
f"colors/theme={EXPECTED_COLORS_PER_THEME} "
|
||||
f"widget_entries={len(widget)} "
|
||||
f"displayP3_themes={sorted(display_p3) if display_p3 else 'none'}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
139
scripts/extract_ios_screens.py
Normal file
139
scripts/extract_ios_screens.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static inventory of SwiftUI screens in iosApp/iosApp/.
|
||||
|
||||
Finds every `struct <Name>View: View { ... }` or `struct <Name>Screen: View { ... }`
|
||||
declaration across the production iOS source tree (excluding generated/build
|
||||
dirs) and categorises them by path.
|
||||
|
||||
Output schema:
|
||||
{
|
||||
"screens": [
|
||||
{"name": "LoginView", "path": "iosApp/iosApp/Login/LoginView.swift", "category": "auth"},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SOURCE_ROOT = REPO_ROOT / "iosApp" / "iosApp"
|
||||
OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "screens.json"
|
||||
|
||||
# Also scan widget target (HoneyDue/) for completeness.
|
||||
WIDGET_SOURCE_ROOT = REPO_ROOT / "iosApp" / "HoneyDue"
|
||||
|
||||
EXCLUDED_DIR_PARTS = {"build", "DerivedData", ".build", "Pods"}
|
||||
|
||||
STRUCT_RE = re.compile(
|
||||
r"^\s*struct\s+([A-Za-z_][A-Za-z0-9_]*(?:View|Screen))\s*:\s*View\s*\{",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
CATEGORY_RULES: list[tuple[str, str]] = [
|
||||
# path-part substring (case-insensitive) -> category
|
||||
("Login", "auth"),
|
||||
("Register", "auth"),
|
||||
("PasswordReset", "auth"),
|
||||
("VerifyEmail", "auth"),
|
||||
("Onboarding", "onboarding"),
|
||||
("Task", "task"),
|
||||
("Residence", "residence"),
|
||||
("Document", "document"),
|
||||
("Contractor", "contractor"),
|
||||
("Profile", "profile"),
|
||||
("Subscription", "subscription"),
|
||||
("Widget", "widget"), # matches WidgetIconView etc. (HoneyDue/)
|
||||
("HoneyDue", "widget"), # widget target dir
|
||||
("Shared", "shared"),
|
||||
("Core", "shared"),
|
||||
("Subviews", "shared"),
|
||||
("Dev", "dev"),
|
||||
("AnimationTesting", "dev"),
|
||||
]
|
||||
|
||||
|
||||
def category_for(rel_path: Path) -> str:
|
||||
parts_lower = [p.lower() for p in rel_path.parts]
|
||||
for needle, cat in CATEGORY_RULES:
|
||||
if needle.lower() in parts_lower:
|
||||
return cat
|
||||
# filename fallback
|
||||
stem = rel_path.stem.lower()
|
||||
for needle, cat in CATEGORY_RULES:
|
||||
if needle.lower() in stem:
|
||||
return cat
|
||||
return "shared"
|
||||
|
||||
|
||||
def should_skip(path: Path) -> bool:
|
||||
return any(part in EXCLUDED_DIR_PARTS for part in path.parts)
|
||||
|
||||
|
||||
def find_swift_files(root: Path) -> list[Path]:
|
||||
if not root.is_dir():
|
||||
return []
|
||||
out: list[Path] = []
|
||||
for p in root.rglob("*.swift"):
|
||||
if should_skip(p.relative_to(REPO_ROOT)):
|
||||
continue
|
||||
out.append(p)
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def extract_from(path: Path) -> list[dict[str, Any]]:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
return []
|
||||
found: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for m in STRUCT_RE.finditer(text):
|
||||
name = m.group(1)
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
found.append(
|
||||
{
|
||||
"name": name,
|
||||
"path": str(rel),
|
||||
"category": category_for(rel),
|
||||
}
|
||||
)
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
screens: list[dict[str, Any]] = []
|
||||
for swift_file in find_swift_files(SOURCE_ROOT):
|
||||
screens.extend(extract_from(swift_file))
|
||||
for swift_file in find_swift_files(WIDGET_SOURCE_ROOT):
|
||||
screens.extend(extract_from(swift_file))
|
||||
|
||||
# Sort by category then name for stable output.
|
||||
screens.sort(key=lambda s: (s["category"], s["name"], s["path"]))
|
||||
|
||||
output = {"screens": screens}
|
||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with OUT_PATH.open("w", encoding="utf-8") as f:
|
||||
json.dump(output, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"[extract_ios_screens] wrote {OUT_PATH}")
|
||||
print(f" screens={len(screens)}")
|
||||
# category histogram
|
||||
hist: dict[str, int] = {}
|
||||
for s in screens:
|
||||
hist[s["category"]] = hist.get(s["category"], 0) + 1
|
||||
for cat, n in sorted(hist.items(), key=lambda kv: (-kv[1], kv[0])):
|
||||
print(f" {cat}: {n}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user