@@ -26,23 +26,8 @@ struct CategoryManageView: View {
2626 }
2727 . margins ( . horizontal, 16 )
2828 . margins ( . vertical, 8 ) ,
29- footer: UIHostingConfiguration {
30- Button {
31- store. send ( . tapAddUserCategory)
32- } label: {
33- Label (
34- String ( localized: " todo_manage_add_category_title " , bundle: PresentationResources . bundle) ,
35- systemImage: " plus.circle.fill "
36- )
37- . font ( . headline)
38- . foregroundStyle ( Color . onPrimaryContainer)
39- . frame ( maxWidth: . infinity)
40- . padding ( . vertical, 10 )
41- }
42- . adaptiveButtonStyle ( shape: RoundedRectangle ( cornerRadius: 16 ) , color: . primaryContainer)
43- }
44- . margins ( . horizontal, 16 )
45- . margins ( . vertical, 24 ) ,
29+ footer: UIHostingConfiguration { EmptyView ( ) }
30+ . margins ( . vertical, 8 ) ,
4631 row: { item in
4732 UIHostingConfiguration {
4833 CategoryManageRow (
@@ -60,8 +45,25 @@ struct CategoryManageView: View {
6045 }
6146 )
6247 }
63- . ignoresSafeArea ( edges: . bottom)
6448 . safeAreaInset ( edge: . top, spacing: 0 ) { toolBar }
49+ . safeAreaInset ( edge: . bottom, spacing: 0 ) {
50+ Button {
51+ store. send ( . tapAddUserCategory)
52+ } label: {
53+ Label (
54+ String ( localized: " todo_manage_add_category_title " , bundle: PresentationResources . bundle) ,
55+ systemImage: " plus.circle.fill "
56+ )
57+ . font ( . headline)
58+ . foregroundStyle ( Color . white)
59+ . frame ( maxWidth: . infinity)
60+ . padding ( . vertical, 10 )
61+ }
62+ . adaptiveButtonStyle ( shape: RoundedRectangle ( cornerRadius: 16 ) , color: . accent)
63+ . padding ( . horizontal)
64+ . padding ( . vertical, 12 )
65+ . background ( Color . surface, ignoresSafeAreaEdges: . bottom)
66+ }
6567 . background ( Color . appBackground. ignoresSafeArea ( ) )
6668 . sheet (
6769 item: $store. scope ( state: \. categorySheet, action: \. categorySheet)
@@ -110,7 +112,7 @@ private struct CategoryManageRow: View {
110112
111113 var body : some View {
112114 if item. isUserCategory {
113- content
115+ CategoryManageRowContent ( item : item , onToggle : onToggle )
114116 . background ( Color . surface)
115117 . itemActions {
116118 ItemActionButton (
@@ -126,11 +128,16 @@ private struct CategoryManageRow: View {
126128 )
127129 }
128130 } else {
129- content
131+ CategoryManageRowContent ( item : item , onToggle : onToggle )
130132 }
131133 }
134+ }
135+
136+ private struct CategoryManageRowContent : View {
137+ let item : TodoCategoryItem
138+ let onToggle : ( ) -> Void
132139
133- private var content : some View {
140+ var body : some View {
134141 HStack ( spacing: 12 ) {
135142 Image ( systemName: item. symbolName)
136143 . font ( . headline)
@@ -154,60 +161,189 @@ private struct CategoryManageRow: View {
154161 }
155162}
156163
164+ private let categoryColorHexValues = [
165+ " #FF3B30 " , " #FF9500 " , " #34C759 " , " #1EA0A6 " , " #007AFF " , " #AF52DE " , " #FF2D55 " , " #8E8E93 "
166+ ]
167+
157168private struct CategoryManageSheet : View {
158169 @Bindable var store : Store < CategoryManageFeature . CategorySheetState , CategoryManageFeature . Action . CategorySheet >
159170
160171 var body : some View {
161- NavigationStack {
162- Form {
163- Section {
164- HStack ( spacing: 8 ) {
165- TextField (
166- " " ,
167- text: $store. category. name,
168- prompt: Text ( store. placeholder) . foregroundStyle ( . secondary)
169- )
170- . frame ( height: UIFont . preferredFont ( forTextStyle: . body) . lineHeight)
171-
172- Text ( store. categoryNameCountText)
173- . font ( . footnote)
174- . foregroundStyle ( . secondary)
175- . monospacedDigit ( )
172+ ScrollView {
173+ LazyVStack ( spacing: 24 ) {
174+ nameCard
175+ colorCard
176+ previewCard
177+ }
178+ . padding ( . horizontal, 16 )
179+ . padding ( . bottom, 16 )
180+ }
181+ . contentMargins ( . top, 16 , for: . scrollContent)
182+ . safeAreaInset ( edge: . top, spacing: 0 ) { toolBar }
183+ . safeAreaInset ( edge: . bottom, spacing: 0 ) {
184+ Button {
185+ store. send ( . tapSaveButton)
186+ } label: {
187+ Text ( store. submitTitle)
188+ . font ( . headline)
189+ . foregroundStyle ( Color . white)
190+ . frame ( maxWidth: . infinity)
191+ . padding ( . vertical, 10 )
192+ }
193+ . adaptiveButtonStyle ( shape: RoundedRectangle ( cornerRadius: 16 ) , color: . accent)
194+ . disabled ( !store. canSubmitUserCategory)
195+ . padding ( . horizontal)
196+ . padding ( . vertical, 12 )
197+ . background ( Color . surface, ignoresSafeAreaEdges: . bottom)
198+ }
199+ . background ( Color . appBackground. ignoresSafeArea ( ) )
200+ . presentationDragIndicator ( . visible)
201+ }
202+
203+ private var toolBar : some View {
204+ ZStack {
205+ Text ( store. navigationTitle)
206+ . font ( . headline)
207+ HStack {
208+ Button {
209+ store. send ( . tapCloseButton)
210+ } label: {
211+ if #available( iOS 26 . 0 , * ) {
212+ Image ( systemName: " xmark " )
213+ } else {
214+ Text ( String ( localized: " common_close " , bundle: PresentationResources . bundle) )
176215 }
177216 }
217+ . topBarButtonStyle ( )
178218
179- Section {
180- ColorPicker ( selection: $store. category. colorHex. colorValue, supportsOpacity: false ) {
181- Text ( store. category. colorHex. isEmpty ? " # " : store. category. colorHex)
182- . overlay ( alignment: . bottom) {
183- Rectangle ( )
184- . frame ( height: 1 )
185- . offset ( y: 1 )
186- }
187- . foregroundStyle ( store. category. colorHex. colorValue)
188- . onTapGesture {
189- store. send ( . tapRandomColorButton)
190- }
191- }
192- . pickerStyle ( . palette)
219+ Spacer ( )
220+ }
221+ }
222+ . padding ( . horizontal, 16 )
223+ . padding ( . vertical, 12 )
224+ . background ( Color . appBackground, ignoresSafeAreaEdges: . top)
225+ }
226+
227+ private var nameCard : some View {
228+ VStack ( alignment: . leading, spacing: 12 ) {
229+ Text ( String ( localized: " todo_manage_name_placeholder " , bundle: PresentationResources . bundle) )
230+ . font ( . headline)
231+ . padding ( . horizontal, 16 )
232+
233+ HStack ( spacing: 8 ) {
234+ TextField (
235+ " " ,
236+ text: $store. category. name,
237+ prompt: Text ( store. placeholder) . foregroundStyle ( . secondary)
238+ )
239+ . font ( . body)
240+
241+ Text ( store. categoryNameCountText)
242+ . font ( . footnote)
243+ . foregroundStyle ( Color . textSecondary)
244+ . monospacedDigit ( )
245+ }
246+ . padding ( )
247+ . background ( Color . surface, in: . rect( cornerRadius: 16 ) )
248+ . tint ( Color . accent)
249+
250+ Text ( store. nameMessage)
251+ . font ( . footnote)
252+ . foregroundStyle ( store. isDuplicatedName ? Color . danger : Color . textSecondary)
253+ . padding ( . horizontal, 16 )
254+ }
255+ }
256+
257+ private var colorCard : some View {
258+ VStack ( alignment: . leading, spacing: 12 ) {
259+ HStack {
260+ Text ( String ( localized: " todo_manage_color " , bundle: PresentationResources . bundle) )
261+ . font ( . headline)
262+
263+ Spacer ( )
264+
265+ Button {
266+ store. send ( . tapRandomColorButton)
267+ } label: {
268+ Label (
269+ String ( localized: " todo_manage_random_color " , bundle: PresentationResources . bundle) ,
270+ systemImage: " shuffle "
271+ )
272+ . font ( . subheadline)
273+ . foregroundStyle ( Color . accent)
193274 }
275+ . buttonStyle ( . plain)
194276 }
195- . navigationTitle ( store. navigationTitle)
196- . navigationBarTitleDisplayMode ( . inline)
197- . toolbar {
198- ToolbarItem ( placement: . navigationBarLeading) {
199- Button ( String ( localized: " common_close " , bundle: PresentationResources . bundle) ) {
200- store. send ( . tapCloseButton)
277+ . padding ( . horizontal, 16 )
278+
279+ VStack ( spacing: 16 ) {
280+ Image ( systemName: store. todoCategoryItem. symbolName)
281+ . font ( . largeTitle)
282+ . frame ( width: 88 , height: 88 )
283+ . iconStyle ( color: store. todoCategoryItem. color, in: Circle ( ) )
284+
285+ HStack ( spacing: 4 ) {
286+ ForEach ( categoryColorHexValues, id: \. self) { colorHex in
287+ let color = Color ( hexString: colorHex) ?? . gray
288+ let isSelected = store. category. colorHex. caseInsensitiveCompare ( colorHex) == . orderedSame
289+
290+ Button {
291+ store. send ( . binding( . set( \. category. colorHex, colorHex) ) )
292+ } label: {
293+ Circle ( )
294+ . fill ( color)
295+ . padding ( 3 )
296+ . overlay {
297+ if isSelected {
298+ Image ( systemName: " checkmark " )
299+ . font ( . caption. bold ( ) )
300+ . foregroundStyle ( . white)
301+ Circle ( )
302+ . strokeBorder ( color, lineWidth: 2 )
303+ }
304+ }
305+ . aspectRatio ( 1 , contentMode: . fit)
306+ }
307+ . buttonStyle ( . plain)
308+ . frame ( maxWidth: . infinity)
201309 }
310+
311+ ColorPicker (
312+ store. category. colorHex. isEmpty ? " # " : store. category. colorHex,
313+ selection: $store. category. colorHex. colorValue,
314+ supportsOpacity: false
315+ )
316+ . labelsHidden ( )
317+ . frame ( maxWidth: . infinity)
202318 }
319+ }
320+ . padding ( 16 )
321+ . frame ( maxWidth: . infinity)
322+ . background ( Color . surface, in: . rect( cornerRadius: 16 ) )
323+ }
324+ }
203325
204- ToolbarItem ( placement: . navigationBarTrailing) {
205- Button ( store. submitTitle) {
206- store. send ( . tapSaveButton)
207- }
208- . disabled ( !store. canSubmitUserCategory)
326+ private var previewCard : some View {
327+ VStack ( alignment: . leading, spacing: 12 ) {
328+ Text ( String ( localized: " todo_preview " , bundle: PresentationResources . bundle) )
329+ . font ( . headline)
330+ . padding ( . horizontal, 16 )
331+
332+ VStack ( alignment: . leading, spacing: 12 ) {
333+ Text ( String ( localized: " todo_manage_preview_description " , bundle: PresentationResources . bundle) )
334+ . font ( . footnote)
335+ . foregroundStyle ( Color . textSecondary)
336+
337+ HStack ( spacing: 12 ) {
338+ CategoryManageRowContent ( item: store. todoCategoryItem, onToggle: { } )
339+ Image ( systemName: " line.3.horizontal " )
340+ . foregroundStyle ( Color . textTertiary)
209341 }
342+ . allowsHitTesting ( false )
210343 }
344+ . padding ( 16 )
345+ . frame ( maxWidth: . infinity, alignment: . leading)
346+ . background ( Color . surface, in: . rect( cornerRadius: 16 ) )
211347 }
212348 }
213349}
0 commit comments