diff --git a/AGENTS.md b/AGENTS.md index 3c1f642..4e345c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ shell.qml Entry point: Scope { Bar {}; Notification {} } │ ├── Cava.qml Spawns `cava`, parses stdout into `values[]` │ └── Notifications.qml NotificationServer: popups, DND flag, history ├── config/ QML Singletons for configuration/theming -│ ├── Appearance.qml colors, font, margin/radius/spacing/padding/duration +│ ├── Theme.qml colors, font, margin/radius/spacing/padding/duration, per-widget sizes │ └── BarLayout.qml Which widgets go in left/center/right bar sections ├── ui/ Shared reusable primitives │ ├── BarButton.qml Themed button used by bar widgets @@ -66,6 +66,6 @@ shell.qml Entry point: Scope { Bar {}; Notification {} } - Start every QML file with `pragma ComponentBehavior: Bound` where bindings reference outer ids. - Indentation is inconsistent across the repo (2-space in newer UI/window files, 4-space in some services); match the surrounding file when editing. -- Prefer `readonly property` for derived values; keep widgets presentational and pull all theme values from `Appearance.*` rather than hardcoding colors/sizes/fonts. +- Prefer `readonly property` for derived values; keep widgets presentational and pull all theme values from `Theme.*` rather than hardcoding colors/sizes/fonts. - Access asset paths via `Quickshell.shellPath(...)` (see `Battery.qml`, `NotificationCard.qml`) so paths work regardless of install location. - Commit messages follow Conventional Commits style (`feat(notifications): ...`, `refactor(bar): ...`). diff --git a/config/Appearance.qml b/config/Appearance.qml deleted file mode 100644 index 0b17d9a..0000000 --- a/config/Appearance.qml +++ /dev/null @@ -1,35 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell - -Singleton { - id: root - property QtObject colors - property QtObject font - - property int margin - property int radius - property int spacing - property int padding - property int duration - - margin: 8 - radius: 8 - spacing: 8 - padding: 8 - duration: 150 - - font: QtObject { - property string family: "FiraCode Nerd Font Mono SemBd" - property int pointSize: 9 - } - - colors: QtObject { - property color accent: "#6791c9" - property color foreground: "#ffffff" - property color background: "#101213" - property color inActive: "#1b1d1e" - } -} diff --git a/config/Theme.qml b/config/Theme.qml new file mode 100644 index 0000000..32b21a4 --- /dev/null +++ b/config/Theme.qml @@ -0,0 +1,60 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + property int margin: 8 + property int radius: 8 + property int spacing: 8 + property int padding: 8 + property int duration: 150 + + font: QtObject { + property string family: "FiraCode Nerd Font Mono SemBd" + property int pointSize: 9 + readonly property int body: pointSize - 1 + readonly property int caption: pointSize - 2 + } + + colors: QtObject { + property color accent: "#6791c9" + property color foreground: "#ffffff" + property color background: "#101213" + property color inActive: "#1b1d1e" + } + + bar: QtObject { + property int thickness: 10 + property int height: 50 + } + + workspaces: QtObject { + property int height: 15 + property int radius: 1000 + property real activeWidthRatio: 2.3 + } + + cava: QtObject { + property int width: 96 + property int height: 32 + property int innerMargin: 8 + property int barSpacing: 3 + property int barMinHeight: 2 + } + + tray: QtObject { + property int iconSize: 16 + } + + notification: QtObject { + property int width: 420 + property int iconSize: 40 + property int borderWidth: 1 + property int textSpacing: 2 + property real bodyOpacity: 0.8 + } +} diff --git a/modules/bar/CavaVisualizer.qml b/modules/bar/CavaVisualizer.qml index b5ae8ba..f48be89 100644 --- a/modules/bar/CavaVisualizer.qml +++ b/modules/bar/CavaVisualizer.qml @@ -11,18 +11,18 @@ Rectangle { property real maxValue: 100 readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001) - implicitWidth: 96 - implicitHeight: 32 - radius: Appearance.radius - color: Appearance.colors.inActive + implicitWidth: Theme.cava.width + implicitHeight: Theme.cava.height + radius: Theme.radius + color: Theme.colors.inActive visible: shouldVisualize Row { id: barRow anchors.fill: parent - anchors.margins: 8 - spacing: 3 + anchors.margins: Theme.cava.innerMargin + spacing: Theme.cava.barSpacing Repeater { model: root.barCount @@ -34,15 +34,15 @@ Rectangle { readonly property real value: Cava.values[index] ?? 0 width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount - height: Math.max(2, (bar.value / root.maxValue) * barRow.height) + height: Math.max(Theme.cava.barMinHeight, (bar.value / root.maxValue) * barRow.height) anchors.bottom: parent.bottom radius: bar.width / 2 - color: Appearance.colors.accent + color: Theme.colors.accent antialiasing: true Behavior on height { NumberAnimation { - duration: Appearance.duration + duration: Theme.duration } } } diff --git a/modules/bar/SystemTrayWidget.qml b/modules/bar/SystemTrayWidget.qml index e8ba870..093767d 100644 --- a/modules/bar/SystemTrayWidget.qml +++ b/modules/bar/SystemTrayWidget.qml @@ -8,14 +8,12 @@ import qs.ui BarButton { id: root - property int iconSize: 16 - hoverHighlight: false pointerCursor: false visible: SystemTray.items.values.length > 0 contentItem: Row { - spacing: Appearance.spacing + spacing: Theme.spacing Repeater { model: SystemTray.items.values @@ -24,8 +22,8 @@ BarButton { required property var modelData source: modelData.icon - width: root.iconSize - height: root.iconSize + width: Theme.tray.iconSize + height: Theme.tray.iconSize } } } diff --git a/modules/bar/Workspaces.qml b/modules/bar/Workspaces.qml index b4003d1..8f468ed 100644 --- a/modules/bar/Workspaces.qml +++ b/modules/bar/Workspaces.qml @@ -9,7 +9,7 @@ Row { property int count: 7 - spacing: Appearance.spacing + spacing: Theme.spacing Repeater { model: root.count @@ -21,15 +21,15 @@ Row { property var ws: Hyprland.workspaces.values.find(w => w.id === pill.index + 1) property bool isActive: Hyprland.focusedWorkspace?.id === (pill.index + 1) - radius: 1000 - implicitHeight: 15 - implicitWidth: pill.isActive ? pill.implicitHeight * 2.3 : pill.implicitHeight + radius: Theme.workspaces.radius + implicitHeight: Theme.workspaces.height + implicitWidth: pill.isActive ? pill.implicitHeight * Theme.workspaces.activeWidthRatio : pill.implicitHeight color: { if (handler.hovered) - return Appearance.colors.accent; + return Theme.colors.accent; if (pill.isActive || pill.ws) - return Appearance.colors.accent; - return Appearance.colors.inActive; + return Theme.colors.accent; + return Theme.colors.inActive; } MouseArea { @@ -45,13 +45,13 @@ Row { Behavior on color { ColorAnimation { - duration: Appearance.duration + duration: Theme.duration } } Behavior on implicitWidth { NumberAnimation { - duration: Appearance.duration + duration: Theme.duration easing.type: Easing.OutQuad } } diff --git a/modules/osd/NotificationCard.qml b/modules/osd/NotificationCard.qml index 863773f..bbf508c 100644 --- a/modules/osd/NotificationCard.qml +++ b/modules/osd/NotificationCard.qml @@ -26,12 +26,12 @@ Rectangle { readonly property url themedIconUrl: iconName !== "" ? Quickshell.iconPath(iconName, false) : "" readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/notification.svg` - implicitWidth: 360 - implicitHeight: content.implicitHeight + Appearance.padding * 2 - radius: Appearance.radius - color: Appearance.colors.background - border.width: critical ? 1 : 0 - border.color: Appearance.colors.accent + implicitWidth: Theme.notification.width + implicitHeight: content.implicitHeight + Theme.padding * 2 + radius: Theme.radius + color: Theme.colors.background + border.width: critical ? Theme.notification.borderWidth : 0 + border.color: Theme.colors.accent Timer { id: countdown @@ -52,12 +52,12 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding - spacing: Appearance.spacing + anchors.margins: Theme.padding + spacing: Theme.spacing Item { - width: 40 - height: 40 + width: Theme.notification.iconSize + height: Theme.notification.iconSize Layout.alignment: Qt.AlignTop Image { @@ -77,23 +77,23 @@ Rectangle { } ColumnLayout { - spacing: 2 + spacing: Theme.notification.textSpacing Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter Text { text: root.notif?.appName ?? "" - color: Appearance.colors.accent - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize - 2 + color: Theme.colors.accent + font.family: Theme.font.family + font.pointSize: Theme.font.caption elide: Text.ElideRight Layout.fillWidth: true } Text { text: root.notif?.summary ?? "" - color: Appearance.colors.foreground - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize + color: Theme.colors.foreground + font.family: Theme.font.family + font.pointSize: Theme.font.pointSize font.weight: Font.DemiBold elide: Text.ElideRight Layout.fillWidth: true @@ -101,10 +101,10 @@ Rectangle { Text { text: root.notif?.body ?? "" visible: text !== "" - color: Appearance.colors.foreground - opacity: 0.8 - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize - 1 + color: Theme.colors.foreground + opacity: Theme.notification.bodyOpacity + font.family: Theme.font.family + font.pointSize: Theme.font.body wrapMode: Text.Wrap maximumLineCount: 3 elide: Text.ElideRight diff --git a/ui/BarButton.qml b/ui/BarButton.qml index e8742b9..b1f5b22 100644 --- a/ui/BarButton.qml +++ b/ui/BarButton.qml @@ -13,14 +13,14 @@ Button { property bool hoverHighlight: true property bool pointerCursor: true - padding: Appearance.padding + padding: Theme.padding display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon text: label - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize - palette.buttonText: Appearance.colors.foreground - icon.color: Appearance.colors.foreground + font.family: Theme.font.family + font.pointSize: Theme.font.pointSize + palette.buttonText: Theme.colors.foreground + icon.color: Theme.colors.foreground icon.source: iconSource HoverHandler { @@ -30,16 +30,16 @@ Button { background: Rectangle { anchors.fill: parent - radius: Appearance.radius + radius: Theme.radius color: { if (!root.hoverHighlight) - return Appearance.colors.inActive; - return root.hovered ? Appearance.colors.inActive : Appearance.colors.background; + return Theme.colors.inActive; + return root.hovered ? Theme.colors.inActive : Theme.colors.background; } Behavior on color { ColorAnimation { - duration: Appearance.duration + duration: Theme.duration } } } diff --git a/ui/RoundedCorner.qml b/ui/RoundedCorner.qml index 4075ecd..4079535 100644 --- a/ui/RoundedCorner.qml +++ b/ui/RoundedCorner.qml @@ -9,7 +9,7 @@ WrapperItem { id: root property int corner - property real radius: Appearance.radius + property real radius: Theme.radius property color color Component.onCompleted: { diff --git a/windows/Bar.qml b/windows/Bar.qml index e5e751f..47319c1 100644 --- a/windows/Bar.qml +++ b/windows/Bar.qml @@ -10,7 +10,7 @@ import qs.ui PanelWindow { id: window - property color barColor: Appearance.colors.background + property color barColor: Theme.colors.background color: "transparent" exclusionMode: ExclusionMode.Ignore @@ -41,7 +41,7 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2 - spacing: Appearance.spacing + spacing: Theme.spacing Repeater { model: section.widgets @@ -84,7 +84,7 @@ PanelWindow { Rectangle { id: leftBar - implicitWidth: 10 + implicitWidth: Theme.bar.thickness implicitHeight: QsWindow.window?.height ?? 0 color: window.barColor anchors.left: parent.left @@ -92,7 +92,7 @@ PanelWindow { Rectangle { id: rightBar - implicitWidth: 10 + implicitWidth: Theme.bar.thickness implicitHeight: QsWindow.window?.height ?? 0 color: window.barColor anchors.right: parent.right @@ -101,7 +101,7 @@ PanelWindow { id: bottomBar implicitWidth: QsWindow.window?.width ?? 0 - implicitHeight: 10 + implicitHeight: Theme.bar.thickness color: window.barColor anchors.bottom: parent.bottom } @@ -109,7 +109,7 @@ PanelWindow { id: topBar implicitWidth: QsWindow.window?.width ?? 0 - implicitHeight: 50 + implicitHeight: Theme.bar.height color: window.barColor anchors.top: parent.top @@ -117,7 +117,7 @@ PanelWindow { id: flexLayout anchors.fill: parent - anchors.margins: Appearance.margin + anchors.margins: Theme.margin wrap: FlexboxLayout.Wrap direction: FlexboxLayout.Row diff --git a/windows/Notification.qml b/windows/Notification.qml index 924f2a9..e6a30a4 100644 --- a/windows/Notification.qml +++ b/windows/Notification.qml @@ -10,12 +10,10 @@ import "../modules/osd" PanelWindow { id: root - readonly property int cardWidth: 420 - color: "transparent" exclusionMode: ExclusionMode.Ignore visible: Notifications.popupCount > 0 - implicitWidth: root.cardWidth + implicitWidth: Theme.notification.width implicitHeight: stack.implicitHeight anchors { @@ -23,8 +21,8 @@ PanelWindow { right: true } margins { - top: 50 + Appearance.margin - right: 10 + Appearance.margin + top: Theme.bar.height + Theme.margin + right: Theme.bar.thickness + Theme.margin } mask: Region { @@ -37,7 +35,7 @@ PanelWindow { anchors.top: parent.top anchors.right: parent.right anchors.left: parent.left - spacing: Appearance.spacing + spacing: Theme.spacing Repeater { model: [...Notifications.popups.values].reverse()