// // ActionGrid.swift // 划词侠 plugin // // 2x2 grid of the four primary actions plus the segmented sub-options // for translate-target and rewrite-tone. Tappable even when input is // empty — store decides whether to short-circuit. // import SwiftUI struct ActionGrid: View { @ObservedObject var store: ClipperStore var body: some View { VStack(spacing: 10) { HStack(spacing: 10) { actionCard( icon: "globe", title: "翻译", subtitle: targetSubLabel, accent: ClipperTheme.cyan ) { store.runAction(.translate(target: store.translateTarget)) } actionCard( icon: "list.bullet.rectangle", title: "总结", subtitle: "3 条要点", accent: ClipperTheme.lime ) { store.runAction(.summarize) } } HStack(spacing: 10) { actionCard( icon: "bubble.left.and.bubble.right", title: "改语气", subtitle: store.rewriteTone.label, accent: ClipperTheme.amber ) { store.runAction(.rewrite(tone: store.rewriteTone)) } actionCard( icon: "envelope", title: "写邮件回复", subtitle: "礼貌专业", accent: Color(red: 0xFF/255, green: 0x9E/255, blue: 0xC4/255) ) { store.runAction(.emailReply) } } // Segmented controls for the two actions that have options. VStack(spacing: 6) { segmented( title: "翻译目标", options: ClipperAction.TranslateTarget.allCases.map { ($0.label, $0) }, selection: $store.translateTarget ) segmented( title: "改写语气", options: ClipperAction.ToneOption.allCases.map { ($0.label, $0) }, selection: $store.rewriteTone ) } } } private var targetSubLabel: String { "译为 " + store.translateTarget.label } // MARK: subviews private func actionCard(icon: String, title: String, subtitle: String, accent: Color, action: @escaping () -> Void) -> some View { Button(action: action) { VStack(alignment: .leading, spacing: 6) { Image(systemName: icon) .font(.system(size: 18, weight: .semibold)) .foregroundColor(accent) Text(title) .font(.system(size: 14, weight: .semibold)) .foregroundColor(ClipperTheme.fgPrimary) Text(subtitle) .font(.system(size: 10.5)) .foregroundColor(ClipperTheme.fg55) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 12) .fill(ClipperTheme.overlay06) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(ClipperTheme.overlay08, lineWidth: 0.5) ) ) } .buttonStyle(.plain) } private func segmented( title: String, options: [(String, T)], selection: Binding ) -> some View { HStack(spacing: 6) { Text(title) .font(.system(size: 10.5, weight: .medium)) .foregroundColor(ClipperTheme.fg45) .frame(width: 56, alignment: .leading) HStack(spacing: 4) { ForEach(options, id: \.1) { (label, value) in let isSel = selection.wrappedValue == value Button { selection.wrappedValue = value } label: { Text(label) .font(.system(size: 11, weight: .semibold)) .foregroundColor(isSel ? Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255) : ClipperTheme.fg70) .padding(.horizontal, 10) .frame(height: 22) .background( Capsule().fill(isSel ? ClipperTheme.lime : ClipperTheme.overlay04) ) } .buttonStyle(.plain) } Spacer(minLength: 0) } } } }