macOS client parity review
This commit is contained in:
@@ -88,6 +88,7 @@ private final class CardModel {
|
||||
let context: [MDBlock]
|
||||
let firstChoice: [String: Int]
|
||||
var busy = false
|
||||
@ObservationIgnored var shown: Set<String> = []
|
||||
|
||||
init(_ item: InboxItem, _ store: Store, _ overlays: Overlays) {
|
||||
self.item = item
|
||||
@@ -144,6 +145,17 @@ private final class CardModel {
|
||||
store.drafts?.setCheckedAnswer(item.id, options.filter { now.contains($0) })
|
||||
}
|
||||
|
||||
func pick(_ text: String) {
|
||||
if busy {
|
||||
return
|
||||
}
|
||||
if multiple {
|
||||
toggle(text, !checked.contains(text))
|
||||
return
|
||||
}
|
||||
submit(InboxResponseCreate(itemId: item.id, _type: .option, options: [text]))
|
||||
}
|
||||
|
||||
func submit(_ response: InboxResponseCreate, withImages: Bool = false) {
|
||||
if busy {
|
||||
return
|
||||
@@ -225,7 +237,7 @@ private struct InboxCard: View {
|
||||
}
|
||||
|
||||
private func move(_ model: CardModel, _ by: Int) -> KeyPress.Result {
|
||||
var stops = model.options
|
||||
var stops = model.firstChoice.filter { model.shown.contains($0.key) }.sorted { $0.value < $1.value }.map(\.key)
|
||||
if !model.editorOpen {
|
||||
stops.append(otherStop)
|
||||
}
|
||||
@@ -240,6 +252,13 @@ private struct InboxCard: View {
|
||||
private struct CardBody: View {
|
||||
let model: CardModel
|
||||
var focus: FocusState<String?>.Binding
|
||||
@ViewState private var wide = true
|
||||
|
||||
private var contextCtx: MDContext {
|
||||
MDContext(taskId: model.item.taskId, choice: { md, first in
|
||||
AnyView(ChoiceBox(model: model, item: md, first: first, ctx: self.contextCtx, focus: focus))
|
||||
}, gridColumns: wide ? 4 : 2)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let p = model.payload
|
||||
@@ -247,10 +266,9 @@ private struct CardBody: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
switch item.kind {
|
||||
case .question:
|
||||
MarkdownView(blocks: model.context, ctx: MDContext(taskId: item.taskId, choice: { md in
|
||||
AnyView(ChoiceBox(model: model, item: md, focus: focus))
|
||||
}))
|
||||
.padding(.vertical, 10)
|
||||
MarkdownView(blocks: model.context, ctx: contextCtx)
|
||||
.onGeometryChange(for: Bool.self) { $0.size.width > 480 } action: { wide = $0 }
|
||||
.padding(.vertical, 10)
|
||||
MarkdownView(blocks: parseMarkdown(jsString(p["question"] ?? "")), ctx: MDContext(taskId: item.taskId))
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 4)
|
||||
@@ -308,6 +326,8 @@ private struct WhyLine: View {
|
||||
private struct ChoiceBox: View {
|
||||
let model: CardModel
|
||||
let item: MDItem
|
||||
let first: Bool
|
||||
let ctx: MDContext
|
||||
var focus: FocusState<String?>.Binding
|
||||
@Environment(Store.self) private var store
|
||||
@ViewState private var hover = false
|
||||
@@ -321,7 +341,7 @@ private struct ChoiceBox: View {
|
||||
Text("•")
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
InlineView(inlines: parseInline(item.raw), ctx: MDContext(taskId: model.item.taskId))
|
||||
ItemBody(item: item, ctx: MDContext(taskId: model.item.taskId))
|
||||
ItemBody(item: item, ctx: ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +353,7 @@ private struct ChoiceBox: View {
|
||||
let lit = (hover && !model.multiple) || focused
|
||||
let hasDetail = !item.inline.isEmpty || !item.blocks.isEmpty
|
||||
let on = model.checked.contains(text)
|
||||
let keys: Set<KeyEquivalent> = model.multiple ? [.space] : [.return, .space]
|
||||
return VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
if model.multiple {
|
||||
@@ -342,29 +363,42 @@ private struct ChoiceBox: View {
|
||||
.tint(Palette.accent)
|
||||
.disabled(model.busy)
|
||||
}
|
||||
InlineView(inlines: parseInline(text), ctx: MDContext(taskId: model.item.taskId))
|
||||
.font(Fonts.font(13, weight: 600))
|
||||
InlineView(inlines: parseInline(text), ctx: MDContext(taskId: model.item.taskId, weight: 600))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.allowsHitTesting(false)
|
||||
if rec {
|
||||
Text("recommended").font(Fonts.font(11)).foregroundStyle(Palette.ok)
|
||||
Text("recommended")
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.ok)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(lit ? Palette.accentTint : rec ? Palette.ok.opacity(0.12) : Palette.surface)
|
||||
.background {
|
||||
(lit ? Palette.accentTint : rec ? Palette.ok.opacity(0.12) : Palette.surface)
|
||||
.onTapGesture {
|
||||
model.pick(text)
|
||||
}
|
||||
}
|
||||
if hasDetail {
|
||||
ItemBody(item: MDItem(inline: item.inline, blocks: item.blocks), ctx: MDContext(taskId: model.item.taskId))
|
||||
ItemBody(item: MDItem(inline: item.inline, blocks: item.blocks), ctx: ctx)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.allowsHitTesting(false)
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(Palette.raised)
|
||||
.background {
|
||||
Palette.raised
|
||||
.onTapGesture {
|
||||
model.pick(text)
|
||||
}
|
||||
}
|
||||
.environment(\.choiceTap, { [model] in
|
||||
model.pick(text)
|
||||
})
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(lit ? Palette.accent : rec ? Palette.ok.opacity(0.55) : Palette.border))
|
||||
.overlay {
|
||||
@@ -377,26 +411,18 @@ private struct ChoiceBox: View {
|
||||
.focusable(!model.busy)
|
||||
.focusEffectDisabled()
|
||||
.focused(focus, equals: text)
|
||||
.onTapGesture {
|
||||
pick(text)
|
||||
}
|
||||
.onKeyPress(keys: [.return, .space]) { _ in
|
||||
pick(text)
|
||||
.onKeyPress(keys: keys) { _ in
|
||||
model.pick(text)
|
||||
return .handled
|
||||
}
|
||||
.onHover { hover = $0 }
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
private func pick(_ text: String) {
|
||||
if model.busy {
|
||||
return
|
||||
.onAppear {
|
||||
model.shown.insert(text)
|
||||
}
|
||||
if model.multiple {
|
||||
model.toggle(text, !model.checked.contains(text))
|
||||
return
|
||||
.onDisappear {
|
||||
model.shown.remove(text)
|
||||
}
|
||||
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .option, options: [text]))
|
||||
.padding(.top, first ? 0 : 6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,9 +639,7 @@ private struct Attachments: View {
|
||||
}
|
||||
|
||||
private func code(_ s: String) -> Text {
|
||||
var a = AttributedString(s)
|
||||
a.backgroundColor = Palette.surface
|
||||
return Text(a)
|
||||
Text(verbatim: s)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -628,15 +652,18 @@ private struct Attachments: View {
|
||||
Text("diff \(code(a.ref ?? ""))").textSelection(.enabled)
|
||||
case .image:
|
||||
let src = (a.image?.isEmpty == false ? a.image : a.path) ?? ""
|
||||
let caption = a.caption ?? ""
|
||||
if let source = worktreeImageSource(src, taskId) {
|
||||
ByreImage(source: source, src: src)
|
||||
} else if !src.isEmpty {
|
||||
InlineView(inlines: [.link(href: src, [.text(caption.isEmpty ? src : caption)])], ctx: MDContext(taskId: taskId))
|
||||
}
|
||||
case .diagram:
|
||||
DiagramView(source: a.mermaid ?? "")
|
||||
case .task:
|
||||
let id = a.taskId ?? ""
|
||||
Button("task \(id)") {
|
||||
store.focusTask(id)
|
||||
store.route("#/task/\(id)")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(store.taskColor(id) ?? Palette.accent)
|
||||
|
||||
Reference in New Issue
Block a user