Appearance now opens on Themes: light and dark side by side, each remembering its own choice, over galleries of ten shipped themes — Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and Everforest in both modes. A theme is a complete palette: the catalog lives in themes.json, Theme.qml reads every color token from the active record, and one render pipeline carries it to kitty, tmux, btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme editor builds new ones from four wells — wheel, hex, or eyedropper — with derived surfaces, a saturation slider, debounced fine-tune, and effects that save with the theme. Custom edits finally keep GNOME's accent, kitty's border, and hyprlock in sync. Wallpapers can be video: mpvpaper per output, hardware-decoded, muted and looped, supervised and respawned. Panama owns the pausing — games, battery, and a bar pill for right now — because the compositor rebuilds full-screen blur for every frame a video wallpaper draws. The lock screen gets a still frame. Titlebars stop lying. GNOME apps get close-only on your chosen side, the maximize and double-click settings are gone, the Settings window obeys the same rules, and its titlebar can be turned off entirely. Typography becomes five labeled dropdowns instead of a wall of samples. Contracts updated and written throughout (165 now); per the redesign workflow none were executed — the full sweep runs once at the end. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
208 lines
6.6 KiB
QML
208 lines
6.6 KiB
QML
// One theme, drawn as the desktop it produces.
|
|
//
|
|
// A row of flat swatches would name a theme's colors without showing what any
|
|
// of them does. So the card is a miniature of the real thing: the ground, a
|
|
// window sitting on it wearing the accent as a border, the two text weights
|
|
// the shell actually reads with, and only then the four remaining colors as
|
|
// dots. Clicking applies it immediately -- there is no preview mode here,
|
|
// because the whole shell is the preview.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
// A catalog record or a saved profile -- the same shape either way.
|
|
required property var theme
|
|
|
|
signal chosen
|
|
signal removed
|
|
|
|
// A custom profile saved before palettes existed carries none of its own
|
|
// and inherits its scheme's default, exactly as ThemeProfiles resolves it.
|
|
readonly property var palette: root.theme.palette
|
|
?? ThemeCatalog.defaultPalette(root.theme.scheme)
|
|
|
|
readonly property bool active: root.theme.id === ThemeProfiles.activeId
|
|
readonly property bool shipped: root.theme.shipped === true
|
|
readonly property bool isDefault: root.theme.id === (root.theme.scheme === "light"
|
|
? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark)
|
|
|
|
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))
|
|
clip: true
|
|
activeFocusOnTab: true
|
|
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: root.theme.name
|
|
Accessible.description: (root.theme.scheme === "light" ? "Light theme" : "Dark theme")
|
|
+ (root.active ? ", selected" : "")
|
|
Accessible.focusable: true
|
|
Accessible.focused: root.activeFocus
|
|
|
|
Keys.onReturnPressed: root.chosen()
|
|
Keys.onSpacePressed: root.chosen()
|
|
|
|
Rectangle {
|
|
id: preview
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 2
|
|
height: 84
|
|
color: root.palette.bg
|
|
border.width: 0
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: 11
|
|
radius: 7
|
|
color: root.palette.bgPanel
|
|
border.width: 2
|
|
border.color: root.theme.accent
|
|
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 8
|
|
spacing: 7
|
|
|
|
Rectangle {
|
|
width: parent.width * 0.64
|
|
height: 4
|
|
radius: 2
|
|
color: root.palette.fg
|
|
border.width: 0
|
|
}
|
|
|
|
Row {
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: [root.theme.accent, root.theme.secondary,
|
|
root.palette.fgDim, root.palette.fgMuted]
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
width: 9
|
|
height: 9
|
|
radius: 5
|
|
color: modelData
|
|
border.width: 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: caption
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: preview.bottom
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 10
|
|
height: 30
|
|
|
|
Text {
|
|
id: name
|
|
anchors.left: parent.left
|
|
anchors.right: marks.left
|
|
anchors.rightMargin: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.theme.name + (root.isDefault ? " · today's default" : "")
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.DemiBold
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Row {
|
|
id: marks
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 6
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: root.active
|
|
text: "✓"
|
|
color: Theme.accent
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
// 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".
|
|
Rectangle {
|
|
id: remove
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: !root.shipped
|
|
width: 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
|
|
? Theme.danger : Theme.alpha(Theme.fg, 0.08)
|
|
activeFocusOnTab: visible
|
|
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: "Delete " + root.theme.name
|
|
Accessible.focusable: true
|
|
Accessible.focused: remove.activeFocus
|
|
|
|
Keys.onReturnPressed: root.removed()
|
|
Keys.onSpacePressed: root.removed()
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "✕"
|
|
color: Theme.danger
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
|
|
HoverHandler {
|
|
id: removeHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
// An exclusive grab, so the delete press does not also reach
|
|
// the card's own tap handler underneath and select the theme
|
|
// on its way out.
|
|
TapHandler {
|
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
onTapped: root.removed()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: cardHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: root.chosen()
|
|
}
|
|
}
|