No forgetting, deleting or clearing on a single press, anywhere

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 00:41:39 -04:00
parent 88371d19f0
commit f9e5d3f470
8 changed files with 267 additions and 84 deletions
@@ -30,13 +30,54 @@ Rectangle {
readonly property bool isDefault: root.theme.id === (root.theme.scheme === "light"
? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark)
// Delete arms in place rather than through ConfirmAction. The ✕ badge is a
// 22px corner of a card whose whole body is the apply target roughly ten
// pixels away -- there is nowhere in that caption to put a Keep beside a
// Delete it without the pair becoming the card. So the badge itself is the
// two presses: the first turns it into a labelled danger pill and reddens
// the card's border, the second deletes. The token is ShellState's, the
// same one ConfirmAction uses, so one confirm is armed app-wide.
readonly property string removeActionId: "theme-card-delete:" + root.theme.id
readonly property bool removeArmed: ShellState.armedConfirm === root.removeActionId
function pressRemove(): void {
if (!root.removeArmed) {
ShellState.armedConfirm = root.removeActionId;
return;
}
root.disarmRemove();
root.removed();
}
function disarmRemove(): void {
if (root.removeArmed)
ShellState.armedConfirm = "";
}
// Armed, the card body is the way out: a press anywhere on it takes the
// arming back instead of applying the theme, because the miss this guards
// against is a press landing next to the badge rather than on it.
function press(): void {
if (root.removeArmed) {
root.disarmRemove();
return;
}
root.chosen();
}
// A card that scrolls away, or a gallery that rebuilds, must not leave the
// token claiming a theme nothing is showing.
Component.onDestruction: root.disarmRemove()
implicitHeight: preview.height + caption.height + 4
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.fg, 0.04)
border.width: 2
border.color: root.activeFocus
? Theme.accentSecondary
: (root.active ? Theme.accent : Theme.alpha(Theme.fg, 0.12))
border.color: root.removeArmed
? Theme.danger
: (root.activeFocus
? Theme.accentSecondary
: (root.active ? Theme.accent : Theme.alpha(Theme.fg, 0.12)))
clip: true
activeFocusOnTab: true
@@ -44,11 +85,13 @@ Rectangle {
Accessible.name: root.theme.name
Accessible.description: (root.theme.scheme === "light" ? "Light theme" : "Dark theme")
+ (root.active ? ", selected" : "")
+ (root.removeArmed ? ", delete is armed — press this card to keep it" : "")
Accessible.focusable: true
Accessible.focused: root.activeFocus
Keys.onReturnPressed: root.chosen()
Keys.onSpacePressed: root.chosen()
Keys.onReturnPressed: root.press()
Keys.onSpacePressed: root.press()
Keys.onEscapePressed: root.disarmRemove()
Rectangle {
id: preview
@@ -148,36 +191,52 @@ Rectangle {
// Only saved themes can be removed, and the affordance is a
// separate focus stop so Tab never lands on "delete" when the
// person meant "select".
//
// At rest the badge is neutral: danger marks the press that
// confirms, never the one that initiates, which is SettingsButton's
// contract and holds here too. Armed, it stops being a glyph and
// becomes a labelled pill wide enough to read.
Rectangle {
id: remove
anchors.verticalCenter: parent.verticalCenter
visible: !root.shipped
width: 22
width: root.removeArmed ? removeLabel.implicitWidth + 16 : 22
height: 22
radius: 7
color: removeHover.hovered
? Theme.alpha(Theme.danger, 0.22)
: Theme.alpha(Theme.fg, 0.07)
border.width: remove.activeFocus ? 2 : 1
border.color: remove.activeFocus
color: root.removeArmed
? Theme.alpha(Theme.danger, 0.3)
: (removeHover.hovered
? Theme.alpha(Theme.fg, 0.14)
: Theme.alpha(Theme.fg, 0.07))
border.width: root.removeArmed || remove.activeFocus ? 2 : 1
border.color: root.removeArmed || remove.activeFocus
? Theme.danger : Theme.alpha(Theme.fg, 0.08)
activeFocusOnTab: visible
Accessible.role: Accessible.Button
Accessible.name: "Delete " + root.theme.name
Accessible.name: root.removeArmed
? "Delete " + root.theme.name + ", press again to confirm"
: "Delete " + root.theme.name
Accessible.description: root.removeArmed
? "This theme's palette, terminal colours and effects go with it"
: ""
Accessible.focusable: true
Accessible.focused: remove.activeFocus
Keys.onReturnPressed: root.removed()
Keys.onSpacePressed: root.removed()
Keys.onReturnPressed: root.pressRemove()
Keys.onSpacePressed: root.pressRemove()
Keys.onEscapePressed: root.disarmRemove()
Text {
id: removeLabel
anchors.centerIn: parent
text: "✕"
color: Theme.danger
text: root.removeArmed ? "Delete it" : "✕"
color: root.removeArmed ? Theme.danger : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: root.removeArmed ? Font.DemiBold : Font.Normal
}
HoverHandler {
@@ -190,7 +249,7 @@ Rectangle {
// on its way out.
TapHandler {
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: root.removed()
onTapped: root.pressRemove()
}
}
}
@@ -202,6 +261,6 @@ Rectangle {
}
TapHandler {
onTapped: root.chosen()
onTapped: root.press()
}
}