Build the styling system and shared style editor
One style-editor component, anchor-agnostic: a background grid (None well plus the 12 palette colors) and a curated symbol grid (the pathfinder's five-dozen set, leading well removing the icon key for the level default), selection-aware across cards, lanes, and the board itself. Batch edits compute per-dimension state — uniform, mixed (no well selected), or an off-palette value labeled verbatim outside the grids — and choosing a well applies to the whole target set as one write bracket, skipping no-ops per field. The popover tracks its target set live per the freshly ratified rule: targets re-resolve by UUID on every reload, a vanished target leaves the set, an emptied set dismisses the editor, and nothing ever silently retargets to the board. Anchors landing now: Board > Style (Opt-Cmd-S) and the card/lane context menus, which also carry the quick-style recents row (app-wide, persisted, capped at six, None never recorded) and the lane's width control twinning the menu chords. The styling system's other two renders arrive with it: a lane's background paints the C7 top-edge band, the board's paints the window content background — malformed values paint nothing and stay byte-identical on disk. 31 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -323,6 +323,18 @@ public final class TransientBoardState {
|
||||
/// out from under a commit that was about to read its draft.
|
||||
public private(set) var renameEditor: RenameEditor?
|
||||
|
||||
/// The open Style… popover's target, or `nil` when no popover is open (03-board-ui.md § Styling
|
||||
/// ▸ Controls). See `StyleEditorSession` for the lifecycle it implements and why it lives here
|
||||
/// rather than in a view.
|
||||
///
|
||||
/// **Not an inline editor**, deliberately: it is not a title field, it does not hold the text
|
||||
/// domain's keyboard, and `isEditingInline` must stay the answer to "may board commands run" —
|
||||
/// Style… itself is *disabled* while an inline editor is open, so the two never coexist anyway.
|
||||
///
|
||||
/// `private(set)` for `renameEditor`'s reason: its two transitions are the methods below, and a
|
||||
/// session assignable from anywhere could be re-aimed behind the reload rule's back.
|
||||
public private(set) var styleEditor: StyleEditorSession?
|
||||
|
||||
/// Whether a title editor holds focus — 04-interactions.md's **focused-editor rule** as one
|
||||
/// boolean: "while an inline title editor — rename or the new-card placeholder — is focused,
|
||||
/// board-scoped menu commands (Delete, New Card, Paste, Move, Style, …) disable via menu
|
||||
@@ -466,6 +478,26 @@ public final class TransientBoardState {
|
||||
renameEditor = nil
|
||||
}
|
||||
|
||||
// MARK: - The style editor's lifecycle
|
||||
|
||||
/// Opens the Style… popover on `target` — the menu item's call and both context menus'
|
||||
/// (03-board-ui.md § Styling ▸ Controls).
|
||||
///
|
||||
/// Replacing any session already open, for the inline editors' reason turned inside out: there is
|
||||
/// one popover, so a second Style… is the first one being re-aimed by a fresh gesture. Unlike
|
||||
/// `beginRename`/`beginPlaceholder` it does **not** clear the inline editors — it cannot coexist
|
||||
/// with one (Style… disables while an editor is focused), so clearing them here would be a rule
|
||||
/// about a state that menu validation already rules out.
|
||||
public func beginStyleEditor(for target: StyleTarget) {
|
||||
styleEditor = StyleEditorSession(target: target)
|
||||
}
|
||||
|
||||
/// Closes the popover — the user dismissing it, and the anchor's response to a session the
|
||||
/// reload rule emptied.
|
||||
public func discardStyleEditor() {
|
||||
styleEditor = nil
|
||||
}
|
||||
|
||||
// MARK: - Reload
|
||||
|
||||
/// The one reload hook: re-grounds every piece of this container on a freshly applied snapshot.
|
||||
@@ -508,6 +540,13 @@ public final class TransientBoardState {
|
||||
/// current universe does not have. It is not an `ItemReferenceSet` only because it is one
|
||||
/// optional rather than a set on a side — the rule it obeys is the same one.
|
||||
///
|
||||
/// **The style editor tracks its target set live** (03-board-ui.md § Styling ▸ Controls,
|
||||
/// settled): a member that vanishes or flips liveness leaves the set — so the editor's
|
||||
/// mixed-state display recomputes off the survivors — and a set emptied by a foreign reload
|
||||
/// clears the session, which is how "the popover dismisses when it empties" reaches the screen.
|
||||
/// It never becomes a board session on the way; `StyleEditorSession.resolved(against:)` owns
|
||||
/// both halves.
|
||||
///
|
||||
/// `searchQuery` and `isTrashVisible` are deliberately not mentioned below. Neither references
|
||||
/// an item, so no snapshot can invalidate either — the query's *results* change with every
|
||||
/// snapshot, which is precisely why the results are not stored here.
|
||||
@@ -516,6 +555,7 @@ public final class TransientBoardState {
|
||||
dragMembers = dragMembers.resolved(against: snapshot)
|
||||
pendingCut = pendingCut.resolved(against: snapshot)
|
||||
newCardPlaceholder = resolvedPlaceholder(against: snapshot)
|
||||
styleEditor = styleEditor?.resolved(against: snapshot)
|
||||
|
||||
// One universe computed once and asked three questions — the rename target's liveness, the
|
||||
// last-active lane's, and (via the placeholder above, which asks its own way) the anchor's.
|
||||
|
||||
Reference in New Issue
Block a user