Redesign recipe creation and edit view with Form-based layout and URL import

Replace the single "+" button with a 2-option menu (Create New Recipe / Import
from URL) across RecipeTabView, RecipeListView, and AllRecipesListView. Add
ImportURLSheet for server-side recipe import with loading and error states.

Completely redesign edit mode to use a native Form layout with inline editing
for all sections (metadata, duration, ingredients, instructions, tools,
nutrition) instead of the previous sheet-based EditableListView approach. Move
delete action from edit toolbar to view mode context menu. Add recipe image
display to the edit form.

Refactor RecipeListView and AllRecipesListView to use closure-based callbacks
instead of Binding<Bool> for the create/import actions. Add preloadedRecipeDetail
support to RecipeView.ViewModel for imported recipes.

Add DE/ES/FR translations for all new UI strings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 03:29:20 +01:00
parent 98c82dc537
commit 1536174586
13 changed files with 1085 additions and 444 deletions

View File

@@ -87,6 +87,52 @@ struct RecipeMetadataSection: View {
}
}
// MARK: - Recipe Edit Metadata Section (Form-based)
struct RecipeEditMetadataSection: View {
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel
var categories: [String] {
appState.categories.map { $0.name }
}
var body: some View {
Section("Details") {
Picker("Category", selection: $viewModel.observableRecipeDetail.recipeCategory) {
Text("None").tag("")
ForEach(categories, id: \.self) { item in
Text(item).tag(item)
}
}
.pickerStyle(.menu)
Stepper("Servings: \(viewModel.observableRecipeDetail.recipeYield)", value: $viewModel.observableRecipeDetail.recipeYield, in: 1...99)
Button {
viewModel.presentKeywordSheet = true
} label: {
HStack {
Text("Keywords")
.foregroundStyle(.primary)
Spacer()
if viewModel.observableRecipeDetail.keywords.isEmpty {
Text("None")
.foregroundStyle(.secondary)
} else {
Text(viewModel.observableRecipeDetail.keywords.joined(separator: ", "))
.foregroundStyle(.secondary)
.lineLimit(1)
}
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, Collection: Sequence>: View where Collection.Element == Item {
@Binding var isPresented: Bool
@Binding var value: Item