diff --git a/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/Contents.json b/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/Contents.json new file mode 100644 index 000000000..2a2c492c5 --- /dev/null +++ b/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-24×24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/icon-24\303\22724.pdf" "b/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/icon-24\303\22724.pdf" new file mode 100644 index 000000000..37c293462 Binary files /dev/null and "b/SDDSComponents/Assets.xcassets/textFieldChipIcon.imageset/icon-24\303\22724.pdf" differ diff --git a/SDDSComponents/Package.swift b/SDDSComponents/Package.swift index ddd48e24a..06d108075 100644 --- a/SDDSComponents/Package.swift +++ b/SDDSComponents/Package.swift @@ -31,7 +31,7 @@ let package = Package( ), .target( name: "SDDSComponentsPreview", - dependencies: ["SDDSServTheme", "SDDSThemeCore"], + dependencies: ["SDDSServTheme", "SDDSThemeCore", "SDDSComponents"], path: "SDDSComponentsPreview", exclude: ["SDDSComponentsPreview.h"], resources: [ diff --git a/SDDSComponents/SDDSComponents.xcodeproj/project.pbxproj b/SDDSComponents/SDDSComponents.xcodeproj/project.pbxproj index 47b2031c0..1fbd44f49 100644 --- a/SDDSComponents/SDDSComponents.xcodeproj/project.pbxproj +++ b/SDDSComponents/SDDSComponents.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 8102BA2F2CBE9B3300C589D3 /* SDDSTextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA2A2CBE9B3300C589D3 /* SDDSTextArea.swift */; }; + 8102BA302CBE9B3300C589D3 /* TextAreaAccessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA2B2CBE9B3300C589D3 /* TextAreaAccessibility.swift */; }; + 8102BA312CBE9B3300C589D3 /* TextAreaAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA2C2CBE9B3300C589D3 /* TextAreaAppearance.swift */; }; + 8102BA322CBE9B3300C589D3 /* TextAreaSizeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA2D2CBE9B3300C589D3 /* TextAreaSizeConfiguration.swift */; }; + 8102BA3A2CBEAFB800C589D3 /* PlaceholderTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA392CBEAFB800C589D3 /* PlaceholderTextEditor.swift */; }; + 8102BA3C2CBEB32700C589D3 /* ExpandingTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA3B2CBEB32700C589D3 /* ExpandingTextEditor.swift */; }; 811DE1542C50098D000DD354 /* SDDSChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1532C50098D000DD354 /* SDDSChip.swift */; }; 811DE1562C50179F000DD354 /* ChipAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1552C50179F000DD354 /* ChipAppearance.swift */; }; 811DE1582C5017C3000DD354 /* ProgressBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1572C5017C3000DD354 /* ProgressBarAppearance.swift */; }; @@ -18,7 +24,6 @@ 814E30252C9994AC004601F7 /* SDDSServTheme in Frameworks */ = {isa = PBXBuildFile; productRef = 814E30242C9994AC004601F7 /* SDDSServTheme */; }; 814E30272C99A502004601F7 /* TextFieldChipSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E30262C99A502004601F7 /* TextFieldChipSize.swift */; }; 814E30292C99A58B004601F7 /* ChipAppearance+SDDSTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E30282C99A58B004601F7 /* ChipAppearance+SDDSTextField.swift */; }; - 814E30352C99AF75004601F7 /* SDDSCheckboxSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE5002C99925A00F427DE /* SDDSCheckboxSize.swift */; }; 814E30362C99AFAB004601F7 /* SDDSAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CBC08E2C821A5600FBDAC8 /* SDDSAvatar.swift */; }; 814E30372C99AFAE004601F7 /* SDDSAvatarData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CBC09E2C82334100FBDAC8 /* SDDSAvatarData.swift */; }; 814E30382C99AFB0004601F7 /* SDDSAvatarModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CBC0A22C82368300FBDAC8 /* SDDSAvatarModifiers.swift */; }; @@ -27,20 +32,6 @@ 814E303D2C99B013004601F7 /* SDDSAvatarPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E303C2C99B013004601F7 /* SDDSAvatarPreview.swift */; }; 814E30402C99B067004601F7 /* SDDSAvatarGroupPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E303F2C99B067004601F7 /* SDDSAvatarGroupPreview.swift */; }; 814E30412C99B090004601F7 /* Text+FillModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CBC0972C82273100FBDAC8 /* Text+FillModifier.swift */; }; - 814E30432C99B0E4004601F7 /* SDDSSwitchSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4FA2C99925A00F427DE /* SDDSSwitchSize.swift */; }; - 814E30442C99B10A004601F7 /* SwitchTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4F82C99925A00F427DE /* SwitchTypography.swift */; }; - 814E30462C99B15B004601F7 /* CheckboxTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4FE2C99925A00F427DE /* CheckboxTypography.swift */; }; - 814E30472C99B1AE004601F7 /* RadioboxTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E32C99925900F427DE /* RadioboxTypography.swift */; }; - 814E30482C99B1B5004601F7 /* SDDSRadioboxSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E22C99925900F427DE /* SDDSRadioboxSize.swift */; }; - 814E304A2C99B1F0004601F7 /* DefaultProgressBarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE5022C99925A00F427DE /* DefaultProgressBarSize.swift */; }; - 814E304C2C99B1F9004601F7 /* DefaultChipGroupSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE5072C99925A00F427DE /* DefaultChipGroupSize.swift */; }; - 814E304E2C99B20A004601F7 /* TextFieldAppearance+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E52C99925900F427DE /* TextFieldAppearance+Extensions.swift */; }; - 814E304F2C99B20C004601F7 /* TextFieldTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E82C99925900F427DE /* TextFieldTypography.swift */; }; - 814E30502C99B20E004601F7 /* TextFieldDefaultSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E72C99925900F427DE /* TextFieldDefaultSize.swift */; }; - 814E30512C99B210004601F7 /* TextFieldChipSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E30262C99A502004601F7 /* TextFieldChipSize.swift */; }; - 814E30522C99B212004601F7 /* ChipAppearance+SDDSTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E30282C99A58B004601F7 /* ChipAppearance+SDDSTextField.swift */; }; - 814E30542C99B39E004601F7 /* SDDSTextField+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE4E62C99925900F427DE /* SDDSTextField+Preview.swift */; }; - 814E30552C99B3D0004601F7 /* SDDSChipSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817AE50C2C99925A00F427DE /* SDDSChipSize.swift */; }; 814E30582C99B414004601F7 /* SDDSServTheme in Frameworks */ = {isa = PBXBuildFile; productRef = 814E30572C99B414004601F7 /* SDDSServTheme */; }; 814E307E2C99CEE1004601F7 /* View+DebugModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814E307D2C99CEE1004601F7 /* View+DebugModifiers.swift */; }; 815142892C99942E00E6A00D /* SDDSThemeCore in Frameworks */ = {isa = PBXBuildFile; productRef = 815142882C99942E00E6A00D /* SDDSThemeCore */; }; @@ -121,10 +112,16 @@ 81C01F9A2CA59C9400D7363E /* AvatarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81C01F972CA59C9300D7363E /* AvatarAppearance.swift */; }; 81C01F9B2CA59C9400D7363E /* AvatarTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81C01F982CA59C9400D7363E /* AvatarTypography.swift */; }; 81C01F9C2CA59C9400D7363E /* DefaultAvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81C01F992CA59C9400D7363E /* DefaultAvatarSize.swift */; }; + 81CA2C4B2CC693E6002AF2DF /* ScrollViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CA2C4A2CC693E6002AF2DF /* ScrollViewWrapper.swift */; }; 81CF12192C6E686D0074174F /* SDDSRadioboxGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CF12182C6E686D0074174F /* SDDSRadioboxGroup.swift */; }; 81CF12202C6E74180074174F /* RoundedCornersMask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CF121F2C6E74180074174F /* RoundedCornersMask.swift */; }; 81D2B1912C32B39B00CAA7FD /* SDDSComponents.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D2B1902C32B39B00CAA7FD /* SDDSComponents.h */; settings = {ATTRIBUTES = (Public, ); }; }; 81D2B1992C32B3E400CAA7FD /* SDDSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D2B1982C32B3E400CAA7FD /* SDDSButton.swift */; }; + 81D73C462CC952A900B7025C /* SDDSTextAreaSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D73C452CC952A900B7025C /* SDDSTextAreaSize.swift */; }; + 81D73C482CC952E700B7025C /* SDDSTextAreaAppearance+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D73C472CC952E700B7025C /* SDDSTextAreaAppearance+Extensions.swift */; }; + 81D73C492CC9530C00B7025C /* SDDSTextArea+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8102BA372CBEAC1600C589D3 /* SDDSTextArea+Preview.swift */; }; + 81D73C4A2CC9530F00B7025C /* SDDSTextAreaChipGroupSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CA2C4C2CC69406002AF2DF /* SDDSTextAreaChipGroupSize.swift */; }; + 81D73C4C2CC9547500B7025C /* TextAreaChipSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D73C4B2CC9547500B7025C /* TextAreaChipSize.swift */; }; 81E968142CBD194F00256968 /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E968132CBD194F00256968 /* FocusableTextField.swift */; }; 81E9FA8F2C92B13E0041B5FF /* SDDSTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E9FA8E2C92B13E0041B5FF /* SDDSTextField.swift */; }; /* End PBXBuildFile section */ @@ -137,7 +134,7 @@ remoteGlobalIDString = 81D2B18C2C32B39B00CAA7FD; remoteInfo = SDDSComponents; }; - 81BBC5792C862499009616CE /* PBXContainerItemProxy */ = { + 81CA2C592CC69768002AF2DF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 81D2B1842C32B39B00CAA7FD /* Project object */; proxyType = 1; @@ -147,6 +144,13 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8102BA2A2CBE9B3300C589D3 /* SDDSTextArea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDDSTextArea.swift; sourceTree = ""; }; + 8102BA2B2CBE9B3300C589D3 /* TextAreaAccessibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAreaAccessibility.swift; sourceTree = ""; }; + 8102BA2C2CBE9B3300C589D3 /* TextAreaAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAreaAppearance.swift; sourceTree = ""; }; + 8102BA2D2CBE9B3300C589D3 /* TextAreaSizeConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAreaSizeConfiguration.swift; sourceTree = ""; }; + 8102BA372CBEAC1600C589D3 /* SDDSTextArea+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDDSTextArea+Preview.swift"; sourceTree = ""; }; + 8102BA392CBEAFB800C589D3 /* PlaceholderTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderTextEditor.swift; sourceTree = ""; }; + 8102BA3B2CBEB32700C589D3 /* ExpandingTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingTextEditor.swift; sourceTree = ""; }; 811DE1532C50098D000DD354 /* SDDSChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDSChip.swift; sourceTree = ""; }; 811DE1552C50179F000DD354 /* ChipAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipAppearance.swift; sourceTree = ""; }; 811DE1572C5017C3000DD354 /* ProgressBarAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarAppearance.swift; sourceTree = ""; }; @@ -249,6 +253,8 @@ 81C01F972CA59C9300D7363E /* AvatarAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarAppearance.swift; sourceTree = ""; }; 81C01F982CA59C9400D7363E /* AvatarTypography.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarTypography.swift; sourceTree = ""; }; 81C01F992CA59C9400D7363E /* DefaultAvatarSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAvatarSize.swift; sourceTree = ""; }; + 81CA2C4A2CC693E6002AF2DF /* ScrollViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewWrapper.swift; sourceTree = ""; }; + 81CA2C4C2CC69406002AF2DF /* SDDSTextAreaChipGroupSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDDSTextAreaChipGroupSize.swift; sourceTree = ""; }; 81CBC08E2C821A5600FBDAC8 /* SDDSAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDSAvatar.swift; sourceTree = ""; }; 81CBC0932C82244000FBDAC8 /* BackportAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackportAsyncImage.swift; sourceTree = ""; }; 81CBC0972C82273100FBDAC8 /* Text+FillModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+FillModifier.swift"; sourceTree = ""; }; @@ -265,6 +271,9 @@ 81D2B18D2C32B39B00CAA7FD /* SDDSComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDDSComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81D2B1902C32B39B00CAA7FD /* SDDSComponents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDDSComponents.h; sourceTree = ""; }; 81D2B1982C32B3E400CAA7FD /* SDDSButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDSButton.swift; sourceTree = ""; }; + 81D73C452CC952A900B7025C /* SDDSTextAreaSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDSTextAreaSize.swift; sourceTree = ""; }; + 81D73C472CC952E700B7025C /* SDDSTextAreaAppearance+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDDSTextAreaAppearance+Extensions.swift"; sourceTree = ""; }; + 81D73C4B2CC9547500B7025C /* TextAreaChipSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAreaChipSize.swift; sourceTree = ""; }; 81E968132CBD194F00256968 /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = ""; }; 81E9FA8E2C92B13E0041B5FF /* SDDSTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDSTextField.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -301,6 +310,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 8102BA282CBE9B3300C589D3 /* SDDSTextArea */ = { + isa = PBXGroup; + children = ( + 8102BA2A2CBE9B3300C589D3 /* SDDSTextArea.swift */, + 8102BA2B2CBE9B3300C589D3 /* TextAreaAccessibility.swift */, + 8102BA2C2CBE9B3300C589D3 /* TextAreaAppearance.swift */, + 8102BA2D2CBE9B3300C589D3 /* TextAreaSizeConfiguration.swift */, + ); + path = SDDSTextArea; + sourceTree = ""; + }; 811DE1522C500980000DD354 /* SDDSChip */ = { isa = PBXGroup; children = ( @@ -394,6 +414,7 @@ 817AE4DC2C99925900F427DE /* Components */ = { isa = PBXGroup; children = ( + 81D73C442CC9528300B7025C /* SDDSTextArea */, 814E303E2C99B04C004601F7 /* SDDSAvatarGroup */, 814E303B2C99AFF2004601F7 /* SDDSAvatar */, 817AE4DD2C99925900F427DE /* SDDSCheckboxGroup */, @@ -525,6 +546,7 @@ 818C03AF2C418705002C6D0A /* Components */ = { isa = PBXGroup; children = ( + 8102BA282CBE9B3300C589D3 /* SDDSTextArea */, 81E9FA8D2C92B12C0041B5FF /* SDDSTextField */, 81CBC09B2C82326600FBDAC8 /* SDDSAvatarGroup */, 81CBC08D2C821A4400FBDAC8 /* SDDSAvatar */, @@ -553,6 +575,7 @@ 818C03B32C418C3E002C6D0A /* Common */ = { isa = PBXGroup; children = ( + 81CA2C4A2CC693E6002AF2DF /* ScrollViewWrapper.swift */, 81CBC0922C82241100FBDAC8 /* BackportAsyncImage */, 818C03B42C418C50002C6D0A /* TypographyConfiguration.swift */, 818C03B82C43B99B002C6D0A /* ColorToken+Extensions.swift */, @@ -562,7 +585,9 @@ 81CBC0972C82273100FBDAC8 /* Text+FillModifier.swift */, 8154644A2C96FDE600DAD8EA /* ViewProvider.swift */, 816AA9A62C97280400C3347C /* PlaceholderTextField.swift */, + 8102BA392CBEAFB800C589D3 /* PlaceholderTextEditor.swift */, 814E307D2C99CEE1004601F7 /* View+DebugModifiers.swift */, + 8102BA3B2CBEB32700C589D3 /* ExpandingTextEditor.swift */, ); path = Common; sourceTree = ""; @@ -759,6 +784,18 @@ name = Products; sourceTree = ""; }; + 81D73C442CC9528300B7025C /* SDDSTextArea */ = { + isa = PBXGroup; + children = ( + 8102BA372CBEAC1600C589D3 /* SDDSTextArea+Preview.swift */, + 81CA2C4C2CC69406002AF2DF /* SDDSTextAreaChipGroupSize.swift */, + 81D73C452CC952A900B7025C /* SDDSTextAreaSize.swift */, + 81D73C472CC952E700B7025C /* SDDSTextAreaAppearance+Extensions.swift */, + 81D73C4B2CC9547500B7025C /* TextAreaChipSize.swift */, + ); + path = SDDSTextArea; + sourceTree = ""; + }; 81E968122CBD194300256968 /* FocusableTextField */ = { isa = PBXGroup; children = ( @@ -831,7 +868,7 @@ buildRules = ( ); dependencies = ( - 81BBC57A2C862499009616CE /* PBXTargetDependency */, + 81CA2C5A2CC69768002AF2DF /* PBXTargetDependency */, ); name = SDDSComponentsPreview; packageProductDependencies = ( @@ -960,7 +997,9 @@ 81C01F9B2CA59C9400D7363E /* AvatarTypography.swift in Sources */, 817AE5192C99925A00F427DE /* SDDSButtonPreviewTextWithLeftIconAndSubtitle.swift in Sources */, 817AE5282C99925A00F427DE /* SwitchAppearance+Extensions.swift in Sources */, + 81D73C4C2CC9547500B7025C /* TextAreaChipSize.swift in Sources */, 817AE5162C99925A00F427DE /* TextFieldDefaultSize.swift in Sources */, + 81D73C462CC952A900B7025C /* SDDSTextAreaSize.swift in Sources */, 817AE5252C99925A00F427DE /* SwitchTypography.swift in Sources */, 81C01F9C2CA59C9400D7363E /* DefaultAvatarSize.swift in Sources */, 817AE5202C99925A00F427DE /* SDDSButton+Preview.swift in Sources */, @@ -976,11 +1015,13 @@ 817AE51A2C99925A00F427DE /* SDDSButtonPreviewCircle.swift in Sources */, 817AE5182C99925A00F427DE /* SDDSButtonPreviewTextWithSubtitle.swift in Sources */, 814E30402C99B067004601F7 /* SDDSAvatarGroupPreview.swift in Sources */, + 81D73C4A2CC9530F00B7025C /* SDDSTextAreaChipGroupSize.swift in Sources */, 817AE51C2C99925A00F427DE /* ButtonTypography.swift in Sources */, 817AE5242C99925A00F427DE /* ButtonAppearance.swift in Sources */, 817AE5172C99925A00F427DE /* TextFieldTypography.swift in Sources */, 817AE52C2C99925A00F427DE /* SDDSCheckboxSize.swift in Sources */, 817AE5312C99925A00F427DE /* DefaultChipGroupSize.swift in Sources */, + 81D73C482CC952E700B7025C /* SDDSTextAreaAppearance+Extensions.swift in Sources */, 817AE52A2C99925A00F427DE /* CheckboxTypography.swift in Sources */, 814E30272C99A502004601F7 /* TextFieldChipSize.swift in Sources */, 814E30292C99A58B004601F7 /* ChipAppearance+SDDSTextField.swift in Sources */, @@ -990,6 +1031,7 @@ 817AE51E2C99925A00F427DE /* SDDSButtonPreviewLink.swift in Sources */, 817AE52D2C99925A00F427DE /* DefaultProgressBarSize.swift in Sources */, 817AE5102C99925A00F427DE /* SDDSRadiobox+Preview.swift in Sources */, + 81D73C492CC9530C00B7025C /* SDDSTextArea+Preview.swift in Sources */, 817AE52F2C99925A00F427DE /* ProgressBarAppearance+Extensions.swift in Sources */, 817AE5362C99925A00F427DE /* ChipAppearance+Extensions.swift in Sources */, 817AE52E2C99925A00F427DE /* SDDSProgress+Preview.swift in Sources */, @@ -1010,6 +1052,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8102BA322CBE9B3300C589D3 /* TextAreaSizeConfiguration.swift in Sources */, 818C03E62C47BCDC002C6D0A /* SDDSProgressBar.swift in Sources */, 818C03BF2C43C371002C6D0A /* DebugModifier.swift in Sources */, 814E30392C99AFB3004601F7 /* SDDSAvatarGroup.swift in Sources */, @@ -1021,38 +1064,30 @@ 81E968142CBD194F00256968 /* FocusableTextField.swift in Sources */, 816AA9AC2C97419A00C3347C /* TextFieldDebugConfiguration.swift in Sources */, 818C03E32C479003002C6D0A /* Image+Extensions.swift in Sources */, - 814E30542C99B39E004601F7 /* SDDSTextField+Preview.swift in Sources */, - 814E304C2C99B1F9004601F7 /* DefaultChipGroupSize.swift in Sources */, 816AA9B22C97425F00C3347C /* TextFieldAccessibility.swift in Sources */, - 814E30432C99B0E4004601F7 /* SDDSSwitchSize.swift in Sources */, 814E303A2C99AFBD004601F7 /* BackportAsyncImage.swift in Sources */, 81CF12192C6E686D0074174F /* SDDSRadioboxGroup.swift in Sources */, 814185CA2C34260300D8E524 /* ButtonSize.swift in Sources */, 814E30412C99B090004601F7 /* Text+FillModifier.swift in Sources */, 816C62A22CB80EC400352891 /* Opacity.swift in Sources */, - 814E30442C99B10A004601F7 /* SwitchTypography.swift in Sources */, - 814E30462C99B15B004601F7 /* CheckboxTypography.swift in Sources */, 814E30382C99AFB0004601F7 /* SDDSAvatarModifiers.swift in Sources */, 81D2B1992C32B3E400CAA7FD /* SDDSButton.swift in Sources */, - 814E304E2C99B20A004601F7 /* TextFieldAppearance+Extensions.swift in Sources */, 818FE9352C3C32F100F64958 /* SDDSButton+Extensions.swift in Sources */, 811DE1582C5017C3000DD354 /* ProgressBarAppearance.swift in Sources */, 814E30452C99B155004601F7 /* CheckboxAppearance+Extensions.swift in Sources */, 811DE1562C50179F000DD354 /* ChipAppearance.swift in Sources */, - 814E30512C99B210004601F7 /* TextFieldChipSize.swift in Sources */, 81CF12202C6E74180074174F /* RoundedCornersMask.swift in Sources */, + 8102BA2F2CBE9B3300C589D3 /* SDDSTextArea.swift in Sources */, 818C03C92C451424002C6D0A /* RadioboxAppearance.swift in Sources */, + 8102BA312CBE9B3300C589D3 /* TextAreaAppearance.swift in Sources */, 8159F7302C5D1CFE00622836 /* FillStyle.swift in Sources */, 816AA9B02C97424000C3347C /* TextFieldSizeConfiguration.swift in Sources */, 818C03CE2C4515ED002C6D0A /* SDDSRadiobox.swift in Sources */, 81998FF92C35503D009074B7 /* View+Modifiers.swift in Sources */, 816AA9A72C97280400C3347C /* PlaceholderTextField.swift in Sources */, - 814E30472C99B1AE004601F7 /* RadioboxTypography.swift in Sources */, 811DE1542C50098D000DD354 /* SDDSChip.swift in Sources */, - 814E304F2C99B20C004601F7 /* TextFieldTypography.swift in Sources */, - 814E30352C99AF75004601F7 /* SDDSCheckboxSize.swift in Sources */, + 8102BA3C2CBEB32700C589D3 /* ExpandingTextEditor.swift in Sources */, 818C03B92C43B99B002C6D0A /* ColorToken+Extensions.swift in Sources */, - 814E30522C99B212004601F7 /* ChipAppearance+SDDSTextField.swift in Sources */, 811DE1712C5783B6000DD354 /* HierarchicalList.swift in Sources */, 818C03D02C451651002C6D0A /* SelectionControlAppearance.swift in Sources */, 814E30362C99AFAB004601F7 /* SDDSAvatar.swift in Sources */, @@ -1061,12 +1096,10 @@ 818C03C52C4512A6002C6D0A /* SelectionControl.swift in Sources */, 811DE15B2C5017FE000DD354 /* SDDSChipGroup.swift in Sources */, 81E9FA8F2C92B13E0041B5FF /* SDDSTextField.swift in Sources */, + 8102BA3A2CBEAFB800C589D3 /* PlaceholderTextEditor.swift in Sources */, 818C03C72C45140B002C6D0A /* CheckboxAppearance.swift in Sources */, - 814E30552C99B3D0004601F7 /* SDDSChipSize.swift in Sources */, 814E30372C99AFAE004601F7 /* SDDSAvatarData.swift in Sources */, - 814E30482C99B1B5004601F7 /* SDDSRadioboxSize.swift in Sources */, - 814E30502C99B20E004601F7 /* TextFieldDefaultSize.swift in Sources */, - 814E304A2C99B1F0004601F7 /* DefaultProgressBarSize.swift in Sources */, + 8102BA302CBE9B3300C589D3 /* TextAreaAccessibility.swift in Sources */, 817580E92C37E04000E45207 /* SpinnerView.swift in Sources */, 818C03B52C418C50002C6D0A /* TypographyConfiguration.swift in Sources */, ); @@ -1085,10 +1118,10 @@ isa = PBXTargetDependency; productRef = 819CB7FE2C7CC35E00B4FBF4 /* SDDSThemeCore */; }; - 81BBC57A2C862499009616CE /* PBXTargetDependency */ = { + 81CA2C5A2CC69768002AF2DF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 81D2B18C2C32B39B00CAA7FD /* SDDSComponents */; - targetProxy = 81BBC5792C862499009616CE /* PBXContainerItemProxy */; + targetProxy = 81CA2C592CC69768002AF2DF /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxDark.svg b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxDark.svg new file mode 100644 index 000000000..924c0b508 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxDark.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxOff 1.svg b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxOff 1.svg new file mode 100644 index 000000000..1dc629733 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/CheckBoxOff 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/Contents.json b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/Contents.json index 7ae6f840a..a1f131dc8 100644 --- a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/Contents.json +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/checkboxOff.imageset/Contents.json @@ -3,6 +3,26 @@ { "filename" : "CheckBoxOff.svg", "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "CheckBoxOff 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "CheckBoxDark.svg", + "idiom" : "universal" } ], "info" : { diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/Contents.json b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/Contents.json index fb0a7af63..8fee71d35 100644 --- a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/Contents.json +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/Contents.json @@ -3,6 +3,26 @@ { "filename" : "RadioBoxOff.svg", "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "RadioBoxDark 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "RadioBoxDark.svg", + "idiom" : "universal" } ], "info" : { diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark 1.svg b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark 1.svg new file mode 100644 index 000000000..2faf8e7d2 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark.svg b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark.svg new file mode 100644 index 000000000..2faf8e7d2 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOff.imageset/RadioBoxDark.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/AvatarTypography.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/AvatarTypography.swift index e5e2ecad2..66dfeed0d 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/AvatarTypography.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/AvatarTypography.swift @@ -31,7 +31,7 @@ extension AvatarTypography { extraExtraLarge: Typographies.headerH2Bold.typography, large: Typographies.headerH4Bold.typography, medium: Typographies.headerH5Bold.typography, - small: Typographies.headerH5Bold.typography + small: Typographies.textXsBold.typography ).asContainer } } diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/SDDSAvatarPreview.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/SDDSAvatarPreview.swift index 609316139..5594c9983 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/SDDSAvatarPreview.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSAvatar/SDDSAvatarPreview.swift @@ -114,7 +114,7 @@ struct SDDSAvatarPreview: PreviewProvider { .padding() } - static var defaultAccessibility: AvatarAccessibility { + private static var defaultAccessibility: AvatarAccessibility { AvatarAccessibility(label: "User Avatar", hint: "Displays user status and initials or image") } } diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSButton/SDDSButton+SDDSServeB2CStyle.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSButton/SDDSButton+SDDSServeB2CStyle.swift index 31291cd17..80cbe5041 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSButton/SDDSButton+SDDSServeB2CStyle.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSButton/SDDSButton+SDDSServeB2CStyle.swift @@ -1,8 +1,8 @@ import Foundation import SDDSComponents -public extension SDDSServeB2CStyle { - var defaultButtonAppearance: ButtonAppearance { +extension SDDSServeB2CStyle { + public var defaultButtonAppearance: ButtonAppearance { switch self { case .default: return .default @@ -25,7 +25,7 @@ public extension SDDSServeB2CStyle { } } - var defaultLinkButtonAppearance: ButtonAppearance { + public var defaultLinkButtonAppearance: ButtonAppearance { switch self { case .default: return .linkDefault diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSCheckbox/CheckboxAppearance+Extensions.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSCheckbox/CheckboxAppearance+Extensions.swift index e985a9789..14d7c2aec 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSCheckbox/CheckboxAppearance+Extensions.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSCheckbox/CheckboxAppearance+Extensions.swift @@ -10,8 +10,7 @@ public extension CheckboxAppearance { subtitleTypography: RadioboxTypography.description, titleColor: .backgroundInversePrimary, subtitleColor: .surfaceInverseSolidPrimary.withOpacity(0.56), - disabledAlpha: 0.4, - imageTintColor: .surfaceDefaultAccent + disabledAlpha: 0.4 ) } } diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/DefaultChipGroupSize.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/DefaultChipGroupSize.swift index 0a92b15ea..dccbcc121 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/DefaultChipGroupSize.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/DefaultChipGroupSize.swift @@ -11,9 +11,9 @@ import SDDSComponents - maxColumns: Максимальное количество столбцов в ряду. - alignment: Выравнивание группы чипов. */ -public struct DefaultChipGroupSize: ChipGroupSizeConfiguration { +public struct DefaultChipGroupSize: ChipGroupSizeConfiguration, Hashable { public var debugDescription: String { - String(reflecting: self) + alignment.rawValue } public var insets: EdgeInsets { @@ -29,4 +29,12 @@ public struct DefaultChipGroupSize: ChipGroupSizeConfiguration { public init(alignment: ChipGroupAlignment = .center) { self.alignment = alignment } + + public static var allCases: [DefaultChipGroupSize] { + [.init(alignment: .center), .init(alignment: .left), .init(alignment: .right), .init(alignment: .decreasingLeft), .init(alignment: .decreasingRight)] + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(alignment) + } } diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/SDDSChipGroup+Preview.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/SDDSChipGroup+Preview.swift index db2f208f0..9e7c2f64b 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/SDDSChipGroup+Preview.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSChipGroup/SDDSChipGroup+Preview.swift @@ -8,7 +8,7 @@ struct SDDSChipGroupPreview: PreviewProvider { let chipSize = SDDSChipSize.medium(.pilled) let chipAccessibility = ChipAccessibility() - let chipData = (1...12).map { index in + let chipData = (1...32).map { index in ChipData( title: "Label", isEnabled: true, diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSRadiobox/RadioboxAppearance+Extensions.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSRadiobox/RadioboxAppearance+Extensions.swift index 23e27921b..e259d233c 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSRadiobox/RadioboxAppearance+Extensions.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSRadiobox/RadioboxAppearance+Extensions.swift @@ -10,8 +10,8 @@ public extension RadioboxAppearance { subtitleTypography: RadioboxTypography.description, titleColor: .backgroundInversePrimary, subtitleColor: .surfaceInverseSolidPrimary.withOpacity(0.56), - disabledAlpha: 0.4, - imageTintColor: .surfaceDefaultAccent + disabledAlpha: 0.4, + imageTintColor: nil ) } } diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextArea+Preview.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextArea+Preview.swift new file mode 100644 index 000000000..6ca2498a7 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextArea+Preview.swift @@ -0,0 +1,87 @@ +import Foundation +import SDDSComponents +import SwiftUI +import SDDSServTheme + +// MARK: - Preview + +struct SDDSTextAreaPreview: PreviewProvider { + static var previews: some View { + let chips = (1...12).map { index in + ChipData( + title: "Label", + isEnabled: true, + iconImage: nil, + buttonImage: Image.image("textFieldChipIcon"), + appearance: .textArea, + size: TextAreaChipSize.medium, + accessibility: ChipAccessibility(), + removeAction: {} + ) + } + + return Group { + SDDSTextArea( + value: .constant(.single("")), + title: "Title", + optionalTitle: "optional", + placeholder: "Placeholder", + caption: "caption", + counter: "counter", + disabled: false, + readOnly: false, + style: .default, + labelPlacement: .none, + required: true, + requiredPlacement: .left, + appearance: .defaultAppearance, + size: SDDSTextAreaSize.medium, + chipGroupSize: SDDSTextAreaSize.large.chipGroupSize, + layout: .default, + iconActionViewProvider: ViewProvider(iconActionView) + ) + .previewDisplayName("Outer Label") + .previewLayout(.sizeThatFits) + .padding() + + SDDSTextArea( + value: .constant(.multiple("", chips)), + title: "Title", + optionalTitle: "optional", + placeholder: "Placeholder", + caption: "caption", + counter: "counter", + disabled: false, + style: .default, + labelPlacement: .none, + required: true, + requiredPlacement: .left, + appearance: .defaultAppearance, + size: SDDSTextAreaSize.large, + chipGroupSize: SDDSTextAreaSize.large.chipGroupSize, + layout: .default, + iconActionViewProvider: ViewProvider(iconActionView) + ) + .previewDisplayName("Multiple – Default Label") + .previewLayout(.sizeThatFits) + .padding() + } + } + + @ViewBuilder + private static var iconView: some View { + Image.image("textFieldIcon") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + } + + @ViewBuilder + private static var iconActionView: some View { + Image.image("textFieldIconAction") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + + } +} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaAppearance+Extensions.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaAppearance+Extensions.swift new file mode 100644 index 000000000..5ba379e38 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaAppearance+Extensions.swift @@ -0,0 +1,170 @@ +import Foundation +import SwiftUI +import SDDSThemeCore +import SDDSServTheme +import SDDSComponents + +public extension TextAreaAppearance { + static var defaultAppearance: TextAreaAppearance { + .init( + textTypography: TextAreaTypography.text, + titleTypography: TextAreaTypography.title, + innerTitleTypography: TextAreaTypography.innerTitle, + captionTypography: TextAreaTypography.caption, + counterTypography: TextAreaTypography.counter, + titleColor: .surfaceInverseSolidPrimary.withOpacity(0.96), + optionalTitleColor: .surfaceInverseSolidPrimary.withOpacity(0.28), + textColor: .surfaceInverseSolidPrimary.withOpacity(0.96), + textColorError: .textDefaultNegative, + textColorWarning: .textDefaultWarning, + textColorSuccess: .textDefaultAccent, + disabledAlpha: 0.4, + requiredIndicatorColor: .surfaceOnDarkNegative, + cursorColor: .textDefaultAccent, + focusedBackgroundColor: .surfaceDefaultTransparentSecondary, + lineColor: .surfaceDefaultTransparentTertiary, + focusedLineColor: .textDefaultAccent, + focusedLineColorError: .textDefaultAccent, + focusedLineColorWarning: .textDefaultAccent, + focusedLineColorSuccess: .textDefaultAccent, + borderColorDefault: Color.clear.token, + borderColorError: Color.clear.token, + borderColorWarning: Color.clear.token, + borderColorSuccess: Color.clear.token, + backgroundColorDefault: .surfaceDefaultTransparentPrimary, + backgroundColorError: ColorToken( + darkColor: Color(UIColor(hex: "#FF293E").withAlphaComponent(0.2)), + lightColor: Color(UIColor(hex: "#FF293E").withAlphaComponent(0.12)) + ), + backgroundColorWarning: .surfaceDefaultTransparentWarning.inverted, + backgroundColorSuccess: ColorToken( + darkColor: Color(UIColor(hex: "#1A9E32").withAlphaComponent(0.2)), + lightColor: Color(UIColor(hex: "#1A9E32").withAlphaComponent(0.12)) + ), + captionColorDefault: .surfaceInverseSolidPrimary.withOpacity(0.56), + captionColorError: .textDefaultNegative, + captionColorWarning: .textDefaultWarning, + captionColorSuccess: .textDefaultAccent, + counterColorDefault: .surfaceInverseSolidPrimary.withOpacity(0.96), + placeholderColorDefault: .textDefaultSecondary, + placeholderColorError: .textDefaultNegative, + placeholderColorWarning: .textDefaultWarning, + placeholderColorSuccess: .textDefaultAccent + ) + } +} + +struct TextAreaTypography: GeneralTypographyConfiguration { + let large: TypographyToken? + let medium: TypographyToken? + let small: TypographyToken? + let extraSmall: TypographyToken? + + func typography(with size: TextAreaSizeConfiguration) -> TypographyToken? { + switch size as? SDDSTextAreaSize { + case .large: + return large + case .medium: + return medium + case .small, .none: + return small + case .extraSmall: + return extraSmall + } + } +} + +extension TextAreaTypography { + static var title: TypographyConfiguration { + TextAreaTypography( + large: Typographies.bodyLNormal.typography, + medium: Typographies.bodyMNormal.typography, + small: Typographies.bodySNormal.typography, + extraSmall: Typographies.bodyXsNormal.typography + ).asContainer + } + + static var text: TypographyConfiguration { + TextAreaTypography( + large: Typographies.bodyLNormal.typography, + medium: Typographies.bodyMNormal.typography, + small: Typographies.bodySNormal.typography, + extraSmall: Typographies.bodyXsNormal.typography + ).asContainer + } + + static var innerTitle: TypographyConfiguration { + TextAreaTypography( + large: Typographies.bodyXsNormal.typography, + medium: Typographies.bodyXsNormal.typography, + small: Typographies.bodyXsNormal.typography, + extraSmall: nil + ).asContainer + } + + static var caption: TypographyConfiguration { + TextAreaTypography( + large: Typographies.bodyXsNormal.typography, + medium: Typographies.bodyXsNormal.typography, + small: Typographies.bodyXsNormal.typography, + extraSmall: Typographies.bodyXsNormal.typography + ).asContainer + } + + static var counter: TypographyConfiguration { + TextAreaTypography( + large: Typographies.bodyXsNormal.typography, + medium: Typographies.bodyXsNormal.typography, + small: Typographies.bodyXsNormal.typography, + extraSmall: Typographies.bodyXsNormal.typography + ).asContainer + } +} + +public extension ChipAppearance { + static var textArea: ChipAppearance { + ChipAppearance( + titleColor: .surfaceInverseSolidPrimary.withOpacity(0.96), + titleTypography: ChipTextAreaTypography.text, + imageTintColor: Color.clear.token, + buttonTintColor: Color.clear.token, + backgroundColor: .surfaceDefaultTransparentSecondary, + disabledAlpha: 0.5 + ) + } +} + +struct ChipTextAreaTypography: GeneralTypographyConfiguration { + let large: TypographyToken? + let medium: TypographyToken? + let small: TypographyToken? + let extraSmall: TypographyToken? + + func typography(with size: ChipSizeConfiguration) -> TypographyToken? { + switch size as? SDDSChipSize { + case .large: + return large + case .medium: + return medium + case .small, .none: + return small + case .extraSmall: + return extraSmall + } + } +} + +extension ChipTextAreaTypography { + static var text: TypographyConfiguration { + ChipTextAreaTypography( + large: Typographies.bodyLNormal.typography, + medium: Typographies.bodyMNormal.typography, + small: Typographies.bodySNormal.typography, + extraSmall: Typographies.bodyXsNormal.typography + ).asContainer + } +} + +#Preview { + SDDSTextAreaPreview.previews +} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaChipGroupSize.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaChipGroupSize.swift new file mode 100644 index 000000000..cf446d445 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaChipGroupSize.swift @@ -0,0 +1,40 @@ +import Foundation +import SDDSThemeCore +import SwiftUI +import SDDSComponents + +public enum SDDSTextAreaChipGroupSize: ChipGroupSizeConfiguration { + case large(ChipGroupAlignment) + case medium(ChipGroupAlignment) + case small(ChipGroupAlignment) + case extraSmall(ChipGroupAlignment) + + public var debugDescription: String { + String(reflecting: self) + } + + public var insets: EdgeInsets { + EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 4) + } + + public var maxColumns: Int { + 8 + } + + public var alignment: ChipGroupAlignment { + switch self { + case .large(let value): + return value + case .medium(let value): + return value + case .small(let value): + return value + case .extraSmall(let value): + return value + } + } +} + +#Preview { + SDDSTextAreaPreview.previews +} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaSize.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaSize.swift new file mode 100644 index 000000000..386442d17 --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/SDDSTextAreaSize.swift @@ -0,0 +1,289 @@ +import Foundation +import SwiftUI +import SDDSComponents + +public enum SDDSTextAreaSize: String, TextAreaSizeConfiguration { + case large + case medium + case small + case extraSmall + + public var titleBottomPadding: CGFloat { + switch self { + case .large: + 12 + case .medium: + 10 + case .small: + 8 + case .extraSmall: + 6 + } + } + + public var titleInnerPadding: CGFloat { + switch self { + case .large: + 4 + case .medium: + 4 + case .small: + 2 + case .extraSmall: + 2 + } + } + + public var textBeforeLeadingPadding: CGFloat { + return 0 + } + + public var textBeforeTrailingPadding: CGFloat { + return 2 + } + + public var textAfterLeadingPadding: CGFloat { + return 0 + } + + public var textAfterTrailingPadding: CGFloat { + return 2 + } + + public var fieldHorizontalPadding: CGFloat { + switch self { + case .large: + 16 + case .medium: + 8 + case .small: + 8 + case .extraSmall: + 8 + } + } + + public var captionTopPadding: CGFloat { + 4 + } + + public var captionBottomPadding: CGFloat { + 4 + } + + public var textInputPaddings: EdgeInsets { + switch self { + case .large: + .init(top: 16, leading: 0, bottom: 8, trailing: 0) + case .medium: + .init(top: 14, leading: 0, bottom: 6, trailing: 0) + case .small: + .init(top: 12, leading: 0, bottom: 4, trailing: 0) + case .extraSmall: + .init(top: 8, leading: 0, bottom: 4, trailing: 0) + } + } + + public var textHorizontalPadding: CGFloat { + return 1.0 + } + + public var cornerRadius: CGFloat { + switch self { + case .large: + 14 + case .medium: + 12 + case .small: + 10 + case .extraSmall: + 8 + } + } + + public var borderWidth: CGFloat { + 0 + } + + public var iconActionPadding: CGFloat { + switch self { + case .large: + 10 + case .medium: + 8 + case .small: + 4 + case .extraSmall: + 4 + } + } + + public var indicatorSize: CGSize { + switch self { + case .large: + CGSize(width: 8, height: 8) + case .medium: + CGSize(width: 8, height: 8) + case .small: + CGSize(width: 6, height: 6) + case .extraSmall: + CGSize(width: 6, height: 6) + } + } + + public func indicatorYOffset(labelPlacement: TextAreaLabelPlacement, requiredPlacement: TextAreaRequiredPlacement, layout: TextAreaLayout) -> CGFloat { + switch (self, labelPlacement, requiredPlacement, layout) { + case (.large, .outer, .left, _): + return 7 + case (.medium, .outer, .left, _): + return 6 + case (.small, .outer, .left, _): + return 5 + case (.extraSmall, .outer, .left, _): + return 4 + case (.large, .inner, .right, .clear), (.large, .inner, .left, .clear): + return 24 + case (.medium, .inner, .right, .clear), (.medium, .inner, .left, .clear): + return 23 + case (.small, .inner, .right, .clear), (.small, .inner, .left, .clear): + return 22 + case (.extraSmall, .inner, .right, .clear), (.extraSmall, .inner, .left, .clear): + return 21 + case (.large, .none, .left, .clear), (.large, .none, .right, .clear): + return 24 + case (.medium, .none, .left, .clear), (.medium, .none, .right, .clear): + return 23 + case (.small, .none, .left, .clear), (.small, .none, .right, .clear): + return 22 + case (.extraSmall, .none, .left, .clear), (.extraSmall, .none, .right, .clear): + return 21 + case (.large, .outer, .right, _), (.medium, .outer, .right, _), (.small, .outer, .right, _): + return -2 + case (.extraSmall, .outer, .right, _): + return 0 + default: + break + } + + return 0 + } + + public func indicatorPadding(labelPlacement: TextAreaLabelPlacement, requiredPlacement: TextAreaRequiredPlacement, layout: TextAreaLayout) -> CGFloat { + switch (self, labelPlacement, requiredPlacement, layout) { + case (.large, .outer, .left, _), (.medium, .outer, .left, _): + return 6 + case (.small, .outer, .left, _), (.extraSmall, .outer, .left, _): + return 4 + case (.large, .outer, .right, _), (.medium, .outer, .right, .default), (.small, .outer, .right, _), (.extraSmall, .outer, .right, _): + return 4 + case (.large, .inner, .left, .clear), (.medium, .inner, .left, .clear): + return 6 + case (.small, .inner, .left, .clear), (.extraSmall, .inner, .left, .clear): + return 4 + case (.large, .inner, .right, .clear), (.medium, .inner, .right, .clear), (.small, .inner, .right, .clear), (.extraSmall, .inner, .right, .clear): + return 4 + case (.large, .none, .right, .clear), (.medium, .none, .right, .clear): + return 6 + case (.small, .none, .right, .clear), (.extraSmall, .none, .right, .clear): + return 4 + default: + return 6 + } + } + public func fieldHeight(layout: TextAreaLayout) -> CGFloat { + switch layout { + case .default: + switch self { + case .large: + 130 + case .medium: + 134 + case .small: + 140 + case .extraSmall: + 140 + } + case .clear: + switch self { + case .large: + 56 + case .medium: + 48 + case .small: + 40 + case .extraSmall: + 32 + } + } + } + + public var iconActionSize: CGSize { + switch self { + case .large: + CGSize(width: 24, height: 24) + case .medium: + CGSize(width: 24, height: 24) + case .small: + CGSize(width: 24, height: 24) + case .extraSmall: + CGSize(width: 16, height: 16) + } + } + + public var multipleValueHorizontalPadding: CGFloat { + 2 + } + + public var lineWidth: CGFloat { + 1 + } + + public var chipContainerHorizontalPadding: CGFloat { + 4 + } + + public var chipGroupHeight: CGFloat { + return 82 + } + + public var chipGroupVerticalBottomPadding: CGFloat { + return 8 + } + + public var chipGroupVerticalTopPadding: CGFloat { + return 8 + } + + public var textInputBottomPadding: CGFloat { + return 0 + } + + public var debugDescription: String { + rawValue + } +} + +extension SDDSTextAreaSize { + public var chipGroupSize: SDDSTextAreaChipGroupSize { + switch self { + case .large: + return .large(.left) + case .medium: + return .medium(.left) + case .small: + return .small(.left) + case .extraSmall: + return .extraSmall(.left) + } + } +} + +extension SDDSTextAreaSize: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.rawValue) + } +} + +#Preview { + SDDSTextAreaPreview.previews +} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/TextAreaChipSize.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/TextAreaChipSize.swift new file mode 100644 index 000000000..36b822dcf --- /dev/null +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextArea/TextAreaChipSize.swift @@ -0,0 +1,110 @@ +import Foundation +import SwiftUI +import SDDSComponents + +public enum TextAreaChipSize: String, ChipSizeConfiguration { + case large + case medium + case small + case extraSmall + + public var debugDescription: String { + rawValue + } + + public var iconImageSize: CGSize? { + switch self { + case .large: + return CGSize(width: 24, height: 24) + case .medium: + return CGSize(width: 20, height: 20) + case .small: + return CGSize(width: 16, height: 16) + case .extraSmall: + return CGSize(width: 12, height: 12) + } + } + + public var buttonImageSize: CGSize? { + switch self { + case .large: + return CGSize(width: 24, height: 24) + case .medium: + return CGSize(width: 20, height: 20) + case .small: + return CGSize(width: 16, height: 16) + case .extraSmall: + return CGSize(width: 12, height: 12) + } + } + + public var leadingInset: CGFloat { + switch self { + case .large: + return 16 + case .medium: + return 14 + case .small: + return 12 + case .extraSmall: + return 10 + } + } + + public var trailingInset: CGFloat { + switch self { + case .large: + return 16 + case .medium: + return 14 + case .small: + return 12 + case .extraSmall: + return 10 + } + } + + public var spacing: CGFloat { + switch self { + case .large: + return 6 + case .medium: + return 4 + case .small: + return 2 + case .extraSmall: + return 2 + } + } + + public var height: CGFloat { + switch self { + case .large: + return 44 + case .medium: + return 40 + case .small: + return 32 + case .extraSmall: + return 24 + } + } + + public var borderStyle: ChipBorderStyle { + switch self { + case .large: + return .default(8) + case .medium: + return .default(6) + case .small: + return .default(4) + case .extraSmall: + return .default(2) + } + } + +} + +#Preview { + SDDSTextAreaPreview.previews +} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/ChipAppearance+SDDSTextField.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/ChipAppearance+SDDSTextField.swift index 5dacb86fe..4db164bb4 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/ChipAppearance+SDDSTextField.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/ChipAppearance+SDDSTextField.swift @@ -8,8 +8,8 @@ public extension ChipAppearance { ChipAppearance( titleColor: .surfaceInverseSolidPrimary.withOpacity(0.96), titleTypography: ChipTextFieldTypography.text, - imageTintColor: Color.clear.token, - buttonTintColor: Color.clear.token, + imageTintColor: ColorToken.textDefaultPrimary, + buttonTintColor: ColorToken.textDefaultPrimary, backgroundColor: .surfaceDefaultTransparentSecondary, disabledAlpha: 0.5 ) @@ -46,7 +46,3 @@ extension ChipTextFieldTypography { ).asContainer } } - -#Preview { - SDDSTextFieldPreview.previews -} diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/SDDSTextField+Preview.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/SDDSTextField+Preview.swift index d09f8befd..0fa9756a3 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/SDDSTextField+Preview.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/SDDSTextField+Preview.swift @@ -29,12 +29,12 @@ struct SDDSTextFieldPreview: PreviewProvider { optionalTitle: "optional", placeholder: "Placeholder", caption: "caption", - textBefore: "TB", - textAfter: "TA", + textBefore: "", + textAfter: "", disabled: false, - readOnly: false, + readOnly: true, style: .default, - labelPlacement: .inner, + labelPlacement: .none, required: false, requiredPlacement: .right, appearance: .defaultAppearance, diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldAppearance+Extensions.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldAppearance+Extensions.swift index b0e788ca4..c729123ed 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldAppearance+Extensions.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldAppearance+Extensions.swift @@ -34,6 +34,7 @@ public extension TextFieldAppearance { borderColorWarning: Color.clear.token, borderColorSuccess: Color.clear.token, backgroundColorDefault: .surfaceDefaultTransparentPrimary, + backgroundColorReadOnly: .surfaceDefaultTransparentPrimary, backgroundColorError: ColorToken( darkColor: Color(UIColor(hex: "#FF293E").withAlphaComponent(0.2)), lightColor: Color(UIColor(hex: "#FF293E").withAlphaComponent(0.12)) diff --git a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldDefaultSize.swift b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldDefaultSize.swift index d4b2d00fc..c15e82f8e 100644 --- a/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldDefaultSize.swift +++ b/SDDSComponents/SDDSComponentsPreview/Components/SDDSTextField/TextFieldDefaultSize.swift @@ -254,7 +254,3 @@ public enum SDDSTextFieldSize: String, TextFieldSizeConfiguration { String(reflecting: self) } } - -#Preview { - SDDSTextFieldPreview.previews -} diff --git a/SDDSComponents/Sources/SDDSComponents/Common/ColorToken+Extensions.swift b/SDDSComponents/Sources/SDDSComponents/Common/ColorToken+Extensions.swift index 710c070ac..d51a2cf23 100644 --- a/SDDSComponents/Sources/SDDSComponents/Common/ColorToken+Extensions.swift +++ b/SDDSComponents/Sources/SDDSComponents/Common/ColorToken+Extensions.swift @@ -63,7 +63,7 @@ public extension TypographyToken { fontName: "SF Pro", weight: .semibold, style: .normal, - size: 18, + size: 15, lineHeight: 22, kerning: 0 ) diff --git a/SDDSComponents/Sources/SDDSComponents/Common/ExpandingTextEditor.swift b/SDDSComponents/Sources/SDDSComponents/Common/ExpandingTextEditor.swift new file mode 100644 index 000000000..78ad9da62 --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Common/ExpandingTextEditor.swift @@ -0,0 +1,162 @@ +import SwiftUI +import SDDSThemeCore + +struct ExpandingTextEditor: UIViewRepresentable { + @Binding var text: String + @Binding var textHeight: CGFloat + @Binding var isFocused: Bool + let readOnly: Bool + let typographyToken: TypographyToken + let accentColor: Color + let textColor: Color + let textAlignment: TextAlignment + let paddingInsets: EdgeInsets + let showsVerticalScrollIndicator: Bool + let dynamicHeight: Bool + let onChange: (_ newText: String) -> () + + init(text: Binding, + textHeight: Binding, + isFocused: Binding, + readOnly: Bool, + typographyToken: TypographyToken, + accentColor: Color = .blue, + textColor: Color = .black, + textAlignment: TextAlignment = .leading, + paddingInsets: EdgeInsets = .init(), + trailingContentPadding: CGFloat = 0, + showsVerticalScrollIndicator: Bool = true, + dynamicHeight: Bool = true, + onChange: @escaping (_ newText: String) -> () + ) { + _text = text + _textHeight = textHeight + _isFocused = isFocused + self.readOnly = readOnly + self.typographyToken = typographyToken + self.accentColor = accentColor + self.textColor = textColor + self.textAlignment = textAlignment + self.paddingInsets = paddingInsets + self.showsVerticalScrollIndicator = showsVerticalScrollIndicator + self.dynamicHeight = dynamicHeight + self.onChange = onChange + } + + func makeUIView(context: Context) -> UIView { + let containerView = UIView() + containerView.backgroundColor = .clear + + let textView = UITextView() + textView.delegate = context.coordinator + textView.isScrollEnabled = !dynamicHeight + textView.backgroundColor = .clear + textView.textContainer.lineBreakMode = .byCharWrapping + textView.textContainer.lineFragmentPadding = 0 + textView.textContainer.maximumNumberOfLines = 0 + textView.autocorrectionType = .no + textView.translatesAutoresizingMaskIntoConstraints = false + updateTextViewProperties(textView: textView) + + containerView.addSubview(textView) + + NSLayoutConstraint.activate([ + textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + textView.topAnchor.constraint(equalTo: containerView.topAnchor), + textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + return containerView + } + + func updateUIView(_ uiView: UIView, context: Context) { + guard let textView = uiView.subviews.first as? UITextView else { + return + } + + updateTextViewProperties(textView: textView) + + if isFocused { + textView.becomeFirstResponder() + } else { + textView.resignFirstResponder() + } + + DispatchQueue.main.async { + textView.text = text + + self.updateHeight(textView) + } + } + + private func updateHeight(_ textView: UITextView) { + guard dynamicHeight else { + return + } + + var fixedWidth = textView.bounds.size.width + let size = CGSize(width: fixedWidth, height: .greatestFiniteMagnitude) + let estimatedSize = textView.sizeThatFits(size) + + let newHeight = max(typographyToken.lineHeight, estimatedSize.height) + if textHeight != newHeight { + textHeight = newHeight + } + } + + private func updateTextViewProperties(textView: UITextView) { + textView.textContainerInset = UIEdgeInsets( + top: paddingInsets.top, + left: paddingInsets.leading, + bottom: paddingInsets.bottom, + right: paddingInsets.trailing + ) + textView.textAlignment = textAlignment.nsTextAlignment + textView.font = typographyToken.uiFont + textView.textColor = UIColor(textColor) + textView.tintColor = UIColor(accentColor) + textView.isEditable = !readOnly + textView.showsVerticalScrollIndicator = showsVerticalScrollIndicator + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UITextViewDelegate { + var parent: ExpandingTextEditor + + init(_ parent: ExpandingTextEditor) { + self.parent = parent + } + + func textViewDidChange(_ textView: UITextView) { + parent.text = textView.text + parent.updateHeight(textView) + + parent.onChange(textView.text) + } + + func textViewDidBeginEditing(_ textView: UITextView) { + parent.isFocused = true + } + + func textViewDidEndEditing(_ textView: UITextView) { + parent.isFocused = false + } + } +} + +private extension TextAlignment { + var nsTextAlignment: NSTextAlignment { + switch self { + case .center: + return .center + case .leading: + return .left + case .trailing: + return .right + } + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextEditor.swift b/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextEditor.swift new file mode 100644 index 000000000..c769cafb4 --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextEditor.swift @@ -0,0 +1,44 @@ +import Foundation +import SwiftUI +import SDDSThemeCore +import SDDSComponents + +struct PlaceholderTextEditor: View { + @Binding var text: String + @Binding var textHeight: CGFloat + @Binding var isFocused: Bool + let readOnly: Bool + @ViewBuilder var placeholderContent: () -> PlaceholderContent + let textTypography: TypographyToken + let appearance: TextAreaAppearance + let showsVerticalScrollIndicator: Bool + let trailingContentPadding: CGFloat + let dynamicHeight: Bool + let textColor: Color + let colorScheme: ColorScheme + let onChange: (_ newText: String) -> () + + var body: some View { + ZStack(alignment: .topLeading) { + if text.isEmpty { + placeholderContent() + } + ExpandingTextEditor( + text: $text, + textHeight: $textHeight, + isFocused: $isFocused, + readOnly: readOnly, + typographyToken: textTypography, + accentColor: appearance.cursorColor.color(for: colorScheme), + textColor: textColor, + textAlignment: appearance.inputTextAlignment, + paddingInsets: .init(top: 0, leading: 0, bottom: 0, trailing: trailingContentPadding), + showsVerticalScrollIndicator: showsVerticalScrollIndicator, + dynamicHeight: dynamicHeight, + onChange: onChange + ) + .frame(maxWidth: .infinity) + } + .applyIf(dynamicHeight) { $0.frame(height: textHeight) } + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextField.swift b/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextField.swift index 0aa574873..d0b7c7b11 100644 --- a/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextField.swift +++ b/SDDSComponents/Sources/SDDSComponents/Common/PlaceholderTextField.swift @@ -2,13 +2,16 @@ import Foundation import SwiftUI import SDDSThemeCore -struct PlaceholderTextField: View { +struct PlaceholderTextField: View { @Binding var text: String @Binding var isFocused: Bool - let placeholder: String - let placeholderColor: Color - let placeholderTypography: TypographyToken + let textColor: Color + let textAlignment: TextAlignment + let cursorColor: Color + let textTypography: TypographyToken + let readOnly: Bool @ViewBuilder var placeholderBeforeContent: () -> PlaceholderBeforeContent + @ViewBuilder var placeholderContent: () -> PlaceholderContent @ViewBuilder var placeholderAfterContent: () -> PlaceholderAfterContent let onEditingChanged: ((Bool) -> Void) @@ -19,15 +22,23 @@ struct PlaceholderTextField: UIViewRepresentable { + @Binding var contentOffset: CGPoint + @Binding var scrollViewHeight: CGFloat + @Binding var visibleHeight: CGFloat + + let content: () -> Content + + public init( + contentOffset: Binding = .constant(.zero), + scrollViewHeight: Binding = .constant(0), + visibleHeight: Binding = .constant(0), + @ViewBuilder _ content: @escaping () -> Content) { + self._contentOffset = contentOffset + self._scrollViewHeight = scrollViewHeight + self._visibleHeight = visibleHeight + + self.content = content + } + + public func makeUIView(context: UIViewRepresentableContext) -> UIScrollView { + let view = UIScrollView() + view.delegate = context.coordinator + + let controller = UIHostingController(rootView: content()) + controller.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(controller.view) + + NSLayoutConstraint.activate([ + controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + controller.view.topAnchor.constraint(equalTo: view.topAnchor), + controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + controller.view.widthAnchor.constraint(equalTo: view.widthAnchor) + ]) + + return view + } + + public func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext) { + uiView.contentOffset = self.contentOffset + + DispatchQueue.main.async { + self.scrollViewHeight = uiView.contentSize.height + self.visibleHeight = uiView.frame.size.height + + if let hostedView = uiView.subviews.first { + hostedView.frame = CGRect(origin: .zero, size: uiView.contentSize) + } + } + } + + public func makeCoordinator() -> Coordinator { + Coordinator(contentOffset: self._contentOffset, scrollViewHeight: self._scrollViewHeight) + } + + public class Coordinator: NSObject, UIScrollViewDelegate { + let contentOffset: Binding + let scrollViewHeight: Binding + + init(contentOffset: Binding, scrollViewHeight: Binding) { + self.contentOffset = contentOffset + self.scrollViewHeight = scrollViewHeight + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + contentOffset.wrappedValue = scrollView.contentOffset + } + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckbox/SDDSCheckbox.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckbox/SDDSCheckbox.swift index ff9b999e3..78cdccf87 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckbox/SDDSCheckbox.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckbox/SDDSCheckbox.swift @@ -47,10 +47,11 @@ public struct CheckboxData: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(id.uuidString) + hasher.combine(state.wrappedValue.rawValue) } public static func == (lhs: CheckboxData, rhs: CheckboxData) -> Bool { - lhs.id == rhs.id + lhs.id == rhs.id && lhs.state.wrappedValue == rhs.state.wrappedValue } } diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckboxGroup/SDDSCheckboxGroup.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckboxGroup/SDDSCheckboxGroup.swift index ce790a9f9..ff3757cc7 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckboxGroup/SDDSCheckboxGroup.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSCheckboxGroup/SDDSCheckboxGroup.swift @@ -1,6 +1,9 @@ import Foundation import SwiftUI +public typealias CheckboxGroupOnChange = ((_ index: Int, _ state: SelectionControlState) -> ()) +public typealias CheckboxGroupOnParentChange = ((_ state: SelectionControlState) -> ()) + /** `CheckboxGroupBehaviour` определяет поведение группы чекбоксов. @@ -14,8 +17,8 @@ import SwiftUI - data: Массив данных для инициализации группы чекбоксов. */ public enum CheckboxGroupBehaviour { - case hierarchical(parent: CheckboxData, child: [CheckboxData]) - case `default`(data: [CheckboxData]) + case hierarchical(parent: CheckboxData, child: [CheckboxData], onChildChange: CheckboxGroupOnChange? = nil, onParentChange: CheckboxGroupOnParentChange? = nil) + case `default`(data: [CheckboxData], onChange: CheckboxGroupOnChange? = nil) } /** @@ -47,10 +50,10 @@ public struct SDDSCheckboxGroup: View { self.behaviour = behaviour switch behaviour { - case .hierarchical(let parent, let children): + case .hierarchical(let parent, let children, _, _): self._parentState = State(initialValue: parent.state.wrappedValue) self._childStates = State(initialValue: children.map { $0.state.wrappedValue }) - case .default(let data): + case .default(let data, _): self._parentState = State(initialValue: nil) self._childStates = State(initialValue: data.map { $0.state.wrappedValue }) } @@ -61,9 +64,15 @@ public struct SDDSCheckboxGroup: View { public var body: some View { VStack(spacing: size.verticalSpacing) { switch behaviour { - case .hierarchical(let parent, let children): + case .hierarchical(let parent, let children, let onChildChange, let onParentChange): SDDSCheckbox( - state: Binding(get: { self.parentState ?? .deselected }, set: { self.updateParentState($0) }), + state: Binding( + get: { self.parentState ?? .deselected }, + set: { + self.updateParentState($0) + onParentChange?($0) + } + ), title: parent.title, subtitle: parent.subtitle, isEnabled: parent.isEnabled, @@ -74,7 +83,13 @@ public struct SDDSCheckboxGroup: View { ) ForEach(Array(children.enumerated()), id: \.offset) { index, child in SDDSCheckbox( - state: Binding(get: { self.childStates[index] }, set: { self.updateChildState($0, at: index) }), + state: Binding( + get: { self.childStates[index] }, + set: { + self.updateChildState($0, at: index) + onChildChange?(index, $0) + } + ), title: child.title, subtitle: child.subtitle, isEnabled: child.isEnabled, @@ -85,10 +100,16 @@ public struct SDDSCheckboxGroup: View { ) .padding(.leading, size.horizontalIndent) } - case .default(let data): + case .default(let data, let onChange): ForEach(Array(data.enumerated()), id: \.offset) { index, item in SDDSCheckbox( - state: Binding(get: { self.childStates[index] }, set: { self.childStates[index] = $0 }), + state: Binding( + get: { self.childStates[index] }, + set: { + self.childStates[index] = $0 + onChange?(index, $0) + } + ), title: item.title, subtitle: item.subtitle, isEnabled: item.isEnabled, @@ -106,7 +127,7 @@ public struct SDDSCheckboxGroup: View { private func updateParentState(_ newState: SelectionControlState) { parentState = newState childStates = Array(repeating: newState, count: childStates.count) - if case .hierarchical(_, let children) = behaviour { + if case .hierarchical(_, let children, _, _) = behaviour { for (index, _) in children.enumerated() { children[index].state.wrappedValue = newState } @@ -115,7 +136,7 @@ public struct SDDSCheckboxGroup: View { private func updateChildState(_ newState: SelectionControlState, at index: Int) { childStates[index] = newState - if case .hierarchical(let parent, let children) = behaviour { + if case .hierarchical(let parent, let children, _, _) = behaviour { children[index].state.wrappedValue = newState if childStates.allSatisfy({ $0 == .selected }) { parentState = .selected diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSChip/SDDSChip.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSChip/SDDSChip.swift index 6e858eb2a..7f89cef27 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSChip/SDDSChip.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSChip/SDDSChip.swift @@ -136,7 +136,7 @@ public struct SDDSChip: View { } public var body: some View { - HStack(spacing: size.spacing) { + HStack(spacing: 0) { Spacer() .frame(width: size.leadingInset) if let iconImage = iconImage, let iconImageSize = size.iconImageSize { @@ -147,16 +147,21 @@ public struct SDDSChip: View { .frame(width: iconImageSize.width, height: iconImageSize.height) .foregroundColor(appearance.imageTintColor.color(for: colorScheme)) .accessibilityHidden(true) + Spacer() + .frame(width: size.spacing) } Text(title) .lineLimit(1) .typography(appearance.titleTypography.typography(with: size) ?? .undefined) + .frame(width: textWidth) .foregroundColor(appearance.titleColor.color(for: colorScheme)) .accessibilityLabel(Text(accessibility.titleLabel)) .accessibilityValue(Text(title)) if let buttonImageSize = size.buttonImageSize, let buttonImage = buttonImage { + Spacer() + .frame(width: size.spacing) Button(action: handleRemove) { buttonImage .resizable() @@ -182,6 +187,13 @@ public struct SDDSChip: View { .accessibilityElement(children: .combine) } + private var textWidth: CGFloat { + let titleTypography = appearance.titleTypography.typography(with: size) ?? .undefined + let textWidth = title.size(withAttributes: [.font: titleTypography.uiFont]).width + + return textWidth + } + private var borderRadius: CGFloat { switch size.borderStyle { case .default(let cornerRadius): diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSChipGroup/SDDSChipGroup.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSChipGroup/SDDSChipGroup.swift index 23bbc6dcb..5f543a7e3 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSChipGroup/SDDSChipGroup.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSChipGroup/SDDSChipGroup.swift @@ -16,7 +16,7 @@ import SwiftUI - removeAction: Действие при нажатии на кнопку удаления. */ public struct ChipData: Hashable { - let id = UUID() + public let id: UUID public let title: String public let isEnabled: Bool public let iconImage: Image? @@ -26,7 +26,8 @@ public struct ChipData: Hashable { public let accessibility: ChipAccessibility public let removeAction: () -> Void - public init(title: String, isEnabled: Bool, iconImage: Image?, buttonImage: Image?, appearance: ChipAppearance, size: ChipSizeConfiguration, accessibility: ChipAccessibility, removeAction: @escaping () -> Void) { + public init(id: UUID = UUID(), title: String, isEnabled: Bool, iconImage: Image?, buttonImage: Image?, appearance: ChipAppearance, size: ChipSizeConfiguration, accessibility: ChipAccessibility, removeAction: @escaping () -> Void) { + self.id = id self.title = title self.isEnabled = isEnabled self.iconImage = iconImage @@ -56,7 +57,7 @@ public struct ChipData: Hashable { - decreasingLeft: Убывающее количество элементов, выравненных по левому краю. - decreasingRight: Убывающее количество элементов, выравненных по правому краю. */ -public enum ChipGroupAlignment { +public enum ChipGroupAlignment: String { case left case right case center @@ -85,38 +86,51 @@ public protocol ChipGroupSizeConfiguration: SizeConfiguration, CustomDebugString - data: Массив данных для чипов. - size: Конфигурация размеров для группы чипов. */ + public struct SDDSChipGroup: View { let data: [ChipData] let size: ChipGroupSizeConfiguration - - public init(data: [ChipData], size: ChipGroupSizeConfiguration) { + @Binding var height: CGFloat + + public init(data: [ChipData], size: ChipGroupSizeConfiguration, height: Binding = .constant(0)) { self.data = data self.size = size + _height = height } - + public var body: some View { GeometryReader { geometry in let maxWidth = geometry.size.width - size.insets.leading - size.insets.trailing VStack(spacing: size.insets.top) { - ForEach(layoutRows(maxWidth: maxWidth), id: \.self) { row in - HStack(spacing: size.insets.leading) { - if size.alignment == .right || size.alignment == .decreasingRight { + ForEach(layoutRows(maxWidth: maxWidth, data: data), id: \.self) { row in + HStack(spacing: 0) { + if size.alignment == .decreasingRight { Spacer() } ForEach(row, id: \.self) { chipData in SDDSChip(data: chipData) + .padding(.trailing, size.insets.trailing) } - if size.alignment == .left || size.alignment == .decreasingLeft { + if size.alignment == .decreasingLeft { Spacer() } } .frame(maxWidth: .infinity, alignment: alignment) } } - .padding(size.insets) + .onAppear { + self.height = calculateTotalHeight(maxWidth: maxWidth, data: data) + } + .onChange(of: data) { value in + self.height = calculateTotalHeight(maxWidth: maxWidth, data: value) + } } + .padding(.leading, size.insets.leading) + .padding(.top, size.insets.top) + .padding(.bottom, size.insets.bottom) + .frame(height: height) } - + private var alignment: SwiftUI.Alignment { switch size.alignment { case .left, .decreasingLeft: @@ -127,8 +141,8 @@ public struct SDDSChipGroup: View { return .center } } - - private func layoutRows(maxWidth: CGFloat) -> [[ChipData]] { + + private func layoutRows(maxWidth: CGFloat, data: [ChipData]) -> [[ChipData]] { var rows: [[ChipData]] = [] var currentRow: [ChipData] = [] var currentRowWidth: CGFloat = 0 @@ -154,17 +168,40 @@ public struct SDDSChipGroup: View { } private func calculateChipWidth(for chipData: ChipData) -> CGFloat { - let titleTypography = chipData.appearance.titleTypography.typography(with: chipData.size) ?? .undefined - let textWidth = chipData.title.size(withAttributes: [.font: titleTypography.uiFont]).width - let iconWidth: CGFloat = chipData.size.iconImageSize?.width ?? 0 - let buttonWidth: CGFloat = chipData.size.buttonImageSize?.width ?? 0 - var totalWidth = textWidth + iconWidth + buttonWidth + chipData.size.leadingInset + chipData.size.trailingInset + 2 * chipData.size.spacing - if iconWidth > 0 { + var totalWidth = CGFloat(0) + totalWidth += chipData.size.leadingInset + if let _ = chipData.iconImage, let iconImageSize = chipData.size.iconImageSize { + totalWidth += iconImageSize.width totalWidth += chipData.size.spacing } - if buttonWidth > 0 { + + let titleTypography = chipData.appearance.titleTypography.typography(with: chipData.size) ?? .undefined + let textWidth = chipData.title.size(withAttributes: [.font: titleTypography.uiFont]).width + totalWidth += textWidth + + if let _ = chipData.buttonImage, let buttomImageSize = chipData.size.buttonImageSize { + totalWidth += buttomImageSize.width totalWidth += chipData.size.spacing } + totalWidth += chipData.size.trailingInset + return totalWidth } + + private func calculateTotalHeight(maxWidth: CGFloat, data: [ChipData]) -> CGFloat { + let rows = layoutRows(maxWidth: maxWidth, data: data) + let rowHeight = chipRowHeight(data: data) + var result = CGFloat(rows.count) * rowHeight + result += CGFloat(rows.count - 1) * size.insets.top + result += (size.insets.bottom + size.insets.top) + + return result + } + + private func chipRowHeight(data: [ChipData]) -> CGFloat { + guard let chipData = data.first else { + return 0 + } + return chipData.size.height + } } diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSRadiobox/SDDSRadiobox.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSRadiobox/SDDSRadiobox.swift index fb685959d..5eeb2df0b 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSRadiobox/SDDSRadiobox.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSRadiobox/SDDSRadiobox.swift @@ -6,7 +6,7 @@ public struct RadioboxImages { public let selectedImage: Image public let deselectedImage: Image - init(selectedImage: Image, deselectedImage: Image) { + public init(selectedImage: Image, deselectedImage: Image) { self.selectedImage = selectedImage self.deselectedImage = deselectedImage } diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/SDDSTextArea.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/SDDSTextArea.swift new file mode 100644 index 000000000..671cfd11e --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/SDDSTextArea.swift @@ -0,0 +1,817 @@ +import SwiftUI +import SDDSThemeCore +import SDDSComponents + +// MARK: - Enums and Configurations + +/// Определяет возможные стили для текстового поля. +public enum TextAreaStyle: String, CaseIterable { + case `default` + case error + case warning + case success +} + +/// Определяет размещение метки текстового поля. +public enum TextAreaLabelPlacement: String, CaseIterable { + case outer + case inner + case none +} + +/// Определяет возможные макеты для текстового поля. +public enum TextAreaLayout: String, CaseIterable { + case `default` + case clear +} + +/// Определяет размещение обязательного индикатора. +public enum TextAreaRequiredPlacement: String, CaseIterable { + case left + case right +} + +/// Определяет возможные значения текстового поля. +public enum TextAreaValue: Equatable { + /// Одиночное текстовое значение. + case single(String) + /// Множественное значение с чипсами. + case multiple(String, [ChipData]) + + public static func == (lhs: TextAreaValue, rhs: TextAreaValue) -> Bool { + switch (lhs, rhs) { + case (.single(let lhs), .single(let rhs)): + return lhs == rhs + case (.multiple(let lhsText, let lhsChips), .multiple(let rhsText, let rhsChips)): + return lhsText == rhsText && lhsChips == rhsChips + default: + return false + } + } + + public var text: String { + switch self { + case .single(let text): + return text + case .multiple(let text, _): + return text + } + } + + public func updated(with text: String) -> TextAreaValue { + switch self { + case .single: + return .single(text) + case .multiple(_, let chips): + return .multiple(text, chips) + } + } +} + +// MARK: - SDDSTextArea + +/** + `SDDSTextArea` представляет собой настраиваемое текстовое поле с поддержкой различных стилей, макетов и конфигураций. + + - Properties: + - value: Значение текстового поля (`single` или `multiple`). + - title: Заголовок текстового поля. + - optionalTitle: Дополнительный заголовок (например, для опциональных полей). + - placeholder: Текст placeholder, отображаемый при пустом поле. + - caption: Подпись под текстовым полем. + - textBefore: Префикс перед текстом или плейсхолдером. + - textAfter: Суффикс после текста или плейсхолдера. + - disabled: Флаг, указывающий, отключено ли поле. + - readOnly: Флаг, указывающий, включено ли поле только на режим чтения. + - style: Стиль текстового поля (`default`, `error`, `warning`, `success`). + - labelPlacement: Размещение метки (`outer`, `inner`, `none`). + - required: Флаг, указывающий, является ли поле обязательным. + - requiredPlacement: Размещение обязательного индикатора (`left`, `right`). + - dynamicHeight: Флаг, указывающий, расширяется ли текстовое поле по высоте в зависимости от высоты текста. + - appearance: Параметры внешнего вида текстового поля. + - size: Конфигурация размеров текстового поля. + - layout: Макет текстового поля (`default`, `clear`). + - accessibility: Параметры доступности. + - iconViewProvider: Поставщик левого иконки. + - iconActionViewProvider: Поставщик правой иконки действия. + */ +public struct SDDSTextArea: View { + @State var text: String + @Binding public var value: TextAreaValue + public let title: String + public let optionalTitle: String + public let placeholder: String + public let caption: String + public let counter: String + public let disabled: Bool + public let readOnly: Bool + public let style: TextAreaStyle + public let labelPlacement: TextAreaLabelPlacement + public let required: Bool + public let requiredPlacement: TextAreaRequiredPlacement + public let dynamicHeight: Bool + public let appearance: TextAreaAppearance + public let size: TextAreaSizeConfiguration + public let chipGroupSize: ChipGroupSizeConfiguration + public let layout: TextAreaLayout + public let accessibility: TextAreaAccessibility + public let iconActionViewProvider: ViewProvider? + + @Environment(\.colorScheme) private var colorScheme + @State private var isFocused: Bool = false + @State private var textHeight: CGFloat = 0.0 + @State private var chipGroupContentHeight: CGFloat = 0 + private let debugConfiguration: TextFieldDebugConfiguration + + public init( + value: Binding, + title: String = "", + optionalTitle: String = "", + placeholder: String = "", + caption: String = "", + counter: String = "", + disabled: Bool = false, + readOnly: Bool = false, + style: TextAreaStyle = .default, + labelPlacement: TextAreaLabelPlacement = .outer, + required: Bool = false, + requiredPlacement: TextAreaRequiredPlacement = .left, + dynamicHeight: Bool = false, + appearance: TextAreaAppearance, + size: TextAreaSizeConfiguration, + chipGroupSize: ChipGroupSizeConfiguration, + layout: TextAreaLayout, + accessibility: TextAreaAccessibility = TextAreaAccessibility(), + iconActionViewProvider: ViewProvider? = nil + ) { + switch value.wrappedValue { + case .single(let text): + _text = State(wrappedValue: text) + case .multiple(let text, _): + _text = State(wrappedValue: text) + } + _value = value + + self.caption = caption + self.counter = counter + self.disabled = disabled + self.readOnly = readOnly + self.style = style + self.labelPlacement = labelPlacement + self.required = required + self.requiredPlacement = requiredPlacement + self.title = title + self.optionalTitle = optionalTitle + self.placeholder = placeholder + self.dynamicHeight = dynamicHeight + self.appearance = appearance + self.size = size + self.chipGroupSize = chipGroupSize + self.layout = layout + self.accessibility = accessibility + self.iconActionViewProvider = iconActionViewProvider + self.debugConfiguration = TextFieldDebugConfiguration() + } + + public var body: some View { + HStack(alignment: .top, spacing: 0) { + if showOuterTitleIndicatorForDefaultLayout || showInnerTitleIndicatorForClearLayout || showNoneTitleLeftIndicatorForClearLayout { + indicatorWithTrailingPadding + } + + VStack(alignment: .leading, spacing: 0) { + if labelPlacement == .outer { + titleLabel + .multilineTextAlignment(appearance.titleTextAlignment) + .debug(condition: debugConfiguration.title) + } + + HStack(spacing: 0) { + fieldView + } + HStack(spacing: 0) { + if showInnerTitleIndicatorForClearLayout { + Spacer() + .frame(width: size.indicatorSize.width) + .padding(.trailing, indicatorPadding, debug: debugConfiguration.indicatorPadding) + } + if layout == .clear { + captionLabel + Spacer() + counterLabel + } + } + } + + if showInnerTitleRightIndicatorForClearLayout || showNoneTitleIndicatorForClearLayout { + indicator + } + } + .opacity(disabled ? appearance.disabledAlpha : 1) + .debug(condition: debugConfiguration.textField) + } + + // MARK: - Subviews + + @ViewBuilder + private var titleLabel: some View { + HStack(alignment: .center, spacing: 0) { + mainTitleView + + if !optionalTitle.isEmpty && !required { + optionalTitleView + } + if shouldShowRequiredIndicatorAfterTitle { + indicator + } + } + .padding(.bottom, size.titleBottomPadding, debug: debugConfiguration.titleBottomPadding) + } + + @ViewBuilder + private var mainTitleView: some View { + Text(title) + .typography(titleTypography) + .foregroundColor(appearance.titleColor.color(for: colorScheme)) + .multilineTextAlignment(appearance.titleTextAlignment) + .debug(condition: debugConfiguration.title) + } + + @ViewBuilder + private var optionalTitleView: some View { + Text(formattedOptionalTitle) + .typography(titleTypography) + .foregroundColor(appearance.optionalTitleColor.color(for: colorScheme)) + .multilineTextAlignment(appearance.titleTextAlignment) + .debug(condition: debugConfiguration.title) + } + + @ViewBuilder + private var innerOptionalTitleView: some View { + if required { + EmptyView() + } else { + Text(formattedOptionalTitle) + .typography(innerTitleTypography) + .foregroundColor(appearance.optionalTitleColor.color(for: colorScheme)) + .multilineTextAlignment(appearance.titleTextAlignment) + .debug(condition: debugConfiguration.title) + } + } + + @ViewBuilder + private var indicator: some View { + indicatorView + .offset(y: indicatorYOffset, debug: debugConfiguration.indicatorYOffset) + .padding(.leading, indicatorPadding, debug: debugConfiguration.indicatorPadding) + } + + @ViewBuilder + private var indicatorWithTrailingPadding: some View { + indicatorView + .offset(y: indicatorYOffset, debug: debugConfiguration.indicatorYOffset) + .padding(.trailing, indicatorPadding, debug: debugConfiguration.indicatorPadding) + } + + @ViewBuilder + private var contentView: some View { + switch value { + case .single: + ZStack(alignment: .topTrailing) { + if shouldShowInnerTitle { + textEditor(id: textAreaInnerTitleId) + .padding(.bottom, size.textInputPaddings.bottom) + .padding(.leading, size.textInputPaddings.leading) + .padding(.trailing, size.textInputPaddings.trailing) + + iconActionView + .padding(.trailing, fieldHorizontalPadding) + } else { + textEditor(id: textAreaOuterTitleId) + .padding(size.textInputPaddings) + + iconActionView + .padding(.top, size.textInputPaddings.top) + .padding(.trailing, fieldHorizontalPadding) + } + } + case .multiple(_, let chips): + VStack(alignment: .leading, spacing: 0) { + ZStack(alignment: .topTrailing) { + ScrollView { + SDDSChipGroup(data: chips, size: chipGroupSize, height: $chipGroupContentHeight) + .padding(.trailing, iconActionTrailingPadding) + } + .frame(height: calculatedChipGroupHeight) + .padding(.bottom, size.chipGroupVerticalBottomPadding) + .padding(.top, size.chipGroupVerticalTopPadding) + + iconActionView + .padding(.top, size.textInputPaddings.top) + .padding(.trailing, fieldHorizontalPadding) + } + + textEditor(id: textAreaMultipleId) + .padding(.bottom, size.textInputPaddings.bottom) + } + } + } + + @ViewBuilder + private func textEditor(id: (any Hashable)? = nil) -> some View { + PlaceholderTextEditor( + text: $text, + textHeight: $textHeight, + isFocused: $isFocused, + readOnly: readOnly, + placeholderContent: { placeholderView }, + textTypography: textTypography, + appearance: appearance, + showsVerticalScrollIndicator: true, + trailingContentPadding: trailingContentPadding, + dynamicHeight: dynamicHeight, + textColor: textColor, + colorScheme: colorScheme, + onChange: { newText in + if newText != self.value.text { + self.value = self.value.updated(with: newText) + } + } + ) + .allowsHitTesting(!disabled) + .id(textEditorId(with: id)) + .onChange(of: value) { newValue in + guard !readOnly else { + return + } + self.text = newValue.text + } + .onChange(of: readOnly) { newValue in + if newValue { + isFocused = false + } + } + .onChange(of: disabled) { newValue in + if newValue { + isFocused = false + } + } + } + + private func textEditorId(with hashable: (any Hashable)? = nil) -> Int { + var hasher = Hasher() + hasher.combine(readOnly) + hasher.combine(size.fieldHeight(layout: layout)) + if let hashable = hashable { + hasher.combine(hashable) + } + return hasher.finalize() + } + + @ViewBuilder + private var placeholderView: some View { + if placeholder.isEmpty && labelPlacement == .inner && !displayChips { + HStack(spacing: 0) { + if !title.isEmpty { + Text(title) + .typography(textTypography) + .foregroundColor(placeholderColor) + } + if !optionalTitle.isEmpty { + innerOrNonePlacementOptionalTitleView + } + } + } else { + if layout == .clear && isFocused && labelPlacement == .inner { + EmptyView() + } else { + HStack(spacing: 0) { + Text(placeholder) + .typography(textTypography) + .foregroundColor(placeholderColor) + if !optionalTitle.isEmpty && labelPlacement == .none { + innerOrNonePlacementOptionalTitleView + } + } + } + } + } + + @ViewBuilder + private var innerOrNonePlacementOptionalTitleView: some View { + Text(" \(optionalTitle)") + .typography(textTypography) + .foregroundColor(appearance.optionalTitleColor.color(for: colorScheme)) + } + + @ViewBuilder + private var fieldView: some View { + ZStack(alignment: .top) { + if layout == .default { + backgroundView + } + + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + if shouldShowInnerTitle { + innerTitleView + } + + Spacer() + .frame(maxWidth: .infinity) + } + + Spacer() + .frame(maxWidth: .infinity) + } + .frame(height: size.fieldHeight(layout: layout), debug: debugConfiguration.fieldHeight) + .padding(.leading, fieldHorizontalPadding, debug: debugConfiguration.fieldHorizontalPadding) + .padding(.trailing, fieldTrailingPadding, debug: debugConfiguration.fieldHorizontalPadding) + + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + if shouldShowInnerTitle { + Spacer() + .frame(height: innerTitleTypography.lineHeight) + .padding([.top, .bottom], size.titleInnerPadding, debug: debugConfiguration.innerTitle) + } + + contentView + + Spacer() + } + + if layout == .default { + HStack(alignment: .bottom) { + captionLabel + Spacer() + counterLabel + } + .padding(.bottom, size.captionBottomPadding) + .padding(.trailing, captionTrailingPadding) + } + } + .frame(height: totalHeight) + + if layout == .clear { + bottomLineView + } + } + .padding(.leading, fieldHorizontalPadding, debug: debugConfiguration.fieldHorizontalPadding) + .padding(.trailing, fieldTrailingPadding, debug: debugConfiguration.fieldHorizontalPadding) + + if shouldShowIndicatorForInnerLabelDefaultLayout || shouldShowIndicatorForNoneLabelDefaultLayout { + indicatorOverlayView + } + } + .frame(height: totalHeight, debug: debugConfiguration.fieldHeight) + } + + @ViewBuilder + private var backgroundView: some View { + RoundedRectangle(cornerRadius: size.cornerRadius) + .strokeBorder(appearance.borderColor(for: style).color(for: colorScheme), lineWidth: size.borderWidth) + .background( + RoundedRectangle(cornerRadius: size.cornerRadius) + .fill(backgroundColor) + ) + .frame(height: totalHeight, debug: debugConfiguration.fieldView) + } + + private var totalHeight: CGFloat { + var result = CGFloat(0) + if shouldShowInnerTitle { + result += 2 * size.titleInnerPadding + result += innerTitleTypography.lineHeight + } + if displayChips { + result += size.chipGroupVerticalBottomPadding + result += size.chipGroupVerticalTopPadding + } + if layout == .default { + result += 2 * size.captionTopPadding + result += 2 * size.captionBottomPadding + result += max(captionTypography.lineHeight, counterTypography.lineHeight) + } + result += totalTextHeight + return max(result, size.fieldHeight(layout: layout)) + } + + private var totalTextHeight: CGFloat { + var result = CGFloat(0) + result += textHeight + + if displayChips { + result += calculatedChipGroupHeight + } else { + result += size.textInputPaddings.top + } + if labelPlacement != .inner { + result += size.textInputPaddings.bottom + } + if labelPlacement != .outer { + result += size.textInputBottomPadding + } + return result + } + + @ViewBuilder + private var innerTitleView: some View { + HStack(spacing: 0) { + Text(title) + .typography(innerTitleTypography) + .foregroundColor(appearance.titleColor.color(for: colorScheme)) + .multilineTextAlignment(appearance.innerTitleTextAlignment) + .frame(height: innerTitleTypography.lineHeight) + .debug(condition: debugConfiguration.innerTitle) + + if !optionalTitle.isEmpty { + innerOptionalTitleView + } + } + .frame(height: innerTitleTypography.lineHeight) + .padding([.top, .bottom], size.titleInnerPadding, debug: debugConfiguration.innerTitle) + } + + private var counterColor: Color { + return appearance.counterColorDefault.color(for: colorScheme) + } + + private var backgroundColor: Color { + if readOnly { + return appearance.backgroundColorDefault.color(for: colorScheme) + } + return appearance.backgroundColor(for: style, isFocused: isFocused).color(for: colorScheme) + } + + private var captionColor: Color { + if readOnly { + return appearance.captionColorDefault.color(for: colorScheme) + } + return appearance.captionColor(for: isFocused ? .default : style).color(for: colorScheme) + } + + private var placeholderColor: Color { + return appearance.placeholderColor(for: isFocused ? .default : style, layout: layout).color(for: colorScheme) + } + + private var textColor: Color { + return appearance.textColor(for: isFocused ? .default : style, layout: layout).color(for: colorScheme) + } + + private var bottomLineColor: Color { + if isFocused { + return appearance.focusedLineColor(for: style).color(for: colorScheme) + } + + switch style { + case .error, .success, .warning: + return appearance.placeholderColor(for: style, layout: layout).color(for: colorScheme) + case .default: + return appearance.lineColor.color(for: colorScheme) + } + } + + @ViewBuilder + private var iconActionView: some View { + if let rightView = iconActionViewProvider?.view { + rightView + .frame(width: iconActionViewWidth, height: iconActionViewHeight, debug: debugConfiguration.iconAction) + .padding(.leading, size.iconActionPadding, debug: debugConfiguration.iconAction) + } else { + EmptyView() + } + } + + @ViewBuilder + private var captionLabel: some View { + Text(caption) + .typography(captionTypography) + .multilineTextAlignment(appearance.captionTextAlignment) + .foregroundColor(captionColor) + .debug(condition: debugConfiguration.caption) + .frame(height: captionTypography.lineHeight) + .padding(.top, size.captionTopPadding, debug: debugConfiguration.captionTopPadding) + } + + @ViewBuilder + private var counterLabel: some View { + Text(counter) + .typography(counterTypography) + .multilineTextAlignment(.trailing) + .foregroundColor(counterColor) + .frame(height: counterTypography.lineHeight) + .padding(.top, size.captionTopPadding, debug: debugConfiguration.captionTopPadding) + } + + @ViewBuilder + private var indicatorView: some View { + Circle() + .fill(appearance.requiredIndicatorColor.color(for: colorScheme)) + .frame(width: size.indicatorSize.width, height: size.indicatorSize.height, debug: debugConfiguration.indicatorView) + } + + @ViewBuilder + private var indicatorOverlayView: some View { + VStack { + HStack(spacing: 0) { + if requiredPlacement == .right { + Spacer() + } + indicatorView + .debug(condition: debugConfiguration.indicatorView) + if requiredPlacement == .left { + Spacer() + } + } + Spacer() + } + + .frame(height: totalHeight, debug: debugConfiguration.fieldHeight) + } + + @ViewBuilder + private var bottomLineView: some View { + Rectangle() + .fill(bottomLineColor) + .frame(height: size.lineWidth, debug: debugConfiguration.fieldView) + } + + // MARK: - Computed Properties for Conditions + + private var iconActionViewWidth: CGFloat { + min(size.iconActionSize.width, size.fieldHeight(layout: layout)) + } + + private var iconActionViewHeight: CGFloat { + min(size.iconActionSize.height, size.fieldHeight(layout: layout)) + } + + private var indicatorYOffset: CGFloat { + size.indicatorYOffset(labelPlacement: labelPlacement, requiredPlacement: requiredPlacement, layout: layout) + } + + private var indicatorPadding: CGFloat { + size.indicatorPadding(labelPlacement: labelPlacement, requiredPlacement: requiredPlacement, layout: layout) + } + + private var captionTrailingPadding: CGFloat { + fieldHorizontalPadding + } + + private var fieldTrailingPadding: CGFloat { + 0 + } + + private var iconActionTrailingPadding: CGFloat { + if let _ = iconActionViewProvider?.view { + return iconActionViewWidth + size.iconActionPadding + } else { + return 0 + } + } + + private var shouldShowInnerTitle: Bool { + labelPlacement == .inner && + !displayChips && + appearance.innerTitleTypography.typography(with: size) != nil + } + + private var shouldShowIndicatorForInnerLabelDefaultLayout: Bool { + labelPlacement == .inner && + required && + layout == .default + } + + private var shouldShowIndicatorForNoneLabelDefaultLayout: Bool { + labelPlacement == .none && + required && + layout == .default + } + + private var shouldShowRequiredIndicatorAfterTitle: Bool { + required && + requiredPlacement == .right + } + + private var showOuterTitleIndicatorForDefaultLayout: Bool { + labelPlacement == .outer && + required && + requiredPlacement == .left && + layout == .default + } + + private var showNoneTitleIndicatorForClearLayout: Bool { + labelPlacement == .none && + required && + requiredPlacement == .right && + layout == .clear + } + + private var showInnerTitleIndicatorForClearLayout: Bool { + labelPlacement == .inner && + required && + requiredPlacement == .left && + layout == .clear + } + + private var showInnerTitleRightIndicatorForClearLayout: Bool { + labelPlacement == .inner && + required && + requiredPlacement == .right && + layout == .clear + } + + private var showNoneTitleLeftIndicatorForClearLayout: Bool { + labelPlacement == .none && + required && + layout == .clear && + requiredPlacement == .left + } + + private var trailingContentPadding: CGFloat { + iconActionViewProvider?.view != nil ? iconActionTrailingPadding : 0 + } + + private var shouldCenterText: Bool { + return labelPlacement == .outer || labelPlacement == .none || !shouldShowInnerTitle + } + + private var displayChips: Bool { + switch value { + case .single: + return false + case .multiple: + return true + } + } + + private var calculatedChipGroupHeight: CGFloat { + return min(size.chipGroupHeight, chipGroupContentHeight) + } + + private var formattedOptionalTitle: String { + " \(optionalTitle)" + } + + private var chipCornerRadius: CGFloat { + switch value { + case .single: + return 0 + case .multiple(_, let chips): + guard let chipSize = chips.first?.size else { + return 0 + } + switch chipSize.borderStyle { + case .default(let radius): + return radius + case .pilled: + return chipSize.height / 2 + } + } + } + + private let textAreaOuterTitleId = "textAreaOuterTitleId" + private let textAreaInnerTitleId = "textAreaInnerTitleId" + private let textAreaMultipleId = "textAreaMultipleId" + + private var titleTypography: TypographyToken { + guard let typography = appearance.titleTypography.typography(with: size) else { + fatalError("Undefined Title Typography for size \(size).") + } + return typography + } + + private var innerTitleTypography: TypographyToken { + guard let typography = appearance.innerTitleTypography.typography(with: size) else { + fatalError("Undefined Inner Title Typography for size \(size).") + } + return typography + } + + private var captionTypography: TypographyToken { + guard let typography = appearance.captionTypography.typography(with: size) else { + fatalError("Undefined Caption Typography for size \(size).") + } + return typography + } + + private var counterTypography: TypographyToken { + guard let typography = appearance.counterTypography.typography(with: size) else { + fatalError("Undefined Counter Typography for size \(size).") + } + return typography + } + + private var textTypography: TypographyToken { + guard let typography = appearance.textTypography.typography(with: size) else { + fatalError("Undefined Text Typography for size \(size).") + } + return typography + } + + private var fieldHorizontalPadding: CGFloat { + layout == .clear ? 0 : size.fieldHorizontalPadding + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAccessibility.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAccessibility.swift new file mode 100644 index 000000000..2b79a46fe --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAccessibility.swift @@ -0,0 +1,12 @@ +import Foundation + +/// Определяет параметры доступности для текстового поля. +public struct TextAreaAccessibility { + public let label: String + public let hint: String + + public init(label: String = "", hint: String = "") { + self.label = label + self.hint = hint + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAppearance.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAppearance.swift new file mode 100644 index 000000000..a37fa90a2 --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaAppearance.swift @@ -0,0 +1,240 @@ +import Foundation +import SwiftUI +import SDDSThemeCore +import SDDSServTheme + +public struct TextAreaAppearance { + public let id = UUID() + public let textTypography: TypographyConfiguration + public let titleTypography: TypographyConfiguration + public let innerTitleTypography: TypographyConfiguration + public let captionTypography: TypographyConfiguration + public let counterTypography: TypographyConfiguration + public let titleColor: ColorToken + public let optionalTitleColor: ColorToken + public let textColor: ColorToken + public let textColorError: ColorToken + public let textColorWarning: ColorToken + public let textColorSuccess: ColorToken + public let disabledAlpha: CGFloat + public let requiredIndicatorColor: ColorToken + public let cursorColor: ColorToken + public let focusedBackgroundColor: ColorToken + public let titleTextAlignment: TextAlignment + public let captionTextAlignment: TextAlignment + public let inputTextAlignment: TextAlignment + public let innerTitleTextAlignment: TextAlignment + public let lineColor: ColorToken + public let focusedLineColor: ColorToken + public let focusedLineColorError: ColorToken + public let focusedLineColorWarning: ColorToken + public let focusedLineColorSuccess: ColorToken + public let placeholderColor: ColorToken? + public let borderColorDefault: ColorToken + public let borderColorError: ColorToken + public let borderColorWarning: ColorToken + public let borderColorSuccess: ColorToken + public let backgroundColorDefault: ColorToken + public let backgroundColorError: ColorToken + public let backgroundColorWarning: ColorToken + public let backgroundColorSuccess: ColorToken + public let captionColorDefault: ColorToken + public let captionColorError: ColorToken + public let captionColorWarning: ColorToken + public let captionColorSuccess: ColorToken + public let counterColorDefault: ColorToken + public let placeholderColorDefault: ColorToken + public let placeholderColorError: ColorToken + public let placeholderColorWarning: ColorToken + public let placeholderColorSuccess: ColorToken + + public init( + textTypography: TypographyConfiguration, + titleTypography: TypographyConfiguration, + innerTitleTypography: TypographyConfiguration, + captionTypography: TypographyConfiguration, + counterTypography: TypographyConfiguration, + titleColor: ColorToken, + optionalTitleColor: ColorToken, + textColor: ColorToken, + textColorError: ColorToken, + textColorWarning: ColorToken, + textColorSuccess: ColorToken, + disabledAlpha: CGFloat, + requiredIndicatorColor: ColorToken, + cursorColor: ColorToken, + focusedBackgroundColor: ColorToken, + titleTextAlignment: TextAlignment = .leading, + captionTextAlignment: TextAlignment = .leading, + inputTextAlignment: TextAlignment = .leading, + innerTitleTextAlignment: TextAlignment = .leading, + lineColor: ColorToken, + focusedLineColor: ColorToken, + focusedLineColorError: ColorToken, + focusedLineColorWarning: ColorToken, + focusedLineColorSuccess: ColorToken, + placeholderColor: ColorToken? = nil, + borderColorDefault: ColorToken, + borderColorError: ColorToken, + borderColorWarning: ColorToken, + borderColorSuccess: ColorToken, + backgroundColorDefault: ColorToken, + backgroundColorError: ColorToken, + backgroundColorWarning: ColorToken, + backgroundColorSuccess: ColorToken, + captionColorDefault: ColorToken, + captionColorError: ColorToken, + captionColorWarning: ColorToken, + captionColorSuccess: ColorToken, + counterColorDefault: ColorToken, + placeholderColorDefault: ColorToken, + placeholderColorError: ColorToken, + placeholderColorWarning: ColorToken, + placeholderColorSuccess: ColorToken + ) { + self.textTypography = textTypography + self.titleTypography = titleTypography + self.innerTitleTypography = innerTitleTypography + self.captionTypography = captionTypography + self.counterTypography = counterTypography + self.titleColor = titleColor + self.optionalTitleColor = optionalTitleColor + self.textColor = textColor + self.textColorError = textColorError + self.textColorWarning = textColorWarning + self.textColorSuccess = textColorSuccess + self.disabledAlpha = disabledAlpha + self.requiredIndicatorColor = requiredIndicatorColor + self.cursorColor = cursorColor + self.focusedBackgroundColor = focusedBackgroundColor + self.titleTextAlignment = titleTextAlignment + self.captionTextAlignment = captionTextAlignment + self.inputTextAlignment = inputTextAlignment + self.innerTitleTextAlignment = innerTitleTextAlignment + self.lineColor = lineColor + self.focusedLineColor = focusedLineColor + self.focusedLineColorError = focusedLineColorError + self.focusedLineColorWarning = focusedLineColorWarning + self.focusedLineColorSuccess = focusedLineColorSuccess + self.placeholderColor = placeholderColor + self.borderColorDefault = borderColorDefault + self.borderColorError = borderColorError + self.borderColorWarning = borderColorWarning + self.borderColorSuccess = borderColorSuccess + self.backgroundColorDefault = backgroundColorDefault + self.backgroundColorError = backgroundColorError + self.backgroundColorWarning = backgroundColorWarning + self.backgroundColorSuccess = backgroundColorSuccess + self.captionColorDefault = captionColorDefault + self.captionColorError = captionColorError + self.captionColorWarning = captionColorWarning + self.captionColorSuccess = captionColorSuccess + self.counterColorDefault = counterColorDefault + self.placeholderColorDefault = placeholderColorDefault + self.placeholderColorError = placeholderColorError + self.placeholderColorWarning = placeholderColorWarning + self.placeholderColorSuccess = placeholderColorSuccess + } + + public func borderColor(for style: TextAreaStyle) -> ColorToken { + switch style { + case .default: + return borderColorDefault + case .error: + return borderColorError + case .warning: + return borderColorWarning + case .success: + return borderColorSuccess + } + } + + public func backgroundColor(for style: TextAreaStyle, isFocused: Bool) -> ColorToken { + if isFocused { + return focusedBackgroundColor + } + switch style { + case .default: + return backgroundColorDefault + case .error: + return backgroundColorError + case .warning: + return backgroundColorWarning + case .success: + return backgroundColorSuccess + } + } + + public func captionColor(for style: TextAreaStyle) -> ColorToken { + switch style { + case .default: + return captionColorDefault + case .error: + return captionColorError + case .warning: + return captionColorWarning + case .success: + return captionColorSuccess + } + } + + public func focusedLineColor(for style: TextAreaStyle) -> ColorToken { + switch style { + case .default: + return focusedLineColor + case .error: + return focusedLineColorError + case .warning: + return focusedLineColorWarning + case .success: + return focusedLineColorSuccess + } + } + + public func textColor(for style: TextAreaStyle, layout: TextAreaLayout) -> ColorToken { + if layout == .clear { + switch style { + case .default: + return textColor + case .error: + return textColorError + case .warning: + return textColorWarning + case .success: + return textColorSuccess + } + } else { + return textColor + } + } + + public func placeholderColor(for style: TextAreaStyle, layout: TextAreaLayout) -> ColorToken { + if let customColor = placeholderColor { + return customColor + } + if layout == .clear { + switch style { + case .default: + return placeholderColorDefault + case .error: + return placeholderColorError + case .warning: + return placeholderColorWarning + case .success: + return placeholderColorSuccess + } + } else { + return placeholderColorDefault + } + } +} + +extension TextAreaAppearance: Hashable { + public static func == (lhs: TextAreaAppearance, rhs: TextAreaAppearance) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaSizeConfiguration.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaSizeConfiguration.swift new file mode 100644 index 000000000..334fa2bfc --- /dev/null +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextArea/TextAreaSizeConfiguration.swift @@ -0,0 +1,33 @@ +import Foundation +import SwiftUI + +/// Определяет конфигурацию размеров текстового поля. +public protocol TextAreaSizeConfiguration: CustomDebugStringConvertible { + var titleBottomPadding: CGFloat { get } + var titleInnerPadding: CGFloat { get } + var fieldHorizontalPadding: CGFloat { get } + var captionTopPadding: CGFloat { get } + var captionBottomPadding: CGFloat { get } + var textInputPaddings: EdgeInsets { get } + var cornerRadius: CGFloat { get } + var borderWidth: CGFloat { get } + var iconActionPadding: CGFloat { get } + var indicatorSize: CGSize { get } + var iconActionSize: CGSize { get } + var multipleValueHorizontalPadding: CGFloat { get } + var chipContainerHorizontalPadding: CGFloat { get } + var lineWidth: CGFloat { get } + var textBeforeLeadingPadding: CGFloat { get } + var textBeforeTrailingPadding: CGFloat { get } + var textAfterLeadingPadding: CGFloat { get } + var textAfterTrailingPadding: CGFloat { get } + var textHorizontalPadding: CGFloat { get } + var chipGroupHeight: CGFloat { get } + var chipGroupVerticalTopPadding: CGFloat { get } + var chipGroupVerticalBottomPadding: CGFloat { get } + var textInputBottomPadding: CGFloat { get } + + func fieldHeight(layout: TextAreaLayout) -> CGFloat + func indicatorPadding(labelPlacement: TextAreaLabelPlacement, requiredPlacement: TextAreaRequiredPlacement, layout: TextAreaLayout) -> CGFloat + func indicatorYOffset(labelPlacement: TextAreaLabelPlacement, requiredPlacement: TextAreaRequiredPlacement, layout: TextAreaLayout) -> CGFloat +} diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/SDDSTextField.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/SDDSTextField.swift index 5c417c34a..b027d789a 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/SDDSTextField.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/SDDSTextField.swift @@ -118,7 +118,6 @@ public struct SDDSTextField: View { @Environment(\.colorScheme) private var colorScheme @State private var isFocused: Bool = false - @State private var oldText: String private let debugConfiguration: TextFieldDebugConfiguration public init( @@ -148,8 +147,7 @@ public struct SDDSTextField: View { case .multiple(let text, _): _text = State(wrappedValue: text) } - _oldText = _text - + _value = value self.caption = caption self.textBefore = textBefore @@ -223,7 +221,7 @@ public struct SDDSTextField: View { HStack(alignment: .center, spacing: 0) { mainTitleView - if !optionalTitle.isEmpty { + if !optionalTitle.isEmpty && !required { optionalTitleView } if shouldShowRequiredIndicatorAfterTitle { @@ -333,12 +331,17 @@ public struct SDDSTextField: View { PlaceholderTextField( text: $text, isFocused: $isFocused, - placeholder: placeholder, - placeholderColor: placeholderColor, - placeholderTypography: textTypography, + textColor: textColor, + textAlignment: appearance.inputTextAlignment, + cursorColor: appearance.cursorColor.color(for: colorScheme), + textTypography: textTypography, + readOnly: readOnly, placeholderBeforeContent: { textBeforeView }, + placeholderContent: { + placeholderView + }, placeholderAfterContent: { HStack(spacing: 0) { textAfterView @@ -376,10 +379,13 @@ public struct SDDSTextField: View { PlaceholderTextField( text: $text, isFocused: $isFocused, - placeholder: placeholder, - placeholderColor: placeholderColor, - placeholderTypography: textTypography, + textColor: textColor, + textAlignment: appearance.inputTextAlignment, + cursorColor: appearance.cursorColor.color(for: colorScheme), + textTypography: textTypography, + readOnly: readOnly, placeholderBeforeContent: {}, + placeholderContent: { placeholderView }, placeholderAfterContent: { placeholderIndicator }, onEditingChanged: { focused in isFocused = focused @@ -395,16 +401,10 @@ public struct SDDSTextField: View { @ViewBuilder private func textFieldConfiguration(textField: FocusableTextField) -> some View { textField - .accentColor(appearance.cursorColor.color(for: colorScheme)) - .typography(textTypography) - .foregroundColor(textColor) - .multilineTextAlignment(appearance.inputTextAlignment) .padding(size.textInputPaddings, debug: debugConfiguration.textField) .onChange(of: text) { newText in - if readOnly { - self.text = oldText - } else { - self.oldText = newText + guard !readOnly else { + return } if newText != self.value.text { self.value = self.value.updated(with: newText) @@ -417,6 +417,38 @@ public struct SDDSTextField: View { self.text = newValue.text } } + + @ViewBuilder + private var placeholderView: some View { + if placeholder.isEmpty && labelPlacement == .inner && !displayChips { + HStack(spacing: 0) { + if !title.isEmpty { + Text(title) + .typography(textTypography) + .foregroundColor(placeholderColor) + } + if !optionalTitle.isEmpty { + innerOrNonePlacementOptionalTitleView + } + } + } else { + HStack(spacing: 0) { + Text(placeholder) + .typography(textTypography) + .foregroundColor(placeholderColor) + if !optionalTitle.isEmpty && labelPlacement == .none { + innerOrNonePlacementOptionalTitleView + } + } + } + } + + @ViewBuilder + private var innerOrNonePlacementOptionalTitleView: some View { + Text(" \(optionalTitle)") + .typography(textTypography) + .foregroundColor(appearance.optionalTitleColor.color(for: colorScheme)) + } @ViewBuilder private var textBeforeView: some View { @@ -483,7 +515,7 @@ public struct SDDSTextField: View { .strokeBorder(appearance.borderColor(for: style).color(for: colorScheme), lineWidth: size.borderWidth) .background( RoundedRectangle(cornerRadius: size.cornerRadius) - .fill(appearance.backgroundColor(for: style, isFocused: isFocused).color(for: colorScheme)) + .fill(backgroundColor) ) .frame(height: size.fieldHeight, debug: debugConfiguration.fieldView) } @@ -511,7 +543,17 @@ public struct SDDSTextField: View { .frame(height: size.lineWidth, debug: debugConfiguration.fieldView) } + private var backgroundColor: Color { + if readOnly { + appearance.backgroundColorDefault.color(for: colorScheme) + } + return appearance.backgroundColor(for: style, isFocused: isFocused, readOnly: readOnly).color(for: colorScheme) + } + private var captionColor: Color { + if readOnly { + return appearance.captionColorDefault.color(for: colorScheme) + } return appearance.captionColor(for: isFocused ? .default : style).color(for: colorScheme) } @@ -651,7 +693,8 @@ public struct SDDSTextField: View { private var shouldShowInnerTitle: Bool { labelPlacement == .inner && !displayChips && - appearance.innerTitleTypography.typography(with: size) != nil + appearance.innerTitleTypography.typography(with: size) != nil && + !placeholder.isEmpty } private var shouldShowIndicatorForInnerLabelDefaultLayout: Bool { diff --git a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/TextFieldAppearance.swift b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/TextFieldAppearance.swift index 758aeb7b3..93b1b3275 100644 --- a/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/TextFieldAppearance.swift +++ b/SDDSComponents/Sources/SDDSComponents/Components/SDDSTextField/TextFieldAppearance.swift @@ -35,6 +35,7 @@ public struct TextFieldAppearance { public let borderColorWarning: ColorToken public let borderColorSuccess: ColorToken public let backgroundColorDefault: ColorToken + public let backgroundColorReadOnly: ColorToken public let backgroundColorError: ColorToken public let backgroundColorWarning: ColorToken public let backgroundColorSuccess: ColorToken @@ -81,6 +82,7 @@ public struct TextFieldAppearance { borderColorWarning: ColorToken, borderColorSuccess: ColorToken, backgroundColorDefault: ColorToken, + backgroundColorReadOnly: ColorToken, backgroundColorError: ColorToken, backgroundColorWarning: ColorToken, backgroundColorSuccess: ColorToken, @@ -126,6 +128,7 @@ public struct TextFieldAppearance { self.borderColorWarning = borderColorWarning self.borderColorSuccess = borderColorSuccess self.backgroundColorDefault = backgroundColorDefault + self.backgroundColorReadOnly = backgroundColorReadOnly self.backgroundColorError = backgroundColorError self.backgroundColorWarning = backgroundColorWarning self.backgroundColorSuccess = backgroundColorSuccess @@ -154,7 +157,10 @@ public struct TextFieldAppearance { } } - public func backgroundColor(for style: TextFieldStyle, isFocused: Bool) -> ColorToken { + public func backgroundColor(for style: TextFieldStyle, isFocused: Bool, readOnly: Bool) -> ColorToken { + if readOnly { + return backgroundColorReadOnly + } if isFocused { return focusedBackgroundColor } diff --git a/SDDSComponents/Sources/SDDSComponents/Views/FocusableTextField/FocusableTextField.swift b/SDDSComponents/Sources/SDDSComponents/Views/FocusableTextField/FocusableTextField.swift index 2669aa22a..5c0d2a532 100644 --- a/SDDSComponents/Sources/SDDSComponents/Views/FocusableTextField/FocusableTextField.swift +++ b/SDDSComponents/Sources/SDDSComponents/Views/FocusableTextField/FocusableTextField.swift @@ -1,15 +1,16 @@ import SwiftUI +import SDDSThemeCore struct FocusableTextField: UIViewRepresentable { @Binding var text: String @Binding var isFocused: Bool + let textColor: Color + let textAlignment: TextAlignment + let cursorColor: Color + let typography: TypographyToken + let readOnly: Bool var onEditingChanged: ((Bool) -> Void)? = nil - var textColor: UIColor = .black - var textAlignment: NSTextAlignment = .left - var cursorColor: UIColor = .blue - var font: UIFont = UIFont.systemFont(ofSize: 17) - class Coordinator: NSObject, UITextFieldDelegate { var parent: FocusableTextField @@ -20,6 +21,10 @@ struct FocusableTextField: UIViewRepresentable { func textFieldDidChangeSelection(_ textField: UITextField) { parent.text = textField.text ?? "" } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return !parent.readOnly + } func textFieldDidBeginEditing(_ textField: UITextField) { parent.isFocused = true @@ -36,63 +41,59 @@ struct FocusableTextField: UIViewRepresentable { Coordinator(self) } - func makeUIView(context: Context) -> UITextField { + func makeUIView(context: Context) -> UIView { + let containerView = UIView() + containerView.backgroundColor = .clear + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false textField.delegate = context.coordinator configure(textField) - return textField + + containerView.addSubview(textField) + + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + textField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + textField.topAnchor.constraint(equalTo: containerView.topAnchor), + textField.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + return containerView } - func updateUIView(_ uiView: UITextField, context: Context) { - uiView.text = text - configure(uiView) + func updateUIView(_ view: UIView, context: Context) { + guard let textField = view.subviews.first as? UITextField else { + return + } + + textField.text = text + configure(textField) DispatchQueue.main.async { if isFocused { - uiView.becomeFirstResponder() + textField.becomeFirstResponder() } else { - uiView.resignFirstResponder() + textField.resignFirstResponder() } } } private func configure(_ textField: UITextField) { - textField.textColor = textColor - textField.textAlignment = textAlignment - textField.tintColor = cursorColor - textField.font = font - } -} - -extension FocusableTextField { - func foregroundColor(_ color: Color) -> FocusableTextField { - var copy = self - copy.textColor = UIColor(color) - return copy - } - - func multilineTextAlignment(_ alignment: TextAlignment) -> FocusableTextField { - var copy = self - switch alignment { - case .leading: - copy.textAlignment = .left + textField.textColor = UIColor(textColor) + + let nsTextAlignment: NSTextAlignment + switch textAlignment { case .center: - copy.textAlignment = .center + nsTextAlignment = .center + case .leading: + nsTextAlignment = .left case .trailing: - copy.textAlignment = .right + nsTextAlignment = .right } - return copy - } - - func accentColor(_ color: Color) -> FocusableTextField { - var copy = self - copy.cursorColor = UIColor(color) - return copy - } - - func typography(_ token: TypographyToken) -> FocusableTextField { - var copy = self - copy.font = token.uiFont - return copy + + textField.textAlignment = nsTextAlignment + textField.tintColor = UIColor(cursorColor) + textField.font = typography.uiFont } } diff --git a/SDDSComponents/Sources/SDDSComponents/Views/SelectionControl/SelectionControl.swift b/SDDSComponents/Sources/SDDSComponents/Views/SelectionControl/SelectionControl.swift index 00868fdba..3ff8edd0b 100644 --- a/SDDSComponents/Sources/SDDSComponents/Views/SelectionControl/SelectionControl.swift +++ b/SDDSComponents/Sources/SDDSComponents/Views/SelectionControl/SelectionControl.swift @@ -139,11 +139,13 @@ struct SelectionControl: View { if let tintColor = appearance.imageTintColor { image .resizable() + .renderingMode(.template) .aspectRatio(contentMode: .fit) .foregroundColor(tintColor.color(for: colorScheme)) } else { image .resizable() + .renderingMode(.original) .aspectRatio(contentMode: .fit) } } diff --git a/SDDSDemoApp/SDDSDemoApp.xcodeproj/project.pbxproj b/SDDSDemoApp/SDDSDemoApp.xcodeproj/project.pbxproj index 8495d3ba1..1eef0b58d 100644 --- a/SDDSDemoApp/SDDSDemoApp.xcodeproj/project.pbxproj +++ b/SDDSDemoApp/SDDSDemoApp.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 24C154172BB3020B00963FAA /* PlasmaDemoAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C154162BB3020B00963FAA /* PlasmaDemoAppTests.swift */; }; 24C154212BB3020B00963FAA /* PlasmaDemoAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C154202BB3020B00963FAA /* PlasmaDemoAppUITests.swift */; }; 24C154232BB3020B00963FAA /* PlasmaDemoAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C154222BB3020B00963FAA /* PlasmaDemoAppUITestsLaunchTests.swift */; }; + 811C684D2CC8024A00480F89 /* TextAreaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811C684C2CC8024A00480F89 /* TextAreaView.swift */; }; + 811C684F2CC8027600480F89 /* TextAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811C684E2CC8027600480F89 /* TextAreaViewModel.swift */; }; + 811C68512CC8059800480F89 /* ChipGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811C68502CC8059800480F89 /* ChipGroupViewModel.swift */; }; 811DE1352C4D0C7F000DD354 /* ComponentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1342C4D0C7E000DD354 /* ComponentsView.swift */; }; 811DE1392C4D0EC9000DD354 /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1382C4D0EC9000DD354 /* CheckboxView.swift */; }; 811DE13C2C4D0ED5000DD354 /* CheckboxViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE13B2C4D0ED5000DD354 /* CheckboxViewModel.swift */; }; @@ -23,7 +26,6 @@ 811DE1492C4D19DC000DD354 /* ProgressBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE1482C4D19DC000DD354 /* ProgressBarViewModel.swift */; }; 811DE14C2C4D1DA6000DD354 /* SwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE14B2C4D1DA6000DD354 /* SwitchView.swift */; }; 811DE14E2C4D1DAD000DD354 /* SwitchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811DE14D2C4D1DAD000DD354 /* SwitchViewModel.swift */; }; - 8159F7262C5CFD9200622836 /* SDDSComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 8159F7252C5CFD9200622836 /* SDDSComponents */; }; 8159F72C2C5CFDCF00622836 /* SDDSThemeCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8159F72B2C5CFDCF00622836 /* SDDSThemeCore */; }; 816243442CB9057E00506E1C /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816243432CB9057E00506E1C /* AvatarView.swift */; }; 816243462CB907EA00506E1C /* AvatarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816243452CB907E900506E1C /* AvatarViewModel.swift */; }; @@ -50,6 +52,11 @@ 8199A1722C3BF94E00AD650A /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8199A1712C3BF94E00AD650A /* Spacing.swift */; }; 8199A1742C3BF99F00AD650A /* ButtonTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8199A1732C3BF99F00AD650A /* ButtonTypography.swift */; }; 8199E5A32CBD088C00AF3D22 /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8199E5A22CBD088C00AF3D22 /* TextFieldView.swift */; }; + 819A382C2CC6926300A1377F /* SDDSComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 819A382B2CC6926300A1377F /* SDDSComponents */; }; + 819A382F2CC6928700A1377F /* ChipGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819A382E2CC6928700A1377F /* ChipGroupView.swift */; }; + 819A38312CC692BD00A1377F /* SDDSComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 819A38302CC692BD00A1377F /* SDDSComponents */; }; + 81CA2C542CC69549002AF2DF /* SDDSComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 81CA2C532CC69549002AF2DF /* SDDSComponents */; }; + 81CA2C562CC69549002AF2DF /* SDDSComponentsPreview in Frameworks */ = {isa = PBXBuildFile; productRef = 81CA2C552CC69549002AF2DF /* SDDSComponentsPreview */; }; 81E968162CBD20C300256968 /* TextFieldViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E968152CBD20C300256968 /* TextFieldViewModel.swift */; }; /* End PBXBuildFile section */ @@ -80,6 +87,9 @@ 24C1541C2BB3020B00963FAA /* SDDSDemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SDDSDemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 24C154202BB3020B00963FAA /* PlasmaDemoAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlasmaDemoAppUITests.swift; sourceTree = ""; }; 24C154222BB3020B00963FAA /* PlasmaDemoAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlasmaDemoAppUITestsLaunchTests.swift; sourceTree = ""; }; + 811C684C2CC8024A00480F89 /* TextAreaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAreaView.swift; sourceTree = ""; }; + 811C684E2CC8027600480F89 /* TextAreaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAreaViewModel.swift; sourceTree = ""; }; + 811C68502CC8059800480F89 /* ChipGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipGroupViewModel.swift; sourceTree = ""; }; 811DE1342C4D0C7E000DD354 /* ComponentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentsView.swift; sourceTree = ""; }; 811DE1382C4D0EC9000DD354 /* CheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = ""; }; 811DE13B2C4D0ED5000DD354 /* CheckboxViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxViewModel.swift; sourceTree = ""; }; @@ -113,6 +123,7 @@ 8199A1712C3BF94E00AD650A /* Spacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; 8199A1732C3BF99F00AD650A /* ButtonTypography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTypography.swift; sourceTree = ""; }; 8199E5A22CBD088C00AF3D22 /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; + 819A382E2CC6928700A1377F /* ChipGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipGroupView.swift; sourceTree = ""; }; 81D2B19A2C32B58900CAA7FD /* SDDSComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDDSComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81D2B19D2C32B59900CAA7FD /* SDDSComponents.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SDDSComponents.xcframework; path = ../build/SDDSComponents.xcframework; sourceTree = ""; }; 81E968152CBD20C300256968 /* TextFieldViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldViewModel.swift; sourceTree = ""; }; @@ -123,13 +134,16 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8159F7262C5CFD9200622836 /* SDDSComponents in Frameworks */, + 819A38312CC692BD00A1377F /* SDDSComponents in Frameworks */, 816C629B2CB80E0C00352891 /* SDDSComponentsPreview in Frameworks */, + 819A382C2CC6926300A1377F /* SDDSComponents in Frameworks */, + 81CA2C542CC69549002AF2DF /* SDDSComponents in Frameworks */, 8178A7772C74ABDA00DFDA61 /* SDDSComponents in Frameworks */, 816C62992CB80E0C00352891 /* SDDSComponents in Frameworks */, 816C629E2CB80E2E00352891 /* SDDSComponents in Frameworks */, 8159F72C2C5CFDCF00622836 /* SDDSThemeCore in Frameworks */, 816C62A02CB80E2E00352891 /* SDDSComponentsPreview in Frameworks */, + 81CA2C562CC69549002AF2DF /* SDDSComponentsPreview in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -220,6 +234,15 @@ name = Frameworks; sourceTree = ""; }; + 811C684B2CC8023600480F89 /* TextAreaView */ = { + isa = PBXGroup; + children = ( + 811C684C2CC8024A00480F89 /* TextAreaView.swift */, + 811C684E2CC8027600480F89 /* TextAreaViewModel.swift */, + ); + path = TextAreaView; + sourceTree = ""; + }; 811DE1332C4D0C58000DD354 /* Components */ = { isa = PBXGroup; children = ( @@ -231,6 +254,8 @@ 811DE1362C4D0EB2000DD354 /* Views */ = { isa = PBXGroup; children = ( + 811C684B2CC8023600480F89 /* TextAreaView */, + 819A382D2CC6927700A1377F /* ChipGroupView */, 8199E5A12CBD088000AF3D22 /* TextFieldView */, 816243592CB9414000506E1C /* CheckboxGroupView */, 816243582CB9413800506E1C /* RadioboxGroupView */, @@ -377,6 +402,15 @@ path = TextFieldView; sourceTree = ""; }; + 819A382D2CC6927700A1377F /* ChipGroupView */ = { + isa = PBXGroup; + children = ( + 819A382E2CC6928700A1377F /* ChipGroupView.swift */, + 811C68502CC8059800480F89 /* ChipGroupViewModel.swift */, + ); + path = ChipGroupView; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -395,13 +429,16 @@ ); name = SDDSDemoApp; packageProductDependencies = ( - 8159F7252C5CFD9200622836 /* SDDSComponents */, 8159F72B2C5CFDCF00622836 /* SDDSThemeCore */, 8178A7762C74ABDA00DFDA61 /* SDDSComponents */, 816C62982CB80E0C00352891 /* SDDSComponents */, 816C629A2CB80E0C00352891 /* SDDSComponentsPreview */, 816C629D2CB80E2E00352891 /* SDDSComponents */, 816C629F2CB80E2E00352891 /* SDDSComponentsPreview */, + 819A382B2CC6926300A1377F /* SDDSComponents */, + 819A38302CC692BD00A1377F /* SDDSComponents */, + 81CA2C532CC69549002AF2DF /* SDDSComponents */, + 81CA2C552CC69549002AF2DF /* SDDSComponentsPreview */, ); productName = PlasmaDemoApp; productReference = 24C154022BB3020A00963FAA /* SDDSDemoApp.app */; @@ -477,7 +514,7 @@ mainGroup = 24C153F92BB3020A00963FAA; packageReferences = ( 8159F72A2C5CFDCF00622836 /* XCLocalSwiftPackageReference "../SDDSThemeBuilder/SDDSThemeCore" */, - 816C629C2CB80E2E00352891 /* XCLocalSwiftPackageReference "../SDDSComponents" */, + 81CA2C522CC69549002AF2DF /* XCLocalSwiftPackageReference "../SDDSComponents" */, ); productRefGroup = 24C154032BB3020A00963FAA /* Products */; projectDirPath = ""; @@ -495,8 +532,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 24C1540D2BB3020B00963FAA /* Preview Assets.xcassets in Resources */, 24C1540A2BB3020B00963FAA /* Assets.xcassets in Resources */, + 24C1540D2BB3020B00963FAA /* Preview Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,11 +580,14 @@ buildActionMask = 2147483647; files = ( 816243612CB9430200506E1C /* CheckboxGroupView.swift in Sources */, + 819A382F2CC6928700A1377F /* ChipGroupView.swift in Sources */, 811DE1352C4D0C7F000DD354 /* ComponentsView.swift in Sources */, 8199A1742C3BF99F00AD650A /* ButtonTypography.swift in Sources */, + 811C684F2CC8027600480F89 /* TextAreaViewModel.swift in Sources */, 816243572CB940F400506E1C /* ChipViewModel.swift in Sources */, 817580EC2C383BA500E45207 /* ButtonViewModel.swift in Sources */, 811DE1422C4D15FA000DD354 /* RadioboxView.swift in Sources */, + 811C68512CC8059800480F89 /* ChipGroupViewModel.swift in Sources */, 811DE1472C4D19D3000DD354 /* ProgressBarView.swift in Sources */, 8162435B2CB9418B00506E1C /* RadioboxGroupView.swift in Sources */, 816243512CB93EC800506E1C /* AvatarGroupViewModel.swift in Sources */, @@ -557,6 +597,7 @@ 811DE1392C4D0EC9000DD354 /* CheckboxView.swift in Sources */, 8199E5A32CBD088C00AF3D22 /* TextFieldView.swift in Sources */, 8162435D2CB9429400506E1C /* RadioboxGroupViewModel.swift in Sources */, + 811C684D2CC8024A00480F89 /* TextAreaView.swift in Sources */, 816243632CB9430E00506E1C /* CheckboxGroupViewModel.swift in Sources */, 816243442CB9057E00506E1C /* AvatarView.swift in Sources */, 24C154062BB3020A00963FAA /* SDDSDemoApp.swift in Sources */, @@ -662,7 +703,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -719,7 +760,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -741,19 +782,21 @@ DEVELOPMENT_TEAM = KDMYYG676V; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_OPTIMIZATION_LEVEL = s; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.sd.SDDSDemoApp; + PRODUCT_BUNDLE_IDENTIFIER = com.sd.SDDSDemo; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -778,19 +821,21 @@ DEVELOPMENT_TEAM = KDMYYG676V; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_OPTIMIZATION_LEVEL = s; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.sd.SDDSDemoApp; + PRODUCT_BUNDLE_IDENTIFIER = com.sd.SDDSDemo; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -798,6 +843,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -925,17 +971,13 @@ isa = XCLocalSwiftPackageReference; relativePath = ../SDDSThemeBuilder/SDDSThemeCore; }; - 816C629C2CB80E2E00352891 /* XCLocalSwiftPackageReference "../SDDSComponents" */ = { + 81CA2C522CC69549002AF2DF /* XCLocalSwiftPackageReference "../SDDSComponents" */ = { isa = XCLocalSwiftPackageReference; relativePath = ../SDDSComponents; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 8159F7252C5CFD9200622836 /* SDDSComponents */ = { - isa = XCSwiftPackageProductDependency; - productName = SDDSComponents; - }; 8159F72B2C5CFDCF00622836 /* SDDSThemeCore */ = { isa = XCSwiftPackageProductDependency; productName = SDDSThemeCore; @@ -960,6 +1002,22 @@ isa = XCSwiftPackageProductDependency; productName = SDDSComponents; }; + 819A382B2CC6926300A1377F /* SDDSComponents */ = { + isa = XCSwiftPackageProductDependency; + productName = SDDSComponents; + }; + 819A38302CC692BD00A1377F /* SDDSComponents */ = { + isa = XCSwiftPackageProductDependency; + productName = SDDSComponents; + }; + 81CA2C532CC69549002AF2DF /* SDDSComponents */ = { + isa = XCSwiftPackageProductDependency; + productName = SDDSComponents; + }; + 81CA2C552CC69549002AF2DF /* SDDSComponentsPreview */ = { + isa = XCSwiftPackageProductDependency; + productName = SDDSComponentsPreview; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 24C153FA2BB3020A00963FAA /* Project object */; diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3ee..a24458f08 100644 --- a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "icon.jpeg", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/icon.jpeg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/icon.jpeg new file mode 100644 index 000000000..43d9543e7 Binary files /dev/null and b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/AppIcon.appiconset/icon.jpeg differ diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxDark.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxDark.svg new file mode 100644 index 000000000..924c0b508 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxDark.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff 1.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff 1.svg new file mode 100644 index 000000000..1dc629733 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff.svg new file mode 100644 index 000000000..1dc629733 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/CheckBoxOff.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/Contents.json new file mode 100644 index 000000000..0e481e499 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxIconOff.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "CheckBoxOff.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "CheckBoxOff 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "CheckBoxDark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/CheckBoxOn(Multiple).svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/CheckBoxOn(Multiple).svg new file mode 100644 index 000000000..f4d23392f --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/CheckBoxOn(Multiple).svg @@ -0,0 +1,4 @@ + + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/Contents.json new file mode 100644 index 000000000..9a9991442 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxMulti.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CheckBoxOn(Multiple).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/CheckBoxOn(Single).svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/CheckBoxOn(Single).svg new file mode 100644 index 000000000..2149d1eb9 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/CheckBoxOn(Single).svg @@ -0,0 +1,4 @@ + + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/Contents.json new file mode 100644 index 000000000..3fcbd4484 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/checkboxOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CheckBoxOn(Single).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/Contents.json new file mode 100644 index 000000000..2a2c492c5 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-24×24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/icon-24\303\22724.pdf" "b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/icon-24\303\22724.pdf" new file mode 100644 index 000000000..37c293462 Binary files /dev/null and "b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/chipIcon.imageset/icon-24\303\22724.pdf" differ diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/Contents.json new file mode 100644 index 000000000..af5833f09 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "RadioBoxOff.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "RadioBoxOff 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "RadioBoxDark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxDark.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxDark.svg new file mode 100644 index 000000000..2faf8e7d2 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxDark.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff 1.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff 1.svg new file mode 100644 index 000000000..1f7789c5e --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff.svg new file mode 100644 index 000000000..1f7789c5e --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxIconOff.imageset/RadioBoxOff.svg @@ -0,0 +1,3 @@ + + + diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/Contents.json b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/Contents.json new file mode 100644 index 000000000..d6f0e440e --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "RadioBoxOn-2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/RadioBoxOn-2.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/RadioBoxOn-2.svg new file mode 100644 index 000000000..b37daef36 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/RadioBoxOn-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOn.imageset/RadioBoxOn.svg b/SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/RadioBoxOn.svg similarity index 100% rename from SDDSComponents/SDDSComponentsPreview/Assets.xcassets/radioboxOn.imageset/RadioBoxOn.svg rename to SDDSDemoApp/SDDSDemoApp/Assets.xcassets/radioboxOn.imageset/RadioBoxOn.svg diff --git a/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarView.swift b/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarView.swift index 9d0bf91ca..e27110f25 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarView.swift @@ -7,7 +7,6 @@ import PhotosUI struct AvatarView: View { @ObservedObject private var viewModel: AvatarViewModel @State private var showImagePicker = false - @State private var isSelectingPlaceholder = false @State private var showGradientPicker = false init(viewModel: AvatarViewModel = AvatarViewModel()) { @@ -55,7 +54,7 @@ struct AvatarView: View { Text("Image") Spacer() Button("Select") { - isSelectingPlaceholder = false + viewModel.isPlaceholder = false showImagePicker.toggle() } .buttonStyle(.borderless) @@ -70,7 +69,7 @@ struct AvatarView: View { Text("Placeholder Image") Spacer() Button("Select") { - isSelectingPlaceholder = true + viewModel.isPlaceholder = true showImagePicker.toggle() } .buttonStyle(.borderless) @@ -103,7 +102,7 @@ struct AvatarView: View { } } .sheet(isPresented: $showImagePicker) { - PhotoPicker(isPlaceholder: isSelectingPlaceholder, viewModel: viewModel) + PhotoPicker(viewModel: viewModel) } .navigationTitle("SDDSAvatar") } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarViewModel.swift index 37a686605..e31a265ff 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/AvatarView/AvatarViewModel.swift @@ -11,6 +11,7 @@ final class AvatarViewModel: ObservableObject { @Published var appearance: AvatarAppearance = .default @Published var size: DefaultAvatarSize = .large @Published var accessibility: AvatarAccessibility = AvatarAccessibility() + @Published var isPlaceholder = false init() {} } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupView.swift b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupView.swift index 3e98e4098..dcc7ee48a 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupView.swift @@ -2,6 +2,7 @@ import SwiftUI import Combine import SDDSComponents import SDDSComponentsPreview +import SDDSServTheme struct CheckboxGroupView: View { @ObservedObject private var viewModel: CheckboxGroupViewModel @@ -13,10 +14,13 @@ struct CheckboxGroupView: View { var body: some View { List { Section { - SDDSCheckboxGroup( - behaviour: viewModel.groupBehaviour, - size: viewModel.size - ) + if let groupBehaviour = viewModel.groupBehaviour { + SDDSCheckboxGroup( + behaviour: groupBehaviour, + size: viewModel.size + ) + .id(UUID()) + } } Section(header: Text("Configuration")) { @@ -30,10 +34,18 @@ struct CheckboxGroupView: View { VStack(alignment: .leading) { Text("Checkbox \(index + 1) Configuration") .font(.headline) - - Toggle("Is Selected", isOn: $viewModel.checkboxViewModels[index].isSelected) - + Toggle("Is Enabled", isOn: $viewModel.checkboxViewModels[index].isEnabled) + + Picker("State", selection: Binding(get: { + viewModel.states[index] ?? .deselected + }, set: { value in + viewModel.update(at: index, to: value) + })) { + ForEach(SelectionControlState.allCases, id: \.self) { state in + Text(state.rawValue).tag(state.rawValue) + } + } HStack { Text("Title") diff --git a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupViewModel.swift index c48fe2fd5..b20314de3 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxGroupView/CheckboxGroupViewModel.swift @@ -4,44 +4,65 @@ import SDDSComponents import SDDSComponentsPreview final class CheckboxGroupViewModel: ObservableObject { - @Published var checkboxViewModels: [CheckboxItemViewModel] + @Published var checkboxViewModels: [CheckboxItemViewModel] = [] @Published var size: SDDSCheckboxGroupSize = .medium - - var groupBehaviour: CheckboxGroupBehaviour { - .default(data: checkboxData) - } + @Published var groupBehaviour: CheckboxGroupBehaviour? + + @Published var states: [Int: SelectionControlState] = [:] var checkboxData: [CheckboxData] { - checkboxViewModels.map { $0.toCheckboxData(with: size.checkboxSize) } + return checkboxViewModels.enumerated().map { index, value in + return value.toCheckboxData(with: size.checkboxSize, state: Binding(get: { [weak self] in + self?.states[index] ?? .deselected + }, set: { [weak self] newState in + self?.states[index] = newState + })) + } } - + init() { - self.checkboxViewModels = (0..<3).map { index in - CheckboxItemViewModel( + self.update() + } + + func update() { + checkboxViewModels = (0..<3).map { [weak self] index in + self?.states[Int(index)] = .deselected + return CheckboxItemViewModel( + id: index, title: "Label \(index + 1)", subtitle: "Description \(index + 1)", - isSelected: false, isEnabled: true ) } + updateGroupBehaviour() + } + + func update(at index: Int, to newState: SelectionControlState) { + states[index] = newState + updateGroupBehaviour() + } + + func updateGroupBehaviour() { + self.groupBehaviour = .default(data: checkboxData, onChange: { [weak self] index, state in + self?.states[index] = state + self?.updateGroupBehaviour() + }) } } -// MARK: - CheckboxItemViewModel - -struct CheckboxItemViewModel { +struct CheckboxItemViewModel: Identifiable { + var id: Int var title: String var subtitle: String - var isSelected: Bool var isEnabled: Bool - func toCheckboxData(with size: SDDSCheckboxSize) -> CheckboxData { + func toCheckboxData(with size: SDDSCheckboxSize, state: Binding) -> CheckboxData { CheckboxData( - state: .constant(isSelected ? .selected : .deselected), + state: state, title: title, subtitle: subtitle, isEnabled: isEnabled, - images: .checkbox, + images: CheckboxView.checkbox, size: size, appearance: .default, accessibility: .init() diff --git a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxView.swift b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxView.swift index 71013e2ec..d8baea21f 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxView.swift @@ -20,7 +20,7 @@ struct CheckboxView: View { title: viewModel.title, subtitle: viewModel.subtitle, isEnabled: viewModel.isEnabled, - images: .checkbox, + images: CheckboxView.checkbox, size: viewModel.size, appearance: viewModel.appearance ) @@ -119,7 +119,7 @@ extension CheckboxView { static var checkbox: SelectionControlStateImages { .init( selectedImage: Image.image("checkboxOn", bundle: Bundle(for: CheckboxViewModel.self)), - deselectedImage: Image.image("checkboxOff", bundle: Bundle(for: CheckboxViewModel.self)), + deselectedImage: Image.image("checkboxIconOff", bundle: Bundle(for: CheckboxViewModel.self)), indeterminateImage: Image.image("checkboxMulti", bundle: Bundle(for: CheckboxViewModel.self)) ) } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxViewModel.swift index 2674f3ce1..69943b676 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/CheckboxView/CheckboxViewModel.swift @@ -14,7 +14,7 @@ final class CheckboxViewModel: ObservableObject { @Published var appearance: CheckboxAppearance = CheckboxAppearance.default // MARK: - Screen properties - @Published var tintColor: ColorStyle = .green + @Published var tintColor: ColorStyle = .none @Published var imageWidth: CGFloat = 20 @Published var imageHeight: CGFloat = 20 @Published var horizontalGap: CGFloat = 8 @@ -30,7 +30,7 @@ final class CheckboxViewModel: ObservableObject { private func observeColors() { $tintColor .sink { [weak self] style in - self?.appearance = self?.appearance.withTintColor(style.color.token) ?? .default + self?.appearance = self?.appearance.withTintColor(style == .none ? nil : style.color.token) ?? .default } .store(in: &cancellables) } @@ -63,7 +63,7 @@ struct CustomCheckboxSize: SelectionControlSizeConfiguration { // Extension for Appearance to update colors extension CheckboxAppearance { - func withTintColor(_ color: ColorToken) -> CheckboxAppearance { + func withTintColor(_ color: ColorToken?) -> CheckboxAppearance { .init( titleTypography: titleTypography, subtitleTypography: subtitleTypography, @@ -76,10 +76,12 @@ extension CheckboxAppearance { } enum ColorStyle: String, CaseIterable { - case blue, green, red, gray, black + case none, blue, green, red, gray, black var color: Color { switch self { + case .none: + return .clear case .blue: return .blue case .green: diff --git a/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupView.swift b/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupView.swift new file mode 100644 index 000000000..cdc016dd9 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupView.swift @@ -0,0 +1,86 @@ +import SwiftUI +import SDDSComponentsPreview +import Combine +import SDDSComponents + +struct ChipGroupView: View { + @ObservedObject private var viewModel: ChipGroupViewModel + @State var size: CGFloat = 0 + + init(viewModel: ChipGroupViewModel = ChipGroupViewModel()) { + self.viewModel = viewModel + } + + var body: some View { + List { + Section { + HStack { + SDDSChipGroup( + data: viewModel.chips, + size: viewModel.chipGroupSize, + height: $size + ) + .frame(height: size) + } + } + + Section { + Picker("Chip Group Alignment", selection: $viewModel.chipGroupSize) { + ForEach(DefaultChipGroupSize.allCases, id: \.self) { size in + Text(size.debugDescription).tag(size.debugDescription) + } + } + Picker("Chip Size", selection: $viewModel.chipSize) { + ForEach(SDDSChipSize.allCases, id: \.self) { size in + Text(size.debugDescription).tag(size.debugDescription) + } + } + Picker("Appearance", selection: $viewModel.appearance) { + ForEach(ChipAppearance.allCases, id: \.self) { appearance in + Text(appearance.name).tag(appearance) + } + } + } + + Section { + ForEach(viewModel.chips.indices, id: \.self) { index in + HStack { + TextField( + "\(viewModel.chips[index].title)", + text: Binding( + get: { + guard !viewModel.chips.isEmpty else { + return "" + } + return viewModel.chips[index].title + }, + set: { newTitle in + viewModel.updateChipTitle(at: index, with: newTitle) + } + ) + ) + Spacer() + Button("Remove") { + viewModel.removeChip(at: index) + } + .foregroundColor(.red) + } + } + HStack { + TextField("Chip Title", text: $viewModel.chipTitle) + Spacer() + Button("Add Chip") { + viewModel.addChip() + } + } + } + } + .navigationTitle("SDDSChipGroup") + } +} + +#Preview { + NavigationView { + ChipGroupView() + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupViewModel.swift new file mode 100644 index 000000000..2cf4aa713 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Views/ChipGroupView/ChipGroupViewModel.swift @@ -0,0 +1,77 @@ +import SwiftUI +import Combine +import SDDSComponents +import SDDSComponentsPreview +import SDDSServTheme + +final class ChipGroupViewModel: ObservableObject { + @Published var chipTitle: String = "" + @Published var chipSize: SDDSChipSize = .medium(.pilled) + @Published var chipGroupSize: DefaultChipGroupSize = .init(alignment: .left) + @Published var chips: [ChipData] = [] + @Published var appearance: ChipAppearance = .default + + private var cancellables: Set = [] + + init() { + observeSizeChange() + } + + private func observeSizeChange() { + $chipSize + .sink { [weak self] value in + self?.updateChips(size: value) + } + .store(in: &cancellables) + } + + private func updateChips(size: SDDSChipSize) { + chips = chips.map { chip in + ChipData( + title: chip.title, + isEnabled: chip.isEnabled, + iconImage: chip.iconImage, + buttonImage: chip.buttonImage, + appearance: chip.appearance, + size: size, + accessibility: chip.accessibility, + removeAction: chip.removeAction + ) + } + } + + func addChip() { + let newChip = ChipData( + title: chipTitle, + isEnabled: true, + iconImage: Image.image("chipIcon"), + buttonImage: Image.image("chipClose"), + appearance: .default, + size: chipSize, + accessibility: ChipAccessibility(), + removeAction: {} + ) + chips.append(newChip) + } + + func updateChipTitle(at index: Int, with newTitle: String) { + guard chips.indices.contains(index) else { return } + var updatedChip = chips[index] + updatedChip = ChipData( + title: newTitle, + isEnabled: updatedChip.isEnabled, + iconImage: updatedChip.iconImage, + buttonImage: updatedChip.buttonImage, + appearance: updatedChip.appearance, + size: updatedChip.size, + accessibility: updatedChip.accessibility, + removeAction: updatedChip.removeAction + ) + chips[index] = updatedChip + } + + func removeChip(at index: Int) { + guard chips.indices.contains(index) else { return } + chips.remove(at: index) + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipView.swift b/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipView.swift index a1c171a44..6fd79b842 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipView.swift @@ -14,19 +14,18 @@ struct ChipView: View { var body: some View { List { Section { - HStack { - Spacer() + ScrollView(.horizontal) { SDDSChip( title: viewModel.title, isEnabled: viewModel.isEnabled, - iconImage: nil, - buttonImage: nil, + iconImage: viewModel.iconImage, + buttonImage: viewModel.buttonImage, appearance: viewModel.appearance, size: viewModel.size, removeAction: viewModel.removeAction ) - Spacer() } + .scrollIndicators(.hidden) } Section(header: Text("Configuration")) { @@ -38,6 +37,8 @@ struct ChipView: View { } Toggle("Enabled", isOn: $viewModel.isEnabled) + Toggle("Icon Image", isOn: $viewModel.iconImageEnabled) + Toggle("Button Image", isOn: $viewModel.buttomImageEnabled) Picker("Size", selection: $viewModel.size) { ForEach(SDDSChipSize.allCases, id: \.self) { size in @@ -47,7 +48,7 @@ struct ChipView: View { Picker("Border Style", selection: $viewModel.borderStyle) { Text("Default").tag(ChipBorderStyle.default(viewModel.size.shapeToken.cornerRadius)) - Text("Rounded").tag(ChipBorderStyle.pilled) + Text("Pilled").tag(ChipBorderStyle.pilled) } Picker("Appearance", selection: $viewModel.appearance) { @@ -58,6 +59,23 @@ struct ChipView: View { } } .navigationTitle("SDDSChip") + .onChange(of: viewModel.iconImageEnabled) { iconImageEnabled in + if iconImageEnabled { + viewModel.setIconImage() + } else { + viewModel.iconImage = nil + } + } + .onChange(of: viewModel.buttomImageEnabled) { buttomImageEnabled in + if buttomImageEnabled { + viewModel.setButtonImage() + } else { + viewModel.buttonImage = nil + } + } + .onChange(of: viewModel.borderStyle) { borderStyle in + viewModel.updateBorderStyle(borderStyle: borderStyle) + } } } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipViewModel.swift index 399d38221..93c855122 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/ChipView/ChipViewModel.swift @@ -7,13 +7,43 @@ import SDDSServTheme final class ChipViewModel: ObservableObject { @Published var title: String = "Chip Title" @Published var isEnabled: Bool = true + @Published var iconImageEnabled: Bool = true + @Published var buttomImageEnabled: Bool = true @Published var size: SDDSChipSize = .medium(.default(8)) @Published var borderStyle: ChipBorderStyle = .default(8) @Published var appearance: ChipAppearance = .default + @Published var iconImage: Image? = nil + @Published var buttonImage: Image? = nil + + init() { + setIconImage() + setButtonImage() + } var removeAction: () -> Void { { print("Chip removed") } } + + func setIconImage() { + iconImage = Image.image("chipIcon") + } + + func setButtonImage() { + buttonImage = Image.image("chipClose") + } + + func updateBorderStyle(borderStyle: ChipBorderStyle) { + switch size { + case .small: + size = .small(borderStyle) + case .medium: + size = .medium(borderStyle) + case .large: + size = .large(borderStyle) + case .extraSmall: + size = .extraSmall(borderStyle) + } + } } // MARK: - ChipAppearance Extensions diff --git a/SDDSDemoApp/SDDSDemoApp/Views/Components/ComponentsView.swift b/SDDSDemoApp/SDDSDemoApp/Views/Components/ComponentsView.swift index 8efbe65e6..ff335d6c2 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/Components/ComponentsView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/Components/ComponentsView.swift @@ -6,12 +6,14 @@ struct ComponentsView: View { ("SDDSAvatarGroup", AnyView(AvatarGroupView())), ("SDDSButton", AnyView(ButtonView())), ("SDDSChip", AnyView(ChipView())), + ("SDDSChipGroup", AnyView(ChipGroupView())), ("SDDSCheckbox", AnyView(CheckboxView())), ("SDDSCheckboxGroup", AnyView(CheckboxGroupView())), ("SDDSProgressBar", AnyView(ProgressBarView())), ("SDDSRadiobox", AnyView(RadioboxView())), ("SDDSRadioboxGroup", AnyView(RadioboxGroupView())), ("SDDSSwitch", AnyView(SwitchView())), + ("SDDSTextArea", AnyView(TextAreaView())), ("SDDSTextField", AnyView(TextFieldView())) ] diff --git a/SDDSDemoApp/SDDSDemoApp/Views/PhotoPicker/PhotoPicker.swift b/SDDSDemoApp/SDDSDemoApp/Views/PhotoPicker/PhotoPicker.swift index 148a7a3d7..e98bbae4e 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/PhotoPicker/PhotoPicker.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/PhotoPicker/PhotoPicker.swift @@ -3,7 +3,6 @@ import PhotosUI import UIKit struct PhotoPicker: UIViewControllerRepresentable { - var isPlaceholder: Bool @ObservedObject var viewModel: AvatarViewModel func makeUIViewController(context: Context) -> PHPickerViewController { @@ -41,9 +40,11 @@ struct PhotoPicker: UIViewControllerRepresentable { return } DispatchQueue.main.async { - if self.parent.isPlaceholder { + if self.parent.viewModel.isPlaceholder { + self.parent.viewModel.image = nil self.parent.viewModel.placeholderImage = .image(Image(uiImage: uiImage)) } else { + self.parent.viewModel.placeholderImage = nil self.parent.viewModel.image = .image(Image(uiImage: uiImage)) } } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxGroupView/RadioboxGroupViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxGroupView/RadioboxGroupViewModel.swift index e8bcb800b..c022c5f54 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxGroupView/RadioboxGroupViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxGroupView/RadioboxGroupViewModel.swift @@ -35,7 +35,7 @@ struct RadioboxItemViewModel { subtitle: subtitle, isSelected: .constant(isSelected), isEnabled: isEnabled, - images: .defaultImages, + images: RadioboxView.radiobox, size: size, appearance: .default, accessibility: .init() diff --git a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxView.swift b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxView.swift index f8e05bbe9..2383030da 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxView.swift @@ -20,7 +20,7 @@ struct RadioboxView: View { title: viewModel.title, subtitle: viewModel.subtitle, isEnabled: viewModel.isEnabled, - images: .defaultImages, + images: RadioboxView.radiobox, size: viewModel.size, appearance: viewModel.appearance ) @@ -107,6 +107,15 @@ struct RadioboxView: View { } } +extension RadioboxView { + static var radiobox: RadioboxImages { + .init( + selectedImage: Image.image("radioboxOn", bundle: Bundle(for: CheckboxViewModel.self)), + deselectedImage: Image.image("radioboxIconOff", bundle: Bundle(for: CheckboxViewModel.self)) + ) + } +} + #Preview { RadioboxView(viewModel: RadioboxViewModel()) } diff --git a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxViewModel.swift index 48b9c3d2a..54d359f54 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/RadioboxView/RadioboxViewModel.swift @@ -12,7 +12,7 @@ final class RadioboxViewModel: ObservableObject { @Published var size: SelectionControlSizeConfiguration = SDDSRadioboxSize.medium @Published var appearance: RadioboxAppearance = .default - @Published var tintColor: ColorStyle = .green + @Published var tintColor: ColorStyle = .none @Published var imageWidth: CGFloat = 20 @Published var imageHeight: CGFloat = 20 @Published var horizontalGap: CGFloat = 8 @@ -28,7 +28,7 @@ final class RadioboxViewModel: ObservableObject { private func observeColors() { $tintColor .sink { [weak self] style in - self?.appearance = self?.appearance.withTintColor(style.color.token) ?? .default + self?.appearance = self?.appearance.withTintColor(style == .none ? nil : style.color.token) ?? .default } .store(in: &cancellables) } @@ -61,7 +61,7 @@ struct CustomRadioboxSize: SelectionControlSizeConfiguration { // Extension for Appearance to update colors extension RadioboxAppearance { - func withTintColor(_ color: ColorToken) -> RadioboxAppearance { + func withTintColor(_ color: ColorToken?) -> RadioboxAppearance { .init( titleTypography: titleTypography, subtitleTypography: subtitleTypography, diff --git a/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaView.swift b/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaView.swift new file mode 100644 index 000000000..1420802b7 --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaView.swift @@ -0,0 +1,145 @@ +import SwiftUI +import Combine +import SDDSComponents +import SDDSComponentsPreview +import SDDSServTheme + +struct TextAreaView: View { + @ObservedObject private var viewModel: TextAreaViewModel + + init(viewModel: TextAreaViewModel = TextAreaViewModel()) { + self.viewModel = viewModel + } + + var body: some View { + List { + Section { + SDDSTextArea( + value: $viewModel.value, + title: viewModel.title, + optionalTitle: viewModel.optionalTitle, + placeholder: viewModel.placeholder, + caption: viewModel.caption, + counter: viewModel.counter, + disabled: viewModel.disabled, + readOnly: viewModel.readOnly, + style: viewModel.style, + labelPlacement: viewModel.labelPlacement, + required: viewModel.required, + requiredPlacement: viewModel.requiredPlacement, + dynamicHeight: viewModel.dynamicHeight, + appearance: viewModel.appearance, + size: viewModel.size, + chipGroupSize: viewModel.size.chipGroupSize, + layout: viewModel.layout, + iconActionViewProvider: iconActionView + ) + } + + Section { + TextField("Title", text: $viewModel.title) + TextField("Optional Title", text: $viewModel.optionalTitle) + TextField("Placeholder", text: $viewModel.placeholder) + TextField("Caption", text: $viewModel.caption) + TextField("Counter", text: $viewModel.counter) + + TextField("Value", text: $viewModel.textValue) + .onChange(of: viewModel.textValue) { newValue in + viewModel.updateValueText(newValue) + } + .onChange(of: viewModel.value) { newValue in + if newValue.text != viewModel.textValue { + viewModel.textValue = newValue.text + } + } + + Toggle("Disabled", isOn: $viewModel.disabled) + Toggle("Read Only", isOn: $viewModel.readOnly) + Toggle("Required", isOn: $viewModel.required) + Toggle("Action", isOn: $viewModel.iconActionViewEnabled) + Toggle("Dynamic Height", isOn: $viewModel.dynamicHeight) + + Picker("Style", selection: $viewModel.style) { + ForEach(SDDSComponents.TextAreaStyle.allCases, id: \.self) { style in + Text(style.rawValue.capitalized).tag(style) + } + } + + Picker("Label Placement", selection: $viewModel.labelPlacement) { + ForEach(TextAreaLabelPlacement.allCases, id: \.self) { placement in + Text(placement.rawValue.capitalized).tag(placement) + } + } + + Picker("Required Placement", selection: $viewModel.requiredPlacement) { + ForEach(TextAreaRequiredPlacement.allCases, id: \.self) { placement in + Text(placement.rawValue.capitalized).tag(placement) + } + } + + Picker("Layout", selection: $viewModel.layout) { + ForEach(TextAreaLayout.allCases, id: \.self) { layout in + Text(layout.rawValue.capitalized).tag(layout) + } + } + + Picker("Size", selection: $viewModel.size) { + ForEach(SDDSTextAreaSize.allCases, id: \.self) { size in + Text(size.rawValue).tag(size.rawValue) + } + } + .onChange(of: viewModel.size) { newValue in + viewModel.updateChipSize(with: newValue) + } + + Button("Add Chip") { + viewModel.addChip() + } + + ForEach(viewModel.chips.indices, id: \.self) { index in + HStack { + TextField( + "Chip \(index + 1)", + text: Binding( + get: { + guard !viewModel.chips.isEmpty else { + return "" + } + return viewModel.chips[index].title + }, + set: { newTitle in + viewModel.updateChipTitle(at: index, with: newTitle) + } + ) + ) + Spacer() + Button("Remove") { + viewModel.removeChip(at: index) + } + .foregroundColor(.red) + } + } + } + } + .navigationTitle("SDDSTextArea") + } + + private var iconActionView: ViewProvider? { + if viewModel.iconActionViewEnabled { + ViewProvider( + Image.image("textFieldIconAction") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + ) + } else { + nil + } + } +} + +#Preview { + NavigationView { + TextAreaView() + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaViewModel.swift new file mode 100644 index 000000000..e69cc647e --- /dev/null +++ b/SDDSDemoApp/SDDSDemoApp/Views/TextAreaView/TextAreaViewModel.swift @@ -0,0 +1,147 @@ +import SwiftUI +import Combine +import SDDSComponents +import SDDSComponentsPreview +import SDDSServTheme + +final class TextAreaViewModel: ObservableObject { + @Published var value: TextAreaValue = .single("") + @Published var textValue: String = "" + @Published var title: String = "Title" + @Published var optionalTitle: String = "Optional" + @Published var placeholder: String = "Placeholder" + @Published var caption: String = "Caption" + @Published var counter: String = "Counter" + @Published var disabled: Bool = false + @Published var readOnly: Bool = false + @Published var required: Bool = false + @Published var iconActionViewEnabled: Bool = true + @Published var dynamicHeight: Bool = false + + @Published var style: SDDSComponents.TextAreaStyle = .default + @Published var labelPlacement: TextAreaLabelPlacement = .outer + @Published var requiredPlacement: TextAreaRequiredPlacement = .left + @Published var layout: TextAreaLayout = .default + + @Published var size: SDDSTextAreaSize = .medium + @Published var appearance: TextAreaAppearance = .defaultAppearance + + var chips: [ChipData] { + get { + if case let .multiple(_, chips) = value { + return chips + } + return [] + } + set { + value = .multiple(textValue, newValue) + } + } + + func updateValueText(_ newText: String) { + if chips.isEmpty { + value = .single(newText) + } else { + value = .multiple(newText, chips) + } + } + + func addChip() { + let chipSize = mapTextAreaSizeToChipSize(size) + let id = UUID() + let newChip = ChipData( + id: id, + title: "Chip \( chips.count + 1)", + isEnabled: true, + iconImage: nil, + buttonImage: Image.image("textFieldChipIcon"), + appearance: .textField, + size: chipSize, + accessibility: ChipAccessibility(), + removeAction: { [weak self] in + self?.removeChip(with: id) + } + ) + chips.append(newChip) + } + + func updateChipTitle(at index: Int, with newTitle: String) { + guard chips.indices.contains(index) else { return } + var updatedChip = chips[index] + updatedChip = ChipData( + title: newTitle, + isEnabled: updatedChip.isEnabled, + iconImage: updatedChip.iconImage, + buttonImage: updatedChip.buttonImage, + appearance: updatedChip.appearance, + size: updatedChip.size, + accessibility: updatedChip.accessibility, + removeAction: updatedChip.removeAction + ) + chips[index] = updatedChip + } + + func updateChipSize(with size: SDDSTextAreaSize) { + guard !chips.isEmpty else { + return + } + let chipSize = mapTextAreaSizeToChipSize(size) + chips = chips.map { chip in + ChipData( + id: chip.id, + title: chip.title, + isEnabled: chip.isEnabled, + iconImage: chip.iconImage, + buttonImage: chip.buttonImage, + appearance: chip.appearance, + size: chipSize, + accessibility: chip.accessibility, + removeAction: chip.removeAction + ) + } + } + + func removeChip(at index: Int) { + guard chips.indices.contains(index) else { return } + chips.remove(at: index) + if chips.isEmpty { + value = .single(textValue) + } + } + + func removeChip(with id: UUID) { + chips = chips.filter { $0.id != id } + if chips.isEmpty { + value = .single(textValue) + } + } + + private func mapTextAreaSizeToChipSize(_ textFieldSize: SDDSTextAreaSize) -> TextAreaChipSize { + switch textFieldSize { + case .large: + return .large + case .medium: + return .medium + case .small: + return .small + case .extraSmall: + return .extraSmall + } + } +} + +extension SDDSTextAreaSize: CaseIterable { + public static var allCases: [SDDSTextAreaSize] { + [.large, .medium, .small, .extraSmall] + } +} + +extension TextAreaAppearance: CaseIterable { + public static var allCases: [TextAreaAppearance] { + [.defaultAppearance] + } + + public var name: String { + return "Default" + } +} diff --git a/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldView.swift b/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldView.swift index dd40f0500..d00466a16 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldView.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldView.swift @@ -30,7 +30,9 @@ struct TextFieldView: View { requiredPlacement: viewModel.requiredPlacement, appearance: viewModel.appearance, size: viewModel.size, - layout: viewModel.layout + layout: viewModel.layout, + iconViewProvider: iconView, + iconActionViewProvider: iconActionView ) } @@ -55,6 +57,8 @@ struct TextFieldView: View { Toggle("Disabled", isOn: $viewModel.disabled) Toggle("Read Only", isOn: $viewModel.readOnly) Toggle("Required", isOn: $viewModel.required) + Toggle("Icon", isOn: $viewModel.iconViewEnabled) + Toggle("Action", isOn: $viewModel.iconActionViewEnabled) Picker("Style", selection: $viewModel.style) { ForEach(SDDSComponents.TextFieldStyle.allCases, id: \.self) { style in @@ -123,6 +127,32 @@ struct TextFieldView: View { } .navigationTitle("SDDSTextField") } + + private var iconActionView: ViewProvider? { + if viewModel.iconActionViewEnabled { + ViewProvider( + Image.image("textFieldIconAction") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + ) + } else { + nil + } + } + + private var iconView: ViewProvider? { + if viewModel.iconViewEnabled { + ViewProvider( + Image.image("textFieldIcon") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + ) + } else { + nil + } + } } #Preview { diff --git a/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldViewModel.swift b/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldViewModel.swift index 48cae52b0..65c047298 100644 --- a/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldViewModel.swift +++ b/SDDSDemoApp/SDDSDemoApp/Views/TextFieldView/TextFieldViewModel.swift @@ -16,6 +16,8 @@ final class TextFieldViewModel: ObservableObject { @Published var disabled: Bool = false @Published var readOnly: Bool = false @Published var required: Bool = false + @Published var iconViewEnabled: Bool = true + @Published var iconActionViewEnabled: Bool = true @Published var style: SDDSComponents.TextFieldStyle = .default @Published var labelPlacement: TextFieldLabelPlacement = .outer