The Colors panel joins the palette — the combo ratified, and each anchor composes the halves it needs
Four rulings close Redesign Contradiction 3452893f (2026-08-06): the in-app escape hatch is ratified in full, reversing 2026-07-29's palette-only rule — the combo's Other… opens the system Colors panel, a pick landing on a palette color stores the name, anything else the hex. Free-picked colors change no contrast story: they land on the same runtime ink computation hand-written hex always got (10 amended to say so; no warning surface is owed). Anchor ownership: the card sidebar's background story is the combo alone — the well grid's background half stays with the other anchors (StyleEditorView gains showsBackground beside showsSymbols; the popover's symbol half already went to its inline SymbolPicker). Quick-style recents stay palette-vocabulary — a panel pick never enters them. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -35,9 +35,16 @@ struct CardStyleSection: View {
|
||||
/// **This window's undo stack** (13-native-undo.md ▸ Rules ▸ two levels): a colour or symbol
|
||||
/// chosen here is a gesture *issued in this window*, so its step joins the window's session and
|
||||
/// reaches board history only inside the coarse close step. The shared editor takes it as an
|
||||
/// anchor's parameter, exactly as it takes the layout.
|
||||
/// anchor's parameter, exactly as it takes the layout — and so does the background combo below,
|
||||
/// for the same reason.
|
||||
let undo: CardWindowUndo
|
||||
|
||||
/// The trailing debounce on a live colour-panel drag (`ColorComboView`'s `onPanelChange`,
|
||||
/// opened from the combo's **Other…** row): cancelled and replaced on every tick, so only the
|
||||
/// value the user is still on ~400ms after the last one actually reaches disk. One task for the
|
||||
/// section's one combo.
|
||||
@State private var backgroundPanelCommit: Task<Void, Never>?
|
||||
|
||||
/// The live body metric, read here rather than passed in — `CardAttachmentsSection`'s pattern,
|
||||
/// so every section in this sidebar derives its geometry the same way.
|
||||
private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
|
||||
@@ -56,6 +63,10 @@ struct CardStyleSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
|
||||
CardSidebarSectionHeader(title: "Style")
|
||||
backgroundComboRow
|
||||
// Symbols only: the combo row above is this sidebar's whole background story
|
||||
// (03 ▸ Styling ▸ Controls, the 2026-08-06 anchor-ownership rule) — the well grid's
|
||||
// background half stays with the other anchors.
|
||||
StyleEditorView(
|
||||
store: store,
|
||||
recents: recents,
|
||||
@@ -64,11 +75,82 @@ struct CardStyleSection: View {
|
||||
contentWidth: CardWindowMetrics.sidebarContentWidth(bodyPointSize: pointSize),
|
||||
bodyPointSize: pointSize
|
||||
),
|
||||
undo: undo
|
||||
undo: undo,
|
||||
showsBackground: false
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
// MARK: - Background combo
|
||||
|
||||
/// The labeled **Background** row, above the well grid — a narrower, single-value alternative
|
||||
/// to it (`ColorCombo.swift`'s own doc comment): an inspector row, caption leading and a
|
||||
/// compact combo trailing, the arrangement every Xcode inspector uses for exactly this control.
|
||||
/// The combo takes just over half the row rather than filling it — sized off the same metric
|
||||
/// the sidebar's own width comes from, so the pair holds its proportions at every text size.
|
||||
private var backgroundComboRow: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text("Background")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
ColorComboView(
|
||||
role: .background,
|
||||
value: currentBackground,
|
||||
isEnabled: !store.isReadOnly,
|
||||
onChange: { commitBackground($0) },
|
||||
onPanelChange: { debounceBackground($0) }
|
||||
)
|
||||
.frame(width: CardWindowMetrics.sidebarContentWidth(bodyPointSize: pointSize) * 0.55)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
/// The card's `background` field, exactly as written — malformed reads as its raw text, missing
|
||||
/// reads `nil`, both `StyleFieldState.written`'s own rule (`StyleModel.swift`). The **raw**
|
||||
/// string, never a resolved colour: `ColorComboModel`'s matching needs the bytes, not what they
|
||||
/// render as.
|
||||
private var currentBackground: String? {
|
||||
let subject = store.styleSubjects(of: Self.target(forCard: cardID)).first
|
||||
return StyleFieldState.written(subject?.background ?? .missing)
|
||||
}
|
||||
|
||||
/// A discrete pick — commits immediately. `nil` removes; a name from `Palette.backgrounds` goes
|
||||
/// through `StyleCommand.apply` so it feeds `StyleRecents` exactly like a well click would
|
||||
/// ("updated on every background application from any anchor", `StyleEditor.swift`); anything
|
||||
/// else — the dynamic current-value row re-affirming a foreign name or a custom hex — writes
|
||||
/// directly, since it is not the "palette pick" recents was ever meant to remember.
|
||||
private func commitBackground(_ newValue: String?) {
|
||||
let target = Self.target(forCard: cardID)
|
||||
guard let newValue else {
|
||||
store.applyStyle(to: target, background: .remove, icon: .keep, on: undo)
|
||||
return
|
||||
}
|
||||
if Palette.backgrounds.contains(where: { $0.name == newValue }) {
|
||||
StyleCommand.apply(background: .set(newValue), to: target, in: store, recents: recents, on: undo)
|
||||
} else {
|
||||
store.applyStyle(to: target, background: .set(newValue), icon: .keep, on: undo)
|
||||
}
|
||||
}
|
||||
|
||||
/// One tick of a live colour-panel drag: cancels whatever commit was pending and schedules a new
|
||||
/// one ~400ms out, so a drag writes once it settles rather than on every pixel it passes through.
|
||||
/// Never routed through `StyleCommand.apply` — a drag that passes through a palette-exact hex
|
||||
/// mid-gesture must not spam the recents row the way a deliberate pick would.
|
||||
private func debounceBackground(_ newValue: String?) {
|
||||
backgroundPanelCommit?.cancel()
|
||||
let target = Self.target(forCard: cardID)
|
||||
backgroundPanelCommit = Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(400))
|
||||
guard !Task.isCancelled else { return }
|
||||
if let newValue {
|
||||
store.applyStyle(to: target, background: .set(newValue), icon: .keep, on: undo)
|
||||
} else {
|
||||
store.applyStyle(to: target, background: .remove, icon: .keep, on: undo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
Reference in New Issue
Block a user