Files
dotfiles/quickshell/components/NotifCard.qml
T
nvrlandClaude Opus 5.5 a8c19961e2 theme: soften the YoRHa shapes; SUPER+D opens the dashboard
The square markers, block meters and hard-cornered fills read as boxy
next to the porthole. The desktop is now soft throughout: the porthole
is the shape everything is built around and every smaller surface echoes
it.

- Theme gains radiusS / radiusM / radiusL. radiusL is frameRadius - padS,
  so anything inset padS inside one of the frame's curves is concentric
  with it.
- P5Panel and Skew draw rounded rectangles (Geom.roundedRect) whenever
  they neither lean nor are chopped, which is always now; the polygon
  path is kept for shapes brought back on purpose.
- Markers are dots; workspace pips, toggles, transport and media chips,
  notification actions and today's date are pills or discs; the slider
  handle is round. Meters are one continuous rounded line that glides to
  its level instead of a row of segments.
- Album art is rounded with ClippingRectangle in the bar, the media
  popout and the dashboard.
- Free-standing panels (launcher, ask) drop the corner brackets and take
  the frame radius.
- Fix: a hovered notification's wash was a full-bleed square, so in the
  drawer's bottom corner it ran past the drawer's curve. It is inset by
  padS/2 and rounded by radiusL, which keeps it inside the curve.

Hyprland: the discord special workspace and its window rule are gone,
SUPER+S stays the only scratchpad, and SUPER+D now opens the dashboard
(it was on SUPER+I).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:56:43 +02:00

276 lines
9.3 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// One notification, used both in the history list and in the toast drawer.
//
// Laid out like an entry in NieR's message log rather than as a card: a dot
// and the sender's name running into a hairline, the summary, the body, and
// pill buttons for any actions. There is no panel of its own — both places it
// appears are already hull surfaces — so rest is bare, hover lifts the entry
// onto a soft rounded wash, and entries are divided by the hairline in their
// own header.
//
// Urgency is the dot: the light for normal, grey for low, red for critical.
// A critical entry also takes the red into its sender line, and nothing else.
Item {
id: root
required property var notif
property bool showActions: true
readonly property bool critical: root.notif?.urgency === NotificationUrgency.Critical
readonly property color tone: root.notif
? Notifs.urgencyColor(root.notif.urgency)
: Theme.primary
// Kept for callers that size things to the card's edges; there is no lean
// or chop under the YoRHa theme.
readonly property real lean: 0
readonly property real chop: 0
readonly property bool hovered: hover.hovered
signal dismissed()
implicitHeight: layout.implicitHeight + Theme.padM * 2
// Hover wash. Inset by padS and rounded by radiusL, which is the frame's
// radius less that inset: an entry in the bottom corner of the drawer gets
// a corner concentric with the drawer's own, instead of a square one that
// ran out past the drawer's curve.
Rectangle {
anchors.fill: parent
anchors.margins: Theme.padS / 2
radius: Theme.radiusL
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.padM
spacing: 4
// Sender: marker, glyph, name, then the hairline to the edge.
Item {
width: parent.width
height: 16
Rectangle {
id: marker
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: root.tone
}
Icon {
id: glyph
visible: text !== ""
anchors.left: marker.right
anchors.leftMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: Notifs.appIconFor(root.notif)
color: root.critical ? Theme.alarm : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
id: sender
anchors.left: glyph.visible ? glyph.right : marker.right
anchors.leftMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: (root.notif?.appName || "System").toUpperCase()
color: root.critical ? Theme.alarm : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sender.right
anchors.leftMargin: Theme.padS
anchors.right: close.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
// Close affordance: always there, quiet until the row is hovered.
Icon {
id: close
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "󰅖"
opacity: root.hovered ? 1 : 0.0
color: closeHover.hovered ? Theme.accent : Theme.muted
font.pixelSize: Theme.fsBody
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
HoverHandler {
id: closeHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.dismissed()
}
}
}
Text {
width: parent.width
leftPadding: Theme.marker + Theme.padS
text: root.notif?.summary || ""
color: root.hovered ? Theme.accent : Theme.text
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Text {
width: parent.width
leftPadding: Theme.marker + Theme.padS
visible: text !== ""
text: root.notif?.body || ""
color: Theme.subtext
elide: Text.ElideRight
maximumLineCount: 4
wrapMode: Text.WordWrap
textFormat: Text.StyledText
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
lineHeight: 1.1
renderType: Text.NativeRendering
}
// Inline image, when the sender supplied one, rounded in a hairline frame.
ClippingRectangle {
x: Theme.marker + Theme.padS
visible: inline.source != "" && inline.status === Image.Ready
width: inline.width + 2
height: inline.height + 2
radius: Theme.radiusM
color: "transparent"
border.width: 1
border.color: Theme.overlay
Image {
id: inline
x: 1
y: 1
source: root.notif?.image || ""
sourceSize.height: 90
fillMode: Image.PreserveAspectFit
asynchronous: true
}
}
Row {
x: Theme.marker + Theme.padS
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
spacing: Theme.padS
topPadding: 4
Repeater {
model: root.notif?.actions ?? []
// A hairline pill. Hover lifts and lights it; it never inverts.
Item {
id: actionChip
required property var modelData
implicitWidth: chipRow.implicitWidth + Theme.padM * 2
implicitHeight: 24
Rectangle {
anchors.fill: parent
radius: height / 2
color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
border.width: 1
border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Row {
id: chipRow
anchors.centerIn: parent
spacing: Theme.padS
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 4
height: 4
radius: 2
color: actionLabel.color
opacity: actionHover.hovered ? 1 : 0.4
}
Text {
id: actionLabel
anchors.verticalCenter: parent.verticalCenter
text: actionChip.modelData.text.toUpperCase()
color: actionHover.hovered ? Theme.accent : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.letterSpacing: Theme.tracking
renderType: Text.NativeRendering
}
}
HoverHandler {
id: actionHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
actionChip.modelData.invoke();
root.dismissed();
}
}
}
}
}
}
HoverHandler {
id: hover
}
// Middle click dismisses, matching the old dunst muscle memory.
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.dismissed()
}
}