import SwiftUI struct DictionaryView: View { @StateObject private var viewModel = DictionaryViewModel() private let panelBackground = Color(red: 0.96, green: 0.97, blue: 0.98) var body: some View { GeometryReader { proxy in let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52) let dynamicBottomPadding = min(max(proxy.size.height * 0.06, 28), 56) ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { header .padding(.bottom, 28) contentCard .padding(.bottom, 24) if viewModel.hasResult { resultCard } } .frame(maxWidth: 1200, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.horizontal, dynamicHorizontalPadding) .padding(.top, AppTheme.brandLabelTopInset) .padding(.bottom, dynamicBottomPadding) } } .onChange(of: viewModel.query) { newValue in if newValue.count > DictionaryViewModel.maxQueryLength { viewModel.query = String(newValue.prefix(DictionaryViewModel.maxQueryLength)) } if viewModel.hasResult { viewModel.resetResult() } } } private var header: some View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 8) { Text("Dictionary") .font(.system(size: 30, weight: .bold)) .foregroundStyle(AppTheme.textPrimary) HStack(spacing: 0) { Text("Find ") .foregroundStyle(AppTheme.textSecondary) Text("meaning") .fontWeight(.semibold) .foregroundStyle(AppTheme.teal) Text(". Learn ") .foregroundStyle(AppTheme.textSecondary) Text("words") .fontWeight(.semibold) .foregroundStyle(AppTheme.teal) Text(". Expand your ") .foregroundStyle(AppTheme.textSecondary) Text("vocabulary") .fontWeight(.semibold) .foregroundStyle(AppTheme.teal) Text(".") .foregroundStyle(AppTheme.textSecondary) } .font(.system(size: 15)) } Spacer() IconButton { SettingsGearIcon() } action: {} .accessibilityLabel("Settings") } } private var contentCard: some View { VStack(alignment: .leading, spacing: 18) { Image("YourContentLogo") .resizable() .aspectRatio(contentMode: .fit) .frame(height: 52, alignment: .topLeading) .frame(height: 46, alignment: .topLeading) .clipped() .accessibilityLabel("Your Content") searchField if let errorMessage = viewModel.errorMessage { errorBanner(errorMessage) } if viewModel.isLoading { loadingState } else if !viewModel.hasResult { emptyState } if !viewModel.recentSearches.isEmpty { recentSearchesSection } } .padding(22) .background(AppTheme.cardBackground) .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)) .overlay( RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius) .stroke(AppTheme.border, lineWidth: 1) ) .shadow(color: AppTheme.cardShadow, radius: 16, y: 6) } private var searchField: some View { VStack(alignment: .leading, spacing: 12) { Text("Look up a word") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) HStack(spacing: 10) { HStack(spacing: 10) { Image(systemName: "magnifyingglass") .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppTheme.textMuted) TextField("Type a word and press Return...", text: $viewModel.query, onCommit: viewModel.lookUp) .textFieldStyle(.plain) .font(.system(size: 16)) .foregroundStyle(AppTheme.textPrimary) } .padding(.horizontal, 16) .padding(.vertical, 14) .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)) .overlay( RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius) .stroke(AppTheme.inputBorder, lineWidth: 1) ) Button(action: viewModel.lookUp) { HStack(spacing: 8) { Image(systemName: "book.fill") .font(.system(size: 13, weight: .semibold)) Text("Look Up") .font(.system(size: 14, weight: .semibold)) } .foregroundStyle(.white) .padding(.horizontal, 20) .padding(.vertical, 14) .background(viewModel.canLookUp ? AppTheme.primaryGradient : LinearGradient(colors: [AppTheme.border, AppTheme.border], startPoint: .leading, endPoint: .trailing)) .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)) } .buttonStyle(.plain) .disabled(!viewModel.canLookUp) } HStack(spacing: 8) { ToolbarButton(title: "Paste Word", iconName: "doc.on.clipboard") { viewModel.pasteWord() } Spacer() ClearButton { viewModel.clearSearch() } } } } private var emptyState: some View { VStack(spacing: 12) { Image(systemName: "text.book.closed") .font(.system(size: 34)) .foregroundStyle(AppTheme.teal.opacity(0.45)) Text("Search for any English word to see definitions, examples, and synonyms.") .font(.system(size: 14)) .foregroundStyle(AppTheme.textSecondary) .multilineTextAlignment(.center) .frame(maxWidth: 420) } .frame(maxWidth: .infinity) .padding(.vertical, 28) .padding(.horizontal, 20) .background(panelBackground) .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)) .overlay( RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius) .stroke(AppTheme.border.opacity(0.6), lineWidth: 1) ) } private var loadingState: some View { HStack(spacing: 12) { ProgressView() .controlSize(.small) Text("Looking up definition...") .font(.system(size: 14)) .foregroundStyle(AppTheme.textSecondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(18) .background(panelBackground) .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)) } private func errorBanner(_ message: String) -> some View { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 14)) .foregroundStyle(AppTheme.clearForeground) Text(message) .font(.system(size: 14)) .foregroundStyle(AppTheme.clearForeground) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .padding(.vertical, 12) .background(AppTheme.clearBackground) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(AppTheme.clearBorder, lineWidth: 1) ) } private var recentSearchesSection: some View { VStack(alignment: .leading, spacing: 10) { Text("Recent Searches") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) LazyVGrid( columns: [GridItem(.adaptive(minimum: 72), spacing: 8)], alignment: .leading, spacing: 8 ) { ForEach(viewModel.recentSearches, id: \.self) { word in Button { viewModel.lookUpRecent(word) } label: { Text(word) .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppTheme.teal) .padding(.horizontal, 12) .padding(.vertical, 7) .background(AppTheme.tealLight) .clipShape(Capsule()) } .buttonStyle(.plain) } } } } private var resultCard: some View { Group { if let entry = viewModel.entry { VStack(alignment: .leading, spacing: 20) { wordHeader(entry) ForEach(entry.meanings) { meaning in meaningSection(meaning) } } .padding(22) .background(AppTheme.cardBackground) .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)) .overlay( RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius) .stroke(AppTheme.border, lineWidth: 1) ) .shadow(color: AppTheme.cardShadow, radius: 16, y: 6) } } } private func wordHeader(_ entry: DictionaryEntry) -> some View { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading, spacing: 6) { Text(entry.word) .font(.system(size: 34, weight: .bold)) .foregroundStyle(AppTheme.textPrimary) if let phonetic = entry.phonetic { Text(phonetic) .font(.system(size: 16)) .foregroundStyle(AppTheme.textSecondary) } } Spacer() Button { viewModel.playPronunciation(for: entry) } label: { HStack(spacing: 8) { Image(systemName: "speaker.wave.2.fill") .font(.system(size: 13, weight: .semibold)) Text("Listen") .font(.system(size: 13, weight: .semibold)) } .foregroundStyle(AppTheme.teal) .padding(.horizontal, 14) .padding(.vertical, 9) .background(AppTheme.tealLight) .clipShape(RoundedRectangle(cornerRadius: 10)) } .buttonStyle(.plain) .accessibilityLabel("Play pronunciation") } } private func meaningSection(_ meaning: DictionaryMeaning) -> some View { VStack(alignment: .leading, spacing: 14) { PartOfSpeechBadge(title: meaning.partOfSpeech) VStack(alignment: .leading, spacing: 12) { ForEach(Array(meaning.definitions.enumerated()), id: \.element.id) { index, definition in definitionRow(number: index + 1, definition: definition) } } if !meaning.synonyms.isEmpty { relatedWordsSection(title: "Synonyms", words: meaning.synonyms) } if !meaning.antonyms.isEmpty { relatedWordsSection(title: "Antonyms", words: meaning.antonyms) } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .background(panelBackground) .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)) .overlay( RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius) .stroke(AppTheme.border.opacity(0.6), lineWidth: 1) ) } private func definitionRow(number: Int, definition: DictionaryDefinition) -> some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top, spacing: 10) { Text("\(number).") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(AppTheme.teal) .frame(width: 20, alignment: .leading) Text(definition.text) .font(.system(size: 14)) .foregroundStyle(AppTheme.textPrimary) .fixedSize(horizontal: false, vertical: true) } if let example = definition.example { Text("\"\(example)\"") .font(.system(size: 13)) .italic() .foregroundStyle(AppTheme.textMuted) .padding(.leading, 30) } } } private func relatedWordsSection(title: String, words: [String]) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) LazyVGrid( columns: [GridItem(.adaptive(minimum: 72), spacing: 8)], alignment: .leading, spacing: 8 ) { ForEach(words, id: \.self) { word in Button { viewModel.lookUpRecent(word) } label: { Text(word) .font(.system(size: 12, weight: .medium)) .foregroundStyle(AppTheme.textSecondary) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Color.white) .clipShape(Capsule()) .overlay( Capsule() .stroke(AppTheme.border.opacity(0.7), lineWidth: 1) ) } .buttonStyle(.plain) } } } .padding(.top, 4) } } private struct PartOfSpeechBadge: View { let title: String var body: some View { Text(title.capitalized) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AppTheme.teal) .padding(.horizontal, 12) .padding(.vertical, 6) .background(AppTheme.tealLight) .clipShape(Capsule()) } }